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