@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,41 @@
|
|
|
1
|
+
// German catalog. Mirrors every key in `en.ts` — `defineLocale<typeof en>()`
|
|
2
|
+
// fails the build on a missing/misspelled key. Translate prose only; keep ICU
|
|
3
|
+
// placeholders (`{days}`, `{percent}`, `{when}`) identical to en.ts.
|
|
4
|
+
import { defineLocale } from '@voltro/i18n'
|
|
5
|
+
import en from './en'
|
|
6
|
+
|
|
7
|
+
export default defineLocale<typeof en>()({
|
|
8
|
+
'meta.title': 'Status',
|
|
9
|
+
'meta.description': 'Live-Servicestatus, Verfügbarkeit und Störungsverlauf.',
|
|
10
|
+
|
|
11
|
+
'lang.label': 'Sprache',
|
|
12
|
+
'header.status': 'Status',
|
|
13
|
+
|
|
14
|
+
'overall.operational': 'Alle Systeme betriebsbereit',
|
|
15
|
+
'overall.degraded': 'Einige Systeme beeinträchtigt',
|
|
16
|
+
'overall.major_outage': 'Schwerwiegende Störung',
|
|
17
|
+
|
|
18
|
+
'uptime.heading': 'Verfügbarkeit über {days} Tage',
|
|
19
|
+
'uptime.percent': '{percent}% Verfügbarkeit',
|
|
20
|
+
|
|
21
|
+
'components.heading': 'Komponenten',
|
|
22
|
+
'components.empty': 'Noch keine Komponenten konfiguriert.',
|
|
23
|
+
'comp.status.operational': 'Betriebsbereit',
|
|
24
|
+
'comp.status.degraded': 'Beeinträchtigt',
|
|
25
|
+
'comp.status.partial_outage': 'Teilausfall',
|
|
26
|
+
'comp.status.major_outage': 'Schwerer Ausfall',
|
|
27
|
+
|
|
28
|
+
'incidents.heading': 'Störungen',
|
|
29
|
+
'incidents.none': 'Keine Störungen gemeldet.',
|
|
30
|
+
'incident.started': 'Begonnen {when}',
|
|
31
|
+
'incident.ongoing': 'Andauernd',
|
|
32
|
+
'incident.resolved': 'Behoben {when}',
|
|
33
|
+
|
|
34
|
+
'impact.minor': 'Gering',
|
|
35
|
+
'impact.major': 'Erheblich',
|
|
36
|
+
'impact.critical': 'Kritisch',
|
|
37
|
+
'status.investigating': 'Wird untersucht',
|
|
38
|
+
'status.identified': 'Identifiziert',
|
|
39
|
+
'status.monitoring': 'Wird beobachtet',
|
|
40
|
+
'status.resolved': 'Behoben',
|
|
41
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Base catalog — source of truth. Every key MUST be mirrored in `de.ts`
|
|
2
|
+
// (`defineLocale<typeof en>()` enforces parity at build time). ICU `{token}`
|
|
3
|
+
// placeholders are substituted via `<T values={{ … }} />`.
|
|
4
|
+
import { defineCatalog } from '@voltro/i18n'
|
|
5
|
+
|
|
6
|
+
export default defineCatalog({
|
|
7
|
+
'meta.title': 'Status',
|
|
8
|
+
'meta.description': 'Live service status, uptime and incident history.',
|
|
9
|
+
|
|
10
|
+
'lang.label': 'Language',
|
|
11
|
+
'header.status': 'Status',
|
|
12
|
+
|
|
13
|
+
// Overall banner
|
|
14
|
+
'overall.operational': 'All systems operational',
|
|
15
|
+
'overall.degraded': 'Some systems degraded',
|
|
16
|
+
'overall.major_outage': 'Major outage',
|
|
17
|
+
|
|
18
|
+
// Uptime
|
|
19
|
+
'uptime.heading': '{days}-day uptime',
|
|
20
|
+
'uptime.percent': '{percent}% uptime',
|
|
21
|
+
|
|
22
|
+
// Components
|
|
23
|
+
'components.heading': 'Components',
|
|
24
|
+
'components.empty': 'No components configured yet.',
|
|
25
|
+
'comp.status.operational': 'Operational',
|
|
26
|
+
'comp.status.degraded': 'Degraded',
|
|
27
|
+
'comp.status.partial_outage': 'Partial outage',
|
|
28
|
+
'comp.status.major_outage': 'Major outage',
|
|
29
|
+
|
|
30
|
+
// Incidents
|
|
31
|
+
'incidents.heading': 'Incidents',
|
|
32
|
+
'incidents.none': 'No incidents reported.',
|
|
33
|
+
'incident.started': 'Started {when}',
|
|
34
|
+
'incident.ongoing': 'Ongoing',
|
|
35
|
+
'incident.resolved': 'Resolved {when}',
|
|
36
|
+
|
|
37
|
+
// Impact + lifecycle labels
|
|
38
|
+
'impact.minor': 'Minor',
|
|
39
|
+
'impact.major': 'Major',
|
|
40
|
+
'impact.critical': 'Critical',
|
|
41
|
+
'status.investigating': 'Investigating',
|
|
42
|
+
'status.identified': 'Identified',
|
|
43
|
+
'status.monitoring': 'Monitoring',
|
|
44
|
+
'status.resolved': 'Resolved',
|
|
45
|
+
} as const)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Catalog lookup OUTSIDE React — for the page's `meta({ locale })` locale-aware
|
|
2
|
+
// <title>. Cookie-i18n (no URL prefix), so @voltro/web hands `meta` the active
|
|
3
|
+
// locale from the `voltro:locale` cookie. Inside React use `<T>` / `useT()`.
|
|
4
|
+
import en from './en'
|
|
5
|
+
import de from './de'
|
|
6
|
+
|
|
7
|
+
// Both catalogs share `en`'s keys (defineLocale enforces the mirror), so a
|
|
8
|
+
// known-key lookup returns `string` — what `PageMeta.title` requires under
|
|
9
|
+
// exactOptionalPropertyTypes.
|
|
10
|
+
export type Messages = Record<keyof typeof en, string>
|
|
11
|
+
|
|
12
|
+
export const getCatalog = (locale?: string): Messages =>
|
|
13
|
+
(locale === 'de' ? de : en) as Messages
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Root layout — the status-page header (brand + language) and the stylesheet.
|
|
2
|
+
// Don't render <html>/<head>/<body> here — the framework shell owns them.
|
|
3
|
+
import type { ReactNode } from 'react'
|
|
4
|
+
import { T, useLocale, useT } from '@voltro/i18n'
|
|
5
|
+
import { LocaleSwitcher } from '@voltro/ui-shadcn'
|
|
6
|
+
import { APP_NAME } from '../config'
|
|
7
|
+
import '../globals.css'
|
|
8
|
+
|
|
9
|
+
const LOCALES = [
|
|
10
|
+
{ code: 'en', label: 'English' },
|
|
11
|
+
{ code: 'de', label: 'Deutsch' },
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
export default function Layout({ children }: { readonly children: ReactNode }): ReactNode {
|
|
15
|
+
const locale = useLocale()
|
|
16
|
+
const langLabel = useT('lang.label')
|
|
17
|
+
return (
|
|
18
|
+
<div className="shell">
|
|
19
|
+
<header>
|
|
20
|
+
<span className="brand">{APP_NAME} <span className="muted"><T id="header.status" /></span></span>
|
|
21
|
+
<span className="spacer" />
|
|
22
|
+
<LocaleSwitcher locales={LOCALES} current={locale} ariaLabel={langLabel} />
|
|
23
|
+
</header>
|
|
24
|
+
{children}
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// The status page. We mock `@voltro/web` (the loader hook) + `@voltro/client`
|
|
4
|
+
// (the three subscriptions, keyed by tag) and assert: the loader drains the api,
|
|
5
|
+
// the SSR snapshot renders the banner/components/incidents, live subscription
|
|
6
|
+
// data overrides the snapshot, and the pure status math (banner + uptime) is
|
|
7
|
+
// correct on its own.
|
|
8
|
+
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
|
|
10
|
+
import { act, createElement, type ReactNode } from 'react'
|
|
11
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
12
|
+
import { I18nProvider } from '@voltro/i18n'
|
|
13
|
+
import enCatalog from '../locales/en'
|
|
14
|
+
import { deriveOverall, uptimeGrid, uptimePercent, type Incident } from '../lib/status'
|
|
15
|
+
|
|
16
|
+
const loaderData = vi.fn<() => unknown>()
|
|
17
|
+
const subs = new Map<string, unknown>()
|
|
18
|
+
vi.mock('@voltro/web', () => ({ useLoaderData: () => loaderData() }))
|
|
19
|
+
vi.mock('@voltro/client', () => ({ useSubscription: (_api: string, tag: string) => ({ data: subs.get(tag) }) }))
|
|
20
|
+
|
|
21
|
+
const { default: StatusPage, meta, loader, renderMode } = await import('./page')
|
|
22
|
+
|
|
23
|
+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
|
24
|
+
|
|
25
|
+
let container: HTMLDivElement
|
|
26
|
+
let root: Root
|
|
27
|
+
|
|
28
|
+
const render = (node: ReactNode): void => {
|
|
29
|
+
container = document.createElement('div')
|
|
30
|
+
document.body.appendChild(container)
|
|
31
|
+
act(() => {
|
|
32
|
+
root = createRoot(container)
|
|
33
|
+
root.render(
|
|
34
|
+
createElement(I18nProvider, { locale: 'en', messages: enCatalog, defaultLocale: 'en', children: node }),
|
|
35
|
+
)
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const emptyData = { incidents: [], components: [], updates: [] }
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
loaderData.mockReset()
|
|
43
|
+
loaderData.mockReturnValue(emptyData)
|
|
44
|
+
subs.clear()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
if (root) act(() => root.unmount())
|
|
49
|
+
container?.remove()
|
|
50
|
+
document.body.innerHTML = ''
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('status page — config + loader', () => {
|
|
54
|
+
test('is an SSR page titled Status', () => {
|
|
55
|
+
expect(renderMode).toBe('ssr')
|
|
56
|
+
expect(meta({ locale: 'en' }).title).toBe('Status')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('the loader drains all three feeds from the api', async () => {
|
|
60
|
+
const query = vi.fn(async (tag: string) =>
|
|
61
|
+
tag === 'incidents.live' ? [{ id: 'inc_1' }]
|
|
62
|
+
: tag === 'components.list' ? [{ id: 'cmp_1' }]
|
|
63
|
+
: [{ id: 'iu_1' }])
|
|
64
|
+
const data = await loader({ query } as never)
|
|
65
|
+
expect(query).toHaveBeenCalledWith('incidents.live', {})
|
|
66
|
+
expect(data.incidents).toHaveLength(1)
|
|
67
|
+
expect(data.components).toHaveLength(1)
|
|
68
|
+
expect(data.updates).toHaveLength(1)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('returns empty feeds on a client navigation (no server query fn)', async () => {
|
|
72
|
+
const data = await loader({ query: undefined } as never)
|
|
73
|
+
expect(data).toEqual(emptyData)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('status page — render', () => {
|
|
78
|
+
test('shows the all-operational banner with no incidents', () => {
|
|
79
|
+
render(createElement(StatusPage))
|
|
80
|
+
expect(container.querySelector('.banner--operational')).not.toBeNull()
|
|
81
|
+
expect(container.textContent).toContain('All systems operational')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('renders a live incident over the (empty) SSR snapshot', () => {
|
|
85
|
+
subs.set('incidents.live', [
|
|
86
|
+
{ id: 'inc_9', title: 'API degraded', impact: 'major', status: 'investigating', startedAt: '2026-08-01T00:00:00Z', resolvedAt: null },
|
|
87
|
+
])
|
|
88
|
+
subs.set('updates.list', [
|
|
89
|
+
{ id: 'iu_1', incidentId: 'inc_9', body: 'Looking into it', status: 'investigating', createdAt: '2026-08-01T00:05:00Z' },
|
|
90
|
+
])
|
|
91
|
+
render(createElement(StatusPage))
|
|
92
|
+
expect(container.querySelector('.banner--degraded')).not.toBeNull()
|
|
93
|
+
expect(container.textContent).toContain('API degraded')
|
|
94
|
+
expect(container.querySelector('.timeline')).not.toBeNull()
|
|
95
|
+
expect(container.textContent).toContain('Looking into it')
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('status math (pure)', () => {
|
|
100
|
+
const inc = (over: Partial<Incident>): Incident => ({
|
|
101
|
+
id: 'i', title: 't', impact: 'minor', status: 'investigating',
|
|
102
|
+
startedAt: '2026-08-01T00:00:00Z', resolvedAt: null, ...over,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('deriveOverall escalates with impact and clears when resolved', () => {
|
|
106
|
+
expect(deriveOverall([], [])).toBe('operational')
|
|
107
|
+
expect(deriveOverall([inc({ impact: 'major' })], [])).toBe('degraded')
|
|
108
|
+
expect(deriveOverall([inc({ impact: 'critical' })], [])).toBe('major_outage')
|
|
109
|
+
expect(deriveOverall([inc({ impact: 'critical', status: 'resolved', resolvedAt: '2026-08-02T00:00:00Z' })], [])).toBe('operational')
|
|
110
|
+
expect(deriveOverall([], [{ id: 'c', name: 'API', status: 'major_outage' }])).toBe('major_outage')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('uptimeGrid marks a day down only for a major/critical incident overlapping it', () => {
|
|
114
|
+
const now = Date.parse('2026-08-10T12:00:00Z')
|
|
115
|
+
const minorOnly = uptimeGrid([inc({ impact: 'minor', startedAt: '2026-08-09T00:00:00Z', resolvedAt: '2026-08-09T06:00:00Z' })], now, 90)
|
|
116
|
+
expect(minorOnly.every(Boolean)).toBe(true) // minor never dings uptime
|
|
117
|
+
expect(uptimePercent(minorOnly)).toBe(100)
|
|
118
|
+
|
|
119
|
+
const majorDay = uptimeGrid([inc({ impact: 'critical', startedAt: '2026-08-09T00:00:00Z', resolvedAt: '2026-08-09T06:00:00Z' })], now, 90)
|
|
120
|
+
expect(majorDay.filter((up) => !up)).toHaveLength(1) // exactly one down day
|
|
121
|
+
expect(uptimePercent(majorDay)).toBeLessThan(100)
|
|
122
|
+
})
|
|
123
|
+
})
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// `/` — the public status page. `renderMode: 'ssr'`, fed by the sibling api:
|
|
2
|
+
// the loader bakes the current incidents + components + updates into the first
|
|
3
|
+
// paint (crawlable), then the component upgrades each to live with
|
|
4
|
+
// `useSubscription`, so a newly-declared or resolved incident lands on every
|
|
5
|
+
// open page within seconds — no polling.
|
|
6
|
+
import type { ReactNode } from 'react'
|
|
7
|
+
import { useLoaderData, type LoaderFn, type PageMeta } from '@voltro/web'
|
|
8
|
+
import { useSubscription } from '@voltro/client'
|
|
9
|
+
import { T } from '@voltro/i18n'
|
|
10
|
+
import { getCatalog } from '../locales'
|
|
11
|
+
import {
|
|
12
|
+
deriveOverall, uptimeGrid, uptimePercent,
|
|
13
|
+
type Incident, type Component, type Update,
|
|
14
|
+
} from '../lib/status'
|
|
15
|
+
|
|
16
|
+
interface PageData {
|
|
17
|
+
readonly incidents: ReadonlyArray<Incident>
|
|
18
|
+
readonly components: ReadonlyArray<Component>
|
|
19
|
+
readonly updates: ReadonlyArray<Update>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const renderMode = 'ssr' as const
|
|
23
|
+
|
|
24
|
+
// `query` is present ONLY server-side. It invokes the api's rpc over POST /rpc;
|
|
25
|
+
// a streaming query is drained to its first snapshot. On a client navigation
|
|
26
|
+
// `query` is undefined and we start empty — the subscriptions fill in.
|
|
27
|
+
export const loader: LoaderFn<PageData> = async ({ query }) => ({
|
|
28
|
+
incidents: query ? await query<ReadonlyArray<Incident>>('incidents.live', {}) : [],
|
|
29
|
+
components: query ? await query<ReadonlyArray<Component>>('components.list', {}) : [],
|
|
30
|
+
updates: query ? await query<ReadonlyArray<Update>>('updates.list', {}) : [],
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
|
|
34
|
+
title: getCatalog(locale)['meta.title'],
|
|
35
|
+
description: getCatalog(locale)['meta.description'],
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const fmtDate = (v: string | Date | null): string =>
|
|
39
|
+
v == null ? '' : new Date(v).toLocaleString()
|
|
40
|
+
|
|
41
|
+
export default function StatusPage(): ReactNode {
|
|
42
|
+
const initial = useLoaderData<PageData>()
|
|
43
|
+
const { data: iLive } = useSubscription<ReadonlyArray<Incident>>('app', 'incidents.live')
|
|
44
|
+
const { data: cLive } = useSubscription<ReadonlyArray<Component>>('app', 'components.list')
|
|
45
|
+
const { data: uLive } = useSubscription<ReadonlyArray<Update>>('app', 'updates.list')
|
|
46
|
+
|
|
47
|
+
const incidents = iLive ?? initial.incidents
|
|
48
|
+
const components = cLive ?? initial.components
|
|
49
|
+
const updates = uLive ?? initial.updates
|
|
50
|
+
|
|
51
|
+
const overall = deriveOverall(incidents, components)
|
|
52
|
+
const grid = uptimeGrid(incidents)
|
|
53
|
+
const pct = uptimePercent(grid)
|
|
54
|
+
|
|
55
|
+
const updatesFor = (incidentId: string): ReadonlyArray<Update> =>
|
|
56
|
+
updates
|
|
57
|
+
.filter((u) => u.incidentId === incidentId)
|
|
58
|
+
.slice()
|
|
59
|
+
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<main>
|
|
63
|
+
{/* Overall banner */}
|
|
64
|
+
<div className={`banner banner--${overall}`} role="status">
|
|
65
|
+
<span className="dot" aria-hidden="true" />
|
|
66
|
+
<strong><T id={`overall.${overall}`} /></strong>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{/* Uptime */}
|
|
70
|
+
<section>
|
|
71
|
+
<h2><T id="uptime.heading" values={{ days: String(grid.length) }} /></h2>
|
|
72
|
+
<div className="uptime" aria-hidden="true">
|
|
73
|
+
{grid.map((up, idx) => (
|
|
74
|
+
<span key={idx} className={up ? 'day up' : 'day down'} />
|
|
75
|
+
))}
|
|
76
|
+
</div>
|
|
77
|
+
<p className="muted"><T id="uptime.percent" values={{ percent: String(pct) }} /></p>
|
|
78
|
+
</section>
|
|
79
|
+
|
|
80
|
+
{/* Components */}
|
|
81
|
+
<section>
|
|
82
|
+
<h2><T id="components.heading" /></h2>
|
|
83
|
+
{components.length === 0 ? (
|
|
84
|
+
<p className="muted"><T id="components.empty" /></p>
|
|
85
|
+
) : (
|
|
86
|
+
<ul className="components">
|
|
87
|
+
{components.map((c) => (
|
|
88
|
+
<li key={c.id}>
|
|
89
|
+
<span>{c.name}</span>
|
|
90
|
+
<span className={`pill pill--${c.status}`}><T id={`comp.status.${c.status}`} /></span>
|
|
91
|
+
</li>
|
|
92
|
+
))}
|
|
93
|
+
</ul>
|
|
94
|
+
)}
|
|
95
|
+
</section>
|
|
96
|
+
|
|
97
|
+
{/* Incidents */}
|
|
98
|
+
<section>
|
|
99
|
+
<h2><T id="incidents.heading" /></h2>
|
|
100
|
+
{incidents.length === 0 ? (
|
|
101
|
+
<p className="muted"><T id="incidents.none" /></p>
|
|
102
|
+
) : (
|
|
103
|
+
<ul className="incidents">
|
|
104
|
+
{incidents.map((inc) => (
|
|
105
|
+
<li key={inc.id} className={`incident impact--${inc.impact}`}>
|
|
106
|
+
<div className="incident-head">
|
|
107
|
+
<strong>{inc.title}</strong>
|
|
108
|
+
<span className="pill"><T id={`impact.${inc.impact}`} /></span>
|
|
109
|
+
<span className="pill"><T id={`status.${inc.status}`} /></span>
|
|
110
|
+
</div>
|
|
111
|
+
<p className="muted">
|
|
112
|
+
<T id="incident.started" values={{ when: fmtDate(inc.startedAt) }} />
|
|
113
|
+
{' · '}
|
|
114
|
+
{inc.resolvedAt == null
|
|
115
|
+
? <T id="incident.ongoing" />
|
|
116
|
+
: <T id="incident.resolved" values={{ when: fmtDate(inc.resolvedAt) }} />}
|
|
117
|
+
</p>
|
|
118
|
+
{updatesFor(inc.id).length > 0 ? (
|
|
119
|
+
<ul className="timeline">
|
|
120
|
+
{updatesFor(inc.id).map((u) => (
|
|
121
|
+
<li key={u.id}>
|
|
122
|
+
<span className="ts">{fmtDate(u.createdAt)}</span>
|
|
123
|
+
<span className="pill"><T id={`status.${u.status}`} /></span>
|
|
124
|
+
<span>{u.body}</span>
|
|
125
|
+
</li>
|
|
126
|
+
))}
|
|
127
|
+
</ul>
|
|
128
|
+
) : null}
|
|
129
|
+
</li>
|
|
130
|
+
))}
|
|
131
|
+
</ul>
|
|
132
|
+
)}
|
|
133
|
+
</section>
|
|
134
|
+
</main>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "frontend-status",
|
|
3
|
+
"kind": "web",
|
|
4
|
+
"summary": "A public status page — a clean live-useSubscription showcase. An SSR shell bakes the current incidents + components into the first paint (crawlable), then upgrades to live: the banner, 90-day uptime grid and incident timeline update within seconds when an operator posts, with no polling. Pairs with the api-status backend. Bilingual (en/de), no auth (it must stay up when the product is down).",
|
|
5
|
+
"tags": ["web", "frontend", "status-page", "live", "useSubscription", "ssr", "uptime"]
|
|
6
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"jsx": "react-jsx",
|
|
5
|
+
"lib": ["ES2024", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"moduleResolution": "Bundler"
|
|
8
|
+
},
|
|
9
|
+
"include": ["src/**/*"],
|
|
10
|
+
"exclude": ["node_modules", "dist", ".framework"]
|
|
11
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# {{capProjectName}} — mobile (Expo + Voltro)
|
|
2
|
+
|
|
3
|
+
An [Expo](https://expo.dev) (React Native) app that is the **third consumer** of
|
|
4
|
+
your Voltro api — the same typed hooks you use on the web (`useSubscription` /
|
|
5
|
+
`useMutation`), offline-first by default, with typed deep links and device
|
|
6
|
+
registration for push.
|
|
7
|
+
|
|
8
|
+
> **Expo owns the dev loop, not `voltro dev`.** This app is started with
|
|
9
|
+
> `expo start` / `expo run:ios`, not `voltro dev`. The sibling api still runs
|
|
10
|
+
> under `voltro dev`; this app connects to it over the network.
|
|
11
|
+
|
|
12
|
+
## Run it
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
# 1. In one shell — the api (scaffold it with --api=api-backend):
|
|
16
|
+
voltro dev
|
|
17
|
+
|
|
18
|
+
# 2. Point this app at the api. On a simulator/device use your machine's LAN
|
|
19
|
+
# address, NOT localhost:
|
|
20
|
+
export EXPO_PUBLIC_API_URL="http://192.168.x.x:4000"
|
|
21
|
+
|
|
22
|
+
# 3. In another shell — the app:
|
|
23
|
+
pnpm ios # or: pnpm android · pnpm start (Expo Go)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## What's here
|
|
27
|
+
|
|
28
|
+
| Path | What |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `src/lib/deeplinks.ts` | the typed deep-link table + pure resolver (**tested**) |
|
|
31
|
+
| `src/lib/sync.ts` | the background-sync decision layer (**tested**) |
|
|
32
|
+
| `src/lib/notifications.ts` | build a `_voltro_devices` row from a push token (**tested**) |
|
|
33
|
+
| `src/client.ts` | the Voltro client + the **M0 seam** (read its header) |
|
|
34
|
+
| `src/persistence.ts` | RN key-value with the secure/plain split |
|
|
35
|
+
| `src/app/*` | Expo Router screens (list, detail, settings) |
|
|
36
|
+
| `voltro.mobile.ts` | which api(s) this app consumes |
|
|
37
|
+
|
|
38
|
+
## Honest status (read before you expect the loop to work on a device)
|
|
39
|
+
|
|
40
|
+
The **pure logic** in `src/lib/*` is real and unit-tested (`pnpm test`). The
|
|
41
|
+
**client wiring** — building the runtime + subscription cache + rpc client that
|
|
42
|
+
the hooks read — is **not yet implemented for React Native**: on the web it lives
|
|
43
|
+
in `@voltro/web`'s web-coupled boot. Wiring an RN-safe equivalent is Phase **M0**
|
|
44
|
+
in the framework's mobile plan (`plans/open/mobile/`). Until it lands,
|
|
45
|
+
`buildApiHandles()` in `src/client.ts` returns an empty map and the screens show
|
|
46
|
+
their loading state — deliberately, not a crash. `src/client.ts`'s header spells
|
|
47
|
+
out exactly what M0 must build.
|
|
48
|
+
|
|
49
|
+
The tests here run without a simulator. Booting the app on a device (`expo
|
|
50
|
+
run:ios`) is your Expo/EAS CI step — there is no iOS/Android toolchain in the
|
|
51
|
+
framework repo's sandbox.
|
|
52
|
+
|
|
53
|
+
## Tests
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
pnpm test # the pure lib — no simulator needed
|
|
57
|
+
pnpm typecheck # tsc over the app (needs Expo/RN types installed)
|
|
58
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Expo app config for the {{projectName}} mobile app.
|
|
2
|
+
//
|
|
3
|
+
// NOTE: this is EXPO's config, NOT Voltro's `type: 'web'|'api'` app.config. A
|
|
4
|
+
// mobile app is not run by `voltro dev` — Expo owns Metro and the simulator
|
|
5
|
+
// (`expo start` / `expo run:ios`). The sibling api this app consumes still runs
|
|
6
|
+
// under `voltro dev`; the app connects to it over HTTP/WebSocket via
|
|
7
|
+
// `EXPO_PUBLIC_API_URL` (see src/client.ts).
|
|
8
|
+
//
|
|
9
|
+
// `scheme` is the custom URL scheme that makes deep links open THIS app
|
|
10
|
+
// (`{{appName}}://orders/42`). Keep it in sync with the deep-link table in
|
|
11
|
+
// src/lib/deeplinks.ts — `voltro mobile doctor` (planned) checks this.
|
|
12
|
+
import type { ExpoConfig } from 'expo/config'
|
|
13
|
+
|
|
14
|
+
const config: ExpoConfig = {
|
|
15
|
+
name: '{{capProjectName}}{{capAppName}}',
|
|
16
|
+
slug: '{{projectName}}-{{appName}}',
|
|
17
|
+
scheme: '{{appName}}',
|
|
18
|
+
version: '0.0.0',
|
|
19
|
+
orientation: 'portrait',
|
|
20
|
+
userInterfaceStyle: 'automatic',
|
|
21
|
+
newArchEnabled: true,
|
|
22
|
+
ios: { supportsTablet: true, bundleIdentifier: 'com.{{projectName}}.{{appName}}' },
|
|
23
|
+
android: { package: 'com.{{projectName}}.{{appName}}' },
|
|
24
|
+
plugins: [
|
|
25
|
+
'expo-router',
|
|
26
|
+
'expo-secure-store',
|
|
27
|
+
// Add 'expo-notifications' here once you wire the push pipeline
|
|
28
|
+
// (see plans/open/mobile/02-push-pipeline.md).
|
|
29
|
+
],
|
|
30
|
+
experiments: { typedRoutes: true },
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default config
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Metro config — the ONLY Voltro-specific wrinkle in a monorepo: watch the
|
|
2
|
+
// workspace root so `@voltro/*` (and the sibling api's generated types) resolve
|
|
3
|
+
// from the repo, not just this app's node_modules. In a standalone (non-
|
|
4
|
+
// monorepo) app you can delete this file; Expo's default config is enough.
|
|
5
|
+
const { getDefaultConfig } = require('expo/metro-config')
|
|
6
|
+
const path = require('node:path')
|
|
7
|
+
|
|
8
|
+
const projectRoot = __dirname
|
|
9
|
+
const workspaceRoot = path.resolve(projectRoot, '../../..')
|
|
10
|
+
|
|
11
|
+
const config = getDefaultConfig(projectRoot)
|
|
12
|
+
config.watchFolders = [workspaceRoot]
|
|
13
|
+
config.resolver.nodeModulesPaths = [
|
|
14
|
+
path.resolve(projectRoot, 'node_modules'),
|
|
15
|
+
path.resolve(workspaceRoot, 'node_modules'),
|
|
16
|
+
]
|
|
17
|
+
// Prefer a single physical React across the workspace (RN + web share one).
|
|
18
|
+
config.resolver.disableHierarchicalLookup = true
|
|
19
|
+
|
|
20
|
+
module.exports = config
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{projectName}}/{{appName}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"main": "expo-router/entry",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "expo start",
|
|
8
|
+
"ios": "expo run:ios",
|
|
9
|
+
"android": "expo run:android",
|
|
10
|
+
"web": "expo start --web",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"typecheck": "tsc --noEmit"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@voltro/client": "0.30.0",
|
|
16
|
+
"@voltro/react-native": "0.30.0",
|
|
17
|
+
"expo": "^52.0.0",
|
|
18
|
+
"expo-constants": "~17.0.0",
|
|
19
|
+
"expo-linking": "~7.0.0",
|
|
20
|
+
"expo-router": "~4.0.0",
|
|
21
|
+
"expo-secure-store": "~14.0.0",
|
|
22
|
+
"expo-status-bar": "~2.0.0",
|
|
23
|
+
"@react-native-async-storage/async-storage": "1.23.1",
|
|
24
|
+
"@react-native-community/netinfo": "11.4.1",
|
|
25
|
+
"react": "^19.0.0",
|
|
26
|
+
"react-native": "0.76.5"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/react": "^19.0.0",
|
|
30
|
+
"typescript": "^6.0.3",
|
|
31
|
+
"vitest": "^4.1.10"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Root layout — wires the Voltro client provider, deep-link routing, and a
|
|
2
|
+
// connection banner around the whole app.
|
|
3
|
+
//
|
|
4
|
+
// NOTE: `buildApiHandles()` is the M0 seam (see ../client.ts). Until the RN boot
|
|
5
|
+
// helper lands it returns an empty map, so the hooks render their loading state
|
|
6
|
+
// rather than live data — deliberately honest, not a crash.
|
|
7
|
+
import { useEffect, useState } from 'react'
|
|
8
|
+
import { Stack, useRouter } from 'expo-router'
|
|
9
|
+
import * as Linking from 'expo-linking'
|
|
10
|
+
import { StatusBar } from 'expo-status-bar'
|
|
11
|
+
import { FrameworkRuntimesProvider, type ApiHandle } from '@voltro/client'
|
|
12
|
+
import { useMobileConnectionStatus } from '@voltro/react-native'
|
|
13
|
+
import { Text, View } from 'react-native'
|
|
14
|
+
import { buildApiHandles } from '../client'
|
|
15
|
+
import { handleDeepLink, makeDeepLinks } from '../lib/deeplinks'
|
|
16
|
+
|
|
17
|
+
function ConnectionBanner() {
|
|
18
|
+
const { status } = useMobileConnectionStatus()
|
|
19
|
+
if (status === 'connected') return null
|
|
20
|
+
return (
|
|
21
|
+
<View style={{ backgroundColor: status === 'offline' ? '#b91c1c' : '#b45309', padding: 8 }}>
|
|
22
|
+
<Text style={{ color: 'white', textAlign: 'center' }}>
|
|
23
|
+
{status === 'offline' ? 'Offline — changes will sync when you reconnect' : 'Reconnecting…'}
|
|
24
|
+
</Text>
|
|
25
|
+
</View>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default function RootLayout() {
|
|
30
|
+
// Build the api handle map once on mount (async — it opens the WebSocket via
|
|
31
|
+
// @voltro/client's buildApiRuntime). Empty until it resolves; the hooks render
|
|
32
|
+
// their loading state meanwhile.
|
|
33
|
+
const [apis, setApis] = useState<ReadonlyMap<string, ApiHandle>>(() => new Map())
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
let live = true
|
|
36
|
+
void buildApiHandles().then((map) => { if (live) setApis(map) })
|
|
37
|
+
return () => { live = false }
|
|
38
|
+
}, [])
|
|
39
|
+
const router = useRouter()
|
|
40
|
+
|
|
41
|
+
// Route inbound URLs (universal links, custom scheme, push taps) through the
|
|
42
|
+
// ONE typed link table — the same `matchFirstDeepLink` path push will use.
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const links = makeDeepLinks((href) => router.push(href as never))
|
|
45
|
+
const sub = Linking.addEventListener('url', ({ url }) => {
|
|
46
|
+
handleDeepLink(links, url)
|
|
47
|
+
})
|
|
48
|
+
void Linking.getInitialURL().then((url) => {
|
|
49
|
+
if (url) handleDeepLink(links, url)
|
|
50
|
+
})
|
|
51
|
+
return () => sub.remove()
|
|
52
|
+
}, [router])
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<FrameworkRuntimesProvider apis={apis}>
|
|
56
|
+
<StatusBar style="auto" />
|
|
57
|
+
<ConnectionBanner />
|
|
58
|
+
<Stack screenOptions={{ headerTitle: '{{capProjectName}}' }} />
|
|
59
|
+
</FrameworkRuntimesProvider>
|
|
60
|
+
)
|
|
61
|
+
}
|