cartethyia 2.5.2 → 2.5.4
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/dist/dashboard/assets/{BannedPage-MsQaeQTs.js → BannedPage-C2orBsI2.js} +1 -1
- package/dist/dashboard/assets/BannedPage-C2orBsI2.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js +1 -0
- package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-DfWHTJ7v.js → CliToolsPage-BV6pxVQo.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BV6pxVQo.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-BrCBQDAc.js → CombosPage-Bsan4679.js} +1 -1
- package/dist/dashboard/assets/CombosPage-Bsan4679.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-C6Uh33er.js → ConsoleLogPage-CW-wPmJ6.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-CW-wPmJ6.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-8aS9hyJu.js → CustomizationPage-0p8uX7vB.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-0p8uX7vB.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-C8hKyXBe.js → LoginPage-BvzCx8Kr.js} +1 -1
- package/dist/dashboard/assets/LoginPage-BvzCx8Kr.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-CWkTUtpX.js → ModelPicker-BY1aKEkP.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js +1 -0
- package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js +7 -0
- package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js +1 -0
- package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CS86ho5X.js +1 -0
- package/dist/dashboard/assets/SettingsPage-CS86ho5X.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-CXjUlfqr.js → SetupPage-DDbMQsoo.js} +1 -1
- package/dist/dashboard/assets/SetupPage-DDbMQsoo.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-C-_oD3F2.js → SortableList-CB3Z7VTf.js} +1 -1
- package/dist/dashboard/assets/SortableList-CB3Z7VTf.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-u9IxmYOI.js → StudioPage-D95UCPHI.js} +3 -3
- package/dist/dashboard/assets/StudioPage-D95UCPHI.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-BEVLVrwP.js → UsagePage-Cnae9SWA.js} +13 -13
- package/dist/dashboard/assets/UsagePage-Cnae9SWA.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-uNywYlfv.js → api-keys-VY68Mhd1.js} +1 -1
- package/dist/dashboard/assets/api-keys-VY68Mhd1.js.gz +0 -0
- package/dist/dashboard/assets/app-COlK95oi.css +2 -0
- package/dist/dashboard/assets/app-COlK95oi.css.gz +0 -0
- package/dist/dashboard/assets/{assert-NHwYq95t.js → assert-Chi7pCnt.js} +1 -1
- package/dist/dashboard/assets/assert-Chi7pCnt.js.gz +0 -0
- package/dist/dashboard/assets/{badge-6uzDDvqs.js → badge-D4pQapKj.js} +1 -1
- package/dist/dashboard/assets/badge-D4pQapKj.js.gz +0 -0
- package/dist/dashboard/assets/{banner-4_WNg2WZ.js → banner-DH6cg-_B.js} +1 -1
- package/dist/dashboard/assets/banner-DH6cg-_B.js.gz +0 -0
- package/dist/dashboard/assets/{card--AtFedAq.js → card-BKqlUOe4.js} +1 -1
- package/dist/dashboard/assets/card-BKqlUOe4.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-B3e7dx3N.js → cli-tools-fTgljTGA.js} +1 -1
- package/dist/dashboard/assets/cli-tools-fTgljTGA.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-NreUx6c8.js → clipboard-button-D5Ys_sM0.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-D5Ys_sM0.js.gz +0 -0
- package/dist/dashboard/assets/database-Dz9N3zGd.js +1 -0
- package/dist/dashboard/assets/database-Dz9N3zGd.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-BtpsJU98.js → dialog-BYj-8WuK.js} +1 -1
- package/dist/dashboard/assets/dialog-BYj-8WuK.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-WwwtzihF.js → drawer-CmXIZ6H1.js} +1 -1
- package/dist/dashboard/assets/drawer-CmXIZ6H1.js.gz +0 -0
- package/dist/dashboard/assets/entry-CBg5zeu8.js +1 -0
- package/dist/dashboard/assets/entry-CBg5zeu8.js.gz +0 -0
- package/dist/dashboard/assets/entry-CQBr6U9c.js +1 -0
- package/dist/dashboard/assets/entry-CQBr6U9c.js.gz +0 -0
- package/dist/dashboard/assets/entry-CgdSAzEV.js +4 -0
- package/dist/dashboard/assets/entry-CgdSAzEV.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-D_eM3IgG.js → github-badge-BlptL8Uf.js} +1 -1
- package/dist/dashboard/assets/github-badge-BlptL8Uf.js.gz +0 -0
- package/dist/dashboard/assets/{index-DCBtROoK.js → index-js6TJvdr.js} +2 -2
- package/dist/dashboard/assets/index-js6TJvdr.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BcNGF8Gr.js → inline-DUWDSmDV.js} +1 -1
- package/dist/dashboard/assets/inline-DUWDSmDV.js.gz +0 -0
- package/dist/dashboard/assets/{input-BG41et9G.js → input-LVjgZq4f.js} +1 -1
- package/dist/dashboard/assets/input-LVjgZq4f.js.gz +0 -0
- package/dist/dashboard/assets/{layout-C8mer_Ak.js → layout-CPfxpsfy.js} +1 -1
- package/dist/dashboard/assets/layout-CPfxpsfy.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-Gh_W4OR_.js → list-row-DupvLfDx.js} +1 -1
- package/dist/dashboard/assets/list-row-DupvLfDx.js.gz +0 -0
- package/dist/dashboard/assets/{live-BJGYRBnR.js → live-C4pEJUqI.js} +1 -1
- package/dist/dashboard/assets/live-C4pEJUqI.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-BRiuufkB.js +1 -0
- package/dist/dashboard/assets/model-bans-BRiuufkB.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-BkoLM5Ba.js → mutation-B4ROwk8L.js} +1 -1
- package/dist/dashboard/assets/mutation-B4ROwk8L.js.gz +0 -0
- package/dist/dashboard/assets/numbers-YNcHvRY7.js +2 -0
- package/dist/dashboard/assets/numbers-YNcHvRY7.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-D7wX2vM5.js → query-keys-gmjzC3RS.js} +1 -1
- package/dist/dashboard/assets/{query-keys-D7wX2vM5.js.gz → query-keys-gmjzC3RS.js.gz} +0 -0
- package/dist/dashboard/assets/{routing-CcusqXA7.js → routing-C7tBT5o-.js} +1 -1
- package/dist/dashboard/assets/routing-C7tBT5o-.js.gz +0 -0
- package/dist/dashboard/assets/{select-DsX5QT8K.js → select-WUCXorXw.js} +2 -2
- package/dist/dashboard/assets/select-WUCXorXw.js.gz +0 -0
- package/dist/dashboard/assets/{settings-mpmXTgUa.js → settings-C_zKl1GS.js} +1 -1
- package/dist/dashboard/assets/settings-C_zKl1GS.js.gz +0 -0
- package/dist/dashboard/assets/{stack-DwLmjUFK.js → stack-_3O0F8Xn.js} +1 -1
- package/dist/dashboard/assets/stack-_3O0F8Xn.js.gz +0 -0
- package/dist/dashboard/assets/{state-DYdQmdAl.js → state-BgTxHcJS.js} +1 -1
- package/dist/dashboard/assets/state-BgTxHcJS.js.gz +0 -0
- package/dist/dashboard/assets/switch-DEjTtOWf.js +1 -0
- package/dist/dashboard/assets/switch-DEjTtOWf.js.gz +0 -0
- package/dist/dashboard/assets/terminal-0_IjPWTk.js +1 -0
- package/dist/dashboard/assets/terminal-0_IjPWTk.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-w4u4CVue.js → useMutation-BWSC0UAS.js} +1 -1
- package/dist/dashboard/assets/useMutation-BWSC0UAS.js.gz +0 -0
- package/dist/dashboard/assets/zap-C4EeVW4q.js +1 -0
- package/dist/dashboard/assets/zap-C4EeVW4q.js.gz +0 -0
- package/dist/dashboard/donation/qris-risun-marke.png +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +54 -54
- package/package.json +1 -1
- package/scripts/cli/autostart.ts +237 -0
- package/scripts/cli/command/console-menu.ts +244 -246
- package/scripts/cli/command/tray.ts +208 -204
- package/scripts/cli/command/update.ts +27 -10
- package/scripts/cli/main.ts +28 -1
- package/src/boot/app.ts +693 -693
- package/src/boot/dependencies.ts +646 -616
- package/src/config/spec.ts +81 -2
- package/src/config/telemetry.ts +46 -0
- package/src/console/api-keys/bucket-spend.ts +15 -4
- package/src/console/backup/contracts.ts +15 -2
- package/src/console/backup/routes.ts +52 -1
- package/src/console/backup/service.ts +60 -4
- package/src/console/console-router.ts +8 -0
- package/src/console/data-management/contracts.ts +93 -0
- package/src/console/data-management/operations.ts +229 -0
- package/src/console/data-management/routes.ts +104 -0
- package/src/console/domain-registration.ts +39 -0
- package/src/console/observability/contracts.ts +524 -485
- package/src/console/observability/store.ts +1115 -1018
- package/src/console/providers/catalog/contracts.ts +6 -0
- package/src/console/providers/catalog/provider-operations.ts +20 -0
- package/src/console/providers/catalog/routes.ts +7 -0
- package/src/console/providers/catalog/store.ts +22 -1
- package/src/console/quota/account-quota-shared.ts +52 -39
- package/src/console/quota/refresh.ts +441 -429
- package/src/console/routing/pools/import-parse.ts +206 -0
- package/src/console/routing/pools/operations.ts +543 -501
- package/src/console/routing/pools/routes.ts +181 -164
- package/src/console/settings/contracts.ts +37 -0
- package/src/console/settings/store.ts +51 -0
- package/src/console/share/share-router.ts +6 -9
- package/src/console/updates/routes.ts +16 -0
- package/src/main.ts +10 -0
- package/src/observability/event-compaction.ts +315 -0
- package/src/observability/event-day-index.ts +442 -0
- package/src/observability/event-log.ts +152 -3
- package/src/observability/payload-capture.ts +373 -344
- package/src/observability/payload-store.ts +440 -366
- package/src/observability/telemetry-status.ts +28 -16
- package/src/persistence/health-event-flush.ts +36 -0
- package/src/persistence/schema.ts +1402 -1360
- package/src/persistence/stores/api-key-store.ts +6 -0
- package/src/persistence/stores/share-store.ts +583 -556
- package/src/persistence/stores/telemetry-store.ts +237 -214
- package/src/protocol/codecs.ts +1 -2
- package/src/protocol/image-clamp.ts +19 -1
- package/src/protocol/registry.ts +8 -0
- package/src/protocol/request/gemini.ts +9 -2
- package/src/protocol/request/messages.ts +14 -2
- package/src/protocol/response/gemini.ts +18 -4
- package/src/providers/adapters/compatible-adapter.ts +3 -0
- package/src/providers/bundled-discovery-providers.ts +122 -119
- package/src/providers/configured-openai-providers.ts +136 -134
- package/src/providers/default-registry.ts +5 -4
- package/src/providers/discovery/openai-model-discovery.ts +5 -0
- package/src/providers/discovery/openrouter-free-roster.ts +15 -17
- package/src/providers/integrations/antigravity/antigravity-quota.ts +11 -3
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +4 -4
- package/src/providers/integrations/buddy/workbuddy/shared.ts +201 -200
- package/src/providers/integrations/cline/cline-quota.ts +132 -128
- package/src/providers/integrations/codex/codex-headers.ts +25 -39
- package/src/providers/integrations/codex/codex.ts +2 -29
- package/src/providers/integrations/cursor/cursor-quota.ts +310 -0
- package/src/providers/integrations/cursor/cursor-request.ts +616 -471
- package/src/providers/integrations/cursor/cursor-session-manager.ts +319 -281
- package/src/providers/integrations/cursor/cursor.ts +1485 -960
- package/src/providers/integrations/cursor/protocol/messages.ts +25 -0
- package/src/providers/integrations/factory/factory.ts +8 -1
- package/src/providers/integrations/gemini.ts +6 -1
- package/src/providers/integrations/grok/grok-quota.ts +279 -177
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -0
- package/src/providers/integrations/grok/grok-ratelimit.ts +218 -0
- package/src/providers/integrations/opencode/provider.ts +531 -532
- package/src/providers/integrations/qoder/qoder-quota.ts +192 -0
- package/src/providers/integrations/tokenharbor.ts +2 -2
- package/src/providers/integrations/trae/trae-catalog.ts +5 -3
- package/src/providers/integrations/trae/trae-quota.ts +384 -0
- package/src/providers/operations/account-health-service.ts +1523 -1457
- package/src/providers/operations/client-versions.ts +9 -2
- package/src/providers/provider-contracts.ts +7 -0
- package/src/providers/provider-metadata.ts +13 -1
- package/src/providers/quota/quota-support.ts +8 -0
- package/src/security/api-key-auth.ts +504 -494
- package/src/transport/dispatch/attempt-finalize.ts +46 -5
- package/src/transport/dispatch/generation.ts +6 -0
- package/src/transport/dispatch/proxy-request.ts +33 -1
- package/src/transport/dispatch/ratelimit-capture.ts +37 -0
- package/src/transport/dispatch/streaming/stream.ts +30 -2
- package/src/transport/dispatch/upstream.ts +9 -0
- package/src/transport/request/preparer.ts +7 -6
- package/src/transport/request/state.ts +9 -0
- package/src/transport/request/tenant-preferences.ts +60 -0
- package/src/transport/routing/route-catalog.ts +854 -799
- package/dist/dashboard/assets/BannedPage-MsQaeQTs.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-BO9nKg9w.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-BO9nKg9w.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DfWHTJ7v.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BrCBQDAc.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-C6Uh33er.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-8aS9hyJu.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-qomXThxX.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-qomXThxX.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-C8hKyXBe.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-C-ExCy1V.js +0 -1
- package/dist/dashboard/assets/OverviewPage-C-ExCy1V.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-tr811uaF.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-tr811uaF.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BS9IEpvv.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-BS9IEpvv.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-Rn_qXScb.js +0 -7
- package/dist/dashboard/assets/ProxyPage-Rn_qXScb.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BihkgQSq.js +0 -1
- package/dist/dashboard/assets/QuotaPage-BihkgQSq.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-DhiEjm6T.js +0 -1
- package/dist/dashboard/assets/SettingsPage-DhiEjm6T.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-CXjUlfqr.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-C-_oD3F2.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-u9IxmYOI.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BEVLVrwP.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-uNywYlfv.js.gz +0 -0
- package/dist/dashboard/assets/app-DxivpfYG.css +0 -2
- package/dist/dashboard/assets/app-DxivpfYG.css.gz +0 -0
- package/dist/dashboard/assets/assert-NHwYq95t.js.gz +0 -0
- package/dist/dashboard/assets/badge-6uzDDvqs.js.gz +0 -0
- package/dist/dashboard/assets/banner-4_WNg2WZ.js.gz +0 -0
- package/dist/dashboard/assets/card--AtFedAq.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-B3e7dx3N.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-NreUx6c8.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BtpsJU98.js.gz +0 -0
- package/dist/dashboard/assets/download-A0SW9OCW.js +0 -1
- package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
- package/dist/dashboard/assets/drawer-WwwtzihF.js.gz +0 -0
- package/dist/dashboard/assets/entry-Dj9AR0gY.js +0 -4
- package/dist/dashboard/assets/entry-Dj9AR0gY.js.gz +0 -0
- package/dist/dashboard/assets/entry-TJIye_jT.js +0 -1
- package/dist/dashboard/assets/entry-TJIye_jT.js.gz +0 -0
- package/dist/dashboard/assets/entry-jy9_4-Ke.js +0 -1
- package/dist/dashboard/assets/entry-jy9_4-Ke.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-D_eM3IgG.js.gz +0 -0
- package/dist/dashboard/assets/index-DCBtROoK.js.gz +0 -0
- package/dist/dashboard/assets/inline-BcNGF8Gr.js.gz +0 -0
- package/dist/dashboard/assets/input-BG41et9G.js.gz +0 -0
- package/dist/dashboard/assets/layout-C8mer_Ak.js.gz +0 -0
- package/dist/dashboard/assets/list-row-Gh_W4OR_.js.gz +0 -0
- package/dist/dashboard/assets/live-BJGYRBnR.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-DeVLDA7I.js +0 -1
- package/dist/dashboard/assets/model-bans-DeVLDA7I.js.gz +0 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
- package/dist/dashboard/assets/numbers-Bs4LOOuf.js +0 -2
- package/dist/dashboard/assets/numbers-Bs4LOOuf.js.gz +0 -0
- package/dist/dashboard/assets/routing-CcusqXA7.js.gz +0 -0
- package/dist/dashboard/assets/select-DsX5QT8K.js.gz +0 -0
- package/dist/dashboard/assets/settings-mpmXTgUa.js.gz +0 -0
- package/dist/dashboard/assets/sparkles-BRaRUxAU.js +0 -1
- package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
- package/dist/dashboard/assets/stack-DwLmjUFK.js.gz +0 -0
- package/dist/dashboard/assets/state-DYdQmdAl.js.gz +0 -0
- package/dist/dashboard/assets/switch-CidSfbG7.js +0 -1
- package/dist/dashboard/assets/switch-CidSfbG7.js.gz +0 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js +0 -1
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-w4u4CVue.js.gz +0 -0
- /package/dist/dashboard/assets/{app-CafWtVXA.js → app-DCQZRmHY.js} +0 -0
- /package/dist/dashboard/assets/{app-CafWtVXA.js.gz → app-DCQZRmHY.js.gz} +0 -0
package/src/config/spec.ts
CHANGED
|
@@ -148,7 +148,62 @@ export const CONFIG_SPEC = {
|
|
|
148
148
|
*/
|
|
149
149
|
CARTETHYIA_DISABLED_TASKS: { kind: "list" },
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* How long a day's telemetry is kept, in days.
|
|
153
|
+
*
|
|
154
|
+
* The retention unit is the whole day: a day's event file and its index are
|
|
155
|
+
* deleted together, and the current day is never touched. Past this age an
|
|
156
|
+
* individual request no longer exists anywhere, so its detail is gone —
|
|
157
|
+
* totals, breakdowns and spend are unaffected, because the index keeps
|
|
158
|
+
* running sums rather than the rows.
|
|
159
|
+
*
|
|
160
|
+
* This is the knob that decides how long the request list, per-request
|
|
161
|
+
* filters, and the detail drawer stay complete.
|
|
162
|
+
*/
|
|
151
163
|
CARTETHYIA_TELEMETRY_RETENTION_DAYS: { kind: "int", default: 30, min: 3, max: 365 },
|
|
164
|
+
/**
|
|
165
|
+
* Age at which a request's detail is described as no longer stored.
|
|
166
|
+
*
|
|
167
|
+
* Retention itself is `CARTETHYIA_TELEMETRY_RETENTION_DAYS`; this only words
|
|
168
|
+
* the message shown when a request that old is asked for. Kept because the
|
|
169
|
+
* message is operator-facing and was written against this knob.
|
|
170
|
+
*/
|
|
171
|
+
CARTETHYIA_TELEMETRY_COMPACT_AFTER_DAYS: { kind: "int", default: 7, min: 1, max: 365 },
|
|
172
|
+
/**
|
|
173
|
+
* How often each day's index is brought up to date, in minutes.
|
|
174
|
+
*
|
|
175
|
+
* Bringing an index up to date means folding the bytes appended since the
|
|
176
|
+
* last pass — not re-reading the day. Reads fold before they answer too, so
|
|
177
|
+
* raising this does not make the numbers stale; it only leaves the on-disk
|
|
178
|
+
* index further behind, and the next read pays for the remainder.
|
|
179
|
+
*/
|
|
180
|
+
CARTETHYIA_TELEMETRY_COMPACT_INTERVAL_MINUTES: {
|
|
181
|
+
kind: "int",
|
|
182
|
+
default: 60,
|
|
183
|
+
min: 5,
|
|
184
|
+
max: 1440,
|
|
185
|
+
},
|
|
186
|
+
/**
|
|
187
|
+
* Inert. Accepted so existing deployments keep booting; nothing reads it.
|
|
188
|
+
*
|
|
189
|
+
* It sized the one-file-per-window scheme — events were written to a file
|
|
190
|
+
* per this many minutes, 96 a day at the default. Events now append to one
|
|
191
|
+
* file per UTC day, so there is no window left to size.
|
|
192
|
+
*/
|
|
193
|
+
CARTETHYIA_TELEMETRY_WINDOW_MINUTES: {
|
|
194
|
+
kind: "int",
|
|
195
|
+
default: 15,
|
|
196
|
+
min: 1,
|
|
197
|
+
max: 1440,
|
|
198
|
+
},
|
|
199
|
+
/**
|
|
200
|
+
* Inert. Accepted so existing deployments keep booting; nothing reads it.
|
|
201
|
+
*
|
|
202
|
+
* It capped how many window files one pass could fold. Folding is driven by
|
|
203
|
+
* a byte offset into the day file now and never deletes the file, so there
|
|
204
|
+
* is no per-pass batch to bound.
|
|
205
|
+
*/
|
|
206
|
+
CARTETHYIA_TELEMETRY_COMPACT_MAX_FILES: { kind: "int", default: 48, min: 1, max: 10_000 },
|
|
152
207
|
CARTETHYIA_TELEMETRY_PAYLOAD_MAX_BYTES: {
|
|
153
208
|
kind: "int",
|
|
154
209
|
default: 16 * 1024 * 1024,
|
|
@@ -157,7 +212,14 @@ export const CONFIG_SPEC = {
|
|
|
157
212
|
},
|
|
158
213
|
|
|
159
214
|
// Upstream request timeout + retry backoff
|
|
160
|
-
|
|
215
|
+
//
|
|
216
|
+
// Strictly under 90s: a client whose own deadline is 90s must never be
|
|
217
|
+
// waiting on a gateway that is still holding a request open past it, and a
|
|
218
|
+
// provider that has not answered within 75s is not answering — the request
|
|
219
|
+
// is queued upstream or the egress is stuck, and the honest move is to fail
|
|
220
|
+
// over to another account rather than burn the caller's whole budget on one.
|
|
221
|
+
// 120s outlived the clients that call this, which cap lower.
|
|
222
|
+
CARTETHYIA_UPSTREAM_TIMEOUT_MS: { kind: "int", default: 75_000, min: 5_000, max: 600_000 },
|
|
161
223
|
CARTETHYIA_STREAM_STALL_TIMEOUT_MS: {
|
|
162
224
|
kind: "int",
|
|
163
225
|
default: 360_000,
|
|
@@ -181,7 +243,24 @@ export const CONFIG_SPEC = {
|
|
|
181
243
|
CARTETHYIA_ACCOUNT_QUOTA_COOLDOWN_MS: { kind: "int", default: 3_600_000, min: 0, max: 86_400_000 },
|
|
182
244
|
CARTETHYIA_ACCOUNT_MODEL_CAPACITY_COOLDOWN_MS: { kind: "int", default: 120_000, min: 0, max: 86_400_000 },
|
|
183
245
|
CARTETHYIA_ACCOUNT_TRANSIENT_COOLDOWN_MS: { kind: "int", default: 30_000, min: 0, max: 86_400_000 },
|
|
184
|
-
|
|
246
|
+
/**
|
|
247
|
+
* An unmapped upstream failure cools the account down for 15 minutes.
|
|
248
|
+
*
|
|
249
|
+
* It used to be 60s, which put the account straight back into rotation and
|
|
250
|
+
* re-dialled an upstream that had just answered something this gateway does
|
|
251
|
+
* not understand — so the same unknown error was retried every minute, and
|
|
252
|
+
* an account that was actually broken contributed a steady stream of failed
|
|
253
|
+
* attempts instead of one. Fifteen minutes is long enough for a transient
|
|
254
|
+
* provider condition to clear on its own and short enough that no operator
|
|
255
|
+
* action is needed to recover it. It is still a cooldown with a deadline,
|
|
256
|
+
* never `disabled`, so nothing here can strand an account.
|
|
257
|
+
*/
|
|
258
|
+
CARTETHYIA_ACCOUNT_UNCLASSIFIED_COOLDOWN_MS: {
|
|
259
|
+
kind: "int",
|
|
260
|
+
default: 900_000,
|
|
261
|
+
min: 0,
|
|
262
|
+
max: 86_400_000,
|
|
263
|
+
},
|
|
185
264
|
CARTETHYIA_ACCOUNT_COOLDOWN_ESCALATION_MAX_MS: { kind: "int", default: 3_600_000, min: 0, max: 86_400_000 },
|
|
186
265
|
CARTETHYIA_POOL_COOLDOWN_MS: { kind: "int", default: 120_000, min: 0, max: 86_400_000 },
|
|
187
266
|
CARTETHYIA_ACCOUNT_AUTH_REJECTED_COOLDOWN_MS: { kind: "int", default: 300_000, min: 0, max: 86_400_000 },
|
package/src/config/telemetry.ts
CHANGED
|
@@ -19,3 +19,49 @@ export function resolveTelemetryPayloadMaxBytes(): number {
|
|
|
19
19
|
CONFIG_SPEC.CARTETHYIA_TELEMETRY_PAYLOAD_MAX_BYTES,
|
|
20
20
|
);
|
|
21
21
|
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Age at which hourly telemetry files are compacted into a per-day summary.
|
|
25
|
+
*
|
|
26
|
+
* Clamped below the retention window: compacting at or past the delete age
|
|
27
|
+
* would fold a file and throw it away in the same pass, spending the read for
|
|
28
|
+
* totals that were about to be discarded.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveTelemetryCompactAfterDays(): number {
|
|
31
|
+
const compacted = readInt(
|
|
32
|
+
"CARTETHYIA_TELEMETRY_COMPACT_AFTER_DAYS",
|
|
33
|
+
CONFIG_SPEC.CARTETHYIA_TELEMETRY_COMPACT_AFTER_DAYS,
|
|
34
|
+
);
|
|
35
|
+
return Math.min(compacted, resolveTelemetryRetentionDays());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Minutes between compaction passes. */
|
|
39
|
+
export function resolveTelemetryCompactIntervalMs(): number {
|
|
40
|
+
const minutes = readInt(
|
|
41
|
+
"CARTETHYIA_TELEMETRY_COMPACT_INTERVAL_MINUTES",
|
|
42
|
+
CONFIG_SPEC.CARTETHYIA_TELEMETRY_COMPACT_INTERVAL_MINUTES,
|
|
43
|
+
);
|
|
44
|
+
return minutes * 60_000;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Minutes of events per source file, before it is folded into the day summary.
|
|
49
|
+
*
|
|
50
|
+
* Defaults to the sweep interval so exactly one window closes per pass: a
|
|
51
|
+
* window longer than the interval would leave files waiting several passes, and
|
|
52
|
+
* a shorter one would let several close between passes.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveTelemetryWindowMinutes(): number {
|
|
55
|
+
return readInt(
|
|
56
|
+
"CARTETHYIA_TELEMETRY_WINDOW_MINUTES",
|
|
57
|
+
CONFIG_SPEC.CARTETHYIA_TELEMETRY_WINDOW_MINUTES,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Maximum hourly files folded in one compaction pass. */
|
|
62
|
+
export function resolveTelemetryCompactMaxFiles(): number {
|
|
63
|
+
return readInt(
|
|
64
|
+
"CARTETHYIA_TELEMETRY_COMPACT_MAX_FILES",
|
|
65
|
+
CONFIG_SPEC.CARTETHYIA_TELEMETRY_COMPACT_MAX_FILES,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -2,7 +2,7 @@ import { eq } from "drizzle-orm";
|
|
|
2
2
|
|
|
3
3
|
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
4
4
|
import { apiKeys } from "../../persistence/schema";
|
|
5
|
-
import {
|
|
5
|
+
import { queryIndexedBuckets } from "../../observability/event-day-index";
|
|
6
6
|
import { utcDayStart, utcMonthStart } from "../shared/day";
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -33,9 +33,20 @@ export async function familyBucketSpend(
|
|
|
33
33
|
.where(eq(apiKeys.parentKeyId, keyId));
|
|
34
34
|
const keyIds = [keyId, ...children.map((child) => child.id)];
|
|
35
35
|
const spentSince = async (start: Date): Promise<number> => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
// Folded first, so the index covers every event written so far including the
|
|
37
|
+
// current day's — then read from the index alone.
|
|
38
|
+
//
|
|
39
|
+
// Deliberately not index *plus* events: they hold the same requests, and
|
|
40
|
+
// adding them doubled every bucket. Folding first is what makes the index
|
|
41
|
+
// sufficient on its own; the watermark is the point up to which it is
|
|
42
|
+
// complete.
|
|
43
|
+
const allowed = new Set(keyIds);
|
|
44
|
+
const indexed = await queryIndexedBuckets({ since: start, foldFirst: true }).catch(
|
|
45
|
+
() => [],
|
|
46
|
+
);
|
|
47
|
+
return indexed
|
|
48
|
+
.filter((bucket) => bucket.apiKeyId !== null && allowed.has(bucket.apiKeyId))
|
|
49
|
+
.reduce((sum, bucket) => sum + bucket.inputTokens + bucket.outputTokens, 0);
|
|
39
50
|
};
|
|
40
51
|
const [daily, monthly] = await Promise.all([
|
|
41
52
|
spentSince(utcDayStart(now)),
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* as sensitive as the database.
|
|
21
21
|
*/
|
|
22
22
|
import { getTableColumns, getTableName, type Column, type Table } from "drizzle-orm";
|
|
23
|
+
import { resolveMaxBodyBytes } from "../../config/runtime";
|
|
23
24
|
import {
|
|
24
25
|
apiKeys,
|
|
25
26
|
cliToolMappings,
|
|
@@ -45,8 +46,20 @@ import {
|
|
|
45
46
|
export const BACKUP_APP = "cartethyia";
|
|
46
47
|
export const BACKUP_VERSION = 1;
|
|
47
48
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Upper bound for restore payloads.
|
|
51
|
+
*
|
|
52
|
+
* Deliberately not larger than the listener's own body cap. It used to claim
|
|
53
|
+
* 8 GiB while the server admitted 4 MiB, so the number described a limit that
|
|
54
|
+
* could never be reached: a file above the listener's cap is cut off before
|
|
55
|
+
* this check runs, and a large upload fails as a reset connection rather than
|
|
56
|
+
* the 413 this constant promises. The bound now states what the install
|
|
57
|
+
* actually admits, so the message an operator sees matches what happened.
|
|
58
|
+
*
|
|
59
|
+
* Raise `CARTETHYIA_SERVER_MAX_BODY_BYTES` to accept larger backups; this
|
|
60
|
+
* constant follows it rather than the two drifting apart again.
|
|
61
|
+
*/
|
|
62
|
+
export const MAX_BACKUP_BYTES = resolveMaxBodyBytes();
|
|
50
63
|
|
|
51
64
|
/** Defense-in-depth row cap per table, after the byte bound. */
|
|
52
65
|
export const MAX_ROWS_PER_TABLE = 500_000;
|
|
@@ -78,6 +78,24 @@ const BACKUP_SCOPES = ["dashboard:write"] as const;
|
|
|
78
78
|
const importBody = t.Object({
|
|
79
79
|
password: t.String(),
|
|
80
80
|
backup: t.Unknown(),
|
|
81
|
+
/**
|
|
82
|
+
* Table names to restore. Absent or empty restores every table in the file,
|
|
83
|
+
* which is what the dashboard's checklist defaults to and what the API did
|
|
84
|
+
* before selection existed.
|
|
85
|
+
*/
|
|
86
|
+
tables: t.Optional(t.Array(t.String())),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The inspect body: a file, no password.
|
|
91
|
+
*
|
|
92
|
+
* Inspection writes nothing and reads no secret — it only reports which tables
|
|
93
|
+
* the file carries and which of them the live schema has — so it needs no
|
|
94
|
+
* re-authentication. Requiring the console password here would mean typing it
|
|
95
|
+
* twice for one restore.
|
|
96
|
+
*/
|
|
97
|
+
const inspectBody = t.Object({
|
|
98
|
+
backup: t.Unknown(),
|
|
81
99
|
});
|
|
82
100
|
|
|
83
101
|
const exportQuery = t.Object({
|
|
@@ -164,7 +182,12 @@ export function createBackupRoutes(config: BackupRoutesConfig): Elysia {
|
|
|
164
182
|
// from nowhere else. A request body may not name the tenant it writes
|
|
165
183
|
// into: that would let any holder of the scope import into, and
|
|
166
184
|
// overwrite, a tenant they are not.
|
|
167
|
-
const result = await config.backupFor(request).restore(
|
|
185
|
+
const result = await config.backupFor(request).restore(
|
|
186
|
+
body.password,
|
|
187
|
+
body.backup,
|
|
188
|
+
access.tenantId,
|
|
189
|
+
body.tables === undefined ? undefined : { tables: body.tables },
|
|
190
|
+
);
|
|
168
191
|
// Routing-visible write: the restore has committed, so the cached
|
|
169
192
|
// snapshot must be rebuilt before the next `/v1/*` dispatch.
|
|
170
193
|
await config.snapshotInvalidator?.invalidate();
|
|
@@ -214,5 +237,33 @@ export function createBackupRoutes(config: BackupRoutesConfig): Elysia {
|
|
|
214
237
|
} catch (error) {
|
|
215
238
|
return errorResponse(error, set, "Delete all failed");
|
|
216
239
|
}
|
|
240
|
+
})
|
|
241
|
+
.post("/inspect", { body: inspectBody }, async ({ request, body, set }) => {
|
|
242
|
+
try {
|
|
243
|
+
const access = requireAnyScope(config.accessResolver(request), BACKUP_SCOPES);
|
|
244
|
+
if (access.tenantId === null) {
|
|
245
|
+
throw new ConsoleDomainError("tenant_required", 403, "Tenant isolation required");
|
|
246
|
+
}
|
|
247
|
+
// Same byte bound as the import, for the same reason: the file is
|
|
248
|
+
// parsed here too, and an unbounded one would be buffered before any
|
|
249
|
+
// validation could refuse it.
|
|
250
|
+
const declared = Number(request.headers.get("content-length") ?? "0");
|
|
251
|
+
const actualBytes = JSON.stringify(body ?? null).length;
|
|
252
|
+
if (
|
|
253
|
+
(Number.isFinite(declared) && declared > MAX_BACKUP_BYTES) ||
|
|
254
|
+
actualBytes > MAX_BACKUP_BYTES
|
|
255
|
+
) {
|
|
256
|
+
set.status = 413;
|
|
257
|
+
return {
|
|
258
|
+
error: `backup exceeds the ${MAX_BACKUP_BYTES} byte limit`,
|
|
259
|
+
code: "request_too_large",
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
// Reads nothing and writes nothing: the checklist the restore wizard
|
|
263
|
+
// shows, so an operator can pick sections before typing a password.
|
|
264
|
+
return config.backupFor(request).inspect(body.backup);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
return errorResponse(error, set, "Backup inspection failed");
|
|
267
|
+
}
|
|
217
268
|
}) as unknown as Elysia;
|
|
218
269
|
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* re-authentication.
|
|
20
20
|
*/
|
|
21
21
|
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
22
|
+
import type { Table } from "drizzle-orm";
|
|
22
23
|
import {
|
|
23
24
|
models,
|
|
24
25
|
providerAccounts,
|
|
@@ -29,7 +30,12 @@ import {
|
|
|
29
30
|
} from "../../persistence/schema";
|
|
30
31
|
import { ConsoleDomainError } from "../shared/errors";
|
|
31
32
|
import { tablesForSection, tableName } from "./contracts";
|
|
32
|
-
import type {
|
|
33
|
+
import type {
|
|
34
|
+
BackupSection,
|
|
35
|
+
RestoreWarning,
|
|
36
|
+
ValidatedRestore,
|
|
37
|
+
ValidatedTable,
|
|
38
|
+
} from "./contracts";
|
|
33
39
|
import {
|
|
34
40
|
applyRestore,
|
|
35
41
|
decompressBackup,
|
|
@@ -90,6 +96,53 @@ export interface ImportResult {
|
|
|
90
96
|
readonly format: "native" | "nine_router";
|
|
91
97
|
}
|
|
92
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Which parts of a backup file a restore should actually write.
|
|
101
|
+
*
|
|
102
|
+
* Absent or empty means "everything", which is the historical behavior and
|
|
103
|
+
* what the dashboard's checklist defaults to. A table named here but absent
|
|
104
|
+
* from the file is simply not restored — it is not an error, because the
|
|
105
|
+
* checklist is built from an inspection of *this* file and a selection never
|
|
106
|
+
* invents a table.
|
|
107
|
+
*/
|
|
108
|
+
export interface BackupRestoreSelection {
|
|
109
|
+
/** Table names to restore; absent or empty restores every table present. */
|
|
110
|
+
readonly tables?: readonly string[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Narrows a validated backup to the tables the operator selected.
|
|
115
|
+
*
|
|
116
|
+
* The tenant row is the one thing that is not independently selectable: it is
|
|
117
|
+
* not a table in either section (`BACKUP_SECTIONS` are config and telemetry),
|
|
118
|
+
* so it never appears on the checklist, and every tenant-scoped table
|
|
119
|
+
* cascades from it. Dropping it because it was not named would make a partial
|
|
120
|
+
* restore fail on a foreign key for a table the operator *did* ask for. So it
|
|
121
|
+
* rides along with any config selection and is written only when one is made.
|
|
122
|
+
*
|
|
123
|
+
* A table named in the selection but missing from the file is a no-op rather
|
|
124
|
+
* than an error: the checklist is built from an inspection of this file, but
|
|
125
|
+
* the selection is validated as names only, and refusing to restore anything
|
|
126
|
+
* over one stale name would be worse than ignoring it.
|
|
127
|
+
*/
|
|
128
|
+
function restrictToSelection(
|
|
129
|
+
validation: ValidatedRestore,
|
|
130
|
+
selection: BackupRestoreSelection | undefined,
|
|
131
|
+
): ValidatedRestore {
|
|
132
|
+
const wanted = selection?.tables;
|
|
133
|
+
if (wanted === undefined || wanted.length === 0) return validation;
|
|
134
|
+
const selected = new Set(wanted);
|
|
135
|
+
const tables = new Map<Table, ValidatedTable>();
|
|
136
|
+
for (const [table, entry] of validation.tables) {
|
|
137
|
+
if (selected.has(tableName(table))) tables.set(table, entry);
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
...validation,
|
|
141
|
+
tables,
|
|
142
|
+
tenantRows: tables.size > 0 ? validation.tenantRows : [],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
93
146
|
function sectionsOrDefault(sections: readonly BackupSection[] | undefined): readonly BackupSection[] {
|
|
94
147
|
if (sections === undefined || sections.length === 0) return ["config"];
|
|
95
148
|
for (const section of sections) {
|
|
@@ -246,6 +299,7 @@ export class BackupService {
|
|
|
246
299
|
password: unknown,
|
|
247
300
|
payload: unknown,
|
|
248
301
|
tenantId: string,
|
|
302
|
+
selection?: BackupRestoreSelection,
|
|
249
303
|
): Promise<ImportResult> {
|
|
250
304
|
if (typeof password !== "string" || !(await this.options.verifyPassword(password))) {
|
|
251
305
|
throw new ConsoleDomainError("unauthorized", 401, "password is incorrect");
|
|
@@ -258,11 +312,11 @@ export class BackupService {
|
|
|
258
312
|
|
|
259
313
|
if (detected.kind === "nine_router") {
|
|
260
314
|
const { payload: converted, report } = convert9RouterBackup(detected.payload, tenantId);
|
|
261
|
-
const result = await this.apply(converted, tenantId);
|
|
315
|
+
const result = await this.apply(converted, tenantId, selection);
|
|
262
316
|
return { ...result, report, format: "nine_router" };
|
|
263
317
|
}
|
|
264
318
|
|
|
265
|
-
const result = await this.apply(detected.payload, tenantId);
|
|
319
|
+
const result = await this.apply(detected.payload, tenantId, selection);
|
|
266
320
|
return { ...result, format: "native" };
|
|
267
321
|
}
|
|
268
322
|
|
|
@@ -348,13 +402,15 @@ export class BackupService {
|
|
|
348
402
|
private async apply(
|
|
349
403
|
payload: unknown,
|
|
350
404
|
tenantId: string,
|
|
405
|
+
selection?: BackupRestoreSelection,
|
|
351
406
|
): Promise<{ restored: Record<string, number>; skipped: Record<string, number>; warnings: readonly RestoreWarning[] }> {
|
|
352
407
|
const validation = validateRestorePayload(payload, tenantId);
|
|
353
408
|
if (!validation.ok) {
|
|
354
409
|
throw new ConsoleDomainError("invalid_request", 400, validation.error);
|
|
355
410
|
}
|
|
356
411
|
try {
|
|
357
|
-
const
|
|
412
|
+
const scoped = restrictToSelection(validation.value, selection);
|
|
413
|
+
const compatible = await omitRowsForUnavailableProviders(this.options.db, scoped, tenantId);
|
|
358
414
|
const applied = await applyRestore(this.options.db, compatible, restoreOrder(), tenantId);
|
|
359
415
|
return { ...applied, warnings: compatible.warnings };
|
|
360
416
|
} catch (error) {
|
|
@@ -48,6 +48,13 @@ export interface ConsoleApiCompositionDeps {
|
|
|
48
48
|
readonly routeSnapshotService: { invalidate(): Promise<number> };
|
|
49
49
|
readonly poolSelector: NetworkPoolSelector;
|
|
50
50
|
readonly telemetryBuffer: TelemetryBatchBuffer;
|
|
51
|
+
/**
|
|
52
|
+
* Queued writers the console can drain on demand (telemetry, usage, health
|
|
53
|
+
* events). Optional for the same reason the other reduced composition
|
|
54
|
+
* surface is: absent means the flush control is not offered rather than the
|
|
55
|
+
* console failing to build.
|
|
56
|
+
*/
|
|
57
|
+
readonly flushBuffers?: { flush(): Promise<void> };
|
|
51
58
|
/**
|
|
52
59
|
* The request state store owns the live in-flight gauge, so the console reads
|
|
53
60
|
* its snapshot/subscription from here rather than a free-standing module.
|
|
@@ -189,6 +196,7 @@ export function createConsoleRouter(deps: ConsoleApiCompositionDeps): Elysia {
|
|
|
189
196
|
admissionService: deps.admissionService,
|
|
190
197
|
...(deps.modelStrikes ? { modelStrikes: deps.modelStrikes } : {}),
|
|
191
198
|
readRoutingAccountInflight: deps.readRoutingAccountInflight,
|
|
199
|
+
...(deps.flushBuffers ? { flushBuffers: deps.flushBuffers } : {}),
|
|
192
200
|
credentialService,
|
|
193
201
|
// The backup surface re-authenticates the operator, so it needs the current
|
|
194
202
|
// user's hash. Read from the session on the request that asks for it, never
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disk housekeeping: what the telemetry and payload directories hold, and the
|
|
3
|
+
* reclaim actions an operator can run from the console.
|
|
4
|
+
*
|
|
5
|
+
* Everything here is files on disk, except one deliberate row-level action.
|
|
6
|
+
* Reclaiming a file is safe because the data it held is already past the point
|
|
7
|
+
* where the gateway would read it: payload frames expire 15 minutes after they
|
|
8
|
+
* are written, and telemetry files past the retention window are outside every
|
|
9
|
+
* query's scan bound. Deleting rows is a different decision with a different
|
|
10
|
+
* blast radius, and the only row data the console offers purging is the health
|
|
11
|
+
* & error log — purely an operator's diagnostic trail with no request-path
|
|
12
|
+
* reader, unlike everything else in this panel.
|
|
13
|
+
*
|
|
14
|
+
* Sizes are bytes as `number`. A directory is scanned on each read rather than
|
|
15
|
+
* cached, because the point of the panel is to show what is true now — a
|
|
16
|
+
* stale size would be worse than none.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** One measured category of on-disk data. */
|
|
20
|
+
export interface DiskUsageBucket {
|
|
21
|
+
/** Stable id, so the dashboard can key rows without matching on copy. */
|
|
22
|
+
readonly id: "telemetry_hourly" | "telemetry_compacted" | "payload_frames";
|
|
23
|
+
readonly label: string;
|
|
24
|
+
readonly bytes: number;
|
|
25
|
+
readonly files: number;
|
|
26
|
+
/** Why this data exists, shown next to the size. */
|
|
27
|
+
readonly note: string;
|
|
28
|
+
/** Bytes reclaimable right now, when that is knowable without a dry run. */
|
|
29
|
+
readonly reclaimableBytes: number | null;
|
|
30
|
+
readonly reclaimableFiles: number | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DiskUsageResponse {
|
|
34
|
+
readonly buckets: readonly DiskUsageBucket[];
|
|
35
|
+
readonly totalBytes: number;
|
|
36
|
+
/** Retention in days; telemetry older than this is outside every scan. */
|
|
37
|
+
readonly retentionDays: number;
|
|
38
|
+
/** Payload frames live this long before they are reclaimable. */
|
|
39
|
+
readonly payloadRetentionMs: number;
|
|
40
|
+
/**
|
|
41
|
+
* Stored health & error rows, for the purge row's label. Absent when the
|
|
42
|
+
* composition does not wire the health-event store (a reduced console),
|
|
43
|
+
* which is also when the purge control is not offered.
|
|
44
|
+
*/
|
|
45
|
+
readonly healthEventCount?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type ReclaimAction = "payload_frames" | "telemetry_retention" | "telemetry_hourly";
|
|
49
|
+
|
|
50
|
+
export interface ReclaimResponse {
|
|
51
|
+
readonly action: ReclaimAction;
|
|
52
|
+
readonly deletedFiles: number;
|
|
53
|
+
readonly reclaimedBytes: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Row-level deletion targets. The health & error log is the only one. */
|
|
57
|
+
export type PurgeTarget = "health_events";
|
|
58
|
+
|
|
59
|
+
export interface PurgeResponse {
|
|
60
|
+
readonly target: PurgeTarget;
|
|
61
|
+
/** Rows actually deleted — the number the operator's decision produced. */
|
|
62
|
+
readonly deleted: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface FlushResponse {
|
|
66
|
+
readonly flushed: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A reclaim the caller is not allowed to make.
|
|
71
|
+
*
|
|
72
|
+
* Kept as a named class so the route maps it to 403 rather than letting it
|
|
73
|
+
* surface as a 500 — an operator asking for something out of policy is a
|
|
74
|
+
* normal request, not a failure.
|
|
75
|
+
*/
|
|
76
|
+
export class ReclaimNotAllowedError extends Error {
|
|
77
|
+
readonly status = 403;
|
|
78
|
+
|
|
79
|
+
constructor(message: string) {
|
|
80
|
+
super(message);
|
|
81
|
+
this.name = "ReclaimNotAllowedError";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A purge whose backing store is not wired in this composition. */
|
|
86
|
+
export class PurgeNotWiredError extends Error {
|
|
87
|
+
readonly status = 501;
|
|
88
|
+
|
|
89
|
+
constructor() {
|
|
90
|
+
super("Health & error log purging is not offered in this deployment's console composition.");
|
|
91
|
+
this.name = "PurgeNotWiredError";
|
|
92
|
+
}
|
|
93
|
+
}
|