@reddoorla/maintenance 0.88.2 → 0.89.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 (116) hide show
  1. package/dist/{announce-GKD5YJNB.js → announce-OJFAS5JT.js} +6 -6
  2. package/dist/{blux-DN4MI532.js → blux-EATHOHKK.js} +5 -5
  3. package/dist/{chunk-7UAYMAKR.js → chunk-3VQWSE3F.js} +4 -4
  4. package/dist/{chunk-A2UNJIKH.js → chunk-5PWB3JHJ.js} +6 -4
  5. package/dist/chunk-5PWB3JHJ.js.map +1 -0
  6. package/dist/{chunk-K6SLF6GI.js → chunk-62NYED6E.js} +6 -6
  7. package/dist/{chunk-B23FFWU4.js → chunk-AEXJDCJN.js} +96 -19
  8. package/dist/{chunk-B23FFWU4.js.map → chunk-AEXJDCJN.js.map} +1 -1
  9. package/dist/{chunk-7FFUBF4I.js → chunk-AX22DH6S.js} +2 -2
  10. package/dist/{chunk-7TMWBXX7.js → chunk-DXKF552B.js} +269 -7
  11. package/dist/chunk-DXKF552B.js.map +1 -0
  12. package/dist/{chunk-6I6L3CMX.js → chunk-KGUKL3CV.js} +2 -2
  13. package/dist/{chunk-ULHPALI5.js → chunk-NDCOOI3V.js} +2 -2
  14. package/dist/{chunk-GQDDWYU2.js → chunk-QBAAZY7W.js} +1 -1
  15. package/dist/{chunk-GQDDWYU2.js.map → chunk-QBAAZY7W.js.map} +1 -1
  16. package/dist/{chunk-KMCZA7CH.js → chunk-QLIUDTY6.js} +2 -2
  17. package/dist/{chunk-XP5WM2LV.js → chunk-SKI23ZZH.js} +4 -4
  18. package/dist/chunk-SVAAONYB.js +21 -0
  19. package/dist/chunk-SVAAONYB.js.map +1 -0
  20. package/dist/{chunk-DGZWFI2G.js → chunk-VBEKL445.js} +17 -8
  21. package/dist/chunk-VBEKL445.js.map +1 -0
  22. package/dist/{chunk-ANJNTAMM.js → chunk-ZOCJBJQV.js} +4 -2
  23. package/dist/{chunk-J3WEZGIN.js → chunk-ZRI5VRPG.js} +4 -4
  24. package/dist/cli/bin.js +27 -18
  25. package/dist/cli/bin.js.map +1 -1
  26. package/dist/cli/commands/audit.js +5 -5
  27. package/dist/{db-V6UIS433.js → db-DAJX7NNW.js} +51 -10
  28. package/dist/db-DAJX7NNW.js.map +1 -0
  29. package/dist/{digest-2JEAEJK6.js → digest-T6TAZNLF.js} +73 -16
  30. package/dist/digest-T6TAZNLF.js.map +1 -0
  31. package/dist/digest-collectors-NI2M3UTC.js +50 -0
  32. package/dist/digest-state-3KHZF7GD.js +50 -0
  33. package/dist/digest-state-3KHZF7GD.js.map +1 -0
  34. package/dist/{dump-EPL7AZFR.js → dump-QK7LNOGC.js} +14 -1
  35. package/dist/{dump-EPL7AZFR.js.map → dump-QK7LNOGC.js.map} +1 -1
  36. package/dist/{email-SL7464YI.js → email-LJQT5J7F.js} +6 -7
  37. package/dist/{email-SL7464YI.js.map → email-LJQT5J7F.js.map} +1 -1
  38. package/dist/{ensure-site-FU6527OU.js → ensure-site-NSNY2QHN.js} +2 -2
  39. package/dist/{fleet-state-R5LU7IC3.js → fleet-state-3LQUQHYD.js} +2 -2
  40. package/dist/{forms-notify-target-SGDGXXK5.js → forms-notify-target-LYSKME5L.js} +2 -2
  41. package/dist/freeze-A6MUCZU4.js +9 -0
  42. package/dist/{github-signals-GWGCKEE2.js → github-signals-4YML6NY5.js} +4 -4
  43. package/dist/{health-mirror-GCDN7BJL.js → health-mirror-7LKC7Q5Z.js} +5 -5
  44. package/dist/index.d.ts +19 -4
  45. package/dist/index.js +29 -28
  46. package/dist/index.js.map +1 -1
  47. package/dist/{init-FGTYNGUI.js → init-NHKADHB6.js} +7 -7
  48. package/dist/{launch-5H7LPEGP.js → launch-VQJ4Z35C.js} +6 -6
  49. package/dist/{orchestrate-JNKCWU5K.js → orchestrate-6HROYTM6.js} +4 -4
  50. package/dist/{parity-NBYZUBFU.js → parity-623WNFZG.js} +3 -3
  51. package/dist/{pipeline-WYTXSLHX.js → pipeline-MQJD4CKN.js} +6 -2
  52. package/dist/{preflight-IMTYDDD4.js → preflight-LIN5NCYU.js} +5 -5
  53. package/dist/{prismic-ci-MY2U6NN7.js → prismic-ci-PZ5LCPWT.js} +4 -4
  54. package/dist/{prismic-models-NJN5JORF.js → prismic-models-6OYYZUR5.js} +3 -3
  55. package/dist/prospect/types.d.ts +27 -0
  56. package/dist/{prospect-audit-JMBRVXSG.js → prospect-audit-DBBXL7EG.js} +8 -6
  57. package/dist/prospect-audit-DBBXL7EG.js.map +1 -0
  58. package/dist/recipes/sync-configs.js +2 -2
  59. package/dist/{render-VX72AESY.js → render-P3SBUFVB.js} +3 -3
  60. package/dist/{render-from-row-DOYJDO2L.js → render-from-row-P5CWM3MQ.js} +3 -3
  61. package/dist/render-from-row-P5CWM3MQ.js.map +1 -0
  62. package/dist/{report-TALY5P3L.js → report-2N4PXO3L.js} +33 -17
  63. package/dist/report-2N4PXO3L.js.map +1 -0
  64. package/dist/{report-mirror-AAK3MXHY.js → report-mirror-3ENU6OOK.js} +5 -5
  65. package/dist/{selftest-NZTR4SM2.js → selftest-CUX2FIQD.js} +7 -7
  66. package/dist/{site-mirror-Q763PLGG.js → site-mirror-RQTEAID5.js} +5 -5
  67. package/dist/{sync-GBFV45WX.js → sync-ESP5ZQUU.js} +3 -3
  68. package/dist/{sync-configs-34JIA6KZ.js → sync-configs-LVBITPLU.js} +3 -3
  69. package/dist/usage-UHKU76VK.js +147 -0
  70. package/dist/usage-UHKU76VK.js.map +1 -0
  71. package/dist/{write-audits-to-airtable-OBHGR7DQ.js → write-audits-to-airtable-K352LZQY.js} +3 -3
  72. package/dist/write-audits-to-airtable-K352LZQY.js.map +1 -0
  73. package/package.json +1 -1
  74. package/dist/chunk-7TMWBXX7.js.map +0 -1
  75. package/dist/chunk-A2UNJIKH.js.map +0 -1
  76. package/dist/chunk-DGZWFI2G.js.map +0 -1
  77. package/dist/chunk-RUPPWAD6.js +0 -7
  78. package/dist/chunk-RUPPWAD6.js.map +0 -1
  79. package/dist/db-V6UIS433.js.map +0 -1
  80. package/dist/digest-2JEAEJK6.js.map +0 -1
  81. package/dist/digest-state-CRZRNGDI.js +0 -25
  82. package/dist/digest-state-CRZRNGDI.js.map +0 -1
  83. package/dist/prospect-audit-JMBRVXSG.js.map +0 -1
  84. package/dist/report-TALY5P3L.js.map +0 -1
  85. /package/dist/{announce-GKD5YJNB.js.map → announce-OJFAS5JT.js.map} +0 -0
  86. /package/dist/{blux-DN4MI532.js.map → blux-EATHOHKK.js.map} +0 -0
  87. /package/dist/{chunk-7UAYMAKR.js.map → chunk-3VQWSE3F.js.map} +0 -0
  88. /package/dist/{chunk-K6SLF6GI.js.map → chunk-62NYED6E.js.map} +0 -0
  89. /package/dist/{chunk-7FFUBF4I.js.map → chunk-AX22DH6S.js.map} +0 -0
  90. /package/dist/{chunk-6I6L3CMX.js.map → chunk-KGUKL3CV.js.map} +0 -0
  91. /package/dist/{chunk-ULHPALI5.js.map → chunk-NDCOOI3V.js.map} +0 -0
  92. /package/dist/{chunk-KMCZA7CH.js.map → chunk-QLIUDTY6.js.map} +0 -0
  93. /package/dist/{chunk-XP5WM2LV.js.map → chunk-SKI23ZZH.js.map} +0 -0
  94. /package/dist/{chunk-ANJNTAMM.js.map → chunk-ZOCJBJQV.js.map} +0 -0
  95. /package/dist/{chunk-J3WEZGIN.js.map → chunk-ZRI5VRPG.js.map} +0 -0
  96. /package/dist/{fleet-state-R5LU7IC3.js.map → digest-collectors-NI2M3UTC.js.map} +0 -0
  97. /package/dist/{ensure-site-FU6527OU.js.map → ensure-site-NSNY2QHN.js.map} +0 -0
  98. /package/dist/{orchestrate-JNKCWU5K.js.map → fleet-state-3LQUQHYD.js.map} +0 -0
  99. /package/dist/{forms-notify-target-SGDGXXK5.js.map → forms-notify-target-LYSKME5L.js.map} +0 -0
  100. /package/dist/{parity-NBYZUBFU.js.map → freeze-A6MUCZU4.js.map} +0 -0
  101. /package/dist/{github-signals-GWGCKEE2.js.map → github-signals-4YML6NY5.js.map} +0 -0
  102. /package/dist/{health-mirror-GCDN7BJL.js.map → health-mirror-7LKC7Q5Z.js.map} +0 -0
  103. /package/dist/{init-FGTYNGUI.js.map → init-NHKADHB6.js.map} +0 -0
  104. /package/dist/{launch-5H7LPEGP.js.map → launch-VQJ4Z35C.js.map} +0 -0
  105. /package/dist/{pipeline-WYTXSLHX.js.map → orchestrate-6HROYTM6.js.map} +0 -0
  106. /package/dist/{render-VX72AESY.js.map → parity-623WNFZG.js.map} +0 -0
  107. /package/dist/{render-from-row-DOYJDO2L.js.map → pipeline-MQJD4CKN.js.map} +0 -0
  108. /package/dist/{preflight-IMTYDDD4.js.map → preflight-LIN5NCYU.js.map} +0 -0
  109. /package/dist/{prismic-ci-MY2U6NN7.js.map → prismic-ci-PZ5LCPWT.js.map} +0 -0
  110. /package/dist/{prismic-models-NJN5JORF.js.map → prismic-models-6OYYZUR5.js.map} +0 -0
  111. /package/dist/{write-audits-to-airtable-OBHGR7DQ.js.map → render-P3SBUFVB.js.map} +0 -0
  112. /package/dist/{report-mirror-AAK3MXHY.js.map → report-mirror-3ENU6OOK.js.map} +0 -0
  113. /package/dist/{selftest-NZTR4SM2.js.map → selftest-CUX2FIQD.js.map} +0 -0
  114. /package/dist/{site-mirror-Q763PLGG.js.map → site-mirror-RQTEAID5.js.map} +0 -0
  115. /package/dist/{sync-GBFV45WX.js.map → sync-ESP5ZQUU.js.map} +0 -0
  116. /package/dist/{sync-configs-34JIA6KZ.js.map → sync-configs-LVBITPLU.js.map} +0 -0
@@ -1,19 +1,19 @@
1
1
  import {
2
2
  buildReportDataForSite,
3
3
  scoresFromRow
4
- } from "./chunk-ULHPALI5.js";
4
+ } from "./chunk-NDCOOI3V.js";
5
5
  import {
6
6
  operatorEmail
7
7
  } from "./chunk-5ML7FMBD.js";
8
- import "./chunk-J3WEZGIN.js";
8
+ import "./chunk-ZRI5VRPG.js";
9
+ import "./chunk-YFAXOD7Z.js";
9
10
  import {
10
11
  ELIGIBLE_STATUSES
11
12
  } from "./chunk-PZR4EBPY.js";
12
- import "./chunk-YFAXOD7Z.js";
13
13
  import {
14
14
  isProbablyEmail,
15
15
  parseAddresses
16
- } from "./chunk-DGZWFI2G.js";
16
+ } from "./chunk-VBEKL445.js";
17
17
  import "./chunk-F3JWOPTG.js";
18
18
  import "./chunk-VAD43MOR.js";
19
19
  import {
@@ -26,12 +26,12 @@ import {
26
26
  import {
27
27
  prepareHeaderImage,
28
28
  renderReportEmail
29
- } from "./chunk-XP5WM2LV.js";
29
+ } from "./chunk-SKI23ZZH.js";
30
30
  import "./chunk-GECESMVS.js";
31
+ import "./chunk-KJGKZLCT.js";
31
32
  import {
32
33
  applyReportTypeHeadline
33
34
  } from "./chunk-5UINJNAJ.js";
34
- import "./chunk-KJGKZLCT.js";
35
35
  import "./chunk-BGTYPVLT.js";
36
36
  import {
37
37
  listWebsites,
@@ -172,4 +172,4 @@ async function runSelftestCommand(kind, site, opts) {
172
172
  export {
173
173
  runSelftestCommand
174
174
  };
175
- //# sourceMappingURL=selftest-NZTR4SM2.js.map
175
+ //# sourceMappingURL=selftest-CUX2FIQD.js.map
@@ -1,11 +1,11 @@
1
- import {
2
- TURSO_IS_AUTHORITATIVE
3
- } from "./chunk-RUPPWAD6.js";
4
1
  import {
5
2
  mirrorHealthFields,
6
3
  mirrorSiteFields,
7
4
  mirrorSiteInsert
8
- } from "./chunk-GQDDWYU2.js";
5
+ } from "./chunk-QBAAZY7W.js";
6
+ import {
7
+ TURSO_IS_AUTHORITATIVE
8
+ } from "./chunk-SVAAONYB.js";
9
9
  import "./chunk-P6AORG32.js";
10
10
  import "./chunk-F3JWOPTG.js";
11
11
  import "./chunk-VAD43MOR.js";
@@ -59,4 +59,4 @@ async function makeSiteMirror(open = () => openDb(readDbConfig()), strict = TURS
59
59
  export {
60
60
  makeSiteMirror
61
61
  };
62
- //# sourceMappingURL=site-mirror-Q763PLGG.js.map
62
+ //# sourceMappingURL=site-mirror-RQTEAID5.js.map
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  checkFleetParity,
3
3
  formatParityResult
4
- } from "./chunk-7FFUBF4I.js";
5
- import "./chunk-GQDDWYU2.js";
4
+ } from "./chunk-AX22DH6S.js";
5
+ import "./chunk-QBAAZY7W.js";
6
6
  import {
7
7
  formatReapSummary,
8
8
  importFleetState
@@ -43,4 +43,4 @@ export {
43
43
  formatSyncResult,
44
44
  syncFleetState
45
45
  };
46
- //# sourceMappingURL=sync-GBFV45WX.js.map
46
+ //# sourceMappingURL=sync-ESP5ZQUU.js.map
@@ -15,12 +15,12 @@ import {
15
15
  isConfigName,
16
16
  mergeGitignore,
17
17
  syncConfigs
18
- } from "./chunk-7UAYMAKR.js";
18
+ } from "./chunk-3VQWSE3F.js";
19
+ import "./chunk-3G25KIWW.js";
19
20
  import {
20
21
  ALL_TEMPLATES,
21
22
  templatesByName
22
23
  } from "./chunk-73UFRZKY.js";
23
- import "./chunk-3G25KIWW.js";
24
24
  import "./chunk-Z3JXLULD.js";
25
25
  import "./chunk-RARREDQE.js";
26
26
  import "./chunk-XTK5VIQB.js";
@@ -113,4 +113,4 @@ async function runSyncConfigsCommand(site, opts) {
113
113
  export {
114
114
  runSyncConfigsCommand
115
115
  };
116
- //# sourceMappingURL=sync-configs-34JIA6KZ.js.map
116
+ //# sourceMappingURL=sync-configs-LVBITPLU.js.map
@@ -0,0 +1,147 @@
1
+ // src/db/usage.ts
2
+ var CUMULATIVE_METRICS = ["rows_read", "rows_written", "bytes_synced"];
3
+ var LEVEL_METRICS = ["storage_bytes"];
4
+ var CAPACITY_METRICS = ["databases", "locations", "groups"];
5
+ var QUOTA_KEY = {
6
+ rows_read: "rowsRead",
7
+ rows_written: "rowsWritten",
8
+ bytes_synced: "bytesSynced",
9
+ storage_bytes: "storage",
10
+ databases: "databases",
11
+ locations: "locations",
12
+ groups: "groups"
13
+ };
14
+ var ELAPSED_FLOOR_MS = 6 * 60 * 60 * 1e3;
15
+ var DEFAULT_THRESHOLD_PCT = 50;
16
+ var pct = (used, quota) => quota > 0 ? used / quota * 100 : 0;
17
+ var fmt = (n) => `${n.toFixed(2)}%`;
18
+ function assessUsage(input) {
19
+ const threshold = input.thresholdPct ?? DEFAULT_THRESHOLD_PCT;
20
+ const cycleMs = Math.max(1, input.cycleEnd.getTime() - input.cycleStart.getTime());
21
+ const rawElapsedMs = input.now.getTime() - input.cycleStart.getTime();
22
+ const elapsedMs = Math.min(cycleMs, Math.max(ELAPSED_FLOOR_MS, rawElapsedMs));
23
+ const elapsedFraction = elapsedMs / cycleMs;
24
+ const fields = [];
25
+ const rows = [];
26
+ let measured = 0;
27
+ const atCapacity = [];
28
+ const candidates = [];
29
+ const consider = (name, value) => candidates.push({ name, value });
30
+ const all = [
31
+ ...CUMULATIVE_METRICS.map((m) => ({ metric: m, kind: "cumulative" })),
32
+ ...LEVEL_METRICS.map((m) => ({ metric: m, kind: "level" })),
33
+ ...CAPACITY_METRICS.map((m) => ({ metric: m, kind: "capacity" }))
34
+ ];
35
+ for (const { metric, kind } of all) {
36
+ const quota = input.quotas[QUOTA_KEY[metric]];
37
+ const used = input.usage[metric] ?? 0;
38
+ if (typeof quota !== "number" || !Number.isFinite(quota) || quota <= 0) {
39
+ fields.push(`${metric}=unmeasured`);
40
+ rows.push(` ${metric.padEnd(14)} ${String(used).padStart(15)} / (no published quota)`);
41
+ continue;
42
+ }
43
+ measured += 1;
44
+ const now = pct(used, quota);
45
+ fields.push(`${metric}=${fmt(now)}`);
46
+ if (kind === "cumulative") {
47
+ const projected = now / elapsedFraction;
48
+ fields.push(`${metric}_proj=${fmt(projected)}`);
49
+ consider(`${metric}_proj`, projected);
50
+ rows.push(
51
+ ` ${metric.padEnd(14)} ${String(used).padStart(15)} / ${String(quota).padEnd(13)} ${fmt(now).padStart(8)} \u2192 ${fmt(projected)} by cycle end`
52
+ );
53
+ } else if (kind === "level") {
54
+ consider(metric, now);
55
+ rows.push(
56
+ ` ${metric.padEnd(14)} ${String(used).padStart(15)} / ${String(quota).padEnd(13)} ${fmt(now).padStart(8)}`
57
+ );
58
+ } else {
59
+ if (used >= quota) atCapacity.push(metric);
60
+ rows.push(
61
+ ` ${metric.padEnd(14)} ${String(used).padStart(15)} / ${String(quota).padEnd(13)} ${fmt(now).padStart(8)} (ceiling)`
62
+ );
63
+ }
64
+ }
65
+ const blocked = input.blockedReads ? "reads" : input.blockedWrites ? "writes" : "none";
66
+ const worst = candidates.reduce(
67
+ (acc, c) => acc === null || c.value > acc.value ? c : acc,
68
+ null
69
+ );
70
+ const worstLabel = worst ? `${worst.name}:${fmt(worst.value)}` : "none";
71
+ let verdict;
72
+ let code;
73
+ if (measured === 0) {
74
+ verdict = "unmeasurable";
75
+ code = 1;
76
+ } else if (blocked !== "none") {
77
+ verdict = "blocked";
78
+ code = 1;
79
+ } else if (worst && worst.value >= threshold) {
80
+ verdict = "over-threshold";
81
+ code = 1;
82
+ } else {
83
+ verdict = "ok";
84
+ code = 0;
85
+ }
86
+ const marker = [
87
+ "FLEET_DB_USAGE",
88
+ `plan=${input.plan}`,
89
+ `elapsed=${fmt(elapsedFraction * 100)}`,
90
+ ...fields,
91
+ `measured=${measured}`,
92
+ `worst=${worstLabel}`,
93
+ `threshold=${fmt(threshold)}`,
94
+ `blocked=${blocked}`,
95
+ `at_capacity=${atCapacity.length ? atCapacity.join(",") : "none"}`,
96
+ `verdict=${verdict}`
97
+ ].join(" ");
98
+ return { lines: rows, marker, code };
99
+ }
100
+ var API = "https://api.turso.tech/v1";
101
+ async function collectUsage(deps) {
102
+ const doFetch = deps.fetchImpl ?? globalThis.fetch;
103
+ const get = async (path) => {
104
+ const url = `${API}${path}`;
105
+ const res = await doFetch(url, {
106
+ headers: { Authorization: `Bearer ${deps.token}` }
107
+ });
108
+ if (!res.ok) throw new Error(`Turso platform API ${res.status} for ${path}`);
109
+ return res.json();
110
+ };
111
+ let org = deps.org;
112
+ if (!org) {
113
+ const orgs = await get("/organizations");
114
+ org = orgs?.[0]?.slug;
115
+ if (!org) throw new Error("Turso platform API returned no organizations for this token");
116
+ }
117
+ const [usageRes, plansRes, subRes, dbsRes] = await Promise.all([
118
+ get(`/organizations/${org}/usage`),
119
+ get(`/organizations/${org}/plans`),
120
+ get(`/organizations/${org}/subscription`),
121
+ get(`/organizations/${org}/databases`)
122
+ ]);
123
+ const sub = subRes.subscription ?? {};
124
+ const plan = sub.plan ?? "unknown";
125
+ const quotas = plansRes.plans?.find((p) => p.name === plan)?.quotas ?? {};
126
+ const dbs = dbsRes.databases ?? [];
127
+ return {
128
+ plan,
129
+ quotas,
130
+ usage: usageRes.total ?? {},
131
+ cycleStart: new Date(sub.current_billing_period_start ?? deps.now.toISOString()),
132
+ cycleEnd: new Date(sub.current_billing_period_end ?? deps.now.toISOString()),
133
+ now: deps.now,
134
+ blockedReads: dbs.some((d) => d.block_reads === true),
135
+ blockedWrites: dbs.some((d) => d.block_writes === true)
136
+ };
137
+ }
138
+ export {
139
+ CAPACITY_METRICS,
140
+ CUMULATIVE_METRICS,
141
+ DEFAULT_THRESHOLD_PCT,
142
+ LEVEL_METRICS,
143
+ QUOTA_KEY,
144
+ assessUsage,
145
+ collectUsage
146
+ };
147
+ //# sourceMappingURL=usage-UHKU76VK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/db/usage.ts"],"sourcesContent":["/** Turso plan-quota headroom (#539 HIGH-10).\n *\n * The starter plan carries `\"overages\": false`, which means crossing a quota\n * BLOCKS reads and writes rather than billing for them. From the Airtable\n * cutover on, Turso is the only store the fleet has — so a silently-approached\n * quota is a fleet outage with no warning shot. This module turns the platform\n * API's usage numbers into one machine line the nightly can gate on.\n *\n * Quotas are read from the API's own /plans response rather than hardcoded:\n * a plan upgrade must not leave the alarm measuring against a stale ceiling.\n */\n\n/** Consumption that RESETS each billing cycle. A raw percentage of one of these\n * is not comparable across the month — 30% on day 3 is a fire, 30% on day 28\n * is fine — so these are projected to the cycle's end at the current rate. */\nexport const CUMULATIVE_METRICS = [\"rows_read\", \"rows_written\", \"bytes_synced\"] as const;\n\n/** Consumption that is a LEVEL, not a per-cycle counter. Projecting storage\n * linearly would invent growth the number does not describe. */\nexport const LEVEL_METRICS = [\"storage_bytes\"] as const;\n\n/** Configuration ceilings, not consumption. Hitting one blocks CREATING another\n * database/location/group; it does not degrade the database already running.\n *\n * These are REPORTED but never alarm, at any level. Two reasons, both from\n * reading the live numbers: the fleet sits at 2 of 3 locations by design, so a\n * flat threshold would call a healthy fleet an emergency; and the starter plan\n * allows exactly one group while the fleet runs exactly one, so a ceiling alarm\n * would fire every single night about a standing, accepted plan constraint.\n * A capacity ceiling also fails LOUDLY at creation time, where quota exhaustion\n * silently blocks a live database — only the latter needs a warning shot. */\nexport const CAPACITY_METRICS = [\"databases\", \"locations\", \"groups\"] as const;\n\nexport type UsageMetric =\n | (typeof CUMULATIVE_METRICS)[number]\n | (typeof LEVEL_METRICS)[number]\n | (typeof CAPACITY_METRICS)[number];\n\n/** usage-response key → /plans quota key. The two endpoints disagree on casing\n * and on one name (`storage_bytes` vs `storage`), and a typo here would read as\n * \"unmeasured\" forever, so a test asserts both directions of this map. */\nexport const QUOTA_KEY: Record<UsageMetric, string> = {\n rows_read: \"rowsRead\",\n rows_written: \"rowsWritten\",\n bytes_synced: \"bytesSynced\",\n storage_bytes: \"storage\",\n databases: \"databases\",\n locations: \"locations\",\n groups: \"groups\",\n};\n\n/** Below this much of a billing cycle, treat the cycle as this far along\n * anyway. Without a floor the first reading after a cycle rolls over divides a\n * real number by ~0 and projects nonsense; six hours is long enough to absorb\n * one nightly batch and short enough to still catch a runaway inside a day. */\nconst ELAPSED_FLOOR_MS = 6 * 60 * 60 * 1000;\n\n/** Fraction of a quota that counts as \"approaching it\". Deliberately far from\n * the ceiling: the point is to leave room to act, and current usage is three\n * orders of magnitude below this. */\nexport const DEFAULT_THRESHOLD_PCT = 50;\n\nexport type UsageInput = {\n plan: string;\n /** The plan's quota object, verbatim from GET /organizations/{org}/plans. */\n quotas: Partial<Record<string, number>>;\n /** The `total` usage object, verbatim from GET /organizations/{org}/usage. */\n usage: Partial<Record<string, number>>;\n cycleStart: Date;\n cycleEnd: Date;\n now: Date;\n blockedReads: boolean;\n blockedWrites: boolean;\n thresholdPct?: number;\n};\n\nexport type UsageAssessment = {\n /** Human-readable table, one row per metric. */\n lines: string[];\n /** The single greppable line, emitted on every run including clean ones. */\n marker: string;\n code: number;\n};\n\nconst pct = (used: number, quota: number): number => (quota > 0 ? (used / quota) * 100 : 0);\nconst fmt = (n: number): string => `${n.toFixed(2)}%`;\n\n/** Turn one usage reading into a verdict. Pure: every input that varies —\n * including `now` — is a parameter, so both the passing and the failing state\n * are reachable from a test. */\nexport function assessUsage(input: UsageInput): UsageAssessment {\n const threshold = input.thresholdPct ?? DEFAULT_THRESHOLD_PCT;\n\n const cycleMs = Math.max(1, input.cycleEnd.getTime() - input.cycleStart.getTime());\n const rawElapsedMs = input.now.getTime() - input.cycleStart.getTime();\n const elapsedMs = Math.min(cycleMs, Math.max(ELAPSED_FLOOR_MS, rawElapsedMs));\n const elapsedFraction = elapsedMs / cycleMs;\n\n const fields: string[] = [];\n const rows: string[] = [];\n /** Metrics whose quota the plan publishes — the denominator of the check. */\n let measured = 0;\n const atCapacity: string[] = [];\n /** Every metric eligible to drive the verdict, with the number it is judged\n * on — the projection for cumulative metrics, the raw level for storage.\n * Capacity metrics never land here. */\n const candidates: Array<{ name: string; value: number }> = [];\n const consider = (name: string, value: number) => candidates.push({ name, value });\n\n const all: Array<{ metric: UsageMetric; kind: \"cumulative\" | \"level\" | \"capacity\" }> = [\n ...CUMULATIVE_METRICS.map((m) => ({ metric: m as UsageMetric, kind: \"cumulative\" as const })),\n ...LEVEL_METRICS.map((m) => ({ metric: m as UsageMetric, kind: \"level\" as const })),\n ...CAPACITY_METRICS.map((m) => ({ metric: m as UsageMetric, kind: \"capacity\" as const })),\n ];\n\n for (const { metric, kind } of all) {\n const quota = input.quotas[QUOTA_KEY[metric]];\n const used = input.usage[metric] ?? 0;\n\n // A metric with no published quota is UNMEASURED, not passing. The `pro`\n // plan really does omit `databases`, so this branch is live, not defensive.\n if (typeof quota !== \"number\" || !Number.isFinite(quota) || quota <= 0) {\n fields.push(`${metric}=unmeasured`);\n rows.push(` ${metric.padEnd(14)} ${String(used).padStart(15)} / (no published quota)`);\n continue;\n }\n measured += 1;\n\n const now = pct(used, quota);\n fields.push(`${metric}=${fmt(now)}`);\n\n if (kind === \"cumulative\") {\n const projected = now / elapsedFraction;\n fields.push(`${metric}_proj=${fmt(projected)}`);\n consider(`${metric}_proj`, projected);\n rows.push(\n ` ${metric.padEnd(14)} ${String(used).padStart(15)} / ${String(quota).padEnd(13)} ${fmt(now).padStart(8)} → ${fmt(projected)} by cycle end`,\n );\n } else if (kind === \"level\") {\n consider(metric, now);\n rows.push(\n ` ${metric.padEnd(14)} ${String(used).padStart(15)} / ${String(quota).padEnd(13)} ${fmt(now).padStart(8)}`,\n );\n } else {\n // Capacity: recorded for the marker line so the state is greppable, but\n // deliberately kept out of `worst` and out of the exit code.\n if (used >= quota) atCapacity.push(metric);\n rows.push(\n ` ${metric.padEnd(14)} ${String(used).padStart(15)} / ${String(quota).padEnd(13)} ${fmt(now).padStart(8)} (ceiling)`,\n );\n }\n }\n\n const blocked = input.blockedReads ? \"reads\" : input.blockedWrites ? \"writes\" : \"none\";\n const worst = candidates.reduce<{ name: string; value: number } | null>(\n (acc, c) => (acc === null || c.value > acc.value ? c : acc),\n null,\n );\n const worstLabel = worst ? `${worst.name}:${fmt(worst.value)}` : \"none\";\n\n // Ordered by how bad the state actually is: already blocked beats a\n // projection, and a check that measured nothing beats both — a green\n // \"nothing to report\" from an instrument with no denominator is the exact\n // shape this repo keeps re-learning.\n let verdict: string;\n let code: number;\n if (measured === 0) {\n verdict = \"unmeasurable\";\n code = 1;\n } else if (blocked !== \"none\") {\n verdict = \"blocked\";\n code = 1;\n } else if (worst && worst.value >= threshold) {\n verdict = \"over-threshold\";\n code = 1;\n } else {\n verdict = \"ok\";\n code = 0;\n }\n\n const marker = [\n \"FLEET_DB_USAGE\",\n `plan=${input.plan}`,\n `elapsed=${fmt(elapsedFraction * 100)}`,\n ...fields,\n `measured=${measured}`,\n `worst=${worstLabel}`,\n `threshold=${fmt(threshold)}`,\n `blocked=${blocked}`,\n `at_capacity=${atCapacity.length ? atCapacity.join(\",\") : \"none\"}`,\n `verdict=${verdict}`,\n ].join(\" \");\n\n return { lines: rows, marker, code };\n}\n\n/* ------------------------------------------------------------------ *\n * Transport\n * ------------------------------------------------------------------ */\n\nconst API = \"https://api.turso.tech/v1\";\n\nexport type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;\n\nexport type CollectDeps = {\n /** A PLATFORM API token (`turso auth api-tokens mint …`). This is NOT the\n * database-level TURSO_AUTH_TOKEN the app runs on — that one cannot read\n * quota state at all. */\n token: string;\n /** Org slug; discovered from the token when omitted. */\n org?: string | undefined;\n fetchImpl?: FetchLike | undefined;\n now: Date;\n};\n\n/** Read the platform API and assemble everything `assessUsage` needs.\n * Deliberately throws on a bad response instead of returning zeros: a\n * transport failure that reports 0% used would be an alarm that can never\n * fire, which is the failure mode this whole module exists to avoid. */\nexport async function collectUsage(deps: CollectDeps): Promise<UsageInput> {\n const doFetch = deps.fetchImpl ?? (globalThis.fetch as FetchLike);\n const get = async (path: string): Promise<unknown> => {\n const url = `${API}${path}`;\n const res = await doFetch(url, {\n headers: { Authorization: `Bearer ${deps.token}` },\n });\n if (!res.ok) throw new Error(`Turso platform API ${res.status} for ${path}`);\n return res.json();\n };\n\n let org = deps.org;\n if (!org) {\n const orgs = (await get(\"/organizations\")) as Array<{ slug?: string }>;\n org = orgs?.[0]?.slug;\n if (!org) throw new Error(\"Turso platform API returned no organizations for this token\");\n }\n\n const [usageRes, plansRes, subRes, dbsRes] = await Promise.all([\n get(`/organizations/${org}/usage`) as Promise<{ total?: Record<string, number> }>,\n get(`/organizations/${org}/plans`) as Promise<{\n plans?: Array<{ name?: string; quotas?: Record<string, number> }>;\n }>,\n get(`/organizations/${org}/subscription`) as Promise<{\n subscription?: {\n plan?: string;\n current_billing_period_start?: string;\n current_billing_period_end?: string;\n };\n }>,\n get(`/organizations/${org}/databases`) as Promise<{\n databases?: Array<{ block_reads?: boolean; block_writes?: boolean }>;\n }>,\n ]);\n\n const sub = subRes.subscription ?? {};\n const plan = sub.plan ?? \"unknown\";\n // Match the quota block to the SUBSCRIBED plan. Taking plans[0] would keep\n // reporting starter percentages forever after an upgrade.\n const quotas = plansRes.plans?.find((p) => p.name === plan)?.quotas ?? {};\n\n const dbs = dbsRes.databases ?? [];\n\n return {\n plan,\n quotas,\n usage: usageRes.total ?? {},\n cycleStart: new Date(sub.current_billing_period_start ?? deps.now.toISOString()),\n cycleEnd: new Date(sub.current_billing_period_end ?? deps.now.toISOString()),\n now: deps.now,\n blockedReads: dbs.some((d) => d.block_reads === true),\n blockedWrites: dbs.some((d) => d.block_writes === true),\n };\n}\n"],"mappings":"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","names":[]}
@@ -3,9 +3,9 @@ import {
3
3
  formatFleetWriteSummary,
4
4
  writeAuditsToAirtable,
5
5
  writeFleetAuditsToAirtable
6
- } from "./chunk-KMCZA7CH.js";
6
+ } from "./chunk-QLIUDTY6.js";
7
7
  import "./chunk-7BZUOZPU.js";
8
- import "./chunk-RUPPWAD6.js";
8
+ import "./chunk-SVAAONYB.js";
9
9
  import "./chunk-5L3FPQIJ.js";
10
10
  import "./chunk-7NYN2P6I.js";
11
11
  export {
@@ -14,4 +14,4 @@ export {
14
14
  writeAuditsToAirtable,
15
15
  writeFleetAuditsToAirtable
16
16
  };
17
- //# sourceMappingURL=write-audits-to-airtable-OBHGR7DQ.js.map
17
+ //# sourceMappingURL=write-audits-to-airtable-K352LZQY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reddoorla/maintenance",
3
- "version": "0.88.2",
3
+ "version": "0.89.0",
4
4
  "description": "Canonical maintenance configs, audits, and recipes for the reddoor stack.",
5
5
  "type": "module",
6
6
  "license": "MIT",