@wendellresearch-ai/benchwarmers 0.4.0 → 0.5.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.
- package/GATEWAY.md +9 -5
- package/PRIVACY.md +9 -3
- package/README.md +16 -8
- package/USAGE.md +73 -14
- package/VERSION +1 -1
- package/cli.py +13 -4
- package/gateway.py +34 -2
- package/launcher.py +2 -1
- package/metrics/discover.py +68 -18
- package/metrics/pacing.py +23 -3
- package/metrics/quotaview.py +460 -0
- package/metrics/scheduler.py +2 -1
- package/metrics/sources.py +29 -6
- package/package.json +1 -1
- package/scripts/check.py +1 -0
- package/scripts/test_hubs.py +10 -5
- package/scripts/test_no_owui.py +9 -0
- package/scripts/test_quota.py +12 -2
- package/scripts/test_serve_edit.py +55 -2
- package/serve.py +286 -101
- package/ui/bench.css +71 -6
- package/ui/bench.js +456 -39
package/ui/bench.js
CHANGED
|
@@ -20,9 +20,11 @@
|
|
|
20
20
|
// The page's own confirm, a Promise of true/false: window.confirm is
|
|
21
21
|
// silently false where a browser blocks dialogs. Opens over any open popup.
|
|
22
22
|
var askD = $('ask-dialog'), askDone = null;
|
|
23
|
-
function ask(text, yes, danger) {
|
|
23
|
+
function ask(text, yes, danger, onboard) {
|
|
24
24
|
if (askDone) askDone(false);
|
|
25
25
|
$('ask-text').textContent = text;
|
|
26
|
+
$('ask-onboard').hidden = !onboard; // Discover's Add: the Onboard dialog's mode and Vision box
|
|
27
|
+
$('ask-vision').checked = false;
|
|
26
28
|
$('ask-yes').textContent = yes || 'OK';
|
|
27
29
|
$('ask-yes').className = 'btn ' + (danger ? 'danger' : 'primary');
|
|
28
30
|
askD.showModal(); $('ask-no').focus();
|
|
@@ -33,6 +35,34 @@
|
|
|
33
35
|
$('ask-no').addEventListener('click', function () { answer(false); });
|
|
34
36
|
askD.addEventListener('close', function () { answer(false); }); // Escape
|
|
35
37
|
|
|
38
|
+
// ---- restart (serve.restart): the page reloads once the new one answers ----
|
|
39
|
+
var rs = $('restart');
|
|
40
|
+
function restart(force) {
|
|
41
|
+
var out = $('restart-out');
|
|
42
|
+
rs.disabled = true; out.textContent = 'Restarting…';
|
|
43
|
+
post('/api/restart', {force: force}).then(function (res) {
|
|
44
|
+
if (res.ok) return back(Date.now());
|
|
45
|
+
rs.disabled = false; out.textContent = '';
|
|
46
|
+
if (!res.busy) { listErrors(out, res.errors); return; }
|
|
47
|
+
ask('Still in progress: ' + res.errors.join('; ') + '. Restart anyway and cut them off?', 'Restart anyway', true)
|
|
48
|
+
.then(function (yes) { if (yes) restart(true); });
|
|
49
|
+
}).catch(function (err) { rs.disabled = false; out.textContent = String(err); });
|
|
50
|
+
}
|
|
51
|
+
// The old one stops half a second after answering; /health answers again once the new one is up.
|
|
52
|
+
function back(since) {
|
|
53
|
+
setTimeout(function () {
|
|
54
|
+
fetch('/health', {cache: 'no-store'}).then(function (r) { if (!r.ok) throw r.status; location.reload(); })
|
|
55
|
+
.catch(function () {
|
|
56
|
+
if (Date.now() - since < 120000) back(since);
|
|
57
|
+
else $('restart-out').textContent = 'Not back after two minutes: look at whatever runs it.';
|
|
58
|
+
});
|
|
59
|
+
}, 1500);
|
|
60
|
+
}
|
|
61
|
+
if (rs) rs.addEventListener('click', function () {
|
|
62
|
+
ask('Restart Benchwarmers? The dashboard and the gateway on this port stop for a few seconds.', 'Restart')
|
|
63
|
+
.then(function (yes) { if (yes) restart(false); });
|
|
64
|
+
});
|
|
65
|
+
|
|
36
66
|
// ---- tabs, remembered per browser ----------------------------------
|
|
37
67
|
var tabs = document.querySelectorAll('.tabs button[data-tab]'); // not the sidebar's own toggle
|
|
38
68
|
var onShow = {}; // a page's loader, run each time it is shown
|
|
@@ -743,19 +773,21 @@
|
|
|
743
773
|
function addVia(x, b) {
|
|
744
774
|
var direct = x.via === 'direct';
|
|
745
775
|
ask('Add ' + x.model + ' via ' + x.label + (x.free ? '' : ' (PAID)') + '?\n\n'
|
|
746
|
-
+ (direct ? 'The bench only: register it on the ' + x.endpoint + ' endpoint
|
|
776
|
+
+ (direct ? 'The bench only: register it on the ' + x.endpoint + ' endpoint and onboard it as chosen below.'
|
|
747
777
|
// A LiteLLM or CLI Proxy API route says its own steps: only what that hub takes.
|
|
748
778
|
: x.steps ? x.steps.map(function (s, i) { return (i + 1) + '. ' + s; }).join('\n')
|
|
749
779
|
: '1. Open WebUI: add it to the ' + x.connection + ' connection\'s Model IDs (if that connection lists them)\n'
|
|
750
780
|
+ '2. Open WebUI: a public model record, tagged and with the logo of the connection\'s other models\n'
|
|
751
|
-
+ '3. The bench: register
|
|
781
|
+
+ '3. The bench: register it and onboard it as chosen below'), 'Add', false, true).then(function (yes) {
|
|
782
|
+
if (yes) addNow(x, b, direct, {mode: $('ask-mode').value, vision: $('ask-vision').checked});
|
|
783
|
+
});
|
|
752
784
|
}
|
|
753
|
-
function addNow(x, b, direct) {
|
|
785
|
+
function addNow(x, b, direct, how) {
|
|
754
786
|
var title = 'Adding ' + x.model + ' via ' + x.label;
|
|
755
787
|
$('d-errors').textContent = '';
|
|
756
788
|
if (direct) {
|
|
757
789
|
b.disabled = true; b.textContent = 'Adding…';
|
|
758
|
-
post('/api/onboard-queue', {items: [{served: x.model, endpoint: x.endpoint}]}).then(function (res) {
|
|
790
|
+
post('/api/onboard-queue', {items: [Object.assign({served: x.model, endpoint: x.endpoint}, how)]}).then(function (res) {
|
|
759
791
|
if (!res.ok) { listErrors($('d-errors'), res.errors); b.disabled = false; b.textContent = 'Add via this'; return; }
|
|
760
792
|
dd.close(); preStep = null; o.errors.textContent = ''; od.showModal();
|
|
761
793
|
afterQueued(res, {served: x.model, endpoint: x.endpoint}, title, '');
|
|
@@ -770,7 +802,7 @@
|
|
|
770
802
|
runView(title, 'Adding it to ' + x.label + '…');
|
|
771
803
|
oq.box.hidden = false; oq.chip.classList.remove('done', 'fail'); oq.chip.style.setProperty('--fill', '3%');
|
|
772
804
|
oq.text.textContent = oq.text.title = title; oq.stop.hidden = oq.clear.hidden = true;
|
|
773
|
-
post('/api/discover/add', {connection: x.connection, model: x.model, endpoint: x.endpoint}).then(function (res) {
|
|
805
|
+
post('/api/discover/add', Object.assign({connection: x.connection, model: x.model, endpoint: x.endpoint}, how)).then(function (res) {
|
|
774
806
|
var hubDone = !!(res.served && res.steps && res.steps.length); // in the hub, even if queueing it was refused
|
|
775
807
|
preStep.state = hubDone ? 'done' : 'fail';
|
|
776
808
|
preStep.note = (res.steps || []).join('. ') + (hubDone ? '' : (res.steps && res.steps.length ? '. ' : '') + res.errors.join(' '));
|
|
@@ -802,8 +834,16 @@
|
|
|
802
834
|
if (res.errors.length) listErrors($('d-errors'), res.errors); // a hub that could not be asked
|
|
803
835
|
var r = res.model, p = $('d-profile'), list = $('d-routes');
|
|
804
836
|
$('d-title').textContent = r.name;
|
|
805
|
-
|
|
806
|
-
|
|
837
|
+
// Each source a link: FreeLLMAPI's page for the model where its sitemap has one, else its catalog.
|
|
838
|
+
var sv = $('d-served');
|
|
839
|
+
sv.textContent = r.provider_name + ' · ' + r.model + ' · listed by ';
|
|
840
|
+
r.sources.forEach(function (s, i) {
|
|
841
|
+
var fl = s === 'freellmapi';
|
|
842
|
+
if (i) sv.appendChild(document.createTextNode(' and '));
|
|
843
|
+
sv.appendChild(el('a', {href: fl ? r.page || 'https://freellmapi.co/models' : 'https://models.dev/',
|
|
844
|
+
target: '_blank', rel: 'noopener'}, fl ? 'FreeLLMAPI' : 'models.dev'));
|
|
845
|
+
});
|
|
846
|
+
if (r.on_bench_as) sv.appendChild(document.createTextNode(' · on the bench as ' + r.on_bench_as));
|
|
807
847
|
p.textContent = '';
|
|
808
848
|
[['Input', r.in_mods], ['Output', r.out_mods]].forEach(function (x) {
|
|
809
849
|
var row = el('div', {class: 'prof'});
|
|
@@ -814,16 +854,26 @@
|
|
|
814
854
|
var can = [['tools', r.tools], ['vision', r.vision], ['reasons', r.reasoning]]
|
|
815
855
|
.filter(function (x) { return x[1]; }).map(function (x) { return x[0]; });
|
|
816
856
|
var facts = [
|
|
857
|
+
r.rank != null ? 'smartest #' + r.rank + (r.speed ? ', fastest #' + r.speed : '') + ' on FreeLLMAPI' : '',
|
|
858
|
+
r.size || '',
|
|
817
859
|
r.context ? 'context ' + Number(r.context).toLocaleString() + ' tokens' : 'context unknown',
|
|
818
860
|
r.output ? 'output up to ' + Number(r.output).toLocaleString() : '',
|
|
819
861
|
can.length ? 'can: ' + can.join(', ') : '',
|
|
820
862
|
price(r),
|
|
821
863
|
r.limits ? 'limits ' + limits(r.limits) : '',
|
|
864
|
+
r.budget ? r.budget + ' tokens a month' : '',
|
|
822
865
|
r.released ? 'released ' + r.released : '',
|
|
823
866
|
r.knowledge ? 'knowledge to ' + r.knowledge : '',
|
|
824
867
|
r.open_weights != null ? (r.open_weights ? 'open weights' : 'closed weights') : '',
|
|
825
868
|
'first seen ' + (r.first_seen === 'baseline' ? 'before the list began' : r.first_seen)];
|
|
826
869
|
p.appendChild(el('div', {class: 'prof'}, facts.filter(Boolean).join(' · ')));
|
|
870
|
+
// FreeLLMAPI's operating notes for this provider, e.g. thinking tokens that eat the output cap.
|
|
871
|
+
(r.quirks || []).forEach(function (q) {
|
|
872
|
+
var row = el('div', {class: 'prof'});
|
|
873
|
+
row.appendChild(el('strong', {}, q.title + ': '));
|
|
874
|
+
row.appendChild(document.createTextNode(q.body || ''));
|
|
875
|
+
p.appendChild(row);
|
|
876
|
+
});
|
|
827
877
|
if (r.also_on && r.also_on.length) p.appendChild(el('div', {class: 'prof muted'}, 'Also on the bench as ' + r.also_on.join(', ')));
|
|
828
878
|
list.textContent = '';
|
|
829
879
|
res.routes.forEach(function (x) {
|
|
@@ -1279,7 +1329,8 @@
|
|
|
1279
1329
|
shape: $('c-shape'), desc: $('c-desc'), tags: $('c-tags'), errors: $('c-errors'), done: $('c-done'),
|
|
1280
1330
|
head: $('c-form-head'), maxCalls: $('g-max-calls'), maxExp: $('g-max-explorers'),
|
|
1281
1331
|
gErrors: $('g-errors'), gDone: $('g-done'), form: $('c-form'), status: $('c-status'),
|
|
1282
|
-
del: $('c-delete'), reply: $('c-reply'), merger: $('c-merger'), find: $('c-member-filter')
|
|
1332
|
+
del: $('c-delete'), reply: $('c-reply'), merger: $('c-merger'), find: $('c-member-filter'),
|
|
1333
|
+
seats: $('c-seats')};
|
|
1283
1334
|
var C = null, open = null; // the collection whose details are showing
|
|
1284
1335
|
function explorersFor(n) {
|
|
1285
1336
|
return Math.max(0, Math.min(C.settings.max_explorers, C.settings.max_calls - n));
|
|
@@ -1299,10 +1350,37 @@
|
|
|
1299
1350
|
(shown.length < rows.length ? ' · showing ' + shown.length + ' of ' + rows.length + ' models' : '') +
|
|
1300
1351
|
(paid ? ' (' + paid + ' paid hidden by the free tag)' : '') +
|
|
1301
1352
|
(greyed ? ' · ' + greyed + ' greyed out by the tags (hover one for why)' : '') +
|
|
1302
|
-
(clash ? ' · ' + clash + ' ticked member' + (clash === 1 ? ' does' : 's do') + ' not fit the tags' : '')
|
|
1353
|
+
(clash ? ' · ' + clash + ' ticked member' + (clash === 1 ? ' does' : 's do') + ' not fit the tags' : '') +
|
|
1354
|
+
twice().map(function (t) {
|
|
1355
|
+
return ' · ' + t.name + ' is ticked on ' + t.on.join(' and ') + ': one model agrees with itself, it does not check itself';
|
|
1356
|
+
}).join('');
|
|
1303
1357
|
c.members.querySelectorAll('input').forEach(function (x) {
|
|
1304
1358
|
x.disabled = !x.checked && (n >= C.settings.max_calls || !!x.dataset.blocked);
|
|
1305
1359
|
});
|
|
1360
|
+
drawSeats(ex);
|
|
1361
|
+
}
|
|
1362
|
+
function fact(alias) { return (C.facts || {})[alias] || {}; }
|
|
1363
|
+
// The same model ticked on two providers (route.family_of, as the gateway
|
|
1364
|
+
// keeps explorers off a member's model): allowed, and said.
|
|
1365
|
+
function twice() {
|
|
1366
|
+
var by = {};
|
|
1367
|
+
picked().forEach(function (a) { var f = fact(a).family || a; (by[f] = by[f] || []).push(a); });
|
|
1368
|
+
return Object.keys(by).filter(function (f) { return by[f].length > 1; }).map(function (f) {
|
|
1369
|
+
var row = c.members.querySelector('.m-row[data-family="' + CSS.escape(f) + '"]');
|
|
1370
|
+
return {name: row ? row.dataset.name : by[f][0], on: by[f].map(function (a) { return fact(a).provider || a; })};
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
// The panel as it will sit, as the Collections list draws it.
|
|
1374
|
+
function drawSeats(ex) {
|
|
1375
|
+
c.seats.textContent = '';
|
|
1376
|
+
picked().forEach(function (a) {
|
|
1377
|
+
var seat = el('span', {class: 'seat'}, a);
|
|
1378
|
+
if (fact(a).provider) seat.appendChild(el('span', {class: 'k after'}, fact(a).provider));
|
|
1379
|
+
c.seats.appendChild(seat);
|
|
1380
|
+
});
|
|
1381
|
+
for (var i = 0; i < ex; i++) {
|
|
1382
|
+
c.seats.appendChild(el('span', {class: 'seat explorer'}, 'explorer' + (c.shape.value ? ' · ' + c.shape.value : '')));
|
|
1383
|
+
}
|
|
1306
1384
|
}
|
|
1307
1385
|
// Tags that say what every member must do: why a model does not fit them
|
|
1308
1386
|
// (none: it fits). From collections_view's facts; an unknown context is
|
|
@@ -1340,20 +1418,28 @@
|
|
|
1340
1418
|
lab.classList.toggle('clash', why.length > 0 && box.checked);
|
|
1341
1419
|
lab.dataset.paid = paid ? '1' : '';
|
|
1342
1420
|
if (why.length) box.dataset.blocked = '1'; else delete box.dataset.blocked;
|
|
1421
|
+
// A chip says only the provider: the alias and served name are its first line.
|
|
1343
1422
|
var served = lab.dataset.served;
|
|
1344
|
-
lab.title = (served && served !== box.value ?
|
|
1423
|
+
lab.title = [box.value + (served && served !== box.value ? ' (' + served + ')' : '')].concat(why).join('\n');
|
|
1345
1424
|
});
|
|
1346
1425
|
filterMembers();
|
|
1347
1426
|
}
|
|
1348
1427
|
// The member search: the filter boxes' match (any text, case-insensitive),
|
|
1349
|
-
// over
|
|
1350
|
-
//
|
|
1428
|
+
// over the model's name and each chip's alias, served name and provider.
|
|
1429
|
+
// It only hides chips, and never a ticked one, so what is picked is always
|
|
1430
|
+
// in view and every count still includes it; a row goes when all its chips do.
|
|
1351
1431
|
function filterMembers() {
|
|
1352
1432
|
var q = c.find.value.trim().toLowerCase();
|
|
1353
|
-
c.members.querySelectorAll('
|
|
1354
|
-
var
|
|
1355
|
-
|
|
1356
|
-
|
|
1433
|
+
c.members.querySelectorAll('.m-row').forEach(function (row) {
|
|
1434
|
+
var named = !q || row.dataset.name.toLowerCase().indexOf(q) !== -1, any = false;
|
|
1435
|
+
row.querySelectorAll('label.check').forEach(function (lab) {
|
|
1436
|
+
var box = lab.querySelector('input');
|
|
1437
|
+
var text = (box.value + ' ' + (lab.dataset.served || '') + ' ' + (fact(box.value).connection || '') + ' ' +
|
|
1438
|
+
lab.textContent).toLowerCase();
|
|
1439
|
+
lab.hidden = !box.checked && (!!lab.dataset.paid || !named && text.indexOf(q) === -1);
|
|
1440
|
+
any = any || !lab.hidden;
|
|
1441
|
+
});
|
|
1442
|
+
row.hidden = !any;
|
|
1357
1443
|
});
|
|
1358
1444
|
recount();
|
|
1359
1445
|
}
|
|
@@ -1384,6 +1470,7 @@
|
|
|
1384
1470
|
c.tags.addEventListener('input', function () { drawTagPicks(); applyTags(); });
|
|
1385
1471
|
function showMerger() { $('c-merger-row').hidden = c.reply.value !== 'merged'; }
|
|
1386
1472
|
c.reply.addEventListener('change', showMerger);
|
|
1473
|
+
c.shape.addEventListener('change', recount); // the explorer seats say their shape
|
|
1387
1474
|
// A collection opens in a popup, as every other edit on the dashboard does:
|
|
1388
1475
|
// the one clicked, or a new one from New collection.
|
|
1389
1476
|
function closeForm() {
|
|
@@ -1400,14 +1487,37 @@
|
|
|
1400
1487
|
c.desc.value = row ? row.description : ''; c.tags.value = row ? row.tags.join(', ') : '';
|
|
1401
1488
|
drawTagPicks();
|
|
1402
1489
|
c.members.textContent = ''; c.find.value = '';
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1490
|
+
// One row per model, named by its shortest alias, with what the first
|
|
1491
|
+
// route says it holds and can do; each provider it is on is a chip that
|
|
1492
|
+
// ticks that alias. A chip whose window differs from the row's says its own.
|
|
1493
|
+
var groups = {};
|
|
1494
|
+
C.models.forEach(function (a) { var f = fact(a).family || a; (groups[f] = groups[f] || []).push(a); });
|
|
1495
|
+
Object.keys(groups).map(function (f) {
|
|
1496
|
+
var routes = groups[f].slice().sort(function (x, y) { return x.length - y.length || (x < y ? -1 : 1); });
|
|
1497
|
+
return {family: f, name: routes[0], routes: routes};
|
|
1498
|
+
}).sort(function (x, y) { return x.name.toLowerCase() < y.name.toLowerCase() ? -1 : 1; }).forEach(function (g) {
|
|
1499
|
+
var first = fact(g.name), caps = first.caps || {};
|
|
1500
|
+
var can = ['vision', 'tools'].filter(function (k) { return caps[k]; });
|
|
1501
|
+
var line = el('div', {class: 'm-row', role: 'group', 'aria-label': g.name, 'data-family': g.family, 'data-name': g.name});
|
|
1502
|
+
var name = el('div', {class: 'm-name'});
|
|
1503
|
+
name.appendChild(el('span', {class: 'mono'}, g.name));
|
|
1504
|
+
name.appendChild(el('span', {class: 'm-facts'}, [first.context ? tokens(first.context) : '', can.join(', '),
|
|
1505
|
+
first.price === 'unknown' ? '' : first.price].filter(Boolean).join(' · ')));
|
|
1506
|
+
line.appendChild(name);
|
|
1507
|
+
var chips = el('div', {class: 'm-routes'});
|
|
1508
|
+
g.routes.forEach(function (alias) {
|
|
1509
|
+
var f = fact(alias);
|
|
1510
|
+
var box = el('input', {type: 'checkbox', value: alias, 'aria-label': alias + ' on ' + (f.provider || 'its endpoint')});
|
|
1511
|
+
box.checked = !!row && row.members.indexOf(alias) !== -1;
|
|
1512
|
+
box.addEventListener('change', applyTags); // ticked or not changes what a misfit shows as
|
|
1513
|
+
var lab = el('label', {class: 'check', 'data-served': (C.served || {})[alias] || ''});
|
|
1514
|
+
lab.appendChild(box);
|
|
1515
|
+
lab.appendChild(el('span', null, (f.provider || alias) +
|
|
1516
|
+
(f.context && first.context && f.context !== first.context ? ' · ' + tokens(f.context) : '')));
|
|
1517
|
+
chips.appendChild(lab);
|
|
1518
|
+
});
|
|
1519
|
+
line.appendChild(chips);
|
|
1520
|
+
c.members.appendChild(line);
|
|
1411
1521
|
});
|
|
1412
1522
|
c.shape.textContent = '';
|
|
1413
1523
|
c.shape.appendChild(el('option', {value: ''}, 'automatic'));
|
|
@@ -1418,10 +1528,18 @@
|
|
|
1418
1528
|
c.merger.textContent = '';
|
|
1419
1529
|
c.merger.appendChild(el('option', {value: ''}, 'automatic'));
|
|
1420
1530
|
C.models.forEach(function (alias) {
|
|
1421
|
-
var o = el('option', {value: alias}, alias
|
|
1531
|
+
var o = el('option', {value: alias}, alias + (fact(alias).provider ? ' · ' + fact(alias).provider : ''));
|
|
1532
|
+
if (row && row.merger === alias) o.selected = true; c.merger.appendChild(o);
|
|
1422
1533
|
});
|
|
1423
1534
|
showMerger(); applyTags();
|
|
1424
1535
|
}
|
|
1536
|
+
// What a collection's members can all do, in the words the tag checks use.
|
|
1537
|
+
var CAN = {reasoning: 'reason'};
|
|
1538
|
+
Object.keys(CAP_TAGS).forEach(function (t) { CAN[CAP_TAGS[t][0]] = CAP_TAGS[t][1]; });
|
|
1539
|
+
function tokens(n) { // 512k, 1M (2^20), 1.9M
|
|
1540
|
+
return n >= 1000000 ? String(Math.round(n / 104857.6) / 10).replace(/\.0$/, '') + 'M'
|
|
1541
|
+
: Math.floor(n / 1000).toLocaleString() + 'k';
|
|
1542
|
+
}
|
|
1425
1543
|
function loadCollections(keep) {
|
|
1426
1544
|
fetch('/api/collections').then(function (r) { return r.json(); }).then(function (res) {
|
|
1427
1545
|
C = res; c.list.textContent = '';
|
|
@@ -1430,24 +1548,38 @@
|
|
|
1430
1548
|
var box = el('button', {class: 'endpoint c-row', type: 'button', 'data-name': row.name,
|
|
1431
1549
|
'aria-label': 'Open bench/' + row.name});
|
|
1432
1550
|
var left = el('div');
|
|
1433
|
-
var title = el('div', {class: '
|
|
1434
|
-
title.appendChild(el('span', {class: 'alias'}, 'bench/' + row.name
|
|
1435
|
-
row.tags.forEach(function (t) { title.appendChild(el('span', {class: 'chip'
|
|
1551
|
+
var title = el('div', {class: 'top'});
|
|
1552
|
+
title.appendChild(el('span', {class: 'alias'}, 'bench/' + row.name));
|
|
1553
|
+
row.tags.forEach(function (t) { title.appendChild(el('span', {class: 'chip'}, t)); });
|
|
1436
1554
|
left.appendChild(title);
|
|
1437
|
-
if (row.description) left.appendChild(el('
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1555
|
+
if (row.description) left.appendChild(el('p', {class: 'desc'}, row.description));
|
|
1556
|
+
// The panel as it sits: a seat per member, an empty one per explorer,
|
|
1557
|
+
// and the merger after an arrow.
|
|
1558
|
+
var seats = el('div', {class: 'seats'});
|
|
1559
|
+
row.members.forEach(function (m) { seats.appendChild(el('span', {class: 'seat'}, m)); });
|
|
1560
|
+
for (var i = 0; i < row.explorers; i++) {
|
|
1561
|
+
seats.appendChild(el('span', {class: 'seat explorer'}, 'explorer' + (row.shape ? ' · ' + row.shape : '')));
|
|
1562
|
+
}
|
|
1563
|
+
if (row.reply === 'merged') {
|
|
1564
|
+
var merge = el('span', {class: 'merge'}), seat = el('span', {class: 'seat merger'});
|
|
1565
|
+
merge.appendChild(el('span', {'aria-hidden': 'true'}, '→'));
|
|
1566
|
+
seat.appendChild(el('span', {class: 'k'}, 'merger'));
|
|
1567
|
+
seat.appendChild(document.createTextNode(row.merger || 'chosen by verdicts'));
|
|
1568
|
+
merge.appendChild(seat); seats.appendChild(merge);
|
|
1569
|
+
}
|
|
1570
|
+
left.appendChild(seats);
|
|
1441
1571
|
var t = row.together;
|
|
1442
1572
|
if (t) {
|
|
1443
1573
|
var lim = Object.keys(t.limits || {}).filter(function (k) { return t.limits[k]; })
|
|
1444
1574
|
.map(function (k) { return t.limits[k].toLocaleString() + ' ' + k; });
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
'
|
|
1449
|
-
|
|
1450
|
-
(
|
|
1575
|
+
var can = t.capabilities.map(function (k) { return CAN[k] || k; });
|
|
1576
|
+
var n = t.connections.length + ' connection' + (t.connections.length === 1 ? '' : 's');
|
|
1577
|
+
left.appendChild(el('p', {class: 'together'},
|
|
1578
|
+
(t.context_window ? 'Prompts up to ' + tokens(t.context_window) + ' tokens' : 'Context window unknown') +
|
|
1579
|
+
(t.context_unknown.length ? ' (unknown for ' + t.context_unknown.join(', ') + '). ' : '. ') +
|
|
1580
|
+
(can.length ? 'All can ' + (can.length > 1 ? can.slice(0, -1).join(', ') + ' and ' : '') + can[can.length - 1] + '. '
|
|
1581
|
+
: 'Nothing known they can all do. ') +
|
|
1582
|
+
(lim.length ? 'Tightest limit ' + lim.join(', ') + ' across ' + n + '.' : 'No limits known across ' + n + '.')));
|
|
1451
1583
|
}
|
|
1452
1584
|
box.appendChild(left);
|
|
1453
1585
|
box.appendChild(el('span', {class: 'open-hint', 'aria-hidden': 'true'}, 'Open ›'));
|
|
@@ -1544,6 +1676,290 @@
|
|
|
1544
1676
|
}).catch(function (err) { $('v-errors').textContent = String(err); });
|
|
1545
1677
|
});
|
|
1546
1678
|
|
|
1679
|
+
// ---- Quota: a tile per provider (serve.quota_tiles, metrics/quotaview.py) ----
|
|
1680
|
+
// Each tile takes its provider's shape: groups of windows (a 5-hour and a
|
|
1681
|
+
// weekly limit boxed together), one pool (OpenRouter's free requests a day,
|
|
1682
|
+
// a bar per key) or, where nothing is stated, each model's last day. A
|
|
1683
|
+
// meter is ten segments filled like a bar: 85% is eight and a half.
|
|
1684
|
+
var QD = $('quota-data') ? JSON.parse($('quota-data').textContent) : null;
|
|
1685
|
+
var qBox = $('q-tiles');
|
|
1686
|
+
function qWhen(iso) {
|
|
1687
|
+
if (!iso) return '';
|
|
1688
|
+
var d = new Date(iso), s = (d - Date.now()) / 1000;
|
|
1689
|
+
if (s <= 0) return 'now';
|
|
1690
|
+
if (s < 86400) {
|
|
1691
|
+
var h = Math.floor(s / 3600), m = Math.round((s % 3600) / 60);
|
|
1692
|
+
return 'in ' + (h ? h + 'h ' : '') + m + 'm';
|
|
1693
|
+
}
|
|
1694
|
+
if (s < 6 * 86400) return d.toLocaleDateString([], {weekday: 'short'}) + ' ' + d.toLocaleTimeString([], {hour: 'numeric', minute: '2-digit'});
|
|
1695
|
+
return d.toLocaleDateString([], {month: 'numeric', day: 'numeric'}) + ' ' + d.toLocaleTimeString([], {hour: 'numeric', minute: '2-digit'});
|
|
1696
|
+
}
|
|
1697
|
+
function qAgo(iso) {
|
|
1698
|
+
var s = (Date.now() - new Date(iso)) / 1000;
|
|
1699
|
+
return s < 90 ? 'just now' : s < 5400 ? Math.round(s / 60) + ' min ago' : s < 129600 ? Math.round(s / 3600) + ' h ago' : Math.round(s / 86400) + ' days ago';
|
|
1700
|
+
}
|
|
1701
|
+
function qTone(left) { return left >= 70 ? 'ok' : left >= 25 ? 'warn' : 'err'; }
|
|
1702
|
+
function qSegs(left) {
|
|
1703
|
+
var box = el('div', {class: 'segs ' + qTone(left), role: 'img', 'aria-label': left + '% left'});
|
|
1704
|
+
for (var i = 0; i < 10; i++) {
|
|
1705
|
+
var s = el('span'), f = el('i');
|
|
1706
|
+
f.style.width = Math.round(Math.min(Math.max(left / 10 - i, 0), 1) * 100) + '%';
|
|
1707
|
+
s.appendChild(f); box.appendChild(s);
|
|
1708
|
+
}
|
|
1709
|
+
return box;
|
|
1710
|
+
}
|
|
1711
|
+
function qLine(label, right) {
|
|
1712
|
+
var row = el('div', {class: 'q-line'});
|
|
1713
|
+
row.appendChild(el('span', {}, label));
|
|
1714
|
+
var r = el('span'); r.innerHTML = right; row.appendChild(r);
|
|
1715
|
+
return row;
|
|
1716
|
+
}
|
|
1717
|
+
function qNum(n) { return Number(n).toLocaleString(); }
|
|
1718
|
+
function qWindow(w) {
|
|
1719
|
+
var frag = document.createDocumentFragment();
|
|
1720
|
+
var row = qLine(w.label, '<b>' + w.left + '%</b>' + (w.reset_at ? ' · ' + qWhen(w.reset_at) : ''));
|
|
1721
|
+
if (w.of) row.title = qNum(w.remaining) + ' of ' + qNum(w.of) + ' left';
|
|
1722
|
+
frag.appendChild(row);
|
|
1723
|
+
frag.appendChild(qSegs(w.left));
|
|
1724
|
+
return frag;
|
|
1725
|
+
}
|
|
1726
|
+
function qResting(m) { return m.until && new Date(m.until) > Date.now(); }
|
|
1727
|
+
function qTile(t) {
|
|
1728
|
+
var node = el('article', {class: 'q-tile' + (t.out ? ' out' : ''), draggable: 'true', tabindex: '0',
|
|
1729
|
+
'data-id': t.id, 'aria-label': t.name + ': drag, or use the arrow keys, to move it'});
|
|
1730
|
+
var head = el('div', {class: 'q-head'});
|
|
1731
|
+
head.appendChild(el('b', {class: 'nm'}, t.name));
|
|
1732
|
+
var f = t.fresh, how = f.how === 'live' ? 'read ' : f.how === 'seen' ? 'seen ' : 'answered ';
|
|
1733
|
+
head.appendChild(el('span', {class: 'chip ' + (f.how === 'live' ? 'ok' : 'via')}, f.at ? how + qAgo(f.at) : 'nothing yet'));
|
|
1734
|
+
node.appendChild(head);
|
|
1735
|
+
node.appendChild(el('div', {class: 'q-tag mono'}, t.tag + (t.plan ? ' · ' + t.plan : '')));
|
|
1736
|
+
if (t.kind === 'groups') {
|
|
1737
|
+
t.groups.forEach(function (g) {
|
|
1738
|
+
var box = el('section', {class: g.label ? 'q-group' : 'q-group bare'});
|
|
1739
|
+
if (g.label) box.appendChild(el('h4', {}, g.label));
|
|
1740
|
+
g.windows.forEach(function (w) { box.appendChild(qWindow(w)); });
|
|
1741
|
+
node.appendChild(box);
|
|
1742
|
+
});
|
|
1743
|
+
} else if (t.kind === 'pool') {
|
|
1744
|
+
t.pool.forEach(function (p) {
|
|
1745
|
+
var box = el('section', {class: 'q-group bare'});
|
|
1746
|
+
if (p.of) {
|
|
1747
|
+
box.appendChild(qLine(p.label, '<b>' + qNum(Math.max(0, p.of - p.used)) + '</b> of ' + qNum(p.of) + ' left'
|
|
1748
|
+
+ (p.reset_at ? ' · ' + qWhen(p.reset_at) : '')));
|
|
1749
|
+
box.appendChild(qSegs(p.left));
|
|
1750
|
+
} else {
|
|
1751
|
+
box.appendChild(qLine(p.label, '<b>' + qNum(p.used) + '</b> sent · limit not known yet'));
|
|
1752
|
+
}
|
|
1753
|
+
node.appendChild(box);
|
|
1754
|
+
});
|
|
1755
|
+
} else {
|
|
1756
|
+
var shown = t.models.slice(0, 6);
|
|
1757
|
+
shown.forEach(function (m) {
|
|
1758
|
+
var right = qResting(m) ? '<span class="warn-t">resting · ' + qWhen(m.until) + '</span>'
|
|
1759
|
+
: m.ok + m.failed ? m.ok + ' ok · ' + m.failed + ' failed' : 'no calls today';
|
|
1760
|
+
var row = qLine('', right); row.firstChild.className = 'mono'; row.firstChild.textContent = m.alias;
|
|
1761
|
+
node.appendChild(row);
|
|
1762
|
+
});
|
|
1763
|
+
if (t.models.length > shown.length) node.appendChild(el('div', {class: 'muted q-small'}, '+ ' + (t.models.length - shown.length) + ' more in Models · last 24 h'));
|
|
1764
|
+
else node.appendChild(el('div', {class: 'muted q-small'}, 'last 24 h'));
|
|
1765
|
+
}
|
|
1766
|
+
if (t.kind !== 'models') {
|
|
1767
|
+
t.models.filter(qResting).forEach(function (m) {
|
|
1768
|
+
node.appendChild(el('div', {class: 'q-rest'}, m.alias + ' resting · back ' + qWhen(m.until)));
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
if (t.conn_limit) node.appendChild(el('div', {class: 'muted q-small'}, t.conn_limit + ', set by you'));
|
|
1772
|
+
(t.notes || []).forEach(function (n) { node.appendChild(el('div', {class: 'muted q-small'}, n)); });
|
|
1773
|
+
var msg = el('div', {class: 'q-msg', 'aria-live': 'polite'});
|
|
1774
|
+
node.appendChild(msg);
|
|
1775
|
+
var foot = el('div', {class: 'q-foot'});
|
|
1776
|
+
var rb = el('button', {class: 'btn small', type: 'button', 'aria-label': 'Refresh ' + t.name,
|
|
1777
|
+
title: t.refresh === 'live' ? 'Ask the provider what is left (free)' : 'Read the bench\'s own records again (free)'});
|
|
1778
|
+
rb.innerHTML = '↻ Refresh';
|
|
1779
|
+
rb.addEventListener('click', function () { qAct('refresh', t.id, node); });
|
|
1780
|
+
foot.appendChild(rb);
|
|
1781
|
+
if (t.check) {
|
|
1782
|
+
var cb = el('button', {class: 'btn small', type: 'button', title: 'One short call to one of its models; its reply says what is left'}, 'Check now · 1 request');
|
|
1783
|
+
cb.addEventListener('click', function () { qAct('check', t.id, node); });
|
|
1784
|
+
foot.appendChild(cb);
|
|
1785
|
+
}
|
|
1786
|
+
var mb = el('button', {class: 'btn small', type: 'button'}, 'Models ');
|
|
1787
|
+
mb.appendChild(el('span', {class: 'chip ' + (t.live === t.bench ? 'ok' : t.live ? 'warn' : 'err')}, t.live + '/' + t.bench));
|
|
1788
|
+
mb.title = t.live + ' of the ' + t.bench + ' bench models on it are free to call';
|
|
1789
|
+
mb.addEventListener('click', function () { qModels(t); });
|
|
1790
|
+
foot.appendChild(mb);
|
|
1791
|
+
node.appendChild(foot);
|
|
1792
|
+
return node;
|
|
1793
|
+
}
|
|
1794
|
+
function qAct(which, id, node) {
|
|
1795
|
+
var msg = node.querySelector('.q-msg');
|
|
1796
|
+
msg.className = 'q-msg muted'; msg.textContent = which === 'check' ? 'Asking…' : 'Refreshing…';
|
|
1797
|
+
return post('/api/quota/' + which, {tag: id}).then(function (r) {
|
|
1798
|
+
if (!r.ok) { msg.className = 'q-msg errors'; msg.textContent = r.errors.join('; '); return; }
|
|
1799
|
+
var i = QD.tiles.findIndex(function (t) { return t.id === id; });
|
|
1800
|
+
if (r.tile && i >= 0) {
|
|
1801
|
+
QD.tiles[i] = r.tile;
|
|
1802
|
+
var fresh = qTile(r.tile);
|
|
1803
|
+
node.replaceWith(fresh);
|
|
1804
|
+
if (r.note) { var m = fresh.querySelector('.q-msg'); m.className = 'q-msg muted'; m.textContent = r.note; }
|
|
1805
|
+
}
|
|
1806
|
+
drawCharts();
|
|
1807
|
+
}).catch(function (e) { msg.className = 'q-msg errors'; msg.textContent = String(e); });
|
|
1808
|
+
}
|
|
1809
|
+
function qModels(t) {
|
|
1810
|
+
$('q-models-h').textContent = t.name + ': ' + t.bench + ' on the bench';
|
|
1811
|
+
$('q-models-sub').textContent = t.live + ' free to call now · the last 24 hours of calls';
|
|
1812
|
+
var list = $('q-models-list'); list.textContent = '';
|
|
1813
|
+
var tb = el('table'), hd = el('tr');
|
|
1814
|
+
['Model', 'Now', 'Last 24 h', 'Last answered', 'Known limit'].forEach(function (h) { hd.appendChild(el('th', {}, h)); });
|
|
1815
|
+
tb.appendChild(el('thead')).appendChild(hd);
|
|
1816
|
+
var body = tb.appendChild(el('tbody'));
|
|
1817
|
+
t.models.forEach(function (m) {
|
|
1818
|
+
var tr = el('tr');
|
|
1819
|
+
tr.appendChild(el('td', {class: 'mono'}, m.alias));
|
|
1820
|
+
var now = el('td'); now.appendChild(el('span', {class: 'chip ' + (qResting(m) ? 'warn' : 'ok')}, qResting(m) ? 'resting · ' + qWhen(m.until) : 'ready'));
|
|
1821
|
+
tr.appendChild(now);
|
|
1822
|
+
tr.appendChild(el('td', {}, m.ok + ' ok · ' + m.failed + ' failed'));
|
|
1823
|
+
tr.appendChild(el('td', {}, m.last_ok ? qAgo(m.last_ok) : '—'));
|
|
1824
|
+
tr.appendChild(el('td', {}, m.limit || '—'));
|
|
1825
|
+
body.appendChild(tr);
|
|
1826
|
+
});
|
|
1827
|
+
list.appendChild(tb);
|
|
1828
|
+
$('q-models').showModal();
|
|
1829
|
+
}
|
|
1830
|
+
function qDefault(a, b) {
|
|
1831
|
+
return (b.out - a.out) || ((a.next_reset || '9999') < (b.next_reset || '9999') ? -1 : (a.next_reset || '9999') > (b.next_reset || '9999') ? 1 : 0)
|
|
1832
|
+
|| (a.id < b.id ? -1 : 1);
|
|
1833
|
+
}
|
|
1834
|
+
function qDraw() {
|
|
1835
|
+
qBox.textContent = '';
|
|
1836
|
+
QD.tiles.forEach(function (t) { qBox.appendChild(qTile(t)); });
|
|
1837
|
+
if (!QD.tiles.length) qBox.appendChild(el('p', {class: 'muted'}, 'No model on the bench yet.'));
|
|
1838
|
+
}
|
|
1839
|
+
function qSaveOrder() {
|
|
1840
|
+
var ids = Array.prototype.map.call(qBox.querySelectorAll('.q-tile'), function (n) { return n.dataset.id; });
|
|
1841
|
+
var at = {}; ids.forEach(function (id, i) { at[id] = i; });
|
|
1842
|
+
QD.tiles.sort(function (a, b) { return at[a.id] - at[b.id]; });
|
|
1843
|
+
post('/api/quota/order', {order: ids}).then(function (r) {
|
|
1844
|
+
$('q-done').textContent = r.ok ? 'Order kept.' : ''; $('q-errors').textContent = r.ok ? '' : r.errors.join('; ');
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
if (QD && qBox) {
|
|
1848
|
+
qDraw();
|
|
1849
|
+
$('q-models-close').addEventListener('click', function () { $('q-models').close(); });
|
|
1850
|
+
var dragged = null;
|
|
1851
|
+
qBox.addEventListener('dragstart', function (e) {
|
|
1852
|
+
dragged = e.target.closest && e.target.closest('.q-tile');
|
|
1853
|
+
if (!dragged) return;
|
|
1854
|
+
dragged.classList.add('dragging');
|
|
1855
|
+
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', dragged.dataset.id);
|
|
1856
|
+
});
|
|
1857
|
+
qBox.addEventListener('dragover', function (e) {
|
|
1858
|
+
if (!dragged) return;
|
|
1859
|
+
e.preventDefault();
|
|
1860
|
+
var over = e.target.closest && e.target.closest('.q-tile');
|
|
1861
|
+
if (!over || over === dragged) return;
|
|
1862
|
+
var r = over.getBoundingClientRect();
|
|
1863
|
+
var after = e.clientX > r.left + r.width / 2;
|
|
1864
|
+
over.parentNode.insertBefore(dragged, after ? over.nextSibling : over);
|
|
1865
|
+
});
|
|
1866
|
+
qBox.addEventListener('dragend', function () {
|
|
1867
|
+
if (!dragged) return;
|
|
1868
|
+
dragged.classList.remove('dragging'); dragged = null; qSaveOrder();
|
|
1869
|
+
});
|
|
1870
|
+
qBox.addEventListener('keydown', function (e) {
|
|
1871
|
+
var t = e.target;
|
|
1872
|
+
if (!t.classList || !t.classList.contains('q-tile')) return;
|
|
1873
|
+
var back = e.key === 'ArrowLeft' || e.key === 'ArrowUp', fwd = e.key === 'ArrowRight' || e.key === 'ArrowDown';
|
|
1874
|
+
if (!back && !fwd) return;
|
|
1875
|
+
e.preventDefault();
|
|
1876
|
+
var sib = back ? t.previousElementSibling : t.nextElementSibling;
|
|
1877
|
+
if (!sib) return;
|
|
1878
|
+
qBox.insertBefore(t, back ? sib : sib.nextSibling);
|
|
1879
|
+
t.focus(); qSaveOrder();
|
|
1880
|
+
});
|
|
1881
|
+
$('q-reorder').addEventListener('click', function () {
|
|
1882
|
+
QD.tiles.sort(qDefault); qDraw();
|
|
1883
|
+
post('/api/quota/order', {order: []}).then(function (r) { $('q-done').textContent = r.ok ? 'Sorted: out first, then the soonest reset.' : ''; });
|
|
1884
|
+
});
|
|
1885
|
+
$('q-all').addEventListener('click', function () {
|
|
1886
|
+
var live = QD.tiles.filter(function (t) { return t.refresh === 'live'; });
|
|
1887
|
+
$('q-done').textContent = 'Refreshing ' + live.length + '…';
|
|
1888
|
+
live.reduce(function (p, t) {
|
|
1889
|
+
return p.then(function () {
|
|
1890
|
+
var node = qBox.querySelector('.q-tile[data-id="' + CSS.escape(t.id) + '"]');
|
|
1891
|
+
return node ? qAct('refresh', t.id, node) : null;
|
|
1892
|
+
});
|
|
1893
|
+
}, Promise.resolve()).then(function () { $('q-done').textContent = 'Refreshed ' + live.length + '.'; });
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
// The Quota page's two charts, in the theme's colours like the Benchmarks ones.
|
|
1898
|
+
function drawQuotaCharts(tok, style, width) {
|
|
1899
|
+
if (!QD) return;
|
|
1900
|
+
if (!window.Plot) {
|
|
1901
|
+
['chart-sent', 'chart-resets'].forEach(function (id) {
|
|
1902
|
+
if ($(id)) $(id).textContent = 'Charts need Observable Plot from cdn.jsdelivr.net, which did not load.';
|
|
1903
|
+
});
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
var ink = tok('--ink', '#24331f'), good = tok('--ok', '#327a16'), warn = tok('--warn', '#7a4d00'), line = tok('--line', '#e4dfd2');
|
|
1907
|
+
var sent = $('chart-sent');
|
|
1908
|
+
if (sent) {
|
|
1909
|
+
var R = QD.sent.slice().sort(function (a, b) { return b.month - a.month; });
|
|
1910
|
+
sent.textContent = '';
|
|
1911
|
+
var lg = el('div', {class: 'q-legend'});
|
|
1912
|
+
[['This month, requests', 0.3], ['Last 24 hours, requests', 1]].forEach(function (k) {
|
|
1913
|
+
var s = el('span'), sw = el('i'); sw.style.background = good; sw.style.opacity = k[1];
|
|
1914
|
+
s.appendChild(sw); s.appendChild(document.createTextNode(k[0])); lg.appendChild(s);
|
|
1915
|
+
});
|
|
1916
|
+
sent.appendChild(lg);
|
|
1917
|
+
sent.appendChild(R.length ? Plot.plot({
|
|
1918
|
+
width: width(), height: 22 * R.length + 60, style: style, marginLeft: 120, marginRight: 90,
|
|
1919
|
+
x: {label: 'Requests →', grid: true}, y: {domain: R.map(function (r) { return r.provider; }), label: null},
|
|
1920
|
+
marks: [
|
|
1921
|
+
Plot.barX(R, {x: 'month', y: 'provider', fill: good, fillOpacity: 0.3,
|
|
1922
|
+
title: function (r) { return r.provider + '\nthis month: ' + qNum(r.month) + '\nlast 24 hours: ' + qNum(r.day); }}),
|
|
1923
|
+
Plot.barX(R, {x: 'day', y: 'provider', fill: good, insetTop: 4, insetBottom: 4}),
|
|
1924
|
+
Plot.text(R, {x: 'month', y: 'provider', text: function (r) { return qNum(r.day) + ' / ' + qNum(r.month); }, dx: 6, textAnchor: 'start', fill: ink}),
|
|
1925
|
+
Plot.ruleX([0], {stroke: ink})
|
|
1926
|
+
]}) : el('p', {class: 'hint'}, 'No call in the ledger this month.'));
|
|
1927
|
+
}
|
|
1928
|
+
var res = $('chart-resets');
|
|
1929
|
+
if (res) {
|
|
1930
|
+
var now = Date.now(), end = now + 7 * 86400e3, dots = [], bars = [];
|
|
1931
|
+
QD.tiles.forEach(function (t) {
|
|
1932
|
+
var wins = [];
|
|
1933
|
+
t.groups.forEach(function (g) { g.windows.forEach(function (w) { wins.push({label: (g.label ? g.label + ' · ' : '') + w.label, at: w.reset_at}); }); });
|
|
1934
|
+
t.pool.forEach(function (p) { if (p.reset_at) wins.push({label: p.label, at: p.reset_at, daily: true}); });
|
|
1935
|
+
wins.forEach(function (w) {
|
|
1936
|
+
var at = +new Date(w.at), daily = w.daily || / day$/.test(w.label);
|
|
1937
|
+
for (; at && at <= end; at += 86400e3) {
|
|
1938
|
+
if (at >= now) dots.push({tile: t.name, at: new Date(at), what: w.label});
|
|
1939
|
+
if (!daily) break;
|
|
1940
|
+
}
|
|
1941
|
+
});
|
|
1942
|
+
t.models.filter(qResting).forEach(function (m) {
|
|
1943
|
+
bars.push({tile: t.name, x1: new Date(now), x2: new Date(Math.min(+new Date(m.until), end)), what: m.alias + ' resting until ' + new Date(m.until).toLocaleString()});
|
|
1944
|
+
});
|
|
1945
|
+
});
|
|
1946
|
+
var names = QD.tiles.map(function (t) { return t.name; }).filter(function (n) {
|
|
1947
|
+
return dots.some(function (d) { return d.tile === n; }) || bars.some(function (b) { return b.tile === n; });
|
|
1948
|
+
});
|
|
1949
|
+
res.textContent = '';
|
|
1950
|
+
res.appendChild(names.length ? Plot.plot({
|
|
1951
|
+
width: width(), height: 26 * names.length + 60, style: style, marginLeft: 120,
|
|
1952
|
+
x: {domain: [new Date(now), new Date(end)], label: null, grid: true}, y: {domain: names, label: null},
|
|
1953
|
+
marks: [
|
|
1954
|
+
Plot.barX(bars, {x1: 'x1', x2: 'x2', y: 'tile', fill: warn, fillOpacity: 0.35, insetTop: 6, insetBottom: 6, title: 'what'}),
|
|
1955
|
+
Plot.dot(dots, {x: 'at', y: 'tile', r: 4, fill: good, stroke: ink, strokeWidth: 0.5,
|
|
1956
|
+
title: function (d) { return d.what + ' resets ' + d.at.toLocaleString(); }}),
|
|
1957
|
+
Plot.ruleX([new Date(now)], {stroke: tok('--err', '#a12b2b'), strokeWidth: 1.5}),
|
|
1958
|
+
Plot.ruleY(names, {stroke: line, strokeOpacity: 0.5})
|
|
1959
|
+
]}) : el('p', {class: 'hint'}, 'No reset or rest known for the next 7 days.'));
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1547
1963
|
// ---- Benchmarks charts: Observable Plot, data from serve.charts_data ------
|
|
1548
1964
|
// A function, not run once: a theme switched on Settings redraws them in its colours.
|
|
1549
1965
|
function drawCharts() {
|
|
@@ -1566,6 +1982,7 @@
|
|
|
1566
1982
|
var col = document.querySelector('.panes') || document.querySelector('main');
|
|
1567
1983
|
return Math.max(520, (col.clientWidth || 900) - 50);
|
|
1568
1984
|
}
|
|
1985
|
+
drawQuotaCharts(tok, style, width);
|
|
1569
1986
|
function put(id, make) {
|
|
1570
1987
|
var box = $(id);
|
|
1571
1988
|
if (!box) return;
|