@krtclcdy/tersio 2.22.0 → 2.23.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 (47) hide show
  1. package/README.md +38 -30
  2. package/cli/common.js +43 -39
  3. package/cli/currency.js +2 -2
  4. package/cli/dashboard.js +60 -116
  5. package/cli/doctor-fix.js +35 -29
  6. package/cli/doctor.js +26 -15
  7. package/cli/install.js +15 -34
  8. package/cli/profile.js +2 -1
  9. package/cli/settings.js +6 -6
  10. package/cli/update.js +12 -11
  11. package/cli/usage.js +6 -1
  12. package/dashboard/dist/index.html +65 -0
  13. package/extensions/ai-addons-updater/index.js +7 -3
  14. package/extensions/ai-addons-updater/index.ts +15 -3
  15. package/extensions/caveman-session/index.js +17 -12
  16. package/extensions/caveman-session/index.ts +17 -12
  17. package/extensions/caveman-session/rule.md +90 -0
  18. package/extensions/combo-toggle/index.js +22 -2
  19. package/extensions/combo-toggle/index.ts +26 -2
  20. package/extensions/lib/utils.js +20 -4
  21. package/extensions/lib/utils.ts +18 -4
  22. package/extensions/rtk-session/index.js +11 -10
  23. package/extensions/rtk-session/index.ts +10 -11
  24. package/extensions/shared/plugin-settings.js +37 -3
  25. package/extensions/shared/plugin-settings.ts +34 -3
  26. package/extensions/shared/rtk-gain.js +2 -2
  27. package/extensions/shared/rtk-gain.ts +3 -3
  28. package/extensions/shared/session-state.js +5 -3
  29. package/extensions/shared/session-state.ts +4 -3
  30. package/extensions/shared/types.ts +5 -0
  31. package/extensions/shared/usage-ledger.js +114 -4
  32. package/extensions/shared/usage-ledger.ts +122 -4
  33. package/extensions/tersio-commands/index.js +10 -13
  34. package/extensions/tersio-commands/index.ts +10 -13
  35. package/package.json +21 -12
  36. package/tersio.js +10 -10
  37. package/dashboard/charts.js +0 -1208
  38. package/dashboard/core.js +0 -357
  39. package/dashboard/settings.js +0 -166
  40. package/dashboard/share.js +0 -218
  41. package/dashboard/styles.css +0 -379
  42. package/dashboard/template.html +0 -515
  43. package/extensions/shared/mode-reinforcement.ts +0 -29
  44. package/gain/dist/brand.webp +0 -0
  45. package/gain/dist/index.html +0 -65
  46. package/gain/dist/vite.svg +0 -1
  47. /package/dashboard/{brand.webp → dist/brand.webp} +0 -0
package/dashboard/core.js DELETED
@@ -1,357 +0,0 @@
1
- (function() {
2
- var T = window.Tersio = {};
3
- var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
4
- var hasGsap = !reduce && typeof window.gsap !== 'undefined';
5
- var hasST = hasGsap && typeof window.ScrollTrigger !== 'undefined';
6
- if (hasST) gsap.registerPlugin(ScrollTrigger);
7
-
8
- function fmt(n) { return Number(n).toLocaleString('en-US'); }
9
- function fmtShort(n) {
10
- if (n >= 1e12) return (n / 1e12).toFixed(1) + 'T';
11
- if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
12
- if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
13
- if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
14
- return String(Math.round(n));
15
- }
16
-
17
- var CURS = {
18
- USD: { s: '$', d: 2 }, PHP: { s: '₱', d: 2 }, EUR: { s: '€', d: 2 }, GBP: { s: '£', d: 2 },
19
- JPY: { s: '¥', d: 0 }, KRW: { s: '₩', d: 0 }, SGD: { s: 'S$', d: 2 }, AUD: { s: 'A$', d: 2 },
20
- CAD: { s: 'C$', d: 2 }, INR: { s: '₹', d: 2 }
21
- };
22
- var FLAGS = { USD: '🇺🇸', PHP: '🇵🇭', EUR: '🇪🇺', GBP: '🇬🇧', JPY: '🇯🇵', KRW: '🇰🇷', SGD: '🇸🇬', AUD: '🇦🇺', CAD: '🇨🇦', INR: '🇮🇳' };
23
- // ponytail: static snapshot; live rates from frankfurter replace it when reachable
24
- var fx = { cur: 'USD', rates: { USD: 1, PHP: 58.7, EUR: 0.92, GBP: 0.79, JPY: 149.8, KRW: 1385, SGD: 1.34, AUD: 1.52, CAD: 1.37, INR: 88.2 }, live: false };
25
- try {
26
- var savedCur = localStorage.getItem('tersio-fx-cur');
27
- if (savedCur && CURS[savedCur]) fx.cur = savedCur;
28
- var savedFx = JSON.parse(localStorage.getItem('tersio-fx') || 'null');
29
- if (savedFx && savedFx.rates && savedFx.rates.USD === 1) fx.rates = savedFx.rates;
30
- } catch (e) { }
31
- var repaintFxPicker = function() {};
32
- // Shared by the hero picker and the settings currency pane: persist
33
- // server-side (fresh ephemeral port per run, so localStorage alone dies
34
- // on restart) and re-render live figures.
35
- function applyCurrency(k) {
36
- if (!CURS[k]) return;
37
- fx.cur = k;
38
- try { localStorage.setItem('tersio-fx-cur', fx.cur); } catch (e) { }
39
- if (window.location.protocol !== 'file:' && typeof fetch === 'function') {
40
- try {
41
- fetch('currency', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ currency: k }) }).catch(function() { });
42
- } catch (e) { }
43
- }
44
- repaintFxPicker();
45
- if (typeof DATA !== 'undefined' && DATA) render(DATA);
46
- }
47
- // Magnitude-aware decimals. Model prices keep falling, so a flat 2dp rounds
48
- // real spend down to "$0.00"; small amounts keep enough digits to stay
49
- // readable (0.000187, not 0). Larger amounts use the currency's own scale.
50
- function moneyDecimals(v, base) {
51
- var a = Math.abs(v);
52
- if (a >= 0.1 || a === 0) return base;
53
- if (a >= 0.001) return Math.max(base, 4);
54
- if (a >= 0.00001) return Math.max(base, 6);
55
- return Math.max(base, 8);
56
- }
57
- function fxMoney(v) {
58
- var c = CURS[fx.cur] || CURS.USD, r = fx.rates[fx.cur] || 1, amt = v * r;
59
- var d = moneyDecimals(amt, c.d);
60
- return c.s + amt.toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d });
61
- }
62
- // Tooltips build innerHTML, and error notes come from providers, so they are
63
- // never interpolated raw.
64
- function esc(s) {
65
- return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
66
- }
67
-
68
- function countUp(el, target, money) {
69
- function text(v) { return money ? fxMoney(v) : fmt(Math.round(v)); }
70
- if (!hasGsap || target <= 0) { el.textContent = text(target); return; }
71
- var o = { v: 0 };
72
- gsap.to(o, { v: target, duration: 1.1, ease: 'power3.out', onUpdate: function() { el.textContent = text(o.v); } });
73
- }
74
-
75
- function tick() {
76
- var c = document.getElementById('clock');
77
- if (c) c.textContent = new Date().toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
78
- }
79
- tick(); setInterval(tick, 1000);
80
- function currentTheme() {
81
- try { return localStorage.getItem('tersio-theme') || 'system'; } catch (e) { return 'system'; }
82
- }
83
- function paintThemeTabs() {
84
- var v = currentTheme();
85
- Array.prototype.forEach.call(document.querySelectorAll('[data-theme-val]'), function(b) {
86
- b.classList.toggle('on', b.getAttribute('data-theme-val') === v);
87
- });
88
- }
89
- function applyTheme(v) {
90
- if (v === 'system') document.documentElement.removeAttribute('data-theme');
91
- else document.documentElement.setAttribute('data-theme', v);
92
- try { localStorage.setItem('tersio-theme', v); } catch (e) { }
93
- paintThemeTabs();
94
- }
95
- applyTheme(currentTheme());
96
- (function initRail() {
97
- var rail = document.getElementById('rail');
98
- if (!rail || initRail.done) return;
99
- initRail.done = true;
100
- var N = 24, ticks = [];
101
- for (var i = 0; i < N; i++) {
102
- (function(i) {
103
- var b = document.createElement('button');
104
- b.type = 'button'; b.tabIndex = -1; b.setAttribute('aria-hidden', 'true');
105
- b.addEventListener('click', function() {
106
- var max = document.documentElement.scrollHeight - window.innerHeight;
107
- window.scrollTo({ top: max * (i / (N - 1)), behavior: 'smooth' });
108
- });
109
- rail.appendChild(b); ticks.push(b);
110
- })(i);
111
- }
112
- var queued = false;
113
- function paint() {
114
- queued = false;
115
- var max = document.documentElement.scrollHeight - window.innerHeight;
116
- var f = max > 0 ? window.scrollY / max : 0;
117
- ticks.forEach(function(b, j) { b.classList.toggle('lit', j / (N - 1) <= f); });
118
- }
119
- window.addEventListener('scroll', function() {
120
- if (!queued) { queued = true; requestAnimationFrame(paint); }
121
- }, { passive: true });
122
- paint();
123
- })();
124
- (function initFx() {
125
- var btn = document.getElementById('fxBtn'), panel = document.getElementById('fxPanel'), cur = document.getElementById('fxCur');
126
- var keys = Object.keys(CURS), active = -1;
127
- function paint() {
128
- cur.textContent = FLAGS[fx.cur] + ' ' + fx.cur;
129
- Array.prototype.forEach.call(panel.children, function(o) {
130
- o.setAttribute('aria-selected', o.dataset.cur === fx.cur ? 'true' : 'false');
131
- });
132
- }
133
- function setCur(k) {
134
- fx.cur = k; active = keys.indexOf(k);
135
- applyCurrency(k);
136
- paint();
137
- }
138
- function open(show) {
139
- var willOpen = show === undefined ? panel.classList.contains('hidden') : show;
140
- panel.classList.toggle('hidden', !willOpen);
141
- btn.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
142
- if (willOpen) mark(keys.indexOf(fx.cur));
143
- }
144
- function mark(i) {
145
- active = (i + keys.length) % keys.length;
146
- Array.prototype.forEach.call(panel.children, function(o, j) {
147
- o.classList.toggle('active', j === active);
148
- });
149
- var el = panel.children[active];
150
- if (el) el.focus();
151
- }
152
- keys.forEach(function(k) {
153
- var o = document.createElement('button');
154
- o.type = 'button'; o.className = 'fxopt mono'; o.dataset.cur = k;
155
- o.setAttribute('role', 'option');
156
- o.innerHTML = '<span>' + FLAGS[k] + ' ' + k + '</span><svg class="tick" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>';
157
- o.addEventListener('click', function() { setCur(k); open(false); btn.focus(); });
158
- o.addEventListener('mousemove', function() { mark(keys.indexOf(k)); });
159
- panel.appendChild(o);
160
- });
161
- paint();
162
- repaintFxPicker = paint;
163
- btn.addEventListener('click', function() { open(); });
164
- btn.addEventListener('keydown', function(ev) {
165
- if (ev.key === 'ArrowDown' || ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); open(true); }
166
- });
167
- panel.addEventListener('keydown', function(ev) {
168
- if (ev.key === 'Escape') { open(false); btn.focus(); }
169
- else if (ev.key === 'ArrowDown') { ev.preventDefault(); mark(active + 1); }
170
- else if (ev.key === 'ArrowUp') { ev.preventDefault(); mark(active - 1); }
171
- else if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); setCur(keys[active]); open(false); btn.focus(); }
172
- else if (ev.key === 'Tab') open(false);
173
- });
174
- document.addEventListener('click', function(ev) {
175
- if (!panel.classList.contains('hidden') && !btn.contains(ev.target) && !panel.contains(ev.target)) open(false);
176
- });
177
- if (typeof fetch !== 'function' || typeof AbortController === 'undefined') return;
178
- var ctl = new AbortController();
179
- var to = setTimeout(function() { ctl.abort(); }, 4000);
180
- fetch('https://api.frankfurter.app/latest?from=USD', { signal: ctl.signal }).then(function(r) { return r.json(); }).then(function(j) {
181
- clearTimeout(to);
182
- if (!j || !j.rates) return;
183
- var ok = Object.keys(CURS).every(function(k) { return k === 'USD' || typeof j.rates[k] === 'number'; });
184
- if (!ok) return;
185
- fx.rates = Object.assign({ USD: 1 }, j.rates); fx.live = true;
186
- try { localStorage.setItem('tersio-fx', JSON.stringify({ rates: fx.rates, ts: Date.now() })); } catch (e) { }
187
- if (DATA) render(DATA);
188
- }).catch(function() { clearTimeout(to); });
189
- })();
190
- Array.prototype.forEach.call(document.querySelectorAll('[data-theme-val]'), function(b) {
191
- b.addEventListener('click', function() { applyTheme(b.getAttribute('data-theme-val')); });
192
- });
193
- (function() {
194
- var sysMedia = window.matchMedia('(prefers-color-scheme: dark)');
195
- if (sysMedia && typeof sysMedia.addEventListener === 'function') sysMedia.addEventListener('change', function() {
196
- if (currentTheme() === 'system') applyTheme('system');
197
- });
198
- })();
199
- var DATA = null;
200
- var renderHooks = [];
201
- function render(d) {
202
- DATA = d;
203
- T.renderMain(d);
204
- for (var i = 0; i < renderHooks.length; i++) renderHooks[i](d);
205
- }
206
- T.getData = function() { return DATA; };
207
- T.render = render;
208
- T.onRender = function(fn) { renderHooks.push(fn); };
209
- T.load = load;
210
- var tipEl = null;
211
- function tip() {
212
- if (!tipEl) {
213
- tipEl = document.createElement('div');
214
- tipEl.id = 'tip'; tipEl.className = 'mono'; tipEl.setAttribute('role', 'tooltip');
215
- document.body.appendChild(tipEl);
216
- }
217
- return tipEl;
218
- }
219
- function showTip(html, x, y) {
220
- var t = tip();
221
- t.innerHTML = html;
222
- if (window.lucide) lucide.createIcons();
223
- t.classList.add('show');
224
- moveTip(x, y);
225
- }
226
- function moveTip(x, y) {
227
- if (!tipEl) return;
228
- var w = 300, h = tipEl.offsetHeight || 220;
229
- var lx = x + 16 > window.innerWidth - w ? x - w - 12 : x + 16;
230
- var ly = y + 16 > window.innerHeight - h ? y - h - 12 : y + 16;
231
- tipEl.style.left = Math.max(8, lx) + 'px';
232
- tipEl.style.top = Math.max(8, ly) + 'px';
233
- }
234
- function hideTip() { if (tipEl) tipEl.classList.remove('show'); }
235
-
236
- var seen = new WeakSet();
237
- function observe() {
238
- var els = document.querySelectorAll('.rise');
239
- if (!('IntersectionObserver' in window) || reduce) {
240
- els.forEach(function(el) { el.classList.add('in'); });
241
- return;
242
- }
243
- var io = new IntersectionObserver(function(entries) {
244
- entries.forEach(function(e) {
245
- if (e.isIntersecting && !seen.has(e.target)) {
246
- seen.add(e.target); e.target.classList.add('in'); io.unobserve(e.target);
247
- }
248
- });
249
- }, { threshold: 0.12 });
250
- els.forEach(function(el) { if (!seen.has(el)) io.observe(el); });
251
- }
252
- var lastJson = '';
253
- function load() {
254
- // Served mode only: file:// exports have no data.json endpoint, so
255
- // polling there would just burn cycles on 404s.
256
- if (window.location.protocol === 'file:') return;
257
- // Skip background tabs: no render work while hidden, instant refresh on return.
258
- if (document.hidden) return;
259
- fetch('data.json').then(function(r) { return r.json(); }).then(function(d) {
260
- var json = JSON.stringify(d);
261
- if (json === lastJson) return;
262
- lastJson = json;
263
- render(d);
264
- }).catch(function() {
265
- var cards = document.getElementById('modelCards');
266
- if (cards && !cards.children.length) cards.innerHTML = '<div class="empty md:col-span-3">' + T.emptyState('cloud-off', 'Could not load data', 'Serve with tersio gain instead of opening this file directly.') + '</div>';
267
- if (window.lucide) lucide.createIcons();
268
- observe();
269
- });
270
- }
271
- // Live view: re-read data.json every 5s while served and visible; identical
272
- // payloads skip render. The manual Reload button stays as an instant refresh.
273
- setInterval(load, 5000);
274
- document.addEventListener('visibilitychange', function() { if (!document.hidden) load(); });
275
- function toast(title, desc, icon) {
276
- var wrap = document.querySelector('.toaster');
277
- if (!wrap) {
278
- wrap = document.createElement('div');
279
- wrap.className = 'toaster';
280
- wrap.popover = 'manual';
281
- document.body.appendChild(wrap);
282
- if (wrap.showPopover) wrap.showPopover(); // top layer: paints above the native settings dialog
283
- }
284
- var el = document.createElement('div');
285
- el.className = 'toast';
286
- el.innerHTML = '<span class="toast-icon"><i data-lucide="' + (icon || 'check') + '" class="size-4"></i></span><div><p class="toast-title"></p><p class="toast-desc"></p></div>';
287
- el.querySelector('.toast-title').textContent = title;
288
- el.querySelector('.toast-desc').textContent = desc;
289
- wrap.appendChild(el);
290
- if (window.lucide) lucide.createIcons();
291
- setTimeout(function() {
292
- el.classList.add('out');
293
- setTimeout(function() { el.remove(); }, 200);
294
- }, 4000);
295
- }
296
- (function() {
297
- var dlg = document.getElementById('settings');
298
- if (dlg && typeof dlg.showModal === 'function') {
299
- document.getElementById('settingsBtn').addEventListener('click', function() { dlg.showModal(); });
300
- document.getElementById('settingsClose').addEventListener('click', function() { dlg.close(); });
301
- dlg.addEventListener('click', function(ev) { if (ev.target === dlg) dlg.close(); });
302
- }
303
- var md = document.getElementById('modelDialog');
304
- if (md && typeof md.showModal === 'function') {
305
- document.getElementById('mdClose').addEventListener('click', function() { md.close(); });
306
- md.addEventListener('click', function(ev) { if (ev.target === md) md.close(); });
307
- md.addEventListener('close', function() { document.body.style.overflow = ''; });
308
- new MutationObserver(function() {
309
- if (md.open) document.body.style.overflow = 'hidden';
310
- }).observe(md, { attributes: true, attributeFilter: ['open'] });
311
- }
312
- })();
313
- (function() {
314
- var resetBtn = document.getElementById('reset');
315
- if (!resetBtn) return;
316
- if (window.location.protocol === 'file:') { resetBtn.style.display = 'none'; return; }
317
- var label = document.getElementById('resetLabel');
318
- resetBtn.addEventListener('click', function() {
319
- if (resetBtn.dataset.armed) {
320
- delete resetBtn.dataset.armed;
321
- label.textContent = 'Clearing…';
322
- fetch('reset', { method: 'POST' }).then(function(r) { return r.json(); }).then(function() {
323
- label.textContent = 'Reset';
324
- load();
325
- toast('Statistics reset', 'The usage statistics view now starts fresh. Transcripts and RTK history were never touched.', 'rotate-ccw');
326
- }).catch(function() {
327
- label.textContent = 'Reset';
328
- toast('Reset failed', 'Could not reach the server. Try again.', 'circle-alert');
329
- });
330
- } else {
331
- resetBtn.dataset.armed = '1';
332
- label.textContent = 'Sure?';
333
- setTimeout(function() { delete resetBtn.dataset.armed; if (label.textContent === 'Sure?') label.textContent = 'Reset'; }, 3000);
334
- }
335
- });
336
- })();
337
- load();
338
- observe();
339
- T.fmt = fmt;
340
- T.fmtShort = fmtShort;
341
- T.CURS = CURS;
342
- T.FLAGS = FLAGS;
343
- T.fx = fx;
344
- T.fxMoney = fxMoney;
345
- T.esc = esc;
346
- T.countUp = countUp;
347
- T.applyCurrency = applyCurrency;
348
- T.applyTheme = applyTheme;
349
- T.toast = toast;
350
- T.showTip = showTip;
351
- T.moveTip = moveTip;
352
- T.hideTip = hideTip;
353
- T.observe = observe;
354
- T.reduce = reduce;
355
- T.hasGsap = hasGsap;
356
- T.hasST = hasST;
357
- })();
@@ -1,166 +0,0 @@
1
- (function() {
2
- var T = window.Tersio;
3
- var CURS = T.CURS, FLAGS = T.FLAGS, fx = T.fx,
4
- applyCurrency = T.applyCurrency, toast = T.toast;
5
- (function initSettings() {
6
- var dlg = document.getElementById('settings');
7
- if (!dlg) return;
8
- var nav = document.getElementById('setNav');
9
- var title = document.getElementById('settingsTitle');
10
- var TITLES = { general: 'General', connection: 'Connection', diagnosis: 'Diagnosis', data: 'Data' };
11
- function show(name) {
12
- if (title) title.textContent = TITLES[name] || 'Settings';
13
- Array.prototype.forEach.call(nav.querySelectorAll('.set-navitem'), function(b) {
14
- b.classList.toggle('on', b.dataset.pane === name);
15
- });
16
- Array.prototype.forEach.call(dlg.querySelectorAll('.set-pane'), function(p) {
17
- p.classList.toggle('on', p.dataset.pane === name);
18
- });
19
- }
20
- nav.addEventListener('click', function(ev) {
21
- var b = ev.target.closest ? ev.target.closest('.set-navitem') : null;
22
- if (b) show(b.dataset.pane);
23
- });
24
- var search = document.getElementById('setSearch');
25
- search.addEventListener('input', function() {
26
- var q = search.value.trim().toLowerCase();
27
- Array.prototype.forEach.call(nav.querySelectorAll('.set-navitem'), function(b) {
28
- b.classList.toggle('hide', !!q && b.textContent.toLowerCase().indexOf(q) < 0);
29
- });
30
- });
31
- function escH(s) { return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;'); }
32
- function row(k, v, ok, sub) {
33
- return '<div class="set-row"><span><span class="k">' + k + '</span>' +
34
- (sub ? '<p class="s mono">' + sub + '</p>' : '') + '</span>' +
35
- '<span class="v"><span class="set-dot' + (ok === true ? ' ok' : ok === false ? ' bad' : '') + '"></span>' + v + '</span></div>';
36
- }
37
- var SNAP = window.__TERSIO_SNAP || null;
38
- function apiGet(name) {
39
- if (SNAP && SNAP[name]) return Promise.resolve({ json: function() { return SNAP[name]; } });
40
- return fetch(name);
41
- }
42
- // shadcn-style select: button + listbox panel, shared by currency/schedule.
43
- // Panel uses fixed positioning from the button rect so scroll containers
44
- // never clip it halfway.
45
- function buildSelect(btnId, panelId, labelId, options, current, onPick) {
46
- var btn = document.getElementById(btnId), panel = document.getElementById(panelId);
47
- function labelOf(val) {
48
- for (var i = 0; i < options.length; i++) if (options[i].val === val) return options[i].label;
49
- return options.length ? options[0].label : val;
50
- }
51
- function paint(val) {
52
- document.getElementById(labelId).textContent = labelOf(val);
53
- Array.prototype.forEach.call(panel.children, function(o) {
54
- o.setAttribute('aria-selected', o.dataset.val === val ? 'true' : 'false');
55
- });
56
- }
57
- options.forEach(function(opt) {
58
- var o = document.createElement('button');
59
- o.type = 'button'; o.className = 'selopt mono'; o.dataset.val = opt.val;
60
- o.setAttribute('role', 'option');
61
- o.innerHTML = '<span>' + opt.label + '</span><svg class="tick" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>';
62
- o.addEventListener('click', function() { open(false); onPick(opt.val); paint(opt.val); });
63
- panel.appendChild(o);
64
- });
65
- function open(showIt) {
66
- var will = showIt === undefined ? panel.classList.contains('hidden') : showIt;
67
- panel.classList.toggle('hidden', !will);
68
- btn.setAttribute('aria-expanded', will ? 'true' : 'false');
69
- if (will) {
70
- var r = btn.getBoundingClientRect();
71
- panel.style.top = Math.min(window.innerHeight - panel.offsetHeight - 8, r.bottom + 6) + 'px';
72
- panel.style.left = Math.max(8, r.right - panel.offsetWidth) + 'px';
73
- }
74
- }
75
- btn.addEventListener('click', function() { open(); });
76
- document.addEventListener('click', function(ev) {
77
- if (!panel.classList.contains('hidden') && !btn.contains(ev.target) && !panel.contains(ev.target)) open(false);
78
- });
79
- paint(current);
80
- return { paint: paint };
81
- }
82
- var curSel = buildSelect('setCurBtn', 'setCurPanel', 'setCurLabel',
83
- Object.keys(CURS).map(function(k) { return { val: k, label: FLAGS[k] + ' ' + k }; }),
84
- fx.cur, function(k) { applyCurrency(k); });
85
- var SCHEDS = [
86
- { val: 'manual', label: 'Manual' },
87
- { val: 'daily', label: 'Daily' },
88
- { val: 'weekly', label: 'Weekly' },
89
- { val: 'monthly', label: 'Monthly' },
90
- ];
91
- var schedSel = buildSelect('setSchedBtn', 'setSchedPanel', 'setSchedLabel', SCHEDS, 'manual', function(v) {
92
- if (window.location.protocol === 'file:') return;
93
- fetch('doctor', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ schedule: v }) })
94
- .then(function(r) { return r.json(); }).then(function(d) { paintDoctor(d); }).catch(function() { });
95
- });
96
- function ago(ts) {
97
- var s = Math.max(0, Math.round((Date.now() - ts) / 1000));
98
- if (s < 60) return 'just now';
99
- if (s < 3600) return Math.floor(s / 60) + 'm ago';
100
- if (s < 86400) return Math.floor(s / 3600) + 'h ago';
101
- return Math.floor(s / 86400) + 'd ago';
102
- }
103
- function paintDoctor(d) {
104
- var el = document.getElementById('setDoctor');
105
- var groups = [], seen = {};
106
- (d.rows || []).forEach(function(r) {
107
- var g = r.group || 'Other';
108
- if (!seen[g]) { seen[g] = true; groups.push(g); }
109
- });
110
- el.innerHTML = groups.map(function(g) {
111
- var items = (d.rows || []).filter(function(r) { return (r.group || 'Other') === g; }).map(function(r) {
112
- return row(escH(r.label), r.ok ? 'pass' : 'fix', !!r.ok, escH(r.detail || ''));
113
- }).join('');
114
- return '<p class="set-group">' + escH(g) + '</p>' + items;
115
- }).join('') || row('Diagnosis', 'empty', false);
116
- document.getElementById('setDiagChecked').textContent = d.checkedAt ? 'Checked ' + ago(d.checkedAt) + '.' : 'Never checked.';
117
- schedSel.paint(d.schedule || 'manual');
118
- }
119
- function loadHealth() {
120
- var el = document.getElementById('setHealth');
121
- apiGet('health').then(function(r) { return r.json(); }).then(function(h) {
122
- el.innerHTML =
123
- row('Status', h.omp ? 'Connected' : 'Offline', !!h.omp, h.omp ? 'wrapped with omp ' + escH(h.omp) : 'omp CLI not found');
124
- }).catch(function() { el.innerHTML = row('Status', 'unreachable', false); });
125
- }
126
- function loadDoctor(fresh) {
127
- var el = document.getElementById('setDoctor');
128
- el.innerHTML = skeleton();
129
- var snapMode = window.location.protocol === 'file:';
130
- apiGet(fresh ? 'doctor?fresh=1' : 'doctor').then(function(r) { return r.json(); }).then(function(d) {
131
- paintDoctor(d);
132
- if (fresh && snapMode) toast('Snapshot export', 'Live scan needs tersio gain.', 'scan-line');
133
- }).catch(function() { el.innerHTML = row('Diagnosis', 'unreachable', false); });
134
- }
135
- function skeleton() {
136
- var h = '';
137
- for (var i = 0; i < 5; i++) h += '<div class="skel-row" aria-hidden="true"><span class="skel-col"><span class="skel t"></span><span class="skel s"></span></span><span class="skel v"></span></div>';
138
- return h;
139
- }
140
- document.getElementById('setDiagRefresh').addEventListener('click', function() { loadDoctor(true); });
141
- document.getElementById('setFixIssues').addEventListener('click', function() {
142
- var label = document.getElementById('setFixLabel');
143
- if (window.location.protocol === 'file:') { toast('Serve with tersio gain', 'Fix runs on the live server only.', 'wrench'); return; }
144
- label.textContent = 'Fixing…';
145
- fetch('doctor/fix', { method: 'POST' }).then(function(r) { return r.json(); }).then(function(d) {
146
- label.textContent = 'Fix issues';
147
- if (d.failed && d.failed.length) toast('Fix incomplete', d.failed.join(', '), 'circle-alert');
148
- else toast('Fix done', 'Repairs applied. Restart OMP.', 'wrench');
149
- loadDoctor(true);
150
- }).catch(function() {
151
- label.textContent = 'Fix issues';
152
- toast('Fix failed', 'Could not reach the server.', 'circle-alert');
153
- });
154
- });
155
- var opened = false;
156
- dlg.addEventListener('close', function() { opened = false; document.body.style.overflow = ''; });
157
- new MutationObserver(function() {
158
- if (dlg.open && !opened) {
159
- opened = true; show('general');
160
- document.body.style.overflow = 'hidden';
161
- curSel.paint(fx.cur);
162
- loadHealth(); loadDoctor(false);
163
- }
164
- }).observe(dlg, { attributes: true, attributeFilter: ['open'] });
165
- })();
166
- })();