@askalf/dario 6.16.0 → 6.17.1
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/README.md +21 -23
- package/dist/analytics-ui.html +835 -0
- package/dist/analytics.d.ts +26 -1
- package/dist/analytics.js +51 -3
- package/dist/cc-template-data.json +3 -3
- package/dist/cli.js +9 -0
- package/dist/credits-rung.d.ts +72 -0
- package/dist/credits-rung.js +133 -0
- package/dist/doctor-core.d.ts +2 -0
- package/dist/doctor-core.js +47 -10
- package/dist/donuts.d.ts +18 -17
- package/dist/donuts.js +46 -54
- package/dist/ledger.d.ts +20 -0
- package/dist/ledger.js +40 -1
- package/dist/live-fingerprint.d.ts +1 -1
- package/dist/live-fingerprint.js +1 -1
- package/dist/metrics.js +2 -1
- package/dist/proxy.d.ts +10 -0
- package/dist/proxy.js +292 -74
- package/docs/analytics.md +10 -8
- package/docs/api-equivalent-spend.md +9 -2
- package/docs/faq.md +5 -0
- package/docs/multi-account-pool.md +1 -1
- package/docs/pool-keys-and-analytics.md +2 -0
- package/docs/returning.md +1 -1
- package/docs/two-plans.md +2 -0
- package/docs/who-its-for.md +2 -2
- package/package.json +8 -4
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
6
|
+
<title>dario</title>
|
|
7
|
+
<style>:root{padding-top:env(safe-area-inset-top,0px)}[hidden]{display:none!important}</style>
|
|
8
|
+
<script>window.DARIO_COCKPIT="served"</script>
|
|
9
|
+
<style>
|
|
10
|
+
/* Layout: a sticky brand bar, then a 12-column control room. Summary first (hero figure + KPI row),
|
|
11
|
+
seats and routing next, the live feed under them, and the ledger below its own range row.
|
|
12
|
+
Dark first: the brand palette from scripts/readme/lib.mjs; light mirrors it. Chart series slots
|
|
13
|
+
were validated with the dataviz six checks on both surfaces (#12111a and #ffffff). */
|
|
14
|
+
:root{
|
|
15
|
+
color-scheme:dark;
|
|
16
|
+
--bg:#0a0a0f; --panel:#12111a; --panel2:#17161f; --line:#2a2740; --line2:#1f1d2e;
|
|
17
|
+
--text:#e8e6f5; --dim:#8a85a8; --muted:#5a5670;
|
|
18
|
+
--accent:#8b5cf6; --bright:#c084fc; --glow:rgba(124,58,237,.22);
|
|
19
|
+
--good:#34d399; --warn:#fbbf24; --bad:#f87171; --info:#22d3ee;
|
|
20
|
+
--s1:#7c3aed; --s2:#0891b2; --s3:#2563eb; --s4:#ea580c; --s5:#c026d3; --other:#5a5670;
|
|
21
|
+
--grid:#1f1d2e; --axis:#2a2740; --track:#26243a;
|
|
22
|
+
--ink-on-fill:#ffffff;
|
|
23
|
+
--shadow:0 10px 30px rgba(0,0,0,.35);
|
|
24
|
+
--font-ui:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
|
|
25
|
+
--font-mono:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;
|
|
26
|
+
}
|
|
27
|
+
@media (prefers-color-scheme: light){
|
|
28
|
+
:root:not([data-theme="dark"]){
|
|
29
|
+
color-scheme:light;
|
|
30
|
+
--bg:#f6f8fa; --panel:#ffffff; --panel2:#f6f8fa; --line:#d0d7de; --line2:#e4e1ee;
|
|
31
|
+
--text:#1f2328; --dim:#59636e; --muted:#8b949e;
|
|
32
|
+
--accent:#6d28d9; --bright:#7c3aed; --glow:rgba(109,40,217,.14);
|
|
33
|
+
--good:#1a7f37; --warn:#9a6700; --bad:#cf222e; --info:#0e7490;
|
|
34
|
+
--s1:#6d28d9; --s2:#0891b2; --s3:#1d4ed8; --s4:#ea580c; --s5:#c026d3; --other:#8b949e;
|
|
35
|
+
--grid:#eceaf3; --axis:#d0d7de; --track:#e6e0f5;
|
|
36
|
+
--shadow:0 10px 30px rgba(31,35,40,.10);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
:root[data-theme="light"]{
|
|
40
|
+
color-scheme:light;
|
|
41
|
+
--bg:#f6f8fa; --panel:#ffffff; --panel2:#f6f8fa; --line:#d0d7de; --line2:#e4e1ee;
|
|
42
|
+
--text:#1f2328; --dim:#59636e; --muted:#8b949e;
|
|
43
|
+
--accent:#6d28d9; --bright:#7c3aed; --glow:rgba(109,40,217,.14);
|
|
44
|
+
--good:#1a7f37; --warn:#9a6700; --bad:#cf222e; --info:#0e7490;
|
|
45
|
+
--s1:#6d28d9; --s2:#0891b2; --s3:#1d4ed8; --s4:#ea580c; --s5:#c026d3; --other:#8b949e;
|
|
46
|
+
--grid:#eceaf3; --axis:#d0d7de; --track:#e6e0f5;
|
|
47
|
+
--shadow:0 10px 30px rgba(31,35,40,.10);
|
|
48
|
+
}
|
|
49
|
+
*{box-sizing:border-box}
|
|
50
|
+
body{margin:0;background:var(--bg);color:var(--text);font:14px/1.45 var(--font-ui);-webkit-font-smoothing:antialiased}
|
|
51
|
+
body::before{content:"";position:fixed;inset:0 0 auto;height:env(safe-area-inset-top,0px);z-index:30;background:var(--bg)}
|
|
52
|
+
a{color:var(--bright)}
|
|
53
|
+
button{font:inherit;color:inherit}
|
|
54
|
+
h1,h2,h3{margin:0;font-weight:600;text-wrap:balance}
|
|
55
|
+
.eyebrow{font:700 10.5px/1 var(--font-mono);letter-spacing:.12em;text-transform:uppercase;color:var(--dim)}
|
|
56
|
+
.mono{font-family:var(--font-mono)}
|
|
57
|
+
.num{font-variant-numeric:tabular-nums}
|
|
58
|
+
.dimtext{color:var(--dim)}
|
|
59
|
+
.mutedtext{color:var(--muted)}
|
|
60
|
+
.hidden{display:none !important}
|
|
61
|
+
|
|
62
|
+
/* bar */
|
|
63
|
+
.bar{position:sticky;top:env(safe-area-inset-top,0px);z-index:20;background:color-mix(in oklab,var(--bg) 86%,transparent);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid var(--line)}
|
|
64
|
+
.bar-in{max-width:1440px;margin:0 auto;display:flex;align-items:center;gap:14px;flex-wrap:wrap;padding-left:max(16px,env(safe-area-inset-left,0px));padding-right:max(16px,env(safe-area-inset-right,0px));padding-block:10px;min-height:54px}
|
|
65
|
+
.brand{display:flex;align-items:baseline;gap:10px}
|
|
66
|
+
.brand .mark{font:700 20px/1 var(--font-mono);letter-spacing:-.02em;color:var(--text)}
|
|
67
|
+
.brand .mark b{color:var(--bright);font-weight:700}
|
|
68
|
+
.pills{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
69
|
+
.bar-right{margin-left:auto;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
|
70
|
+
.meta{font:12px/1.2 var(--font-mono);color:var(--dim)}
|
|
71
|
+
.pill{display:inline-flex;align-items:center;gap:7px;padding:5px 10px;border-radius:999px;border:1px solid var(--line);background:var(--panel);font:12px/1 var(--font-mono);color:var(--text)}
|
|
72
|
+
.pill .dot{width:8px;height:8px;border-radius:50%;background:var(--muted)}
|
|
73
|
+
.pill.good .dot{background:var(--good);box-shadow:0 0 0 3px color-mix(in oklab,var(--good) 25%,transparent)}
|
|
74
|
+
.pill.warn .dot{background:var(--warn);box-shadow:0 0 0 3px color-mix(in oklab,var(--warn) 25%,transparent)}
|
|
75
|
+
.pill.bad .dot{background:var(--bad);box-shadow:0 0 0 3px color-mix(in oklab,var(--bad) 25%,transparent)}
|
|
76
|
+
.pill.info .dot{background:var(--info);box-shadow:0 0 0 3px color-mix(in oklab,var(--info) 25%,transparent)}
|
|
77
|
+
.pill.live .dot{animation:pulse 1.6s ease-in-out infinite}
|
|
78
|
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}
|
|
79
|
+
.btn{display:inline-flex;align-items:center;gap:8px;padding:7px 12px;border-radius:8px;border:1px solid var(--line);background:var(--panel);cursor:pointer;font-size:13px;font-weight:500}
|
|
80
|
+
.btn:hover{border-color:var(--accent)}
|
|
81
|
+
.btn.primary{background:var(--accent);border-color:var(--accent);color:#fff}
|
|
82
|
+
.btn.primary:hover{filter:brightness(1.08)}
|
|
83
|
+
.icon-btn{width:34px;height:34px;display:inline-grid;place-items:center;border-radius:8px;border:1px solid var(--line);background:var(--panel);cursor:pointer}
|
|
84
|
+
.icon-btn:hover{border-color:var(--accent)}
|
|
85
|
+
.icon-btn svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
|
|
86
|
+
:focus-visible{outline:2px solid var(--bright);outline-offset:2px}
|
|
87
|
+
|
|
88
|
+
.pop{position:absolute;right:max(16px,env(safe-area-inset-right,0px));top:calc(100% + 8px);width:min(420px,calc(100vw - 32px));background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 16px;box-shadow:var(--shadow);z-index:25}
|
|
89
|
+
.pop h3{font-size:14px;margin-bottom:6px}
|
|
90
|
+
.pop p{margin:0 0 10px;color:var(--dim);font-size:12.5px}
|
|
91
|
+
.pop .row{display:flex;gap:8px}
|
|
92
|
+
.pop input{flex:1;min-width:0;padding:8px 10px;border-radius:8px;border:1px solid var(--line);background:var(--panel2);color:var(--text);font:13px var(--font-mono)}
|
|
93
|
+
.pop .note{margin-top:8px;font-size:12px;color:var(--warn);min-height:1em}
|
|
94
|
+
.pop .ok{color:var(--good)}
|
|
95
|
+
|
|
96
|
+
/* page */
|
|
97
|
+
.wrap{max-width:1440px;margin:0 auto;padding-left:max(16px,env(safe-area-inset-left,0px));padding-right:max(16px,env(safe-area-inset-right,0px));padding-block:18px 48px;display:flex;flex-direction:column;gap:16px}
|
|
98
|
+
.banner{display:none;align-items:center;gap:10px;padding:10px 14px;border-radius:10px;border:1px solid color-mix(in oklab,var(--bad) 45%,var(--line));background:color-mix(in oklab,var(--bad) 10%,var(--panel));font-size:13px}
|
|
99
|
+
.banner.show{display:flex}
|
|
100
|
+
.banner.warn{border-color:color-mix(in oklab,var(--warn) 45%,var(--line));background:color-mix(in oklab,var(--warn) 10%,var(--panel))}
|
|
101
|
+
.banner .btn{margin-left:auto}
|
|
102
|
+
|
|
103
|
+
.grid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:14px}
|
|
104
|
+
.span3{grid-column:span 3}.span4{grid-column:span 4}.span5{grid-column:span 5}.span6{grid-column:span 6}.span7{grid-column:span 7}.span8{grid-column:span 8}.span12{grid-column:span 12}
|
|
105
|
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:16px 18px;min-width:0;display:flex;flex-direction:column;gap:12px}
|
|
106
|
+
.card-h{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
|
107
|
+
.card-h h2{font-size:15px}
|
|
108
|
+
.card-h .sub{color:var(--dim);font-size:12.5px}
|
|
109
|
+
.card-h .tools{margin-left:auto;display:flex;gap:6px;align-items:center}
|
|
110
|
+
.tbtn{padding:4px 9px;border-radius:6px;border:1px solid var(--line);background:transparent;font:11.5px var(--font-mono);color:var(--dim);cursor:pointer}
|
|
111
|
+
.tbtn[aria-pressed="true"]{color:var(--text);border-color:var(--accent);background:color-mix(in oklab,var(--accent) 14%,transparent)}
|
|
112
|
+
.tbtn:hover{color:var(--text)}
|
|
113
|
+
.stale{opacity:.55;transition:opacity .2s}
|
|
114
|
+
|
|
115
|
+
/* hero */
|
|
116
|
+
.hero-row{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:14px}
|
|
117
|
+
.hero{position:relative;overflow:hidden;background:radial-gradient(120% 140% at 0% 0%,var(--glow),transparent 55%),var(--panel)}
|
|
118
|
+
.hero .big{font-size:clamp(40px,4.6vw,56px);font-weight:600;line-height:1;letter-spacing:-.02em;margin:6px 0 4px}
|
|
119
|
+
.hero .big small{font-size:.42em;font-weight:500;color:var(--dim);letter-spacing:0;margin-left:6px}
|
|
120
|
+
.hero .lede{color:var(--dim);font-size:13px;max-width:36em}
|
|
121
|
+
.hero .split{display:flex;gap:18px;flex-wrap:wrap;margin-top:auto}
|
|
122
|
+
.hero .split div{display:flex;flex-direction:column;gap:3px}
|
|
123
|
+
.hero .split b{font-size:18px;font-weight:600}
|
|
124
|
+
.hero .split .eyebrow{font-size:10px}
|
|
125
|
+
.kpis{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}
|
|
126
|
+
.tile{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:6px;min-width:0}
|
|
127
|
+
.tile .lbl{font-size:12px;color:var(--dim)}
|
|
128
|
+
.tile .val{font-size:24px;font-weight:600;line-height:1.1;letter-spacing:-.01em}
|
|
129
|
+
.tile .val small{font-size:13px;color:var(--dim);font-weight:500;margin-left:4px}
|
|
130
|
+
.tile .foot{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:11.5px;color:var(--dim)}
|
|
131
|
+
.tile .delta{font-weight:500}
|
|
132
|
+
.tile .delta.up{color:var(--good)}.tile .delta.down{color:var(--bad)}.tile .delta.flat{color:var(--dim)}
|
|
133
|
+
.spark{width:120px;height:36px;flex:none}
|
|
134
|
+
.spark path.area{fill:var(--s1);fill-opacity:.12}
|
|
135
|
+
.spark path.line{fill:none;stroke:var(--s1);stroke-width:2;stroke-linejoin:round;stroke-linecap:round}
|
|
136
|
+
.spark circle.end{fill:var(--s1);stroke:var(--panel);stroke-width:2}
|
|
137
|
+
|
|
138
|
+
/* meters */
|
|
139
|
+
.meter{height:8px;border-radius:4px;background:var(--track);position:relative;overflow:hidden}
|
|
140
|
+
.meter i{position:absolute;inset:0 auto 0 0;border-radius:4px;background:var(--accent);min-width:2px}
|
|
141
|
+
.meter.warn{background:color-mix(in oklab,var(--warn) 22%,var(--track))}.meter.warn i{background:var(--warn)}
|
|
142
|
+
.meter.bad{background:color-mix(in oklab,var(--bad) 22%,var(--track))}.meter.bad i{background:var(--bad)}
|
|
143
|
+
.meter.ok{background:color-mix(in oklab,var(--accent) 22%,var(--track))}.meter.ok i{background:var(--accent)}
|
|
144
|
+
|
|
145
|
+
/* seats */
|
|
146
|
+
.seats{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:12px}
|
|
147
|
+
.seat{border:1px solid var(--line2);border-radius:12px;padding:12px 14px;background:var(--panel2);display:flex;flex-direction:column;gap:10px;min-width:0}
|
|
148
|
+
.seat .top{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
149
|
+
.seat .alias{font:700 14px/1 var(--font-mono)}
|
|
150
|
+
.seat .head{display:flex;align-items:baseline;justify-content:space-between;gap:8px}
|
|
151
|
+
.seat .head b{font-size:26px;font-weight:600;letter-spacing:-.01em;line-height:1}
|
|
152
|
+
.seat .head span{font-size:11.5px;color:var(--dim);text-align:right}
|
|
153
|
+
.seat .u{display:flex;flex-direction:column;gap:4px}
|
|
154
|
+
.seat .u .r{display:flex;justify-content:space-between;font-size:11.5px;color:var(--dim)}
|
|
155
|
+
.seat .u .r b{color:var(--text);font-weight:500}
|
|
156
|
+
.seat .foot{display:flex;justify-content:space-between;gap:8px;font-size:11.5px;color:var(--dim);flex-wrap:wrap}
|
|
157
|
+
.chip{display:inline-flex;align-items:center;gap:5px;padding:2px 7px;border-radius:999px;font:11px/1.5 var(--font-mono);background:color-mix(in oklab,var(--muted) 22%,transparent);color:var(--text);white-space:nowrap}
|
|
158
|
+
.chip.good{background:color-mix(in oklab,var(--good) 16%,transparent);color:var(--good)}
|
|
159
|
+
.chip.warn{background:color-mix(in oklab,var(--warn) 18%,transparent);color:var(--warn)}
|
|
160
|
+
.chip.bad{background:color-mix(in oklab,var(--bad) 16%,transparent);color:var(--bad)}
|
|
161
|
+
.chip.info{background:color-mix(in oklab,var(--info) 16%,transparent);color:var(--info)}
|
|
162
|
+
.chip.accent{background:color-mix(in oklab,var(--accent) 18%,transparent);color:var(--bright)}
|
|
163
|
+
.chip.ghost{background:transparent;border:1px solid var(--line);color:var(--dim)}
|
|
164
|
+
|
|
165
|
+
/* stacked single bar + legend */
|
|
166
|
+
.stack{display:flex;gap:2px;height:22px;border-radius:0 4px 4px 0;overflow:hidden}
|
|
167
|
+
.stack i{display:flex;align-items:center;justify-content:center;min-width:0;height:100%;color:var(--ink-on-fill);font:600 11px/1 var(--font-ui);white-space:nowrap;cursor:default;transition:filter .15s}
|
|
168
|
+
.stack i:last-child{border-radius:0 4px 4px 0}
|
|
169
|
+
.stack i:hover{filter:brightness(1.15)}
|
|
170
|
+
.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--dim)}
|
|
171
|
+
.legend span{display:inline-flex;align-items:center;gap:6px}
|
|
172
|
+
.legend span b{color:var(--text);font-weight:500}
|
|
173
|
+
.legend .sw{width:10px;height:10px;border-radius:2px;flex:none}
|
|
174
|
+
.legend .lk{width:14px;height:2px;border-radius:1px;flex:none}
|
|
175
|
+
.figure{display:flex;align-items:baseline;gap:8px}
|
|
176
|
+
.figure b{font-size:28px;font-weight:600;letter-spacing:-.01em;line-height:1}
|
|
177
|
+
.figure span{color:var(--dim);font-size:12.5px}
|
|
178
|
+
|
|
179
|
+
/* horizontal bars */
|
|
180
|
+
.hbars{display:grid;grid-template-columns:minmax(90px,150px) minmax(0,1fr) auto;gap:8px 10px;align-items:center}
|
|
181
|
+
.hbars .lab{font-size:12.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
|
|
182
|
+
.hbars .bar{display:flex;gap:2px;height:16px;min-width:0}
|
|
183
|
+
.hbars .bar i{height:100%;min-width:0;display:block;cursor:default;transition:filter .15s}
|
|
184
|
+
.hbars .bar i:last-child{border-radius:0 4px 4px 0}
|
|
185
|
+
.hbars .bar i:hover{filter:brightness(1.15)}
|
|
186
|
+
.hbars .v{font-size:12.5px;color:var(--text);text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
187
|
+
|
|
188
|
+
/* tables */
|
|
189
|
+
.tbl{width:100%;border-collapse:collapse;font-size:12.5px}
|
|
190
|
+
.tbl th,.tbl td{text-align:right;padding:6px 8px;border-bottom:1px solid var(--line2);white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
191
|
+
.tbl th:first-child,.tbl td:first-child,.tbl th.l,.tbl td.l{text-align:left}
|
|
192
|
+
.tbl th{font:700 10.5px/1.2 var(--font-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--dim);position:sticky;top:0;background:var(--panel)}
|
|
193
|
+
.tbl td.mono{font-family:var(--font-mono);font-size:11.5px}
|
|
194
|
+
.tbl tr.new td{animation:flash 1.2s ease-out}
|
|
195
|
+
@keyframes flash{0%{background:color-mix(in oklab,var(--accent) 22%,transparent)}100%{background:transparent}}
|
|
196
|
+
.tscroll{overflow:auto;max-height:420px;border:1px solid var(--line2);border-radius:10px}
|
|
197
|
+
.tscroll.short{max-height:300px}
|
|
198
|
+
.tbl.twin{margin-top:4px}
|
|
199
|
+
|
|
200
|
+
/* feed */
|
|
201
|
+
.feed-tools{display:flex;gap:8px;align-items:center;font-size:12px;color:var(--dim)}
|
|
202
|
+
.feed .tscroll{max-height:460px}
|
|
203
|
+
|
|
204
|
+
/* ledger */
|
|
205
|
+
.range-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
|
206
|
+
.range-row .eyebrow{margin-right:4px}
|
|
207
|
+
.seg{display:inline-flex;border:1px solid var(--line);border-radius:8px;overflow:hidden;background:var(--panel)}
|
|
208
|
+
.seg button{padding:6px 12px;border:0;background:transparent;cursor:pointer;font-size:12.5px;color:var(--dim)}
|
|
209
|
+
.seg button+button{border-left:1px solid var(--line)}
|
|
210
|
+
.seg button[aria-pressed="true"]{background:color-mix(in oklab,var(--accent) 16%,transparent);color:var(--text);font-weight:500}
|
|
211
|
+
.range-row .scope{color:var(--dim);font-size:12.5px}
|
|
212
|
+
|
|
213
|
+
/* area chart */
|
|
214
|
+
.chart{position:relative;width:100%;min-width:0}
|
|
215
|
+
.chart svg{display:block;width:100%;height:auto;overflow:visible}
|
|
216
|
+
.chart .grid line{stroke:var(--grid);stroke-width:1}
|
|
217
|
+
.chart .axis line{stroke:var(--axis);stroke-width:1}
|
|
218
|
+
.chart text{font:11px var(--font-ui);fill:var(--dim);font-variant-numeric:tabular-nums}
|
|
219
|
+
.chart .area{fill:var(--s1);fill-opacity:.12}
|
|
220
|
+
.chart .line{fill:none;stroke:var(--s1);stroke-width:2;stroke-linejoin:round;stroke-linecap:round}
|
|
221
|
+
.chart .end{fill:var(--s1);stroke:var(--panel);stroke-width:2}
|
|
222
|
+
.chart .ctx{opacity:.45}
|
|
223
|
+
.chart .xh line{stroke:var(--dim);stroke-width:1}
|
|
224
|
+
.chart .xh circle{fill:var(--s1);stroke:var(--panel);stroke-width:2}
|
|
225
|
+
.chart .today{fill:var(--s1);stroke:var(--panel);stroke-width:2}
|
|
226
|
+
.chart .endlab{fill:var(--text);font-weight:500}
|
|
227
|
+
|
|
228
|
+
/* keys */
|
|
229
|
+
.keys{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px}
|
|
230
|
+
.key{border:1px solid var(--line2);border-radius:12px;padding:12px 14px;background:var(--panel2);display:flex;flex-direction:column;gap:8px;min-width:0}
|
|
231
|
+
.key .top{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
232
|
+
.key .name{font:700 13.5px/1 var(--font-mono)}
|
|
233
|
+
.key .r{display:flex;justify-content:space-between;font-size:11.5px;color:var(--dim)}
|
|
234
|
+
.key .r b{color:var(--text);font-weight:500}
|
|
235
|
+
.key .models{font-size:11.5px;color:var(--dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
236
|
+
.hint{font-size:12.5px;color:var(--dim);padding:10px 12px;border:1px dashed var(--line);border-radius:10px}
|
|
237
|
+
.hint code{font-family:var(--font-mono);font-size:11.5px;color:var(--text)}
|
|
238
|
+
|
|
239
|
+
/* tooltip */
|
|
240
|
+
.tip{position:fixed;z-index:60;pointer-events:none;background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:8px 10px;font-size:12px;box-shadow:var(--shadow);max-width:280px;min-width:140px}
|
|
241
|
+
.tip .t{color:var(--dim);margin-bottom:4px;font-family:var(--font-mono);font-size:11px}
|
|
242
|
+
.tip .r{display:flex;align-items:center;gap:8px;justify-content:space-between;padding:1px 0}
|
|
243
|
+
.tip .r .k{display:inline-flex;align-items:center;gap:6px;color:var(--dim)}
|
|
244
|
+
.tip .r .k i{width:12px;height:2px;border-radius:1px;display:inline-block}
|
|
245
|
+
.tip .r b{font-weight:600;color:var(--text);font-variant-numeric:tabular-nums}
|
|
246
|
+
|
|
247
|
+
footer{color:var(--muted);font-size:12px;display:flex;gap:14px;flex-wrap:wrap;padding-top:8px;border-top:1px solid var(--line2)}
|
|
248
|
+
footer code{font-family:var(--font-mono);font-size:11px;color:var(--dim)}
|
|
249
|
+
|
|
250
|
+
@media (max-width:1100px){.span5,.span7{grid-column:span 6}}
|
|
251
|
+
@media (max-width:960px){.hero-row{grid-template-columns:1fr}.span3,.span4,.span5,.span6,.span7,.span8{grid-column:span 12}}
|
|
252
|
+
@media (max-width:560px){.kpis{grid-template-columns:repeat(2,minmax(0,1fr))}.hbars{grid-template-columns:minmax(70px,100px) minmax(0,1fr) auto}.card{padding:14px}.spark{width:84px;height:28px}.hero .big{font-size:38px}}
|
|
253
|
+
@media (prefers-reduced-motion: reduce){.pill.live .dot{animation:none}.tbl tr.new td{animation:none}.stale{transition:none}}
|
|
254
|
+
</style>
|
|
255
|
+
</head>
|
|
256
|
+
<body>
|
|
257
|
+
|
|
258
|
+
<header class="bar">
|
|
259
|
+
<div class="bar-in">
|
|
260
|
+
<div class="brand"><span class="mark">d<b>a</b>rio</span><span class="eyebrow">cockpit</span><span class="chip ghost mono" id="verChip">v0.0.0</span></div>
|
|
261
|
+
<div class="pills">
|
|
262
|
+
<span class="pill" id="statusPill"><i class="dot"></i><span id="statusText">connecting</span></span>
|
|
263
|
+
<span class="pill" id="modePill"><i class="dot"></i><span id="modeText">detecting</span></span>
|
|
264
|
+
</div>
|
|
265
|
+
<div class="bar-right">
|
|
266
|
+
<span class="meta" id="bindMeta"></span>
|
|
267
|
+
<button class="icon-btn" id="themeBtn" aria-label="Theme: follows the system" title="Theme: follows the system">
|
|
268
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
|
|
269
|
+
</button>
|
|
270
|
+
<button class="btn" id="connectBtn" aria-expanded="false" aria-controls="connectPop">
|
|
271
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" width="14" height="14" style="fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round"><circle cx="8" cy="15" r="4"/><path d="M10.8 12.2 20 3M15 8l3 3M18 5l2 2"/></svg>
|
|
272
|
+
Token
|
|
273
|
+
</button>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
<div class="pop" id="connectPop" hidden>
|
|
277
|
+
<h3>Read access</h3>
|
|
278
|
+
<p>Paste the analytics token (<code class="mono">DARIO_ANALYTICS_TOKEN</code>) to read spend and the live feed, or an API key to read seats and keys as well. Kept in this tab only.</p>
|
|
279
|
+
<div class="row"><input id="tokenInput" type="password" placeholder="analytics token or API key" autocomplete="off" aria-label="Analytics token or API key"><button class="btn primary" id="tokenSave">Connect</button></div>
|
|
280
|
+
<div class="note" id="tokenNote"></div>
|
|
281
|
+
</div>
|
|
282
|
+
</header>
|
|
283
|
+
|
|
284
|
+
<main class="wrap">
|
|
285
|
+
<div class="banner" id="banner" role="status"><span id="bannerText"></span><button class="btn" id="bannerBtn" hidden>Resume</button></div>
|
|
286
|
+
|
|
287
|
+
<section class="hero-row">
|
|
288
|
+
<article class="card hero">
|
|
289
|
+
<div class="eyebrow">Covered by subscriptions · today</div>
|
|
290
|
+
<div class="big"><span id="heroVal">$0.00</span><small id="heroPct"></small></div>
|
|
291
|
+
<p class="lede" id="heroLede">What the same requests would have cost at API list prices, and the share your seats absorbed.</p>
|
|
292
|
+
<div class="split">
|
|
293
|
+
<div><span class="eyebrow">metered today</span><b id="heroMetered">$0.00</b></div>
|
|
294
|
+
<div><span class="eyebrow">requests today</span><b id="heroReq">0</b></div>
|
|
295
|
+
<div><span class="eyebrow">lifetime covered</span><b id="heroLife">$0</b></div>
|
|
296
|
+
<div><span class="eyebrow">ledger since</span><b id="heroSince" class="mono" style="font-size:14px">...</b></div>
|
|
297
|
+
</div>
|
|
298
|
+
</article>
|
|
299
|
+
<div class="kpis" id="kpis"></div>
|
|
300
|
+
</section>
|
|
301
|
+
|
|
302
|
+
<section class="grid">
|
|
303
|
+
<article class="card span7" id="seatsCard">
|
|
304
|
+
<div class="card-h"><h2>Seats</h2><span class="sub" id="seatsSub">pool · headroom routing</span><div class="tools"><span class="chip ghost" id="poolChip"></span></div></div>
|
|
305
|
+
<div class="seats" id="seats"></div>
|
|
306
|
+
</article>
|
|
307
|
+
<article class="card span5" id="routingCard">
|
|
308
|
+
<div class="card-h"><h2>Routing</h2><span class="sub">who paid for the last 60 minutes</span><div class="tools"><button class="tbtn" data-table="routing" aria-pressed="false">Table</button></div></div>
|
|
309
|
+
<div class="figure"><b id="subPct">0%</b><span>of requests served on a subscription</span></div>
|
|
310
|
+
<div id="routingChart"></div>
|
|
311
|
+
<div id="routingTable" class="hidden"></div>
|
|
312
|
+
<div class="card-h" style="margin-top:4px"><h2 style="font-size:13.5px">Burn</h2><span class="sub">window rates</span></div>
|
|
313
|
+
<div class="kpis" id="burn" style="grid-template-columns:repeat(auto-fit,minmax(140px,1fr))"></div>
|
|
314
|
+
</article>
|
|
315
|
+
|
|
316
|
+
<article class="card span7 feed" id="feedCard">
|
|
317
|
+
<div class="card-h"><h2>Live feed</h2><span class="sub" id="feedSub">replaying the last 50, then live</span>
|
|
318
|
+
<div class="tools feed-tools"><span id="feedCount"></span><button class="tbtn" id="pauseBtn" aria-pressed="false">Pause</button></div></div>
|
|
319
|
+
<div class="tscroll"><table class="tbl" id="feedTable"><thead><tr><th class="l">time</th><th class="l">consumer</th><th class="l">model</th><th class="l">seat</th><th>in</th><th>out</th><th>cache</th><th>ms</th><th class="l">status</th><th class="l">claim</th></tr></thead><tbody id="feedBody"></tbody></table></div>
|
|
320
|
+
</article>
|
|
321
|
+
<article class="card span5" id="anatomyCard">
|
|
322
|
+
<div class="card-h"><h2>Request anatomy</h2><span class="sub">average milliseconds, last 60 min</span><div class="tools"><button class="tbtn" data-table="anatomy" aria-pressed="false">Table</button></div></div>
|
|
323
|
+
<div class="figure"><b id="anatomyTotal">0 ms</b><span>mean wall time per request</span></div>
|
|
324
|
+
<div id="anatomyChart"></div>
|
|
325
|
+
<div id="anatomyTable" class="hidden"></div>
|
|
326
|
+
<div class="card-h" style="margin-top:4px"><h2 style="font-size:13.5px">Consumers</h2><span class="sub">last 60 min, by key</span></div>
|
|
327
|
+
<div class="tscroll short"><table class="tbl" id="consumersTable"><thead><tr><th class="l">consumer</th><th>req</th><th>api-equiv</th><th>cached</th><th class="l">last model</th><th class="l">seats</th></tr></thead><tbody id="consumersBody"></tbody></table></div>
|
|
328
|
+
</article>
|
|
329
|
+
</section>
|
|
330
|
+
|
|
331
|
+
<section class="card" id="ledgerCard" style="gap:14px">
|
|
332
|
+
<div class="range-row">
|
|
333
|
+
<span class="eyebrow">Ledger</span>
|
|
334
|
+
<div class="seg" role="group" aria-label="Ledger range" id="rangeSeg">
|
|
335
|
+
<button data-range="today" aria-pressed="true">Today</button><button data-range="7d" aria-pressed="false">7 days</button><button data-range="30d" aria-pressed="false">30 days</button><button data-range="all" aria-pressed="false">All time</button>
|
|
336
|
+
</div>
|
|
337
|
+
<span class="scope" id="rangeScope"></span>
|
|
338
|
+
<div class="tools" style="margin-left:auto"><span class="chip ghost mono" id="ledgerPath"></span></div>
|
|
339
|
+
</div>
|
|
340
|
+
<div class="kpis" id="ledgerTiles" style="grid-template-columns:repeat(auto-fit,minmax(150px,1fr))"></div>
|
|
341
|
+
<div class="grid">
|
|
342
|
+
<article class="card span6" style="background:var(--panel2);border-color:var(--line2)">
|
|
343
|
+
<div class="card-h"><h2 style="font-size:14px">Spend per model</h2><span class="sub">API-equivalent, covered vs metered</span><div class="tools"><button class="tbtn" data-table="models" aria-pressed="false">Table</button></div></div>
|
|
344
|
+
<div id="modelsChart"></div><div id="modelsTable" class="hidden"></div>
|
|
345
|
+
<div class="legend"><span><i class="sw" style="background:var(--s1)"></i>covered by a seat</span><span><i class="sw" style="background:var(--s2)"></i>metered (API or extra usage)</span></div>
|
|
346
|
+
</article>
|
|
347
|
+
<article class="card span6" style="background:var(--panel2);border-color:var(--line2)">
|
|
348
|
+
<div class="card-h"><h2 style="font-size:14px">Spend per consumer</h2><span class="sub">API-equivalent, covered vs metered</span><div class="tools"><button class="tbtn" data-table="consumersL" aria-pressed="false">Table</button></div></div>
|
|
349
|
+
<div id="consumersLChart"></div><div id="consumersLTable" class="hidden"></div>
|
|
350
|
+
<div class="legend"><span><i class="sw" style="background:var(--s1)"></i>covered by a seat</span><span><i class="sw" style="background:var(--s2)"></i>metered (API or extra usage)</span></div>
|
|
351
|
+
</article>
|
|
352
|
+
<article class="card span12" style="background:var(--panel2);border-color:var(--line2)">
|
|
353
|
+
<div class="card-h"><h2 style="font-size:14px">API-equivalent per day</h2><span class="sub" id="dailySub"></span><div class="tools"><button class="tbtn" data-table="daily" aria-pressed="false">Table</button></div></div>
|
|
354
|
+
<div class="chart" id="dailyChart"></div><div id="dailyTable" class="hidden"></div>
|
|
355
|
+
</article>
|
|
356
|
+
</div>
|
|
357
|
+
</section>
|
|
358
|
+
|
|
359
|
+
<section class="grid">
|
|
360
|
+
<article class="card span12" id="keysCard">
|
|
361
|
+
<div class="card-h"><h2>Keys and budgets</h2><span class="sub" id="keysSub">named keys, daily budget used</span></div>
|
|
362
|
+
<div id="keysWrap"></div>
|
|
363
|
+
</article>
|
|
364
|
+
</section>
|
|
365
|
+
|
|
366
|
+
<footer>
|
|
367
|
+
<span id="footLeft">dario</span>
|
|
368
|
+
<span>reads <code>/analytics</code> every 5 s · <code>/accounts</code> <code>/codex</code> every 15 s · <code>/analytics/ledger</code> every 60 s · <code>/analytics/stream</code> live</span>
|
|
369
|
+
<span id="footRight"></span>
|
|
370
|
+
</footer>
|
|
371
|
+
</main>
|
|
372
|
+
<div class="tip" id="tip" hidden></div>
|
|
373
|
+
|
|
374
|
+
<script id="cockpit-maths">
|
|
375
|
+
/* The ledger arithmetic and the boot decision, kept free of the DOM so test/analytics-ui.mjs can run this block on its own. */
|
|
376
|
+
var DarioMaths=(function(){
|
|
377
|
+
'use strict';
|
|
378
|
+
var DAY=86400000;
|
|
379
|
+
function dayKey(ms){ return new Date(ms).toISOString().slice(0,10); }
|
|
380
|
+
/* The UTC calendar days a range covers, oldest first and ending today. An idle day is still a day: "today" is today even when the last request was yesterday, and "7d" is the last seven dates, not the last seven dates with traffic. "all" runs from the oldest day in the table to today, every date in between: the proxy keeps 730 dates with traffic, which can span many more calendar days, and none of them is dropped. */
|
|
381
|
+
function calendarDays(ledger, range, nowMs){
|
|
382
|
+
var n=range==='today'?1:range==='7d'?7:range==='30d'?30:0;
|
|
383
|
+
if(!n){ var keys=Object.keys((ledger&&ledger.days)||{}).sort(); var today=dayKey(nowMs); var first=keys.length&&keys[0]<today?keys[0]:today; n=Math.round((Date.parse(today+'T00:00:00Z')-Date.parse(first+'T00:00:00Z'))/DAY)+1; if(!(n>=1)) n=1; }
|
|
384
|
+
var out=[]; for(var i=n-1;i>=0;i--) out.push(dayKey(nowMs-i*DAY)); return out;
|
|
385
|
+
}
|
|
386
|
+
function usdOf(row, bucket){ var v=row&&row[bucket]; return typeof v==='number'&&isFinite(v)?v:0; }
|
|
387
|
+
function reqOf(cell){ return (cell&&cell.requests)||0; }
|
|
388
|
+
function tokOf(cell){ return cell?((cell.inputTokens||0)+(cell.outputTokens||0)+(cell.cacheReadTokens||0)+(cell.cacheCreateTokens||0)):0; }
|
|
389
|
+
/* Totals for a range. Money comes only from `priced`, the rows the proxy already priced at each day's rate; the token counts are never priced here. */
|
|
390
|
+
function ledgerSlice(ledger, range, nowMs){
|
|
391
|
+
var days=calendarDays(ledger, range, nowMs); var P=(ledger&&ledger.priced)||{}; var pd=P.days||{}, pc=P.consumers||{};
|
|
392
|
+
var perModel={}, perConsumer={}, daily=[], tot={covered:0,metered:0,requests:0,tokens:0};
|
|
393
|
+
days.forEach(function(key){ var dm=((ledger&&ledger.days)||{})[key]||{}; var pm=pd[key]||{}; var dayCov=0, dayMet=0, dayReq=0;
|
|
394
|
+
Object.keys(dm).forEach(function(m){ var cov=usdOf(pm[m],'covered'), met=usdOf(pm[m],'metered'); var req=reqOf(dm[m].covered)+reqOf(dm[m].metered); var tok=tokOf(dm[m].covered)+tokOf(dm[m].metered); var p=(perModel[m]=perModel[m]||{covered:0,metered:0,requests:0,tokens:0}); p.covered+=cov; p.metered+=met; p.requests+=req; p.tokens+=tok; dayCov+=cov; dayMet+=met; dayReq+=req; tot.tokens+=tok; });
|
|
395
|
+
var dc=((ledger&&ledger.consumers)||{})[key]||{}; var pcd=pc[key]||{}; Object.keys(dc).forEach(function(c){ var pcr=pcd[c]||{}; Object.keys(dc[c]).forEach(function(m){ var p=(perConsumer[c]=perConsumer[c]||{covered:0,metered:0,requests:0,tokens:0}); p.covered+=usdOf(pcr[m],'covered'); p.metered+=usdOf(pcr[m],'metered'); p.requests+=reqOf(dc[c][m].covered)+reqOf(dc[c][m].metered); p.tokens+=tokOf(dc[c][m].covered)+tokOf(dc[c][m].metered); }); });
|
|
396
|
+
daily.push({day:key,covered:dayCov,metered:dayMet,requests:dayReq}); tot.covered+=dayCov; tot.metered+=dayMet; tot.requests+=dayReq; });
|
|
397
|
+
return { days:days, perModel:perModel, perConsumer:perConsumer, daily:daily, total:tot };
|
|
398
|
+
}
|
|
399
|
+
/* How the page boots. Opened from disk it is always the sample, whatever the head says; served over http(s) with the marker it is live at once; served without the marker it probes /livez. */
|
|
400
|
+
function modeFor(protocol, marker){ if(protocol!=='http:'&&protocol!=='https:') return 'sample'; if(marker==='served') return 'live'; return 'probe'; }
|
|
401
|
+
/* A record's time as epoch ms. The stream sends RequestRecord.timestamp as the number the proxy recorded; the sample uses ISO strings; both read the same way, anything else is NaN. */
|
|
402
|
+
function tsOf(x){ var t=x&&x.timestamp; if(typeof t==='number') return t; if(typeof t==='string'){ var n=Date.parse(t); if(!isNaN(n)) return n; var f=Number(t); if(t.trim()!==''&&isFinite(f)) return f; } return NaN; }
|
|
403
|
+
/* Requests per five-minute bin over the last hour, oldest first, for the sparkline. */
|
|
404
|
+
function perFiveMinutes(records, nowMs){ var bins=[]; for(var i=0;i<12;i++) bins.push(0); (records||[]).forEach(function(x){ var age=nowMs-tsOf(x); if(!(age>=0&&age<3600000)) return; bins[11-Math.floor(age/300000)]++; }); return bins; }
|
|
405
|
+
/* A key's budget as its card shows it: the dollar limit, the token limit, what is used of each, and the tighter of the two as the meter. /admin/keys carries the limits as budget.usd_per_day and budget.tokens_per_day; /analytics carries them with today's use as usdPerDay, tokensPerDay, usedUsd and usedTokens. A key has no budget only when it has neither limit. */
|
|
406
|
+
function budgetView(key, b){ var kb=(key&&key.budget)||{}; b=b||{}; var usd=kb.usd_per_day!=null?kb.usd_per_day:b.usdPerDay; var tok=kb.tokens_per_day!=null?kb.tokens_per_day:b.tokensPerDay; var usedUsd=b.usedUsd||0, usedTok=b.usedTokens||0; var fu=usd>0?usedUsd/usd:null, ft=tok>0?usedTok/tok:null; var util=fu==null?ft:ft==null?fu:Math.max(fu,ft); return { usdLimit:usd>0?usd:null, tokenLimit:tok>0?tok:null, usedUsd:usedUsd, usedTokens:usedTok, usdFraction:fu, tokenFraction:ft, utilisation:util, binding:util==null?null:(ft!=null&&(fu==null||ft>fu)?'tokens':'usd') }; }
|
|
407
|
+
/* /health as the page holds it. The body carries the verdict whatever the HTTP code: 200 with status "ok", 503 with status "degraded" (OAuth broken, or expired past refresh, or a failed serving probe). A transport failure, or an answer without a verdict (a reverse proxy's own error page), keeps the last body and marks it unreachable, so an outage never leaves an old "healthy" on screen. */
|
|
408
|
+
function healthFrom(res, prev, nowMs){ var b=res&&res.body; if(b&&typeof b==='object'&&typeof b.status==='string') return { body:b, http:res.status, seenAt:nowMs, unreachable:false }; return { body:(prev&&prev.body)||null, http:res?res.status:0, seenAt:prev?prev.seenAt:null, unreachable:true }; }
|
|
409
|
+
/* The status pill from that: a halt wins; nothing yet is "connecting"; unreachable says so and names the last verdict; otherwise the body's verdict, with the OAuth state when it is not healthy. */
|
|
410
|
+
function headerStatus(v, halted){ if(halted) return { cls:'bad', text:'halted' }; if(!v||!v.body) return v&&v.unreachable?{ cls:'bad', text:'unreachable' }:{ cls:'', text:'connecting' }; var h=v.body; var st=(h.oauth==='broken'||h.status!=='ok')?'degraded':'healthy'; if(v.unreachable) return { cls:'bad', text:'unreachable · last '+st }; return { cls:st==='degraded'?'warn':'good', text:st+(h.oauth&&h.oauth!=='healthy'?' · oauth '+h.oauth:'') }; }
|
|
411
|
+
/* One SSE block ("event:" and "data:" lines) as {event, data}, or null when there is no JSON in it. */
|
|
412
|
+
function parseSse(chunk){ var ev='message', data=[]; chunk.split('\n').forEach(function(line){ if(line.indexOf('event:')===0) ev=line.slice(6).trim(); else if(line.indexOf('data:')===0) data.push(line.slice(5).trim()); }); if(!data.length) return null; var obj; try{ obj=JSON.parse(data.join('\n')); }catch(e){ return null; } return { event:ev, data:obj }; }
|
|
413
|
+
/* What the stream loop does after an attempt ends. A cancelled attempt stops; a refused credential stops and asks for the token (retrying would only repeat the 401); everything else, a 502 from a reverse proxy mid-restart, a dropped connection, a stream the server closed, retries with a backoff of 2 s doubling to 30 s. The attempt count resets when a connection opens. */
|
|
414
|
+
function streamNext(outcome, attempt){ if(!outcome||outcome.aborted) return { action:'stop' }; if(outcome.status===401||outcome.status===403) return { action:'auth' }; return { action:'retry', delayMs:Math.min(30000, 2000*Math.pow(2, Math.min(attempt||0, 4))) }; }
|
|
415
|
+
/* The stream loop, with the transport passed in so a test can hand it a failed response and then a good one. o.fetchFn(path, init) resolves a Response-like {ok, status, body} whose body.getReader().read() yields {done, value}; o.decoder() returns a function from a chunk to text; o.abort() returns an AbortController-like; o.schedule(fn, ms) is setTimeout; o.onState(ok, note) feeds the header; o.onOpen() marks a (re)connection, after which the server replays its backlog; o.onChunk({event, data}) gets each block; o.onAuth() when the credential is refused. stop() ends it. */
|
|
416
|
+
function sseLoop(o){
|
|
417
|
+
var attempt=0, stopped=false, ctrl=null;
|
|
418
|
+
function again(outcome){ if(stopped) return; var n=streamNext(outcome, attempt++); if(n.action==='retry'){ o.onState(false,'stream reconnecting'); o.schedule(connect, n.delayMs); } else if(n.action==='auth'){ o.onState(false,'stream needs the token'); if(o.onAuth) o.onAuth(); } }
|
|
419
|
+
function connect(){ if(stopped) return; ctrl=o.abort(); var dec=o.decoder(), buf='';
|
|
420
|
+
o.fetchFn(o.path,{ headers:o.headers(), cache:'no-store', signal:ctrl.signal }).then(function(r){
|
|
421
|
+
if(!r.ok||!r.body) return again({ status:r.status });
|
|
422
|
+
attempt=0; o.onState(true,'live · stream'); if(o.onOpen) o.onOpen(); var reader=r.body.getReader();
|
|
423
|
+
function pump(){ return reader.read().then(function(res){ if(res.done) return again({ closed:true }); buf+=dec(res.value); var i; while((i=buf.indexOf('\n\n'))>=0){ var chunk=buf.slice(0,i); buf=buf.slice(i+2); if(chunk.trim()&&chunk.charAt(0)!==':'){ var m=parseSse(chunk); if(m) o.onChunk(m); } } return pump(); }); }
|
|
424
|
+
return pump();
|
|
425
|
+
}).catch(function(){ again(ctrl.signal&&ctrl.signal.aborted?{ aborted:true }:{ error:true }); });
|
|
426
|
+
}
|
|
427
|
+
connect();
|
|
428
|
+
return { stop:function(){ stopped=true; if(ctrl) ctrl.abort(); } };
|
|
429
|
+
}
|
|
430
|
+
/* The queue tile. /analytics carries the queue snapshot for every viewer; /health carries it only for a trusted caller (a viewer with the analytics token gets the liveness verdict alone), so the window's copy comes first and the health one is a fallback for an older proxy, and no snapshot at all is "unavailable", never zero. */
|
|
431
|
+
function queueView(analytics, health){ var q=(analytics&&analytics.queue)||(health&&health.queue)||null; if(!q) return { available:false, value:'n/a', note:'no queue snapshot in this window', tone:null }; var active=q.active||0, queued=q.queued||0; return { available:true, active:active, queued:queued, value:active+' / '+queued, note:'cap '+q.maxConcurrent+' · '+q.consumersActive+' consumers active', tone:queued>0?'warn':null }; }
|
|
432
|
+
/* The exhaustion tile from /analytics predictions. The proxy's estimate is a trend, not a forecast: it takes the first and the last request of the window, needs three requests over at least a minute with the 5h utilisation rising between them, and projects the seat of the last request to 100% at that rate; 0 means that seat is at 95% already. It is not per seat and it knows nothing about resets, so the tile says that and nothing more; null is "no estimate" with the reason, never an assurance. */
|
|
433
|
+
function exhaustionView(p, windowRequests){ var ex=p&&p.estimatedExhaustionMinutes; var n=windowRequests||0;
|
|
434
|
+
if(ex==null) return { text:'no estimate', minutes:null, note:n<3?'fewer than three requests in the window':'5h utilisation not rising across the window', tone:null };
|
|
435
|
+
if(ex<=0) return { text:'now', minutes:0, note:'the seat of the last request is at 95% of its 5h window', tone:'warn' };
|
|
436
|
+
return { text:null, minutes:ex, note:'until the seat of the last request reaches its 5h limit at the window trend; not per seat, not reset-aware', tone:ex<60?'warn':null }; }
|
|
437
|
+
/* The hour's bins for the sparkline and the "vs prior 30 min" figure. The proxy sends perFiveMinutes on the summary's hour, twelve bins oldest first, counted from every request it holds for the hour; the stream replays only its last records, so bins counted from the page's own records are partial: they say so and carry no comparison. */
|
|
438
|
+
function hourBins(w, records, nowMs){ var b=w&&w.perFiveMinutes; if(Array.isArray(b)&&b.length===12) return { bins:b.map(function(v){ return +v||0; }), complete:true, count:b.reduce(function(x,y){ return x+(+y||0); },0) }; var r=records||[]; var oldest=Infinity; r.forEach(function(x){ var t=tsOf(x); if(t<oldest) oldest=t; }); return { bins:perFiveMinutes(r, nowMs), complete:false, observedSinceMs:isFinite(oldest)?oldest:null, count:r.length }; }
|
|
439
|
+
/* The last 30 minutes against the 30 before, only when the whole hour was counted. */
|
|
440
|
+
function hourDelta(hb){ if(!hb||!hb.complete) return null; var prev=0, cur=0; hb.bins.forEach(function(v,i){ if(i<6) prev+=v; else cur+=v; }); return { prev:prev, cur:cur, delta:cur-prev }; }
|
|
441
|
+
/* A record's identity, for the replay the proxy sends on every connection: RequestRecord has no id, so the millisecond with the consumer, seat, model, tokens, latency and status is one. */
|
|
442
|
+
function recordKey(x){ return [tsOf(x), x.consumer||'', x.account||'', x.model||'', x.inputTokens||0, x.outputTokens||0, x.latencyMs||0, x.status||0].join('|'); }
|
|
443
|
+
/* The page's records after new ones arrive: duplicates skipped, oldest first, kept to the window with a floor for the feed. */
|
|
444
|
+
function mergeRecords(records, incoming, nowMs, keepMs, minKeep){ keepMs=keepMs||3600000; minKeep=minKeep||200; var seen={}, out=[], added=[]; (records||[]).forEach(function(x){ var k=recordKey(x); if(!seen[k]){ seen[k]=1; out.push(x); } }); (incoming||[]).forEach(function(x){ var k=recordKey(x); if(seen[k]) return; seen[k]=1; out.push(x); added.push(x); }); var cut=nowMs-keepMs, drop=0; while(out.length-drop>minKeep&&tsOf(out[drop])<cut) drop++; if(drop) out=out.slice(drop); return { records:out, added:added }; }
|
|
445
|
+
/* The halt state from a stream event, or from the overage field of /analytics ("snapshot"). The stream replays a halt only while one is in force and a viewer with the read-only token cannot read /admin/resume, so the snapshot on every poll is what clears a halt that was lifted while the viewer was disconnected. Anything else leaves the state alone (null). */
|
|
446
|
+
function haltFrom(ev, data){ if(ev==='overage_halt') return { halted:true, reason:(data&&data.reason)||'' }; if(ev==='overage_resume') return { halted:false, reason:'' }; if(ev==='snapshot'){ if(!data||typeof data!=='object') return null; return { halted:!!data.halted, reason:(data.state&&data.state.reason)||'' }; } return null; }
|
|
447
|
+
/* The token burn tile, by what the proxy sent: predictions.tokenBurnRate is input and output only (cache reads and writes are not in it), and a proxy that also sends totalTokenBurnRate counts every kind. The tile shows the total when it is there and says what it shows either way. */
|
|
448
|
+
function tokenBurnView(p){ var io=(p&&typeof p.tokenBurnRate==='number')?p.tokenBurnRate:0; if(p&&typeof p.totalTokenBurnRate==='number') return { rate:p.totalTokenBurnRate, ioRate:io, scope:'all', label:'all token kinds, cache reads and writes included' }; return { rate:io, ioRate:io, scope:'io', label:'input and output only, cache excluded' }; }
|
|
449
|
+
return { dayKey:dayKey, calendarDays:calendarDays, ledgerSlice:ledgerSlice, modeFor:modeFor, tsOf:tsOf, perFiveMinutes:perFiveMinutes, budgetView:budgetView, healthFrom:healthFrom, headerStatus:headerStatus, parseSse:parseSse, streamNext:streamNext, sseLoop:sseLoop, queueView:queueView, exhaustionView:exhaustionView, hourBins:hourBins, hourDelta:hourDelta, recordKey:recordKey, mergeRecords:mergeRecords, haltFrom:haltFrom, tokenBurnView:tokenBurnView };
|
|
450
|
+
})();
|
|
451
|
+
</script>
|
|
452
|
+
<script>
|
|
453
|
+
(function(){
|
|
454
|
+
'use strict';
|
|
455
|
+
var $ = function(id){ return document.getElementById(id); };
|
|
456
|
+
var state = { mode:'detecting', token:'', health:null, analytics:null, accounts:null, codex:null, ledger:null, keys:null, keysStatus:'', records:[], paused:false, range:'today', stream:null, streamOk:false, streamNote:'', healthView:null, authNeeded:false, pollers:[], demoTimer:null, accountsStatus:'' };
|
|
457
|
+
|
|
458
|
+
/* ---------- formatting ---------- */
|
|
459
|
+
function usd(x){ if(x==null||isNaN(x)) return '$0'; var a=Math.abs(x); if(a>=1e6) return '$'+(x/1e6).toFixed(2)+'M'; if(a>=1e4) return '$'+(x/1e3).toFixed(1)+'K'; if(a>=100) return '$'+x.toFixed(0); if(a>=10) return '$'+x.toFixed(1); return '$'+x.toFixed(2); }
|
|
460
|
+
function usdFull(x){ return '$'+(x||0).toLocaleString('en-US',{minimumFractionDigits:2,maximumFractionDigits:2}); }
|
|
461
|
+
function money(x){ return Math.abs(x||0)<10000?usdFull(x):usd(x); }
|
|
462
|
+
function compact(n){ if(n==null||isNaN(n)) return '0'; var a=Math.abs(n); if(a>=1e9) return (n/1e9).toFixed(2)+'B'; if(a>=1e6) return (n/1e6).toFixed(a>=1e7?1:2)+'M'; if(a>=1e4) return (n/1e3).toFixed(1)+'K'; if(a>=1e3) return (n/1e3).toFixed(2)+'K'; return String(Math.round(n)); }
|
|
463
|
+
function int(n){ return (n||0).toLocaleString('en-US'); }
|
|
464
|
+
function pct(x,d){ return (x*100).toFixed(d==null?0:d)+'%'; }
|
|
465
|
+
function ms(x){ if(x==null) return '0 ms'; if(x>=1000) return (x/1000).toFixed(x>=10000?0:1)+' s'; return Math.round(x)+' ms'; }
|
|
466
|
+
function dur(msv){ if(msv==null||!isFinite(msv)) return ''; var s=Math.max(0,Math.round(msv/1000)); var d=Math.floor(s/86400), h=Math.floor(s%86400/3600), m=Math.floor(s%3600/60); if(d) return d+'d '+h+'h'; if(h) return h+'h '+(m<10?'0':'')+m+'m'; return m+'m'; }
|
|
467
|
+
function hms(ts){ var d=new Date(ts); return [d.getHours(),d.getMinutes(),d.getSeconds()].map(function(v){return (v<10?'0':'')+v;}).join(':'); }
|
|
468
|
+
function dayKey(d){ return d.toISOString().slice(0,10); }
|
|
469
|
+
function shortDay(key){ var d=new Date(key+'T00:00:00Z'); return d.toLocaleDateString('en-US',{month:'short',day:'numeric',timeZone:'UTC'}); }
|
|
470
|
+
function el(tag, cls, text){ var e=document.createElement(tag); if(cls) e.className=cls; if(text!=null) e.textContent=text; return e; }
|
|
471
|
+
function clear(node){ while(node.firstChild) node.removeChild(node.firstChild); return node; }
|
|
472
|
+
function sevClass(u){ return u>=0.9?'bad':u>=0.7?'warn':'ok'; }
|
|
473
|
+
var MODEL_SHORT = function(m){ return String(m).replace(/^claude-/,'').replace(/-\d{8}$/,''); };
|
|
474
|
+
|
|
475
|
+
/* ---------- sample rates ----------
|
|
476
|
+
The sample's own rate card (the published per-million prices on 2026-10-25), used only to make the
|
|
477
|
+
synthetic ledger and window look right. A served page never prices anything: the proxy sends every
|
|
478
|
+
ledger row priced (`priced` on /analytics/ledger) and every window figure costed (/analytics). */
|
|
479
|
+
var SAMPLE_RATES = {
|
|
480
|
+
'claude-opus-5-5':{i:4,o:20,cr:0.2,cw:5}, 'claude-sonnet-5':{i:2,o:10,cr:0.2,cw:2.5}, 'claude-haiku-4-5':{i:1,o:5,cr:0.1,cw:1.25},
|
|
481
|
+
'gpt-5.6-terra':{i:2,o:12,cr:0.2,cw:2.5}, 'gpt-5.6-luna':{i:0.2,o:1.2,cr:0.02,cw:0.25}
|
|
482
|
+
};
|
|
483
|
+
function sampleCost(model, c){ var p=SAMPLE_RATES[model]||SAMPLE_RATES['claude-sonnet-5']; return ((c.inputTokens||0)*p.i+(c.outputTokens||0)*p.o+(c.cacheReadTokens||0)*p.cr+(c.cacheCreateTokens||0)*p.cw)/1e6; }
|
|
484
|
+
/* the ledger maths live in the cockpit-maths block above; ranges are calendar days ending now */
|
|
485
|
+
var ledgerSlice=function(L, range){ return DarioMaths.ledgerSlice(L, range, Date.now()); };
|
|
486
|
+
|
|
487
|
+
/* ---------- theme ---------- */
|
|
488
|
+
(function theme(){
|
|
489
|
+
var btn=$('themeBtn'); var order=['system','dark','light']; var cur='system';
|
|
490
|
+
try{ cur=localStorage.getItem('dario.cockpit.theme')||'system'; }catch(e){}
|
|
491
|
+
function apply(){ var root=document.documentElement; if(cur==='system') root.removeAttribute('data-theme'); else root.setAttribute('data-theme',cur); var label='Theme: '+(cur==='system'?'follows the system':cur); btn.setAttribute('aria-label',label); btn.title=label; try{ localStorage.setItem('dario.cockpit.theme',cur); }catch(e){} }
|
|
492
|
+
btn.addEventListener('click',function(){ cur=order[(order.indexOf(cur)+1)%order.length]; apply(); renderAll(); });
|
|
493
|
+
if(cur!=='system') apply();
|
|
494
|
+
})();
|
|
495
|
+
|
|
496
|
+
/* ---------- token popover ---------- */
|
|
497
|
+
(function tokenUi(){
|
|
498
|
+
var pop=$('connectPop'), btn=$('connectBtn'), input=$('tokenInput'), note=$('tokenNote');
|
|
499
|
+
try{ state.token=sessionStorage.getItem('dario.analytics.token')||''; }catch(e){}
|
|
500
|
+
input.value=state.token;
|
|
501
|
+
function toggle(open){ pop.hidden=!open; btn.setAttribute('aria-expanded',String(open)); if(open) input.focus(); }
|
|
502
|
+
btn.addEventListener('click',function(){ toggle(pop.hidden); });
|
|
503
|
+
document.addEventListener('click',function(e){ if(!pop.hidden && !pop.contains(e.target) && e.target!==btn && !btn.contains(e.target)) toggle(false); });
|
|
504
|
+
document.addEventListener('keydown',function(e){ if(e.key==='Escape' && !pop.hidden) toggle(false); });
|
|
505
|
+
function save(){ state.token=input.value.trim(); try{ sessionStorage.setItem('dario.analytics.token',state.token); }catch(e){} note.textContent=''; if(state.mode==='live'){ restartLive(); note.textContent='Reconnecting with the new token.'; note.className='note ok'; } else { note.textContent='This page is showing sample data; open it from a running dario to connect.'; note.className='note'; } }
|
|
506
|
+
$('tokenSave').addEventListener('click',save);
|
|
507
|
+
input.addEventListener('keydown',function(e){ if(e.key==='Enter') save(); });
|
|
508
|
+
state.openToken=function(msg){ toggle(true); note.className='note'; note.textContent=msg||''; };
|
|
509
|
+
})();
|
|
510
|
+
|
|
511
|
+
/* ---------- tooltip ---------- */
|
|
512
|
+
var tip=$('tip');
|
|
513
|
+
function showTip(e, title, rows){ clear(tip); if(title){ tip.appendChild(el('div','t',title)); } rows.forEach(function(r){ var row=el('div','r'); var k=el('span','k'); if(r.color){ var i=document.createElement('i'); i.style.background=r.color; k.appendChild(i); } k.appendChild(document.createTextNode(r.label)); row.appendChild(k); row.appendChild(el('b',null,r.value)); tip.appendChild(row); }); tip.hidden=false; moveTip(e); }
|
|
514
|
+
function moveTip(e){ var x=e.clientX+14, y=e.clientY+14; var w=tip.offsetWidth, h=tip.offsetHeight; if(x+w>window.innerWidth-8) x=e.clientX-w-14; if(y+h>window.innerHeight-8) y=e.clientY-h-14; tip.style.left=x+'px'; tip.style.top=y+'px'; }
|
|
515
|
+
function hideTip(){ tip.hidden=true; }
|
|
516
|
+
function hover(node, fn){ node.addEventListener('pointerenter',function(e){ var d=fn(); showTip(e,d.title,d.rows); }); node.addEventListener('pointermove',moveTip); node.addEventListener('pointerleave',hideTip); node.tabIndex=0; node.addEventListener('focus',function(e){ var d=fn(); var r=node.getBoundingClientRect(); showTip({clientX:r.left+r.width/2,clientY:r.top},d.title,d.rows); }); node.addEventListener('blur',hideTip); }
|
|
517
|
+
|
|
518
|
+
/* ---------- table toggles ---------- */
|
|
519
|
+
document.querySelectorAll('.tbtn[data-table]').forEach(function(b){ b.addEventListener('click',function(){ var on=b.getAttribute('aria-pressed')!=='true'; b.setAttribute('aria-pressed',String(on)); var key=b.getAttribute('data-table'); $(key+'Chart').classList.toggle('hidden',on); $(key+'Table').classList.toggle('hidden',!on); }); });
|
|
520
|
+
function tableFrom(cols, rows){ var t=el('table','tbl twin'); var th=el('thead'); var tr=el('tr'); cols.forEach(function(c,i){ var h=el('th',i===0||c.left?'l':null,c.label||c); tr.appendChild(h); }); th.appendChild(tr); t.appendChild(th); var tb=el('tbody'); rows.forEach(function(r){ var row=el('tr'); r.forEach(function(v,i){ row.appendChild(el('td',i===0||(cols[i]&&cols[i].left)?'l':null,v)); }); tb.appendChild(row); }); t.appendChild(tb); var wrap=el('div','tscroll short'); wrap.appendChild(t); return wrap; }
|
|
521
|
+
|
|
522
|
+
/* ---------- sample data (deterministic) ---------- */
|
|
523
|
+
function rng(seed){ var a=seed>>>0; return function(){ a|=0; a=a+0x6D2B79F5|0; var t=Math.imul(a^a>>>15,1|a); t=t+Math.imul(t^t>>>7,61|t)^t; return ((t^t>>>14)>>>0)/4294967296; }; }
|
|
524
|
+
var SAMPLE = (function(){
|
|
525
|
+
var r=rng(20261025);
|
|
526
|
+
var now=Date.now();
|
|
527
|
+
var consumers={
|
|
528
|
+
review:{w:.34,models:[['claude-sonnet-5',.78],['claude-haiku-4-5',.22]]},
|
|
529
|
+
'nightly':{w:.14,models:[['claude-opus-5-5',.45],['gpt-5.6-terra',.55]]},
|
|
530
|
+
ci:{w:.22,models:[['claude-haiku-4-5',.6],['claude-sonnet-5',.4]]},
|
|
531
|
+
agents:{w:.2,models:[['claude-sonnet-5',.9],['claude-opus-5-5',.1]]},
|
|
532
|
+
cron:{w:.1,models:[['claude-haiku-4-5',1]]}
|
|
533
|
+
};
|
|
534
|
+
function pick(pairs){ var x=r(), acc=0; for(var i=0;i<pairs.length;i++){ acc+=pairs[i][1]; if(x<=acc) return pairs[i][0]; } return pairs[pairs.length-1][0]; }
|
|
535
|
+
function pickConsumer(){ var x=r(), acc=0, names=Object.keys(consumers); for(var i=0;i<names.length;i++){ acc+=consumers[names[i]].w; if(x<=acc) return names[i]; } return names[0]; }
|
|
536
|
+
function seatFor(model){ if(/gpt/.test(model)) return 'codex'; var x=r(); return x<.72?'max20':x<.93?'pro1':'pro2'; }
|
|
537
|
+
// Agent loops are many short turns and a few long ones: output tokens are skewed small.
|
|
538
|
+
function tokensFor(model){ var tbl={ 'claude-opus-5-5':[9000,42000,80,2600,.62], 'claude-sonnet-5':[6000,30000,60,2000,.64], 'claude-haiku-4-5':[2500,14000,40,1400,.55], 'gpt-5.6-terra':[7000,36000,80,2200,.5], 'gpt-5.6-luna':[3000,16000,40,1200,.5] }; var base=tbl[model]||tbl['claude-sonnet-5']; var ctx=base[0]+r()*(base[1]-base[0]); var cached=ctx*(base[4]+(r()-.5)*.2); return { inputTokens:Math.round(ctx-cached), cacheReadTokens:Math.round(cached), cacheCreateTokens:Math.round(ctx*0.06*r()), outputTokens:Math.round(base[2]+(base[3]-base[2])*Math.pow(r(),1.8)), thinkingTokens:Math.round(r()<.4?r()*1500:0) }; }
|
|
539
|
+
// milliseconds per output token, roughly the observed streaming rates
|
|
540
|
+
function perTok(m){ return /opus/.test(m)?24:/haiku/.test(m)?6:/gpt/.test(m)?14:13; }
|
|
541
|
+
// 30 days of ledger cells, newest last
|
|
542
|
+
var days=[], consumersByDay={};
|
|
543
|
+
for(var d=29; d>=0; d--){
|
|
544
|
+
var date=new Date(now-d*86400000); var key=dayKey(date); var wd=date.getUTCDay(); var load=(wd===0||wd===6)?.45:1; var reqs=Math.round((620+r()*700)*load);
|
|
545
|
+
var cons={}; days.push(key);
|
|
546
|
+
for(var i=0;i<reqs;i++){ var c=pickConsumer(); var m=pick(consumers[c].models); var t=tokensFor(m); var metered=(r()<(d%9===0?.11:.035)); var cell=(cons[c]=cons[c]||{}); var mc=(cell[m]=cell[m]||{covered:{requests:0,inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreateTokens:0},metered:{requests:0,inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreateTokens:0}}); var k=metered?mc.metered:mc.covered; k.requests++; k.inputTokens+=t.inputTokens; k.outputTokens+=t.outputTokens; k.cacheReadTokens+=t.cacheReadTokens; k.cacheCreateTokens+=t.cacheCreateTokens; }
|
|
547
|
+
consumersByDay[key]=cons;
|
|
548
|
+
}
|
|
549
|
+
// today's cells are scaled to the hour of day so the hero reads as "so far today"
|
|
550
|
+
var todayKey=days[days.length-1]; var frac=Math.min(1,Math.max(.08,(new Date().getUTCHours()*60+new Date().getUTCMinutes())/1440));
|
|
551
|
+
Object.keys(consumersByDay[todayKey]).forEach(function(c){ Object.keys(consumersByDay[todayKey][c]).forEach(function(m){ ['covered','metered'].forEach(function(k){ var cell=consumersByDay[todayKey][c][m][k]; Object.keys(cell).forEach(function(f){ cell[f]=Math.round(cell[f]*frac); }); }); }); });
|
|
552
|
+
var daysTable={}; days.forEach(function(key){ var byModel={}; Object.keys(consumersByDay[key]).forEach(function(c){ Object.keys(consumersByDay[key][c]).forEach(function(m){ var dst=(byModel[m]=byModel[m]||{covered:{requests:0,inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreateTokens:0},metered:{requests:0,inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreateTokens:0}}); ['covered','metered'].forEach(function(k){ var s=consumersByDay[key][c][m][k]; Object.keys(s).forEach(function(f){ dst[k][f]+=s[f]; }); }); }); }); daysTable[key]=byModel; });
|
|
553
|
+
// the rows priced the way the proxy prices them for /analytics/ledger
|
|
554
|
+
var priced={days:{},consumers:{}}; days.forEach(function(key){ var pm=(priced.days[key]={}); Object.keys(daysTable[key]).forEach(function(m){ pm[m]={covered:sampleCost(m,daysTable[key][m].covered),metered:sampleCost(m,daysTable[key][m].metered)}; }); var pcd=(priced.consumers[key]={}); Object.keys(consumersByDay[key]).forEach(function(c){ var row=(pcd[c]={}); Object.keys(consumersByDay[key][c]).forEach(function(m){ row[m]={covered:sampleCost(m,consumersByDay[key][c][m].covered),metered:sampleCost(m,consumersByDay[key][c][m].metered)}; }); }); });
|
|
555
|
+
var ledger={ path:'~/.dario/ledger.json', version:1, since:new Date(now-29*86400000).toISOString(), updated:new Date(now).toISOString(), days:daysTable, consumers:consumersByDay, priced:priced };
|
|
556
|
+
// the last hour of records
|
|
557
|
+
var records=[]; var n=150;
|
|
558
|
+
for(var j=0;j<n;j++){ var ts=now-3600000+Math.round((j+r()*.9)/n*3600000); var c2=pickConsumer(); var m2=pick(consumers[c2].models); var t2=tokensFor(m2); var seat=seatFor(m2); var claimX=r(); var claim=claimX<.94?'subscription':claimX<.97?'subscription_fallback':claimX<.99?'extra_usage':'api'; var sx=r(); var status=sx<.985?200:sx<.993?429:sx<.997?529:500; var ttfb=500+r()*800; var up=ttfb+t2.outputTokens*perTok(m2)*(0.8+r()*.4); var q=r()<.3?r()*400:r()*40; var pace=r()<.25?r()*220:0; var total=q+pace+up+10+r()*20;
|
|
559
|
+
records.push({ timestamp:new Date(ts).toISOString(), consumer:c2, account:seat, model:m2, inputTokens:t2.inputTokens, outputTokens:t2.outputTokens, cacheReadTokens:t2.cacheReadTokens, cacheCreateTokens:t2.cacheCreateTokens, thinkingTokens:t2.thinkingTokens, claim:claim, util5h:seat==='max20'?.62:seat==='pro1'?.31:seat==='pro2'?.08:.23, util7d:seat==='max20'?.48:seat==='pro1'?.55:seat==='pro2'?.12:.01, overageUtil:0, latencyMs:Math.round(total), status:status, isStream:true, isOpenAI:/gpt/.test(m2), timing:{queueMs:Math.round(q),pacingMs:Math.round(pace),upstreamTtfbMs:Math.round(ttfb),upstreamMs:Math.round(up),totalMs:Math.round(total)} }); }
|
|
560
|
+
var accounts={ mode:'pool', strategy:'headroom', headroomFloor:.1, healthy:3, exhausted:0, totalHeadroom:68, bestAccount:'pro2', queued:0, stickyBindings:4, distinctWindows:3, distinctAccounts:3, sharedState:false, accounts:[
|
|
561
|
+
{ alias:'max20', seatTier:'max_20x', rateLimitTier:'max_20x', util5h:.62, util7d:.48, lastObservedAt:new Date(now-9000).toISOString(), utilAgeMs:9000, resetAt:new Date(now+(2*60+14)*60000).toISOString(), resetInMs:(2*60+14)*60000, claim:'subscription', status:'ok', action:'serving', requestCount:9120, rejectedCount:3, parkedBuckets:[], accountEmail:'t***@example.com', grantAgeDays:12, daysToWall:353, grantLevel:'ok' },
|
|
562
|
+
{ alias:'pro1', seatTier:'pro', rateLimitTier:'pro', util5h:.31, util7d:.55, lastObservedAt:new Date(now-41000).toISOString(), utilAgeMs:41000, resetAt:new Date(now+(4*60+2)*60000).toISOString(), resetInMs:(4*60+2)*60000, claim:'subscription', status:'ok', action:'serving', requestCount:2210, rejectedCount:0, parkedBuckets:[], accountEmail:'p***@example.com', grantAgeDays:3, daysToWall:362, grantLevel:'ok' },
|
|
563
|
+
{ alias:'pro2', seatTier:'pro', rateLimitTier:'pro', util5h:.08, util7d:.12, lastObservedAt:new Date(now-120000).toISOString(), utilAgeMs:120000, resetAt:new Date(now+(4*60+51)*60000).toISOString(), resetInMs:(4*60+51)*60000, claim:'subscription', status:'ok', action:'serving', requestCount:188, rejectedCount:0, parkedBuckets:[], accountEmail:'q***@example.com', grantAgeDays:0, daysToWall:365, grantLevel:'ok' }
|
|
564
|
+
]};
|
|
565
|
+
var codex={ backend:'chatgpt', requests:1340, accounts:[{ alias:'codex', expiresAt:new Date(now+9*86400000).toISOString(), expiresInMs:9*86400000, needsRefresh:false, models:['gpt-5.6-terra','gpt-5.6-luna'], requestCount:1340, status:'ok', usage:{ headroom:.77, limitReached:false, source:'headers', observedAt:new Date(now-30000).toISOString(), windows:[{slot:'primary',usedPercent:23,windowMinutes:300,resetAt:new Date(now+(3*60+12)*60000).toISOString()},{slot:'secondary',usedPercent:1,windowMinutes:10080,resetAt:new Date(now+6.4*86400000).toISOString()}] } }] };
|
|
566
|
+
var health={ status:'ok', version:'6.16.0', oauth:'healthy', expiresIn:'9d', requests:12858, sessions:{mode:'sticky',stickyBindings:4}, queue:{active:2,queued:0,maxConcurrent:8,maxQueued:64,stalledSince:null,maxWaitMs:30000,maxConcurrentPerConsumer:4,consumersActive:3}, uptimeMs:3*86400000+4*3600000+17*60000, bind:'127.0.0.1:8787' };
|
|
567
|
+
// review and nightly have a dollar budget, ci a token budget, agents both (the tokens bind), cron none
|
|
568
|
+
var budgets={ review:{usdPerDay:60,usedUsd:4.74,usedTokens:2140000}, 'nightly':{usdPerDay:80,usedUsd:0.96,usedTokens:390000}, ci:{tokensPerDay:20000000,usedUsd:18.2,usedTokens:8900000}, agents:{usdPerDay:120,tokensPerDay:24000000,usedUsd:61.4,usedTokens:16400000}, cron:{usedUsd:2.1,usedTokens:2100000} };
|
|
569
|
+
var keys=[ {name:'review',seat:'any',models:['claude-sonnet-5','claude-haiku-4-5'],created:'2026-09-02',last_used:new Date(now-120000).toISOString(),status:'active',budget:{usd_per_day:60}}, {name:'nightly',seat:'codex',models:['gpt-5.6-terra','claude-opus-5-5'],created:'2026-10-20',last_used:new Date(now-900000).toISOString(),status:'active',budget:{usd_per_day:80}}, {name:'ci',seat:'any',models:['claude-haiku-4-5','claude-sonnet-5'],created:'2026-08-14',last_used:new Date(now-15000).toISOString(),status:'active',budget:{tokens_per_day:20000000}}, {name:'agents',seat:'max20',models:['claude-sonnet-5','claude-opus-5-5'],created:'2026-07-01',last_used:new Date(now-60000).toISOString(),status:'active',budget:{usd_per_day:120,tokens_per_day:24000000}}, {name:'cron',seat:'any',models:['claude-haiku-4-5'],created:'2026-10-11',last_used:new Date(now-3600000*5).toISOString(),status:'active',budget:null} ];
|
|
570
|
+
return { records:records, ledger:ledger, accounts:accounts, codex:codex, health:health, budgets:budgets, keys:keys, consumers:consumers, pick:pick, pickConsumer:pickConsumer, seatFor:seatFor, tokensFor:tokensFor, perTok:perTok, r:r };
|
|
571
|
+
})();
|
|
572
|
+
|
|
573
|
+
/* ---------- a window summary from records (the same shape /analytics returns) ---------- */
|
|
574
|
+
function summarize(records, budgets){
|
|
575
|
+
var now=Date.now(); var cut=now-3600000; var w=records.filter(function(x){ return DarioMaths.tsOf(x)>=cut; });
|
|
576
|
+
var s={ window:{minutes:60,requests:w.length,totalInputTokens:0,totalOutputTokens:0,totalThinkingTokens:0,totalCacheReadTokens:0,totalCacheCreateTokens:0,cachedPromptPercent:0,estimatedCost:0,avgLatencyMs:0,timing:{samples:0,avgQueueMs:0,avgPacingMs:0,avgUpstreamTtfbMs:0,avgUpstreamMs:0,avgOverheadMs:0},errorRate:0,claimBreakdown:{},billingBucketBreakdown:{subscription:0,subscription_fallback:0,extra_usage:0,api:0,credits:0,unknown:0},subscriptionPercent:0}, perAccount:{}, perConsumer:{}, perModel:{}, predictions:{}, budgets:budgets||{} };
|
|
577
|
+
var lat=0, err=0, tq=0,tp=0,tt=0,tu=0,to=0,ts=0;
|
|
578
|
+
w.forEach(function(x){ var ww=s.window; ww.totalInputTokens+=x.inputTokens; ww.totalOutputTokens+=x.outputTokens; ww.totalThinkingTokens+=x.thinkingTokens||0; ww.totalCacheReadTokens+=x.cacheReadTokens; ww.totalCacheCreateTokens+=x.cacheCreateTokens||0; ww.estimatedCost+=sampleCost(x.model,x); lat+=x.latencyMs; if(x.status>=400) err++; var b=ww.billingBucketBreakdown; var claim=x.claim||'unknown'; b[claim in b?claim:'unknown']++; ww.claimBreakdown[claim]=(ww.claimBreakdown[claim]||0)+1; if(x.timing){ ts++; tq+=x.timing.queueMs||0; tp+=x.timing.pacingMs||0; tt+=x.timing.upstreamTtfbMs||0; tu+=x.timing.upstreamMs||0; to+=Math.max(0,(x.timing.totalMs||0)-(x.timing.queueMs||0)-(x.timing.pacingMs||0)-(x.timing.upstreamMs||0)); }
|
|
579
|
+
var pa=(s.perAccount[x.account]=s.perAccount[x.account]||{requests:0,estimatedCost:0,currentUtil5h:0,currentUtil7d:0,lastClaim:''}); pa.requests++; pa.estimatedCost+=sampleCost(x.model,x); pa.currentUtil5h=x.util5h; pa.currentUtil7d=x.util7d; pa.lastClaim=claim;
|
|
580
|
+
var pc=(s.perConsumer[x.consumer||'(none)']=s.perConsumer[x.consumer||'(none)']||{requests:0,estimatedCost:0,inputTokens:0,cacheReadTokens:0,accounts:[],lastModel:''}); pc.requests++; pc.estimatedCost+=sampleCost(x.model,x); pc.inputTokens+=x.inputTokens; pc.cacheReadTokens+=x.cacheReadTokens; if(pc.accounts.indexOf(x.account)<0) pc.accounts.push(x.account); pc.lastModel=x.model;
|
|
581
|
+
var pm=(s.perModel[x.model]=s.perModel[x.model]||{requests:0,estimatedCost:0}); pm.requests++; pm.estimatedCost+=sampleCost(x.model,x); });
|
|
582
|
+
var n=w.length||1; s.window.avgLatencyMs=lat/n; s.window.errorRate=err/n; s.window.cachedPromptPercent=100*s.window.totalCacheReadTokens/Math.max(1,s.window.totalCacheReadTokens+s.window.totalInputTokens);
|
|
583
|
+
s.window.timing={samples:ts,avgQueueMs:tq/(ts||1),avgPacingMs:tp/(ts||1),avgUpstreamTtfbMs:tt/(ts||1),avgUpstreamMs:tu/(ts||1),avgOverheadMs:to/(ts||1)};
|
|
584
|
+
var b2=s.window.billingBucketBreakdown; s.window.subscriptionPercent=100*(b2.subscription+b2.subscription_fallback)/n;
|
|
585
|
+
s.window.perFiveMinutes=DarioMaths.perFiveMinutes(w, now); s.overage={ halted:false, state:null };
|
|
586
|
+
Object.keys(s.perConsumer).forEach(function(c){ var pc=s.perConsumer[c]; pc.cachedPromptPercent=100*pc.cacheReadTokens/Math.max(1,pc.cacheReadTokens+pc.inputTokens); });
|
|
587
|
+
var io=s.window.totalInputTokens+s.window.totalOutputTokens; var all=io+s.window.totalCacheReadTokens+s.window.totalCacheCreateTokens; s.predictions={ tokenBurnRate:io/60, totalTokenBurnRate:all/60, costBurnRate:s.window.estimatedCost, estimatedExhaustionMinutes:null };
|
|
588
|
+
return s;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/* ---------- renderers ---------- */
|
|
592
|
+
function setPill(node, cls, text){ node.className='pill '+cls; node.querySelector('span').textContent=text; }
|
|
593
|
+
|
|
594
|
+
function renderHeader(){
|
|
595
|
+
var h=state.health; var a=state.analytics;
|
|
596
|
+
$('verChip').textContent='v'+((h&&h.version)||'?');
|
|
597
|
+
// the verdict comes from the body whatever the status code; a proxy that stopped answering says so, with how long ago it was last heard
|
|
598
|
+
var hv=state.healthView; var hs=DarioMaths.headerStatus(hv, state.halted); setPill($('statusPill'), hs.cls, hs.text+(hv&&hv.unreachable&&hv.seenAt?' '+dur(Date.now()-hv.seenAt)+' ago':''));
|
|
599
|
+
var m=state.mode; var pill=$('modePill');
|
|
600
|
+
if(m==='sample') setPill(pill,'info','sample data'); else if(m==='live') setPill(pill, state.streamOk?'good live':'warn', state.streamOk?'live · stream':(state.streamNote||'live · polling')); else setPill(pill,'','detecting');
|
|
601
|
+
$('bindMeta').textContent=h?[(h.bind||(state.mode==='live'?location.host:'')),h.uptimeMs!=null?'up '+dur(h.uptimeMs):''].filter(Boolean).join(' · '):'';
|
|
602
|
+
$('footLeft').textContent='dario '+((h&&h.version)||'')+(state.mode==='sample'?' · sample data, deterministic':' · '+location.host);
|
|
603
|
+
$('footRight').textContent='rendered '+new Date().toISOString().slice(0,19).replace('T',' ')+' UTC';
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function renderHero(){
|
|
607
|
+
var L=state.ledger; if(!L){ $('heroVal').textContent='$0.00'; return; }
|
|
608
|
+
var today=ledgerSlice(L,'today'); var all=ledgerSlice(L,'all'); var t=today.total; var apiEq=t.covered+t.metered;
|
|
609
|
+
$('heroVal').textContent=usdFull(t.covered); $('heroPct').textContent=apiEq>0?Math.round(100*t.covered/apiEq)+'% of '+money(apiEq):'';
|
|
610
|
+
$('heroMetered').textContent=usdFull(t.metered); $('heroReq').textContent=int(t.requests); $('heroLife').textContent=money(all.total.covered); $('heroSince').textContent=(L.since||'').slice(0,10);
|
|
611
|
+
$('heroLede').textContent=apiEq>0?'What today’s '+int(t.requests)+' requests would have cost at API list prices, and the share your seats absorbed. The rest was metered: extra usage or an API key.':'No requests in the ledger yet today.';
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function sparkline(points){
|
|
615
|
+
var W=120,H=36,p=3; var max=Math.max.apply(null,points.concat([1])); var n=points.length; var step=(W-2*p)/Math.max(1,n-1);
|
|
616
|
+
var pts=points.map(function(v,i){ return [p+i*step, H-p-(v/max)*(H-2*p)]; });
|
|
617
|
+
var d=pts.map(function(q,i){ return (i?'L':'M')+q[0].toFixed(1)+' '+q[1].toFixed(1); }).join(' ');
|
|
618
|
+
var area=d+' L'+pts[pts.length-1][0].toFixed(1)+' '+(H-p)+' L'+pts[0][0].toFixed(1)+' '+(H-p)+' Z';
|
|
619
|
+
var last=pts[pts.length-1];
|
|
620
|
+
var svg=document.createElementNS('http://www.w3.org/2000/svg','svg'); svg.setAttribute('class','spark'); svg.setAttribute('viewBox','0 0 '+W+' '+H); svg.setAttribute('aria-hidden','true');
|
|
621
|
+
var a=document.createElementNS('http://www.w3.org/2000/svg','path'); a.setAttribute('class','area'); a.setAttribute('d',area); svg.appendChild(a);
|
|
622
|
+
var l=document.createElementNS('http://www.w3.org/2000/svg','path'); l.setAttribute('class','line'); l.setAttribute('d',d); svg.appendChild(l);
|
|
623
|
+
var c=document.createElementNS('http://www.w3.org/2000/svg','circle'); c.setAttribute('class','end'); c.setAttribute('cx',last[0]); c.setAttribute('cy',last[1]); c.setAttribute('r','4'); svg.appendChild(c);
|
|
624
|
+
return svg;
|
|
625
|
+
}
|
|
626
|
+
var perFiveMinutes=function(records){ return DarioMaths.perFiveMinutes(records, Date.now()); };
|
|
627
|
+
function tile(label, value, unit, foot, sev){ var t=el('div','tile'); t.appendChild(el('div','lbl',label)); var v=el('div','val'); v.appendChild(document.createTextNode(value)); if(unit){ v.appendChild(el('small',null,unit)); } t.appendChild(v); if(foot){ var f=el('div','foot'); if(typeof foot==='string') f.appendChild(el('span',null,foot)); else f.appendChild(foot); t.appendChild(f); } if(sev){ t.style.borderColor='color-mix(in oklab, var(--'+sev+') 45%, var(--line))'; } return t; }
|
|
628
|
+
function meter(u, cls){ var m=el('div','meter '+(cls||sevClass(u))); var i=document.createElement('i'); i.style.width=Math.min(100,Math.max(0,u*100)).toFixed(1)+'%'; m.appendChild(i); return m; }
|
|
629
|
+
|
|
630
|
+
function renderKpis(){
|
|
631
|
+
var a=state.analytics; var k=clear($('kpis')); if(!a) return; var w=a.window; var h=state.health; var acc=state.accounts;
|
|
632
|
+
// the hour from the proxy's bins; an older proxy without them gets the page's own records, labelled as the partial view they are and without a comparison
|
|
633
|
+
var hb=DarioMaths.hourBins(w, state.records, Date.now()); var d=DarioMaths.hourDelta(hb);
|
|
634
|
+
var f1=el('div','foot'); if(d){ var delta=el('span','delta '+(d.delta>0?'up':d.delta<0?'down':'flat')); delta.textContent=(d.delta>=0?'+':'')+d.delta+' vs prior 30 min'; f1.appendChild(delta); } else { f1.appendChild(el('span','delta flat',hb.count+' records seen, not the hour')); } f1.appendChild(sparkline(hb.bins));
|
|
635
|
+
k.appendChild(tile('Requests · last 60 min', int(w.requests), null, f1));
|
|
636
|
+
var headroom=acc?acc.totalHeadroom/100:null; var seatsIn=acc?acc.accounts.length:0;
|
|
637
|
+
var f2=el('div','foot'); if(headroom!=null){ var mm=meter(1-headroom); mm.style.flex='1'; f2.appendChild(mm); f2.appendChild(el('span',null,seatsIn+' seats · best '+(acc.bestAccount||''))); }
|
|
638
|
+
k.appendChild(tile('Pool headroom', headroom!=null?pct(headroom):'n/a', null, headroom!=null?f2:(state.accountsStatus||'needs an API key'), headroom!=null&&headroom<.1?'bad':headroom!=null&&headroom<.3?'warn':null));
|
|
639
|
+
k.appendChild(tile('Error rate', pct(w.errorRate,1), null, w.errorRate?Math.round(w.errorRate*w.requests)+' non-2xx in the window':'every response 2xx', w.errorRate>.05?'bad':w.errorRate>.01?'warn':null));
|
|
640
|
+
k.appendChild(tile('Avg latency', ms(w.avgLatencyMs), null, 'TTFB '+ms(w.timing.avgUpstreamTtfbMs)+' · queue '+ms(w.timing.avgQueueMs)));
|
|
641
|
+
k.appendChild(tile('Cached prompt', Math.round(w.cachedPromptPercent)+'%', null, compact(w.totalCacheReadTokens)+' cache reads of '+compact(w.totalCacheReadTokens+w.totalInputTokens)+' prompt tokens'));
|
|
642
|
+
var qv=DarioMaths.queueView(a, h); k.appendChild(tile('In flight / queued', qv.value, null, qv.note, qv.tone));
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function renderSeats(){
|
|
646
|
+
var box=clear($('seats')); var acc=state.accounts; var cx=state.codex;
|
|
647
|
+
if(!acc&&!cx){ var hint=el('div','hint'); hint.textContent=state.accountsStatus||'Seats need an API key: the analytics token reads spend and the feed only.'; box.appendChild(hint); $('poolChip').textContent=''; return; }
|
|
648
|
+
if(acc){ $('poolChip').textContent=acc.strategy+' · floor '+pct(acc.headroomFloor)+' · '+acc.healthy+' healthy'+(acc.exhausted?' · '+acc.exhausted+' exhausted':''); $('seatsSub').textContent=acc.mode+' of '+acc.accounts.length+(cx&&cx.accounts&&cx.accounts.length?' Claude + '+cx.accounts.length+' ChatGPT':'')+' · headroom = 1 - max(5h, 7d)'; }
|
|
649
|
+
(acc?acc.accounts:[]).forEach(function(s){
|
|
650
|
+
var head=1-Math.max(s.util5h||0,s.util7d||0); var card=el('div','seat'); var top=el('div','top'); top.appendChild(el('span','alias',s.alias));
|
|
651
|
+
var tier=el('span','chip ghost',({max_20x:'Max 20x',max_5x:'Max 5x',pro:'Pro',team:'Team',enterprise:'Enterprise'})[s.seatTier||s.rateLimitTier]||s.seatTier||s.rateLimitTier||'seat'); top.appendChild(tier);
|
|
652
|
+
var st=s.status||'ok'; var stCls=st==='ok'?'good':/cool|parked|exhaust|refresh/.test(st)?'warn':'bad'; var stChip=el('span','chip '+stCls,st+(s.action&&s.action!=='serving'?' · '+s.action:'')); top.appendChild(stChip); card.appendChild(top);
|
|
653
|
+
var hd=el('div','head'); hd.appendChild(el('b',null,pct(head))); hd.appendChild(el('span',null,'headroom\nresets in '+dur(s.resetInMs))); hd.lastChild.style.whiteSpace='pre-line'; card.appendChild(hd);
|
|
654
|
+
[['5h window',s.util5h],['7d window',s.util7d]].forEach(function(u){ var w=el('div','u'); var r=el('div','r'); r.appendChild(el('span',null,u[0])); r.appendChild(el('b',null,pct(u[1]||0)+' used')); w.appendChild(r); w.appendChild(meter(u[1]||0)); card.appendChild(w); });
|
|
655
|
+
var foot=el('div','foot'); foot.appendChild(el('span',null,int(s.requestCount)+' req'+(s.rejectedCount?' · '+s.rejectedCount+' rejected':''))); foot.appendChild(el('span',null,(s.parkedBuckets&&s.parkedBuckets.length?'parked: '+s.parkedBuckets.join(', '):'observed '+dur(s.utilAgeMs)+' ago'))); card.appendChild(foot);
|
|
656
|
+
hover(card,function(){ return {title:s.alias+' · '+(s.accountEmail||''), rows:[{label:'5h used',value:pct(s.util5h||0)},{label:'7d used',value:pct(s.util7d||0)},{label:'last claim',value:s.claim||''},{label:'grant age',value:(s.grantAgeDays!=null?s.grantAgeDays+' d':'')},{label:'refresh wall in',value:(s.daysToWall!=null?s.daysToWall+' d':'')}]}; });
|
|
657
|
+
box.appendChild(card);
|
|
658
|
+
});
|
|
659
|
+
(cx&&cx.accounts?cx.accounts:[]).forEach(function(s){
|
|
660
|
+
var u=s.usage||{}; var card=el('div','seat'); var top=el('div','top'); top.appendChild(el('span','alias',s.alias)); top.appendChild(el('span','chip ghost','ChatGPT')); var stCls=s.status==='ok'?'good':s.status==='cooling'?'warn':'bad'; top.appendChild(el('span','chip '+stCls,s.status+(u.limitReached?' · limit reached':''))); card.appendChild(top);
|
|
661
|
+
var hd=el('div','head'); hd.appendChild(el('b',null,u.headroom!=null?pct(u.headroom):'n/a')); hd.appendChild(el('span',null,'headroom\ntoken '+(s.needsRefresh?'needs refresh':'ok, '+dur(s.expiresInMs)))); hd.lastChild.style.whiteSpace='pre-line'; card.appendChild(hd);
|
|
662
|
+
(u.windows||[]).forEach(function(w){ var wrap=el('div','u'); var r=el('div','r'); var label=w.windowMinutes>=10080?'weekly window':w.windowMinutes>=1440?Math.round(w.windowMinutes/1440)+'d window':Math.round(w.windowMinutes/60)+'h window'; r.appendChild(el('span',null,label+' · resets in '+dur(Date.parse(w.resetAt)-Date.now()))); r.appendChild(el('b',null,Math.round(w.usedPercent)+'% used')); wrap.appendChild(r); wrap.appendChild(meter((w.usedPercent||0)/100)); card.appendChild(wrap); });
|
|
663
|
+
var foot=el('div','foot'); foot.appendChild(el('span',null,int(s.requestCount)+' req')); foot.appendChild(el('span',null,(s.models||[]).map(MODEL_SHORT).join(', '))); card.appendChild(foot);
|
|
664
|
+
box.appendChild(card);
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
var CLAIMS=[['subscription','subscription','--s1'],['subscription_fallback','fallback seat','--s2'],['extra_usage','extra usage','--s3'],['api','API key','--s4'],['credits','API credits','--s5'],['unknown','unknown','--other']];
|
|
669
|
+
function stackChart(container, segs, total, fmt){ var c=clear(container); var bar=el('div','stack'); var W=c.clientWidth||320; segs.forEach(function(s){ if(!s.value) return; var i=document.createElement('i'); var share=s.value/total; i.style.flex=String(share*1000); i.style.background='var('+s.color+')'; var label=s.label+' '+fmt(s.value); var estW=label.length*6.4+16; if(share*W>estW) i.textContent=label; i.setAttribute('role','img'); i.setAttribute('aria-label',s.label+': '+fmt(s.value)); hover(i,function(){ return {title:null, rows:[{label:s.label,value:fmt(s.value)+' · '+pct(share)}]}; }); bar.appendChild(i); }); c.appendChild(bar); var lg=el('div','legend'); segs.forEach(function(s){ var sp=el('span'); var sw=el('i','sw'); sw.style.background='var('+s.color+')'; sp.appendChild(sw); sp.appendChild(document.createTextNode(s.label+' ')); sp.appendChild(el('b',null,fmt(s.value))); lg.appendChild(sp); }); c.appendChild(lg); }
|
|
670
|
+
|
|
671
|
+
function renderRouting(){
|
|
672
|
+
var a=state.analytics; if(!a) return; var b=a.window.billingBucketBreakdown||{}; var total=Object.keys(b).reduce(function(x,k){return x+(b[k]||0);},0)||1;
|
|
673
|
+
$('subPct').textContent=Math.round(a.window.subscriptionPercent||0)+'%';
|
|
674
|
+
var segs=CLAIMS.map(function(c){ return {key:c[0],label:c[1],color:c[2],value:b[c[0]]||0}; });
|
|
675
|
+
stackChart($('routingChart'), segs, total, function(v){ return int(v); });
|
|
676
|
+
var tb=clear($('routingTable')); tb.appendChild(tableFrom(['claim','requests','share'], segs.map(function(s){ return [s.label,int(s.value),pct(s.value/total,1)]; })));
|
|
677
|
+
var burn=clear($('burn')); var p=a.predictions||{}; var acc=state.accounts;
|
|
678
|
+
burn.appendChild(tile('Cost burn', money(p.costBurnRate||0), '/h', 'API-equivalent'));
|
|
679
|
+
var tb=DarioMaths.tokenBurnView(p); burn.appendChild(tile('Token burn', compact(tb.rate), '/min', tb.scope==='all'?tb.label+' · '+compact(tb.ioRate)+' input + output':tb.label));
|
|
680
|
+
var ev=DarioMaths.exhaustionView(p, a.window.requests); burn.appendChild(tile('5h exhaustion trend', ev.text||dur(ev.minutes*60000), null, ev.note, ev.tone));
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
var ANATOMY=[['avgQueueMs','queue','--s1'],['avgPacingMs','pacing','--s2'],['avgUpstreamTtfbMs','upstream TTFB','--s3'],['streamMs','upstream streaming','--s4'],['avgOverheadMs','dario overhead','--s5']];
|
|
684
|
+
function renderAnatomy(){
|
|
685
|
+
var a=state.analytics; if(!a) return; var t=a.window.timing||{}; var stream=Math.max(0,(t.avgUpstreamMs||0)-(t.avgUpstreamTtfbMs||0)); var vals={avgQueueMs:t.avgQueueMs||0,avgPacingMs:t.avgPacingMs||0,avgUpstreamTtfbMs:t.avgUpstreamTtfbMs||0,streamMs:stream,avgOverheadMs:t.avgOverheadMs||0}; var total=Object.keys(vals).reduce(function(x,k){return x+vals[k];},0)||1;
|
|
686
|
+
$('anatomyTotal').textContent=ms(total);
|
|
687
|
+
var segs=ANATOMY.map(function(c){ return {label:c[1],color:c[2],value:vals[c[0]]}; });
|
|
688
|
+
stackChart($('anatomyChart'), segs, total, function(v){ return ms(v); });
|
|
689
|
+
var tb=clear($('anatomyTable')); tb.appendChild(tableFrom(['stage','mean','share'], segs.map(function(s){ return [s.label,ms(s.value),pct(s.value/total,1)]; })));
|
|
690
|
+
var body=clear($('consumersBody')); var pc=a.perConsumer||{}; Object.keys(pc).sort(function(x,y){ return pc[y].requests-pc[x].requests; }).forEach(function(c){ var r=pc[c]; var tr=el('tr'); tr.appendChild(el('td','l mono',c)); tr.appendChild(el('td',null,int(r.requests))); tr.appendChild(el('td',null,money(r.estimatedCost))); tr.appendChild(el('td',null,Math.round(r.cachedPromptPercent||0)+'%')); tr.appendChild(el('td','l',MODEL_SHORT(r.lastModel||''))); tr.appendChild(el('td','l mono',(r.accounts||[]).join(', '))); body.appendChild(tr); });
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function statusChip(code){ var cls=code<300?'good':code<500?'warn':'bad'; return el('span','chip '+cls,String(code)); }
|
|
694
|
+
function claimChip(claim){ var cls=claim==='subscription'?'accent':claim==='subscription_fallback'?'warn':claim==='extra_usage'?'bad':claim==='api'||claim==='credits'?'info':''; var label=claim==='subscription_fallback'?'fallback':claim==='extra_usage'?'extra':claim||'?'; return el('span','chip '+cls,label); }
|
|
695
|
+
function feedRow(x, isNew){ var tr=el('tr',isNew?'new':null); tr.appendChild(el('td','l mono',hms(DarioMaths.tsOf(x)))); tr.appendChild(el('td','l mono',x.consumer||'')); tr.appendChild(el('td','l',MODEL_SHORT(x.model))); tr.appendChild(el('td','l mono',x.account||'')); tr.appendChild(el('td',null,compact(x.inputTokens))); tr.appendChild(el('td',null,compact(x.outputTokens))); tr.appendChild(el('td',null,compact(x.cacheReadTokens))); tr.appendChild(el('td',null,int(x.latencyMs))); var s=el('td','l'); s.appendChild(statusChip(x.status)); tr.appendChild(s); var c=el('td','l'); c.appendChild(claimChip(x.claim)); tr.appendChild(c); return tr; }
|
|
696
|
+
function renderFeed(){ var body=clear($('feedBody')); var rows=state.records.slice(-120).reverse(); rows.forEach(function(x){ body.appendChild(feedRow(x,false)); }); $('feedCount').textContent=state.records.length+' records'; }
|
|
697
|
+
/* A record into the page: skipped when it is already there (the stream replays its last records on every connection), the rest kept to the hour with a floor for the feed. */
|
|
698
|
+
function pushRecord(x){ var m=DarioMaths.mergeRecords(state.records,[x],Date.now()); if(!m.added.length) return false; state.records=m.records; if(state.paused) { $('feedCount').textContent=state.records.length+' records · paused'; return true; } var body=$('feedBody'); body.insertBefore(feedRow(x,true), body.firstChild); while(body.children.length>120) body.removeChild(body.lastChild); $('feedCount').textContent=state.records.length+' records'; return true; }
|
|
699
|
+
$('pauseBtn').addEventListener('click',function(){ state.paused=!state.paused; this.setAttribute('aria-pressed',String(state.paused)); this.textContent=state.paused?'Resume':'Pause'; if(!state.paused) renderFeed(); });
|
|
700
|
+
|
|
701
|
+
function hbarsChart(container, entries, labelOf){
|
|
702
|
+
var c=clear(container); var rows=entries.slice(0,7); var rest=entries.slice(7); if(rest.length){ rows.push({key:'Other ('+rest.length+')',covered:rest.reduce(function(a,x){return a+x.covered;},0),metered:rest.reduce(function(a,x){return a+x.metered;},0),requests:rest.reduce(function(a,x){return a+x.requests;},0),other:true}); }
|
|
703
|
+
var max=Math.max.apply(null,rows.map(function(r){return r.covered+r.metered;}).concat([0.000001])); var g=el('div','hbars');
|
|
704
|
+
rows.forEach(function(r){ var total=r.covered+r.metered; g.appendChild(el('div','lab',labelOf(r.key))); var bar=el('div','bar'); bar.style.width=(100*total/max).toFixed(2)+'%'; [['covered','--s1',r.covered],['metered','--s2',r.metered]].forEach(function(s){ if(!s[2]) return; var i=document.createElement('i'); i.style.flex=String(s[2]/total*1000); i.style.background=r.other?'var(--other)':'var('+s[1]+')'; hover(i,function(){ return {title:labelOf(r.key), rows:[{label:s[0],value:usdFull(s[2])+' · '+pct(s[2]/total)},{label:'requests',value:int(r.requests)}]}; }); bar.appendChild(i); }); g.appendChild(bar); g.appendChild(el('div','v',money(total))); });
|
|
705
|
+
c.appendChild(g);
|
|
706
|
+
}
|
|
707
|
+
function renderLedger(){
|
|
708
|
+
var L=state.ledger; var tiles=clear($('ledgerTiles'));
|
|
709
|
+
if(!L){ $('rangeScope').textContent='ledger disabled on this proxy (start without --no-ledger)'; $('ledgerPath').textContent=''; clear($('modelsChart')); clear($('consumersLChart')); clear($('dailyChart')); return; }
|
|
710
|
+
var sl=ledgerSlice(L,state.range); var days=sl.days; $('rangeScope').textContent=days.length?(days.length===1?shortDay(days[0]):shortDay(days[0])+' to '+shortDay(days[days.length-1])+' · '+days.length+' days'):'no days yet'; $('ledgerPath').textContent=L.path||'';
|
|
711
|
+
var t=sl.total; var apiEq=t.covered+t.metered;
|
|
712
|
+
tiles.appendChild(tile('Requests', int(t.requests)));
|
|
713
|
+
tiles.appendChild(tile('API-equivalent', money(apiEq), null, 'list price of everything'));
|
|
714
|
+
tiles.appendChild(tile('Covered by seats', money(t.covered), null, apiEq?pct(t.covered/apiEq)+' of the total':''));
|
|
715
|
+
tiles.appendChild(tile('Metered', money(t.metered), null, 'extra usage and API keys', t.metered>0.1*apiEq?'warn':null));
|
|
716
|
+
tiles.appendChild(tile('Tokens', compact(t.tokens), null, 'in, out, cache read and write'));
|
|
717
|
+
var models=Object.keys(sl.perModel).map(function(k){ var p=sl.perModel[k]; return {key:k,covered:p.covered,metered:p.metered,requests:p.requests}; }).sort(function(a,b){ return (b.covered+b.metered)-(a.covered+a.metered); });
|
|
718
|
+
hbarsChart($('modelsChart'), models, MODEL_SHORT);
|
|
719
|
+
clear($('modelsTable')).appendChild(tableFrom(['model','requests','covered','metered','api-equivalent'], models.map(function(m){ return [MODEL_SHORT(m.key),int(m.requests),usdFull(m.covered),usdFull(m.metered),usdFull(m.covered+m.metered)]; })));
|
|
720
|
+
var cons=Object.keys(sl.perConsumer).map(function(k){ var p=sl.perConsumer[k]; return {key:k,covered:p.covered,metered:p.metered,requests:p.requests}; }).sort(function(a,b){ return (b.covered+b.metered)-(a.covered+a.metered); });
|
|
721
|
+
hbarsChart($('consumersLChart'), cons, function(k){ return k; });
|
|
722
|
+
clear($('consumersLTable')).appendChild(tableFrom(['consumer','requests','covered','metered','api-equivalent'], cons.map(function(m){ return [m.key,int(m.requests),usdFull(m.covered),usdFull(m.metered),usdFull(m.covered+m.metered)]; })));
|
|
723
|
+
renderDaily(sl, L);
|
|
724
|
+
}
|
|
725
|
+
function niceMax(v){ if(v<=0) return 1; var e=Math.pow(10,Math.floor(Math.log10(v))); var f=v/e; var n=f<=1?1:f<=2?2:f<=2.5?2.5:f<=5?5:10; return n*e; }
|
|
726
|
+
function renderDaily(sl, L){
|
|
727
|
+
var box=clear($('dailyChart')); var series; var emphasisToday=false;
|
|
728
|
+
if(state.range==='today'){ var ctx=ledgerSlice(L,'7d'); series=ctx.daily; emphasisToday=true; $('dailySub').textContent='today, with the past week for context'; } else { series=sl.daily; $('dailySub').textContent=series.length+' days · API-equivalent per day'; }
|
|
729
|
+
if(!series.length){ box.appendChild(el('div','hint','No days in this range yet.')); return; }
|
|
730
|
+
var W=Math.max(320,box.clientWidth||600), H=240, m={l:48,r:18,t:14,b:30}; var pw=W-m.l-m.r, ph=H-m.t-m.b;
|
|
731
|
+
var vals=series.map(function(d){ return d.covered+d.metered; }); var ymax=niceMax(Math.max.apply(null,vals)*1.08); var n=series.length; var xs=function(i){ return m.l+(n===1?pw/2:i*pw/(n-1)); }; var ys=function(v){ return m.t+ph-(v/ymax)*ph; };
|
|
732
|
+
var NS='http://www.w3.org/2000/svg'; var svg=document.createElementNS(NS,'svg'); svg.setAttribute('viewBox','0 0 '+W+' '+H); svg.setAttribute('width',W); svg.setAttribute('height',H); svg.setAttribute('role','img'); svg.setAttribute('aria-label','API-equivalent spend per day');
|
|
733
|
+
var mk=function(name, attrs, parent){ var e=document.createElementNS(NS,name); Object.keys(attrs).forEach(function(k){ e.setAttribute(k,attrs[k]); }); (parent||svg).appendChild(e); return e; };
|
|
734
|
+
var grid=mk('g',{'class':'grid'}); var ticks=4; for(var i=0;i<=ticks;i++){ var v=ymax*i/ticks; var y=ys(v); mk('line',{x1:m.l,x2:W-m.r,y1:y,y2:y},grid); var tx=mk('text',{x:m.l-8,y:y+4,'text-anchor':'end'},svg); tx.textContent=v>=1000?'$'+(v/1000).toFixed(v%1000?1:0)+'K':'$'+(v%1?v.toFixed(v<10?1:0):v); }
|
|
735
|
+
var axis=mk('g',{'class':'axis'}); mk('line',{x1:m.l,x2:W-m.r,y1:m.t+ph,y2:m.t+ph},axis);
|
|
736
|
+
var every=Math.max(1,Math.ceil(n/Math.floor(pw/70))); series.forEach(function(d,i){ if(i%every===0||i===n-1){ var t=mk('text',{x:xs(i),y:H-8,'text-anchor':i===n-1?'end':i===0?'start':'middle'},svg); t.textContent=shortDay(d.day); } });
|
|
737
|
+
var path=''; var area=''; series.forEach(function(d,i){ var x=xs(i), y=ys(vals[i]); path+=(i?' L':'M')+x.toFixed(1)+' '+y.toFixed(1); }); area=path+' L'+xs(n-1).toFixed(1)+' '+(m.t+ph)+' L'+xs(0).toFixed(1)+' '+(m.t+ph)+' Z';
|
|
738
|
+
var g=mk('g',{'class':emphasisToday?'ctx':''}); mk('path',{'class':'area',d:area},g); mk('path',{'class':'line',d:path},g);
|
|
739
|
+
var last=n-1; mk('circle',{'class':emphasisToday?'today':'end',cx:xs(last),cy:ys(vals[last]),r:emphasisToday?6:4});
|
|
740
|
+
var lab=mk('text',{'class':'endlab',x:Math.min(xs(last),W-m.r-4),y:ys(vals[last])-12,'text-anchor':'end'}); lab.textContent=usd(vals[last])+(emphasisToday?' today':'');
|
|
741
|
+
var xh=mk('g',{'class':'xh'}); xh.setAttribute('visibility','hidden'); var xl=mk('line',{x1:0,x2:0,y1:m.t,y2:m.t+ph},xh); var xc=mk('circle',{cx:0,cy:0,r:4},xh);
|
|
742
|
+
var hit=mk('rect',{x:m.l,y:m.t,width:pw,height:ph,fill:'transparent'});
|
|
743
|
+
function at(clientX){ var r=svg.getBoundingClientRect(); var x=(clientX-r.left)*(W/r.width); var i=n===1?0:Math.round((x-m.l)/(pw/(n-1))); return Math.max(0,Math.min(n-1,i)); }
|
|
744
|
+
hit.addEventListener('pointermove',function(e){ var i=at(e.clientX); var d=series[i]; xh.setAttribute('visibility','visible'); xl.setAttribute('x1',xs(i)); xl.setAttribute('x2',xs(i)); xc.setAttribute('cx',xs(i)); xc.setAttribute('cy',ys(vals[i])); showTip(e, shortDay(d.day)+(i===n-1&&emphasisToday?' · today so far':''), [{label:'API-equivalent',value:usdFull(vals[i]),color:'var(--s1)'},{label:'covered',value:usdFull(d.covered)},{label:'metered',value:usdFull(d.metered)},{label:'requests',value:int(d.requests)}]); });
|
|
745
|
+
hit.addEventListener('pointerleave',function(){ xh.setAttribute('visibility','hidden'); hideTip(); });
|
|
746
|
+
box.appendChild(svg);
|
|
747
|
+
clear($('dailyTable')).appendChild(tableFrom(['day','requests','covered','metered','api-equivalent'], series.map(function(d){ return [shortDay(d.day),int(d.requests),usdFull(d.covered),usdFull(d.metered),usdFull(d.covered+d.metered)]; })));
|
|
748
|
+
}
|
|
749
|
+
$('rangeSeg').addEventListener('click',function(e){ var b=e.target.closest('button[data-range]'); if(!b) return; state.range=b.getAttribute('data-range'); this.querySelectorAll('button').forEach(function(x){ x.setAttribute('aria-pressed',String(x===b)); }); renderHero(); renderLedger(); });
|
|
750
|
+
|
|
751
|
+
function renderKeys(){
|
|
752
|
+
var wrap=clear($('keysWrap')); var budgets=(state.analytics&&state.analytics.budgets)||{}; var keys=state.keys; var names={}; Object.keys(budgets).forEach(function(n){ names[n]=true; }); (keys||[]).forEach(function(k){ names[k.name]=true; });
|
|
753
|
+
var list=Object.keys(names);
|
|
754
|
+
if(!list.length){ var h=el('div','hint'); h.textContent=state.mode==='sample'?'No named keys.':'No named keys with budgets in the window. Keys with budgets appear here from /analytics; names, seats and models need DARIO_ADMIN=1 and an admin token (/admin/keys).'; wrap.appendChild(h); $('keysSub').textContent=''; return; }
|
|
755
|
+
$('keysSub').textContent=list.length+' keys · '+(keys?'names, seats, models and budgets':'budgets only, '+(state.keysStatus||'admin API not reachable'));
|
|
756
|
+
var grid=el('div','keys');
|
|
757
|
+
list.sort().forEach(function(n){ var k=(keys||[]).filter(function(x){ return x.name===n; })[0]||{}; var b=budgets[n]||{}; var v=DarioMaths.budgetView(k,b); var card=el('div','key'); var top=el('div','top'); top.appendChild(el('span','name',n)); if(k.seat) top.appendChild(el('span','chip ghost',k.seat==='any'?'any seat':k.seat)); if(k.status) top.appendChild(el('span','chip '+(k.status==='active'?'good':'warn'),k.status)); card.appendChild(top);
|
|
758
|
+
// dollars on the first row, tokens on the second; the meter is the tighter of the two limits, and "no budget" only when there is neither
|
|
759
|
+
var r=el('div','r'); r.appendChild(el('span',null,v.usdLimit?'today '+usdFull(v.usedUsd)+' of '+usd(v.usdLimit):'today '+usdFull(v.usedUsd)+(v.tokenLimit?'':' · no budget'))); r.appendChild(el('b',null,v.utilisation!=null?pct(v.utilisation)+(v.usdLimit&&v.tokenLimit?' · '+v.binding+' bind':''):'')); card.appendChild(r); card.appendChild(v.utilisation!=null?meter(v.utilisation):meter(0,'ok'));
|
|
760
|
+
var r2=el('div','r'); r2.appendChild(el('span',null,v.tokenLimit?compact(v.usedTokens)+' of '+compact(v.tokenLimit)+' tokens today · '+pct(v.tokenFraction):(b.usedTokens!=null?compact(v.usedTokens)+' tokens today':''))); r2.appendChild(el('span',null,k.last_used?'used '+dur(Date.now()-Date.parse(k.last_used))+' ago':'')); card.appendChild(r2);
|
|
761
|
+
if(k.models&&k.models.length){ card.appendChild(el('div','models',k.models.map(MODEL_SHORT).join(' · '))); }
|
|
762
|
+
grid.appendChild(card); });
|
|
763
|
+
wrap.appendChild(grid);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function renderBanner(){ var b=$('banner'); var txt=$('bannerText'); var btn=$('bannerBtn'); if(state.halted){ b.className='banner show'; txt.textContent='Overage guard halted routing: '+(state.haltReason||'a request would have been billed as extra usage')+'. Nothing is forwarded until it resumes.'; btn.hidden=state.mode!=='live'; } else if(state.warnOverage){ b.className='banner warn show'; txt.textContent=state.warnOverage; btn.hidden=true; } else { b.className='banner'; } }
|
|
767
|
+
$('bannerBtn').addEventListener('click',function(){ fetch('/admin/resume',{method:'POST',headers:authHeaders()}).then(function(r){ if(r.ok){ state.halted=false; renderBanner(); renderHeader(); } }).catch(function(){}); });
|
|
768
|
+
|
|
769
|
+
function renderAll(){ renderHeader(); renderHero(); renderKpis(); renderSeats(); renderRouting(); renderFeed(); renderAnatomy(); renderLedger(); renderKeys(); renderBanner(); }
|
|
770
|
+
function renderWindow(){ renderHeader(); renderKpis(); renderRouting(); renderAnatomy(); }
|
|
771
|
+
|
|
772
|
+
/* ---------- sample mode: a ticking simulation ---------- */
|
|
773
|
+
function startSample(){
|
|
774
|
+
state.mode='sample'; state.health=SAMPLE.health; state.healthView={ body:SAMPLE.health, http:200, seenAt:Date.now(), unreachable:false }; state.accounts=SAMPLE.accounts; state.codex=SAMPLE.codex; state.ledger=SAMPLE.ledger; state.keys=SAMPLE.keys; state.records=SAMPLE.records.slice(); state.streamOk=true; state.analytics=summarize(state.records,SAMPLE.budgets); state.analytics.predictions.estimatedExhaustionMinutes=186;
|
|
775
|
+
renderAll();
|
|
776
|
+
function tick(){ var c=SAMPLE.pickConsumer(); var m=SAMPLE.pick(SAMPLE.consumers[c].models); var t=SAMPLE.tokensFor(m); var seat=SAMPLE.seatFor(m); var r=SAMPLE.r; var claimX=r(); var claim=claimX<.94?'subscription':claimX<.97?'subscription_fallback':claimX<.99?'extra_usage':'api'; var sx=r(); var status=sx<.985?200:sx<.993?429:sx<.997?529:500; var ttfb=500+r()*800; var up=ttfb+t.outputTokens*SAMPLE.perTok(m)*(0.8+r()*.4); var q=r()<.3?r()*400:r()*40; var pace=r()<.25?r()*220:0; var total=q+pace+up+10+r()*20;
|
|
777
|
+
var acc=state.accounts.accounts.filter(function(a){ return a.alias===seat; })[0]; if(acc){ acc.util5h=Math.min(.98,acc.util5h+0.0015); acc.util7d=Math.min(.98,acc.util7d+0.0004); acc.requestCount++; acc.resetInMs=Math.max(0,acc.resetInMs-(Date.now()-(state.lastTick||Date.now()))); acc.utilAgeMs=0; }
|
|
778
|
+
state.accounts.accounts.forEach(function(a){ if(a.alias!==seat) a.utilAgeMs+=Date.now()-(state.lastTick||Date.now()); });
|
|
779
|
+
state.accounts.totalHeadroom=Math.round(100*state.accounts.accounts.reduce(function(s,a){ return s+(1-Math.max(a.util5h,a.util7d)); },0)/state.accounts.accounts.length);
|
|
780
|
+
state.lastTick=Date.now();
|
|
781
|
+
pushRecord({ timestamp:new Date().toISOString(), consumer:c, account:seat, model:m, inputTokens:t.inputTokens, outputTokens:t.outputTokens, cacheReadTokens:t.cacheReadTokens, cacheCreateTokens:t.cacheCreateTokens, thinkingTokens:t.thinkingTokens, claim:claim, util5h:acc?acc.util5h:.23, util7d:acc?acc.util7d:.01, overageUtil:0, latencyMs:Math.round(total), status:status, isStream:true, isOpenAI:/gpt/.test(m), timing:{queueMs:Math.round(q),pacingMs:Math.round(pace),upstreamTtfbMs:Math.round(ttfb),upstreamMs:Math.round(up),totalMs:Math.round(total)} });
|
|
782
|
+
// the newest request lands in today's ledger too
|
|
783
|
+
var key=dayKey(new Date()); var L=state.ledger; var dm=(L.days[key]=L.days[key]||{}); var cell=(dm[m]=dm[m]||{covered:{requests:0,inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreateTokens:0},metered:{requests:0,inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreateTokens:0}}); var k=(claim==='subscription'||claim==='subscription_fallback')?cell.covered:cell.metered; k.requests++; k.inputTokens+=t.inputTokens; k.outputTokens+=t.outputTokens; k.cacheReadTokens+=t.cacheReadTokens; k.cacheCreateTokens+=t.cacheCreateTokens;
|
|
784
|
+
var dc=(L.consumers[key]=L.consumers[key]||{}); var cc=(dc[c]=dc[c]||{}); var cm=(cc[m]=cc[m]||{covered:{requests:0,inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreateTokens:0},metered:{requests:0,inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreateTokens:0}}); var k2=(claim==='subscription'||claim==='subscription_fallback')?cm.covered:cm.metered; k2.requests++; k2.inputTokens+=t.inputTokens; k2.outputTokens+=t.outputTokens; k2.cacheReadTokens+=t.cacheReadTokens; k2.cacheCreateTokens+=t.cacheCreateTokens;
|
|
785
|
+
var bucket=(claim==='subscription'||claim==='subscription_fallback')?'covered':'metered'; var add=sampleCost(m,t); var P=L.priced; var pdm=(P.days[key]=P.days[key]||{}); var pr=(pdm[m]=pdm[m]||{covered:0,metered:0}); pr[bucket]+=add; var pcc=(P.consumers[key]=P.consumers[key]||{}); var pcr=(pcc[c]=pcc[c]||{}); var pcm=(pcr[m]=pcr[m]||{covered:0,metered:0}); pcm[bucket]+=add;
|
|
786
|
+
SAMPLE.budgets[c]=SAMPLE.budgets[c]||{usedUsd:0,usedTokens:0}; SAMPLE.budgets[c].usedUsd+=add; SAMPLE.budgets[c].usedTokens+=t.inputTokens+t.outputTokens+t.cacheReadTokens;
|
|
787
|
+
state.analytics=summarize(state.records,SAMPLE.budgets); state.analytics.predictions.estimatedExhaustionMinutes=Math.max(5,Math.round(186-(Date.now()-SAMPLE.t0)/60000));
|
|
788
|
+
renderWindow(); renderSeats(); renderHero(); if(state.tickCount++%6===0){ renderLedger(); renderKeys(); }
|
|
789
|
+
state.demoTimer=setTimeout(tick, 1500+r()*3500);
|
|
790
|
+
}
|
|
791
|
+
SAMPLE.t0=Date.now(); state.tickCount=1; state.demoTimer=setTimeout(tick,1800);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/* ---------- live mode ---------- */
|
|
795
|
+
function authHeaders(){ var h={}; if(state.token) h['Authorization']='Bearer '+state.token; return h; }
|
|
796
|
+
function getJson(path){ return fetch(path,{headers:authHeaders(),cache:'no-store'}).then(function(r){ if(r.status===401||r.status===403){ var e=new Error(String(r.status)); e.status=r.status; throw e; } if(!r.ok){ var e2=new Error(String(r.status)); e2.status=r.status; throw e2; } return r.json(); }); }
|
|
797
|
+
/* /health answers 503 with a JSON verdict in the body, so the body is read whatever the status; an unparseable answer is a null body. */
|
|
798
|
+
function getResponse(path){ return fetch(path,{headers:authHeaders(),cache:'no-store'}).then(function(r){ return r.text().then(function(t){ var b=null; try{ b=JSON.parse(t); }catch(e){} return { status:r.status, body:b }; }); }); }
|
|
799
|
+
function stopLive(){ state.pollers.forEach(clearInterval); state.pollers=[]; if(state.stream) state.stream.stop(); state.stream=null; }
|
|
800
|
+
function restartLive(){ stopLive(); startLive(); }
|
|
801
|
+
function startLive(){
|
|
802
|
+
state.mode='live'; state.streamOk=false; state.streamNote=''; renderHeader();
|
|
803
|
+
// the halt state rides on /analytics for every viewer: a halt seen on the stream and lifted while the viewer was disconnected clears on the next poll
|
|
804
|
+
function pollAnalytics(){ return getJson('/analytics').then(function(a){ state.analytics=a; state.authNeeded=false; var hs=DarioMaths.haltFrom('snapshot', a.overage); if(hs&&hs.halted!==state.halted){ state.halted=hs.halted; state.haltReason=hs.reason; renderBanner(); renderHeader(); } renderWindow(); renderKeys(); }).catch(function(e){ if(e.status===401){ if(!state.authNeeded){ state.authNeeded=true; state.openToken('This proxy is keyed: paste its analytics token or an API key.'); } } }); }
|
|
805
|
+
// a 503 body is the degraded verdict and renders as such; a failed fetch marks the last verdict unreachable instead of leaving it on screen
|
|
806
|
+
function applyHealth(v){ state.healthView=v; state.health=v.body; renderHeader(); renderKpis(); }
|
|
807
|
+
function pollHealth(){ return getResponse('/health').then(function(res){ applyHealth(DarioMaths.healthFrom(res, state.healthView, Date.now())); }).catch(function(){ applyHealth(DarioMaths.healthFrom(null, state.healthView, Date.now())); }); }
|
|
808
|
+
function pollSeats(){ return Promise.all([ getJson('/accounts').then(function(a){ state.accounts=a; state.accountsStatus=''; }).catch(function(e){ state.accounts=null; state.accountsStatus=e.status===401?'Seats need an API key: the analytics token reads spend and the feed only.':'/accounts: '+e.message; }), getJson('/codex').then(function(c){ state.codex=c; }).catch(function(){ state.codex=null; }) ]).then(function(){ renderSeats(); renderKpis(); }); }
|
|
809
|
+
function pollLedger(){ $('ledgerCard').classList.add('stale'); return getJson('/analytics/ledger').then(function(l){ state.ledger=l; }).catch(function(e){ if(e.status===404) state.ledger=null; }).then(function(){ $('ledgerCard').classList.remove('stale'); renderHero(); renderLedger(); }); }
|
|
810
|
+
function pollKeys(){ return getJson('/admin/keys').then(function(k){ state.keys=Array.isArray(k)?k:(k.keys||[]); state.keysStatus=''; }).catch(function(e){ state.keys=null; state.keysStatus=e.status===403?'DARIO_ADMIN is off':e.status===401?'the token is not the admin token':'admin API '+e.message; }).then(renderKeys); }
|
|
811
|
+
function pollHalt(){ return getJson('/admin/resume').then(function(h){ state.halted=!!h.halted; state.haltReason=h.state&&h.state.reason; renderBanner(); renderHeader(); }).catch(function(){}); }
|
|
812
|
+
pollAnalytics(); pollHealth(); pollSeats(); pollLedger(); pollKeys(); pollHalt();
|
|
813
|
+
state.pollers.push(setInterval(pollAnalytics,5000), setInterval(pollHealth,30000), setInterval(pollSeats,15000), setInterval(pollLedger,60000), setInterval(pollKeys,60000), setInterval(pollHalt,20000));
|
|
814
|
+
streamRecords();
|
|
815
|
+
}
|
|
816
|
+
/* The live tail. The loop itself (connect, parse, retry) is DarioMaths.sseLoop; this wires the browser's fetch, decoder and timer to it and renders what arrives. */
|
|
817
|
+
function streamRecords(){
|
|
818
|
+
if(state.stream) state.stream.stop();
|
|
819
|
+
state.stream=DarioMaths.sseLoop({ path:'/analytics/stream', fetchFn:function(p,init){ return fetch(p,init); }, headers:authHeaders, abort:function(){ return new AbortController(); }, decoder:function(){ var d=new TextDecoder(); return function(u8){ return d.decode(u8,{stream:true}); }; }, schedule:function(fn,ms){ setTimeout(fn,ms); },
|
|
820
|
+
onState:function(ok,note){ state.streamOk=ok; state.streamNote=note; renderHeader(); },
|
|
821
|
+
onAuth:function(){ if(!state.authNeeded){ state.authNeeded=true; state.openToken('This proxy is keyed: paste its analytics token or an API key.'); } },
|
|
822
|
+
onChunk:function(m){ var ev=m.event, obj=m.data;
|
|
823
|
+
var hs=DarioMaths.haltFrom(ev,obj); if(hs){ state.halted=hs.halted; state.haltReason=hs.reason; renderBanner(); renderHeader(); return; } if(ev==='overage_warn'){ state.warnOverage='Overage guard warning: '+(obj.reason||'a seat is near its limit'); renderBanner(); return; }
|
|
824
|
+
// every connection replays the proxy's last records; the ones already on the page are skipped by pushRecord, so the feed keeps what it collected
|
|
825
|
+
if(obj&&obj.timestamp) pushRecord(obj); } });
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/* ---------- boot ---------- */
|
|
829
|
+
function detectLive(){ var mode=DarioMaths.modeFor(location.protocol, window.DARIO_COCKPIT); if(mode!=='probe') return Promise.resolve(mode==='live'); var timeout=new Promise(function(res){ setTimeout(function(){ res(false); },1500); }); var probe=fetch('/livez',{cache:'no-store'}).then(function(r){ return r.ok?r.json():null; }).then(function(j){ return !!(j&&j.status==='ok'); }).catch(function(){ return false; }); return Promise.race([probe,timeout]); }
|
|
830
|
+
var resizeTimer=null; window.addEventListener('resize',function(){ clearTimeout(resizeTimer); resizeTimer=setTimeout(function(){ renderRouting(); renderAnatomy(); renderLedger(); },150); });
|
|
831
|
+
detectLive().then(function(live){ if(live){ startLive(); } else { startSample(); } });
|
|
832
|
+
})();
|
|
833
|
+
</script>
|
|
834
|
+
</body>
|
|
835
|
+
</html>
|