@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,73 @@
|
|
|
1
|
+
// Team — URL `/dashboard/team`. Lists the tenant's invites live
|
|
2
|
+
// (`invites.list`) and creates them (`invites.create`). Creating an invite
|
|
3
|
+
// spends a `seats` entitlement on the api, so on the free plan (1 seat) the
|
|
4
|
+
// FIRST invite trips the same typed `EntitlementExceeded` — we branch on it and
|
|
5
|
+
// point the user at the Billing page to add a seat.
|
|
6
|
+
import type { ReactNode } from 'react'
|
|
7
|
+
import { useState, type FormEvent } from 'react'
|
|
8
|
+
import type { PageMeta } from '@voltro/web'
|
|
9
|
+
import { useSubscription, useMutation } from '@voltro/client'
|
|
10
|
+
import { T, useT } from '@voltro/i18n'
|
|
11
|
+
import { getCatalog } from '../../../locales/index'
|
|
12
|
+
import { isEntitlementExceeded, type Invite } from '../../../lib/api'
|
|
13
|
+
|
|
14
|
+
export const renderMode = 'ssr' as const
|
|
15
|
+
|
|
16
|
+
export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
|
|
17
|
+
title: getCatalog(locale)['meta.team.title'],
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const STATUS_KEY: Record<string, 'team.status.pending' | 'team.status.accepted' | 'team.status.revoked'> = {
|
|
21
|
+
pending: 'team.status.pending',
|
|
22
|
+
accepted: 'team.status.accepted',
|
|
23
|
+
revoked: 'team.status.revoked',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default function Team(): ReactNode {
|
|
27
|
+
const { data: live } = useSubscription<ReadonlyArray<Invite>>('app', 'invites.list')
|
|
28
|
+
const { mutate, pending } = useMutation<{ email: string }, Invite>('app', 'invites.create')
|
|
29
|
+
const invites = live ?? []
|
|
30
|
+
const [email, setEmail] = useState('')
|
|
31
|
+
const [outOfSeats, setOutOfSeats] = useState(false)
|
|
32
|
+
const placeholder = useT('team.invite.placeholder')
|
|
33
|
+
|
|
34
|
+
const onInvite = (e: FormEvent): void => {
|
|
35
|
+
e.preventDefault()
|
|
36
|
+
const trimmed = email.trim()
|
|
37
|
+
if (trimmed === '') return
|
|
38
|
+
void mutate({ email: trimmed })
|
|
39
|
+
.then(() => { setEmail(''); setOutOfSeats(false) })
|
|
40
|
+
.catch((err: unknown) => { if (isEntitlementExceeded(err)) setOutOfSeats(true) })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<section>
|
|
45
|
+
<h1><T id="team.heading" /></h1>
|
|
46
|
+
|
|
47
|
+
<form className="row" onSubmit={onInvite}>
|
|
48
|
+
<input
|
|
49
|
+
type="email" aria-label={placeholder} placeholder={placeholder}
|
|
50
|
+
value={email} onChange={(e) => setEmail(e.target.value)}
|
|
51
|
+
/>
|
|
52
|
+
<button type="submit" className="btn" disabled={pending}>
|
|
53
|
+
<T id="team.invite.submit" />
|
|
54
|
+
</button>
|
|
55
|
+
</form>
|
|
56
|
+
|
|
57
|
+
{outOfSeats ? <p className="error-text" role="alert"><T id="team.paywall" /></p> : null}
|
|
58
|
+
|
|
59
|
+
{invites.length === 0 ? (
|
|
60
|
+
<p className="empty"><T id="team.empty" /></p>
|
|
61
|
+
) : (
|
|
62
|
+
<ul className="rows" style={{ marginTop: '1.25rem' }}>
|
|
63
|
+
{invites.map((inv) => (
|
|
64
|
+
<li key={inv.id}>
|
|
65
|
+
<span>{inv.email}</span>
|
|
66
|
+
<span className="tag"><T id={STATUS_KEY[inv.status] ?? 'team.status.pending'} /></span>
|
|
67
|
+
</li>
|
|
68
|
+
))}
|
|
69
|
+
</ul>
|
|
70
|
+
)}
|
|
71
|
+
</section>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Root layout — wraps EVERY page (marketing + dashboard). Thin: it loads the
|
|
2
|
+
// stylesheet and lets each AREA layout ((marketing)/layout.tsx,
|
|
3
|
+
// dashboard/layout.tsx) supply its own chrome. Layouts nest, so a dashboard
|
|
4
|
+
// page is wrapped root → dashboard.
|
|
5
|
+
//
|
|
6
|
+
// Don't render <html>/<head>/<body> here — the framework shell owns them.
|
|
7
|
+
import type { ReactNode } from 'react'
|
|
8
|
+
import '../globals.css'
|
|
9
|
+
|
|
10
|
+
export default function RootLayout({ children }: { readonly children: ReactNode }): ReactNode {
|
|
11
|
+
return <>{children}</>
|
|
12
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "frontend-saas",
|
|
3
|
+
"kind": "web",
|
|
4
|
+
"summary": "The SaaS dashboard that closes the loop: a marketing landing, a real sign-up/sign-in form (POST /auth/*), an SSR auth gate that asks the api's session.me and RedirectErrors anonymous visitors to /login, a live projects list (useSubscription) whose create surfaces the typed EntitlementExceeded paywall, and billing (upgrade via billing.startCheckout, seats via billing.changeSeats) + team-invite pages. Pairs with the api-saas-starter api. Bilingual (en/de).",
|
|
5
|
+
"tags": ["web", "frontend", "saas", "auth", "billing", "paywall", "dashboard", "ssr", "useSubscription"]
|
|
6
|
+
}
|
|
@@ -20,7 +20,7 @@ export default {
|
|
|
20
20
|
// <I18nProvider> from these codes and loads the matching
|
|
21
21
|
// `src/locales/<code>.ts` catalog — so `<T id="…" />` / `useT()` /
|
|
22
22
|
// `useLocale()` work app-wide with no manual setup. The <LocaleSwitcher>
|
|
23
|
-
// in the layout writes the `voltro:
|
|
23
|
+
// in the layout writes the `voltro:locale` cookie + reloads. Grow the app to
|
|
24
24
|
// more languages by adding a code here + a `src/locales/<code>.ts`.
|
|
25
25
|
locales: ['en', 'de'] as const,
|
|
26
26
|
defaultLocale: 'en' as const,
|
|
@@ -12,17 +12,17 @@
|
|
|
12
12
|
"typecheck": "tsc --noEmit"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/client": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/i18n": "0.
|
|
19
|
-
"@voltro/ui-shadcn": "0.
|
|
20
|
-
"@voltro/web": "0.
|
|
15
|
+
"@voltro/cli": "0.30.0",
|
|
16
|
+
"@voltro/client": "0.30.0",
|
|
17
|
+
"@voltro/env": "0.30.0",
|
|
18
|
+
"@voltro/i18n": "0.30.0",
|
|
19
|
+
"@voltro/ui-shadcn": "0.30.0",
|
|
20
|
+
"@voltro/web": "0.30.0",
|
|
21
21
|
"react": "^19.0.0",
|
|
22
22
|
"react-dom": "^19.0.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/devtools": "0.
|
|
25
|
+
"@voltro/devtools": "0.30.0",
|
|
26
26
|
"@types/react": "^19.0.0",
|
|
27
27
|
"@types/react-dom": "^19.0.0",
|
|
28
28
|
"typescript": "^6.0.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Catalog lookup OUTSIDE React — for a page's `meta({ locale })` locale-aware
|
|
2
2
|
// <title>. This app is cookie-i18n (no `/<locale>` URL prefix), so @voltro/web
|
|
3
|
-
// hands `meta` the active locale from the `voltro:
|
|
3
|
+
// hands `meta` the active locale from the `voltro:locale` cookie. Inside React,
|
|
4
4
|
// use `<T>` / `useT()` via the framework's auto-wired <I18nProvider> instead.
|
|
5
5
|
|
|
6
6
|
import en from './en'
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// so there is no SSR document to worry about here.
|
|
3
3
|
//
|
|
4
4
|
// The app is bilingual (see `locales` in app.config.ts): <LocaleSwitcher>
|
|
5
|
-
// writes the `voltro:
|
|
5
|
+
// writes the `voltro:locale` cookie + reloads so the framework re-renders in the
|
|
6
6
|
// chosen language.
|
|
7
7
|
|
|
8
8
|
import type { ReactNode } from 'react'
|
|
@@ -15,7 +15,7 @@ import { T } from '@voltro/i18n'
|
|
|
15
15
|
import { getCatalog } from '../locales'
|
|
16
16
|
|
|
17
17
|
// Locale-aware tab title: @voltro/web resolves meta({ locale }) from the
|
|
18
|
-
// active locale (the `voltro:
|
|
18
|
+
// active locale (the `voltro:locale` cookie in this cookie-i18n app).
|
|
19
19
|
export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
|
|
20
20
|
title: getCatalog(locale)['meta.home.title'],
|
|
21
21
|
})
|
|
@@ -24,7 +24,7 @@ export default {
|
|
|
24
24
|
// <I18nProvider> from these codes and loads the matching
|
|
25
25
|
// `src/locales/<code>.ts` catalog — so `<T id="…" />` / `useT()` /
|
|
26
26
|
// `useLocale()` work app-wide (SSR + ISR too) with no manual setup. The
|
|
27
|
-
// <LocaleSwitcher> in the layout writes the `voltro:
|
|
27
|
+
// <LocaleSwitcher> in the layout writes the `voltro:locale` cookie + reloads.
|
|
28
28
|
// Grow the app to more languages by adding a code here + a
|
|
29
29
|
// `src/locales/<code>.ts`.
|
|
30
30
|
locales: ['en', 'de'] as const,
|
|
@@ -12,17 +12,17 @@
|
|
|
12
12
|
"test": "voltro test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/client": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/i18n": "0.
|
|
19
|
-
"@voltro/ui-shadcn": "0.
|
|
20
|
-
"@voltro/web": "0.
|
|
15
|
+
"@voltro/cli": "0.30.0",
|
|
16
|
+
"@voltro/client": "0.30.0",
|
|
17
|
+
"@voltro/env": "0.30.0",
|
|
18
|
+
"@voltro/i18n": "0.30.0",
|
|
19
|
+
"@voltro/ui-shadcn": "0.30.0",
|
|
20
|
+
"@voltro/web": "0.30.0",
|
|
21
21
|
"react": "^19.0.0",
|
|
22
22
|
"react-dom": "^19.0.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/devtools": "0.
|
|
25
|
+
"@voltro/devtools": "0.30.0",
|
|
26
26
|
"@types/react": "^19.0.0",
|
|
27
27
|
"@types/react-dom": "^19.0.0",
|
|
28
28
|
"typescript": "^6.0.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Catalog lookup OUTSIDE React — for a page's `meta({ locale })` locale-aware
|
|
2
2
|
// <title> / <meta description>. This app is cookie-i18n (no `/<locale>` URL
|
|
3
|
-
// prefix), so @voltro/web hands `meta` the active locale from the `voltro:
|
|
3
|
+
// prefix), so @voltro/web hands `meta` the active locale from the `voltro:locale`
|
|
4
4
|
// cookie. Inside React, use `<T>` / `useT()` via the framework's auto-wired
|
|
5
5
|
// <I18nProvider> instead.
|
|
6
6
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// intercepts them) after the first load.
|
|
4
4
|
//
|
|
5
5
|
// The app is bilingual (see `locales` in app.config.ts): nav labels come from
|
|
6
|
-
// the catalog via `<T>`, and <LocaleSwitcher> writes the `voltro:
|
|
6
|
+
// the catalog via `<T>`, and <LocaleSwitcher> writes the `voltro:locale` cookie
|
|
7
7
|
// + reloads so the framework re-renders in the chosen language.
|
|
8
8
|
|
|
9
9
|
import type { ReactNode } from 'react'
|
|
@@ -15,7 +15,7 @@ import { getCatalog } from '../locales'
|
|
|
15
15
|
export const renderMode = 'ssr' as const
|
|
16
16
|
|
|
17
17
|
// Locale-aware tab title + description: @voltro/web resolves meta({ locale })
|
|
18
|
-
// from the active locale (the `voltro:
|
|
18
|
+
// from the active locale (the `voltro:locale` cookie in this cookie-i18n app).
|
|
19
19
|
export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
|
|
20
20
|
title: getCatalog(locale)['meta.ssr.title'],
|
|
21
21
|
description: getCatalog(locale)['meta.ssr.description'],
|
|
@@ -29,7 +29,7 @@ export default {
|
|
|
29
29
|
// <I18nProvider> from these codes and loads the matching
|
|
30
30
|
// `src/locales/<code>.ts` catalog — so `<T id="…" />` / `useT()` /
|
|
31
31
|
// `useLocale()` work app-wide (SSR too) with no manual setup. The
|
|
32
|
-
// <LocaleSwitcher> in the layout writes the `voltro:
|
|
32
|
+
// <LocaleSwitcher> in the layout writes the `voltro:locale` cookie + reloads.
|
|
33
33
|
// Grow the app to more languages by adding a code here + a
|
|
34
34
|
// `src/locales/<code>.ts`.
|
|
35
35
|
locales: ['en', 'de'] as const,
|
|
@@ -12,18 +12,18 @@
|
|
|
12
12
|
"test": "voltro test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@{{projectName}}/api": "0.
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/client": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/i18n": "0.
|
|
20
|
-
"@voltro/ui-shadcn": "0.
|
|
21
|
-
"@voltro/web": "0.
|
|
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
22
|
"react": "^19.0.0",
|
|
23
23
|
"react-dom": "^19.0.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/devtools": "0.
|
|
26
|
+
"@voltro/devtools": "0.30.0",
|
|
27
27
|
"@types/react": "^19.0.0",
|
|
28
28
|
"@types/react-dom": "^19.0.0",
|
|
29
29
|
"typescript": "^6.0.3",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Catalog lookup OUTSIDE React — for the page's `meta({ locale, loaderData })`
|
|
2
2
|
// locale-aware <title> / <meta description>. This app is cookie-i18n (no
|
|
3
3
|
// `/<locale>` URL prefix), so @voltro/web hands `meta` the active locale from
|
|
4
|
-
// the `voltro:
|
|
4
|
+
// the `voltro:locale` cookie. Inside React, use `<T>` / `useT()` via the
|
|
5
5
|
// framework's auto-wired <I18nProvider> instead.
|
|
6
6
|
|
|
7
7
|
import en from './en'
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Don't render <html>/<head>/<body> here — the framework shell owns them.
|
|
3
3
|
//
|
|
4
4
|
// The app is bilingual (see `locales` in app.config.ts): <LocaleSwitcher>
|
|
5
|
-
// writes the `voltro:
|
|
5
|
+
// writes the `voltro:locale` cookie + reloads so the framework re-renders in the
|
|
6
6
|
// chosen language.
|
|
7
7
|
import type { ReactNode } from 'react'
|
|
8
8
|
import { useLocale, useT } from '@voltro/i18n'
|
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
"test": "voltro test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/client": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/i18n": "0.
|
|
19
|
-
"@voltro/web": "0.
|
|
15
|
+
"@voltro/cli": "0.30.0",
|
|
16
|
+
"@voltro/client": "0.30.0",
|
|
17
|
+
"@voltro/env": "0.30.0",
|
|
18
|
+
"@voltro/i18n": "0.30.0",
|
|
19
|
+
"@voltro/web": "0.30.0",
|
|
20
20
|
"react": "^19.0.0",
|
|
21
21
|
"react-dom": "^19.0.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/devtools": "0.
|
|
24
|
+
"@voltro/devtools": "0.30.0",
|
|
25
25
|
"@types/react": "^19.0.0",
|
|
26
26
|
"@types/react-dom": "^19.0.0",
|
|
27
27
|
"typescript": "^6.0.3",
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# {{capProjectName}} {{capAppName}} — public status page
|
|
2
|
+
|
|
3
|
+
A public status page, and a clean **live-`useSubscription`** showcase. Consumes
|
|
4
|
+
the `api-status` backend. Bilingual (en/de), no auth.
|
|
5
|
+
|
|
6
|
+
Scaffold both halves together:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
voltro init my-status --api=api-status --web=frontend-status
|
|
10
|
+
voltro dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## What it does
|
|
14
|
+
|
|
15
|
+
- **SSR first paint.** The index loader pulls the current incidents, components
|
|
16
|
+
and updates over the api's `/rpc` before the HTML is sent, so the page is
|
|
17
|
+
correct + crawlable on first load (no client round-trip).
|
|
18
|
+
- **Live upgrade.** The component then subscribes with `useSubscription('app',
|
|
19
|
+
'incidents.live' | 'components.list' | 'updates.list')`, so when an operator
|
|
20
|
+
declares or resolves an incident on the api, the banner, uptime grid and
|
|
21
|
+
timeline update on every open page within seconds — no polling.
|
|
22
|
+
- **Derived state.** The overall banner and the 90-day uptime grid are computed
|
|
23
|
+
from incident timestamps (`src/lib/status.ts`), not external monitoring — the
|
|
24
|
+
operator controls the narrative. That math is pure and unit-tested.
|
|
25
|
+
|
|
26
|
+
## No auth by design
|
|
27
|
+
|
|
28
|
+
The api leaves the read queries ungated, and this page carries no session gate —
|
|
29
|
+
a status page must stay up when the product (and its login) is down. Operators
|
|
30
|
+
post incidents against the api's operator-gated mutations from a separate
|
|
31
|
+
authenticated surface.
|
|
32
|
+
|
|
33
|
+
## Files
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
src/pages/
|
|
37
|
+
layout.tsx header (brand + language) + stylesheet
|
|
38
|
+
page.tsx the status page — SSR loader + 3 live subscriptions
|
|
39
|
+
page.test.tsx loader + render + the pure status math
|
|
40
|
+
src/lib/status.ts deriveOverall() + uptimeGrid() + uptimePercent() (pure, tested)
|
|
41
|
+
src/locales/{en,de}.ts bilingual catalogs
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Anti-patterns
|
|
45
|
+
|
|
46
|
+
- **Polling for updates.** The whole point is the live subscription — don't add
|
|
47
|
+
a `setInterval`; `useSubscription` pushes deltas.
|
|
48
|
+
- **Gating the reads.** These must work for anonymous visitors. Keep the api's
|
|
49
|
+
read queries ungated; the rbac guard belongs on the write mutations.
|
|
50
|
+
- **A secret in this app's env.** The browser bundle is public — secrets live on
|
|
51
|
+
the api.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Web app for the {{projectName}} project — a PUBLIC status page fed by the
|
|
2
|
+
// sibling `api-status` api.
|
|
3
|
+
//
|
|
4
|
+
// The index page is `renderMode: 'ssr'`: its loader runs server-side per request
|
|
5
|
+
// and pulls the current incidents + components over the api's POST /rpc, so the
|
|
6
|
+
// first paint + the document <title> reflect real state (crawlable, no client
|
|
7
|
+
// round-trip). The component then upgrades to live with `useSubscription`, so a
|
|
8
|
+
// new incident lands on every open page within seconds — the whole point of a
|
|
9
|
+
// status page.
|
|
10
|
+
//
|
|
11
|
+
// No auth: the reads are public (the api leaves them ungated), and the page must
|
|
12
|
+
// stay up even when the rest of the product — and its login — is down.
|
|
13
|
+
import { defineEnv, envVar } from '@voltro/env'
|
|
14
|
+
|
|
15
|
+
// Public, build-time env (baked into the bundle; read via `publicEnv`).
|
|
16
|
+
// Never a secret here — declare secrets on the api.
|
|
17
|
+
export const env = defineEnv({
|
|
18
|
+
VOLTRO_PUBLIC_APP_NAME: envVar.string({ access: 'public', default: '{{capProjectName}}' }),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
type: 'web' as const,
|
|
23
|
+
name: '{{capProjectName}}{{capAppName}}',
|
|
24
|
+
port: {{port}},
|
|
25
|
+
theme: 'system' as const,
|
|
26
|
+
env,
|
|
27
|
+
// Bilingual by default (cookie-driven — no `/<locale>` prefix). The framework
|
|
28
|
+
// auto-wires <I18nProvider> from these codes + the `src/locales/<code>.ts`
|
|
29
|
+
// catalogs, so `<T>` / `useT()` / `useLocale()` work app-wide (SSR too). Add a
|
|
30
|
+
// code + a `src/locales/<code>.ts` to grow to more languages.
|
|
31
|
+
locales: ['en', 'de'] as const,
|
|
32
|
+
defaultLocale: 'en' as const,
|
|
33
|
+
// The api this page reads. The NAME ('app') is the lookup key the loader's
|
|
34
|
+
// `query` + the hooks use: `query('incidents.live', {})`,
|
|
35
|
+
// `useSubscription('app', 'incidents.live')`.
|
|
36
|
+
apis: {
|
|
37
|
+
app: { package: '@{{projectName}}/api' },
|
|
38
|
+
},
|
|
39
|
+
}
|
|
@@ -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,7 @@
|
|
|
1
|
+
// The brand name shown in the status-page header.
|
|
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; to read it typed instead, after one `voltro dev`
|
|
6
|
+
// swap this for `publicEnv.VOLTRO_PUBLIC_APP_NAME` from '@voltro/env/public'.
|
|
7
|
+
export const APP_NAME = '{{capProjectName}}'
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
--border: #e5e7eb;
|
|
7
|
+
--panel: #f9fafb;
|
|
8
|
+
--up: #16a34a;
|
|
9
|
+
--down: #dc2626;
|
|
10
|
+
--warn: #d97706;
|
|
11
|
+
}
|
|
12
|
+
:root.dark {
|
|
13
|
+
--bg: #0a0a0a;
|
|
14
|
+
--fg: #fafafa;
|
|
15
|
+
--muted: #9ca3af;
|
|
16
|
+
--border: #1f2937;
|
|
17
|
+
--panel: #111827;
|
|
18
|
+
--up: #22c55e;
|
|
19
|
+
--down: #f87171;
|
|
20
|
+
--warn: #f59e0b;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
* { box-sizing: border-box; }
|
|
24
|
+
body {
|
|
25
|
+
margin: 0;
|
|
26
|
+
background: var(--bg);
|
|
27
|
+
color: var(--fg);
|
|
28
|
+
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
29
|
+
line-height: 1.55;
|
|
30
|
+
}
|
|
31
|
+
.muted { color: var(--muted); }
|
|
32
|
+
.spacer { flex: 1; }
|
|
33
|
+
.shell { max-width: 46rem; margin: 0 auto; padding: 1.5rem 1.25rem 4rem; }
|
|
34
|
+
.shell header { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem; }
|
|
35
|
+
.brand { font-weight: 700; }
|
|
36
|
+
h2 { font-size: 1rem; margin: 2rem 0 0.75rem; }
|
|
37
|
+
|
|
38
|
+
/* Overall banner */
|
|
39
|
+
.banner { display: flex; align-items: center; gap: 0.6rem; padding: 1rem 1.25rem; border-radius: 12px; border: 1px solid var(--border); }
|
|
40
|
+
.banner .dot { width: 0.7rem; height: 0.7rem; border-radius: 999px; background: var(--muted); }
|
|
41
|
+
.banner--operational { background: color-mix(in oklab, var(--up) 12%, transparent); border-color: var(--up); }
|
|
42
|
+
.banner--operational .dot { background: var(--up); }
|
|
43
|
+
.banner--degraded { background: color-mix(in oklab, var(--warn) 12%, transparent); border-color: var(--warn); }
|
|
44
|
+
.banner--degraded .dot { background: var(--warn); }
|
|
45
|
+
.banner--major_outage { background: color-mix(in oklab, var(--down) 12%, transparent); border-color: var(--down); }
|
|
46
|
+
.banner--major_outage .dot { background: var(--down); }
|
|
47
|
+
|
|
48
|
+
/* Uptime grid */
|
|
49
|
+
.uptime { display: flex; gap: 2px; flex-wrap: wrap; }
|
|
50
|
+
.uptime .day { width: 6px; height: 26px; border-radius: 2px; background: var(--up); }
|
|
51
|
+
.uptime .day.down { background: var(--down); }
|
|
52
|
+
|
|
53
|
+
/* Components */
|
|
54
|
+
.components, .incidents, .timeline { list-style: none; padding: 0; margin: 0; }
|
|
55
|
+
.components li { display: flex; justify-content: space-between; align-items: center; padding: 0.6rem 0; border-bottom: 1px solid var(--border); }
|
|
56
|
+
.pill { font-size: 0.78rem; border: 1px solid var(--border); border-radius: 999px; padding: 0.05rem 0.55rem; color: var(--muted); }
|
|
57
|
+
.pill--operational { color: var(--up); border-color: var(--up); }
|
|
58
|
+
.pill--degraded, .pill--partial_outage { color: var(--warn); border-color: var(--warn); }
|
|
59
|
+
.pill--major_outage { color: var(--down); border-color: var(--down); }
|
|
60
|
+
|
|
61
|
+
/* Incidents */
|
|
62
|
+
.incidents { display: flex; flex-direction: column; gap: 0.75rem; }
|
|
63
|
+
.incident { border: 1px solid var(--border); border-left-width: 3px; border-radius: 10px; padding: 0.9rem 1.1rem; background: var(--panel); }
|
|
64
|
+
.incident.impact--critical { border-left-color: var(--down); }
|
|
65
|
+
.incident.impact--major { border-left-color: var(--warn); }
|
|
66
|
+
.incident.impact--minor { border-left-color: var(--muted); }
|
|
67
|
+
.incident-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
|
|
68
|
+
.timeline { margin-top: 0.6rem; display: flex; flex-direction: column; gap: 0.35rem; border-top: 1px dashed var(--border); padding-top: 0.6rem; }
|
|
69
|
+
.timeline li { display: flex; align-items: baseline; gap: 0.5rem; font-size: 0.9rem; }
|
|
70
|
+
.timeline .ts { color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Ambient declarations.
|
|
2
|
+
//
|
|
3
|
+
// `declare module '*.css'`: TypeScript with `moduleResolution: Bundler`
|
|
4
|
+
// rejects a bare `import './globals.css'` unless the asset module is
|
|
5
|
+
// declared. Vite resolves it at build time; the declaration is type-only.
|
|
6
|
+
declare module '*.css'
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Pure status math — no React, no api. Kept here so it's unit-testable on its
|
|
2
|
+
// own (see page.test.tsx) and reused by the page's banner + uptime grid.
|
|
3
|
+
|
|
4
|
+
export interface Incident {
|
|
5
|
+
readonly id: string
|
|
6
|
+
readonly title: string
|
|
7
|
+
readonly impact: string // 'minor' | 'major' | 'critical'
|
|
8
|
+
readonly status: string // 'investigating' | 'identified' | 'monitoring' | 'resolved'
|
|
9
|
+
readonly startedAt: string | Date
|
|
10
|
+
readonly resolvedAt: string | Date | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Component {
|
|
14
|
+
readonly id: string
|
|
15
|
+
readonly name: string
|
|
16
|
+
readonly status: string // 'operational' | 'degraded' | 'partial_outage' | 'major_outage'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Update {
|
|
20
|
+
readonly id: string
|
|
21
|
+
readonly incidentId: string
|
|
22
|
+
readonly body: string
|
|
23
|
+
readonly status: string
|
|
24
|
+
readonly createdAt: string | Date
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type Overall = 'operational' | 'degraded' | 'major_outage'
|
|
28
|
+
|
|
29
|
+
const isOpen = (i: Incident): boolean => i.status !== 'resolved'
|
|
30
|
+
|
|
31
|
+
// The banner state, from current incidents + component statuses. A live critical
|
|
32
|
+
// incident (or a component in major_outage) is the worst; any other open
|
|
33
|
+
// incident or non-operational component is a partial degradation.
|
|
34
|
+
export const deriveOverall = (
|
|
35
|
+
incidents: ReadonlyArray<Incident>,
|
|
36
|
+
components: ReadonlyArray<Component>,
|
|
37
|
+
): Overall => {
|
|
38
|
+
const open = incidents.filter(isOpen)
|
|
39
|
+
const worstOpen = open.some((i) => i.impact === 'critical')
|
|
40
|
+
const worstComponent = components.some((c) => c.status === 'major_outage')
|
|
41
|
+
if (worstOpen || worstComponent) return 'major_outage'
|
|
42
|
+
if (open.length > 0 || components.some((c) => c.status !== 'operational')) return 'degraded'
|
|
43
|
+
return 'operational'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const DAY_MS = 24 * 60 * 60 * 1000
|
|
47
|
+
const ms = (v: string | Date): number => (v instanceof Date ? v.getTime() : Date.parse(v))
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A 90-day uptime grid (oldest → newest). A day counts as "down" when a
|
|
51
|
+
* major-or-critical incident overlapped it — derived from incident timestamps,
|
|
52
|
+
* NOT external monitoring, so operators control the narrative. `now` is a seam
|
|
53
|
+
* for deterministic tests.
|
|
54
|
+
*/
|
|
55
|
+
export const uptimeGrid = (
|
|
56
|
+
incidents: ReadonlyArray<Incident>,
|
|
57
|
+
now: number = Date.now(),
|
|
58
|
+
days = 90,
|
|
59
|
+
): ReadonlyArray<boolean> => {
|
|
60
|
+
const impactful = incidents.filter((i) => i.impact === 'major' || i.impact === 'critical')
|
|
61
|
+
const todayStart = Math.floor(now / DAY_MS) * DAY_MS
|
|
62
|
+
const grid: boolean[] = []
|
|
63
|
+
for (let d = days - 1; d >= 0; d--) {
|
|
64
|
+
const dayStart = todayStart - d * DAY_MS
|
|
65
|
+
const dayEnd = dayStart + DAY_MS
|
|
66
|
+
const down = impactful.some((i) => {
|
|
67
|
+
const start = ms(i.startedAt)
|
|
68
|
+
const end = i.resolvedAt == null ? now : ms(i.resolvedAt)
|
|
69
|
+
return start < dayEnd && end >= dayStart
|
|
70
|
+
})
|
|
71
|
+
grid.push(!down)
|
|
72
|
+
}
|
|
73
|
+
return grid
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Uptime percentage over the grid window, to one decimal.
|
|
77
|
+
export const uptimePercent = (grid: ReadonlyArray<boolean>): number => {
|
|
78
|
+
if (grid.length === 0) return 100
|
|
79
|
+
const up = grid.filter(Boolean).length
|
|
80
|
+
return Math.round((up / grid.length) * 1000) / 10
|
|
81
|
+
}
|