@2lab.ai/ui 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +55 -2
  2. package/dist/agent-inbox.js +1061 -0
  3. package/dist/agent-inbox.js.map +1 -0
  4. package/dist/agent-queue.js +1610 -0
  5. package/dist/agent-queue.js.map +1 -0
  6. package/dist/agent-run.js +859 -0
  7. package/dist/agent-run.js.map +1 -0
  8. package/dist/ai-answer.js +687 -0
  9. package/dist/ai-answer.js.map +1 -0
  10. package/dist/components/agent-inbox/agent-inbox.d.ts +158 -0
  11. package/dist/components/agent-inbox/index.d.ts +2 -0
  12. package/dist/components/agent-inbox/styles.d.ts +6 -0
  13. package/dist/components/agent-queue/agent-queue.d.ts +41 -0
  14. package/dist/components/agent-queue/index.d.ts +6 -0
  15. package/dist/components/agent-queue/logic.d.ts +261 -0
  16. package/dist/components/agent-queue/strings.d.ts +473 -0
  17. package/dist/components/agent-queue/styles.d.ts +8 -0
  18. package/dist/components/agent-run/agent-run.d.ts +273 -0
  19. package/dist/components/agent-run/index.d.ts +2 -0
  20. package/dist/components/agent-run/styles.d.ts +6 -0
  21. package/dist/components/ai-answer/ai-answer.d.ts +92 -0
  22. package/dist/components/ai-answer/index.d.ts +2 -0
  23. package/dist/components/ai-answer/styles.d.ts +2 -0
  24. package/dist/components/context-window/context-window.d.ts +156 -0
  25. package/dist/components/context-window/index.d.ts +2 -0
  26. package/dist/components/context-window/styles.d.ts +2 -0
  27. package/dist/components/memory-panel/index.d.ts +2 -0
  28. package/dist/components/memory-panel/memory-panel.d.ts +101 -0
  29. package/dist/components/memory-panel/styles.d.ts +2 -0
  30. package/dist/components/model-picker/index.d.ts +2 -0
  31. package/dist/components/model-picker/model-picker.d.ts +136 -0
  32. package/dist/components/model-picker/morph.d.ts +9 -0
  33. package/dist/components/model-picker/styles.d.ts +6 -0
  34. package/dist/components/tool-call-card/index.d.ts +7 -0
  35. package/dist/components/tool-call-card/logic.d.ts +87 -0
  36. package/dist/components/tool-call-card/strings.d.ts +94 -0
  37. package/dist/components/tool-call-card/styles.d.ts +5 -0
  38. package/dist/components/tool-call-card/tool-call-card.d.ts +49 -0
  39. package/dist/components/tool-call-card/tool-call-group.d.ts +32 -0
  40. package/dist/context-window.js +384 -0
  41. package/dist/context-window.js.map +1 -0
  42. package/dist/core/element.d.ts +41 -0
  43. package/dist/core/format.d.ts +14 -0
  44. package/dist/core/html.d.ts +10 -0
  45. package/dist/core/tokens.d.ts +14 -0
  46. package/dist/format-BBGWXPzk.js +51 -0
  47. package/dist/format-BBGWXPzk.js.map +1 -0
  48. package/dist/html-elD6Bb_u.js +175 -0
  49. package/dist/html-elD6Bb_u.js.map +1 -0
  50. package/dist/index.d.ts +16 -0
  51. package/dist/index.js +129 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/memory-panel.js +1255 -0
  54. package/dist/memory-panel.js.map +1 -0
  55. package/dist/model-picker.js +858 -0
  56. package/dist/model-picker.js.map +1 -0
  57. package/dist/tokens.css +4 -0
  58. package/dist/tool-call-card.js +1046 -0
  59. package/dist/tool-call-card.js.map +1 -0
  60. package/package.json +79 -4
@@ -0,0 +1,1610 @@
1
+ import { d as define, U as UiElement, e as esc, f as escAttr } from "./html-elD6Bb_u.js";
2
+ const HOUR = 36e5;
3
+ const DAY = 864e5;
4
+ const D7 = 7 * DAY;
5
+ const LOW_PCT = 20;
6
+ const EPS = 1e-9;
7
+ function toMs(v, unit = "ms") {
8
+ if (v === void 0 || v === null || v === "") return void 0;
9
+ const n = Number(v);
10
+ if (!Number.isFinite(n)) return void 0;
11
+ const ms = unit === "s" ? n * 1e3 : n;
12
+ return Math.abs(ms) <= 864e13 ? ms : void 0;
13
+ }
14
+ function normalize(d) {
15
+ const u = d.timeUnit ?? "ms";
16
+ const ms = (v) => toMs(v, u);
17
+ return {
18
+ ...d,
19
+ timeUnit: "ms",
20
+ now: ms(d.now),
21
+ fetchedAt: ms(d.fetchedAt),
22
+ providers: d.providers?.map((p) => ({
23
+ ...p,
24
+ measuredAt: ms(p.measuredAt),
25
+ refills: (p.refills ?? []).map((e) => ({ ...e, at: ms(e.at) ?? Number.NaN })).filter((e) => Number.isFinite(e.at)).sort((a, b) => a.at - b.at),
26
+ accounts: p.accounts?.map((a) => ({ ...a, state: accountState(a.state), returnAt: ms(a.returnAt) }))
27
+ })),
28
+ lane: d.lane && {
29
+ ...d.lane,
30
+ jobs: (d.lane.jobs ?? []).map((j) => ({
31
+ ...j,
32
+ nextRunAt: ms(j.nextRunAt),
33
+ runs: (j.runs ?? []).map((r) => ({ ...r, at: ms(r.at) ?? Number.NaN })).filter((r) => Number.isFinite(r.at))
34
+ })),
35
+ runs24h: d.lane.runs24h?.map((r) => ({ ...r, at: ms(r.at) ?? Number.NaN })).filter((r) => Number.isFinite(r.at))
36
+ }
37
+ };
38
+ }
39
+ function plain(s, max = 0) {
40
+ const t = Array.from(String(s ?? ""), (c) => c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127 ? " " : c).join("").replace(/\s+/g, " ").trim();
41
+ return max && t.length > max ? `${t.slice(0, max - 1)}…` : t;
42
+ }
43
+ const pad2 = (n) => String(n).padStart(2, "0");
44
+ function clockLabel(atMs, nowMs) {
45
+ const d = new Date(atMs);
46
+ const n = new Date(nowMs);
47
+ const hm2 = `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
48
+ return d.toDateString() === n.toDateString() ? hm2 : `${d.getMonth() + 1}/${d.getDate()} ${hm2}`;
49
+ }
50
+ function hm(ms) {
51
+ const d = new Date(ms);
52
+ return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
53
+ }
54
+ function spanShort(secs) {
55
+ const s = Math.max(0, Math.floor(Number(secs) || 0));
56
+ if (s < 60) return "<1m";
57
+ const m = Math.floor(s / 60);
58
+ if (m < 60) return `${m}m`;
59
+ const h = Math.floor(m / 60);
60
+ if (h < 24) return `${h}h ${m % 60}m`;
61
+ return `${Math.floor(h / 24)}d ${h % 24}h`;
62
+ }
63
+ function fmt1(x) {
64
+ return (Math.round((Number(x) || 0) * 10) / 10).toFixed(1);
65
+ }
66
+ function fmtCount(v, total) {
67
+ return fmt1(v) === fmt1(total) && v < total - 5e-3 ? (Math.round(v * 100) / 100).toFixed(2) : fmt1(v);
68
+ }
69
+ function fmtPct(v, total) {
70
+ if (!(total > 0)) return "—";
71
+ const p = v / total * 100;
72
+ return `${p >= 99.95 ? 100 : Math.min(99, Math.round(p))}%`;
73
+ }
74
+ const gainOf = (e, key) => Number(e[key]) || 0;
75
+ function has5h(p) {
76
+ return p.fiveHour !== void 0 && p.fiveHour !== null;
77
+ }
78
+ function primaryKey(p) {
79
+ return has5h(p) ? "fiveHour" : "weekly";
80
+ }
81
+ const POOL_STATES = ["ok", "low", "empty", "unknown"];
82
+ const ACCOUNT_STATES = ["available", "limited", "paused", "spent", "unmeasured", "excluded"];
83
+ function accountState(v) {
84
+ return ACCOUNT_STATES.includes(v) ? v : "unmeasured";
85
+ }
86
+ function poolState(p) {
87
+ if (p.state && POOL_STATES.includes(p.state)) return p.state;
88
+ const total = Number(p.total) || 0;
89
+ const primary = Number(p[primaryKey(p)]) || 0;
90
+ if (total <= EPS) return "unknown";
91
+ if (primary <= EPS) return "empty";
92
+ return primary / total * 100 < LOW_PCT ? "low" : "ok";
93
+ }
94
+ function advanceProvider(p, nowMs) {
95
+ const evs = p.refills ?? [];
96
+ const due = evs.filter((e) => e.at <= nowMs);
97
+ if (!due.length) return p;
98
+ const out = { ...p, refills: evs.filter((e) => e.at > nowMs) };
99
+ if (has5h(p)) out.fiveHour = due.reduce((t, e) => t + gainOf(e, "fiveHour"), Number(p.fiveHour) || 0);
100
+ if (p.weekly !== void 0) out.weekly = due.reduce((t, e) => t + gainOf(e, "weekly"), Number(p.weekly) || 0);
101
+ return out;
102
+ }
103
+ function nextRefill(p, key, nowMs) {
104
+ for (const e of p.refills ?? []) {
105
+ if (e.at > nowMs + D7) break;
106
+ if (e.at > nowMs && gainOf(e, key) >= 0.05) return e;
107
+ }
108
+ return null;
109
+ }
110
+ function gainWithin(p, key, nowMs, span = D7) {
111
+ return (p.refills ?? []).filter((e) => e.at > nowMs && e.at <= nowMs + span).reduce((t, e) => t + gainOf(e, key), 0);
112
+ }
113
+ function limitedAccounts(p) {
114
+ return (p.accounts ?? []).filter((a) => a.state === "limited");
115
+ }
116
+ function firstReturn(p) {
117
+ let at = null;
118
+ for (const a of limitedAccounts(p)) if (a.returnAt && (at === null || a.returnAt < at)) at = a.returnAt;
119
+ return at;
120
+ }
121
+ function emptyReturnAt(p, nowMs) {
122
+ return firstReturn(p) ?? nextRefill(p, primaryKey(p), nowMs)?.at ?? null;
123
+ }
124
+ function countedAccounts(p) {
125
+ if (p.countedAccounts !== void 0) return p.countedAccounts;
126
+ return (p.accounts ?? []).filter((a) => a.state !== "paused" && a.state !== "excluded").length;
127
+ }
128
+ function exclusions(p) {
129
+ if (p.excluded)
130
+ return {
131
+ held: p.excluded.held ?? 0,
132
+ unknownTier: p.excluded.unknownTier ?? 0,
133
+ noMeasure: p.excluded.noMeasure ?? 0
134
+ };
135
+ const out = { held: 0, unknownTier: 0, noMeasure: 0 };
136
+ for (const a of p.accounts ?? []) {
137
+ if (a.state === "paused") out.held++;
138
+ else if (a.state === "excluded") {
139
+ if (a.reason === "unknownTier") out.unknownTier++;
140
+ else out.noMeasure++;
141
+ }
142
+ }
143
+ return out;
144
+ }
145
+ function axisPos(offsetMs) {
146
+ const s = (Number(offsetMs) || 0) / 1e3;
147
+ if (s <= 0) return 0;
148
+ if (s >= D7 / 1e3) return 1;
149
+ if (s <= 86400) {
150
+ const st = [0, 3600, 10800, 21600, 43200, 86400];
151
+ for (let i = 1; i < st.length; i++) {
152
+ if (s <= st[i]) return (i - 1 + (s - st[i - 1]) / (st[i] - st[i - 1])) * 0.11;
153
+ }
154
+ }
155
+ return 0.55 + (s - 86400) / (6 * 86400) * 0.45;
156
+ }
157
+ function timelineMarks(p, W, nowMs, pad = 16) {
158
+ const X = (at) => pad + axisPos(at - nowMs) * (W - 2 * pad);
159
+ const LW = W < 420 ? 46 : 64;
160
+ const out = [];
161
+ for (const e of p.refills ?? []) {
162
+ if (e.at <= nowMs || e.at > nowMs + D7) continue;
163
+ const c = e.cause === "7d" ? "wk" : "h5";
164
+ const d = c === "wk" ? gainOf(e, "weekly") : gainOf(e, "fiveHour");
165
+ if (d < 0.05) continue;
166
+ const x = X(e.at);
167
+ let last;
168
+ for (const q of out) if (q.c === c) last = q;
169
+ if (last && x - (last.x0 + last.x1) / 2 < LW) {
170
+ last.gain += d;
171
+ last.n++;
172
+ last.x1 = x;
173
+ if (e.account) last.names.push(e.account);
174
+ } else out.push({ c, x0: x, x1: x, gain: d, n: 1, names: e.account ? [e.account] : [], at: e.at });
175
+ }
176
+ return out;
177
+ }
178
+ function runKind(status) {
179
+ if (status === "ok") return "ok";
180
+ if (status === "failed") return "fail";
181
+ if (status === "running" || status === "waiting") return "run";
182
+ return "unknown";
183
+ }
184
+ function dayRuns(lane, nowMs) {
185
+ const from = nowMs - DAY;
186
+ const src = lane.runs24h ?? (lane.jobs ?? []).flatMap((j) => j.runs ?? []);
187
+ const runs = [];
188
+ let unknown = 0;
189
+ for (const r of src) {
190
+ if (!(r.at >= from) || r.at > nowMs) continue;
191
+ const k = runKind(r.status);
192
+ if (k === "unknown") unknown++;
193
+ else runs.push({ at: r.at, kind: k });
194
+ }
195
+ const cut = [];
196
+ if (!lane.runs24h) {
197
+ for (const j of lane.jobs ?? []) {
198
+ const kept = j.runs ?? [];
199
+ const cap = Number(j.historyCap) || 0;
200
+ if (cap > 0 && kept.length >= cap) {
201
+ const oldest = Math.min(...kept.map((r) => r.at));
202
+ if (oldest > from) cut.push({ name: j.name, from: oldest, cap });
203
+ }
204
+ }
205
+ }
206
+ const count = (k) => runs.filter((r) => r.kind === k).length;
207
+ return {
208
+ from,
209
+ runs,
210
+ n: runs.length,
211
+ ok: count("ok"),
212
+ fail: count("fail"),
213
+ run: count("run"),
214
+ unknown,
215
+ cut,
216
+ cutFrom: cut.reduce((t, c) => Math.max(t, c.from), 0)
217
+ };
218
+ }
219
+ function runBins(day, nowMs) {
220
+ const start = nowMs - DAY;
221
+ const bins = [];
222
+ for (let k = 0; k < 24; k++) bins.push({ at: start + k * HOUR, ok: 0, run: 0, fail: 0 });
223
+ for (const r of day.runs) {
224
+ const i = Math.max(0, Math.min(23, Math.floor((r.at - start) / HOUR)));
225
+ bins[i][r.kind]++;
226
+ }
227
+ return bins;
228
+ }
229
+ const NICE = [
230
+ 4,
231
+ 6,
232
+ 8,
233
+ 10,
234
+ 12,
235
+ 16,
236
+ 20,
237
+ 24,
238
+ 30,
239
+ 40,
240
+ 50,
241
+ 60,
242
+ 80,
243
+ 100,
244
+ 120,
245
+ 160,
246
+ 200,
247
+ 300,
248
+ 400,
249
+ 500,
250
+ 600,
251
+ 800,
252
+ 1e3
253
+ ];
254
+ function niceMax(peak) {
255
+ const top = Math.max(Number(peak) || 0, 4);
256
+ return NICE.find((v) => v >= top) ?? Math.ceil(top / 500) * 500;
257
+ }
258
+ const newest = (j) => (j.runs ?? []).reduce((t, r) => Math.max(t, r.at), 0);
259
+ function orderJobs(jobs) {
260
+ return jobs.slice().sort((a, b) => {
261
+ const aw = a.state === "waiting";
262
+ const bw = b.state === "waiting";
263
+ if (aw !== bw) return aw ? -1 : 1;
264
+ const ar = a.state === "running";
265
+ const br = b.state === "running";
266
+ if (ar !== br) return ar ? -1 : 1;
267
+ const al = newest(a);
268
+ const bl = newest(b);
269
+ if (al !== bl) return bl - al;
270
+ const an = a.nextRunAt ?? Number.POSITIVE_INFINITY;
271
+ const bn = b.nextRunAt ?? Number.POSITIVE_INFINITY;
272
+ if (an !== bn) return an - bn;
273
+ return String(a.name).localeCompare(String(b.name));
274
+ });
275
+ }
276
+ function upNext(jobs) {
277
+ return jobs.filter((j) => j.nextRunAt !== void 0 && Number.isFinite(j.nextRunAt)).sort((a, b) => a.nextRunAt - b.nextRunAt);
278
+ }
279
+ function jobRuns(j) {
280
+ return (j.runs ?? []).slice().sort((a, b) => b.at - a.at);
281
+ }
282
+ function scheduleLabel(expr, nextRunAt, lang = "en") {
283
+ const raw = String(expr ?? "").trim();
284
+ const f = raw.split(/ +/);
285
+ if (f.length !== 5) return raw;
286
+ const [min, hour, dom, mon, dow] = f;
287
+ const every = /^[*][/]([0-9]+)$/;
288
+ const num = /^[0-9]+$/;
289
+ const ko2 = lang === "ko";
290
+ let m;
291
+ if (hour === "*" && dom === "*" && mon === "*" && dow === "*") {
292
+ m = every.exec(min);
293
+ if (m) return ko2 ? `${m[1]}분마다` : `Every ${m[1]} min`;
294
+ if (min === "*") return ko2 ? "매분" : "Every minute";
295
+ if (num.test(min))
296
+ return Number(min) === 0 ? ko2 ? "매시 정각" : "Hourly" : ko2 ? `매시 ${Number(min)}분` : `Hourly at :${pad2(Number(min))}`;
297
+ }
298
+ if (dom === "*" && mon === "*" && dow === "*" && num.test(min)) {
299
+ m = every.exec(hour);
300
+ if (m) return ko2 ? `${m[1]}시간마다` : `Every ${m[1]} h`;
301
+ }
302
+ if (dom === "*" && mon === "*" && num.test(min) && num.test(hour) && nextRunAt) {
303
+ const d = new Date(nextRunAt);
304
+ const t = `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
305
+ if (dow === "*") return ko2 ? `매일 ${t}` : `Daily ${t}`;
306
+ if (dow === "1-5" && d.getDay() === d.getUTCDay()) return ko2 ? `평일 ${t}` : `Weekdays ${t}`;
307
+ }
308
+ return raw;
309
+ }
310
+ const en = {
311
+ /** Time left, floored: 1d 5h · 1h 20m · 24m · 42s. */
312
+ dur: (s) => {
313
+ if (s < 60) return `${s}s`;
314
+ const m = Math.floor(s / 60);
315
+ const h = Math.floor(m / 60);
316
+ if (h >= 24) return `${Math.floor(h / 24)}d ${h % 24}h`;
317
+ if (h) return `${h}h ${m % 60}m`;
318
+ return `${m}m`;
319
+ },
320
+ soon: "soon",
321
+ in: (x) => `in ${x}`,
322
+ back: (x) => `back in ${x}`,
323
+ backSoon: "back soon",
324
+ runs: (x) => `runs in ${x}`,
325
+ runsSoon: "runs soon",
326
+ since: (x) => `${x} ago`,
327
+ fetched: (s) => s < 60 ? `updated ${s}s ago` : `updated ${Math.floor(s / 60)}m ago`,
328
+ /** Age of a measurement, first unit only: 12m · 4h · 6d. */
329
+ age: (s) => {
330
+ const m = Math.floor(s / 60);
331
+ const h = Math.floor(m / 60);
332
+ return h >= 24 ? `${Math.floor(h / 24)}d` : h ? `${h}h` : `${Math.max(1, m)}m`;
333
+ },
334
+ // Capacity
335
+ capTitle: "LLM capacity",
336
+ capError: (r) => `Couldn’t load capacity — ${r}`,
337
+ capNone: "No accounts in the pool",
338
+ capAria: (n) => `${n} capacity`,
339
+ source: (src, n) => `${src ? `${src} · ` : ""}${n} ${n === 1 ? "account" : "accounts"}`,
340
+ pill: {
341
+ unknown: "◌ Can’t compute",
342
+ allLimited: "✗ All limited",
343
+ empty: "✗ Used up",
344
+ limited: (n) => `⏸ Limited ${n}`,
345
+ low: "▲ Low",
346
+ ok: "● Available"
347
+ },
348
+ disc: {
349
+ reflected: (c, n) => `${c}/${n} reflected`,
350
+ blocked: (b) => ` (${b} limited incl.)`,
351
+ excluded: (n, why) => `excluded ${n} (${why})`,
352
+ held: "held",
353
+ unknownTier: "no tier",
354
+ noMeasure: "no measure",
355
+ assumed: (n, tier) => `no tier ${n} (${tier} assumed)`,
356
+ estimated: (n) => `estimated ${n}`,
357
+ unmeasured: (n) => `5h unmeasured ${n}`,
358
+ measured: (a) => `measured ${a} ago`
359
+ },
360
+ bar: {
361
+ fiveHour: "5-hour",
362
+ fiveHourSub: "available now",
363
+ weekly: "Weekly",
364
+ weeklySub: "left this week",
365
+ of: (total) => `/ ${total} accounts`,
366
+ said: (label, v, total, pct) => `${label} ${v} of ${total} accounts, ${pct}`,
367
+ low: (p) => `Below ${p}% the pool reads low`,
368
+ next: (gain) => `next +${gain} · `,
369
+ heldByWeekly: "held by the weekly limit · ",
370
+ backWithin: (g) => `${g} back within 7 days`,
371
+ nothing: "nothing returns within 7 days",
372
+ unknown: "Can’t compute — no countable accounts"
373
+ },
374
+ gain: (x, f) => x <= 1e-7 ? "+0" : x < 0.05 ? "< +0.1" : `+${f(x)}`,
375
+ cause: { "5h": "5-hour reset", "7d": "weekly reset", release: "block lifted" },
376
+ workers: "Accounts",
377
+ workersAria: (name, r, n) => `${name} accounts: ${r} of ${n} available`,
378
+ reason: {
379
+ weekly: "weekly limit",
380
+ "5h": "5-hour limit",
381
+ model: "model limit",
382
+ cooldown: "cooldown",
383
+ paused: "paused",
384
+ auth: "auth failed",
385
+ held: "held",
386
+ noMeasure: "no measure",
387
+ unknownTier: "no tier"
388
+ },
389
+ stateWord: {
390
+ available: "available",
391
+ limited: "limited",
392
+ paused: "paused",
393
+ spent: "used up",
394
+ unmeasured: "5h unmeasured",
395
+ excluded: "excluded"
396
+ },
397
+ noReturn: "no return time",
398
+ okFold: (n) => `● Available ${n}`,
399
+ okFoldAria: (n) => `${n} available accounts — show list`,
400
+ tierAssumed: (t) => `${t} assumed`,
401
+ noTier: "no tier",
402
+ card: {
403
+ aria: (n) => `${n} details`,
404
+ close: "Close",
405
+ now: "Available now",
406
+ week: "Left this week",
407
+ binds: "Binds first",
408
+ h5: "5-hour",
409
+ wk: "Weekly",
410
+ state: "State",
411
+ returns: "Returns",
412
+ estimated: "Estimated",
413
+ estimatedSay: "last reset counted as full",
414
+ okTitle: "Available",
415
+ okAria: "Available accounts",
416
+ okKind: (p, n) => `${p} · ${n} ${n === 1 ? "account" : "accounts"}`,
417
+ okRow: (now, wk) => `now ${now} · week ${wk}`
418
+ },
419
+ tl: {
420
+ h5: "5-hour reset",
421
+ wk: "Weekly reset",
422
+ note: "Returning over the next 7 days (accounts)",
423
+ none5: " — nothing within 7 days",
424
+ small5: (g) => ` — ${g} within 7 days`,
425
+ aria: "Accounts returning over the next 7 days",
426
+ axis: ["now", "1h", "6h", "24h", "3d", "5d", "7d"],
427
+ mark: (w, gain, when, names) => `${w} +${gain} accounts · ${when}${names ? ` · ${names}` : ""}`,
428
+ lim: (until) => `All limited — until ${until}`,
429
+ limNone: "All limited — no return within 7 days"
430
+ },
431
+ news: {
432
+ allLimited: (n, back) => `${n}: all accounts limited${back ? ` — ${back}` : ""}`,
433
+ empty: (n, back) => `${n}: nothing usable${back ? ` — ${back}` : ""}`,
434
+ again: (n, r, total) => `${n} available again — ${r}/${total} accounts`,
435
+ low: (n, wk, p, v, t) => `${n} below ${p}% ${wk ? "this week" : "now"} — ${v}/${t} accounts`,
436
+ fail: (n, err) => `Cron ${n} failed${err ? `: ${err}` : ""}`,
437
+ wait: (n) => `Cron ${n} needs a reply`
438
+ },
439
+ // Lane
440
+ laneAria: (t) => `${t} lane`,
441
+ running: (n) => `● Running ${n}`,
442
+ waiting: (n) => `▲ Needs reply ${n}`,
443
+ stat: {
444
+ next: "Next run",
445
+ running: "Running",
446
+ runs: "Runs in 24h",
447
+ fails: "Failures · 24h",
448
+ nextTitle: (name, at) => `Soonest next run — ${name} at ${at}`,
449
+ none: "Nothing scheduled",
450
+ runningTitle: (r, n) => `${r} of ${n} jobs have a running session now`,
451
+ unknown: (u) => `+${u} unknown`,
452
+ runsTitle: (n, ok, f, run) => `${n} run records in the last 24 h: ${ok} ok · ${f} failed · ${run} in progress`,
453
+ unknownTitle: (u) => ` · ${u} with an unknown result not counted`,
454
+ failsTitle: "Failed run records in the last 24 h",
455
+ cut: (name, cap, from) => `${name} keeps ${cap} runs, so its history starts at ${from}`
456
+ },
457
+ queue: {
458
+ head: "Up next",
459
+ headTitle: "Jobs with a next run, soonest first",
460
+ aria: "Next runs in order",
461
+ empty: "Nothing scheduled",
462
+ said: (name, when, i, n) => `${name}, ${when}, ${i} of ${n} — show past runs`
463
+ },
464
+ job: {
465
+ past: (n) => `Past runs ${n}`,
466
+ next: (clock) => `next ${clock} (`,
467
+ exprTitle: (e) => `${e} — cron expression (UTC)`,
468
+ strip: (name, k, sum) => `${name}: last ${k} runs${sum ? ` (${sum})` : ""} — show past runs`,
469
+ noRuns: "No runs yet",
470
+ noRecords: "No run records",
471
+ running: "● Running",
472
+ waiting: "▲ Needs reply"
473
+ },
474
+ run: {
475
+ ok: "OK",
476
+ running: "Running",
477
+ waiting: "Needs reply",
478
+ failed: "Failed",
479
+ unknown: "Result unknown"
480
+ },
481
+ runCount: { ok: "ok", run: "in progress", fail: "failed", unknown: "unknown" },
482
+ runSum: (n, word) => `${n} ${word}`,
483
+ chart: {
484
+ title: "Runs · 24 hours",
485
+ ok: "OK",
486
+ run: "In progress",
487
+ fail: "Failed",
488
+ okTitle: "Success record, session idle",
489
+ runTitle: "Recorded run whose session is running or waiting now",
490
+ failTitle: "Failed record",
491
+ axis: ["24h ago", "12h ago", "now"],
492
+ col: (at, ok, f, r) => `1 h from ${at} · ok ${ok} · failed ${f} · in progress ${r}`,
493
+ said: (n, ok, f, r) => `${n} cron run records in the last 24 hours: ${ok} ok, ${f} failed, ${r} in progress`,
494
+ peak: (at, n) => `, busiest hour from ${at} with ${n}`,
495
+ cut: (at) => ` Before ${at} some job histories are truncated.`,
496
+ unknown: (u) => ` ${u} runs with an unknown result are not counted.`,
497
+ hatch: (names) => `${names} history is truncated here — counts may be low`
498
+ }
499
+ };
500
+ const ko = {
501
+ dur: (s) => {
502
+ if (s < 60) return `${s}초`;
503
+ const m = Math.floor(s / 60);
504
+ const h = Math.floor(m / 60);
505
+ if (h >= 24) return `${Math.floor(h / 24)}일 ${h % 24}시간`;
506
+ if (h) return `${h}시간 ${m % 60}분`;
507
+ return `${m}분`;
508
+ },
509
+ soon: "곧",
510
+ in: (x) => `${x} 후`,
511
+ back: (x) => `${x} 후 복귀`,
512
+ backSoon: "곧 복귀",
513
+ runs: (x) => `${x} 후 실행`,
514
+ runsSoon: "곧 실행",
515
+ since: (x) => `${x} 전`,
516
+ fetched: (s) => s < 60 ? `${s}초 전 갱신` : `${Math.floor(s / 60)}분 전 갱신`,
517
+ age: (s) => {
518
+ const m = Math.floor(s / 60);
519
+ const h = Math.floor(m / 60);
520
+ return h >= 24 ? `${Math.floor(h / 24)}일` : h ? `${h}시간` : `${Math.max(1, m)}분`;
521
+ },
522
+ capTitle: "LLM 가용량",
523
+ capError: (r) => `가용량을 불러오지 못했습니다 — ${r}`,
524
+ capNone: "풀에 계정이 없습니다",
525
+ capAria: (n) => `${n} 가용량`,
526
+ source: (src, n) => `${src ? `${src} · ` : ""}계정 ${n}`,
527
+ pill: {
528
+ unknown: "◌ 계산 불가",
529
+ allLimited: "✗ 모두 막힘",
530
+ empty: "✗ 소진",
531
+ limited: (n) => `⏸ 제한 ${n}`,
532
+ low: "▲ 부족",
533
+ ok: "● 사용 가능"
534
+ },
535
+ disc: {
536
+ reflected: (c, n) => `${c}/${n} 반영`,
537
+ blocked: (b) => `(차단 ${b} 포함)`,
538
+ excluded: (n, why) => `제외 ${n}(${why})`,
539
+ held: "보류",
540
+ unknownTier: "티어 미지정",
541
+ noMeasure: "측정 없음",
542
+ assumed: (n, tier) => `티어 미지정 ${n}(${tier} 가정)`,
543
+ estimated: (n) => `추정 ${n}`,
544
+ unmeasured: (n) => `5시간 측정 없음 ${n}`,
545
+ measured: (a) => `측정 ${a} 전`
546
+ },
547
+ bar: {
548
+ fiveHour: "5시간",
549
+ fiveHourSub: "지금 쓸 수 있음",
550
+ weekly: "일주일",
551
+ weeklySub: "이번 주 남음",
552
+ of: (total) => `/ ${total} 계정`,
553
+ said: (label, v, total, pct) => `${label} ${v} / ${total} 계정, ${pct}`,
554
+ low: (p) => `${p}% 아래면 부족`,
555
+ next: (gain) => `다음 +${gain}계정 · `,
556
+ heldByWeekly: "일주일 한도에 묶임 · ",
557
+ backWithin: (g) => `7일 안 ${g} 회복`,
558
+ nothing: "7일 안에 늘어날 양 없음",
559
+ unknown: "계산 불가 — 셀 수 있는 계정이 없음"
560
+ },
561
+ gain: (x, f) => x <= 1e-7 ? "+0" : x < 0.05 ? "+0.1계정 미만" : `+${f(x)}계정`,
562
+ cause: { "5h": "5시간 리셋", "7d": "일주일 리셋", release: "차단 해제" },
563
+ workers: "계정",
564
+ workersAria: (name, r, n) => `${name} 계정 ${r}/${n} 사용 가능`,
565
+ reason: {
566
+ weekly: "주간 한도",
567
+ "5h": "5시간 한도",
568
+ model: "모델 한도",
569
+ cooldown: "쿨다운",
570
+ paused: "일시정지",
571
+ auth: "인증 실패",
572
+ held: "보류",
573
+ noMeasure: "측정 없음",
574
+ unknownTier: "티어 미지정"
575
+ },
576
+ stateWord: {
577
+ available: "사용 가능",
578
+ limited: "차단",
579
+ paused: "보류",
580
+ spent: "소진",
581
+ unmeasured: "5시간 미측정",
582
+ excluded: "제외"
583
+ },
584
+ noReturn: "복귀 약속 없음",
585
+ okFold: (n) => `● 사용 가능 ${n}`,
586
+ okFoldAria: (n) => `사용 가능 계정 ${n}개 — 목록 보기`,
587
+ tierAssumed: (t) => `${t} 가정`,
588
+ noTier: "티어 미지정",
589
+ card: {
590
+ aria: (n) => `${n} 세부`,
591
+ close: "닫기",
592
+ now: "지금 쓸 수 있음",
593
+ week: "이번 주 남음",
594
+ binds: "먼저 닿는 창",
595
+ h5: "5시간",
596
+ wk: "일주일",
597
+ state: "상태",
598
+ returns: "복귀",
599
+ estimated: "추정",
600
+ estimatedSay: "지난 리셋을 가득으로 침",
601
+ okTitle: "사용 가능",
602
+ okAria: "사용 가능 계정 세부",
603
+ okKind: (p, n) => `${p} · 계정 ${n}`,
604
+ okRow: (now, wk) => `지금 ${now} · 이번 주 ${wk}`
605
+ },
606
+ tl: {
607
+ h5: "5시간 리셋",
608
+ wk: "일주일 리셋",
609
+ note: "앞으로 7일 동안 돌아오는 양(계정)",
610
+ none5: " — 7일 안에 늘어날 양 없음",
611
+ small5: (g) => ` — 7일 안 ${g}`,
612
+ aria: "앞으로 7일 동안 돌아오는 계정",
613
+ axis: ["지금", "1h", "6h", "24h", "3d", "5d", "7d"],
614
+ mark: (w, gain, when, names) => `${w} +${gain}계정 · ${when}부터${names ? ` · ${names}` : ""}`,
615
+ lim: (until) => `모두 막힘 — ${until}까지`,
616
+ limNone: "모두 막힘 — 7일 안에 복귀 없음"
617
+ },
618
+ news: {
619
+ allLimited: (n, back) => `${n} 계정이 모두 막힘${back ? ` — ${back}` : ""}`,
620
+ empty: (n, back) => `${n} 쓸 수 있는 양 없음${back ? ` — ${back}` : ""}`,
621
+ again: (n, r, total) => `${n} 다시 사용 가능 — 계정 ${r}/${total}`,
622
+ low: (n, wk, p, v, t) => `${n} ${wk ? "이번 주" : "지금"} ${p}% 아래 — ${v}/${t} 계정`,
623
+ fail: (n, err) => `크론 ${n} 실패${err ? `: ${err}` : ""}`,
624
+ wait: (n) => `크론 ${n} 응답 필요`
625
+ },
626
+ laneAria: (t) => `${t} 레인`,
627
+ running: (n) => `● 실행 중 ${n}`,
628
+ waiting: (n) => `▲ 응답 필요 ${n}`,
629
+ stat: {
630
+ next: "다음 실행",
631
+ running: "실행 중",
632
+ runs: "24시간 실행 기록",
633
+ fails: "실패 · 24시간",
634
+ nextTitle: (name, at) => `가장 이른 다음 실행 — ${name} ${at}`,
635
+ none: "예정 없음",
636
+ runningTitle: (r, n) => `잡 ${n}개 중 지금 세션이 실행 중인 잡 ${r}개`,
637
+ unknown: (u) => `+미확인 ${u}`,
638
+ runsTitle: (n, ok, f, run) => `지난 24시간 실행 기록 ${n}건: 성공(세션 쉼) ${ok} · 실패 ${f} · 진행 중(세션이 지금 실행 중·응답 대기) ${run}`,
639
+ unknownTitle: (u) => ` · 결과 미확인 ${u}건은 세지 않음`,
640
+ failsTitle: "지난 24시간 실패 기록",
641
+ cut: (name, cap, from) => `${name} 기록은 보관 한도 ${cap}회라 ${from}부터`
642
+ },
643
+ queue: {
644
+ head: "다음 실행",
645
+ headTitle: "다음 실행이 있는 잡, 이른 순",
646
+ aria: "다음 실행 순서",
647
+ empty: "예정 없음",
648
+ said: (name, when, i, n) => `${name}, ${when}, ${i}/${n} — 지난 실행 보기`
649
+ },
650
+ job: {
651
+ past: (n) => `지난 실행 ${n}`,
652
+ next: (clock) => `다음 ${clock} (`,
653
+ exprTitle: (e) => `${e} — cron 식은 UTC 기준`,
654
+ strip: (name, k, sum) => `${name} 최근 실행 ${k}회${sum ? `(${sum})` : ""} — 지난 실행 보기`,
655
+ noRuns: "아직 실행 없음",
656
+ noRecords: "실행 기록 없음",
657
+ running: "● 실행 중",
658
+ waiting: "▲ 응답 필요"
659
+ },
660
+ run: { ok: "성공", running: "실행 중", waiting: "응답 필요", failed: "실패", unknown: "결과 미확인" },
661
+ runCount: { ok: "성공", run: "진행 중", fail: "실패", unknown: "미확인" },
662
+ runSum: (n, word) => `${word} ${n}`,
663
+ chart: {
664
+ title: "실행 기록 · 24시간",
665
+ ok: "성공",
666
+ run: "진행 중",
667
+ fail: "실패",
668
+ okTitle: "성공 기록, 세션 쉼",
669
+ runTitle: "실행 기록, 세션이 지금 실행 중·응답 대기",
670
+ failTitle: "실패 기록",
671
+ axis: ["24시간 전", "12시간 전", "지금"],
672
+ col: (at, ok, f, r) => `${at}부터 1시간 · 성공 ${ok} · 실패 ${f} · 진행 중 ${r}`,
673
+ said: (n, ok, f, r) => `지난 24시간 크론 실행 기록 ${n}건: 성공 ${ok}건, 실패 ${f}건, 진행 중 ${r}건`,
674
+ peak: (at, n) => `, 가장 많은 1시간 ${at}부터 ${n}건`,
675
+ cut: (at) => ` ${at} 이전은 일부 잡 기록이 잘림.`,
676
+ unknown: (u) => ` 결과 미확인 ${u}건은 세지 않음.`,
677
+ hatch: (names) => `이 구간은 ${names} 기록이 보관 한도로 잘려 실제보다 적을 수 있음`
678
+ }
679
+ };
680
+ const STRINGS = { en, ko };
681
+ function timeText(kind, atMs, nowMs, t) {
682
+ if (kind === "since") return t.since(spanShort((nowMs - atMs) / 1e3));
683
+ if (kind === "fetched") return t.fetched(Math.max(0, Math.round((nowMs - atMs) / 1e3)));
684
+ const s = Math.floor((atMs - nowMs) / 1e3);
685
+ if (!(s > 0)) return kind === "back" ? t.backSoon : kind === "runs" ? t.runsSoon : t.soon;
686
+ const d = t.dur(s);
687
+ return kind === "in" ? t.in(d) : kind === "back" ? t.back(d) : kind === "runs" ? t.runs(d) : d;
688
+ }
689
+ const styles = `
690
+ :host {
691
+ --aq-now: var(--ui-aq-now, color-mix(in oklch, var(--green) 55%, var(--accent)));
692
+ --aq-wk: var(--ui-aq-wk, var(--purple));
693
+ --aq-warn: var(--st-wait);
694
+ --aq-warn-text: color-mix(in srgb, var(--st-wait) 75%, var(--text));
695
+ }
696
+ .aq { display: grid; gap: 12px; min-width: 0; }
697
+
698
+ /* ── Shared grammar: pill, countdown, column head ── */
699
+ .aq-head { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 10px; min-width: 0; }
700
+ .aq-name { font-size: 12px; color: var(--ib-muted); white-space: nowrap; }
701
+ .aq-pill { display: inline-flex; align-items: center; gap: 4px; min-height: 22px; padding: 2px 9px; border-radius: 999px; font-size: 12px; font-weight: 600; line-height: 1.3; white-space: nowrap; font-variant-numeric: tabular-nums; color: var(--aq-pc, var(--ib-text-2)); background: color-mix(in srgb, var(--aq-pc, var(--ib-muted)) 14%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--aq-pc, var(--ib-muted)) 45%, transparent); }
702
+ .aq-pill.is-ok { --aq-pc: var(--st-done); }
703
+ .aq-pill.is-run { --aq-pc: var(--st-run); }
704
+ .aq-pill.is-paused, .aq-pill.is-low, .aq-pill.is-wait { --aq-pc: var(--aq-warn); color: var(--aq-warn-text); }
705
+ .aq-pill.is-empty { --aq-pc: var(--st-fail); color: var(--st-fail-text); }
706
+ .aq-pill.is-unknown { --aq-pc: var(--ib-muted); }
707
+ .aq-left, .aq-ago { font-variant-numeric: tabular-nums; }
708
+ .aq-col-head { display: flex; align-items: baseline; gap: 6px; font-size: 12px; color: var(--ib-muted); }
709
+ .aq-col-head b { color: var(--ib-text); font-weight: 600; font-variant-numeric: tabular-nums; }
710
+
711
+ /* ── LLM capacity: colour is the window (teal = now · 5h, violet = this week · 7d) ── */
712
+ .capacity { background: var(--ib-col); border-radius: 14px; padding: 12px 14px 10px; color: var(--ib-text); min-width: 0; }
713
+ .cap-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px 12px; margin-bottom: 4px; }
714
+ .cap-title { margin: 0; font-size: 12px; font-weight: 500; letter-spacing: 0.14em; text-transform: uppercase; color: var(--ib-muted); }
715
+ .cap-sub { font-size: 12px; color: var(--ib-text-2); }
716
+ .cap-off { font-size: 13px; color: var(--ib-text-2); padding: 8px 0 10px; }
717
+ .cap-prov { container: aqprov / inline-size; position: relative; background: var(--ib-card); border-radius: 12px; padding: 14px 18px 12px; margin-top: 10px; box-shadow: inset 0 0 0 1px var(--ib-card-border); min-width: 0; }
718
+ .cap-prov.is-empty { box-shadow: inset 0 0 0 1px var(--st-fail); }
719
+ .cap-prov.is-low { box-shadow: inset 0 0 0 1px var(--st-wait); }
720
+ .cap-pname { margin: 0; font-size: 16px; font-weight: 700; color: var(--ib-text); }
721
+ .cap-disc { font-size: 12px; color: var(--ib-muted); margin: 2px 0 8px; }
722
+ .cap-disc .cap-stale { color: var(--aq-warn-text); font-weight: 600; }
723
+ /* One bar per window — label | bar | "x / total accounts %" — and a "next …" line. */
724
+ .cap-bar-row { display: grid; grid-template-columns: 92px minmax(0, 1fr) 200px; align-items: center; gap: 4px 12px; margin: 8px 0; }
725
+ .cap-bar-lbl { font-size: 14px; font-weight: 700; line-height: 1.2; }
726
+ .cap-bar-lbl i { display: block; font-style: normal; font-weight: 400; font-size: 11px; color: var(--ib-muted); }
727
+ .cap-bar-row.is-h5 .cap-bar-lbl { color: var(--aq-now); }
728
+ .cap-bar-row.is-wk .cap-bar-lbl { color: var(--aq-wk); }
729
+ .cap-bar { position: relative; height: 26px; border-radius: 7px; background: var(--ib-pill); overflow: hidden; }
730
+ .cap-bar > span { display: block; height: 100%; border-radius: 7px; transition: width var(--dur-move) var(--ease-out); }
731
+ .cap-bar-row.is-h5 .cap-bar > span { background: var(--aq-now); }
732
+ .cap-bar-row.is-wk .cap-bar > span { background: var(--aq-wk); }
733
+ /* The dashed line is where the pool turns low (the reference's backpressure line). */
734
+ .cap-low { position: absolute; top: 0; bottom: 0; width: 0; border-left: 1px dashed var(--ib-text-2); opacity: 0.7; }
735
+ .cap-bar-val { font-size: 14px; color: var(--ib-text-2); white-space: nowrap; font-variant-numeric: tabular-nums; }
736
+ .cap-bar-val b { font-size: 19px; color: var(--ib-text); }
737
+ .cap-bar-val em { font-style: normal; font-weight: 600; margin-left: 4px; }
738
+ .cap-bar-next { grid-column: 2 / 4; font-size: 12px; color: var(--ib-text-2); }
739
+ .cap-prov.is-empty .cap-bar-row.is-h5 .cap-bar-next { color: var(--st-fail-text); font-weight: 600; }
740
+ .cap-tl-wrap { margin-top: 10px; border-top: 1px solid var(--ib-card-border); padding-top: 8px; }
741
+ .cap-tl-h { display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: 12px; color: var(--ib-text-2); }
742
+ .cap-tl-h i { font-style: normal; }
743
+ .cap-tl-h .is-h5 { color: var(--aq-now); }
744
+ .cap-tl-h .is-wk { color: var(--aq-wk); }
745
+ .cap-tl { display: block; max-width: 100%; height: auto; overflow: visible; }
746
+ .cap-tl text { font-family: inherit; }
747
+ .cap-tl-axis { stroke: var(--ib-muted); stroke-opacity: 0.5; stroke-width: 2; }
748
+ .cap-tl-tick { stroke: var(--ib-card-border); }
749
+ .cap-tl-ax { fill: var(--ib-muted); font-size: 11px; }
750
+ .cap-tl-mk.is-h5 { fill: var(--aq-now); stroke: var(--aq-now); }
751
+ .cap-tl-mk.is-wk { fill: var(--aq-wk); stroke: var(--aq-wk); }
752
+ .cap-tl-mk circle, .cap-tl-mk rect { stroke: var(--ib-card); stroke-width: 1.5; }
753
+ .cap-tl-span { stroke-width: 5; stroke-linecap: round; }
754
+ .cap-tl-mk text { stroke: none; font-size: 12.5px; font-weight: 700; }
755
+ .cap-tl-lim { fill: var(--st-fail); fill-opacity: 0.12; }
756
+ .cap-tl-hatch { stroke: var(--st-fail); stroke-opacity: 0.4; stroke-width: 1.5; }
757
+ /* Narrow card: label and value share the first line, the bar takes the full width, the next line under it. */
758
+ @container aqprov (max-width: 599.98px) {
759
+ .cap-bar-row { grid-template-columns: minmax(0, 1fr) auto; gap: 4px 8px; margin: 10px 0; }
760
+ .cap-bar-lbl { grid-row: 1; grid-column: 1; align-self: baseline; }
761
+ .cap-bar-lbl i { display: inline; margin-left: 6px; }
762
+ .cap-bar-val { grid-row: 1; grid-column: 2; align-self: baseline; }
763
+ .cap-bar { grid-row: 2; grid-column: 1 / -1; height: 20px; }
764
+ .cap-bar-next { grid-row: 3; grid-column: 1 / -1; }
765
+ .cap-bar-val b { font-size: 16px; }
766
+ .cap-tl-h-note { display: none; }
767
+ }
768
+
769
+ /* ── Accounts as workers: state marks + the pinned account card ── */
770
+ .aq-workers { position: relative; margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--ib-card-border); }
771
+ .aq-marks { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
772
+ .aq-mark { display: inline-flex; align-items: center; gap: 5px; min-height: 28px; max-width: 100%; padding: 0 9px; border-radius: 8px; border: 1px solid var(--ib-card-border); background: var(--ib-card); color: var(--ib-text); font: inherit; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; transition: background-color var(--dur-fade) var(--ease-out), color var(--dur-fade) var(--ease-out), border-color var(--dur-fade) var(--ease-out); }
773
+ .aq-mark b { font-weight: 600; }
774
+ .aq-mark .aq-g { font-size: 10px; }
775
+ .aq-mark.is-limited { color: var(--aq-warn-text); background: color-mix(in srgb, var(--aq-warn) 12%, var(--ib-card)); border-color: color-mix(in srgb, var(--aq-warn) 50%, transparent); }
776
+ .aq-mark.is-spent, .aq-mark.is-unmeasured, .aq-mark.is-paused { color: var(--ib-text-2); background: var(--ib-pill); }
777
+ .aq-mark.is-excluded { color: var(--ib-muted); background: transparent; border-style: dashed; }
778
+ .aq-mark[aria-expanded="true"] { border-color: var(--ib-text-2); }
779
+ .aq-mark:focus-visible, .aq-q:focus-visible, .aq-pop-close:focus-visible, .cron-history-btn:focus-visible, .cron-strip:focus-visible { outline: 2px solid var(--border-focus); outline-offset: 2px; }
780
+ @media (hover: hover) and (pointer: fine) { .aq-mark:hover, .aq-q:hover { border-color: var(--ib-muted); } }
781
+ .aq-wdot { flex: none; width: 7px; height: 7px; border-radius: 50%; }
782
+ .aq-wdot.is-h5 { background: var(--aq-now); }
783
+ .aq-wdot.is-wk { background: var(--aq-wk); }
784
+ .aq-pop { position: absolute; z-index: 6; top: 100%; left: 8px; width: min(280px, calc(100% - 16px)); display: grid; gap: 10px; padding: 12px; border-radius: 12px; background: var(--ib-card); color: var(--ib-text); box-shadow: 0 0 0 1px var(--ib-card-border), var(--ib-shadow-raised); animation: aq-open var(--dur-fade) var(--ease-out); }
785
+ .cap-prov.is-narrow .aq-pop { width: calc(100% - 24px); }
786
+ @keyframes aq-open { from { opacity: 0; transform: translateY(-4px) scale(0.97); } to { opacity: 1; transform: none; } }
787
+ .aq-pop-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
788
+ .aq-pop-name { font-size: 14px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
789
+ .aq-pop-kind { flex: 1 1 auto; min-width: 0; font-size: 12px; color: var(--ib-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
790
+ .aq-pop-close { flex: none; width: 28px; height: 28px; border-radius: 8px; border: 1px solid var(--ib-card-border); background: none; color: var(--ib-text-2); font: inherit; font-size: 16px; line-height: 1; cursor: pointer; }
791
+ .aq-pop-stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 12px; margin: 0; padding: 10px 12px; border-radius: 10px; background: var(--ib-col); }
792
+ .aq-pop-stats dt { font-size: 11px; color: var(--ib-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
793
+ .aq-pop-stats dd { margin: 0; font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums; overflow: hidden; text-overflow: ellipsis; }
794
+ .aq-pop-stats .is-wide { grid-column: 1 / -1; }
795
+
796
+ /* ── CRON lane = the queue ── */
797
+ .aq-lane { container: aqlane / inline-size; background: var(--ib-col); border-radius: 14px; padding: 2px 4px 10px; color: var(--ib-text); min-width: 0; }
798
+ .aq-lane-head { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 8px; padding: 10px 8px 8px 10px; min-height: 40px; }
799
+ .aq-lane-ic { display: inline-flex; color: var(--ib-muted); }
800
+ .aq-lane-ic svg { width: 14px; height: 14px; }
801
+ .aq-lane-title { margin: 0; font-size: 12px; font-weight: 500; letter-spacing: 0.14em; text-transform: uppercase; color: var(--ib-muted); }
802
+ .aq-count { background: var(--ib-pill); color: var(--ib-text-2); border-radius: 6px; padding: 1px 7px; font-size: 12px; font-weight: 500; min-width: 22px; text-align: center; font-variant-numeric: tabular-nums; }
803
+ .aq-lane-top { padding: 0 4px 10px; }
804
+ .aq-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 0; padding: 12px 0; border-radius: 12px; background: var(--ib-card); box-shadow: inset 0 0 0 1px var(--ib-card-border); }
805
+ .aq-stat { display: grid; gap: 2px; min-width: 0; padding: 0 16px; }
806
+ .aq-stat + .aq-stat { border-left: 1px solid var(--ib-card-border); }
807
+ .aq-stat dt { font-size: 12px; color: var(--ib-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
808
+ .aq-stat dd { margin: 0; font-size: 16px; font-weight: 600; color: var(--ib-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-variant-numeric: tabular-nums; }
809
+ .aq-stat[data-kind="failed"] dd { color: var(--st-fail-text); }
810
+ .aq-of { margin-left: 4px; font-size: 13px; font-weight: 500; color: var(--ib-muted); }
811
+ .aq-of.is-n { margin-left: 1px; }
812
+ .aq-lane-body { display: grid; grid-template-columns: clamp(188px, 26%, 236px) minmax(0, 1fr); gap: 10px; align-items: start; padding: 0 4px; }
813
+ .aq-queue { display: flex; flex-direction: column; gap: 6px; min-width: 0; padding: 10px; border-radius: 12px; border: 1px solid var(--ib-card-border); background: color-mix(in srgb, var(--ib-card) 45%, transparent); }
814
+ .aq-q { display: flex; align-items: center; gap: 6px; min-height: 28px; padding: 0 8px; border-radius: 8px; border: 1px solid var(--ib-card-border); background: var(--ib-card); color: var(--ib-text); font: inherit; font-size: 12.5px; text-align: left; cursor: pointer; }
815
+ .aq-q.is-new { animation: aq-in var(--dur-fade) var(--ease-out); }
816
+ .aq-q svg { flex: none; width: 12px; height: 12px; color: var(--ib-muted); }
817
+ .aq-q-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
818
+ .aq-q .aq-left { flex: none; font-size: 11.5px; color: var(--ib-muted); }
819
+ .aq-more { align-self: flex-start; padding: 1px 8px; border-radius: 6px; background: var(--ib-pill); color: var(--ib-text-2); font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums; }
820
+ .aq-empty { padding: 4px 2px; font-size: 12px; color: var(--ib-muted); }
821
+ @keyframes aq-in { from { opacity: 0; } to { opacity: 1; } }
822
+
823
+ /* Job cards (CRON tiles) */
824
+ .cron-groups { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr)); gap: 10px; align-items: start; min-width: 0; }
825
+ .cron-group { position: relative; display: flex; flex-direction: column; gap: 8px; min-width: 0; padding: 10px; border-radius: 12px; border: 1px dashed var(--ib-card-border); background: color-mix(in srgb, var(--ib-card) 45%, transparent); transition: border-color 150ms ease; }
826
+ .cron-group.is-running { border-style: solid; border-color: color-mix(in srgb, var(--st-run) 55%, transparent); }
827
+ .cron-group.is-waiting { border-style: solid; border-color: var(--st-wait); }
828
+ .cron-tile { display: flex; flex-direction: column; gap: 6px; min-width: 0; --st-color: var(--ib-text-2); }
829
+ .cron-group.is-running .cron-tile { --st-color: var(--st-run); }
830
+ .cron-group.is-waiting .cron-tile { --st-color: var(--aq-warn-text); }
831
+ .cron-tile-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
832
+ .cron-badge { flex-shrink: 0; display: inline-flex; align-items: center; gap: 4px; padding: 1px 7px 1px 5px; border-radius: 999px; font-size: 11px; font-weight: 700; letter-spacing: 0.06em; line-height: 1.5; color: var(--st-color, var(--ib-text-2)); background: color-mix(in srgb, var(--st-color, var(--ib-muted)) 14%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--st-color, var(--ib-muted)) 45%, transparent); }
833
+ .cron-badge svg { width: 12px; height: 12px; }
834
+ .cron-name { flex: 1 1 auto; min-width: 0; font-size: 14px; font-weight: 600; color: var(--ib-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
835
+ .cron-history-btn { flex-shrink: 0; margin-left: auto; min-height: 28px; padding: 2px 10px; border-radius: 8px; border: 1px solid var(--ib-card-border); background: var(--ib-card); color: var(--ib-text-2); font: inherit; font-size: 12px; cursor: pointer; transition: transform var(--dur-press) var(--ease-out), border-color 150ms ease; }
836
+ .cron-history-btn:active:not(:focus-visible) { transform: scale(0.97); }
837
+ @media (hover: hover) and (pointer: fine) { .cron-history-btn:hover { border-color: var(--ib-muted); color: var(--ib-text); } }
838
+ .cron-sched { display: flex; flex-wrap: wrap; gap: 2px 10px; font-size: 12px; color: var(--ib-text-2); font-variant-numeric: tabular-nums; }
839
+ .cron-expr { color: var(--ib-text); }
840
+ .cron-strip { display: flex; align-items: flex-end; gap: 3px; height: 22px; padding: 2px 0; border: 0; border-radius: 4px; background: none; cursor: pointer; width: max-content; max-width: 100%; }
841
+ .cron-strip.is-empty { cursor: default; font-size: 12px; color: var(--ib-muted); height: auto; }
842
+ .cr-tick { display: block; flex: none; width: 8px; height: 16px; border-radius: 2px; background: var(--st-done); }
843
+ .cr-tick.is-fail { background: var(--st-fail); height: 18px; }
844
+ .cr-tick.is-none { background: transparent; box-shadow: inset 0 0 0 1.5px var(--ib-muted); }
845
+ .cr-tick.is-run { background: var(--st-run); }
846
+ .cr-tick.is-wait { background: var(--st-wait); }
847
+ @keyframes cr-pulse { 50% { opacity: 0.45; } }
848
+ @media (prefers-reduced-motion: no-preference) { .cr-tick.is-run { animation: cr-pulse 1.6s ease-in-out infinite; } }
849
+ .cron-status { font-size: 12px; color: var(--ib-text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
850
+ .cron-status.is-run { color: var(--st-run); font-weight: 600; }
851
+ .cron-status.is-wait { color: var(--aq-warn-text); font-weight: 600; }
852
+ .cron-status.is-fail { color: var(--st-fail-text); }
853
+
854
+ /* 24 hours of runs */
855
+ .aq-chart { margin: 10px 4px 0; padding: 12px 14px 10px; border-radius: 12px; background: var(--ib-card); box-shadow: inset 0 0 0 1px var(--ib-card-border); }
856
+ .aq-chart-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 8px; margin-bottom: 10px; font-size: 12px; }
857
+ .aq-chart-title { font-weight: 500; color: var(--ib-text); }
858
+ .aq-chart-title + .aq-ch-key { margin-left: auto; }
859
+ .aq-ch-key { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; color: var(--ib-text-2); }
860
+ .aq-ch-key i { width: 8px; height: 8px; border-radius: 2px; }
861
+ .aq-ch-key i.is-ok { background: var(--st-done); }
862
+ .aq-ch-key i.is-run { background: var(--st-run); }
863
+ .aq-ch-key i.is-fail { background: var(--st-fail); }
864
+ .aq-plot { position: relative; height: 112px; margin-left: 26px; display: flex; align-items: stretch; gap: 2px; border-bottom: 1px solid var(--ib-card-border); }
865
+ .aq-ch-grid { position: absolute; left: -26px; right: 0; top: 0; height: 0; border-top: 1px dashed var(--ib-pill); pointer-events: none; }
866
+ .aq-ch-grid.is-zero { top: 100%; border-top: 0; }
867
+ .aq-ch-grid span { position: absolute; left: 0; top: -7px; width: 20px; text-align: right; font-size: 11px; line-height: 14px; color: var(--ib-muted); font-variant-numeric: tabular-nums; }
868
+ .aq-ch-cut { position: absolute; top: 0; bottom: 0; background: repeating-linear-gradient(-45deg, transparent 0 5px, color-mix(in srgb, var(--ib-muted) 22%, transparent) 5px 6px); }
869
+ .aq-ch-col { position: relative; z-index: 1; flex: 1 1 0; min-width: 0; display: flex; flex-direction: column-reverse; }
870
+ .aq-ch-ok, .aq-ch-run, .aq-ch-fail { display: block; transition: height var(--dur-expand) var(--ease-out); }
871
+ .aq-ch-ok { background: var(--st-done); }
872
+ .aq-ch-run { background: var(--st-run); }
873
+ .aq-ch-fail { background: var(--st-fail); border-radius: 2px 2px 0 0; }
874
+ .aq-ch-axis { display: flex; justify-content: space-between; margin: 6px 0 0 26px; font-size: 11px; color: var(--ib-muted); }
875
+
876
+ @container aqlane (max-width: 599.98px) {
877
+ .aq-lane-body { grid-template-columns: minmax(0, 1fr); }
878
+ .aq-plot { height: 88px; }
879
+ }
880
+ @container aqlane (max-width: 560px) {
881
+ .aq-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 12px; }
882
+ .aq-stat { padding: 0 12px; }
883
+ .aq-stat:nth-child(odd) { border-left: 0; }
884
+ .aq-stat dd { font-size: 15px; }
885
+ }
886
+ @media (pointer: coarse) {
887
+ .aq-mark, .aq-q, .cron-history-btn, .cron-strip { min-height: 44px; }
888
+ .aq-pop-close { width: 44px; height: 44px; }
889
+ }
890
+ @media (prefers-reduced-motion: reduce) {
891
+ .aq-mark, .aq-q, .aq-pop, .cap-bar > span, .aq-ch-ok, .aq-ch-run, .aq-ch-fail, .cr-tick { animation: none; transition: none; }
892
+ }
893
+ `;
894
+ const CLOCK_SVG = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><circle cx="8" cy="8" r="6"/><path d="M8 4.8V8l2.2 1.4"/></svg>';
895
+ const RUN_STATUSES = ["ok", "running", "waiting", "failed", "unknown"];
896
+ const GLYPH = { ok: "✓", running: "●", waiting: "▲", failed: "✗", unknown: "◌" };
897
+ const TICK = {
898
+ ok: "ok",
899
+ running: "run",
900
+ waiting: "wait",
901
+ failed: "fail",
902
+ unknown: "none"
903
+ };
904
+ const COUNTDOWN = /* @__PURE__ */ new Set(["left", "in", "back", "runs"]);
905
+ const statusOf = (r) => RUN_STATUSES.includes(r.status) ? r.status : "unknown";
906
+ const pill = (tone, html) => `<span class="aq-pill is-${tone}">${html}</span>`;
907
+ function hashId(s) {
908
+ let h = 2166136261;
909
+ for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 16777619);
910
+ return (h >>> 0).toString(36);
911
+ }
912
+ const keyOf = (kind, ...ids) => `${kind}-${ids.map((x) => `${hashId(x)}${x.length.toString(36)}`).join("-")}`;
913
+ const accKey = (pid, aid) => aid === null ? keyOf("ok", pid) : keyOf("acc", pid, aid);
914
+ function capacityNews(prev, next, nowMs, t = STRINGS.en) {
915
+ if (!prev || !next) return "";
916
+ const said = [];
917
+ for (const g0 of next) {
918
+ const p0 = prev.find((x) => x.id === g0.id);
919
+ if (!p0) continue;
920
+ const g = advanceProvider(g0, nowMs);
921
+ const ns = poolState(g);
922
+ const ps = poolState(advanceProvider(p0, nowMs));
923
+ if (ns === ps) continue;
924
+ const name = plain(g.name, 40);
925
+ if (ns === "empty") {
926
+ const at = emptyReturnAt(g, nowMs);
927
+ const back = at ? timeText("back", at, nowMs, t) : "";
928
+ said.push([1, limitedAccounts(g).length ? t.news.allLimited(name, back) : t.news.empty(name, back)]);
929
+ } else if (ps === "empty" && ns !== "unknown") {
930
+ const acc = g.accounts ?? [];
931
+ said.push([4, t.news.again(name, acc.filter((a) => a.state === "available").length, acc.length)]);
932
+ } else if (ps === "ok" && ns === "low") {
933
+ const k = primaryKey(g);
934
+ const v = Number(g[k]) || 0;
935
+ said.push([5, t.news.low(name, k === "weekly", LOW_PCT, fmtCount(v, g.total), fmt1(g.total))]);
936
+ }
937
+ }
938
+ said.sort((a, b) => a[0] - b[0]);
939
+ return said[0]?.[1] ?? "";
940
+ }
941
+ function laneNews(prev, next, t = STRINGS.en) {
942
+ if (!prev || !next) return "";
943
+ let fail = "";
944
+ let wait = "";
945
+ for (const j of next.jobs ?? []) {
946
+ const p = (prev.jobs ?? []).find((x) => x.id === j.id);
947
+ if (!p) continue;
948
+ const nr = jobRuns(j)[0];
949
+ const pr = jobRuns(p)[0];
950
+ if (!fail && nr && nr.status === "failed" && nr.at > (pr?.at ?? 0))
951
+ fail = t.news.fail(plain(j.name, 60), plain(nr.error, 80));
952
+ if (!wait && j.state === "waiting" && p.state !== "waiting") wait = t.news.wait(plain(j.name, 60));
953
+ }
954
+ return fail || wait;
955
+ }
956
+ class UiAgentQueue extends UiElement {
957
+ static styles = [styles];
958
+ static get observedAttributes() {
959
+ return ["lang"];
960
+ }
961
+ #dirty = true;
962
+ #m;
963
+ #skew = 0;
964
+ #open = null;
965
+ #stops = /* @__PURE__ */ new Map();
966
+ #width = 0;
967
+ #ro = null;
968
+ #rz = 0;
969
+ #seenQ = /* @__PURE__ */ new Set();
970
+ #tick = 0;
971
+ #say = 0;
972
+ #live;
973
+ constructor() {
974
+ super();
975
+ this.#live = document.createElement("div");
976
+ this.#live.className = "sr-only";
977
+ this.#live.setAttribute("aria-live", "polite");
978
+ this.#live.setAttribute("aria-atomic", "true");
979
+ this.root.append(this.#live);
980
+ this.root.addEventListener("click", (e) => this.#onClick(e));
981
+ this.root.addEventListener("keydown", (e) => this.#onKey(e));
982
+ this.root.addEventListener("focusin", (e) => this.#onFocus(e));
983
+ }
984
+ get data() {
985
+ return super.data;
986
+ }
987
+ set data(value) {
988
+ this.#dirty = true;
989
+ super.data = value;
990
+ }
991
+ attributeChangedCallback() {
992
+ this.requestRender();
993
+ }
994
+ connectedCallback() {
995
+ super.connectedCallback();
996
+ clearInterval(this.#tick);
997
+ this.#tick = window.setInterval(() => this.#tickNow(), 1e3);
998
+ if (!this.#ro && typeof ResizeObserver !== "undefined") {
999
+ this.#ro = new ResizeObserver((entries) => {
1000
+ const w = entries[0]?.contentRect.width ?? 0;
1001
+ if (Math.abs(w - this.#width) < 4) return;
1002
+ clearTimeout(this.#rz);
1003
+ this.#rz = window.setTimeout(
1004
+ () => {
1005
+ this.#width = w;
1006
+ this.requestRender();
1007
+ },
1008
+ this.#width > 0 ? 120 : 0
1009
+ );
1010
+ });
1011
+ }
1012
+ this.#ro?.observe(this);
1013
+ document.addEventListener("click", this.#onDocClick, true);
1014
+ }
1015
+ disconnectedCallback() {
1016
+ clearInterval(this.#tick);
1017
+ this.#tick = 0;
1018
+ clearTimeout(this.#say);
1019
+ clearTimeout(this.#rz);
1020
+ this.#ro?.disconnect();
1021
+ document.removeEventListener("click", this.#onDocClick, true);
1022
+ }
1023
+ get #lang() {
1024
+ return this.getAttribute("lang") === "ko" ? "ko" : "en";
1025
+ }
1026
+ get #t() {
1027
+ return STRINGS[this.#lang];
1028
+ }
1029
+ #now() {
1030
+ return Date.now() + this.#skew;
1031
+ }
1032
+ // ── Data in ───────────────────────────────────────────────────────────────────────────────
1033
+ #ingest(d) {
1034
+ const next = normalize(d);
1035
+ this.#skew = next.now !== void 0 ? next.now - Date.now() : 0;
1036
+ const prev = this.#m;
1037
+ this.#m = next;
1038
+ const o = this.#open;
1039
+ if (o) {
1040
+ const p = next.providers?.find((x) => x.id === o.pid);
1041
+ if (!p || o.aid !== null && !p.accounts?.some((a) => a.id === o.aid)) this.#open = null;
1042
+ }
1043
+ if (prev) {
1044
+ const now = this.#now();
1045
+ const msg = [capacityNews(prev.providers, next.providers, now, this.#t), laneNews(prev.lane, next.lane, this.#t)].filter(Boolean).join(" · ");
1046
+ if (msg) this.#announce(msg);
1047
+ }
1048
+ }
1049
+ /** Cleared first, written 60 ms later: the same sentence twice is still spoken. textContent only. */
1050
+ #announce(msg) {
1051
+ clearTimeout(this.#say);
1052
+ this.#live.textContent = "";
1053
+ this.#say = window.setTimeout(() => {
1054
+ this.#live.textContent = msg;
1055
+ }, 60);
1056
+ }
1057
+ // ── Render ────────────────────────────────────────────────────────────────────────────────
1058
+ render() {
1059
+ const d = this.data;
1060
+ if (!d) return "";
1061
+ if (this.#dirty || !this.#m) {
1062
+ this.#dirty = false;
1063
+ this.#ingest(d);
1064
+ }
1065
+ const m = this.#m;
1066
+ const t = this.#t;
1067
+ const now = this.#now();
1068
+ let h = "";
1069
+ if (m.providers || m.capacityError !== void 0) h += this.#capacity(m, now, t);
1070
+ if (m.lane) h += this.#lane(m.lane, now, t);
1071
+ return h ? `<div class="aq">${h}</div>` : "";
1072
+ }
1073
+ /** A live time element; the 1 Hz tick rewrites its text in place. */
1074
+ #time(kind, at, now, t, cls = "aq-left") {
1075
+ if (at === void 0 || !Number.isFinite(at)) return "";
1076
+ const cd = COUNTDOWN.has(kind);
1077
+ return `<time class="${cls}"${cd ? ' role="timer"' : ""} datetime="${new Date(at).toISOString()}" data-at="${Math.round(at)}" data-fmt="${kind}"${cd && at <= now ? " data-due" : ""}>${esc(timeText(kind, at, now, t))}</time>`;
1078
+ }
1079
+ // ── Capacity ──────────────────────────────────────────────────────────────────────────────
1080
+ #capacity(m, now, t) {
1081
+ const sub = [m.capacityNote ? esc(m.capacityNote) : "", this.#time("fetched", m.fetchedAt, now, t, "aq-ago")].filter(Boolean).join(" · ");
1082
+ const head = `<div class="cap-head"><h2 class="cap-title">${esc(t.capTitle)}</h2>${sub ? `<span class="cap-sub">${sub}</span>` : ""}</div>`;
1083
+ const open = `<section class="capacity" aria-label="${escAttr(t.capTitle)}">${head}`;
1084
+ if (m.capacityError !== void 0)
1085
+ return `${open}<div class="cap-off">${esc(t.capError(m.capacityError || "—"))}</div></section>`;
1086
+ const ps = m.providers ?? [];
1087
+ if (!ps.length) return `${open}<div class="cap-off">${esc(t.capNone)}</div></section>`;
1088
+ const width = this.#width > 0 ? this.#width : this.clientWidth > 0 ? this.clientWidth : 900;
1089
+ const W = Math.max(260, Math.round(width) - 28 - 36);
1090
+ const narrow = W < 600;
1091
+ return `${open}<div class="cap-rows">${ps.map((p, i) => this.#provider(advanceProvider(p, now), i, now, t, W, narrow)).join("")}</div></section>`;
1092
+ }
1093
+ #provider(p, idx, now, t, W, narrow) {
1094
+ const state = poolState(p);
1095
+ const n = p.accountCount ?? p.accounts?.length ?? 0;
1096
+ const who = p.source || n ? `<span class="aq-name">${esc(t.source(p.source ?? "", n))}</span>` : "";
1097
+ let h = `<section class="cap-prov is-${state}${narrow ? " is-narrow" : ""}" aria-label="${escAttr(t.capAria(p.name))}">`;
1098
+ h += `<div class="aq-head"><h3 class="cap-pname">${esc(p.name)}</h3>${who}${this.#provPill(p, state, now, t)}</div>`;
1099
+ const disc = this.#disclosure(p, now, t);
1100
+ if (disc) h += `<div class="cap-disc">${disc}</div>`;
1101
+ const workers = this.#workers(p, now, t, narrow);
1102
+ if (!(Number(p.total) > 0)) return `${h}<div class="cap-bar-next">${esc(t.bar.unknown)}</div>${workers}</section>`;
1103
+ if (has5h(p)) h += this.#bar(p, "fiveHour", now, t);
1104
+ if (p.weekly !== void 0 && p.weekly !== null) h += this.#bar(p, "weekly", now, t);
1105
+ h += workers;
1106
+ h += this.#timeline(p, idx, state, W, now, t);
1107
+ return `${h}</section>`;
1108
+ }
1109
+ /** The provider pill (D8): can't compute > all limited / used up > limited n > low > available. */
1110
+ #provPill(p, state, now, t) {
1111
+ const lim = limitedAccounts(p).length;
1112
+ if (state === "unknown" || !(Number(p.total) > 0)) return pill("unknown", esc(t.pill.unknown));
1113
+ if (state === "empty") {
1114
+ const at = emptyReturnAt(p, now);
1115
+ return pill(
1116
+ "empty",
1117
+ esc(lim ? t.pill.allLimited : t.pill.empty) + (at ? ` · ${this.#time("back", at, now, t)}` : "")
1118
+ );
1119
+ }
1120
+ if (lim) {
1121
+ const back = firstReturn(p);
1122
+ return pill("paused", esc(t.pill.limited(lim)) + (back ? ` · ${this.#time("back", back, now, t)}` : ""));
1123
+ }
1124
+ if (state === "low") return pill("low", esc(t.pill.low));
1125
+ return pill("ok", esc(t.pill.ok));
1126
+ }
1127
+ /** One muted line, only what is non-zero: reflected / limited, exclusions, estimates, the measurement's age. */
1128
+ #disclosure(p, now, t) {
1129
+ const acc = p.accounts ?? [];
1130
+ const all = p.accountCount ?? acc.length;
1131
+ const lim = limitedAccounts(p).length;
1132
+ const parts = [];
1133
+ if (all > 0) parts.push(t.disc.reflected(countedAccounts(p), all) + (lim ? t.disc.blocked(lim) : ""));
1134
+ const ex = exclusions(p);
1135
+ const why = [];
1136
+ if (ex.held) why.push(t.disc.held);
1137
+ if (ex.unknownTier) why.push(t.disc.unknownTier);
1138
+ if (ex.noMeasure) why.push(t.disc.noMeasure);
1139
+ const exN = ex.held + ex.unknownTier + ex.noMeasure;
1140
+ if (exN) parts.push(t.disc.excluded(exN, why.join("·")));
1141
+ const assumed = acc.filter((a) => a.tierAssumed && a.tier && a.state !== "paused" && a.state !== "excluded");
1142
+ if (assumed.length) parts.push(t.disc.assumed(assumed.length, String(assumed[0].tier)));
1143
+ const est = p.estimated ?? acc.filter((a) => a.estimated).length;
1144
+ if (est) parts.push(t.disc.estimated(est));
1145
+ const un = p.unmeasured5h ?? acc.filter((a) => a.state === "unmeasured").length;
1146
+ if (un) parts.push(t.disc.unmeasured(un));
1147
+ let html = esc(parts.join(" · "));
1148
+ if (p.measuredAt !== void 0) {
1149
+ const age = (now - p.measuredAt) / 1e3;
1150
+ const sep = html ? " · " : "";
1151
+ if (age > 86400) html += `${sep}<b class="cap-stale">${esc(`⚠ ${t.disc.measured(t.age(age))}`)}</b>`;
1152
+ else if (age > 3600) html += sep + esc(t.disc.measured(t.age(age)));
1153
+ }
1154
+ return html;
1155
+ }
1156
+ #bar(p, key, now, t) {
1157
+ const v = Number(p[key]) || 0;
1158
+ const total = Number(p.total) || 0;
1159
+ const frac = total > 0 ? Math.max(0, Math.min(1, v / total)) : 0;
1160
+ const five = key === "fiveHour";
1161
+ const label = five ? t.bar.fiveHour : t.bar.weekly;
1162
+ const sub = five ? t.bar.fiveHourSub : t.bar.weeklySub;
1163
+ const vs = fmtCount(v, total);
1164
+ const ts = fmt1(total);
1165
+ const pct = fmtPct(v, total);
1166
+ const e = nextRefill(p, key, now);
1167
+ const who = e ? ` title="${escAttr([t.cause[e.cause] ?? "", e.account ?? ""].filter(Boolean).join(" · "))}"` : "";
1168
+ return `<div class="cap-bar-row is-${five ? "h5" : "wk"}"><div class="cap-bar-lbl">${esc(label)}<i>${esc(sub)}</i></div><div class="cap-bar" role="img" aria-label="${escAttr(
1169
+ t.bar.said(label, vs, ts, pct)
1170
+ )}"><span style="width:${(frac * 100).toFixed(1)}%"></span><i class="cap-low" style="left:${LOW_PCT}%" title="${escAttr(
1171
+ t.bar.low(LOW_PCT)
1172
+ )}"></i></div><div class="cap-bar-val"><b>${vs}</b> ${esc(t.bar.of(ts))} <em>${pct}</em></div><div class="cap-bar-next"${who}>${this.#nextLine(
1173
+ p,
1174
+ key,
1175
+ now,
1176
+ t
1177
+ )}</div></div>`;
1178
+ }
1179
+ /** “next +0.4 · in 22m”; another cause is named; small gains are summed, never dropped. */
1180
+ #nextLine(p, key, now, t) {
1181
+ const e = nextRefill(p, key, now);
1182
+ if (!e) {
1183
+ const small = gainWithin(p, key, now);
1184
+ return esc(small > 5e-4 ? t.bar.backWithin(t.gain(small, fmt1)) : t.bar.nothing);
1185
+ }
1186
+ const gain = esc(t.bar.next(fmt1(Number(e[key]) || 0))) + this.#time("in", e.at, now, t);
1187
+ if (key === "fiveHour" && e.cause === "7d") return esc(t.bar.heldByWeekly) + gain;
1188
+ const own = key === "fiveHour" ? e.cause === "5h" : e.cause === "7d";
1189
+ return own ? gain : `${gain} · ${esc(t.cause[e.cause] ?? "")}`;
1190
+ }
1191
+ #reason(a, t) {
1192
+ if (a.reason) return t.reason[a.reason] ?? String(a.reason);
1193
+ return t.stateWord[a.state] ?? "";
1194
+ }
1195
+ #tierLabel(a, t) {
1196
+ if (!a.tier) return t.noTier;
1197
+ return a.tierAssumed ? t.tierAssumed(a.tier) : a.tier;
1198
+ }
1199
+ /** The workers strip (D10): one mark per account in the given order, one tab stop; narrow folds the usable ones. */
1200
+ #workers(p, now, t, narrow) {
1201
+ const acc = p.accounts ?? [];
1202
+ if (!acc.length) return "";
1203
+ const okN = acc.filter((a) => a.state === "available").length;
1204
+ const rove = keyOf("cap", p.id);
1205
+ const items = [];
1206
+ let folded = false;
1207
+ for (const a of acc) {
1208
+ if (narrow && a.state === "available") {
1209
+ if (!folded) items.push({ key: accKey(p.id, null), a: null });
1210
+ folded = true;
1211
+ continue;
1212
+ }
1213
+ items.push({ key: accKey(p.id, a.id), a });
1214
+ }
1215
+ const keys = items.map((i) => i.key);
1216
+ const want = this.#stops.get(rove);
1217
+ const stop = want && keys.includes(want) ? want : keys[0];
1218
+ const o = this.#open;
1219
+ const open = o && o.pid === p.id ? accKey(p.id, o.aid) : null;
1220
+ const marks = items.map((it) => {
1221
+ if (it.a) return this.#mark(p, it.a, it.key, now, t, open === it.key, stop === it.key);
1222
+ return `<button type="button" class="aq-mark is-available" data-key="${escAttr(it.key)}" data-pid="${escAttr(
1223
+ p.id
1224
+ )}" data-rove-item tabindex="${it.key === stop ? 0 : -1}" aria-haspopup="dialog" aria-expanded="${open === it.key}" aria-label="${escAttr(t.okFoldAria(okN))}">${esc(t.okFold(okN))}</button>`;
1225
+ }).join("");
1226
+ const card = open && o && keys.includes(open) ? this.#card(p, o.aid, open, now, t) : "";
1227
+ return `<div class="aq-workers" role="group" aria-label="${escAttr(t.workersAria(p.name, okN, acc.length))}" data-rove="${escAttr(
1228
+ rove
1229
+ )}"><div class="aq-col-head"><span>${esc(t.workers)}</span> <b>${okN}/${acc.length}</b></div><div class="aq-marks">${marks}</div>${card}</div>`;
1230
+ }
1231
+ #mark(p, a, key, now, t, open, stop) {
1232
+ const why = this.#reason(a, t);
1233
+ const said = [a.name, a.tier ? this.#tierLabel(a, t) : "", why];
1234
+ let body = `<b>${esc(a.name)}</b>`;
1235
+ if (a.state === "available") {
1236
+ body = `<i class="aq-wdot is-${a.binding === "5h" ? "h5" : "wk"}" aria-hidden="true"></i>${body}`;
1237
+ } else if (a.state === "limited") {
1238
+ body = `<span class="aq-g" aria-hidden="true">⏸</span>${body} · ${esc(why)} · ${a.returnAt ? this.#time("left", a.returnAt, now, t) : esc(t.noReturn)}`;
1239
+ said.push(a.returnAt ? timeText("back", a.returnAt, now, t) : t.noReturn);
1240
+ } else {
1241
+ body += ` · ${esc(why)}`;
1242
+ }
1243
+ return `<button type="button" class="aq-mark is-${a.state}" data-key="${escAttr(key)}" data-pid="${escAttr(
1244
+ p.id
1245
+ )}" data-aid="${escAttr(a.id)}" data-rove-item tabindex="${stop ? 0 : -1}" aria-haspopup="dialog" aria-expanded="${open}" aria-label="${escAttr(
1246
+ said.filter(Boolean).join(", ")
1247
+ )}">${body}</button>`;
1248
+ }
1249
+ /** The account card (D11): every row from the account's own fields; the folded mark's card lists the usable ones. */
1250
+ #card(p, aid, key, now, t) {
1251
+ const close = `<button type="button" class="aq-pop-close" data-key="aq-pop-close" aria-label="${escAttr(t.card.close)}">×</button>`;
1252
+ const row = (dt, dd, wide = false) => `<div${wide ? ' class="is-wide"' : ""}><dt>${esc(dt)}</dt><dd>${dd}</dd></div>`;
1253
+ const pct = (x) => x === void 0 || x === null ? "—" : `${Math.round((Number(x) || 0) * 100)}%`;
1254
+ if (aid === null) {
1255
+ const ok = (p.accounts ?? []).filter((a2) => a2.state === "available");
1256
+ return `<div class="aq-pop" role="dialog" aria-label="${escAttr(t.card.okAria)}" data-for="${escAttr(key)}"><div class="aq-pop-head"><span class="aq-pop-name">${esc(
1257
+ t.card.okTitle
1258
+ )}</span><span class="aq-pop-kind">${esc(t.card.okKind(p.name, ok.length))}</span>${close}</div><dl class="aq-pop-stats">${ok.map((a2) => row(a2.name, esc(t.card.okRow(pct(a2.fiveHour), pct(a2.weekly))))).join("")}</dl></div>`;
1259
+ }
1260
+ const a = p.accounts?.find((x) => x.id === aid);
1261
+ if (!a) return "";
1262
+ const counted = a.state !== "paused" && a.state !== "excluded";
1263
+ const rows = [row(t.card.now, counted ? pct(a.fiveHour) : "—"), row(t.card.week, counted ? pct(a.weekly) : "—")];
1264
+ if (a.binding) rows.push(row(t.card.binds, esc(a.binding === "5h" ? t.card.h5 : t.card.wk)));
1265
+ rows.push(row(t.card.state, esc(this.#reason(a, t))));
1266
+ if (a.returnAt)
1267
+ rows.push(
1268
+ row(t.card.returns, `${esc(clockLabel(a.returnAt, now))} · ${this.#time("left", a.returnAt, now, t)}`, true)
1269
+ );
1270
+ if (a.estimated) rows.push(row(t.card.estimated, esc(t.card.estimatedSay)));
1271
+ const tone = a.state === "available" ? "ok" : a.state === "limited" ? "paused" : a.state === "paused" || a.state === "excluded" ? "unknown" : "low";
1272
+ return `<div class="aq-pop" role="dialog" aria-label="${escAttr(t.card.aria(a.name))}" data-for="${escAttr(key)}"><div class="aq-pop-head"><span class="aq-pop-name">${esc(
1273
+ a.name
1274
+ )}</span><span class="aq-pop-kind">${esc(this.#tierLabel(a, t))}</span>${pill(tone, esc(this.#reason(a, t)))}${close}</div><dl class="aq-pop-stats">${rows.join(
1275
+ ""
1276
+ )}</dl></div>`;
1277
+ }
1278
+ /** The 7-day timeline: ● 5-hour reset (label above), ◆ weekly reset (label below); all limited shades now → lift. */
1279
+ #timeline(p, idx, state, W, now, t) {
1280
+ const marks = timelineMarks(p, W, now);
1281
+ if (!marks.length && state !== "empty") return "";
1282
+ const pad = 16;
1283
+ const y0 = marks.some((b) => b.c === "h5") ? 46 : 20;
1284
+ const h = y0 + 54;
1285
+ const X = (at) => pad + axisPos(at - now) * (W - 2 * pad);
1286
+ let s = `<svg class="cap-tl" width="${W}" height="${h}" viewBox="0 0 ${W} ${h}" role="img" aria-label="${escAttr(t.tl.aria)}">`;
1287
+ if (state === "empty") {
1288
+ const k = primaryKey(p);
1289
+ const lift = (p.refills ?? []).find((e) => e.at > now && (Number(e[k]) || 0) > 0)?.at ?? null;
1290
+ const x1 = lift === null ? W - pad : X(lift);
1291
+ const hid = `aqh-${idx}`;
1292
+ const span = Math.max(0, x1 - pad).toFixed(1);
1293
+ const title = lift === null ? t.tl.limNone : t.tl.lim(clockLabel(lift, now));
1294
+ s += `<defs><pattern id="${hid}" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><line class="cap-tl-hatch" x1="0" y1="0" x2="0" y2="6"/></pattern></defs>`;
1295
+ s += `<rect class="cap-tl-lim" x="${pad}" y="0" width="${span}" height="${y0 + 14}"><title>${esc(title)}</title></rect>`;
1296
+ s += `<rect x="${pad}" y="0" width="${span}" height="${y0 + 14}" fill="url(#${hid})" aria-hidden="true"/>`;
1297
+ }
1298
+ [0, HOUR, 6 * HOUR, DAY, 3 * DAY, 5 * DAY, D7].forEach((off, i) => {
1299
+ const x = (pad + axisPos(off) * (W - 2 * pad)).toFixed(1);
1300
+ s += `<line class="cap-tl-tick" x1="${x}" x2="${x}" y1="${y0 - 6}" y2="${y0 + 6}"/>`;
1301
+ s += `<text class="cap-tl-ax" x="${x}" y="${h - 4}" text-anchor="middle">${esc(t.tl.axis[i])}</text>`;
1302
+ });
1303
+ s += `<line class="cap-tl-axis" x1="${pad}" x2="${W - pad}" y1="${y0}" y2="${y0}"/>`;
1304
+ for (const b of marks) {
1305
+ const x = (b.x0 + b.x1) / 2;
1306
+ const xs = x.toFixed(1);
1307
+ s += `<g class="cap-tl-mk is-${b.c}">`;
1308
+ if (b.x1 - b.x0 > 2)
1309
+ s += `<line class="cap-tl-span" x1="${b.x0.toFixed(1)}" x2="${b.x1.toFixed(1)}" y1="${y0}" y2="${y0}"/>`;
1310
+ if (b.c === "h5") s += `<circle cx="${xs}" cy="${y0}" r="6.5"/>`;
1311
+ else
1312
+ s += `<rect x="${(x - 6).toFixed(1)}" y="${y0 - 6}" width="12" height="12" transform="rotate(45 ${xs} ${y0})"/>`;
1313
+ const tx = Math.max(pad + 14, Math.min(W - pad - 14, x)).toFixed(1);
1314
+ s += `<text x="${tx}" y="${b.c === "h5" ? y0 - 14 : y0 + 24}" text-anchor="middle">+${fmt1(b.gain)}</text>`;
1315
+ s += `<title>${esc(
1316
+ t.tl.mark(
1317
+ b.c === "h5" ? t.bar.fiveHour : t.bar.weekly,
1318
+ fmt1(b.gain),
1319
+ timeText("in", b.at, now, t),
1320
+ b.names.join(", ")
1321
+ )
1322
+ )}</title></g>`;
1323
+ }
1324
+ s += "</svg>";
1325
+ const soon5 = (p.refills ?? []).filter((e) => e.cause !== "7d" && e.at > now && e.at <= now + D7);
1326
+ const any5 = soon5.some((e) => (Number(e.fiveHour) || 0) >= 0.05);
1327
+ const sum5 = soon5.reduce((a, e) => a + (Number(e.fiveHour) || 0), 0);
1328
+ const note5 = any5 ? "" : sum5 > 5e-4 ? t.tl.small5(t.gain(sum5, fmt1)) : t.tl.none5;
1329
+ const key5 = has5h(p) ? `<span><i class="is-h5" aria-hidden="true">●</i> ${esc(t.tl.h5 + note5)}</span>` : "";
1330
+ return `<div class="cap-tl-wrap"><div class="cap-tl-h">${key5}<span><i class="is-wk" aria-hidden="true">◆</i> ${esc(
1331
+ t.tl.wk
1332
+ )}</span><span class="cap-tl-h-note">${esc(t.tl.note)}</span></div>${s}</div>`;
1333
+ }
1334
+ // ── Lane ──────────────────────────────────────────────────────────────────────────────────
1335
+ #lane(lane, now, t) {
1336
+ const jobs = orderJobs(lane.jobs ?? []);
1337
+ const title = lane.title || "CRON";
1338
+ const running = jobs.filter((j) => j.state === "running").length;
1339
+ const waiting = jobs.filter((j) => j.state === "waiting").length;
1340
+ const day = dayRuns({ ...lane, jobs }, now);
1341
+ const head = `<div class="aq-lane-head"><span class="aq-lane-ic" aria-hidden="true">${CLOCK_SVG}</span><h2 class="aq-lane-title">${esc(
1342
+ title
1343
+ )}</h2><span class="aq-count">${jobs.length}</span>${running ? pill("run", esc(t.running(running))) : ""}${waiting ? pill("wait", esc(t.waiting(waiting))) : ""}</div>`;
1344
+ return `<section class="aq-lane" aria-label="${escAttr(t.laneAria(title))}">${head}<div class="aq-lane-top">${this.#stats(
1345
+ jobs,
1346
+ day,
1347
+ now,
1348
+ t
1349
+ )}</div><div class="aq-lane-body"><div class="aq-queue" role="group" aria-label="${escAttr(t.queue.aria)}" data-rove="queue">${this.#queue(
1350
+ jobs,
1351
+ now,
1352
+ t
1353
+ )}</div>${jobs.length ? `<div class="cron-groups">${jobs.map((j) => this.#job(j, now, t, title)).join("")}</div>` : ""}</div><div class="aq-chart">${this.#chart(
1354
+ day,
1355
+ now,
1356
+ t
1357
+ )}</div></section>`;
1358
+ }
1359
+ /** Four numbers, each from a named field (D16): the front of the queue, running r/N, 24 h of runs and failures. */
1360
+ #stats(jobs, day, now, t) {
1361
+ const front = upNext(jobs)[0];
1362
+ const lb = day.cut.length ? "≥" : "";
1363
+ const cutNote = day.cut.map((c) => t.stat.cut(c.name, c.cap, clockLabel(c.from, now))).join(" · ");
1364
+ const running = jobs.filter((j) => j.state === "running").length;
1365
+ const stat = (label, value, title, kind = "") => `<div class="aq-stat"${kind ? ` data-kind="${kind}"` : ""}><dt>${esc(label)}</dt><dd title="${escAttr(title)}">${value}</dd></div>`;
1366
+ const next = front ? stat(
1367
+ t.stat.next,
1368
+ `${this.#time("in", front.nextRunAt, now, t)}<span class="aq-of">${esc(front.name)}</span>`,
1369
+ t.stat.nextTitle(front.name, clockLabel(front.nextRunAt, now))
1370
+ ) : stat(t.stat.next, "—", t.stat.none);
1371
+ return `<dl class="aq-stats">${next}${stat(
1372
+ t.stat.running,
1373
+ `${running}<span class="aq-of is-n">/${jobs.length}</span>`,
1374
+ t.stat.runningTitle(running, jobs.length)
1375
+ )}${stat(
1376
+ t.stat.runs,
1377
+ `${lb}${day.n}${day.unknown ? `<span class="aq-of">${esc(t.stat.unknown(day.unknown))}</span>` : ""}`,
1378
+ t.stat.runsTitle(day.n, day.ok, day.fail, day.run) + (day.unknown ? t.stat.unknownTitle(day.unknown) : "") + (cutNote ? ` · ${cutNote}` : "")
1379
+ )}${stat(t.stat.fails, `${lb}${day.fail}`, t.stat.failsTitle + (cutNote ? ` · ${cutNote}` : ""), day.fail > 0 ? "failed" : "")}</dl>`;
1380
+ }
1381
+ /** The up-next column (D17): jobs by their next run, soonest first, four chips then +N, one tab stop. */
1382
+ #queue(jobs, now, t) {
1383
+ const list = upNext(jobs);
1384
+ const n = list.length;
1385
+ const head = `<div class="aq-col-head" title="${escAttr(t.queue.headTitle)}"><span>${esc(t.queue.head)}</span> <b>${n}</b></div>`;
1386
+ if (!n) return `${head}<div class="aq-empty">${esc(t.queue.empty)}</div>`;
1387
+ const shown = list.slice(0, 4);
1388
+ const keys = shown.map((j) => keyOf("q", j.id));
1389
+ const want = this.#stops.get("queue");
1390
+ const stop = want && keys.includes(want) ? want : keys[0];
1391
+ const seen = this.#seenQ;
1392
+ this.#seenQ = new Set(shown.map((j) => j.id));
1393
+ return head + shown.map((j, i) => {
1394
+ const at = j.nextRunAt;
1395
+ return `<button type="button" class="aq-q${seen.has(j.id) ? "" : " is-new"}" data-job="${escAttr(j.id)}" data-key="${escAttr(keys[i])}" data-rove-item tabindex="${keys[i] === stop ? 0 : -1}" aria-label="${escAttr(t.queue.said(j.name, timeText("runs", at, now, t), i + 1, n))}">${CLOCK_SVG}<span class="aq-q-name">${esc(
1396
+ j.name
1397
+ )}</span>${this.#time("left", at, now, t)}</button>`;
1398
+ }).join("") + (n > 4 ? `<span class="aq-more">+${n - 4}</span>` : "");
1399
+ }
1400
+ #job(j, now, t, badge) {
1401
+ const runs = jobRuns(j);
1402
+ const sched = j.schedule ?? (j.expression ? scheduleLabel(j.expression, j.nextRunAt, this.#lang) : "");
1403
+ const next = j.nextRunAt !== void 0 ? `<span class="cron-next">${esc(t.job.next(clockLabel(j.nextRunAt, now)))}${this.#time("in", j.nextRunAt, now, t)})</span>` : "";
1404
+ const last = runs[0];
1405
+ const ls = last ? statusOf(last) : null;
1406
+ const status = j.state === "waiting" ? esc(t.job.waiting) : j.state === "running" ? esc(t.job.running) : last && ls ? `${GLYPH[ls]} ${esc(last.label || t.run[ls])} · ${this.#time("since", last.at, now, t)}` : esc(t.job.noRuns);
1407
+ const tone = j.state === "waiting" ? "wait" : j.state === "running" ? "run" : ls ? TICK[ls] : "none";
1408
+ const cls = j.state === "waiting" ? " is-waiting" : j.state === "running" ? " is-running" : "";
1409
+ const expr = j.expression ? ` title="${escAttr(t.job.exprTitle(j.expression))}"` : "";
1410
+ return `<section class="cron-group${cls}" aria-label="${escAttr(j.name)}"><div class="cron-tile"><div class="cron-tile-head"><span class="cron-badge" aria-hidden="true">${CLOCK_SVG}<span class="cron-badge-text">${esc(
1411
+ badge
1412
+ )}</span></span><span class="cron-name" title="${escAttr(j.name)}">${esc(j.name)}</span><button type="button" class="cron-history-btn" data-job="${escAttr(
1413
+ j.id
1414
+ )}" data-key="${keyOf("runs", j.id)}">${esc(t.job.past(j.pastRuns ?? runs.length))}</button></div>${sched || next ? `<div class="cron-sched">${sched ? `<span class="cron-expr"${expr}>${esc(sched)}</span>` : ""}${next}</div>` : ""}${this.#strip(j, runs, now, t)}<div class="cron-status is-${tone}">${status}</div></div></section>`;
1415
+ }
1416
+ /** The last 12 runs, oldest → newest. Every state has its own mark; the button says the counts. */
1417
+ #strip(j, runs, now, t) {
1418
+ const last = runs.slice(0, 12).reverse();
1419
+ if (!last.length) return `<div class="cron-strip is-empty">${esc(t.job.noRecords)}</div>`;
1420
+ const counts = {};
1421
+ for (const r of last) {
1422
+ const k = runKind(r.status);
1423
+ counts[k] = (counts[k] ?? 0) + 1;
1424
+ }
1425
+ const sum = ["ok", "run", "fail", "unknown"].filter((k) => counts[k]).map((k) => t.runSum(counts[k], t.runCount[k])).join(", ");
1426
+ const ticks = last.map((r) => {
1427
+ const st = statusOf(r);
1428
+ const title = `${GLYPH[st]} ${r.label || t.run[st]} · ${clockLabel(r.at, now)} · ${timeText("since", r.at, now, t)}${r.error ? ` · ${plain(r.error, 200)}` : ""}`;
1429
+ return `<i class="cr-tick is-${TICK[st]}" title="${escAttr(title)}"></i>`;
1430
+ }).join("");
1431
+ return `<button type="button" class="cron-strip" data-job="${escAttr(j.id)}" data-key="${keyOf("strip", j.id)}" aria-label="${escAttr(
1432
+ t.job.strip(j.name, last.length, sum)
1433
+ )}">${ticks}</button>`;
1434
+ }
1435
+ /** 24 hours of run records (D19): rolling one-hour columns ending now, ok · in progress · failed stacked. */
1436
+ #chart(day, now, t) {
1437
+ const bins = runBins(day, now);
1438
+ let peak = 0;
1439
+ let peakAt = 0;
1440
+ for (const b of bins) {
1441
+ const s = b.ok + b.fail + b.run;
1442
+ if (s > peak) {
1443
+ peak = s;
1444
+ peakAt = b.at;
1445
+ }
1446
+ }
1447
+ const max = niceMax(peak);
1448
+ const pct = (v) => `${(v / max * 100).toFixed(1)}%`;
1449
+ const start = now - DAY;
1450
+ const cut = day.cutFrom > start;
1451
+ const said = t.chart.said(day.n, day.ok, day.fail, day.run) + (peak ? t.chart.peak(hm(peakAt), peak) : "") + "." + (cut ? t.chart.cut(clockLabel(day.cutFrom, now)) : "") + (day.unknown ? t.chart.unknown(day.unknown) : "");
1452
+ const hatch = cut ? `<div class="aq-ch-cut" style="left:0;width:${((day.cutFrom - start) / DAY * 100).toFixed(1)}%" title="${escAttr(
1453
+ t.chart.hatch(day.cut.map((c) => c.name).join(", "))
1454
+ )}"></div>` : "";
1455
+ const cols = bins.map(
1456
+ (b) => `<i class="aq-ch-col" title="${escAttr(t.chart.col(hm(b.at), b.ok, b.fail, b.run))}"><i class="aq-ch-ok" style="height:${pct(
1457
+ b.ok
1458
+ )}"></i><i class="aq-ch-run" style="height:${pct(b.run)}"></i><i class="aq-ch-fail" style="height:${pct(b.fail)}"></i></i>`
1459
+ ).join("");
1460
+ const key = (cls, label, title) => `<span class="aq-ch-key" title="${escAttr(title)}"><i class="is-${cls}" aria-hidden="true"></i>${esc(label)}</span>`;
1461
+ return `<div class="aq-chart-head"><span class="aq-chart-title">${esc(t.chart.title)}</span>${key("ok", t.chart.ok, t.chart.okTitle)}${key(
1462
+ "run",
1463
+ t.chart.run,
1464
+ t.chart.runTitle
1465
+ )}${key("fail", t.chart.fail, t.chart.failTitle)}</div><div class="aq-plot" role="img" aria-label="${escAttr(said)}">${hatch}<div class="aq-ch-grid" aria-hidden="true"><span>${max}</span></div><div class="aq-ch-grid is-zero" aria-hidden="true"><span>0</span></div>${cols}</div><div class="aq-ch-axis" aria-hidden="true">${t.chart.axis.map((a) => `<span>${esc(a)}</span>`).join("")}</div>`;
1466
+ }
1467
+ // ── After render ──────────────────────────────────────────────────────────────────────────
1468
+ update() {
1469
+ const active = this.root.activeElement;
1470
+ const rove = active?.closest?.("[data-rove]")?.dataset.rove ?? null;
1471
+ super.update();
1472
+ if (rove && !this.root.activeElement) {
1473
+ const box = this.$$("[data-rove]").find((b) => b.dataset.rove === rove);
1474
+ box?.querySelector('[data-rove-item][tabindex="0"]')?.focus({ preventScroll: true });
1475
+ }
1476
+ }
1477
+ updated() {
1478
+ if (this.#open && !this.$(".aq-pop")) this.#open = null;
1479
+ for (const pop of this.$$(".aq-pop[data-for]")) {
1480
+ const box = pop.parentElement;
1481
+ const mark = box ? Array.from(box.querySelectorAll("[data-rove-item]")).find(
1482
+ (m) => m.dataset.key === pop.dataset.for
1483
+ ) : void 0;
1484
+ if (!box || !mark) continue;
1485
+ pop.style.left = `${Math.max(8, Math.min(mark.offsetLeft, box.clientWidth - pop.offsetWidth - 8))}px`;
1486
+ pop.style.top = `${mark.offsetTop + mark.offsetHeight + 6}px`;
1487
+ }
1488
+ }
1489
+ /** 1 Hz: rewrite each live time's text only when it changed; a countdown reaching zero repaints once. */
1490
+ #tickNow() {
1491
+ if (typeof document !== "undefined" && document.hidden) return;
1492
+ const now = this.#now();
1493
+ const t = this.#t;
1494
+ let due = false;
1495
+ for (const el of this.$$("time[data-at]")) {
1496
+ const at = Number(el.dataset.at);
1497
+ const kind = el.dataset.fmt ?? "left";
1498
+ const txt = timeText(kind, at, now, t);
1499
+ if (el.textContent !== txt) el.textContent = txt;
1500
+ if (COUNTDOWN.has(kind) && at <= now && !el.hasAttribute("data-due")) due = true;
1501
+ }
1502
+ if (due) this.requestRender();
1503
+ }
1504
+ // ── Events ────────────────────────────────────────────────────────────────────────────────
1505
+ #focusKey(key) {
1506
+ this.$$("[data-key]").find((el) => el.dataset.key === key)?.focus({ preventScroll: true });
1507
+ }
1508
+ #toggle(pid, aid) {
1509
+ const key = accKey(pid, aid);
1510
+ const o = this.#open;
1511
+ if (o && o.pid === pid && o.aid === aid) {
1512
+ this.#open = null;
1513
+ this.renderNow();
1514
+ this.#focusKey(key);
1515
+ return;
1516
+ }
1517
+ if (aid !== null && !this.emit("ui-account-open", { providerId: pid, accountId: aid })) return;
1518
+ this.#open = { pid, aid };
1519
+ this.#stops.set(keyOf("cap", pid), key);
1520
+ this.renderNow();
1521
+ this.#focusKey(key);
1522
+ }
1523
+ #close(refocus) {
1524
+ const o = this.#open;
1525
+ if (!o) return;
1526
+ this.#open = null;
1527
+ this.renderNow();
1528
+ if (refocus) this.#focusKey(accKey(o.pid, o.aid));
1529
+ }
1530
+ #onDocClick = (e) => {
1531
+ if (this.#open && !e.composedPath().includes(this)) this.#close(false);
1532
+ };
1533
+ #onClick(e) {
1534
+ const el = e.target;
1535
+ if (!el?.closest) return;
1536
+ if (el.closest(".aq-pop-close")) {
1537
+ this.#close(true);
1538
+ return;
1539
+ }
1540
+ const mark = el.closest(".aq-mark[data-pid]");
1541
+ if (mark) {
1542
+ this.#toggle(mark.dataset.pid ?? "", mark.dataset.aid ?? null);
1543
+ return;
1544
+ }
1545
+ const job = el.closest("[data-job]");
1546
+ if (job) this.emit("ui-job-open", { jobId: job.dataset.job ?? "" });
1547
+ if (this.#open && !el.closest(".aq-pop")) this.#close(false);
1548
+ }
1549
+ #onKey(e) {
1550
+ if (e.key === "Escape" && this.#open) {
1551
+ e.preventDefault();
1552
+ e.stopPropagation();
1553
+ this.#close(true);
1554
+ return;
1555
+ }
1556
+ const el = e.target;
1557
+ const item = el?.closest?.("[data-rove-item]");
1558
+ const box = item?.closest("[data-rove]");
1559
+ if (!item || !box) return;
1560
+ const step = { ArrowLeft: -1, ArrowUp: -1, ArrowRight: 1, ArrowDown: 1, Home: "first", End: "last" }[e.key];
1561
+ if (step === void 0) return;
1562
+ const items = Array.from(box.querySelectorAll("[data-rove-item]"));
1563
+ const i = items.indexOf(item);
1564
+ if (i < 0) return;
1565
+ const j = step === "first" ? 0 : step === "last" ? items.length - 1 : Math.max(0, Math.min(items.length - 1, i + step));
1566
+ e.preventDefault();
1567
+ items.forEach((it, n) => {
1568
+ it.setAttribute("tabindex", n === j ? "0" : "-1");
1569
+ });
1570
+ items[j].focus();
1571
+ this.#stops.set(box.dataset.rove ?? "", items[j].dataset.key ?? "");
1572
+ }
1573
+ #onFocus(e) {
1574
+ const item = e.target?.closest?.("[data-rove-item]");
1575
+ const box = item?.closest("[data-rove]");
1576
+ if (!item || !box) return;
1577
+ this.#stops.set(box.dataset.rove ?? "", item.dataset.key ?? "");
1578
+ for (const it of Array.from(box.querySelectorAll("[data-rove-item]")))
1579
+ it.setAttribute("tabindex", it === item ? "0" : "-1");
1580
+ }
1581
+ }
1582
+ define("ui-agent-queue", UiAgentQueue);
1583
+ export {
1584
+ LOW_PCT,
1585
+ UiAgentQueue,
1586
+ accountState,
1587
+ advanceProvider,
1588
+ axisPos,
1589
+ capacityNews,
1590
+ dayRuns,
1591
+ emptyReturnAt,
1592
+ firstReturn,
1593
+ fmtCount,
1594
+ fmtPct,
1595
+ laneNews,
1596
+ nextRefill,
1597
+ niceMax,
1598
+ normalize,
1599
+ orderJobs,
1600
+ plain,
1601
+ poolState,
1602
+ runBins,
1603
+ runKind,
1604
+ scheduleLabel,
1605
+ timeText,
1606
+ timelineMarks,
1607
+ toMs,
1608
+ upNext
1609
+ };
1610
+ //# sourceMappingURL=agent-queue.js.map