@voltro/cli 0.50.1 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -0
- package/dist/{apiBuild-D3s7lx57.js → apiBuild-CPDHXF72.js} +2 -2
- package/dist/apiBuild-GDKuGOMV.js +2 -0
- package/dist/appGraph-CFmFKHb2.js +2 -0
- package/dist/appGraph-yRuKjOba.js +111 -0
- package/dist/bin.js +1 -1
- package/dist/{build-CFmqF39c.js → build-DETLZAFt.js} +271 -240
- package/dist/{checkCommand-DomvYDi1.js → checkCommand-CWcnDArJ.js} +1 -1
- package/dist/{checkCommand-DsKIjL6k.js → checkCommand-DNuPiWMc.js} +29 -28
- package/dist/{codegenCommand-C0S27nbB.js → codegenCommand-BNBHcNNj.js} +2 -2
- package/dist/{codemodRunner-DwY8Rjbl.js → codemodRunner-BDVixlSw.js} +278 -191
- package/dist/{commands-Brp4DI8u.js → commands-B1OiS9bX.js} +24 -23
- package/dist/{dashboardCommand-bbLfc9FW.js → dashboardCommand-C-vvPY1B.js} +5 -5
- package/dist/{dataCommand-k9mXTXin.js → dataCommand-C1GxXW5q.js} +279 -233
- package/dist/dbCommand-C27HIsGE.js +2 -0
- package/dist/{dbCommand-Bf7Cy0mn.js → dbCommand-If4Y1xQ-.js} +1 -1
- package/dist/{dev-Dz9GqA6P.js → dev-CK522MV5.js} +1 -1
- package/dist/{dev-C0cbqysu.js → dev-kdAg9Q7l.js} +1246 -1246
- package/dist/doctorCommand-BK4l18eG.js +2 -0
- package/dist/{doctorCommand-DyViaaCP.js → doctorCommand-nKmeW78u.js} +16 -16
- package/dist/{dormancyCommand-DMZPA_HW.js → dormancyCommand-CY3wa_SW.js} +1 -1
- package/dist/{embeddingsCommand-CMxRPkg6.js → embeddingsCommand-BDLIgje_.js} +1 -1
- package/dist/{envCommand-CDLr-yD4.js → envCommand-C6V_xVlT.js} +13 -13
- package/dist/{evolveCommand-Ct4NesL3.js → evolveCommand-D3c4DSfN.js} +2 -2
- package/dist/{generateCommand-BDdOxhnv.js → generateCommand-DTNPGjxo.js} +1 -1
- package/dist/index.js +1 -1
- package/dist/{infoCommand-orsKJKUy.js → infoCommand-BnRFEF1o.js} +1 -1
- package/dist/{migrate-D3MK9BpK.js → migrate-DehuBakM.js} +1 -1
- package/dist/{runtimeTrace-Bo8jWsNe.js → runtimeTrace-ZsBU7Tkx.js} +1 -1
- package/dist/{sdkgen-OZhiT2vz.js → sdkgen-O4XqWOjM.js} +1 -1
- package/dist/serveCommand-BRnPCxVd.js +2 -0
- package/dist/{serveCommand-YbD5VgfV.js → serveCommand-DdiYNBBu.js} +3 -3
- package/dist/serveEntry.js +1 -1
- package/dist/{start-B1nH9ahf.js → start-BLNmWkLa.js} +331 -327
- package/dist/{start-7b_bL8Uc.js → start-Dzicuyw8.js} +1 -1
- package/dist/startEntry.js +1 -1
- package/dist/{updateCommand-BgH6vTtq.js → updateCommand-Bs322Q78.js} +1 -1
- package/dist/updateCommand-eXB35SEv.js +2 -0
- package/dist/{webDev-BbdkzMbd.js → webDev-B-ubQEMX.js} +282 -223
- package/dist/webDev-DposiF3j.js +2 -0
- package/dist/{webhooksCommand-DjE6u0r4.js → webhooksCommand-FLYY9IXh.js} +1 -1
- package/package.json +35 -17
- package/templates/AGENTS.core.md +15 -5
- package/templates/AGENTS.md +16 -6
- package/templates/agent-docs/_index.md +1 -1
- package/templates/agent-docs/cli.md +29 -0
- package/templates/agent-docs/internationalization.md +131 -13
- package/templates/agent-docs/whats-new.md +75 -27
- package/templates/apps/api-ai/package.json +7 -7
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-sqlite/package.json +8 -8
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-cms/package.json +10 -10
- package/templates/apps/api-collab/package.json +8 -8
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-saas-starter/package.json +10 -10
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-status/package.json +8 -8
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-app/package.json +9 -9
- package/templates/apps/frontend-auth/package.json +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-cms/package.json +9 -9
- package/templates/apps/frontend-collab/package.json +10 -10
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-docs/package.json +7 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-portal/package.json +8 -8
- package/templates/apps/frontend-saas/package.json +8 -8
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/templates/apps/frontend-status/package.json +8 -8
- package/templates/apps/mobile-app/package.json +4 -4
- package/dist/apiBuild-BZ-plaQN.js +0 -2
- package/dist/appGraph-CM9wvUmD.js +0 -2
- package/dist/appGraph-Cv2W_wO5.js +0 -111
- package/dist/dbCommand-Bnl7MSGC.js +0 -2
- package/dist/doctorCommand-BlciWUJb.js +0 -2
- package/dist/serveCommand-Bbgg7Paq.js +0 -2
- package/dist/updateCommand-BweY17z-.js +0 -2
- package/dist/webDev-BcRFTsMA.js +0 -2
|
@@ -222,7 +222,7 @@ createVerifier({ secret: [process.env.WEBHOOK_SECRET, process.env.WEBHOOK_SECRET
|
|
|
222
222
|
...t === void 0 ? {} : { payload: t }
|
|
223
223
|
};
|
|
224
224
|
}, S = u({ scope: "voltro:webhooks" }), C = ["--out", "--name"], w = async (e) => {
|
|
225
|
-
let { walk: t, loadDiscovered: n } = await import("./dev-
|
|
225
|
+
let { walk: t, loadDiscovered: n } = await import("./dev-CK522MV5.js"), { outgoingFromEvents: r } = await import("./webhookDiscovery-il9ti-HE.js");
|
|
226
226
|
return r((await n(await t(e))).events.map((e) => ({
|
|
227
227
|
file: e.file,
|
|
228
228
|
descriptor: e.descriptor
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.0",
|
|
4
4
|
"description": "The `voltro` CLI — dev server, codegen, migrations, project scaffolding, agent-docs seeding, and production serve.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -700,6 +700,24 @@
|
|
|
700
700
|
"title": "A COMPUTED `source:` can keep its typed names — do not widen the whole registry",
|
|
701
701
|
"kind": "manual"
|
|
702
702
|
},
|
|
703
|
+
{
|
|
704
|
+
"version": "0.51.0",
|
|
705
|
+
"id": "0.51.0/02_restore-refuses-without-marker",
|
|
706
|
+
"title": "`voltro data restore` refuses when it cannot write the in-progress marker",
|
|
707
|
+
"kind": "manual"
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
"version": "0.51.0",
|
|
711
|
+
"id": "0.51.0/03_restore-assets-returns-counts",
|
|
712
|
+
"title": "`restoreAssetsFromCas` returns `{ count, objects }`, not a number",
|
|
713
|
+
"kind": "manual"
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
"version": "0.51.0",
|
|
717
|
+
"id": "0.51.0/04_i18n-timezone-prop",
|
|
718
|
+
"title": "`<I18nProvider>`: `timeZone` is its own prop, not an `intlConfig` member",
|
|
719
|
+
"kind": "transform"
|
|
720
|
+
},
|
|
703
721
|
{
|
|
704
722
|
"version": "0.6.0",
|
|
705
723
|
"id": "0.6.0/01_no-dev-session-secret",
|
|
@@ -775,22 +793,22 @@
|
|
|
775
793
|
"@effect/platform-node": "^0.108.0",
|
|
776
794
|
"@effect/sql": "^0.52.0",
|
|
777
795
|
"@effect/workflow": "^0.19.0",
|
|
778
|
-
"@voltro/ai": "0.
|
|
779
|
-
"@voltro/cache": "0.
|
|
780
|
-
"@voltro/data-transfer": "0.
|
|
781
|
-
"@voltro/database": "0.
|
|
782
|
-
"@voltro/env": "0.
|
|
783
|
-
"@voltro/kv": "0.
|
|
784
|
-
"@voltro/logger": "0.
|
|
785
|
-
"@voltro/plugin-auth": "0.
|
|
786
|
-
"@voltro/plugin-broadcast": "0.
|
|
787
|
-
"@voltro/plugin-mail": "0.
|
|
788
|
-
"@voltro/plugin-storage": "0.
|
|
789
|
-
"@voltro/plugin-webhooks": "0.
|
|
790
|
-
"@voltro/protocol": "0.
|
|
791
|
-
"@voltro/runtime": "0.
|
|
792
|
-
"@voltro/serverless": "0.
|
|
793
|
-
"@voltro/workflow": "0.
|
|
796
|
+
"@voltro/ai": "0.51.0",
|
|
797
|
+
"@voltro/cache": "0.51.0",
|
|
798
|
+
"@voltro/data-transfer": "0.51.0",
|
|
799
|
+
"@voltro/database": "0.51.0",
|
|
800
|
+
"@voltro/env": "0.51.0",
|
|
801
|
+
"@voltro/kv": "0.51.0",
|
|
802
|
+
"@voltro/logger": "0.51.0",
|
|
803
|
+
"@voltro/plugin-auth": "0.51.0",
|
|
804
|
+
"@voltro/plugin-broadcast": "0.51.0",
|
|
805
|
+
"@voltro/plugin-mail": "0.51.0",
|
|
806
|
+
"@voltro/plugin-storage": "0.51.0",
|
|
807
|
+
"@voltro/plugin-webhooks": "0.51.0",
|
|
808
|
+
"@voltro/protocol": "0.51.0",
|
|
809
|
+
"@voltro/runtime": "0.51.0",
|
|
810
|
+
"@voltro/serverless": "0.51.0",
|
|
811
|
+
"@voltro/workflow": "0.51.0",
|
|
794
812
|
"chokidar": "^5.0.0",
|
|
795
813
|
"ioredis": "^5.11.1",
|
|
796
814
|
"tinyglobby": "^0.2.17",
|
package/templates/AGENTS.core.md
CHANGED
|
@@ -404,11 +404,21 @@ Do NOT write `x.page.tsx`, `index.page.tsx` or `[id].page.tsx` — those were th
|
|
|
404
404
|
**Every suffix above is a CONTRACT, not a label.** Something else depends on each
|
|
405
405
|
promise, and `voltro doctor` enforces all of them:
|
|
406
406
|
|
|
407
|
-
- `*.component.ui.tsx` may read (`useT`, `useCan`, `usePermissions
|
|
408
|
-
never import a write hook
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
407
|
+
- `*.component.ui.tsx` may read (`useT`, `useTFn`, `useCan`, `usePermissions`,
|
|
408
|
+
the `@voltro/i18n` formatters) but must never import a write hook
|
|
409
|
+
(`useMutation`, `useAction`, `useUpload`, …). That is what lets a caller render
|
|
410
|
+
it ten thousand times in a list, reuse it across features, and prerender it
|
|
411
|
+
without reading its source. Lift the write into the owning `*.component.tsx`
|
|
412
|
+
and pass a handler down.
|
|
413
|
+
|
|
414
|
+
**Reading is a recommendation, not a grudging allowance — call the hook, do
|
|
415
|
+
not take a prop.** Threading `t` / `formatDate` / `useCan` through props is
|
|
416
|
+
prop-drilling: it grows the prop surface with every formatter, has to be
|
|
417
|
+
restated in every intermediate signature and every test, and does NOT make the
|
|
418
|
+
component more reusable — it makes every call site worse. The one argument for
|
|
419
|
+
the prop, "it renders without a provider", is already paid for the moment any
|
|
420
|
+
test mounts one. Pick one way per component head: a `t` prop beside a
|
|
421
|
+
formatter hook is the worst of both.
|
|
412
422
|
- `*.internal.ts` is the promise that refactoring inside that directory breaks
|
|
413
423
|
nobody. An import from another subtree revokes it.
|
|
414
424
|
- `*.types.ts` having no runtime export is what makes importing it free and makes
|
package/templates/AGENTS.md
CHANGED
|
@@ -404,11 +404,21 @@ Do NOT write `x.page.tsx`, `index.page.tsx` or `[id].page.tsx` — those were th
|
|
|
404
404
|
**Every suffix above is a CONTRACT, not a label.** Something else depends on each
|
|
405
405
|
promise, and `voltro doctor` enforces all of them:
|
|
406
406
|
|
|
407
|
-
- `*.component.ui.tsx` may read (`useT`, `useCan`, `usePermissions
|
|
408
|
-
never import a write hook
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
407
|
+
- `*.component.ui.tsx` may read (`useT`, `useTFn`, `useCan`, `usePermissions`,
|
|
408
|
+
the `@voltro/i18n` formatters) but must never import a write hook
|
|
409
|
+
(`useMutation`, `useAction`, `useUpload`, …). That is what lets a caller render
|
|
410
|
+
it ten thousand times in a list, reuse it across features, and prerender it
|
|
411
|
+
without reading its source. Lift the write into the owning `*.component.tsx`
|
|
412
|
+
and pass a handler down.
|
|
413
|
+
|
|
414
|
+
**Reading is a recommendation, not a grudging allowance — call the hook, do
|
|
415
|
+
not take a prop.** Threading `t` / `formatDate` / `useCan` through props is
|
|
416
|
+
prop-drilling: it grows the prop surface with every formatter, has to be
|
|
417
|
+
restated in every intermediate signature and every test, and does NOT make the
|
|
418
|
+
component more reusable — it makes every call site worse. The one argument for
|
|
419
|
+
the prop, "it renders without a provider", is already paid for the moment any
|
|
420
|
+
test mounts one. Pick one way per component head: a `t` prop beside a
|
|
421
|
+
formatter hook is the worst of both.
|
|
412
422
|
- `*.internal.ts` is the promise that refactoring inside that directory breaks
|
|
413
423
|
nobody. An import from another subtree revokes it.
|
|
414
424
|
- `*.types.ts` having no runtime export is what makes importing it free and makes
|
|
@@ -707,7 +717,7 @@ each plugin's own README.
|
|
|
707
717
|
|
|
708
718
|
| Topic | Open | Summary |
|
|
709
719
|
|---|---|---|
|
|
710
|
-
| **What's new in 0.
|
|
720
|
+
| **What's new in 0.51.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
|
|
711
721
|
| AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
|
|
712
722
|
| Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
|
|
713
723
|
| Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
|
|
@@ -9,7 +9,7 @@ each plugin's own README.
|
|
|
9
9
|
|
|
10
10
|
| Topic | Open | Summary |
|
|
11
11
|
|---|---|---|
|
|
12
|
-
| **What's new in 0.
|
|
12
|
+
| **What's new in 0.51.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
|
|
13
13
|
| AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
|
|
14
14
|
| Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
|
|
15
15
|
| Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
|
|
@@ -3412,8 +3412,22 @@ Aborting on the first one made `--assets` unusable for exactly the deployment th
|
|
|
3412
3412
|
|
|
3413
3413
|
Only a genuine *not found* is treated this way. A 403 from a rotated credential or a 5xx from a backend outage still fails the capture — calling those "the object is gone" would turn a recoverable outage into a backup that quietly contains nothing.
|
|
3414
3414
|
|
|
3415
|
+
**Three numbers, because they answer three questions.** `_voltro_storage_refs` holds one row per *reference*, several of which legitimately name one *key*, and the content-addressed store keeps one body per distinct *object*:
|
|
3416
|
+
|
|
3417
|
+
```
|
|
3418
|
+
57 reference(s) → 16 key(s) → 16 object(s), 65476 byte(s) under assets/
|
|
3419
|
+
```
|
|
3420
|
+
|
|
3421
|
+
The stamp carries all three (`references`, `count`, `objects`, with `totalBytes` and `objectBytes` beside them). It used to carry only the reference count under the name `count`, so a stamp read `57` over a directory holding 16 files — and anyone answering *"are all the blobs there?"* after a restore compared the two and found a 3.5× gap that was not one. A key named by several references is also fetched once now, rather than downloaded and hashed once per row.
|
|
3422
|
+
|
|
3415
3423
|
Resume is per blob key, so re-running a `--assets` capture that was interrupted transfers only what is missing. The **dump itself has no resume** — a vendor artifact is one opaque file with no offset to restart from. If you need a resumable, chunkable, observable transfer, that is the logical path (`export` / `import`), and it is why the logical path exists.
|
|
3416
3424
|
|
|
3425
|
+
### The stamp's skew warning compares the backup against the TARGET
|
|
3426
|
+
|
|
3427
|
+
`restore` reads `voltro-backup-stamp.json` before touching anything and warns when the backup's schema fingerprint differs from the target's. That warning used to say the difference was against "what this code declares", and it was not — the value it compares against is the target database's *live* schema, read by introspection at restore time. Bringing a target to the backup's shape makes the warning disappear while the declared fingerprint is a third value entirely, which is how the mislabel was caught. The comparison was always the useful one; only the sentence was wrong, and it sent readers looking for a code change where a database differed.
|
|
3428
|
+
|
|
3429
|
+
The same distinction shows up in `voltro db plan`, which prints `live … · declared …` rather than `from → to` for the same reason: **a hash of a live database never equals the hash of the declaration it came from.** Introspection cannot recover everything a declaration carries — generated expressions, `maxLength`, sensitivity markers — so the two are not comparable and are not meant to match. The plan's operation list is what says whether they agree; `0 operations` under two different fingerprints means they do.
|
|
3430
|
+
|
|
3417
3431
|
### A restore that is interrupted refuses the next boot
|
|
3418
3432
|
|
|
3419
3433
|
`restore` writes one row into `_voltro_replace_in_progress` **before** the first destructive statement and removes it **after** the last write — the blobs included. Its presence at boot is a refusal naming the artifact that was going in.
|
|
@@ -3427,6 +3441,17 @@ Two changes, covering different dumps:
|
|
|
3427
3441
|
- **A backup taken by `voltro data backup` excludes the marker table** (`--exclude-table` / `--ignore-table`). It can no longer carry the thing that erases the guard on the way back in.
|
|
3428
3442
|
- **`restore` writes the marker back after the tool exits**, on the failing path as well as the succeeding one. That covers dumps taken before this version and dumps made by hand. If it was removed and rewritten you get a warning saying so; if it could not be rewritten you get an error, because the guard is then off for that run and nothing will stop the next boot.
|
|
3429
3443
|
|
|
3444
|
+
**A restore that cannot write the marker at all is refused.** Two things can prevent it — the bookkeeping store will not open (wrong credentials, an unreachable database, a missing env var, no `app.config.ts` from here), or the table is not there yet — and both mean the same thing to you: this restore would run with no guard. The refusal names which one it was:
|
|
3445
|
+
|
|
3446
|
+
```
|
|
3447
|
+
✗ refusing to restore: the in-progress marker cannot be written.
|
|
3448
|
+
reason: bookkeeping is unavailable: connect ECONNREFUSED 127.0.0.1:5432
|
|
3449
|
+
```
|
|
3450
|
+
|
|
3451
|
+
This used to be a silent hole rather than a refusal, and worse than silent. The failure to open the store was caught and discarded, and the discarded value guarded *every* branch below it — including the refusal that would have reported the guard missing. So a restore ran on and, over a database with zero marker rows, printed *"the next boot will REFUSE, by design"*. The next boot did not refuse, and `voltro data clear-replace-marker` had nothing to clear. A restore is the operation you run against a target that is already unwell, so the precaution was falling away exactly when it was needed.
|
|
3452
|
+
|
|
3453
|
+
**`--no-marker`** restores without the guard, deliberately. It warns every time and names the reason the marker was unavailable. It exists because the accidental way did: if going unguarded is ever right, it should be something you typed.
|
|
3454
|
+
|
|
3430
3455
|
| dialect | shape | effect |
|
|
3431
3456
|
|---|---|---|
|
|
3432
3457
|
| postgres, mysql, mariadb | the dump names the whole database, so the restore drops the marker table too | our backups exclude it; for any other dump the marker is written back after the tool exits |
|
|
@@ -3435,6 +3460,10 @@ Two changes, covering different dumps:
|
|
|
3435
3460
|
|
|
3436
3461
|
Clear a marker deliberately with `voltro data clear-replace-marker --yes` once you have decided the current state is correct.
|
|
3437
3462
|
|
|
3463
|
+
**`_voltro_data_transfers` is excluded for the same reason, one table over.** The restore opens its own run row there *before* the tool starts; a dump that carried the table dropped it mid-flight, and the update recording the outcome then wrote into a table that no longer held the row. The visible result was that a failed native restore did not appear in `voltro data transfers` at all — only the `backup` row the dump had brought over from the *source* database. The command that answers "did the restore finish" could not see the run asking.
|
|
3464
|
+
|
|
3465
|
+
Exactly those two tables are excluded, and the line is deliberate: a native restore into the same deployment *should* bring the migration ledger, the stored plans, the CDC offsets and the schedule claims — they describe the data being restored. These two describe the *restore*, and a record of an operation must not be overwritten by the operation it records.
|
|
3466
|
+
|
|
3438
3467
|
### Both directions are in the history
|
|
3439
3468
|
|
|
3440
3469
|
`backup` and `restore` write a row to the same `_voltro_data_transfers` record `import` and `export` use, so `voltro data transfers` answers "did last night's backup finish" from the instance that ran it:
|
|
@@ -36,6 +36,13 @@ export default {
|
|
|
36
36
|
// supported one. MUST be in `locales`. Pick your source-of-truth
|
|
37
37
|
// language — typically English.
|
|
38
38
|
defaultLocale: 'en' as const,
|
|
39
|
+
// The zone every date/time formatter renders in. Omit it and each
|
|
40
|
+
// runtime uses its own — the pod's on the server, the viewer's in the
|
|
41
|
+
// browser — which is a hydration mismatch on every SSR timestamp.
|
|
42
|
+
// An IANA name pins one zone for everyone; 'viewer' resolves it per
|
|
43
|
+
// request from the `voltro:tz` cookie.
|
|
44
|
+
timeZone: 'viewer' as const,
|
|
45
|
+
defaultTimeZone: 'UTC' as const,
|
|
39
46
|
}
|
|
40
47
|
```
|
|
41
48
|
|
|
@@ -79,6 +86,18 @@ The client **adopts what the server resolved**, reading it from the `<html lang>
|
|
|
79
86
|
|
|
80
87
|
`<html lang>` carries the same resolved locale — the value the `<I18nProvider>` renders with, on the same request. That matters on its own: it is what a screen reader pronounces in, what Chrome offers to translate *from*, and what hyphenation uses.
|
|
81
88
|
|
|
89
|
+
### The same contract carries the render zone and the render clock
|
|
90
|
+
|
|
91
|
+
`lang` is one of three answers the server decides and publishes so the client does not form its own:
|
|
92
|
+
|
|
93
|
+
| attribute | what it carries |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `lang` | the resolved locale |
|
|
96
|
+
| `data-voltro-tz` | the IANA zone every date/time formatter renders in — set `timeZone` in `app.config.ts` |
|
|
97
|
+
| `data-voltro-now` | the server's render instant, so `useRelativeTime` produces the same string in the hydration pass |
|
|
98
|
+
|
|
99
|
+
Locale was already agreed; the zone and the clock were each read from the ambient runtime, which meant a server-rendered timestamp was a hydration mismatch waiting for a wide enough offset or a slow enough connection. See [Plurals & formatting → Timezones under SSR](/docs/i18n/formatting#timezones-under-ssr--the-setting-that-is-not-a-preference) — that is the page to read before you migrate hand-rolled `toLocaleString()` calls onto the hooks.
|
|
100
|
+
|
|
82
101
|
See [Catalogs](/docs/i18n/catalogs) for the type-safe catalog convention and the component hooks, [Plurals & formatting](/docs/i18n/formatting) for CLDR plural selection and the `Intl`-backed date / number / relative-time hooks, and [URL strategies](/docs/i18n/url-strategies) for cookie-only vs URL-prefix routing.
|
|
83
102
|
|
|
84
103
|
## The cookie names are exported
|
|
@@ -675,7 +694,19 @@ formatDate(order.createdAt, { dateStyle: 'medium' })
|
|
|
675
694
|
formatDate(order.createdAt, { dateStyle: 'medium', timeStyle: 'short', timeZone: 'Europe/Berlin' })
|
|
676
695
|
```
|
|
677
696
|
|
|
678
|
-
`(value, options?) => string`, where `value` is a `Date`, a timestamp number, or a date string, and `options` is `Intl.DateTimeFormatOptions`.
|
|
697
|
+
`(value, options?) => string`, where `value` is a `Date`, a timestamp number, or a date string, and `options` is `Intl.DateTimeFormatOptions`.
|
|
698
|
+
|
|
699
|
+
**The zone comes from the provider, which you configure once** — `timeZone` in the web `app.config.ts` (see [Timezones under SSR](#timezones-under-ssr-the-setting-that-is-not-a-preference)). Pass `timeZone` in the options only when the value genuinely belongs to a fixed zone regardless of who is looking (a store's opening hours, a scheduled broadcast); that overrides the provider for one call.
|
|
700
|
+
|
|
701
|
+
**With `timeZone` unset in `app.config.ts` there is no zone at all, and each runtime falls back to its own.** On a server-rendered page that is the pod's zone for the markup and the viewer's for the hydration render — a mismatch on every timestamp, and a different calendar day across midnight. `useTimeZone()` returns `undefined` in exactly that state, so you can assert on it.
|
|
702
|
+
|
|
703
|
+
### `useTimeZone`
|
|
704
|
+
|
|
705
|
+
```tsx
|
|
706
|
+
const timeZone = useTimeZone() // 'Europe/Berlin' — or undefined when none is pinned
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
`undefined` is a real answer and worth branching on: it means nobody decided, so the server and the browser are each using their own zone. It is not the same as `'UTC'`.
|
|
679
710
|
|
|
680
711
|
### `useRelativeTime`
|
|
681
712
|
|
|
@@ -688,7 +719,9 @@ relativeTime(comment.postedAt, { numeric: 'always' }) // "1 day ago" instead of
|
|
|
688
719
|
relativeTime(comment.postedAt, { now: renderedAt }) // measure against a fixed base
|
|
689
720
|
```
|
|
690
721
|
|
|
691
|
-
`(value, options?) => string`. Options are `Intl.RelativeTimeFormatOptions` plus a `now` override (a `Date`, number, or string) for deterministic rendering and tests
|
|
722
|
+
`(value, options?) => string`. Options are `Intl.RelativeTimeFormatOptions` plus a `now` override (a `Date`, number, or string) for deterministic rendering and tests.
|
|
723
|
+
|
|
724
|
+
**Hydration-safe by default.** Without an explicit `now`, the base is the server's render instant for the server render *and* the hydration pass that has to match it — published as `<html data-voltro-now>` — then the live clock once hydration commits. So "3 minutes ago" cannot become "4 minutes ago" between the HTML and the first client render just because the network was slow, which is what a plain `Date.now()` base does whenever a unit boundary falls in the gap.
|
|
692
725
|
|
|
693
726
|
It picks the **largest unit that fits**, so a 90-minute delta reads "1 hour ago", not "90 minutes ago". Anything under a second renders through the `second` unit at 0 — "now" — which avoids the "0 seconds ago" flicker hand-rolled versions produce. `numeric: 'auto'` is the default, so English gets "yesterday" rather than "1 day ago".
|
|
694
727
|
|
|
@@ -721,7 +754,7 @@ For a component that needs several at once, without stacking five hook calls:
|
|
|
721
754
|
import { useFormatters } from '@voltro/i18n'
|
|
722
755
|
|
|
723
756
|
function ActivityRow({ entry }: { readonly entry: Entry }) {
|
|
724
|
-
const { locale, formatDate, relativeTime, formatNumber, plural } = useFormatters()
|
|
757
|
+
const { locale, timeZone, formatDate, relativeTime, formatNumber, plural } = useFormatters()
|
|
725
758
|
|
|
726
759
|
return (
|
|
727
760
|
<li lang={locale}>
|
|
@@ -735,7 +768,77 @@ function ActivityRow({ entry }: { readonly entry: Entry }) {
|
|
|
735
768
|
}
|
|
736
769
|
```
|
|
737
770
|
|
|
738
|
-
It returns the active `locale` plus `formatDate`, `relativeTime`, `formatNumber` and `plural` — memoized together. Currency is not in the bundle because it needs its ISO code up front; call `useFormatCurrency(code)` alongside it when you need one.
|
|
771
|
+
It returns the active `locale` and `timeZone` plus `formatDate`, `relativeTime`, `formatNumber` and `plural` — memoized together. Currency is not in the bundle because it needs its ISO code up front; call `useFormatCurrency(code)` alongside it when you need one.
|
|
772
|
+
|
|
773
|
+
## Timezones under SSR — the setting that is not a preference
|
|
774
|
+
|
|
775
|
+
A formatter is deterministic given the value, the locale, the zone and the clock. The provider supplies all four, and the two beyond locale are the ones that differ between the server and the browser:
|
|
776
|
+
|
|
777
|
+
| | Where it came from before | What that means under SSR |
|
|
778
|
+
|---|---|---|
|
|
779
|
+
| **locale** | the provider, both sides | agreed already — the server publishes `<html lang>` and the client reads the attribute rather than `navigator.languages` |
|
|
780
|
+
| **zone** | the runtime | the POD on the server (UTC on a container with no `TZ`), the VIEWER's machine in the browser |
|
|
781
|
+
| **clock** | `Date.now()` | two numbers, differing by the network latency |
|
|
782
|
+
|
|
783
|
+
So a server-rendered timestamp was a hydration mismatch (React error #418) waiting for a wide enough offset or a slow enough connection, and across midnight it was a different calendar **day**. The fix is the one the locale already used: **the server decides, publishes its answer, and the client reads the answer instead of forming its own.**
|
|
784
|
+
|
|
785
|
+
### Configure it once
|
|
786
|
+
|
|
787
|
+
```ts
|
|
788
|
+
// apps/<project>/web/app.config.ts
|
|
789
|
+
export default {
|
|
790
|
+
type: 'web' as const,
|
|
791
|
+
name: 'myApp',
|
|
792
|
+
port: 5191,
|
|
793
|
+
locales: ['de', 'en'] as const,
|
|
794
|
+
defaultLocale: 'de' as const,
|
|
795
|
+
|
|
796
|
+
timeZone: 'Europe/Berlin' as const, // one zone for every viewer
|
|
797
|
+
// …or:
|
|
798
|
+
// timeZone: 'viewer' as const, // resolve per request, per user
|
|
799
|
+
// defaultTimeZone: 'UTC' as const, // before the viewer's zone is known
|
|
800
|
+
}
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
Whatever it resolves to is stamped on the document as `<html data-voltro-tz>`, and the generated client entry reads that attribute. Both sides then format against one value — which is the property that removes the mismatch, whether or not the value is the viewer's true zone. Being *wrong together* is repairable after mount; being *different* is not.
|
|
804
|
+
|
|
805
|
+
`timeZone` requires `locales`, because the zone rides the `<I18nProvider>` the framework generates from it.
|
|
806
|
+
|
|
807
|
+
### `timeZone: 'viewer'` — how the server learns the zone
|
|
808
|
+
|
|
809
|
+
Through the `voltro:tz` cookie, which has two writers and wants both:
|
|
810
|
+
|
|
811
|
+
1. **The framework's script**, injected into `<head>`, seeds it from `Intl.DateTimeFormat().resolvedOptions().timeZone` when the cookie is absent. From the second request onward the server renders in the browser's zone with no login and no app code. It never overwrites an existing value and never reloads the page.
|
|
812
|
+
2. **Your app, at login** — overwriting it with the zone you hold for the signed-in user. That is the authoritative one: a profile field or an identity provider's `timeZone` claim beats the machine a user happens to be sitting at.
|
|
813
|
+
|
|
814
|
+
Write it from `middleware.ts`, which runs per request and can return cookies:
|
|
815
|
+
|
|
816
|
+
```ts
|
|
817
|
+
// apps/<project>/web/middleware.ts
|
|
818
|
+
import { defineMiddleware } from '@voltro/web/middleware'
|
|
819
|
+
import { TIMEZONE_COOKIE, isSupportedTimeZone } from '@voltro/i18n'
|
|
820
|
+
|
|
821
|
+
export const userTimeZone = defineMiddleware({
|
|
822
|
+
run: async (req) => {
|
|
823
|
+
const zone = await zoneForSession(req.cookies) // your session → the user's own zone
|
|
824
|
+
if (!isSupportedTimeZone(zone) || req.cookies[TIMEZONE_COOKIE] === zone) return undefined
|
|
825
|
+
return {
|
|
826
|
+
setCookies: [
|
|
827
|
+
{ name: TIMEZONE_COOKIE, value: zone, path: '/', maxAge: 31_536_000, sameSite: 'lax' as const },
|
|
828
|
+
],
|
|
829
|
+
}
|
|
830
|
+
},
|
|
831
|
+
})
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
A cookie set here is applied to the jar the SAME render reads, so the zone takes
|
|
835
|
+
effect on the response that sets it rather than the one after.
|
|
836
|
+
|
|
837
|
+
Validate before you write. An unusable zone is dropped on the way in (a stale cookie, a typo in the config, a runtime with a trimmed ICU) rather than forwarded — `Intl.DateTimeFormat` throws on an unknown zone, and one bad value would otherwise degrade every timestamp in the app to a raw `Date` string.
|
|
838
|
+
|
|
839
|
+
### Prerendered pages
|
|
840
|
+
|
|
841
|
+
A `renderMode: 'static'` page is one artefact for every viewer, so `'viewer'` cannot mean the viewer there — it resolves to `defaultTimeZone`. The build publishes that value and its own build instant, so the markup and the first client render still agree; a relative time in a prerendered page corrects itself in one frame after mount rather than mismatching.
|
|
739
842
|
|
|
740
843
|
## Formatters vs. ICU in the catalog
|
|
741
844
|
|
|
@@ -750,7 +853,8 @@ For dates, numbers and relative time the hooks are the blessed path; reach for `
|
|
|
750
853
|
|
|
751
854
|
- **Don't write `(s)`, `count === 1 ? 'x' : 'xs'`, or a `+ 's'` suffix.** It is wrong in most languages and cannot be fixed by a translator. Use `plural` / `usePlural` or ICU in the catalog.
|
|
752
855
|
- **Don't hand-roll "X minutes ago".** `useRelativeTime` is one hook, is localized, and handles the sub-second and unit-selection cases that inline versions get wrong.
|
|
753
|
-
- **Don't
|
|
856
|
+
- **Don't pass `timeZone` at every call site to work around a missing config.** Set `timeZone` in `app.config.ts` once. A per-call convention is a rule every new call has to remember, and the ones that forget are invisible until a viewer in another zone reads a wrong date.
|
|
857
|
+
- **Don't call `Intl.DateTimeFormat().resolvedOptions().timeZone` in a component to "fix" SSR.** It is the viewer's true zone and therefore the wrong value: the server could not know it, so the server did not render with it, and using it on the client guarantees the mismatch. Let the server decide and publish — that is what `timeZone: 'viewer'` does.
|
|
754
858
|
- **Don't pass a locale-formatted string to a machine consumer.** Formatted output is presentation — send ISO strings and raw numbers to APIs, `dateTime` attributes and sort keys.
|
|
755
859
|
- **Don't format inside a `.map()` by constructing `Intl` objects yourself.** The hooks memoize per locale; a fresh `new Intl.NumberFormat(...)` per row is the slow path.
|
|
756
860
|
|
|
@@ -840,14 +944,28 @@ program.pipe(withTimezone('Europe/Berlin'))
|
|
|
840
944
|
`currentTimezone` never fails — an absent context is the documented `UTC`
|
|
841
945
|
default, so call sites don't handle a missing-service error.
|
|
842
946
|
|
|
843
|
-
> **
|
|
844
|
-
>
|
|
845
|
-
>
|
|
846
|
-
>
|
|
847
|
-
>
|
|
848
|
-
>
|
|
849
|
-
>
|
|
850
|
-
>
|
|
947
|
+
> **Two zones, and only one of them is wired. Keep them apart.**
|
|
948
|
+
>
|
|
949
|
+
> The **render zone** — what a date LOOKS like in the UI — is wired end to end.
|
|
950
|
+
> Set `timeZone` in the web `app.config.ts`, and the framework resolves it per
|
|
951
|
+
> request, publishes it on the document, and every `@voltro/i18n` formatter on
|
|
952
|
+
> both sides of the hydration boundary uses it. Read it with `useTimeZone()`
|
|
953
|
+
> from `@voltro/i18n`. See
|
|
954
|
+
> [Formatting → Timezones under SSR](/docs/i18n/formatting#timezones-under-ssr--the-setting-that-is-not-a-preference).
|
|
955
|
+
>
|
|
956
|
+
> The **server-side compute zone** — what `startOfDay` or a workflow's "same
|
|
957
|
+
> time tomorrow" resolves against inside a handler — is still the SEAM only.
|
|
958
|
+
> `@voltro/datetime/context` exports `currentTimezone`, `withTimezone` and
|
|
959
|
+
> `resolvedTimezoneLayer`; the framework does **not** install the layer per
|
|
960
|
+
> request, so resolve the zone yourself (`resolveTimezone`) and provide
|
|
961
|
+
> `resolvedTimezoneLayer` around the work that needs it, or pass an explicit
|
|
962
|
+
> `timeZone` argument. Without a provided layer every call reads the `UTC`
|
|
963
|
+
> default.
|
|
964
|
+
>
|
|
965
|
+
> Rendering a date correctly does NOT give a route handler the user's zone, and
|
|
966
|
+
> a handler that has it does NOT change what the browser renders. They are
|
|
967
|
+
> separate values today and a `timeZone` in `app.config.ts` configures the
|
|
968
|
+
> first one only.
|
|
851
969
|
|
|
852
970
|
## Arithmetic — the DST split is in the names
|
|
853
971
|
|