aiopt 0.3.3 → 0.3.5

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/cli.js CHANGED
@@ -656,7 +656,7 @@ var require_package = __commonJS({
656
656
  "package.json"(exports2, module2) {
657
657
  module2.exports = {
658
658
  name: "aiopt",
659
- version: "0.3.3",
659
+ version: "0.3.5",
660
660
  description: "Pre-deploy LLM cost accident guardrail (serverless local CLI)",
661
661
  bin: {
662
662
  aiopt: "dist/cli.js"
@@ -1817,6 +1817,8 @@ async function startDashboard(cwd, opts) {
1817
1817
  const file = (name) => import_path12.default.join(outDir, name);
1818
1818
  let lastCollect = null;
1819
1819
  let lastCollectError = null;
1820
+ let lastAutoGen = null;
1821
+ let lastAutoGenError = null;
1820
1822
  function ensureUsageFile() {
1821
1823
  try {
1822
1824
  const usagePath = file("usage.jsonl");
@@ -1828,7 +1830,45 @@ async function startDashboard(cwd, opts) {
1828
1830
  lastCollectError = String(e?.message || e || "collect failed");
1829
1831
  }
1830
1832
  }
1833
+ function loadRateTable2() {
1834
+ const p = import_path12.default.join(__dirname, "..", "rates", "rate_table.json");
1835
+ return JSON.parse(import_fs11.default.readFileSync(p, "utf8"));
1836
+ }
1837
+ function readEvents(usagePath) {
1838
+ if (!import_fs11.default.existsSync(usagePath)) return [];
1839
+ return isCsvPath(usagePath) ? readCsv(usagePath) : readJsonl(usagePath);
1840
+ }
1841
+ function ensureScanAndGuard() {
1842
+ const did = [];
1843
+ try {
1844
+ const usagePath = file("usage.jsonl");
1845
+ const rt = loadRateTable2();
1846
+ const needScan = !import_fs11.default.existsSync(file("report.json")) || !import_fs11.default.existsSync(file("report.md"));
1847
+ if (needScan) {
1848
+ const events = readEvents(usagePath);
1849
+ const { analysis, savings, policy, meta } = analyze(rt, events);
1850
+ policy.generated_from.input = usagePath;
1851
+ writeOutputs(outDir, analysis, savings, policy, { ...meta, cwd, cliVersion: "dashboard" });
1852
+ did.push("scan");
1853
+ }
1854
+ const needGuard = !import_fs11.default.existsSync(file("guard-last.txt")) || !import_fs11.default.existsSync(file("guard-last.json"));
1855
+ if (needGuard) {
1856
+ const events = readEvents(usagePath);
1857
+ const r = runGuard(rt, { baselineEvents: events, candidate: {} });
1858
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
1859
+ import_fs11.default.writeFileSync(file("guard-last.txt"), r.message);
1860
+ import_fs11.default.writeFileSync(file("guard-last.json"), JSON.stringify({ ts, exitCode: r.exitCode }, null, 2));
1861
+ import_fs11.default.appendFileSync(file("guard-history.jsonl"), JSON.stringify({ ts, exitCode: r.exitCode, mode: "dashboard", baseline: usagePath, candidate: null }) + "\n");
1862
+ did.push("guard");
1863
+ }
1864
+ if (did.length) lastAutoGen = { ts: (/* @__PURE__ */ new Date()).toISOString(), did };
1865
+ lastAutoGenError = null;
1866
+ } catch (e) {
1867
+ lastAutoGenError = String(e?.message || e || "auto-gen failed");
1868
+ }
1869
+ }
1831
1870
  ensureUsageFile();
1871
+ ensureScanAndGuard();
1832
1872
  function readOrNull(p) {
1833
1873
  try {
1834
1874
  if (!import_fs11.default.existsSync(p)) return null;
@@ -1837,6 +1877,14 @@ async function startDashboard(cwd, opts) {
1837
1877
  return null;
1838
1878
  }
1839
1879
  }
1880
+ function statOrNull(p) {
1881
+ try {
1882
+ const st = import_fs11.default.statSync(p);
1883
+ return { size: st.size, mtimeMs: st.mtimeMs };
1884
+ } catch {
1885
+ return null;
1886
+ }
1887
+ }
1840
1888
  const indexHtml = `<!doctype html>
1841
1889
  <html lang="en">
1842
1890
  <head>
@@ -1906,7 +1954,11 @@ async function startDashboard(cwd, opts) {
1906
1954
  <div class="mini" id="baseDir">base: \u2014</div>
1907
1955
  <div class="mini" id="missingHint" style="margin-top:4px">checking files\u2026</div>
1908
1956
  </div>
1909
- <div class="pill"><span class="dot"></span> local-only \xB7 reads <span class="k">./aiopt-output</span> \xB7 <span id="live" class="muted">live: off</span></div>
1957
+ <div class="pill"><span class="dot"></span> local-only \xB7 reads <span class="k">./aiopt-output</span> \xB7 <span id="live" class="muted">live: off</span>
1958
+ <span class="muted">\xB7</span>
1959
+ <button id="btnRefresh" style="all:unset; cursor:pointer; padding:2px 8px; border:1px solid rgba(255,255,255,.16); border-radius:999px; background:rgba(255,255,255,.04); font-size:12px">Refresh</button>
1960
+ <button id="btnLive" style="all:unset; cursor:pointer; padding:2px 8px; border:1px solid rgba(255,255,255,.16); border-radius:999px; background:rgba(255,255,255,.04); font-size:12px">Live: Off</button>
1961
+ </div>
1910
1962
  </div>
1911
1963
 
1912
1964
  <div class="grid">
@@ -2088,7 +2140,13 @@ async function load(){
2088
2140
  const total = reportJson.summary && reportJson.summary.total_cost_usd;
2089
2141
  const sav = reportJson.summary && reportJson.summary.estimated_savings_usd;
2090
2142
  document.getElementById('totalCost').textContent = 'total: ' + money(total);
2091
- document.getElementById('savings').textContent = 'savings: ' + money(sav);
2143
+
2144
+ // UX: if savings is ~0, explicitly say it's normal (not broken).
2145
+ const sNum = Number(sav || 0);
2146
+ const savingsText = (Number.isFinite(sNum) && sNum <= 0.01)
2147
+ ? 'savings: $0 (no obvious waste found)'
2148
+ : ('savings: ' + money(sav));
2149
+ document.getElementById('savings').textContent = savingsText;
2092
2150
  renderBars(document.getElementById('byModel'), reportJson.top && reportJson.top.by_model);
2093
2151
  renderBars(document.getElementById('byFeature'), reportJson.top && reportJson.top.by_feature);
2094
2152
  document.getElementById('scanMeta').textContent = 'confidence=' + (reportJson.confidence || '\u2014') + ' \xB7 generated_at=' + (reportJson.generated_at || '\u2014');
@@ -2096,125 +2154,111 @@ async function load(){
2096
2154
  document.getElementById('scanMeta').textContent = '(no report.json yet \u2014 run: aiopt scan)';
2097
2155
  }
2098
2156
 
2099
- const usageTxt = await fetch('/api/usage.jsonl', { cache: 'no-store' }).then(r=>r.ok?r.text():null).catch(()=>null);
2100
- if(usageTxt){
2101
- const now = Date.now();
2102
-
2103
- // Live: last 60m (per-minute cost + calls)
2104
- const liveBins = Array.from({length:60}, (_,i)=>({ min:i, cost:0, calls:0 }));
2105
-
2106
- // 7d cost trend: sum(cost_usd) per day from usage.jsonl (ev.ts).
2107
- const bins = Array.from({length:7}, (_,i)=>({ day:i, cost:0, calls:0 }));
2108
-
2109
- for(const line of usageTxt.trim().split('
2110
- ')){
2111
- if(!line) continue;
2112
- try{
2113
- const ev = JSON.parse(line);
2114
- const t = Date.parse(ev.ts);
2115
- if(!Number.isFinite(t)) continue;
2116
-
2117
- const cost = Number(ev.cost_usd);
2118
- const c = Number.isFinite(cost) ? cost : 0;
2119
-
2120
- // live minute bin
2121
- const dm = Math.floor((now - t) / 60000);
2122
- if(dm>=0 && dm<60){
2123
- liveBins[dm].calls++;
2124
- liveBins[dm].cost += c;
2125
- }
2126
-
2127
- // 7d day bin
2128
- const d = Math.floor((now - t) / 86400000);
2129
- if(d>=0 && d<7){
2130
- bins[d].calls++;
2131
- bins[d].cost += c;
2132
- }
2133
- }catch{}
2134
- }
2135
-
2136
- // Live sparkline (last 60m)
2137
- {
2138
- const W=520, H=120, P=12;
2139
- const pts = liveBins.slice().reverse();
2140
- const max = Math.max(...pts.map(b=>b.cost), 0.000001);
2141
- const xs = pts.map((_,i)=> P + (i*(W-2*P))/59);
2142
- const ys = pts.map(b=> H-P - ((b.cost/max)*(H-2*P)) );
2143
- let d = '';
2144
- for(let i=0;i<xs.length;i++) d += (i===0?'M':'L') + xs[i].toFixed(1)+','+ys[i].toFixed(1)+' ';
2145
- const area = 'M'+xs[0].toFixed(1)+','+(H-P).toFixed(1)+' ' + d + 'L'+xs[xs.length-1].toFixed(1)+','+(H-P).toFixed(1)+' Z';
2146
- const svg =
2147
- '<svg viewBox="0 0 '+W+' '+H+'" width="100%" height="'+H+'" xmlns="http://www.w3.org/2000/svg" style="background:rgba(255,255,255,.02); border:1px solid rgba(255,255,255,.10); border-radius:14px">'+
2148
- '<path d="'+area+'" fill="rgba(167,139,250,.10)" />'+
2149
- '<path d="'+d+'" fill="none" stroke="rgba(167,139,250,.95)" stroke-width="2" />'+
2150
- '<text x="'+P+'" y="'+(P+10)+'" fill="rgba(229,231,235,.75)" font-size="11">max/min '+money(max)+'</text>'+
2151
- '</svg>';
2152
- document.getElementById('liveSvg').innerHTML = svg;
2153
-
2154
- const rows = pts.slice(-10).map((b,idx)=>{
2155
- const mAgo = 9-idx;
2156
- const label = (mAgo===0 ? 'now' : (mAgo+'m'));
2157
- const dollars = ('$' + (Math.round(b.cost*100)/100).toFixed(2));
2158
- return String(label).padEnd(5) + ' ' + String(dollars).padStart(9) + ' (' + b.calls + ' calls)';
2159
- });
2160
- document.getElementById('liveText').textContent = rows.join('
2157
+ // Use computed JSON endpoints to avoid downloading/parsing huge usage.jsonl in the browser.
2158
+ const live60 = await fetch('/api/live-60m.json', { cache: 'no-store' }).then(r=>r.ok?r.json():null).catch(()=>null);
2159
+ const sum7d = await fetch('/api/usage-summary.json', { cache: 'no-store' }).then(r=>r.ok?r.json():null).catch(()=>null);
2160
+
2161
+ if(live60 && live60.bins){
2162
+ const pts = (live60.bins || []).slice().reverse();
2163
+ const W=520, H=120, P=12;
2164
+ const max = Math.max(...pts.map(b=>Number(b.cost)||0), 0.000001);
2165
+ const xs = pts.map((_,i)=> P + (i*(W-2*P))/59);
2166
+ const ys = pts.map(b=> H-P - (((Number(b.cost)||0)/max)*(H-2*P)) );
2167
+ let d = '';
2168
+ for(let i=0;i<xs.length;i++) d += (i===0?'M':'L') + xs[i].toFixed(1)+','+ys[i].toFixed(1)+' ';
2169
+ const area = 'M'+xs[0].toFixed(1)+','+(H-P).toFixed(1)+' ' + d + 'L'+xs[xs.length-1].toFixed(1)+','+(H-P).toFixed(1)+' Z';
2170
+ const svg =
2171
+ '<svg viewBox="0 0 '+W+' '+H+'" width="100%" height="'+H+'" xmlns="http://www.w3.org/2000/svg" style="background:rgba(255,255,255,.02); border:1px solid rgba(255,255,255,.10); border-radius:14px">'+
2172
+ '<path d="'+area+'" fill="rgba(167,139,250,.10)" />'+
2173
+ '<path d="'+d+'" fill="none" stroke="rgba(167,139,250,.95)" stroke-width="2" />'+
2174
+ '<text x="'+P+'" y="'+(P+10)+'" fill="rgba(229,231,235,.75)" font-size="11">max/min '+money(max)+'</text>'+
2175
+ '</svg>';
2176
+ document.getElementById('liveSvg').innerHTML = svg;
2177
+
2178
+ const rows = pts.slice(-10).map((b,idx)=>{
2179
+ const mAgo = 9-idx;
2180
+ const label = (mAgo===0 ? 'now' : (mAgo+'m'));
2181
+ const dollars = ('$' + (Math.round((Number(b.cost)||0)*100)/100).toFixed(2));
2182
+ return String(label).padEnd(5) + ' ' + String(dollars).padStart(9) + ' (' + (b.calls||0) + ' calls)';
2183
+ });
2184
+ document.getElementById('liveText').textContent = rows.join('
2161
2185
  ');
2162
2186
 
2163
- const totalLive = pts.reduce((a,b)=>a+b.cost,0);
2164
- const liveEl = document.getElementById('live');
2165
- if(liveEl){
2166
- liveEl.textContent = 'live: on \xB7 last60m ' + money(totalLive);
2167
- }
2187
+ const liveEl = document.getElementById('live');
2188
+ if(liveEl){
2189
+ liveEl.textContent = 'live: on \xB7 last60m ' + money(live60.totalCostUsd || 0);
2168
2190
  }
2191
+ } else {
2192
+ document.getElementById('liveText').textContent = '(no live data yet)';
2193
+ document.getElementById('liveSvg').innerHTML = '';
2194
+ }
2169
2195
 
2170
- // 7d sparkline
2171
- {
2172
- const W=520, H=120, P=12;
2173
- const pts = bins.slice().reverse();
2174
- const max = Math.max(...pts.map(b=>b.cost), 0.000001);
2175
- const xs = pts.map((_,i)=> P + (i*(W-2*P))/6);
2176
- const ys = pts.map(b=> H-P - ((b.cost/max)*(H-2*P)) );
2177
- let d = '';
2178
- for(let i=0;i<xs.length;i++) d += (i===0?'M':'L') + xs[i].toFixed(1)+','+ys[i].toFixed(1)+' ';
2179
- const area = 'M'+xs[0].toFixed(1)+','+(H-P).toFixed(1)+' ' + d + 'L'+xs[xs.length-1].toFixed(1)+','+(H-P).toFixed(1)+' Z';
2180
-
2181
- const circles = xs.map((x,i)=>'<circle cx="'+x.toFixed(1)+'" cy="'+ys[i].toFixed(1)+'" r="2.6" fill="rgba(52,211,153,.9)"/>').join('');
2182
- const svg =
2183
- '<svg viewBox="0 0 '+W+' '+H+'" width="100%" height="'+H+'" xmlns="http://www.w3.org/2000/svg" style="background:rgba(255,255,255,.02); border:1px solid rgba(255,255,255,.10); border-radius:14px">'+
2184
- '<path d="'+area+'" fill="rgba(96,165,250,.12)" />'+
2185
- '<path d="'+d+'" fill="none" stroke="rgba(96,165,250,.95)" stroke-width="2" />'+
2186
- circles+
2187
- '<text x="'+P+'" y="'+(P+10)+'" fill="rgba(229,231,235,.75)" font-size="11">max '+money(max)+'</text>'+
2188
- '</svg>';
2189
- document.getElementById('trendSvg').innerHTML = svg;
2190
-
2191
- const rows = pts.map((b,idx)=>{
2192
- const label = (idx===pts.length-1 ? 'd-6' : (idx===0 ? 'today' : ('d-'+idx)));
2193
- const dollars = ('$' + (Math.round(b.cost*100)/100).toFixed(2));
2194
- return String(label).padEnd(7) + ' ' + String(dollars).padStart(9) + ' (' + b.calls + ' calls)';
2195
- });
2196
- document.getElementById('trend').textContent = rows.join('
2196
+ if(sum7d && sum7d.dayBins){
2197
+ const bins = sum7d.dayBins || [];
2198
+ const W=520, H=120, P=12;
2199
+ const pts = bins.slice().reverse();
2200
+ const max = Math.max(...pts.map(b=>Number(b.cost)||0), 0.000001);
2201
+ const xs = pts.map((_,i)=> P + (i*(W-2*P))/6);
2202
+ const ys = pts.map(b=> H-P - (((Number(b.cost)||0)/max)*(H-2*P)) );
2203
+ let d = '';
2204
+ for(let i=0;i<xs.length;i++) d += (i===0?'M':'L') + xs[i].toFixed(1)+','+ys[i].toFixed(1)+' ';
2205
+ const area = 'M'+xs[0].toFixed(1)+','+(H-P).toFixed(1)+' ' + d + 'L'+xs[xs.length-1].toFixed(1)+','+(H-P).toFixed(1)+' Z';
2206
+ const circles = xs.map((x,i)=>'<circle cx="'+x.toFixed(1)+'" cy="'+ys[i].toFixed(1)+'" r="2.6" fill="rgba(52,211,153,.9)"/>').join('');
2207
+ const svg =
2208
+ '<svg viewBox="0 0 '+W+' '+H+'" width="100%" height="'+H+'" xmlns="http://www.w3.org/2000/svg" style="background:rgba(255,255,255,.02); border:1px solid rgba(255,255,255,.10); border-radius:14px">'+
2209
+ '<path d="'+area+'" fill="rgba(96,165,250,.12)" />'+
2210
+ '<path d="'+d+'" fill="none" stroke="rgba(96,165,250,.95)" stroke-width="2" />'+
2211
+ circles+
2212
+ '<text x="'+P+'" y="'+(P+10)+'" fill="rgba(229,231,235,.75)" font-size="11">max '+money(max)+'</text>'+
2213
+ '</svg>';
2214
+ document.getElementById('trendSvg').innerHTML = svg;
2215
+
2216
+ const rows = pts.map((b,idx)=>{
2217
+ const label = (idx===pts.length-1 ? 'd-6' : (idx===0 ? 'today' : ('d-'+idx)));
2218
+ const dollars = ('$' + (Math.round((Number(b.cost)||0)*100)/100).toFixed(2));
2219
+ return String(label).padEnd(7) + ' ' + String(dollars).padStart(9) + ' (' + (b.calls||0) + ' calls)';
2220
+ });
2221
+ document.getElementById('trend').textContent = rows.join('
2197
2222
  ');
2198
- }
2199
-
2200
2223
  } else {
2201
- document.getElementById('liveText').textContent = '(no usage.jsonl yet)';
2202
- document.getElementById('liveSvg').innerHTML = '';
2203
- document.getElementById('trend').textContent = '(no usage.jsonl yet)';
2224
+ document.getElementById('trend').textContent = '(no 7d data yet)';
2204
2225
  document.getElementById('trendSvg').innerHTML = '';
2205
- const liveEl = document.getElementById('live');
2206
- if(liveEl) liveEl.textContent = 'live: off';
2207
2226
  }
2208
2227
 
2209
2228
  const reportMd = await fetch('/api/report.md').then(r=>r.ok?r.text():null).catch(()=>null);
2210
2229
  document.getElementById('scan').textContent = reportMd || '(no report.md yet \u2014 run: aiopt scan)';
2211
2230
  }
2212
2231
 
2213
- // Auto-refresh (simple polling): updates the dashboard as files change.
2232
+ // Default: no auto-refresh (resource-friendly). User can refresh on demand.
2233
+ let liveTimer = null;
2234
+
2235
+ function setLive(on){
2236
+ const btn = document.getElementById('btnLive');
2237
+ const liveEl = document.getElementById('live');
2238
+ if(!btn || !liveEl) return;
2239
+
2240
+ if(on){
2241
+ btn.textContent = 'Live: On';
2242
+ liveEl.textContent = 'live: on';
2243
+ if(liveTimer) clearInterval(liveTimer);
2244
+ liveTimer = setInterval(()=>{ load(); }, 5000);
2245
+ } else {
2246
+ btn.textContent = 'Live: Off';
2247
+ liveEl.textContent = 'live: off';
2248
+ if(liveTimer) clearInterval(liveTimer);
2249
+ liveTimer = null;
2250
+ }
2251
+ }
2252
+
2253
+ document.getElementById('btnRefresh')?.addEventListener('click', ()=>{ load(); });
2254
+ document.getElementById('btnLive')?.addEventListener('click', ()=>{
2255
+ const on = !liveTimer;
2256
+ setLive(on);
2257
+ load();
2258
+ });
2259
+
2260
+ setLive(false);
2214
2261
  load();
2215
- setInterval(()=>{ load(); }, 2000);
2216
- const liveEl = document.getElementById('live');
2217
- if(liveEl) liveEl.textContent = 'live: on (polling)';
2218
2262
  </script>
2219
2263
  </body>
2220
2264
  </html>`;
@@ -2229,19 +2273,82 @@ if(liveEl) liveEl.textContent = 'live: on (polling)';
2229
2273
  const name = url.replace("/api/", "");
2230
2274
  if (name === "_meta") {
2231
2275
  ensureUsageFile();
2276
+ ensureScanAndGuard();
2232
2277
  const expected = ["guard-last.txt", "guard-last.json", "report.json", "report.md", "usage.jsonl", "guard-history.jsonl"];
2233
2278
  const missing = expected.filter((f) => !import_fs11.default.existsSync(file(f)));
2234
2279
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
2235
- res.end(JSON.stringify({ baseDir: cwd, outDir, missing, collect: lastCollect, collectError: lastCollectError }, null, 2));
2280
+ res.end(JSON.stringify({ baseDir: cwd, outDir, missing, collect: lastCollect, collectError: lastCollectError, autoGen: lastAutoGen, autoGenError: lastAutoGenError }, null, 2));
2236
2281
  return;
2237
2282
  }
2238
- const allow = /* @__PURE__ */ new Set(["guard-last.txt", "guard-last.json", "guard-history.jsonl", "report.md", "report.json", "usage.jsonl"]);
2239
- if (name === "usage.jsonl") ensureUsageFile();
2283
+ const allow = /* @__PURE__ */ new Set([
2284
+ "guard-last.txt",
2285
+ "guard-last.json",
2286
+ "guard-history.jsonl",
2287
+ "report.md",
2288
+ "report.json",
2289
+ "usage.jsonl",
2290
+ "usage-summary.json",
2291
+ "live-60m.json"
2292
+ ]);
2293
+ if (name === "usage.jsonl" || name === "usage-summary.json" || name === "live-60m.json") ensureUsageFile();
2294
+ if (name === "report.json" || name === "report.md" || name === "guard-last.txt" || name === "guard-last.json" || name === "guard-history.jsonl") ensureScanAndGuard();
2240
2295
  if (!allow.has(name)) {
2241
2296
  res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
2242
2297
  res.end("not found");
2243
2298
  return;
2244
2299
  }
2300
+ if (name === "live-60m.json" || name === "usage-summary.json") {
2301
+ const usagePath = file("usage.jsonl");
2302
+ const st = statOrNull(usagePath);
2303
+ if (!st) {
2304
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
2305
+ res.end("missing");
2306
+ return;
2307
+ }
2308
+ globalThis.__aioptDashCache = globalThis.__aioptDashCache || {};
2309
+ const cache = globalThis.__aioptDashCache;
2310
+ if (!cache.usage || cache.usage.mtimeMs !== st.mtimeMs) {
2311
+ const txt2 = import_fs11.default.readFileSync(usagePath, "utf8");
2312
+ const now = Date.now();
2313
+ const liveBins = Array.from({ length: 60 }, () => ({ cost: 0, calls: 0 }));
2314
+ const dayBins = Array.from({ length: 7 }, () => ({ cost: 0, calls: 0 }));
2315
+ for (const line of txt2.split(/\r?\n/)) {
2316
+ if (!line.trim()) continue;
2317
+ try {
2318
+ const ev = JSON.parse(line);
2319
+ const t = Date.parse(ev.ts);
2320
+ if (!Number.isFinite(t)) continue;
2321
+ const cost = Number(ev.cost_usd);
2322
+ const c = Number.isFinite(cost) ? cost : 0;
2323
+ const dm = Math.floor((now - t) / 6e4);
2324
+ if (dm >= 0 && dm < 60) {
2325
+ liveBins[dm].calls += 1;
2326
+ liveBins[dm].cost += c;
2327
+ }
2328
+ const dd = Math.floor((now - t) / 864e5);
2329
+ if (dd >= 0 && dd < 7) {
2330
+ dayBins[dd].calls += 1;
2331
+ dayBins[dd].cost += c;
2332
+ }
2333
+ } catch {
2334
+ }
2335
+ }
2336
+ cache.usage = {
2337
+ mtimeMs: st.mtimeMs,
2338
+ live60m: {
2339
+ bins: liveBins,
2340
+ totalCostUsd: Math.round(liveBins.reduce((a, b) => a + b.cost, 0) * 100) / 100
2341
+ },
2342
+ summary7d: {
2343
+ dayBins
2344
+ }
2345
+ };
2346
+ }
2347
+ const body = name === "live-60m.json" ? cache.usage.live60m : cache.usage.summary7d;
2348
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
2349
+ res.end(JSON.stringify(body, null, 2));
2350
+ return;
2351
+ }
2245
2352
  const p = file(name);
2246
2353
  const txt = readOrNull(p);
2247
2354
  if (txt === null) {
@@ -2250,7 +2357,7 @@ if(liveEl) liveEl.textContent = 'live: on (polling)';
2250
2357
  return;
2251
2358
  }
2252
2359
  const ct = name.endsWith(".json") ? "application/json; charset=utf-8" : "text/plain; charset=utf-8";
2253
- res.writeHead(200, { "content-type": ct });
2360
+ res.writeHead(200, { "content-type": ct, "cache-control": "no-store" });
2254
2361
  res.end(txt);
2255
2362
  return;
2256
2363
  }
@@ -2274,6 +2381,9 @@ var init_dashboard = __esm({
2274
2381
  import_fs11 = __toESM(require("fs"));
2275
2382
  import_path12 = __toESM(require("path"));
2276
2383
  init_collect();
2384
+ init_scan();
2385
+ init_io();
2386
+ init_guard();
2277
2387
  }
2278
2388
  });
2279
2389