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