@nexttylabs/echo 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changeset/README.md +21 -0
- package/.changeset/config.json +11 -0
- package/.changeset/cozy-ghosts-care.md +5 -0
- package/.changeset/sharp-lines-stand.md +5 -0
- package/.changeset/sour-doodles-eat.md +5 -0
- package/.changeset/tender-moose-shop.md +5 -0
- package/.github/pull_request_template.md +13 -0
- package/.github/workflows/ci.yml +41 -0
- package/.github/workflows/publish.yml +44 -0
- package/.github/workflows/release.yml +73 -0
- package/AGENTS.md +92 -0
- package/CHANGELOG.md +13 -0
- package/Dockerfile +57 -0
- package/LICENSE +661 -0
- package/Makefile +77 -0
- package/README.md +198 -0
- package/app/(auth)/login/page.tsx +53 -0
- package/app/(auth)/register/page.tsx +48 -0
- package/app/(auth)/sign-in/page.tsx +22 -0
- package/app/(dashboard)/admin/feedback/[id]/edit/page.tsx +103 -0
- package/app/(dashboard)/admin/feedback/[id]/page.tsx +154 -0
- package/app/(dashboard)/admin/feedback/new/page.tsx +91 -0
- package/app/(dashboard)/admin/feedback/page.tsx +81 -0
- package/app/(dashboard)/admin/layout.tsx +48 -0
- package/app/(dashboard)/analytics/portal/page.tsx +30 -0
- package/app/(dashboard)/dashboard/page.tsx +133 -0
- package/app/(dashboard)/layout.tsx +69 -0
- package/app/(dashboard)/no-access/page.tsx +45 -0
- package/app/(dashboard)/settings/access/page.tsx +56 -0
- package/app/(dashboard)/settings/api-keys/page.tsx +55 -0
- package/app/(dashboard)/settings/appearance/page.tsx +40 -0
- package/app/(dashboard)/settings/branding/page.tsx +62 -0
- package/app/(dashboard)/settings/changelog/page.tsx +51 -0
- package/app/(dashboard)/settings/danger-zone/page.tsx +92 -0
- package/app/(dashboard)/settings/feedback/page.tsx +63 -0
- package/app/(dashboard)/settings/integrations/page.tsx +94 -0
- package/app/(dashboard)/settings/layout.tsx +43 -0
- package/app/(dashboard)/settings/modules/page.tsx +54 -0
- package/app/(dashboard)/settings/notifications/page.tsx +48 -0
- package/app/(dashboard)/settings/organization/page.tsx +104 -0
- package/app/(dashboard)/settings/organization/portal/access/page.tsx +22 -0
- package/app/(dashboard)/settings/organization/portal/experience/page.tsx +22 -0
- package/app/(dashboard)/settings/organization/portal/growth/page.tsx +22 -0
- package/app/(dashboard)/settings/organization/portal/layout.tsx +24 -0
- package/app/(dashboard)/settings/organization/portal/page.tsx +22 -0
- package/app/(dashboard)/settings/organizations/[orgId]/members/page.tsx +69 -0
- package/app/(dashboard)/settings/organizations/new/page.tsx +36 -0
- package/app/(dashboard)/settings/page.tsx +22 -0
- package/app/(dashboard)/settings/portal-access/page.tsx +53 -0
- package/app/(dashboard)/settings/portal-branding/page.tsx +59 -0
- package/app/(dashboard)/settings/portal-growth/page.tsx +57 -0
- package/app/(dashboard)/settings/portal-modules/page.tsx +49 -0
- package/app/(dashboard)/settings/portal-resources/page.tsx +66 -0
- package/app/(dashboard)/settings/profile/page.tsx +48 -0
- package/app/(dashboard)/settings/widgets/page.tsx +63 -0
- package/app/(public)/[organizationSlug]/changelog/page.tsx +109 -0
- package/app/(public)/[organizationSlug]/feedback/[id]/page.tsx +146 -0
- package/app/(public)/[organizationSlug]/page.tsx +160 -0
- package/app/(public)/[organizationSlug]/roadmap/page.tsx +142 -0
- package/app/(public)/docs/page.tsx +48 -0
- package/app/(public)/feedback/[id]/not-found.tsx +33 -0
- package/app/(public)/feedback/[id]/page.tsx +102 -0
- package/app/(public)/invite/[token]/page.tsx +121 -0
- package/app/(public)/page.tsx +22 -0
- package/app/(public)/widget/[organizationId]/page.tsx +122 -0
- package/app/api/_utils.ts +29 -0
- package/app/api/admin/backup/route.ts +72 -0
- package/app/api/api-keys/[keyId]/route.ts +92 -0
- package/app/api/api-keys/route.ts +116 -0
- package/app/api/auth/[...all]/route.ts +21 -0
- package/app/api/auth/clear-session/route.ts +43 -0
- package/app/api/auth/register/handler.ts +176 -0
- package/app/api/auth/register/route.ts +26 -0
- package/app/api/docs/route.ts +28 -0
- package/app/api/feedback/[id]/comments/[commentId]/route.ts +105 -0
- package/app/api/feedback/[id]/comments/route.ts +421 -0
- package/app/api/feedback/[id]/duplicates/route.ts +285 -0
- package/app/api/feedback/[id]/handler.ts +91 -0
- package/app/api/feedback/[id]/processing-status/route.ts +199 -0
- package/app/api/feedback/[id]/reclassify/route.ts +145 -0
- package/app/api/feedback/[id]/route.ts +511 -0
- package/app/api/feedback/[id]/suggest-tags/route.ts +227 -0
- package/app/api/feedback/[id]/sync-github/route.ts +52 -0
- package/app/api/feedback/[id]/vote/route.ts +431 -0
- package/app/api/feedback/bulk/route.ts +212 -0
- package/app/api/feedback/handler.ts +138 -0
- package/app/api/feedback/route.ts +298 -0
- package/app/api/feedback/similar/route.ts +100 -0
- package/app/api/health/route.test.ts +64 -0
- package/app/api/health/route.ts +92 -0
- package/app/api/identify/jwt/route.ts +29 -0
- package/app/api/integrations/github/route.ts +196 -0
- package/app/api/internal/domain-lookup/route.ts +67 -0
- package/app/api/invitations/accept/handler.ts +101 -0
- package/app/api/invitations/accept/route.ts +29 -0
- package/app/api/notifications/preferences/route.ts +109 -0
- package/app/api/organizations/[orgId]/handler.ts +123 -0
- package/app/api/organizations/[orgId]/invitations/handler.ts +121 -0
- package/app/api/organizations/[orgId]/invitations/route.ts +29 -0
- package/app/api/organizations/[orgId]/members/[memberId]/handler.ts +208 -0
- package/app/api/organizations/[orgId]/members/[memberId]/route.ts +30 -0
- package/app/api/organizations/[orgId]/members/handler.ts +77 -0
- package/app/api/organizations/[orgId]/members/route.ts +29 -0
- package/app/api/organizations/[orgId]/route.ts +30 -0
- package/app/api/organizations/handler.ts +97 -0
- package/app/api/organizations/route.ts +29 -0
- package/app/api/tags/sync/route.ts +88 -0
- package/app/api/upload/handler.ts +79 -0
- package/app/api/upload/route.ts +37 -0
- package/app/api/v1/feedback/[id]/route.ts +276 -0
- package/app/api/v1/feedback/route.ts +250 -0
- package/app/api/v1/spec/route.ts +356 -0
- package/app/api/webhooks/[webhookId]/route.ts +213 -0
- package/app/api/webhooks/github/route.ts +158 -0
- package/app/api/webhooks/route.ts +143 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +139 -0
- package/app/health/route.ts +108 -0
- package/app/layout.tsx +60 -0
- package/bun.lock +2503 -0
- package/components/api/rate-limit-info.tsx +86 -0
- package/components/api-keys/api-key-manager.tsx +262 -0
- package/components/auth/login-form.tsx +207 -0
- package/components/auth/register-form.tsx +230 -0
- package/components/comment/comment-form.tsx +111 -0
- package/components/comment/internal-notes.tsx +219 -0
- package/components/comment/public-comments.tsx +387 -0
- package/components/component-example-client-only.tsx +29 -0
- package/components/component-example.tsx +519 -0
- package/components/dashboard/index.ts +22 -0
- package/components/dashboard/organization-switcher.tsx +96 -0
- package/components/dashboard/quick-actions.tsx +57 -0
- package/components/dashboard/recent-feedback-list.tsx +152 -0
- package/components/dashboard/stats-cards.tsx +88 -0
- package/components/dashboard/status-chart.tsx +106 -0
- package/components/example.tsx +70 -0
- package/components/feedback/attachment-list.tsx +103 -0
- package/components/feedback/auto-classification-badge.tsx +92 -0
- package/components/feedback/classification-override.tsx +64 -0
- package/components/feedback/duplicate-suggestions-inline.tsx +158 -0
- package/components/feedback/duplicate-suggestions.tsx +188 -0
- package/components/feedback/embedded-feedback-form.tsx +439 -0
- package/components/feedback/feedback-actions.tsx +160 -0
- package/components/feedback/feedback-bulk-actions.tsx +184 -0
- package/components/feedback/feedback-detail-view.tsx +321 -0
- package/components/feedback/feedback-detail.tsx +305 -0
- package/components/feedback/feedback-edit-form.tsx +131 -0
- package/components/feedback/feedback-filters.tsx +222 -0
- package/components/feedback/feedback-list-controls.tsx +433 -0
- package/components/feedback/feedback-list-item.tsx +298 -0
- package/components/feedback/feedback-list-skeleton.tsx +49 -0
- package/components/feedback/feedback-list.tsx +523 -0
- package/components/feedback/feedback-sorter.tsx +117 -0
- package/components/feedback/feedback-stats.tsx +124 -0
- package/components/feedback/file-upload.tsx +289 -0
- package/components/feedback/processing-status.tsx +161 -0
- package/components/feedback/status-history.tsx +134 -0
- package/components/feedback/status-selector.tsx +153 -0
- package/components/feedback/submit-on-behalf-form.tsx +403 -0
- package/components/feedback/tag-suggestions.tsx +212 -0
- package/components/feedback/vote-button.tsx +113 -0
- package/components/feedback/vote-list.tsx +108 -0
- package/components/integrations/github-config.tsx +200 -0
- package/components/landing/hero.tsx +150 -0
- package/components/layout/dashboard-layout.tsx +59 -0
- package/components/layout/index.ts +20 -0
- package/components/layout/language-switcher.tsx +129 -0
- package/components/layout/mobile-sidebar.tsx +66 -0
- package/components/layout/sidebar.tsx +279 -0
- package/components/portal/changelog-entry.tsx +132 -0
- package/components/portal/changelog-list.tsx +85 -0
- package/components/portal/contributor-badge.tsx +29 -0
- package/components/portal/contributors-sidebar.tsx +98 -0
- package/components/portal/create-post-dialog.tsx +247 -0
- package/components/portal/feedback-board.tsx +205 -0
- package/components/portal/feedback-post-card.tsx +198 -0
- package/components/portal/help-center.tsx +169 -0
- package/components/portal/leaderboard.tsx +29 -0
- package/components/portal/portal-header.tsx +153 -0
- package/components/portal/portal-layout.tsx +62 -0
- package/components/portal/portal-modules-panel.tsx +118 -0
- package/components/portal/portal-nav.tsx +59 -0
- package/components/portal/portal-overview.tsx +174 -0
- package/components/portal/portal-settings-nav.tsx +62 -0
- package/components/portal/portal-settings-shell.tsx +71 -0
- package/components/portal/portal-shell.tsx +62 -0
- package/components/portal/portal-tab-nav.tsx +77 -0
- package/components/portal/project-switcher.tsx +20 -0
- package/components/portal/roadmap-board.tsx +82 -0
- package/components/portal/roadmap-card.tsx +76 -0
- package/components/portal/roadmap-column.tsx +78 -0
- package/components/portal/settings-forms/access-form.tsx +194 -0
- package/components/portal/settings-forms/copy-form.tsx +95 -0
- package/components/portal/settings-forms/index.ts +23 -0
- package/components/portal/settings-forms/languages-form.tsx +223 -0
- package/components/portal/settings-forms/seo-form.tsx +156 -0
- package/components/portal/settings-forms/sharing-form.tsx +155 -0
- package/components/portal/settings-forms/theme-form.tsx +104 -0
- package/components/settings/api-keys-list.tsx +167 -0
- package/components/settings/appearance-form.tsx +71 -0
- package/components/settings/index.ts +25 -0
- package/components/settings/invite-member-form.tsx +119 -0
- package/components/settings/notification-preferences.tsx +174 -0
- package/components/settings/organization-form.tsx +165 -0
- package/components/settings/organization-members-list.tsx +197 -0
- package/components/settings/profile-form.tsx +124 -0
- package/components/settings/role-selector.tsx +57 -0
- package/components/settings/settings-sidebar.tsx +115 -0
- package/components/shared/pagination.tsx +215 -0
- package/components/ui/alert-dialog.tsx +201 -0
- package/components/ui/alert.tsx +75 -0
- package/components/ui/avatar.tsx +126 -0
- package/components/ui/badge.tsx +62 -0
- package/components/ui/button.tsx +77 -0
- package/components/ui/card.tsx +111 -0
- package/components/ui/combobox.tsx +311 -0
- package/components/ui/dialog.tsx +158 -0
- package/components/ui/dropdown-menu.tsx +272 -0
- package/components/ui/field.tsx +256 -0
- package/components/ui/input-group.tsx +164 -0
- package/components/ui/input.tsx +36 -0
- package/components/ui/label.tsx +41 -0
- package/components/ui/pagination.tsx +142 -0
- package/components/ui/select.tsx +202 -0
- package/components/ui/separator.tsx +45 -0
- package/components/ui/sheet.tsx +151 -0
- package/components/ui/skeleton.tsx +32 -0
- package/components/ui/switch.tsx +49 -0
- package/components/ui/table.tsx +118 -0
- package/components/ui/tabs.tsx +107 -0
- package/components/ui/textarea.tsx +35 -0
- package/components/ui/tooltip.tsx +78 -0
- package/components/widget/widget-form.tsx +439 -0
- package/components.json +24 -0
- package/db/init/01-init.sql +13 -0
- package/docker-compose.dev.yml +26 -0
- package/docker-compose.yml +98 -0
- package/docs/architecture.md +259 -0
- package/docs/component-inventory.md +261 -0
- package/docs/database-migrations.md +76 -0
- package/docs/development-guide.md +209 -0
- package/docs/e2e-user-flows.csv +31 -0
- package/docs/er-diagram-feedback.mmd +138 -0
- package/docs/er-diagram.mmd +281 -0
- package/docs/i18n-check-report.md +296 -0
- package/docs/index.md +214 -0
- package/docs/logic-chain.md +94 -0
- package/docs/plans/2026-01-02-database-migration-scripts.md +496 -0
- package/docs/plans/2026-01-02-user-login-design.md +37 -0
- package/docs/plans/2026-01-02-user-login.md +437 -0
- package/docs/plans/2026-01-02-user-registration-design.md +47 -0
- package/docs/plans/2026-01-02-user-registration.md +628 -0
- package/docs/plans/2026-01-03-roles-permissions-design.md +20 -0
- package/docs/plans/2026-01-03-roles-permissions.md +266 -0
- package/docs/plans/2026-01-05-authentication-middleware.md +207 -0
- package/docs/plans/2026-01-05-member-removal.md +186 -0
- package/docs/plans/2026-01-05-organization-creation.md +374 -0
- package/docs/plans/2026-01-05-rbac-middleware.md +112 -0
- package/docs/plans/2026-01-05-role-configuration.md +441 -0
- package/docs/plans/2026-01-06-file-upload-support.md +804 -0
- package/docs/plans/2026-01-06-permission-check-hook.md +155 -0
- package/docs/plans/2026-01-06-resource-ownership-check.md +231 -0
- package/docs/plans/2026-01-07-feedback-tracking-link.md +459 -0
- package/docs/plans/2026-01-09-logout-redirect-design.md +52 -0
- package/docs/plans/2026-01-09-phase2-3-plan.md +654 -0
- package/docs/plans/2026-01-09-portal-execution-plan.md +408 -0
- package/docs/plans/2026-01-09-project-delete-feature-design.md +163 -0
- package/docs/plans/2026-01-09-project-delete-implementation.md +451 -0
- package/docs/plans/2026-01-09-project-edit-delete-design.md +52 -0
- package/docs/plans/2026-01-09-settings-center-design.md +114 -0
- package/docs/plans/2026-01-09-settings-center.md +948 -0
- package/docs/plans/2026-01-10-organization-only-design.md +66 -0
- package/docs/plans/2026-01-10-organization-only-implementation.md +433 -0
- package/docs/plans/2026-01-10-portal-settings-restructure-plan.md +18 -0
- package/docs/plans/2026-01-10-project-settings-tabs-design-implementation.md +296 -0
- package/docs/plans/2026-01-14-e2e-playwright-feedback.md +173 -0
- package/docs/plans/2026-01-15-feedback-management-org-context-design.md +82 -0
- package/docs/plans/2026-01-15-feedback-management-org-context-implementation-plan.md +521 -0
- package/docs/plans/2026-01-16-admin-feedback-filters-design.md +75 -0
- package/docs/plans/2026-01-16-admin-feedback-filters-implementation.md +293 -0
- package/docs/plans/2026-01-16-admin-feedback-route-consolidation.md +180 -0
- package/docs/plans/2026-01-16-e2e-test-fixes.md +158 -0
- package/docs/plans/2026-01-17-admin-feedback-filters.md +214 -0
- package/docs/plans/2026-01-17-admin-feedback-improvements.md +453 -0
- package/docs/plans/2026-01-18-changesets-design.md +40 -0
- package/docs/product_changes.md +37 -0
- package/docs/project-overview.md +159 -0
- package/docs/project-scan-report.json +104 -0
- package/docs/route-role-visibility.md +51 -0
- package/docs/source-tree-analysis.md +150 -0
- package/docs/testing/delete-project-manual-tests.md +18 -0
- package/docs/user-story-tracking.md +191 -0
- package/drizzle.config.ts +32 -0
- package/eslint.config.mjs +19 -0
- package/hooks/use-permissions.ts +56 -0
- package/i18n/config.ts +45 -0
- package/i18n/request.ts +28 -0
- package/i18n/resolve-locale.ts +38 -0
- package/lib/api/errors.ts +62 -0
- package/lib/auth/cli-config.ts +35 -0
- package/lib/auth/client.ts +20 -0
- package/lib/auth/config.ts +55 -0
- package/lib/auth/jwt-identity.ts +21 -0
- package/lib/auth/org-context.ts +71 -0
- package/lib/auth/organization.ts +107 -0
- package/lib/auth/permissions.ts +87 -0
- package/lib/auth/session.ts +23 -0
- package/lib/config/rate-limits.ts +64 -0
- package/lib/dashboard/get-dashboard-stats.ts +136 -0
- package/lib/db/index.ts +41 -0
- package/lib/db/migrate.test.ts +49 -0
- package/lib/db/migrate.ts +62 -0
- package/lib/db/migrations/.gitkeep +0 -0
- package/lib/db/migrations/0000_cynical_gladiator.sql +53 -0
- package/lib/db/migrations/0001_wandering_sunfire.sql +27 -0
- package/lib/db/migrations/0002_shallow_speedball.sql +1 -0
- package/lib/db/migrations/0003_add_org_description.sql +1 -0
- package/lib/db/migrations/0003_boring_wild_pack.sql +13 -0
- package/lib/db/migrations/0004_windy_tyrannus.sql +27 -0
- package/lib/db/migrations/0005_perpetual_doorman.sql +5 -0
- package/lib/db/migrations/0006_aberrant_captain_midlands.sql +13 -0
- package/lib/db/migrations/0007_clever_captain_cross.sql +14 -0
- package/lib/db/migrations/0008_sparkling_pandemic.sql +2 -0
- package/lib/db/migrations/0009_happy_black_tom.sql +29 -0
- package/lib/db/migrations/0010_kind_junta.sql +8 -0
- package/lib/db/migrations/0011_mute_squadron_supreme.sql +25 -0
- package/lib/db/migrations/0012_giant_power_man.sql +24 -0
- package/lib/db/migrations/0013_damp_titanium_man.sql +17 -0
- package/lib/db/migrations/0014_blue_alice.sql +18 -0
- package/lib/db/migrations/0015_webhook_tables.sql +41 -0
- package/lib/db/migrations/0016_github_integration.sql +30 -0
- package/lib/db/migrations/0016_overjoyed_ghost_rider.sql +22 -0
- package/lib/db/migrations/0017_slimy_inhumans.sql +6 -0
- package/lib/db/migrations/0018_same_spitfire.sql +1 -0
- package/lib/db/migrations/0019_jittery_loners.sql +16 -0
- package/lib/db/migrations/0019_remove_projects_add_org_settings.sql +14 -0
- package/lib/db/migrations/meta/0000_snapshot.json +374 -0
- package/lib/db/migrations/meta/0001_snapshot.json +553 -0
- package/lib/db/migrations/meta/0002_snapshot.json +560 -0
- package/lib/db/migrations/meta/0003_snapshot.json +650 -0
- package/lib/db/migrations/meta/0004_snapshot.json +852 -0
- package/lib/db/migrations/meta/0005_snapshot.json +900 -0
- package/lib/db/migrations/meta/0006_snapshot.json +1011 -0
- package/lib/db/migrations/meta/0007_snapshot.json +1125 -0
- package/lib/db/migrations/meta/0008_snapshot.json +1146 -0
- package/lib/db/migrations/meta/0009_snapshot.json +1386 -0
- package/lib/db/migrations/meta/0010_snapshot.json +1419 -0
- package/lib/db/migrations/meta/0011_snapshot.json +1615 -0
- package/lib/db/migrations/meta/0012_snapshot.json +1805 -0
- package/lib/db/migrations/meta/0013_snapshot.json +1948 -0
- package/lib/db/migrations/meta/0014_snapshot.json +2082 -0
- package/lib/db/migrations/meta/0015_snapshot.json +2476 -0
- package/lib/db/migrations/meta/0016_snapshot.json +2633 -0
- package/lib/db/migrations/meta/0017_snapshot.json +2680 -0
- package/lib/db/migrations/meta/0018_snapshot.json +2686 -0
- package/lib/db/migrations/meta/0019_snapshot.json +2741 -0
- package/lib/db/migrations/meta/_journal.json +146 -0
- package/lib/db/schema/ai-processing.ts +90 -0
- package/lib/db/schema/api-keys.ts +61 -0
- package/lib/db/schema/attachments.ts +48 -0
- package/lib/db/schema/auth.ts +111 -0
- package/lib/db/schema/comments.ts +74 -0
- package/lib/db/schema/duplicates.ts +80 -0
- package/lib/db/schema/feedback.ts +88 -0
- package/lib/db/schema/github-integrations.ts +66 -0
- package/lib/db/schema/index.ts +35 -0
- package/lib/db/schema/invitations.ts +32 -0
- package/lib/db/schema/notifications.ts +85 -0
- package/lib/db/schema/organization-members.ts +37 -0
- package/lib/db/schema/organization-settings.ts +134 -0
- package/lib/db/schema/organizations.ts +30 -0
- package/lib/db/schema/projects.ts +145 -0
- package/lib/db/schema/status-history.ts +63 -0
- package/lib/db/schema/tags.ts +194 -0
- package/lib/db/schema/user-profiles.ts +31 -0
- package/lib/db/schema/votes.ts +60 -0
- package/lib/db/schema/webhooks.ts +106 -0
- package/lib/feedback/filters.ts +28 -0
- package/lib/feedback/find-similar.ts +49 -0
- package/lib/feedback/get-feedback-by-id.ts +159 -0
- package/lib/feedback/prefill.ts +51 -0
- package/lib/http/get-request-url.ts +28 -0
- package/lib/integrations/github.ts +159 -0
- package/lib/invitations.ts +22 -0
- package/lib/logger.test.ts +31 -0
- package/lib/logger.ts +58 -0
- package/lib/middleware/api-key.ts +126 -0
- package/lib/middleware/rate-limit-keys.ts +47 -0
- package/lib/middleware/rate-limit.ts +148 -0
- package/lib/middleware/rbac.ts +39 -0
- package/lib/middleware/request-id.test.ts +28 -0
- package/lib/middleware/request-id.ts +30 -0
- package/lib/middleware/request-logger.test.ts +36 -0
- package/lib/middleware/request-logger.ts +41 -0
- package/lib/middleware/with-rate-limit.ts +33 -0
- package/lib/portal/analytics.ts +20 -0
- package/lib/portal/contributors.ts +27 -0
- package/lib/portal/i18n.ts +20 -0
- package/lib/portal/leaderboard-settings.ts +20 -0
- package/lib/portal/modules.ts +20 -0
- package/lib/portal/portal-copy.ts +20 -0
- package/lib/portal/public-context.tsx +110 -0
- package/lib/portal/seo.ts +20 -0
- package/lib/portal/settings-context.ts +56 -0
- package/lib/portal/sharing.ts +20 -0
- package/lib/portal/sorting.ts +20 -0
- package/lib/portal/theme.ts +20 -0
- package/lib/services/ai/classifier.ts +296 -0
- package/lib/services/ai/duplicate-detector.ts +255 -0
- package/lib/services/ai/tag-suggester.ts +108 -0
- package/lib/services/api-keys.ts +164 -0
- package/lib/services/backup.ts +173 -0
- package/lib/services/email/templates.ts +158 -0
- package/lib/services/email.ts +68 -0
- package/lib/services/github-sync.ts +205 -0
- package/lib/services/notifications/index.ts +224 -0
- package/lib/services/portal-settings.ts +157 -0
- package/lib/swagger/config.ts +296 -0
- package/lib/swagger/generate.ts +400 -0
- package/lib/upload/file-validator.ts +52 -0
- package/lib/upload/storage.ts +59 -0
- package/lib/utils/format.ts +26 -0
- package/lib/utils/slug.ts +28 -0
- package/lib/utils.ts +23 -0
- package/lib/validations/auth.ts +56 -0
- package/lib/validations/comment.ts +44 -0
- package/lib/validations/feedback.ts +51 -0
- package/lib/validations/invitations.ts +23 -0
- package/lib/validations/organizations.ts +34 -0
- package/lib/validations/projects.ts +49 -0
- package/lib/validators/feedback.ts +57 -0
- package/lib/validators/index.ts +18 -0
- package/lib/webhooks/events.ts +73 -0
- package/lib/webhooks/index.ts +21 -0
- package/lib/webhooks/retry.ts +188 -0
- package/lib/webhooks/sender.ts +183 -0
- package/lib/webhooks/verify.ts +37 -0
- package/lib/workers/feedback-processor.ts +255 -0
- package/messages/en.json +965 -0
- package/messages/jp.json +862 -0
- package/messages/zh-CN.json +855 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +66 -0
- package/package.json +84 -0
- package/playwright.config.ts +44 -0
- package/postcss.config.mjs +7 -0
- package/proxy.test.ts +131 -0
- package/proxy.ts +190 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/logo-64.svg +5 -0
- package/public/logo.svg +5 -0
- package/public/next.svg +1 -0
- package/public/openapi.json +673 -0
- package/public/uploads/.gitkeep +0 -0
- package/public/uploads/02695701-ded0-4c81-8a21-9326c1d65448.pdf +1 -0
- package/public/uploads/178843ea-2780-48ef-8988-f4cba442e4cb.pdf +1 -0
- package/public/uploads/24b0a9ef-da93-49da-934f-637f89c7871d.pdf +1 -0
- package/public/uploads/7a11626d-a8e4-4b91-a8eb-20b6213b0a5a.pdf +1 -0
- package/public/uploads/b0703f4d-6e7b-4aab-8191-1a7b15f1b8ee.pdf +1 -0
- package/public/uploads/c8de0aed-4d3a-44aa-83bb-6594b7a2ddb3.pdf +1 -0
- package/public/uploads/e4cce295-0d85-4525-a1b0-a61c45722e26.pdf +1 -0
- package/public/uploads/eb4df45e-563c-48b8-9c68-c18212312426.pdf +1 -0
- package/public/vercel.svg +1 -0
- package/public/widget/embed.js +249 -0
- package/public/window.svg +1 -0
- package/scripts/backup-db.sh +57 -0
- package/scripts/backup-db.ts +24 -0
- package/scripts/generate-openapi.ts +22 -0
- package/scripts/migration-helper.ts +39 -0
- package/scripts/pre-deploy.ts +75 -0
- package/scripts/restore-db.sh +60 -0
- package/scripts/rollback.ts +72 -0
- package/scripts/seed-tags.ts +48 -0
- package/tests/api/feedback-bulk.test.ts +47 -0
- package/tests/api/feedback-by-id.test.ts +67 -0
- package/tests/api/feedback-comments-route-import.test.ts +26 -0
- package/tests/api/feedback-create.test.ts +71 -0
- package/tests/api/feedback-delete.test.ts +160 -0
- package/tests/api/feedback-filter.test.ts +250 -0
- package/tests/api/feedback-list.test.ts +234 -0
- package/tests/api/feedback-route-assignee-condition.test.ts +32 -0
- package/tests/api/feedback-similar.test.ts +46 -0
- package/tests/api/feedback-sort.test.ts +261 -0
- package/tests/api/feedback-status-enum.test.ts +49 -0
- package/tests/api/feedback-status-filter.test.ts +117 -0
- package/tests/api/feedback-submit-on-behalf.test.ts +269 -0
- package/tests/api/feedback.test.ts +175 -0
- package/tests/api/identify-jwt.test.ts +25 -0
- package/tests/api/invitation-accept.test.ts +213 -0
- package/tests/api/organization-invitations.test.ts +186 -0
- package/tests/api/organization-members-list.test.ts +79 -0
- package/tests/api/organization-members.test.ts +340 -0
- package/tests/api/organizations.test.ts +149 -0
- package/tests/api/register.test.ts +112 -0
- package/tests/api/upload.test.ts +103 -0
- package/tests/api/vote.test.ts +82 -0
- package/tests/app/admin-feedback-detail-page.test.tsx +25 -0
- package/tests/app/admin-feedback-list-page.test.tsx +25 -0
- package/tests/app/admin-feedback-new-page.test.tsx +25 -0
- package/tests/app/health-route-helpers.test.ts +27 -0
- package/tests/app/login-page.test.ts +26 -0
- package/tests/app/portal-page.test.ts +29 -0
- package/tests/app/project-portal-overview.test.tsx +25 -0
- package/tests/app/widget-page-import.test.ts +25 -0
- package/tests/components/create-post-dialog-defaults.test.ts +43 -0
- package/tests/components/feedback/duplicate-suggestions-inline.test.tsx +27 -0
- package/tests/components/feedback/embedded-feedback-form.test.tsx +96 -0
- package/tests/components/feedback/feedback-detail.test.tsx +25 -0
- package/tests/components/feedback/feedback-stats.test.tsx +49 -0
- package/tests/components/feedback-bulk-actions.test.tsx +39 -0
- package/tests/components/feedback-i18n-keys.test.ts +70 -0
- package/tests/components/feedback-list-controls-compile.test.ts +25 -0
- package/tests/components/feedback-list-controls.test.tsx +204 -0
- package/tests/components/feedback-list-item.test.tsx +67 -0
- package/tests/components/landing/hero.test.tsx +46 -0
- package/tests/components/layout/language-switcher.test.tsx +25 -0
- package/tests/components/layout/sidebar.test.tsx +157 -0
- package/tests/components/login-form.test.ts +25 -0
- package/tests/components/organization-form.test.ts +32 -0
- package/tests/components/organization-switcher.test.ts +25 -0
- package/tests/components/pagination.test.tsx +43 -0
- package/tests/components/portal-overview.test.tsx +25 -0
- package/tests/components/profile-form.test.tsx +139 -0
- package/tests/components/role-selector.test.ts +31 -0
- package/tests/components/status-chart.test.tsx +90 -0
- package/tests/e2e/auth.e2e.ts +323 -0
- package/tests/e2e/feedback-actions.e2e.ts +471 -0
- package/tests/e2e/feedback-attachment.e2e.ts +168 -0
- package/tests/e2e/feedback-customer.e2e.ts +226 -0
- package/tests/e2e/feedback-management.e2e.ts +565 -0
- package/tests/e2e/feedback-submit.e2e.ts +133 -0
- package/tests/e2e/feedback-view.e2e.ts +297 -0
- package/tests/e2e/fixtures/test-data.ts +235 -0
- package/tests/e2e/health-check.e2e.ts +230 -0
- package/tests/e2e/helpers/test-utils-helpers.test.ts +43 -0
- package/tests/e2e/helpers/test-utils.ts +298 -0
- package/tests/e2e/integration-placeholders.e2e.ts +199 -0
- package/tests/e2e/organization.e2e.ts +292 -0
- package/tests/e2e/permissions.e2e.ts +424 -0
- package/tests/e2e/project-widget.e2e.ts +63 -0
- package/tests/feedback/filters.test.ts +29 -0
- package/tests/hooks/use-permissions.test.ts +52 -0
- package/tests/lib/ai/classifier.test.ts +104 -0
- package/tests/lib/ai/duplicate-detector.test.ts +234 -0
- package/tests/lib/attachments-schema.test.ts +30 -0
- package/tests/lib/auth/session.test.ts +49 -0
- package/tests/lib/auth-client.test.ts +37 -0
- package/tests/lib/auth-config.test.ts +26 -0
- package/tests/lib/feedback-prefill.test.ts +52 -0
- package/tests/lib/feedback-processor.test.ts +41 -0
- package/tests/lib/feedback-schema.test.ts +33 -0
- package/tests/lib/file-validator.test.ts +48 -0
- package/tests/lib/get-feedback-by-id.test.ts +37 -0
- package/tests/lib/invitations.test.ts +35 -0
- package/tests/lib/login-schema.test.ts +36 -0
- package/tests/lib/org-context.test.ts +95 -0
- package/tests/lib/organization-access.test.ts +44 -0
- package/tests/lib/organization-member-role-schema.test.ts +41 -0
- package/tests/lib/permissions.test.ts +88 -0
- package/tests/lib/portal-analytics.test.ts +25 -0
- package/tests/lib/portal-contributors.test.ts +25 -0
- package/tests/lib/portal-copy.test.ts +27 -0
- package/tests/lib/portal-i18n.test.ts +30 -0
- package/tests/lib/portal-leaderboard-settings.test.ts +25 -0
- package/tests/lib/portal-modules.test.ts +25 -0
- package/tests/lib/portal-seo.test.ts +25 -0
- package/tests/lib/portal-sharing.test.ts +25 -0
- package/tests/lib/portal-sorting.test.ts +25 -0
- package/tests/lib/portal-theme.test.ts +25 -0
- package/tests/lib/rate-limit.test.ts +142 -0
- package/tests/lib/resolve-locale.test.ts +34 -0
- package/tests/lib/services/backup.test.ts +145 -0
- package/tests/lib/user-organizations.test.ts +42 -0
- package/tests/lib/user-role-schema.test.ts +33 -0
- package/tests/lib/user-schema.test.ts +25 -0
- package/tests/setup.ts +74 -0
- package/tsconfig.json +34 -0
- package/types/bun-test.d.ts +31 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
var ECHO_WIDGET_VERSION = "1.0.0";
|
|
5
|
+
|
|
6
|
+
function getConfigFromScript(script) {
|
|
7
|
+
return {
|
|
8
|
+
organizationId: script.getAttribute("data-organization-id"),
|
|
9
|
+
projectId: script.getAttribute("data-project-id"),
|
|
10
|
+
theme: script.getAttribute("data-theme") || "auto",
|
|
11
|
+
primaryColor: script.getAttribute("data-primary-color") || "#3b82f6",
|
|
12
|
+
buttonText: script.getAttribute("data-button-text") || "反馈",
|
|
13
|
+
buttonPosition: script.getAttribute("data-button-position") || "bottom-right",
|
|
14
|
+
autoLoad: script.getAttribute("data-auto-load") !== "false",
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createWidgetIframe(config, baseUrl) {
|
|
19
|
+
var iframe = document.createElement("iframe");
|
|
20
|
+
iframe.id = "echo-widget-iframe";
|
|
21
|
+
|
|
22
|
+
var params = new URLSearchParams({
|
|
23
|
+
org: config.organizationId,
|
|
24
|
+
project: config.projectId,
|
|
25
|
+
theme: config.theme,
|
|
26
|
+
primaryColor: config.primaryColor,
|
|
27
|
+
buttonText: encodeURIComponent(config.buttonText),
|
|
28
|
+
position: config.buttonPosition,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
iframe.src =
|
|
32
|
+
baseUrl +
|
|
33
|
+
"/widget/" +
|
|
34
|
+
config.organizationId +
|
|
35
|
+
"/" +
|
|
36
|
+
config.projectId +
|
|
37
|
+
"?" +
|
|
38
|
+
params.toString();
|
|
39
|
+
|
|
40
|
+
iframe.style.cssText =
|
|
41
|
+
"border: none; width: 100%; height: 100%; pointer-events: auto;";
|
|
42
|
+
|
|
43
|
+
return iframe;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createFloatingButton(config, onClick) {
|
|
47
|
+
var button = document.createElement("button");
|
|
48
|
+
button.id = "echo-widget-button";
|
|
49
|
+
button.className = "echo-widget-button";
|
|
50
|
+
button.textContent = decodeURIComponent(config.buttonText);
|
|
51
|
+
button.setAttribute("aria-label", "Open feedback form");
|
|
52
|
+
|
|
53
|
+
var positionStyles = {
|
|
54
|
+
"bottom-right": "bottom: 20px; right: 20px;",
|
|
55
|
+
"bottom-left": "bottom: 20px; left: 20px;",
|
|
56
|
+
"top-right": "top: 20px; right: 20px;",
|
|
57
|
+
"top-left": "top: 20px; left: 20px;",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
button.style.cssText =
|
|
61
|
+
(positionStyles[config.buttonPosition] || positionStyles["bottom-right"]) +
|
|
62
|
+
"position: fixed;" +
|
|
63
|
+
"background-color: " +
|
|
64
|
+
config.primaryColor +
|
|
65
|
+
";" +
|
|
66
|
+
"color: white;" +
|
|
67
|
+
"border: none;" +
|
|
68
|
+
"border-radius: 8px;" +
|
|
69
|
+
"padding: 12px 24px;" +
|
|
70
|
+
"font-size: 14px;" +
|
|
71
|
+
"font-weight: 500;" +
|
|
72
|
+
"cursor: pointer;" +
|
|
73
|
+
"box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);" +
|
|
74
|
+
"transition: transform 0.2s, box-shadow 0.2s;" +
|
|
75
|
+
"z-index: 10000;" +
|
|
76
|
+
"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;";
|
|
77
|
+
|
|
78
|
+
button.addEventListener("mouseenter", function () {
|
|
79
|
+
button.style.transform = "translateY(-2px)";
|
|
80
|
+
button.style.boxShadow = "0 6px 16px rgba(0, 0, 0, 0.2)";
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
button.addEventListener("mouseleave", function () {
|
|
84
|
+
button.style.transform = "translateY(0)";
|
|
85
|
+
button.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.15)";
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
button.addEventListener("click", onClick);
|
|
89
|
+
document.body.appendChild(button);
|
|
90
|
+
|
|
91
|
+
return button;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function createModal(config, baseUrl) {
|
|
95
|
+
var modal = document.createElement("div");
|
|
96
|
+
modal.id = "echo-widget-modal";
|
|
97
|
+
modal.className = "echo-widget-modal";
|
|
98
|
+
modal.style.display = "none";
|
|
99
|
+
|
|
100
|
+
var overlay = document.createElement("div");
|
|
101
|
+
overlay.className = "echo-widget-overlay";
|
|
102
|
+
overlay.style.cssText =
|
|
103
|
+
"position: fixed;" +
|
|
104
|
+
"inset: 0;" +
|
|
105
|
+
"background-color: rgba(0, 0, 0, 0.5);" +
|
|
106
|
+
"z-index: 10001;" +
|
|
107
|
+
"opacity: 0;" +
|
|
108
|
+
"transition: opacity 0.2s;";
|
|
109
|
+
|
|
110
|
+
var container = document.createElement("div");
|
|
111
|
+
container.className = "echo-widget-container";
|
|
112
|
+
container.style.cssText =
|
|
113
|
+
"position: fixed;" +
|
|
114
|
+
"top: 50%;" +
|
|
115
|
+
"left: 50%;" +
|
|
116
|
+
"transform: translate(-50%, -50%) scale(0.95);" +
|
|
117
|
+
"width: 90%;" +
|
|
118
|
+
"max-width: 500px;" +
|
|
119
|
+
"height: 600px;" +
|
|
120
|
+
"max-height: 90vh;" +
|
|
121
|
+
"background: white;" +
|
|
122
|
+
"border-radius: 12px;" +
|
|
123
|
+
"box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);" +
|
|
124
|
+
"z-index: 10002;" +
|
|
125
|
+
"opacity: 0;" +
|
|
126
|
+
"transition: opacity 0.2s, transform 0.2s;" +
|
|
127
|
+
"overflow: hidden;";
|
|
128
|
+
|
|
129
|
+
var iframe = createWidgetIframe(config, baseUrl);
|
|
130
|
+
container.appendChild(iframe);
|
|
131
|
+
modal.appendChild(overlay);
|
|
132
|
+
modal.appendChild(container);
|
|
133
|
+
document.body.appendChild(modal);
|
|
134
|
+
|
|
135
|
+
function closeModal() {
|
|
136
|
+
overlay.style.opacity = "0";
|
|
137
|
+
container.style.opacity = "0";
|
|
138
|
+
container.style.transform = "translate(-50%, -50%) scale(0.95)";
|
|
139
|
+
setTimeout(function () {
|
|
140
|
+
modal.style.display = "none";
|
|
141
|
+
}, 200);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function openModal() {
|
|
145
|
+
modal.style.display = "block";
|
|
146
|
+
void modal.offsetHeight; // Force reflow
|
|
147
|
+
overlay.style.opacity = "1";
|
|
148
|
+
container.style.opacity = "1";
|
|
149
|
+
container.style.transform = "translate(-50%, -50%) scale(1)";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
overlay.addEventListener("click", closeModal);
|
|
153
|
+
|
|
154
|
+
modal.open = openModal;
|
|
155
|
+
modal.close = closeModal;
|
|
156
|
+
|
|
157
|
+
return modal;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function showSuccessMessage() {
|
|
161
|
+
var successMessage = document.createElement("div");
|
|
162
|
+
successMessage.className = "echo-widget-success";
|
|
163
|
+
successMessage.textContent = "Thank you for your feedback!";
|
|
164
|
+
successMessage.style.cssText =
|
|
165
|
+
"position: fixed;" +
|
|
166
|
+
"top: 20px;" +
|
|
167
|
+
"right: 20px;" +
|
|
168
|
+
"background: #10b981;" +
|
|
169
|
+
"color: white;" +
|
|
170
|
+
"padding: 12px 24px;" +
|
|
171
|
+
"border-radius: 8px;" +
|
|
172
|
+
"box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);" +
|
|
173
|
+
"z-index: 10003;" +
|
|
174
|
+
"animation: echoSlideIn 0.3s ease-out;" +
|
|
175
|
+
"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;";
|
|
176
|
+
document.body.appendChild(successMessage);
|
|
177
|
+
setTimeout(function () {
|
|
178
|
+
successMessage.remove();
|
|
179
|
+
}, 3000);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function injectStyles() {
|
|
183
|
+
var style = document.createElement("style");
|
|
184
|
+
style.textContent =
|
|
185
|
+
"@keyframes echoSlideIn {" +
|
|
186
|
+
"from { transform: translateX(100%); opacity: 0; }" +
|
|
187
|
+
"to { transform: translateX(0); opacity: 1; }" +
|
|
188
|
+
"}";
|
|
189
|
+
document.head.appendChild(style);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function initWidget(script) {
|
|
193
|
+
var config = getConfigFromScript(script);
|
|
194
|
+
|
|
195
|
+
if (!config.organizationId || !config.projectId) {
|
|
196
|
+
console.error("Echo Widget: Missing organizationId or projectId");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
var baseUrl = script.src.replace(/\/widget\/embed\.js.*$/, "");
|
|
201
|
+
|
|
202
|
+
var modal = createModal(config, baseUrl);
|
|
203
|
+
createFloatingButton(config, function () {
|
|
204
|
+
modal.open();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
window.addEventListener("message", function (event) {
|
|
208
|
+
var allowedOrigin = baseUrl || window.location.origin;
|
|
209
|
+
if (event.origin !== allowedOrigin) return;
|
|
210
|
+
|
|
211
|
+
if (event.data.type === "echo.widget.close") {
|
|
212
|
+
modal.close();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (event.data.type === "echo.widget.submitted") {
|
|
216
|
+
showSuccessMessage();
|
|
217
|
+
modal.close();
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
injectStyles();
|
|
222
|
+
|
|
223
|
+
console.log("Echo Widget v" + ECHO_WIDGET_VERSION + " loaded");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function initAllWidgets() {
|
|
227
|
+
var scripts = document.querySelectorAll('script[src*="embed.js"]');
|
|
228
|
+
scripts.forEach(function (script) {
|
|
229
|
+
initWidget(script);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (document.readyState === "loading") {
|
|
234
|
+
document.addEventListener("DOMContentLoaded", initAllWidgets);
|
|
235
|
+
} else {
|
|
236
|
+
initAllWidgets();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
window.EchoWidget = {
|
|
240
|
+
open: function () {
|
|
241
|
+
var modal = document.getElementById("echo-widget-modal");
|
|
242
|
+
if (modal && modal.open) modal.open();
|
|
243
|
+
},
|
|
244
|
+
close: function () {
|
|
245
|
+
var modal = document.getElementById("echo-widget-modal");
|
|
246
|
+
if (modal && modal.close) modal.close();
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
})();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# scripts/backup-db.sh
|
|
3
|
+
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
# Configuration
|
|
7
|
+
BACKUP_DIR="${BACKUP_DIR:-./backups}"
|
|
8
|
+
RETENTION_DAYS=${RETENTION_DAYS:-30}
|
|
9
|
+
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
|
10
|
+
BACKUP_FILE="$BACKUP_DIR/echo-$TIMESTAMP.sql"
|
|
11
|
+
DATABASE_URL="${DATABASE_URL:-postgresql://echo:changeme@localhost:5432/echo}"
|
|
12
|
+
|
|
13
|
+
# Colors
|
|
14
|
+
RED='\033[0;31m'
|
|
15
|
+
GREEN='\033[0;32m'
|
|
16
|
+
YELLOW='\033[1;33m'
|
|
17
|
+
NC='\033[0m'
|
|
18
|
+
|
|
19
|
+
log_info() {
|
|
20
|
+
echo -e "${GREEN}[INFO]${NC} $1"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
log_warn() {
|
|
24
|
+
echo -e "${YELLOW}[WARN]${NC} $1"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
log_error() {
|
|
28
|
+
echo -e "${RED}[ERROR]${NC} $1"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
mkdir -p "$BACKUP_DIR"
|
|
32
|
+
|
|
33
|
+
log_info "Starting database backup..."
|
|
34
|
+
log_info "Backup file: $BACKUP_FILE"
|
|
35
|
+
|
|
36
|
+
if pg_dump "$DATABASE_URL" > "$BACKUP_FILE"; then
|
|
37
|
+
log_info "Backup completed successfully"
|
|
38
|
+
|
|
39
|
+
gzip "$BACKUP_FILE"
|
|
40
|
+
BACKUP_FILE="$BACKUP_FILE.gz"
|
|
41
|
+
log_info "Backup compressed: $BACKUP_FILE"
|
|
42
|
+
|
|
43
|
+
FILE_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
|
|
44
|
+
log_info "Backup size: $FILE_SIZE"
|
|
45
|
+
|
|
46
|
+
log_info "Cleaning up old backups (older than $RETENTION_DAYS days)..."
|
|
47
|
+
DELETED=$(find "$BACKUP_DIR" -name "echo-*.sql.gz" -mtime +$RETENTION_DAYS -delete -print | wc -l)
|
|
48
|
+
log_info "Deleted $DELETED old backup(s)"
|
|
49
|
+
|
|
50
|
+
log_info "Current backups:"
|
|
51
|
+
ls -lh "$BACKUP_DIR"/echo-*.sql.gz 2>/dev/null | awk '{print " " $9 " (" $5 ")"}' || echo " No backups found"
|
|
52
|
+
|
|
53
|
+
log_info "Backup process completed!"
|
|
54
|
+
else
|
|
55
|
+
log_error "Backup failed!"
|
|
56
|
+
exit 1
|
|
57
|
+
fi
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Echo Team
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
6
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { backupDatabase, getBackupConfig } from "@/lib/services/backup";
|
|
19
|
+
|
|
20
|
+
const config = getBackupConfig();
|
|
21
|
+
|
|
22
|
+
backupDatabase(config)
|
|
23
|
+
.then(() => process.exit(0))
|
|
24
|
+
.catch(() => process.exit(1));
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Echo Team
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
6
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { generateOpenApiSpec } from '../lib/swagger/generate';
|
|
19
|
+
|
|
20
|
+
generateOpenApiSpec()
|
|
21
|
+
.then(() => console.log('OpenAPI spec generated at public/openapi.json'))
|
|
22
|
+
.catch(console.error);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Echo Team
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
6
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { execSync } from "child_process";
|
|
19
|
+
import { logger } from "@/lib/logger";
|
|
20
|
+
|
|
21
|
+
export function generateMigration(name: string) {
|
|
22
|
+
logger.info({ name }, "Generating migration");
|
|
23
|
+
execSync("bun run db:generate", { stdio: "inherit" });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function applyMigrations() {
|
|
27
|
+
logger.info("Applying migrations...");
|
|
28
|
+
execSync("bun run db:migrate", { stdio: "inherit" });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function pushSchema() {
|
|
32
|
+
logger.info("Pushing schema to database...");
|
|
33
|
+
execSync("bun run db:push", { stdio: "inherit" });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function checkSchema() {
|
|
37
|
+
logger.info("Checking schema consistency...");
|
|
38
|
+
execSync("bun run db:check", { stdio: "inherit" });
|
|
39
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Echo Team
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
6
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { sql } from "drizzle-orm";
|
|
19
|
+
import { db } from "@/lib/db";
|
|
20
|
+
import { runMigrations } from "@/lib/db/migrate";
|
|
21
|
+
import { logger } from "@/lib/logger";
|
|
22
|
+
import { backupDatabase, getBackupConfig } from "@/lib/services/backup";
|
|
23
|
+
|
|
24
|
+
async function verifyMigrations() {
|
|
25
|
+
if (!db) {
|
|
26
|
+
throw new Error("Database connection not configured");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const tables = await db.execute(sql`
|
|
30
|
+
SELECT table_name
|
|
31
|
+
FROM information_schema.tables
|
|
32
|
+
WHERE table_schema = 'public'
|
|
33
|
+
ORDER BY table_name;
|
|
34
|
+
`);
|
|
35
|
+
|
|
36
|
+
const rows = (tables as { rows?: unknown[] }).rows ?? [];
|
|
37
|
+
logger.info({ tables: rows }, "Database tables");
|
|
38
|
+
|
|
39
|
+
if (rows.length < 1) {
|
|
40
|
+
throw new Error("Unexpected number of tables, migration may have failed");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function createBackup() {
|
|
45
|
+
logger.info("Creating pre-migration backup...");
|
|
46
|
+
const config = getBackupConfig();
|
|
47
|
+
await backupDatabase(config);
|
|
48
|
+
logger.info("Pre-migration backup completed");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function preDeploy() {
|
|
52
|
+
logger.info("Starting pre-deployment migration...");
|
|
53
|
+
|
|
54
|
+
if (!db) {
|
|
55
|
+
throw new Error("Database connection not configured");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await db.execute(sql`SELECT 1`);
|
|
59
|
+
|
|
60
|
+
if (process.env.DB_BACKUP_BEFORE_MIGRATE === "true") {
|
|
61
|
+
await createBackup();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await runMigrations();
|
|
65
|
+
await verifyMigrations();
|
|
66
|
+
|
|
67
|
+
logger.info("Pre-deployment completed successfully");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
preDeploy()
|
|
71
|
+
.then(() => process.exit(0))
|
|
72
|
+
.catch((error) => {
|
|
73
|
+
logger.error({ err: error }, "Pre-deployment failed");
|
|
74
|
+
process.exit(1);
|
|
75
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# scripts/restore-db.sh
|
|
3
|
+
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
# Configuration
|
|
7
|
+
BACKUP_DIR="${BACKUP_DIR:-./backups}"
|
|
8
|
+
DATABASE_URL="${DATABASE_URL:-postgresql://echo:changeme@localhost:5432/echo}"
|
|
9
|
+
|
|
10
|
+
# Colors
|
|
11
|
+
RED='\033[0;31m'
|
|
12
|
+
GREEN='\033[0;32m'
|
|
13
|
+
YELLOW='\033[1;33m'
|
|
14
|
+
NC='\033[0m'
|
|
15
|
+
|
|
16
|
+
log_info() {
|
|
17
|
+
echo -e "${GREEN}[INFO]${NC} $1"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
log_warn() {
|
|
21
|
+
echo -e "${YELLOW}[WARN]${NC} $1"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
log_error() {
|
|
25
|
+
echo -e "${RED}[ERROR]${NC} $1"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if [ -z "$1" ]; then
|
|
29
|
+
log_error "Usage: $0 <backup-file.sql.gz>"
|
|
30
|
+
echo ""
|
|
31
|
+
echo "Available backups:"
|
|
32
|
+
ls -lh "$BACKUP_DIR"/echo-*.sql.gz 2>/dev/null | awk '{print " " $9 " (" $5 ")"}' || echo " No backups found"
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
BACKUP_FILE="$1"
|
|
37
|
+
|
|
38
|
+
if [ ! -f "$BACKUP_FILE" ]; then
|
|
39
|
+
log_error "Backup file not found: $BACKUP_FILE"
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
log_warn "WARNING: This will replace the entire database!"
|
|
44
|
+
log_warn "Backup to restore: $BACKUP_FILE"
|
|
45
|
+
echo ""
|
|
46
|
+
read -p "Are you sure you want to continue? (type 'yes' to confirm): " confirmation
|
|
47
|
+
|
|
48
|
+
if [ "$confirmation" != "yes" ]; then
|
|
49
|
+
log_info "Restore cancelled"
|
|
50
|
+
exit 0
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
log_info "Starting database restore..."
|
|
54
|
+
|
|
55
|
+
if gzip -dc "$BACKUP_FILE" | psql "$DATABASE_URL"; then
|
|
56
|
+
log_info "Database restored successfully!"
|
|
57
|
+
else
|
|
58
|
+
log_error "Restore failed!"
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Echo Team
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
6
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync } from "fs";
|
|
19
|
+
import { join } from "path";
|
|
20
|
+
import { sql } from "drizzle-orm";
|
|
21
|
+
import { db } from "@/lib/db";
|
|
22
|
+
import { logger } from "@/lib/logger";
|
|
23
|
+
|
|
24
|
+
async function executeRollback(migration: { name: string; hash: string }) {
|
|
25
|
+
const rollbackFile = join(
|
|
26
|
+
process.cwd(),
|
|
27
|
+
"lib/db/migrations",
|
|
28
|
+
`${migration.name}-rollback.sql`
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const rawSql = readFileSync(rollbackFile, "utf8");
|
|
32
|
+
await db!.execute(sql.raw(rawSql));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function rollback(steps = 1) {
|
|
36
|
+
if (!db) {
|
|
37
|
+
throw new Error("Database connection not configured");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
logger.info({ steps }, "Rolling back migrations...");
|
|
41
|
+
|
|
42
|
+
const appliedMigrations = await db.execute(sql`
|
|
43
|
+
SELECT * FROM drizzle_migrations
|
|
44
|
+
ORDER BY created_at DESC
|
|
45
|
+
LIMIT ${steps}
|
|
46
|
+
`);
|
|
47
|
+
|
|
48
|
+
const rows =
|
|
49
|
+
(appliedMigrations as unknown as {
|
|
50
|
+
rows?: Array<{ name: string; hash: string }>;
|
|
51
|
+
}).rows ?? [];
|
|
52
|
+
|
|
53
|
+
for (const migration of rows) {
|
|
54
|
+
logger.info({ migration }, "Rolling back migration");
|
|
55
|
+
await executeRollback(migration);
|
|
56
|
+
await db.execute(sql`
|
|
57
|
+
DELETE FROM drizzle_migrations
|
|
58
|
+
WHERE hash = ${migration.hash}
|
|
59
|
+
`);
|
|
60
|
+
logger.info({ migration: migration.name }, "Rolled back");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
logger.info("Rollback completed");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const stepsArg = Number.parseInt(process.argv[2] ?? "1", 10);
|
|
67
|
+
rollback(Number.isNaN(stepsArg) ? 1 : stepsArg)
|
|
68
|
+
.then(() => process.exit(0))
|
|
69
|
+
.catch((error) => {
|
|
70
|
+
logger.error({ err: error }, "Rollback failed");
|
|
71
|
+
process.exit(1);
|
|
72
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Echo Team
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
6
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { db } from "@/lib/db";
|
|
19
|
+
import { tags, PREDEFINED_TAGS } from "@/lib/db/schema";
|
|
20
|
+
import { logger } from "@/lib/logger";
|
|
21
|
+
|
|
22
|
+
async function seedTags() {
|
|
23
|
+
if (!db) {
|
|
24
|
+
throw new Error("Database connection not configured");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
logger.info("Seeding predefined tags...");
|
|
28
|
+
|
|
29
|
+
for (const tag of PREDEFINED_TAGS) {
|
|
30
|
+
await db
|
|
31
|
+
.insert(tags)
|
|
32
|
+
.values({
|
|
33
|
+
name: tag.name,
|
|
34
|
+
slug: tag.slug,
|
|
35
|
+
color: "#3b82f6",
|
|
36
|
+
})
|
|
37
|
+
.onConflictDoNothing();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
logger.info(`Seeded ${PREDEFINED_TAGS.length} tags successfully`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
seedTags()
|
|
44
|
+
.then(() => process.exit(0))
|
|
45
|
+
.catch((error) => {
|
|
46
|
+
logger.error(error, "Failed to seed tags");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
});
|