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 +222 -112
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
const
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
const
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
'))
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
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
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
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
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
'<
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
const
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
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('
|
|
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
|
-
//
|
|
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([
|
|
2239
|
-
|
|
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
|
|