@voltro/cli 0.28.0 → 0.30.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/CHANGELOG.md +655 -0
- package/THIRD-PARTY-NOTICES.md +90 -4
- package/dist/{apiBuild-eUM32r1u.js → apiBuild-BceikpQf.js} +3 -3
- package/dist/apiBuild-CxvR1Dwt.js +2 -0
- package/dist/bin.js +3 -3
- package/dist/checkCommand-BMr-X48w.js +2427 -0
- package/dist/checkCommand-BOk9y264.js +2 -0
- package/dist/cliRuntime-Oh517vCV.js +96 -0
- package/dist/{commands-CSkrUI1h.js → commands-D4Y1-Nlu.js} +8575 -9366
- package/dist/{dbCommand-CpYgmSw4.js → dbCommand-C7hbl5pk.js} +345 -266
- package/dist/dbCommand-D8nhyppR.js +2 -0
- package/dist/{dev-DlBWWnJQ.js → dev-Cr-teYsz.js} +1 -1
- package/dist/{dev-CEZwJhmb.js → dev-DmnkH-wr.js} +2939 -1832
- package/dist/fileConventions-Cof68_BL.js +33 -0
- package/dist/{frameworkTableAssembly-BwHU9Euq.js → frameworkTableAssembly-BzAajuUv.js} +115 -99
- package/dist/frameworkTableAssembly-DU0aOxjL.js +2 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +1 -1
- package/dist/{inspect-CUCCzw2I.js → inspect-BUUjt773.js} +349 -192
- package/dist/inspect-mmBuRXmy.js +2 -0
- package/dist/{inspectMetrics-BU90mvJN.js → inspectMetrics-BeJdP_3b.js} +1068 -978
- package/dist/{manifestBuild-BnzAxp2O.js → manifestBuild-BLrVuSlM.js} +1 -1
- package/dist/manifestBuild-Dj8Jjoto.js +2 -0
- package/dist/sdkgen-CqpOq_fy.js +582 -0
- package/dist/serveCommand-Ciq_wIY8.js +1577 -0
- package/dist/serveEntry.js +2 -2
- package/dist/{start-BGXIf6zT.js → start-ocqB1seX.js} +283 -270
- package/dist/startEntry.js +2 -2
- package/package.json +17 -17
- package/templates/AGENTS.core.md +50 -0
- package/templates/AGENTS.md +54 -2
- package/templates/agent-docs/_index.md +4 -2
- package/templates/agent-docs/_manifest.json +26 -5
- package/templates/agent-docs/ai.md +191 -0
- package/templates/agent-docs/cli.md +143 -6
- package/templates/agent-docs/configuration.md +32 -0
- package/templates/agent-docs/data.md +282 -0
- package/templates/agent-docs/database/migrations.md +47 -0
- package/templates/agent-docs/database/misc.md +57 -0
- package/templates/agent-docs/database/schema.md +3 -1
- package/templates/agent-docs/internationalization.md +169 -8
- package/templates/agent-docs/local-first-mobile.md +426 -0
- package/templates/agent-docs/routing.md +94 -3
- package/templates/agent-docs/schema-driven-ui.md +12 -0
- package/templates/agent-docs/security.md +17 -0
- package/templates/agent-docs/templates/apibackends.md +210 -1
- package/templates/agent-docs/templates/appshells.md +144 -1
- package/templates/agent-docs/templates/mobile.md +60 -0
- package/templates/agent-docs/templates/overview.md +17 -7
- package/templates/agent-docs/whats-new.md +238 -158
- package/templates/agent-docs/workflows.md +376 -0
- package/templates/apps/api-ai/package.json +7 -7
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-sqlite/.env.example +19 -0
- package/templates/apps/api-backend-sqlite/README.md +38 -0
- package/templates/apps/api-backend-sqlite/app.config.ts +35 -0
- package/templates/apps/api-backend-sqlite/database/schema.ts +54 -0
- package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend-sqlite/package.json +30 -0
- package/templates/apps/api-backend-sqlite/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend-sqlite/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend-sqlite/template.json +6 -0
- package/templates/apps/api-backend-sqlite/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend-sqlite/tsconfig.json +5 -0
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-cms/README.md +104 -0
- package/templates/apps/api-cms/actions/content.get.action.server.ts +27 -0
- package/templates/apps/api-cms/actions/content.get.action.ts +19 -0
- package/templates/apps/api-cms/actions/content.types.action.server.ts +26 -0
- package/templates/apps/api-cms/actions/content.types.action.ts +40 -0
- package/templates/apps/api-cms/actions/me.action.server.ts +18 -0
- package/templates/apps/api-cms/actions/me.action.ts +16 -0
- package/templates/apps/api-cms/app.config.ts +61 -0
- package/templates/apps/api-cms/content/blogPost.contentType.ts +39 -0
- package/templates/apps/api-cms/content/index.ts +18 -0
- package/templates/apps/api-cms/content/page.contentType.ts +24 -0
- package/templates/apps/api-cms/database/schema.ts +64 -0
- package/templates/apps/api-cms/mutations/content.publish.mutation.server.ts +19 -0
- package/templates/apps/api-cms/mutations/content.publish.mutation.ts +15 -0
- package/templates/apps/api-cms/mutations/content.saveDraft.mutation.server.ts +36 -0
- package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +32 -0
- package/templates/apps/api-cms/mutations/content.unpublish.mutation.server.ts +19 -0
- package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +11 -0
- package/templates/apps/api-cms/package.json +32 -0
- package/templates/apps/api-cms/queries/content.list.query.server.ts +0 -0
- package/templates/apps/api-cms/queries/content.list.query.ts +27 -0
- package/templates/apps/api-cms/template.json +6 -0
- package/templates/apps/api-cms/tests/content.descriptors.test.ts +64 -0
- package/templates/apps/api-cms/tests/content.write.test.ts +85 -0
- package/templates/apps/api-cms/tsconfig.json +5 -0
- package/templates/apps/api-collab/README.md +70 -0
- package/templates/apps/api-collab/app.config.ts +28 -0
- package/templates/apps/api-collab/database/schema.ts +63 -0
- package/templates/apps/api-collab/mutations/documents.create.mutation.server.ts +19 -0
- package/templates/apps/api-collab/mutations/documents.create.mutation.ts +36 -0
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.server.ts +20 -0
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +30 -0
- package/templates/apps/api-collab/package.json +30 -0
- package/templates/apps/api-collab/queries/documents.query.server.ts +16 -0
- package/templates/apps/api-collab/queries/documents.query.ts +23 -0
- package/templates/apps/api-collab/template.json +6 -0
- package/templates/apps/api-collab/tests/documents.setBody.test.ts +89 -0
- package/templates/apps/api-collab/tsconfig.json +5 -0
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-saas-starter/README.md +103 -0
- package/templates/apps/api-saas-starter/actions/me.action.server.ts +18 -0
- package/templates/apps/api-saas-starter/actions/me.action.ts +20 -0
- package/templates/apps/api-saas-starter/app.config.ts +87 -0
- package/templates/apps/api-saas-starter/database/schema.ts +57 -0
- package/templates/apps/api-saas-starter/mutations/invites.create.mutation.server.ts +26 -0
- package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +18 -0
- package/templates/apps/api-saas-starter/mutations/projects.create.mutation.server.ts +29 -0
- package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +18 -0
- package/templates/apps/api-saas-starter/package.json +32 -0
- package/templates/apps/api-saas-starter/queries/invites.list.query.server.ts +14 -0
- package/templates/apps/api-saas-starter/queries/invites.list.query.ts +17 -0
- package/templates/apps/api-saas-starter/queries/projects.list.query.server.ts +14 -0
- package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -0
- package/templates/apps/api-saas-starter/template.json +6 -0
- package/templates/apps/api-saas-starter/tests/projects.create.test.ts +62 -0
- package/templates/apps/api-saas-starter/tests/session.test.ts +33 -0
- package/templates/apps/api-saas-starter/tsconfig.json +5 -0
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-status/README.md +88 -0
- package/templates/apps/api-status/app.config.ts +36 -0
- package/templates/apps/api-status/authz.ts +33 -0
- package/templates/apps/api-status/database/schema.ts +70 -0
- package/templates/apps/api-status/mutations/components.create.mutation.server.ts +19 -0
- package/templates/apps/api-status/mutations/components.create.mutation.ts +16 -0
- package/templates/apps/api-status/mutations/incidents.create.mutation.server.ts +25 -0
- package/templates/apps/api-status/mutations/incidents.create.mutation.ts +22 -0
- package/templates/apps/api-status/mutations/incidents.resolve.mutation.server.ts +24 -0
- package/templates/apps/api-status/mutations/incidents.resolve.mutation.ts +19 -0
- package/templates/apps/api-status/mutations/incidents.update.mutation.server.ts +26 -0
- package/templates/apps/api-status/mutations/incidents.update.mutation.ts +21 -0
- package/templates/apps/api-status/package.json +30 -0
- package/templates/apps/api-status/queries/components.list.query.server.ts +14 -0
- package/templates/apps/api-status/queries/components.list.query.ts +13 -0
- package/templates/apps/api-status/queries/incidents.live.query.server.ts +14 -0
- package/templates/apps/api-status/queries/incidents.live.query.ts +20 -0
- package/templates/apps/api-status/queries/updates.list.query.server.ts +14 -0
- package/templates/apps/api-status/queries/updates.list.query.ts +17 -0
- package/templates/apps/api-status/template.json +6 -0
- package/templates/apps/api-status/tests/status.test.ts +70 -0
- package/templates/apps/api-status/tsconfig.json +5 -0
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/app.config.ts +1 -1
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-admin/src/locales/index.ts +1 -1
- package/templates/apps/frontend-app/app.config.ts +1 -1
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-app/src/locales/index.ts +1 -1
- package/templates/apps/frontend-app/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-app/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-auth/README.md +78 -0
- package/templates/apps/frontend-auth/app.config.ts +34 -0
- package/templates/apps/frontend-auth/package.json +32 -0
- package/templates/apps/frontend-auth/src/components/AuthShell.tsx +35 -0
- package/templates/apps/frontend-auth/src/components/PasswordStrength.tsx +33 -0
- package/templates/apps/frontend-auth/src/config.ts +11 -0
- package/templates/apps/frontend-auth/src/globals.css +105 -0
- package/templates/apps/frontend-auth/src/globals.d.ts +6 -0
- package/templates/apps/frontend-auth/src/lib/auth.ts +34 -0
- package/templates/apps/frontend-auth/src/lib/redirect.test.ts +24 -0
- package/templates/apps/frontend-auth/src/lib/redirect.ts +29 -0
- package/templates/apps/frontend-auth/src/locales/de.ts +66 -0
- package/templates/apps/frontend-auth/src/locales/en.ts +76 -0
- package/templates/apps/frontend-auth/src/locales/index.ts +14 -0
- package/templates/apps/frontend-auth/src/pages/forgot/page.tsx +51 -0
- package/templates/apps/frontend-auth/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-auth/src/pages/login/page.test.tsx +53 -0
- package/templates/apps/frontend-auth/src/pages/login/page.tsx +67 -0
- package/templates/apps/frontend-auth/src/pages/logout/page.tsx +28 -0
- package/templates/apps/frontend-auth/src/pages/magic/page.tsx +51 -0
- package/templates/apps/frontend-auth/src/pages/page.tsx +26 -0
- package/templates/apps/frontend-auth/src/pages/reset/page.test.tsx +51 -0
- package/templates/apps/frontend-auth/src/pages/reset/page.tsx +63 -0
- package/templates/apps/frontend-auth/src/pages/signup/page.tsx +60 -0
- package/templates/apps/frontend-auth/src/pages/verify/page.tsx +50 -0
- package/templates/apps/frontend-auth/template.json +6 -0
- package/templates/apps/frontend-auth/tsconfig.json +5 -0
- package/templates/apps/frontend-blank/app.config.ts +1 -1
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-blank/src/locales/index.ts +1 -1
- package/templates/apps/frontend-blank/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-blank/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-cms/README.md +47 -0
- package/templates/apps/frontend-cms/app.config.ts +37 -0
- package/templates/apps/frontend-cms/package.json +33 -0
- package/templates/apps/frontend-cms/src/config.ts +8 -0
- package/templates/apps/frontend-cms/src/globals.css +105 -0
- package/templates/apps/frontend-cms/src/globals.d.ts +6 -0
- package/templates/apps/frontend-cms/src/lib/api.ts +58 -0
- package/templates/apps/frontend-cms/src/locales/de.ts +50 -0
- package/templates/apps/frontend-cms/src/locales/en.ts +55 -0
- package/templates/apps/frontend-cms/src/locales/index.ts +14 -0
- package/templates/apps/frontend-cms/src/pages/(app)/error.tsx +18 -0
- package/templates/apps/frontend-cms/src/pages/(app)/layout.test.tsx +31 -0
- package/templates/apps/frontend-cms/src/pages/(app)/layout.tsx +62 -0
- package/templates/apps/frontend-cms/src/pages/(app)/not-found.tsx +14 -0
- package/templates/apps/frontend-cms/src/pages/(app)/page.test.tsx +110 -0
- package/templates/apps/frontend-cms/src/pages/(app)/page.tsx +159 -0
- package/templates/apps/frontend-cms/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-cms/src/pages/login/page.test.tsx +58 -0
- package/templates/apps/frontend-cms/src/pages/login/page.tsx +93 -0
- package/templates/apps/frontend-cms/template.json +6 -0
- package/templates/apps/frontend-cms/tsconfig.json +5 -0
- package/templates/apps/frontend-collab/README.md +80 -0
- package/templates/apps/frontend-collab/app.config.ts +38 -0
- package/templates/apps/frontend-collab/package.json +33 -0
- package/templates/apps/frontend-collab/src/locales/de.ts +29 -0
- package/templates/apps/frontend-collab/src/locales/en.ts +31 -0
- package/templates/apps/frontend-collab/src/locales/index.ts +15 -0
- package/templates/apps/frontend-collab/src/pages/layout.tsx +39 -0
- package/templates/apps/frontend-collab/src/pages/page.test.tsx +150 -0
- package/templates/apps/frontend-collab/src/pages/page.tsx +186 -0
- package/templates/apps/frontend-collab/template.json +6 -0
- package/templates/apps/frontend-collab/tsconfig.json +11 -0
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/app.config.ts +1 -1
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-dashboard/src/locales/index.ts +1 -1
- package/templates/apps/frontend-docs/package.json +7 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-portal/README.md +71 -0
- package/templates/apps/frontend-portal/app.config.ts +37 -0
- package/templates/apps/frontend-portal/package.json +32 -0
- package/templates/apps/frontend-portal/src/config.ts +8 -0
- package/templates/apps/frontend-portal/src/globals.css +93 -0
- package/templates/apps/frontend-portal/src/globals.d.ts +6 -0
- package/templates/apps/frontend-portal/src/lib/api.ts +62 -0
- package/templates/apps/frontend-portal/src/locales/de.ts +86 -0
- package/templates/apps/frontend-portal/src/locales/en.ts +93 -0
- package/templates/apps/frontend-portal/src/locales/index.ts +14 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.test.tsx +71 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.tsx +124 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.test.tsx +86 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.tsx +97 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/error.tsx +19 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/invoices/page.tsx +45 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/layout.test.tsx +31 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/layout.tsx +63 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/not-found.tsx +15 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/page.tsx +39 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/profile/page.tsx +79 -0
- package/templates/apps/frontend-portal/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-portal/src/pages/login/page.test.tsx +58 -0
- package/templates/apps/frontend-portal/src/pages/login/page.tsx +96 -0
- package/templates/apps/frontend-portal/template.json +6 -0
- package/templates/apps/frontend-portal/tsconfig.json +5 -0
- package/templates/apps/frontend-saas/README.md +73 -0
- package/templates/apps/frontend-saas/app.config.ts +46 -0
- package/templates/apps/frontend-saas/package.json +32 -0
- package/templates/apps/frontend-saas/src/config.ts +8 -0
- package/templates/apps/frontend-saas/src/globals.css +85 -0
- package/templates/apps/frontend-saas/src/globals.d.ts +6 -0
- package/templates/apps/frontend-saas/src/lib/api.ts +47 -0
- package/templates/apps/frontend-saas/src/locales/de.ts +72 -0
- package/templates/apps/frontend-saas/src/locales/en.ts +81 -0
- package/templates/apps/frontend-saas/src/locales/index.ts +14 -0
- package/templates/apps/frontend-saas/src/pages/(marketing)/layout.tsx +30 -0
- package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.test.tsx +58 -0
- package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.tsx +96 -0
- package/templates/apps/frontend-saas/src/pages/(marketing)/page.tsx +27 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +72 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/error.tsx +20 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/layout.test.tsx +32 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/layout.tsx +69 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/not-found.tsx +17 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +113 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +76 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +73 -0
- package/templates/apps/frontend-saas/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-saas/template.json +6 -0
- package/templates/apps/frontend-saas/tsconfig.json +5 -0
- package/templates/apps/frontend-spa/app.config.ts +1 -1
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-spa/src/locales/index.ts +1 -1
- package/templates/apps/frontend-spa/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-spa/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-ssr/app.config.ts +1 -1
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr/src/locales/index.ts +1 -1
- package/templates/apps/frontend-ssr/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-ssr/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-ssr-api/app.config.ts +1 -1
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-ssr-api/src/locales/index.ts +1 -1
- package/templates/apps/frontend-ssr-api/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/templates/apps/frontend-status/README.md +51 -0
- package/templates/apps/frontend-status/app.config.ts +39 -0
- package/templates/apps/frontend-status/package.json +32 -0
- package/templates/apps/frontend-status/src/config.ts +7 -0
- package/templates/apps/frontend-status/src/globals.css +70 -0
- package/templates/apps/frontend-status/src/globals.d.ts +6 -0
- package/templates/apps/frontend-status/src/lib/status.ts +81 -0
- package/templates/apps/frontend-status/src/locales/de.ts +41 -0
- package/templates/apps/frontend-status/src/locales/en.ts +45 -0
- package/templates/apps/frontend-status/src/locales/index.ts +13 -0
- package/templates/apps/frontend-status/src/pages/layout.tsx +27 -0
- package/templates/apps/frontend-status/src/pages/page.test.tsx +123 -0
- package/templates/apps/frontend-status/src/pages/page.tsx +136 -0
- package/templates/apps/frontend-status/template.json +6 -0
- package/templates/apps/frontend-status/tsconfig.json +11 -0
- package/templates/apps/mobile-app/README.md +58 -0
- package/templates/apps/mobile-app/app.config.ts +33 -0
- package/templates/apps/mobile-app/babel.config.js +5 -0
- package/templates/apps/mobile-app/metro.config.js +20 -0
- package/templates/apps/mobile-app/package.json +33 -0
- package/templates/apps/mobile-app/src/app/_layout.tsx +61 -0
- package/templates/apps/mobile-app/src/app/index.tsx +62 -0
- package/templates/apps/mobile-app/src/app/orders/[id].tsx +19 -0
- package/templates/apps/mobile-app/src/app/settings.tsx +51 -0
- package/templates/apps/mobile-app/src/client.ts +74 -0
- package/templates/apps/mobile-app/src/lib/deeplinks.ts +39 -0
- package/templates/apps/mobile-app/src/lib/notifications.ts +31 -0
- package/templates/apps/mobile-app/src/lib/sync.ts +29 -0
- package/templates/apps/mobile-app/src/persistence.ts +38 -0
- package/templates/apps/mobile-app/template.json +6 -0
- package/templates/apps/mobile-app/tests/deeplinks.test.ts +32 -0
- package/templates/apps/mobile-app/tests/notifications.test.ts +26 -0
- package/templates/apps/mobile-app/tests/sync.test.ts +34 -0
- package/templates/apps/mobile-app/tsconfig.json +10 -0
- package/templates/apps/mobile-app/voltro.mobile.ts +11 -0
- package/templates/baselines/compose/docker-compose.prod.yml +15 -0
- package/templates/baselines/compose-mariadb/docker-compose.prod.yml +15 -0
- package/templates/patches/@effect__cluster@0.60.0.patch +6 -6
- package/dist/apiBuild-DgBS9ayv.js +0 -2
- package/dist/dbCommand-DvguqlzF.js +0 -2
- package/dist/fileConventions-3bffWssN.js +0 -30
- package/dist/frameworkTableAssembly-lrjZtk0G.js +0 -2
- package/dist/inspect-gt8bq-Tz.js +0 -2
- package/dist/manifestBuild-ifczArzr.js +0 -2
- package/dist/serveCommand-ZTn-dPFa.js +0 -1425
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# {{capProjectName}} {{capAppName}} — billing & account portal
|
|
2
|
+
|
|
3
|
+
The lean "manage your account" surface customers visit outside the product:
|
|
4
|
+
plan, payment method, invoices, API keys, profile. Consumes the
|
|
5
|
+
**api-saas-starter** backend; billing is plugin-orchestrated, so the template
|
|
6
|
+
never renders an invoice or holds a card. Bilingual (en/de).
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
voltro init my-saas --api=api-saas-starter --web=frontend-portal
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Pages
|
|
13
|
+
|
|
14
|
+
| URL | Reads / drives | Notes |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `/` | `billing.subscription` | overview: plan, status, renewal + quick links |
|
|
17
|
+
| `/billing` | `billing.subscription`, `billing.portalUrl`, `billing.previewChange`, `billing.changePlan` | plan card; card management redirects to the provider portal; a plan change QUOTES the provider's proration before applying |
|
|
18
|
+
| `/invoices` | `billing.invoices` | list; "View" / "PDF" link out to the provider's hosted pages |
|
|
19
|
+
| `/api-keys` | `GET/POST /v1/api-keys*` | the framework's built-in key management (see below) |
|
|
20
|
+
| `/profile` | `session.me`, `/auth/password-reset`, `/auth/sign-out` | identity, self-serve password reset, sign out |
|
|
21
|
+
| `/login` | `POST /auth/*` | sign-in / sign-up |
|
|
22
|
+
|
|
23
|
+
`(portal)/layout.tsx` is the SSR auth gate — it asks `session.me` server-side and
|
|
24
|
+
RedirectErrors an anonymous visitor to `/login`.
|
|
25
|
+
|
|
26
|
+
## API keys — one config step on the API
|
|
27
|
+
|
|
28
|
+
The API-keys page drives the framework's **built-in** management surface
|
|
29
|
+
(`/v1/api-keys`), which the API exposes only when it sets `apiKeys: true` in its
|
|
30
|
+
`app.config.ts`. **api-saas-starter does not enable it by default**, so out of the
|
|
31
|
+
box this page shows a clear "enable it" notice rather than a broken table. To make
|
|
32
|
+
it live:
|
|
33
|
+
|
|
34
|
+
1. On the API: `export default { …, apiKeys: true }`.
|
|
35
|
+
2. Grant the signed-in user an issuing scope (`apikey:issue:self`) or `admin` —
|
|
36
|
+
the management routes are admin-scoped by design (keys are org credentials).
|
|
37
|
+
|
|
38
|
+
The raw key is shown exactly **once** at mint time (hash-stored, unrecoverable).
|
|
39
|
+
The page degrades on `404` (not enabled), `401/403` (no scope), and errors.
|
|
40
|
+
|
|
41
|
+
## What's intentionally NOT here
|
|
42
|
+
|
|
43
|
+
Per the lean brief: **no** support-ticket system and **no** team/member
|
|
44
|
+
management — those belong in the product's own dashboard or a dedicated tool.
|
|
45
|
+
Seat changes stay with the dashboard (`billing.changeSeats`); this portal is plan,
|
|
46
|
+
payment, invoices, keys, profile.
|
|
47
|
+
|
|
48
|
+
## Files
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
app.config.ts web app; apis.app → @{{projectName}}/api (api-saas-starter)
|
|
52
|
+
src/lib/api.ts wire shapes (subscription/invoice/apiKey) + formatMoney
|
|
53
|
+
src/pages/
|
|
54
|
+
login/page.tsx(.test) sign-in / sign-up (POST /auth/*)
|
|
55
|
+
(portal)/layout.tsx(.test) shell + SSR auth gate (session.me)
|
|
56
|
+
(portal)/page.tsx overview
|
|
57
|
+
(portal)/billing/page.tsx(.test) plan + payment method + quoted plan change
|
|
58
|
+
(portal)/invoices/page.tsx invoice history (links out)
|
|
59
|
+
(portal)/api-keys/page.tsx(.test) framework key management (graceful)
|
|
60
|
+
(portal)/profile/page.tsx identity + password reset + sign out
|
|
61
|
+
(portal)/error.tsx · not-found.tsx
|
|
62
|
+
src/locales/{en,de}.ts bilingual catalogs (parity-enforced)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Going to production
|
|
66
|
+
|
|
67
|
+
- **Real Stripe** — configure `billingPlugin({ provider: 'stripe', … })` on the
|
|
68
|
+
API; `billing.portalUrl` then opens Stripe's real hosted portal and
|
|
69
|
+
`billing.invoices` returns real `hostedUrl`/`pdfUrl` links.
|
|
70
|
+
- **Email** — wire `sendEmail` on the API so the profile password-reset actually
|
|
71
|
+
delivers.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Web app for the {{projectName}} project — the customer BILLING + ACCOUNT
|
|
2
|
+
// portal over the sibling `api-saas-starter` api. Slim by design: the screens a
|
|
3
|
+
// customer visits to manage their account OUTSIDE the product — plan, payment
|
|
4
|
+
// method, invoices, API keys, profile.
|
|
5
|
+
//
|
|
6
|
+
// - `/login` — static sign-in that POSTs to the api's `/auth/*`.
|
|
7
|
+
// - The authed portal in the SILENT route group `src/pages/(portal)/`. Its
|
|
8
|
+
// `layout.tsx` loader is the auth gate (asks the api's `session.me`,
|
|
9
|
+
// RedirectErrors an anonymous visitor to /login). Its pages are
|
|
10
|
+
// `renderMode: 'ssr'` so the gate runs per request.
|
|
11
|
+
//
|
|
12
|
+
// Billing is plugin-orchestrated: `billing.subscription` / `billing.invoices`
|
|
13
|
+
// are live reads, and card management / plan changes go through the provider via
|
|
14
|
+
// `billing.portalUrl` / `billing.changePlan` — the template never renders an
|
|
15
|
+
// invoice or holds a card itself.
|
|
16
|
+
import { defineEnv, envVar } from '@voltro/env'
|
|
17
|
+
|
|
18
|
+
export const env = defineEnv({
|
|
19
|
+
VOLTRO_PUBLIC_APP_NAME: envVar.string({ access: 'public', default: '{{capProjectName}}' }),
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export default {
|
|
23
|
+
type: 'web' as const,
|
|
24
|
+
name: '{{capProjectName}}{{capAppName}}',
|
|
25
|
+
port: {{port}},
|
|
26
|
+
theme: 'system' as const,
|
|
27
|
+
env,
|
|
28
|
+
locales: ['en', 'de'] as const,
|
|
29
|
+
defaultLocale: 'en' as const,
|
|
30
|
+
// The api this portal consumes. Pair with api-saas-starter:
|
|
31
|
+
// `voltro init my-saas --api=api-saas-starter --web=frontend-portal`.
|
|
32
|
+
// The hooks pass 'app' as the lookup key; codegen pulls its typed rpc surface
|
|
33
|
+
// (session.me + the billing plugin's `billing.*`).
|
|
34
|
+
apis: {
|
|
35
|
+
app: { package: '@{{projectName}}/api' },
|
|
36
|
+
},
|
|
37
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{projectName}}/{{appName}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "voltro dev .",
|
|
8
|
+
"lint": "voltro doctor .",
|
|
9
|
+
"build": "voltro build .",
|
|
10
|
+
"start": "voltro start .",
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"test": "voltro test"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@{{projectName}}/api": "0.30.0",
|
|
16
|
+
"@voltro/cli": "0.30.0",
|
|
17
|
+
"@voltro/client": "0.30.0",
|
|
18
|
+
"@voltro/env": "0.30.0",
|
|
19
|
+
"@voltro/i18n": "0.30.0",
|
|
20
|
+
"@voltro/ui-shadcn": "0.30.0",
|
|
21
|
+
"@voltro/web": "0.30.0",
|
|
22
|
+
"react": "^19.0.0",
|
|
23
|
+
"react-dom": "^19.0.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@voltro/devtools": "0.30.0",
|
|
27
|
+
"@types/react": "^19.0.0",
|
|
28
|
+
"@types/react-dom": "^19.0.0",
|
|
29
|
+
"typescript": "^6.0.3",
|
|
30
|
+
"vitest": "^4.1.10"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The brand name shown in the marketing header + dashboard sidebar.
|
|
2
|
+
//
|
|
3
|
+
// A plain constant — not `publicEnv` — so the template typechecks BEFORE
|
|
4
|
+
// `voltro dev` generates the env types. `VOLTRO_PUBLIC_APP_NAME` is still
|
|
5
|
+
// declared in app.config.ts (validated + in the env manifest); to read it typed
|
|
6
|
+
// instead, after one `voltro dev` swap this for
|
|
7
|
+
// `publicEnv.VOLTRO_PUBLIC_APP_NAME` from '@voltro/env/public'.
|
|
8
|
+
export const APP_NAME = '{{capProjectName}}'
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/* Plain CSS — no Tailwind, so there's no @source scanner to configure. */
|
|
2
|
+
:root {
|
|
3
|
+
--bg: #ffffff;
|
|
4
|
+
--fg: #0a0a0a;
|
|
5
|
+
--muted: #6b7280;
|
|
6
|
+
--accent: #4f46e5;
|
|
7
|
+
--danger: #dc2626;
|
|
8
|
+
--border: #e5e7eb;
|
|
9
|
+
--panel: #f9fafb;
|
|
10
|
+
}
|
|
11
|
+
:root.dark {
|
|
12
|
+
--bg: #0a0a0a;
|
|
13
|
+
--fg: #fafafa;
|
|
14
|
+
--muted: #9ca3af;
|
|
15
|
+
--accent: #818cf8;
|
|
16
|
+
--danger: #f87171;
|
|
17
|
+
--border: #1f2937;
|
|
18
|
+
--panel: #111827;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
* { box-sizing: border-box; }
|
|
22
|
+
body {
|
|
23
|
+
margin: 0;
|
|
24
|
+
background: var(--bg);
|
|
25
|
+
color: var(--fg);
|
|
26
|
+
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
27
|
+
line-height: 1.55;
|
|
28
|
+
}
|
|
29
|
+
a { color: inherit; }
|
|
30
|
+
.muted { color: var(--muted); }
|
|
31
|
+
.spacer { flex: 1; }
|
|
32
|
+
|
|
33
|
+
/* Buttons + inputs */
|
|
34
|
+
.btn {
|
|
35
|
+
display: inline-block; cursor: pointer; border: 0; border-radius: 8px;
|
|
36
|
+
background: var(--accent); color: #fff; padding: 0.55rem 1rem;
|
|
37
|
+
font: inherit; font-weight: 600; text-decoration: none;
|
|
38
|
+
}
|
|
39
|
+
.btn:disabled { opacity: 0.6; cursor: default; }
|
|
40
|
+
.btn.ghost { background: transparent; color: var(--accent); border: 1px solid var(--border); }
|
|
41
|
+
.link { background: none; border: 0; color: var(--accent); cursor: pointer; font: inherit; padding: 0; }
|
|
42
|
+
input {
|
|
43
|
+
font: inherit; padding: 0.5rem 0.65rem; border: 1px solid var(--border);
|
|
44
|
+
border-radius: 8px; background: var(--bg); color: var(--fg); min-width: 0;
|
|
45
|
+
}
|
|
46
|
+
.error-text { color: var(--danger); }
|
|
47
|
+
|
|
48
|
+
/* Marketing */
|
|
49
|
+
.marketing header {
|
|
50
|
+
display: flex; align-items: center; gap: 1rem;
|
|
51
|
+
padding: 1rem 1.5rem; border-bottom: 1px solid var(--border);
|
|
52
|
+
}
|
|
53
|
+
.marketing .brand { font-weight: 700; text-decoration: none; }
|
|
54
|
+
.hero { max-width: 40rem; margin: 0 auto; padding: 3rem 1.5rem; }
|
|
55
|
+
.hero h1 { margin: 0 0 0.75rem; }
|
|
56
|
+
.auth-form { display: flex; flex-direction: column; gap: 0.85rem; max-width: 22rem; margin-top: 1.25rem; }
|
|
57
|
+
.auth-form label { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.9rem; }
|
|
58
|
+
|
|
59
|
+
/* Dashboard shell */
|
|
60
|
+
.dash { display: grid; grid-template-columns: 15rem 1fr; min-height: 100vh; }
|
|
61
|
+
.dash aside { border-right: 1px solid var(--border); padding: 1.25rem; background: var(--panel); }
|
|
62
|
+
.dash aside .brand { font-weight: 700; margin-bottom: 1rem; }
|
|
63
|
+
.dash nav { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
64
|
+
.dash nav a { text-decoration: none; color: var(--muted); padding: 0.35rem 0; }
|
|
65
|
+
.dash nav a[aria-current="page"] { color: var(--fg); font-weight: 600; }
|
|
66
|
+
.dash main { padding: 1.5rem 2rem; }
|
|
67
|
+
.topbar { display: flex; align-items: center; margin-bottom: 1rem; }
|
|
68
|
+
@media (max-width: 40rem) { .dash { grid-template-columns: 1fr; } .dash aside { border-right: 0; border-bottom: 1px solid var(--border); } }
|
|
69
|
+
|
|
70
|
+
/* Bits */
|
|
71
|
+
.row { display: flex; gap: 0.6rem; align-items: center; flex-wrap: wrap; margin-top: 0.5rem; }
|
|
72
|
+
.empty { color: var(--muted); margin-top: 1.5rem; }
|
|
73
|
+
.cards { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); gap: 0.75rem; }
|
|
74
|
+
.card { border: 1px solid var(--border); border-radius: 10px; padding: 0.9rem 1rem; background: var(--panel); }
|
|
75
|
+
.card .value { font-weight: 600; }
|
|
76
|
+
.card .label { color: var(--muted); font-size: 0.85rem; margin-top: 0.2rem; }
|
|
77
|
+
.paywall { border: 1px solid var(--accent); border-radius: 12px; padding: 1rem 1.25rem; margin-top: 1.25rem; background: color-mix(in oklab, var(--accent) 8%, transparent); }
|
|
78
|
+
.paywall p { margin: 0.35rem 0 0.75rem; }
|
|
79
|
+
.facts { display: grid; grid-template-columns: max-content 1fr; gap: 0.4rem 1.5rem; margin-top: 1rem; }
|
|
80
|
+
.facts dt { color: var(--muted); }
|
|
81
|
+
.facts dd { margin: 0; font-weight: 600; }
|
|
82
|
+
.rows { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.4rem; }
|
|
83
|
+
.rows li { display: flex; justify-content: space-between; align-items: center; border: 1px solid var(--border); border-radius: 8px; padding: 0.6rem 0.9rem; background: var(--panel); }
|
|
84
|
+
.tag { font-size: 0.8rem; color: var(--muted); border: 1px solid var(--border); border-radius: 999px; padding: 0.05rem 0.55rem; }
|
|
85
|
+
code { background: color-mix(in oklab, var(--fg) 8%, transparent); padding: 0.1em 0.35em; border-radius: 4px; }
|
|
86
|
+
|
|
87
|
+
/* Portal extras */
|
|
88
|
+
.rows .grow { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
89
|
+
.rows li { gap: 0.6rem; }
|
|
90
|
+
.notice { border: 1px solid var(--accent); border-radius: 10px; padding: 0.85rem 1rem; background: color-mix(in oklab, var(--accent) 8%, transparent); }
|
|
91
|
+
.ok { color: var(--accent); }
|
|
92
|
+
.token { display: block; margin-top: 0.5rem; padding: 0.6rem 0.75rem; background: var(--bg); border: 1px solid var(--border); border-radius: 8px; word-break: break-all; font-size: 0.85rem; }
|
|
93
|
+
.cards .card a { font-weight: 600; text-decoration: none; color: var(--accent); }
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Ambient declarations.
|
|
2
|
+
//
|
|
3
|
+
// `declare module '*.css'`: TypeScript with `moduleResolution: Bundler` rejects
|
|
4
|
+
// a bare `import './globals.css'` unless the asset module is declared. Vite
|
|
5
|
+
// resolves it at build time; the declaration is type-only.
|
|
6
|
+
declare module '*.css'
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// The shapes this portal reads off the api-saas-starter backend, kept LOCAL so
|
|
2
|
+
// the pages are self-contained. They mirror the api's descriptor outputs
|
|
3
|
+
// (`session.me`, `billing.subscription`, `billing.invoices`) and the framework's
|
|
4
|
+
// `/v1/api-keys` REST surface.
|
|
5
|
+
|
|
6
|
+
// The caller's resolved identity, from `session.me`. The SSR gate reads it.
|
|
7
|
+
export interface Identity {
|
|
8
|
+
readonly type: string
|
|
9
|
+
readonly id: string | null
|
|
10
|
+
readonly tenantId: string | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// `billing.subscription` streams `{ subscription: … | null }` — null before the
|
|
14
|
+
// tenant has any subscription row (they're on the implicit free plan).
|
|
15
|
+
export interface SubscriptionState {
|
|
16
|
+
readonly subscription: {
|
|
17
|
+
readonly plan: string
|
|
18
|
+
readonly status: string
|
|
19
|
+
readonly quantity: number
|
|
20
|
+
readonly currentPeriodEnd: string | null
|
|
21
|
+
readonly cancelAt: string | null
|
|
22
|
+
} | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// One row of `billing.invoices`. `hostedUrl` / `pdfUrl` are the PROVIDER's own
|
|
26
|
+
// pages — the portal links out, it never renders an invoice.
|
|
27
|
+
export interface Invoice {
|
|
28
|
+
readonly providerInvoiceId: string
|
|
29
|
+
readonly amountMinor: number
|
|
30
|
+
readonly currency: string
|
|
31
|
+
readonly status: string
|
|
32
|
+
readonly createdAt: string | null
|
|
33
|
+
readonly hostedUrl: string | null
|
|
34
|
+
readonly pdfUrl: string | null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// `billing.previewChange` — the provider's prorated settlement for a change.
|
|
38
|
+
export interface ChangePreview {
|
|
39
|
+
readonly plan: string
|
|
40
|
+
readonly quantity: number
|
|
41
|
+
readonly prorationMinor: number
|
|
42
|
+
readonly currency: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// One key from `GET /v1/api-keys` (the framework's built-in management surface).
|
|
46
|
+
export interface ApiKey {
|
|
47
|
+
readonly id: string
|
|
48
|
+
readonly name: string
|
|
49
|
+
readonly keyPrefix: string
|
|
50
|
+
readonly createdAt: number
|
|
51
|
+
readonly lastUsedAt: number | null
|
|
52
|
+
readonly revokedAt: number | null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Minor units (cents) → a display string. Intl handles the currency + locale.
|
|
56
|
+
export const formatMoney = (minor: number, currency: string, locale: string): string => {
|
|
57
|
+
try {
|
|
58
|
+
return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(minor / 100)
|
|
59
|
+
} catch {
|
|
60
|
+
return `${(minor / 100).toFixed(2)} ${currency.toUpperCase()}`
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// German catalog. Mirrors every key in `en.ts` — `defineLocale<typeof en>()`
|
|
2
|
+
// fails the build if a key is missing. Translate the prose only; keep ICU
|
|
3
|
+
// placeholders (`{amount}`) identical to en.ts.
|
|
4
|
+
import { defineLocale } from '@voltro/i18n'
|
|
5
|
+
import en from './en'
|
|
6
|
+
|
|
7
|
+
export default defineLocale<typeof en>()({
|
|
8
|
+
'meta.login.title': 'Anmelden',
|
|
9
|
+
'meta.overview.title': 'Konto',
|
|
10
|
+
'meta.billing.title': 'Abrechnung',
|
|
11
|
+
'meta.invoices.title': 'Rechnungen',
|
|
12
|
+
'meta.apiKeys.title': 'API-Schlüssel',
|
|
13
|
+
'meta.profile.title': 'Profil',
|
|
14
|
+
|
|
15
|
+
'lang.label': 'Sprache',
|
|
16
|
+
|
|
17
|
+
'auth.signIn.heading': 'Anmelden',
|
|
18
|
+
'auth.signUp.heading': 'Konto erstellen',
|
|
19
|
+
'auth.email': 'E-Mail',
|
|
20
|
+
'auth.password': 'Passwort',
|
|
21
|
+
'auth.signIn.submit': 'Anmelden',
|
|
22
|
+
'auth.signUp.submit': 'Konto erstellen',
|
|
23
|
+
'auth.toggle.toSignUp': 'Kein Konto? Erstelle eins',
|
|
24
|
+
'auth.toggle.toSignIn': 'Konto vorhanden? Anmelden',
|
|
25
|
+
'auth.pending': 'Bitte warten…',
|
|
26
|
+
'auth.error': 'Das hat nicht geklappt. Prüfe deine Angaben und versuche es erneut.',
|
|
27
|
+
|
|
28
|
+
'nav.overview': 'Übersicht',
|
|
29
|
+
'nav.billing': 'Abrechnung',
|
|
30
|
+
'nav.invoices': 'Rechnungen',
|
|
31
|
+
'nav.apiKeys': 'API-Schlüssel',
|
|
32
|
+
'nav.profile': 'Profil',
|
|
33
|
+
|
|
34
|
+
'overview.heading': 'Kontoübersicht',
|
|
35
|
+
'overview.renews': 'Verlängert am',
|
|
36
|
+
'overview.link.billing': 'Plan & Zahlungsmethode',
|
|
37
|
+
'overview.link.invoices': 'Rechnungsverlauf',
|
|
38
|
+
'overview.link.apiKeys': 'API-Schlüssel verwalten',
|
|
39
|
+
|
|
40
|
+
'billing.heading': 'Abrechnung',
|
|
41
|
+
'billing.plan.label': 'Aktueller Plan',
|
|
42
|
+
'billing.plan.free': 'Kostenlos',
|
|
43
|
+
'billing.status.label': 'Status',
|
|
44
|
+
'billing.seats.label': 'Sitze',
|
|
45
|
+
'billing.card.manage': 'Zahlungsmethode verwalten →',
|
|
46
|
+
'billing.change.title': 'Plan ändern',
|
|
47
|
+
'billing.change.body': 'Upgrade auf Pro. Wir nennen den genauen anteiligen Betrag vor der Abbuchung.',
|
|
48
|
+
'billing.change.preview': 'Upgrade ansehen',
|
|
49
|
+
'billing.change.quote': 'Diese Änderung kostet heute etwa {amount}.',
|
|
50
|
+
'billing.change.confirm': 'Upgrade bestätigen',
|
|
51
|
+
'billing.change.done': 'Dein Plan wurde aktualisiert.',
|
|
52
|
+
'billing.pending': 'Wird ausgeführt…',
|
|
53
|
+
|
|
54
|
+
'invoices.heading': 'Rechnungen',
|
|
55
|
+
'invoices.empty': 'Noch keine Rechnungen.',
|
|
56
|
+
'invoices.view': 'Ansehen',
|
|
57
|
+
'invoices.pdf': 'PDF',
|
|
58
|
+
|
|
59
|
+
'apiKeys.heading': 'API-Schlüssel',
|
|
60
|
+
'apiKeys.loading': 'Wird geladen…',
|
|
61
|
+
'apiKeys.notEnabled': 'API-Schlüssel sind für diese API nicht aktiviert. Setze `apiKeys: true` in der app.config.ts der API, um die eingebaute Schlüsselverwaltung zu aktivieren.',
|
|
62
|
+
'apiKeys.noAccess': 'Dein Konto darf keine API-Schlüssel verwalten. Dafür ist ein Admin-Scope nötig.',
|
|
63
|
+
'apiKeys.error': 'API-Schlüssel konnten nicht geladen werden. Bitte erneut versuchen.',
|
|
64
|
+
'apiKeys.name.placeholder': 'Schlüsselname (z. B. CI-Deploy)',
|
|
65
|
+
'apiKeys.issue': 'Schlüssel erstellen',
|
|
66
|
+
'apiKeys.minted.title': 'Kopiere deinen Schlüssel jetzt',
|
|
67
|
+
'apiKeys.minted.body': 'Der vollständige Schlüssel wird nur dieses eine Mal angezeigt. Bewahre ihn sicher auf.',
|
|
68
|
+
'apiKeys.empty': 'Noch keine Schlüssel — erstelle oben deinen ersten.',
|
|
69
|
+
'apiKeys.revoke': 'Widerrufen',
|
|
70
|
+
'apiKeys.revoked': 'widerrufen',
|
|
71
|
+
|
|
72
|
+
'profile.heading': 'Profil',
|
|
73
|
+
'profile.id': 'Benutzer-ID',
|
|
74
|
+
'profile.tenant': 'Organisation',
|
|
75
|
+
'profile.cp.heading': 'Passwort ändern',
|
|
76
|
+
'profile.cp.email': 'E-Mail deines Kontos',
|
|
77
|
+
'profile.cp.submit': 'Reset-Link senden',
|
|
78
|
+
'profile.cp.sent': 'Falls zu dieser E-Mail ein Konto existiert, ist ein Reset-Link unterwegs.',
|
|
79
|
+
'profile.signOut': 'Abmelden',
|
|
80
|
+
|
|
81
|
+
'error.heading': 'Etwas ist schiefgelaufen',
|
|
82
|
+
'error.retry': 'Erneut versuchen',
|
|
83
|
+
'notFound.heading': 'Nicht gefunden',
|
|
84
|
+
'notFound.body': 'Diese Seite existiert nicht.',
|
|
85
|
+
'notFound.back': '← Zurück zur Übersicht',
|
|
86
|
+
})
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Base catalog — the source of truth for this app's UI strings. Every key here
|
|
2
|
+
// MUST be mirrored in `de.ts`; `defineLocale<typeof en>()` enforces that parity.
|
|
3
|
+
import { defineCatalog } from '@voltro/i18n'
|
|
4
|
+
|
|
5
|
+
export default defineCatalog({
|
|
6
|
+
// Browser tab titles.
|
|
7
|
+
'meta.login.title': 'Sign in',
|
|
8
|
+
'meta.overview.title': 'Account',
|
|
9
|
+
'meta.billing.title': 'Billing',
|
|
10
|
+
'meta.invoices.title': 'Invoices',
|
|
11
|
+
'meta.apiKeys.title': 'API keys',
|
|
12
|
+
'meta.profile.title': 'Profile',
|
|
13
|
+
|
|
14
|
+
'lang.label': 'Language',
|
|
15
|
+
|
|
16
|
+
// ---- Auth form (/login) ----
|
|
17
|
+
'auth.signIn.heading': 'Sign in',
|
|
18
|
+
'auth.signUp.heading': 'Create your account',
|
|
19
|
+
'auth.email': 'Email',
|
|
20
|
+
'auth.password': 'Password',
|
|
21
|
+
'auth.signIn.submit': 'Sign in',
|
|
22
|
+
'auth.signUp.submit': 'Create account',
|
|
23
|
+
'auth.toggle.toSignUp': 'No account? Create one',
|
|
24
|
+
'auth.toggle.toSignIn': 'Have an account? Sign in',
|
|
25
|
+
'auth.pending': 'Please wait…',
|
|
26
|
+
'auth.error': 'That didn’t work. Check your details and try again.',
|
|
27
|
+
|
|
28
|
+
// ---- Sidebar nav ----
|
|
29
|
+
'nav.overview': 'Overview',
|
|
30
|
+
'nav.billing': 'Billing',
|
|
31
|
+
'nav.invoices': 'Invoices',
|
|
32
|
+
'nav.apiKeys': 'API keys',
|
|
33
|
+
'nav.profile': 'Profile',
|
|
34
|
+
|
|
35
|
+
// ---- Overview ----
|
|
36
|
+
'overview.heading': 'Account overview',
|
|
37
|
+
'overview.renews': 'Renews',
|
|
38
|
+
'overview.link.billing': 'Plan & payment method',
|
|
39
|
+
'overview.link.invoices': 'Invoice history',
|
|
40
|
+
'overview.link.apiKeys': 'Manage API keys',
|
|
41
|
+
|
|
42
|
+
// ---- Billing ----
|
|
43
|
+
'billing.heading': 'Billing',
|
|
44
|
+
'billing.plan.label': 'Current plan',
|
|
45
|
+
'billing.plan.free': 'Free',
|
|
46
|
+
'billing.status.label': 'Status',
|
|
47
|
+
'billing.seats.label': 'Seats',
|
|
48
|
+
'billing.card.manage': 'Manage payment method →',
|
|
49
|
+
'billing.change.title': 'Change plan',
|
|
50
|
+
'billing.change.body': 'Upgrade to Pro. We’ll quote the exact prorated amount before charging.',
|
|
51
|
+
'billing.change.preview': 'Preview upgrade',
|
|
52
|
+
'billing.change.quote': 'This change settles at about {amount} today.',
|
|
53
|
+
'billing.change.confirm': 'Confirm upgrade',
|
|
54
|
+
'billing.change.done': 'Your plan has been updated.',
|
|
55
|
+
'billing.pending': 'Working…',
|
|
56
|
+
|
|
57
|
+
// ---- Invoices ----
|
|
58
|
+
'invoices.heading': 'Invoices',
|
|
59
|
+
'invoices.empty': 'No invoices yet.',
|
|
60
|
+
'invoices.view': 'View',
|
|
61
|
+
'invoices.pdf': 'PDF',
|
|
62
|
+
|
|
63
|
+
// ---- API keys ----
|
|
64
|
+
'apiKeys.heading': 'API keys',
|
|
65
|
+
'apiKeys.loading': 'Loading…',
|
|
66
|
+
'apiKeys.notEnabled': 'API keys aren’t enabled on this API. Set `apiKeys: true` in the API’s app.config.ts to turn on the built-in key management surface.',
|
|
67
|
+
'apiKeys.noAccess': 'Your account doesn’t have permission to manage API keys. An admin scope is required.',
|
|
68
|
+
'apiKeys.error': 'Couldn’t load API keys. Please try again.',
|
|
69
|
+
'apiKeys.name.placeholder': 'Key name (e.g. CI deploy)',
|
|
70
|
+
'apiKeys.issue': 'Create key',
|
|
71
|
+
'apiKeys.minted.title': 'Copy your key now',
|
|
72
|
+
'apiKeys.minted.body': 'This is the only time the full key is shown. Store it somewhere safe.',
|
|
73
|
+
'apiKeys.empty': 'No keys yet — create your first above.',
|
|
74
|
+
'apiKeys.revoke': 'Revoke',
|
|
75
|
+
'apiKeys.revoked': 'revoked',
|
|
76
|
+
|
|
77
|
+
// ---- Profile ----
|
|
78
|
+
'profile.heading': 'Profile',
|
|
79
|
+
'profile.id': 'User ID',
|
|
80
|
+
'profile.tenant': 'Organization',
|
|
81
|
+
'profile.cp.heading': 'Change password',
|
|
82
|
+
'profile.cp.email': 'Your account email',
|
|
83
|
+
'profile.cp.submit': 'Email me a reset link',
|
|
84
|
+
'profile.cp.sent': 'If that email has an account, a reset link is on its way.',
|
|
85
|
+
'profile.signOut': 'Sign out',
|
|
86
|
+
|
|
87
|
+
// ---- Scoped fallbacks ----
|
|
88
|
+
'error.heading': 'Something went wrong',
|
|
89
|
+
'error.retry': 'Try again',
|
|
90
|
+
'notFound.heading': 'Not found',
|
|
91
|
+
'notFound.body': 'That page doesn’t exist.',
|
|
92
|
+
'notFound.back': '← Back to overview',
|
|
93
|
+
} as const)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Catalog lookup OUTSIDE React — for a page's `meta({ locale })` locale-aware
|
|
2
|
+
// <title>. This app is cookie-i18n (no `/<locale>` URL prefix), so @voltro/web
|
|
3
|
+
// hands `meta` the active locale from the `voltro:locale` cookie. Inside React use
|
|
4
|
+
// `<T>` / `useT()` via the framework's auto-wired <I18nProvider> instead.
|
|
5
|
+
import en from './en'
|
|
6
|
+
import de from './de'
|
|
7
|
+
|
|
8
|
+
// Both catalogs share `en`'s keys (defineLocale enforces the mirror), so a
|
|
9
|
+
// known-key lookup returns `string` — which is what `PageMeta.title` requires
|
|
10
|
+
// under exactOptionalPropertyTypes.
|
|
11
|
+
export type Messages = Record<keyof typeof en, string>
|
|
12
|
+
|
|
13
|
+
export const getCatalog = (locale?: string): Messages =>
|
|
14
|
+
(locale === 'de' ? de : en) as Messages
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// The API-keys page. We stub `fetch` to the framework's `/v1/api-keys` surface
|
|
4
|
+
// and assert the two paths that matter for a template shipped against an api
|
|
5
|
+
// that does NOT enable keys by default: a 404 shows the "enable apiKeys" notice
|
|
6
|
+
// (degrade, don't break), and a 200 renders the key list + issue form.
|
|
7
|
+
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
|
|
9
|
+
import { act, createElement, type ReactNode } from 'react'
|
|
10
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
11
|
+
import { I18nProvider } from '@voltro/i18n'
|
|
12
|
+
import enCatalog from '../../../locales/en'
|
|
13
|
+
|
|
14
|
+
const { default: ApiKeys } = await import('./page')
|
|
15
|
+
|
|
16
|
+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
|
17
|
+
|
|
18
|
+
let container: HTMLDivElement
|
|
19
|
+
let root: Root
|
|
20
|
+
|
|
21
|
+
const render = (node: ReactNode): void => {
|
|
22
|
+
container = document.createElement('div')
|
|
23
|
+
document.body.appendChild(container)
|
|
24
|
+
act(() => {
|
|
25
|
+
root = createRoot(container)
|
|
26
|
+
root.render(
|
|
27
|
+
createElement(I18nProvider, { locale: 'en', messages: enCatalog, defaultLocale: 'en', children: node }),
|
|
28
|
+
)
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const stubFetch = (status: number, body: unknown): void => {
|
|
33
|
+
;(globalThis as { fetch: unknown }).fetch = vi.fn(async () => ({
|
|
34
|
+
ok: status >= 200 && status < 300,
|
|
35
|
+
status,
|
|
36
|
+
json: async () => body,
|
|
37
|
+
})) as unknown as typeof fetch
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const flush = async (): Promise<void> => {
|
|
41
|
+
await act(async () => { await Promise.resolve(); await Promise.resolve() })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
if (root) act(() => root.unmount())
|
|
46
|
+
container?.remove()
|
|
47
|
+
document.body.innerHTML = ''
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
beforeEach(() => { vi.restoreAllMocks() })
|
|
51
|
+
|
|
52
|
+
describe('api-keys — graceful states', () => {
|
|
53
|
+
test('a 404 (apiKeys not enabled) shows the enable notice, not a broken table', async () => {
|
|
54
|
+
stubFetch(404, { error: 'not_found' })
|
|
55
|
+
render(createElement(ApiKeys))
|
|
56
|
+
await flush()
|
|
57
|
+
expect(container.querySelector('.notice')).not.toBeNull()
|
|
58
|
+
expect(container.textContent).toContain('aren’t enabled')
|
|
59
|
+
// No issue form when the surface is unavailable.
|
|
60
|
+
expect(container.querySelector('form')).toBeNull()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('a 200 renders the key list and the issue form', async () => {
|
|
64
|
+
stubFetch(200, { keys: [{ id: 'k1', name: 'CI', keyPrefix: 'voltro_ab', createdAt: 0, lastUsedAt: null, revokedAt: null }] })
|
|
65
|
+
render(createElement(ApiKeys))
|
|
66
|
+
await flush()
|
|
67
|
+
expect(container.querySelector('form')).not.toBeNull()
|
|
68
|
+
expect(container.textContent).toContain('CI')
|
|
69
|
+
expect(container.textContent).toContain('voltro_ab')
|
|
70
|
+
})
|
|
71
|
+
})
|