@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.
Files changed (354) hide show
  1. package/CHANGELOG.md +655 -0
  2. package/THIRD-PARTY-NOTICES.md +90 -4
  3. package/dist/{apiBuild-eUM32r1u.js → apiBuild-BceikpQf.js} +3 -3
  4. package/dist/apiBuild-CxvR1Dwt.js +2 -0
  5. package/dist/bin.js +3 -3
  6. package/dist/checkCommand-BMr-X48w.js +2427 -0
  7. package/dist/checkCommand-BOk9y264.js +2 -0
  8. package/dist/cliRuntime-Oh517vCV.js +96 -0
  9. package/dist/{commands-CSkrUI1h.js → commands-D4Y1-Nlu.js} +8575 -9366
  10. package/dist/{dbCommand-CpYgmSw4.js → dbCommand-C7hbl5pk.js} +345 -266
  11. package/dist/dbCommand-D8nhyppR.js +2 -0
  12. package/dist/{dev-DlBWWnJQ.js → dev-Cr-teYsz.js} +1 -1
  13. package/dist/{dev-CEZwJhmb.js → dev-DmnkH-wr.js} +2939 -1832
  14. package/dist/fileConventions-Cof68_BL.js +33 -0
  15. package/dist/{frameworkTableAssembly-BwHU9Euq.js → frameworkTableAssembly-BzAajuUv.js} +115 -99
  16. package/dist/frameworkTableAssembly-DU0aOxjL.js +2 -0
  17. package/dist/index.d.ts +7 -2
  18. package/dist/index.js +1 -1
  19. package/dist/{inspect-CUCCzw2I.js → inspect-BUUjt773.js} +349 -192
  20. package/dist/inspect-mmBuRXmy.js +2 -0
  21. package/dist/{inspectMetrics-BU90mvJN.js → inspectMetrics-BeJdP_3b.js} +1068 -978
  22. package/dist/{manifestBuild-BnzAxp2O.js → manifestBuild-BLrVuSlM.js} +1 -1
  23. package/dist/manifestBuild-Dj8Jjoto.js +2 -0
  24. package/dist/sdkgen-CqpOq_fy.js +582 -0
  25. package/dist/serveCommand-Ciq_wIY8.js +1577 -0
  26. package/dist/serveEntry.js +2 -2
  27. package/dist/{start-BGXIf6zT.js → start-ocqB1seX.js} +283 -270
  28. package/dist/startEntry.js +2 -2
  29. package/package.json +17 -17
  30. package/templates/AGENTS.core.md +50 -0
  31. package/templates/AGENTS.md +54 -2
  32. package/templates/agent-docs/_index.md +4 -2
  33. package/templates/agent-docs/_manifest.json +26 -5
  34. package/templates/agent-docs/ai.md +191 -0
  35. package/templates/agent-docs/cli.md +143 -6
  36. package/templates/agent-docs/configuration.md +32 -0
  37. package/templates/agent-docs/data.md +282 -0
  38. package/templates/agent-docs/database/migrations.md +47 -0
  39. package/templates/agent-docs/database/misc.md +57 -0
  40. package/templates/agent-docs/database/schema.md +3 -1
  41. package/templates/agent-docs/internationalization.md +169 -8
  42. package/templates/agent-docs/local-first-mobile.md +426 -0
  43. package/templates/agent-docs/routing.md +94 -3
  44. package/templates/agent-docs/schema-driven-ui.md +12 -0
  45. package/templates/agent-docs/security.md +17 -0
  46. package/templates/agent-docs/templates/apibackends.md +210 -1
  47. package/templates/agent-docs/templates/appshells.md +144 -1
  48. package/templates/agent-docs/templates/mobile.md +60 -0
  49. package/templates/agent-docs/templates/overview.md +17 -7
  50. package/templates/agent-docs/whats-new.md +238 -158
  51. package/templates/agent-docs/workflows.md +376 -0
  52. package/templates/apps/api-ai/package.json +7 -7
  53. package/templates/apps/api-auth/package.json +8 -8
  54. package/templates/apps/api-backend/package.json +7 -7
  55. package/templates/apps/api-backend-deactivation/package.json +7 -7
  56. package/templates/apps/api-backend-mail/package.json +8 -8
  57. package/templates/apps/api-backend-mariadb/package.json +9 -9
  58. package/templates/apps/api-backend-sqlite/.env.example +19 -0
  59. package/templates/apps/api-backend-sqlite/README.md +38 -0
  60. package/templates/apps/api-backend-sqlite/app.config.ts +35 -0
  61. package/templates/apps/api-backend-sqlite/database/schema.ts +54 -0
  62. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.server.ts +19 -0
  63. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +37 -0
  64. package/templates/apps/api-backend-sqlite/package.json +30 -0
  65. package/templates/apps/api-backend-sqlite/queries/notes.query.server.ts +14 -0
  66. package/templates/apps/api-backend-sqlite/queries/notes.query.ts +20 -0
  67. package/templates/apps/api-backend-sqlite/template.json +6 -0
  68. package/templates/apps/api-backend-sqlite/tests/notes.create.test.ts +50 -0
  69. package/templates/apps/api-backend-sqlite/tsconfig.json +5 -0
  70. package/templates/apps/api-backend-storage/package.json +8 -8
  71. package/templates/apps/api-cms/README.md +104 -0
  72. package/templates/apps/api-cms/actions/content.get.action.server.ts +27 -0
  73. package/templates/apps/api-cms/actions/content.get.action.ts +19 -0
  74. package/templates/apps/api-cms/actions/content.types.action.server.ts +26 -0
  75. package/templates/apps/api-cms/actions/content.types.action.ts +40 -0
  76. package/templates/apps/api-cms/actions/me.action.server.ts +18 -0
  77. package/templates/apps/api-cms/actions/me.action.ts +16 -0
  78. package/templates/apps/api-cms/app.config.ts +61 -0
  79. package/templates/apps/api-cms/content/blogPost.contentType.ts +39 -0
  80. package/templates/apps/api-cms/content/index.ts +18 -0
  81. package/templates/apps/api-cms/content/page.contentType.ts +24 -0
  82. package/templates/apps/api-cms/database/schema.ts +64 -0
  83. package/templates/apps/api-cms/mutations/content.publish.mutation.server.ts +19 -0
  84. package/templates/apps/api-cms/mutations/content.publish.mutation.ts +15 -0
  85. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.server.ts +36 -0
  86. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +32 -0
  87. package/templates/apps/api-cms/mutations/content.unpublish.mutation.server.ts +19 -0
  88. package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +11 -0
  89. package/templates/apps/api-cms/package.json +32 -0
  90. package/templates/apps/api-cms/queries/content.list.query.server.ts +0 -0
  91. package/templates/apps/api-cms/queries/content.list.query.ts +27 -0
  92. package/templates/apps/api-cms/template.json +6 -0
  93. package/templates/apps/api-cms/tests/content.descriptors.test.ts +64 -0
  94. package/templates/apps/api-cms/tests/content.write.test.ts +85 -0
  95. package/templates/apps/api-cms/tsconfig.json +5 -0
  96. package/templates/apps/api-collab/README.md +70 -0
  97. package/templates/apps/api-collab/app.config.ts +28 -0
  98. package/templates/apps/api-collab/database/schema.ts +63 -0
  99. package/templates/apps/api-collab/mutations/documents.create.mutation.server.ts +19 -0
  100. package/templates/apps/api-collab/mutations/documents.create.mutation.ts +36 -0
  101. package/templates/apps/api-collab/mutations/documents.setBody.mutation.server.ts +20 -0
  102. package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +30 -0
  103. package/templates/apps/api-collab/package.json +30 -0
  104. package/templates/apps/api-collab/queries/documents.query.server.ts +16 -0
  105. package/templates/apps/api-collab/queries/documents.query.ts +23 -0
  106. package/templates/apps/api-collab/template.json +6 -0
  107. package/templates/apps/api-collab/tests/documents.setBody.test.ts +89 -0
  108. package/templates/apps/api-collab/tsconfig.json +5 -0
  109. package/templates/apps/api-data-advanced/package.json +8 -8
  110. package/templates/apps/api-durable/package.json +8 -8
  111. package/templates/apps/api-feature-flags/package.json +9 -9
  112. package/templates/apps/api-governance/package.json +8 -8
  113. package/templates/apps/api-kv/package.json +8 -8
  114. package/templates/apps/api-moderation/package.json +8 -8
  115. package/templates/apps/api-observability/package.json +8 -8
  116. package/templates/apps/api-ratelimit/package.json +8 -8
  117. package/templates/apps/api-rbac/package.json +8 -8
  118. package/templates/apps/api-rest/package.json +7 -7
  119. package/templates/apps/api-saas/package.json +11 -11
  120. package/templates/apps/api-saas-starter/README.md +103 -0
  121. package/templates/apps/api-saas-starter/actions/me.action.server.ts +18 -0
  122. package/templates/apps/api-saas-starter/actions/me.action.ts +20 -0
  123. package/templates/apps/api-saas-starter/app.config.ts +87 -0
  124. package/templates/apps/api-saas-starter/database/schema.ts +57 -0
  125. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.server.ts +26 -0
  126. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +18 -0
  127. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.server.ts +29 -0
  128. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +18 -0
  129. package/templates/apps/api-saas-starter/package.json +32 -0
  130. package/templates/apps/api-saas-starter/queries/invites.list.query.server.ts +14 -0
  131. package/templates/apps/api-saas-starter/queries/invites.list.query.ts +17 -0
  132. package/templates/apps/api-saas-starter/queries/projects.list.query.server.ts +14 -0
  133. package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -0
  134. package/templates/apps/api-saas-starter/template.json +6 -0
  135. package/templates/apps/api-saas-starter/tests/projects.create.test.ts +62 -0
  136. package/templates/apps/api-saas-starter/tests/session.test.ts +33 -0
  137. package/templates/apps/api-saas-starter/tsconfig.json +5 -0
  138. package/templates/apps/api-search/package.json +8 -8
  139. package/templates/apps/api-status/README.md +88 -0
  140. package/templates/apps/api-status/app.config.ts +36 -0
  141. package/templates/apps/api-status/authz.ts +33 -0
  142. package/templates/apps/api-status/database/schema.ts +70 -0
  143. package/templates/apps/api-status/mutations/components.create.mutation.server.ts +19 -0
  144. package/templates/apps/api-status/mutations/components.create.mutation.ts +16 -0
  145. package/templates/apps/api-status/mutations/incidents.create.mutation.server.ts +25 -0
  146. package/templates/apps/api-status/mutations/incidents.create.mutation.ts +22 -0
  147. package/templates/apps/api-status/mutations/incidents.resolve.mutation.server.ts +24 -0
  148. package/templates/apps/api-status/mutations/incidents.resolve.mutation.ts +19 -0
  149. package/templates/apps/api-status/mutations/incidents.update.mutation.server.ts +26 -0
  150. package/templates/apps/api-status/mutations/incidents.update.mutation.ts +21 -0
  151. package/templates/apps/api-status/package.json +30 -0
  152. package/templates/apps/api-status/queries/components.list.query.server.ts +14 -0
  153. package/templates/apps/api-status/queries/components.list.query.ts +13 -0
  154. package/templates/apps/api-status/queries/incidents.live.query.server.ts +14 -0
  155. package/templates/apps/api-status/queries/incidents.live.query.ts +20 -0
  156. package/templates/apps/api-status/queries/updates.list.query.server.ts +14 -0
  157. package/templates/apps/api-status/queries/updates.list.query.ts +17 -0
  158. package/templates/apps/api-status/template.json +6 -0
  159. package/templates/apps/api-status/tests/status.test.ts +70 -0
  160. package/templates/apps/api-status/tsconfig.json +5 -0
  161. package/templates/apps/api-versioning/package.json +8 -8
  162. package/templates/apps/api-webhooks/package.json +9 -9
  163. package/templates/apps/changelog/package.json +6 -6
  164. package/templates/apps/edge-functions/package.json +2 -2
  165. package/templates/apps/frontend-admin/app.config.ts +1 -1
  166. package/templates/apps/frontend-admin/package.json +8 -8
  167. package/templates/apps/frontend-admin/src/locales/index.ts +1 -1
  168. package/templates/apps/frontend-app/app.config.ts +1 -1
  169. package/templates/apps/frontend-app/package.json +8 -8
  170. package/templates/apps/frontend-app/src/locales/index.ts +1 -1
  171. package/templates/apps/frontend-app/src/pages/layout.tsx +1 -1
  172. package/templates/apps/frontend-app/src/pages/page.tsx +1 -1
  173. package/templates/apps/frontend-auth/README.md +78 -0
  174. package/templates/apps/frontend-auth/app.config.ts +34 -0
  175. package/templates/apps/frontend-auth/package.json +32 -0
  176. package/templates/apps/frontend-auth/src/components/AuthShell.tsx +35 -0
  177. package/templates/apps/frontend-auth/src/components/PasswordStrength.tsx +33 -0
  178. package/templates/apps/frontend-auth/src/config.ts +11 -0
  179. package/templates/apps/frontend-auth/src/globals.css +105 -0
  180. package/templates/apps/frontend-auth/src/globals.d.ts +6 -0
  181. package/templates/apps/frontend-auth/src/lib/auth.ts +34 -0
  182. package/templates/apps/frontend-auth/src/lib/redirect.test.ts +24 -0
  183. package/templates/apps/frontend-auth/src/lib/redirect.ts +29 -0
  184. package/templates/apps/frontend-auth/src/locales/de.ts +66 -0
  185. package/templates/apps/frontend-auth/src/locales/en.ts +76 -0
  186. package/templates/apps/frontend-auth/src/locales/index.ts +14 -0
  187. package/templates/apps/frontend-auth/src/pages/forgot/page.tsx +51 -0
  188. package/templates/apps/frontend-auth/src/pages/layout.tsx +12 -0
  189. package/templates/apps/frontend-auth/src/pages/login/page.test.tsx +53 -0
  190. package/templates/apps/frontend-auth/src/pages/login/page.tsx +67 -0
  191. package/templates/apps/frontend-auth/src/pages/logout/page.tsx +28 -0
  192. package/templates/apps/frontend-auth/src/pages/magic/page.tsx +51 -0
  193. package/templates/apps/frontend-auth/src/pages/page.tsx +26 -0
  194. package/templates/apps/frontend-auth/src/pages/reset/page.test.tsx +51 -0
  195. package/templates/apps/frontend-auth/src/pages/reset/page.tsx +63 -0
  196. package/templates/apps/frontend-auth/src/pages/signup/page.tsx +60 -0
  197. package/templates/apps/frontend-auth/src/pages/verify/page.tsx +50 -0
  198. package/templates/apps/frontend-auth/template.json +6 -0
  199. package/templates/apps/frontend-auth/tsconfig.json +5 -0
  200. package/templates/apps/frontend-blank/app.config.ts +1 -1
  201. package/templates/apps/frontend-blank/package.json +7 -7
  202. package/templates/apps/frontend-blank/src/locales/index.ts +1 -1
  203. package/templates/apps/frontend-blank/src/pages/layout.tsx +1 -1
  204. package/templates/apps/frontend-blank/src/pages/page.tsx +1 -1
  205. package/templates/apps/frontend-cms/README.md +47 -0
  206. package/templates/apps/frontend-cms/app.config.ts +37 -0
  207. package/templates/apps/frontend-cms/package.json +33 -0
  208. package/templates/apps/frontend-cms/src/config.ts +8 -0
  209. package/templates/apps/frontend-cms/src/globals.css +105 -0
  210. package/templates/apps/frontend-cms/src/globals.d.ts +6 -0
  211. package/templates/apps/frontend-cms/src/lib/api.ts +58 -0
  212. package/templates/apps/frontend-cms/src/locales/de.ts +50 -0
  213. package/templates/apps/frontend-cms/src/locales/en.ts +55 -0
  214. package/templates/apps/frontend-cms/src/locales/index.ts +14 -0
  215. package/templates/apps/frontend-cms/src/pages/(app)/error.tsx +18 -0
  216. package/templates/apps/frontend-cms/src/pages/(app)/layout.test.tsx +31 -0
  217. package/templates/apps/frontend-cms/src/pages/(app)/layout.tsx +62 -0
  218. package/templates/apps/frontend-cms/src/pages/(app)/not-found.tsx +14 -0
  219. package/templates/apps/frontend-cms/src/pages/(app)/page.test.tsx +110 -0
  220. package/templates/apps/frontend-cms/src/pages/(app)/page.tsx +159 -0
  221. package/templates/apps/frontend-cms/src/pages/layout.tsx +12 -0
  222. package/templates/apps/frontend-cms/src/pages/login/page.test.tsx +58 -0
  223. package/templates/apps/frontend-cms/src/pages/login/page.tsx +93 -0
  224. package/templates/apps/frontend-cms/template.json +6 -0
  225. package/templates/apps/frontend-cms/tsconfig.json +5 -0
  226. package/templates/apps/frontend-collab/README.md +80 -0
  227. package/templates/apps/frontend-collab/app.config.ts +38 -0
  228. package/templates/apps/frontend-collab/package.json +33 -0
  229. package/templates/apps/frontend-collab/src/locales/de.ts +29 -0
  230. package/templates/apps/frontend-collab/src/locales/en.ts +31 -0
  231. package/templates/apps/frontend-collab/src/locales/index.ts +15 -0
  232. package/templates/apps/frontend-collab/src/pages/layout.tsx +39 -0
  233. package/templates/apps/frontend-collab/src/pages/page.test.tsx +150 -0
  234. package/templates/apps/frontend-collab/src/pages/page.tsx +186 -0
  235. package/templates/apps/frontend-collab/template.json +6 -0
  236. package/templates/apps/frontend-collab/tsconfig.json +11 -0
  237. package/templates/apps/frontend-contact/package.json +7 -7
  238. package/templates/apps/frontend-dashboard/app.config.ts +1 -1
  239. package/templates/apps/frontend-dashboard/package.json +7 -7
  240. package/templates/apps/frontend-dashboard/src/locales/index.ts +1 -1
  241. package/templates/apps/frontend-docs/package.json +7 -7
  242. package/templates/apps/frontend-i18n/package.json +6 -6
  243. package/templates/apps/frontend-landing/package.json +7 -7
  244. package/templates/apps/frontend-portal/README.md +71 -0
  245. package/templates/apps/frontend-portal/app.config.ts +37 -0
  246. package/templates/apps/frontend-portal/package.json +32 -0
  247. package/templates/apps/frontend-portal/src/config.ts +8 -0
  248. package/templates/apps/frontend-portal/src/globals.css +93 -0
  249. package/templates/apps/frontend-portal/src/globals.d.ts +6 -0
  250. package/templates/apps/frontend-portal/src/lib/api.ts +62 -0
  251. package/templates/apps/frontend-portal/src/locales/de.ts +86 -0
  252. package/templates/apps/frontend-portal/src/locales/en.ts +93 -0
  253. package/templates/apps/frontend-portal/src/locales/index.ts +14 -0
  254. package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.test.tsx +71 -0
  255. package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.tsx +124 -0
  256. package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.test.tsx +86 -0
  257. package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.tsx +97 -0
  258. package/templates/apps/frontend-portal/src/pages/(portal)/error.tsx +19 -0
  259. package/templates/apps/frontend-portal/src/pages/(portal)/invoices/page.tsx +45 -0
  260. package/templates/apps/frontend-portal/src/pages/(portal)/layout.test.tsx +31 -0
  261. package/templates/apps/frontend-portal/src/pages/(portal)/layout.tsx +63 -0
  262. package/templates/apps/frontend-portal/src/pages/(portal)/not-found.tsx +15 -0
  263. package/templates/apps/frontend-portal/src/pages/(portal)/page.tsx +39 -0
  264. package/templates/apps/frontend-portal/src/pages/(portal)/profile/page.tsx +79 -0
  265. package/templates/apps/frontend-portal/src/pages/layout.tsx +12 -0
  266. package/templates/apps/frontend-portal/src/pages/login/page.test.tsx +58 -0
  267. package/templates/apps/frontend-portal/src/pages/login/page.tsx +96 -0
  268. package/templates/apps/frontend-portal/template.json +6 -0
  269. package/templates/apps/frontend-portal/tsconfig.json +5 -0
  270. package/templates/apps/frontend-saas/README.md +73 -0
  271. package/templates/apps/frontend-saas/app.config.ts +46 -0
  272. package/templates/apps/frontend-saas/package.json +32 -0
  273. package/templates/apps/frontend-saas/src/config.ts +8 -0
  274. package/templates/apps/frontend-saas/src/globals.css +85 -0
  275. package/templates/apps/frontend-saas/src/globals.d.ts +6 -0
  276. package/templates/apps/frontend-saas/src/lib/api.ts +47 -0
  277. package/templates/apps/frontend-saas/src/locales/de.ts +72 -0
  278. package/templates/apps/frontend-saas/src/locales/en.ts +81 -0
  279. package/templates/apps/frontend-saas/src/locales/index.ts +14 -0
  280. package/templates/apps/frontend-saas/src/pages/(marketing)/layout.tsx +30 -0
  281. package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.test.tsx +58 -0
  282. package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.tsx +96 -0
  283. package/templates/apps/frontend-saas/src/pages/(marketing)/page.tsx +27 -0
  284. package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +72 -0
  285. package/templates/apps/frontend-saas/src/pages/dashboard/error.tsx +20 -0
  286. package/templates/apps/frontend-saas/src/pages/dashboard/layout.test.tsx +32 -0
  287. package/templates/apps/frontend-saas/src/pages/dashboard/layout.tsx +69 -0
  288. package/templates/apps/frontend-saas/src/pages/dashboard/not-found.tsx +17 -0
  289. package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +113 -0
  290. package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +76 -0
  291. package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +73 -0
  292. package/templates/apps/frontend-saas/src/pages/layout.tsx +12 -0
  293. package/templates/apps/frontend-saas/template.json +6 -0
  294. package/templates/apps/frontend-saas/tsconfig.json +5 -0
  295. package/templates/apps/frontend-spa/app.config.ts +1 -1
  296. package/templates/apps/frontend-spa/package.json +7 -7
  297. package/templates/apps/frontend-spa/src/locales/index.ts +1 -1
  298. package/templates/apps/frontend-spa/src/pages/layout.tsx +1 -1
  299. package/templates/apps/frontend-spa/src/pages/page.tsx +1 -1
  300. package/templates/apps/frontend-ssr/app.config.ts +1 -1
  301. package/templates/apps/frontend-ssr/package.json +7 -7
  302. package/templates/apps/frontend-ssr/src/locales/index.ts +1 -1
  303. package/templates/apps/frontend-ssr/src/pages/layout.tsx +1 -1
  304. package/templates/apps/frontend-ssr/src/pages/page.tsx +1 -1
  305. package/templates/apps/frontend-ssr-api/app.config.ts +1 -1
  306. package/templates/apps/frontend-ssr-api/package.json +8 -8
  307. package/templates/apps/frontend-ssr-api/src/locales/index.ts +1 -1
  308. package/templates/apps/frontend-ssr-api/src/pages/layout.tsx +1 -1
  309. package/templates/apps/frontend-static-blog/package.json +6 -6
  310. package/templates/apps/frontend-status/README.md +51 -0
  311. package/templates/apps/frontend-status/app.config.ts +39 -0
  312. package/templates/apps/frontend-status/package.json +32 -0
  313. package/templates/apps/frontend-status/src/config.ts +7 -0
  314. package/templates/apps/frontend-status/src/globals.css +70 -0
  315. package/templates/apps/frontend-status/src/globals.d.ts +6 -0
  316. package/templates/apps/frontend-status/src/lib/status.ts +81 -0
  317. package/templates/apps/frontend-status/src/locales/de.ts +41 -0
  318. package/templates/apps/frontend-status/src/locales/en.ts +45 -0
  319. package/templates/apps/frontend-status/src/locales/index.ts +13 -0
  320. package/templates/apps/frontend-status/src/pages/layout.tsx +27 -0
  321. package/templates/apps/frontend-status/src/pages/page.test.tsx +123 -0
  322. package/templates/apps/frontend-status/src/pages/page.tsx +136 -0
  323. package/templates/apps/frontend-status/template.json +6 -0
  324. package/templates/apps/frontend-status/tsconfig.json +11 -0
  325. package/templates/apps/mobile-app/README.md +58 -0
  326. package/templates/apps/mobile-app/app.config.ts +33 -0
  327. package/templates/apps/mobile-app/babel.config.js +5 -0
  328. package/templates/apps/mobile-app/metro.config.js +20 -0
  329. package/templates/apps/mobile-app/package.json +33 -0
  330. package/templates/apps/mobile-app/src/app/_layout.tsx +61 -0
  331. package/templates/apps/mobile-app/src/app/index.tsx +62 -0
  332. package/templates/apps/mobile-app/src/app/orders/[id].tsx +19 -0
  333. package/templates/apps/mobile-app/src/app/settings.tsx +51 -0
  334. package/templates/apps/mobile-app/src/client.ts +74 -0
  335. package/templates/apps/mobile-app/src/lib/deeplinks.ts +39 -0
  336. package/templates/apps/mobile-app/src/lib/notifications.ts +31 -0
  337. package/templates/apps/mobile-app/src/lib/sync.ts +29 -0
  338. package/templates/apps/mobile-app/src/persistence.ts +38 -0
  339. package/templates/apps/mobile-app/template.json +6 -0
  340. package/templates/apps/mobile-app/tests/deeplinks.test.ts +32 -0
  341. package/templates/apps/mobile-app/tests/notifications.test.ts +26 -0
  342. package/templates/apps/mobile-app/tests/sync.test.ts +34 -0
  343. package/templates/apps/mobile-app/tsconfig.json +10 -0
  344. package/templates/apps/mobile-app/voltro.mobile.ts +11 -0
  345. package/templates/baselines/compose/docker-compose.prod.yml +15 -0
  346. package/templates/baselines/compose-mariadb/docker-compose.prod.yml +15 -0
  347. package/templates/patches/@effect__cluster@0.60.0.patch +6 -6
  348. package/dist/apiBuild-DgBS9ayv.js +0 -2
  349. package/dist/dbCommand-DvguqlzF.js +0 -2
  350. package/dist/fileConventions-3bffWssN.js +0 -30
  351. package/dist/frameworkTableAssembly-lrjZtk0G.js +0 -2
  352. package/dist/inspect-gt8bq-Tz.js +0 -2
  353. package/dist/manifestBuild-ifczArzr.js +0 -2
  354. 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
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.json",
3
+ "include": ["**/*.ts", "**/*.tsx"],
4
+ "exclude": ["node_modules", "dist"]
5
+ }
@@ -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:lang` cookie + reloads. Grow the app to
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.28.0",
16
- "@voltro/client": "0.28.0",
17
- "@voltro/env": "0.28.0",
18
- "@voltro/i18n": "0.28.0",
19
- "@voltro/ui-shadcn": "0.28.0",
20
- "@voltro/web": "0.28.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.28.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:lang` cookie. Inside React,
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:lang` cookie + reloads so the framework re-renders in the
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:lang` cookie in this cookie-i18n app).
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:lang` cookie + reloads.
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.28.0",
16
- "@voltro/client": "0.28.0",
17
- "@voltro/env": "0.28.0",
18
- "@voltro/i18n": "0.28.0",
19
- "@voltro/ui-shadcn": "0.28.0",
20
- "@voltro/web": "0.28.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.28.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:lang`
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:lang` cookie
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:lang` cookie in this cookie-i18n app).
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:lang` cookie + reloads.
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.28.0",
16
- "@voltro/cli": "0.28.0",
17
- "@voltro/client": "0.28.0",
18
- "@voltro/env": "0.28.0",
19
- "@voltro/i18n": "0.28.0",
20
- "@voltro/ui-shadcn": "0.28.0",
21
- "@voltro/web": "0.28.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.28.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:lang` cookie. Inside React, use `<T>` / `useT()` via the
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:lang` cookie + reloads so the framework re-renders in the
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.28.0",
16
- "@voltro/client": "0.28.0",
17
- "@voltro/env": "0.28.0",
18
- "@voltro/i18n": "0.28.0",
19
- "@voltro/web": "0.28.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.28.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
+ }