@adrrr/tarmac 0.9.0 → 0.10.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/README.md +17 -13
- package/dist/demo-history.js +72 -12
- package/dist/fleet.js +14 -4
- package/dist/history-range.js +2 -0
- package/dist/history-view.js +239 -58
- package/dist/map.js +6 -22
- package/dist/render.js +609 -111
- package/dist/schema.js +12 -3
- package/dist/sessions.js +25 -0
- package/dist/wrapper.js +27 -10
- package/package.json +1 -1
package/dist/render.js
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
// they are written side by side, and the suite can reach both.
|
|
9
9
|
import { formatDuration } from './config.js';
|
|
10
10
|
import { HISTORY_MAX_BYTES } from './history-store.js';
|
|
11
|
-
import { buildMap,
|
|
11
|
+
import { buildMap, stateOf } from './map.js';
|
|
12
|
+
import { INTERACTIVE } from './sessions.js';
|
|
12
13
|
import { HISTORY_CSS, HISTORY_PHONE_CSS, HISTORY_PALETTE, HISTORY_TOUCH_CSS, historyScript, renderHistoryView, } from './history-view.js';
|
|
13
|
-
import { schemaNotice } from './schema.js';
|
|
14
|
+
import { schemaNoteParts, schemaNotice } from './schema.js';
|
|
14
15
|
import { LIMIT_WINDOWS, RESET_HORIZON_MS, readLimits } from './limits.js';
|
|
15
16
|
import { accountLimits, busyOnStaleFleet } from './fleet.js';
|
|
16
17
|
/**
|
|
@@ -202,14 +203,18 @@ export function renderTable({ rows, health }) {
|
|
|
202
203
|
warns.push(`! snapshots unavailable — ${health.snapshotsError}`);
|
|
203
204
|
else if (health.schemaBroken)
|
|
204
205
|
warns.push('! every snapshot drifted — the statusline payload schema moved');
|
|
205
|
-
else if (health.covered < health.
|
|
206
|
-
warns.push(
|
|
206
|
+
else if (health.covered < health.chainable) {
|
|
207
207
|
// The count travels, for the same reason `unreadable` does one line up: without it
|
|
208
208
|
// this line reads as "run install", and for a session id the wrapper declines to file
|
|
209
|
-
// that is advice already taken which can never work.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
209
|
+
// that is advice already taken which can never work. And when the denominator is
|
|
210
|
+
// smaller than the fleet, the line says who it left out: "1/2 sessions" over a
|
|
211
|
+
// four-row table is a comparison the reader should not have to resolve alone.
|
|
212
|
+
const excluded = health.sessions - health.chainable;
|
|
213
|
+
const counted = `${health.covered}/${health.chainable} sessions${excluded > 0 ? ` (${excluded} agent(s) draw no frame)` : ''}`;
|
|
214
|
+
warns.push(health.unfilable > 0
|
|
215
|
+
? `! statusline chained on ${counted} — ${health.unfilable} session(s) with an id tarmac never files`
|
|
216
|
+
: `! statusline chained on ${counted}`);
|
|
217
|
+
}
|
|
213
218
|
if (health.stale > 0)
|
|
214
219
|
warns.push(`! ${health.stale} reading(s) marked "!" are older than ${formatDuration(health.staleAfterMs)} (--stale-after)`);
|
|
215
220
|
const skewed = rows.filter(ahead).length;
|
|
@@ -379,13 +384,18 @@ export function renderLive(fleet) {
|
|
|
379
384
|
else if (health.schemaBroken) {
|
|
380
385
|
warnings.push(`Every snapshot drifted — Claude Code's statusline schema has probably moved. Context readings are dead until the payload shape is re-checked.`);
|
|
381
386
|
}
|
|
382
|
-
else if (health.covered < health.
|
|
383
|
-
const blind = health.
|
|
387
|
+
else if (health.covered < health.chainable) {
|
|
388
|
+
const blind = health.chainable - health.covered;
|
|
389
|
+
// Same naming as the table line: the denominator is the chainable population, and the
|
|
390
|
+
// rows below list agents as "not chained" too — without the parenthesis the banner and
|
|
391
|
+
// the rows disagree on how many are missing.
|
|
392
|
+
const excluded = health.sessions - health.chainable;
|
|
393
|
+
const counted = `${health.covered}/${health.chainable} sessions${excluded > 0 ? ` (${excluded} agent(s) draw no frame)` : ''}`;
|
|
384
394
|
warnings.push(health.unfilable === 0
|
|
385
|
-
? `Statusline chained on ${
|
|
395
|
+
? `Statusline chained on ${counted} — the rest report no context. Run \`tarmac install\` and give them one TUI frame.`
|
|
386
396
|
: health.unfilable >= blind
|
|
387
|
-
? `Statusline chained on ${
|
|
388
|
-
: `Statusline chained on ${
|
|
397
|
+
? `Statusline chained on ${counted} — the rest carry a session id that is not the UUID tarmac files snapshots under, so no frame will ever produce one. Installing again will not change that.`
|
|
398
|
+
: `Statusline chained on ${counted} — ${blind} report no context, and ${health.unfilable} of them will never be filed: the session id is not the UUID tarmac files snapshots under. For the others, run \`tarmac install\` and give them one TUI frame.`);
|
|
389
399
|
}
|
|
390
400
|
if (health.unknownStatus > 0) {
|
|
391
401
|
warnings.push(`${health.unknownStatus} session(s) report a status tarmac does not know — treated as unknown, not idle.`);
|
|
@@ -410,14 +420,25 @@ export function renderLive(fleet) {
|
|
|
410
420
|
// banner above could not take the number with it. The second is a maintainer's line: it
|
|
411
421
|
// stands for every user of a released tarmac until the next release ships the fixture, so
|
|
412
422
|
// amber would mean amber forever. Both keep every word they had.
|
|
423
|
+
// Each one in two halves: the fact, printed, and what follows from it behind a disclosure.
|
|
424
|
+
// Between them they keep every word these notes have ever had — what changed is that the
|
|
425
|
+
// page no longer ends in four to nine lines of prose a reader did not scroll there for.
|
|
413
426
|
const notes = [];
|
|
414
427
|
// Not under the stall banner, which names the same threshold two lines up: the pair reads as
|
|
415
428
|
// the alarm followed by its own excuse, and the excuse is the reading the alarm exists to
|
|
416
429
|
// tell you not to accept.
|
|
417
430
|
if (health.stale > 0 && stalled === 0) {
|
|
418
|
-
notes.push(
|
|
431
|
+
notes.push({
|
|
432
|
+
key: 'stale',
|
|
433
|
+
// The legend for the mark, and the only half of this a reader ever has to be handed: it
|
|
434
|
+
// is what makes `! 3h ago` on a row something they can argue with.
|
|
435
|
+
lead: `Readings past the ${formatDuration(health.staleAfterMs)} freshness threshold are dated where they sit.`,
|
|
436
|
+
// Why that is so, and the flag that moves it. Every word it had; one join that was an
|
|
437
|
+
// em dash is a full stop.
|
|
438
|
+
rest: `A statusline is only written when its terminal draws a frame, so an idle session's number is "as of" its last one. Set another with --stale-after.`,
|
|
439
|
+
});
|
|
419
440
|
}
|
|
420
|
-
const schema =
|
|
441
|
+
const schema = schemaNoteParts(health.schemaGuard);
|
|
421
442
|
if (schema)
|
|
422
443
|
notes.push(schema);
|
|
423
444
|
// Both views, every time, out of the one reading the page just asked for. The tabs are
|
|
@@ -445,7 +466,24 @@ export function renderLive(fleet) {
|
|
|
445
466
|
<div class="meta">${health.sessions} session${health.sessions === 1 ? '' : 's'} · ${health.busy} busy · ${cost(health)}<span class="stamp"> · ${esc(new Date(health.generatedAt).toISOString())}</span></div>
|
|
446
467
|
${warnings.map((w) => `<div class="warn">${esc(w)}</div>`).join('')}
|
|
447
468
|
${body}
|
|
448
|
-
<div id="fleet-notes">${notes.map(
|
|
469
|
+
<div id="fleet-notes">${notes.map(renderNote).join('')}</div>`;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* A footnote, folded.
|
|
473
|
+
*
|
|
474
|
+
* `<details>` and not a script: the page's one honest claim is that it is readable without
|
|
475
|
+
* JavaScript, and a disclosure the browser owns is open to a keyboard, to a screen reader and
|
|
476
|
+
* to a find-in-page that reaches inside a closed one. Shut, because open it is the wall of
|
|
477
|
+
* prose this was written to fold — and the LEAD is outside the fold, so what a reader is
|
|
478
|
+
* handed by `aria-describedby` is the fact, never a promise that there is one somewhere.
|
|
479
|
+
*
|
|
480
|
+
* A note with nothing behind its lead is not a disclosure at all: an empty fold is a control
|
|
481
|
+
* that answers a press with nothing.
|
|
482
|
+
*/
|
|
483
|
+
function renderNote(n) {
|
|
484
|
+
return n.rest === ''
|
|
485
|
+
? `<div class="note">${esc(n.lead)}</div>`
|
|
486
|
+
: `<details class="note" id="note-${esc(n.key)}"><summary>${esc(n.lead)}</summary>${esc(n.rest)}</details>`;
|
|
449
487
|
}
|
|
450
488
|
/**
|
|
451
489
|
* The account's two windows, for the page's header.
|
|
@@ -596,44 +634,169 @@ export function renderPage(fleet, view = 'table', { historyEnabled = false, demo
|
|
|
596
634
|
<style>
|
|
597
635
|
/* --wait is a fourth hue rather than the warning one: a session blocked on a human is not
|
|
598
636
|
a fault, and painting it amber puts it in the same column as "tarmac cannot read this". */
|
|
599
|
-
|
|
600
|
-
|
|
637
|
+
/* Two planes, not one. Everything on this page used to be a hairline on the same white:
|
|
638
|
+
a card inside a berth was two identical rules nested, and nothing was ever ON anything.
|
|
639
|
+
--bg is now the FLOOR and --surface the panels standing on it, which is the whole of what
|
|
640
|
+
makes a table read as a panel rather than as a document. The old names all survive, so
|
|
641
|
+
every rule written against them still resolves. */
|
|
642
|
+
:root { color-scheme: light dark;
|
|
643
|
+
--fg:#0f1218; --dim:#5f6875;
|
|
644
|
+
--bg:#f3f4f6; --surface:#fff; --surface-2:#f7f8fa;
|
|
645
|
+
/* A hairline is decoration; a control's edge is not. The second is darker and is
|
|
646
|
+
still under the 3:1 a non-text control would need to be identified by — so no
|
|
647
|
+
control on this page is identified by its border alone: the active tab keeps its
|
|
648
|
+
ink and its weight, the scrubber's thumb is ringed in --fg, states keep their hue. */
|
|
649
|
+
--line:#e2e5ea; --line-strong:#b9c1cb;
|
|
650
|
+
--warn:#b45309; --warnbg:#fffbeb; --busy:#047857; --wait:#1d4ed8;
|
|
651
|
+
--shadow-1:0 1px 2px rgba(16,24,40,.06);
|
|
652
|
+
--shadow-2:0 1px 2px rgba(16,24,40,.06), 0 12px 28px -14px rgba(16,24,40,.22);
|
|
653
|
+
/* The lit top edge of a raised surface. It is a dark-mode trick and reads as dirt in
|
|
654
|
+
light, so in light it is nothing — declared all the same, because every rule that
|
|
655
|
+
wants it lists it unconditionally. */
|
|
656
|
+
--edge:0 0 0 0 transparent;
|
|
657
|
+
--r-sm:8px; --r-md:12px; --r-lg:14px; --r-pill:99px;
|
|
658
|
+
--gutter:1.5rem; --maxw:80rem;
|
|
659
|
+
/* The face the numbers wear. Values only — a table set in mono is a terminal dump,
|
|
660
|
+
not an instrument. */
|
|
661
|
+
--mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,"Cascadia Mono","Segoe UI Mono","Roboto Mono",monospace;
|
|
662
|
+
/* How faint a chart's line goes when another one is being read. Here rather than in
|
|
663
|
+
the script because it is the one drawing value that differs by theme, and the
|
|
664
|
+
script has no business asking a browser which theme it is in. */
|
|
665
|
+
--fade:.72; --fade-iso:.3; }
|
|
666
|
+
@media (prefers-color-scheme: dark) { :root {
|
|
667
|
+
--fg:#e6eaf0; --dim:#8b97a6;
|
|
668
|
+
--bg:#0a0d11; --surface:#12171e; --surface-2:#171d25;
|
|
669
|
+
--line:#232b35; --line-strong:#454f5e;
|
|
670
|
+
--warn:#fbbf24; --warnbg:color-mix(in srgb,#fbbf24 10%,#12171e); --busy:#34d399; --wait:#93c5fd;
|
|
671
|
+
--shadow-1:0 1px 2px rgba(0,0,0,.55);
|
|
672
|
+
--shadow-2:0 1px 2px rgba(0,0,0,.55), 0 12px 28px -14px rgba(0,0,0,.75);
|
|
673
|
+
--edge:inset 0 1px 0 rgba(255,255,255,.045);
|
|
674
|
+
--fade:.6; } }
|
|
601
675
|
/* Eight categorical hues for the history view, kept apart from the four above: those four
|
|
602
676
|
say what a session is DOING, and a chart that borrowed one would be colouring a project
|
|
603
677
|
with the word for busy. */${HISTORY_PALETTE}
|
|
604
|
-
|
|
678
|
+
/* The floor is the html element's, not the body's: the body is now a column with a width, so
|
|
679
|
+
its own background would stop at the column's edge and leave white gutters. */
|
|
680
|
+
html { background:var(--bg); }
|
|
681
|
+
/* A page, not a document. Unbounded, the table spread eight columns over 1440px and the map
|
|
682
|
+
left 400px of dead white on the right — nothing framed the content, so nothing read as a
|
|
683
|
+
product. */
|
|
684
|
+
body { margin-inline:auto; max-width:var(--maxw); padding:1.5rem var(--gutter) 2rem;
|
|
685
|
+
background:var(--bg); color:var(--fg);
|
|
605
686
|
font:14px/1.5 ui-sans-serif,-apple-system,"Segoe UI",sans-serif; }
|
|
606
|
-
header
|
|
607
|
-
|
|
687
|
+
/* The header is a panel like the rest, rather than six unrelated objects on a line. A card
|
|
688
|
+
and not a full-bleed band: a band stopped at the column's width leaves two cut corners at
|
|
689
|
+
the top of the page, and a real full-bleed one needs 100vw, which opens a horizontal
|
|
690
|
+
scrollbar on any OS that draws classic scroll bars. */
|
|
691
|
+
header { display:flex; align-items:center; gap:.85rem; flex-wrap:wrap; margin:0 0 1rem;
|
|
692
|
+
background:var(--surface); border:1px solid var(--line); border-radius:var(--r-lg);
|
|
693
|
+
box-shadow:var(--shadow-1), var(--edge); padding:.6rem .9rem; }
|
|
694
|
+
/* The wordmark in the face the numbers wear. It was the smallest thing on the page that
|
|
695
|
+
mattered — 15.4px beside a 20px dial and an amber badge — and mono separates the name of
|
|
696
|
+
the product from the UI around it for zero bytes and zero claim. */
|
|
697
|
+
h1 { font-family:var(--mono); font-size:1.05rem; font-weight:600; margin:0; letter-spacing:.02em; }
|
|
608
698
|
.meta { color:var(--dim); font-size:.85rem; }${demo
|
|
609
699
|
? `
|
|
610
700
|
/* The demo marker. It borrows the warning's hues rather than the dim chrome the tabs use:
|
|
611
701
|
what it says is not chrome, and a badge a reader's eye files with the furniture is a badge
|
|
612
702
|
that is not in the screenshot as far as anybody looking at the screenshot is concerned.
|
|
613
703
|
Shipped only on a demo, so a plain serve is byte-for-byte the page it always was. */
|
|
614
|
-
.
|
|
704
|
+
/* Filled rather than outlined. It is the one element on this page whose job is to shout, and
|
|
705
|
+
the page around it got quieter: a hairline pill on the header's own white no longer reads
|
|
706
|
+
as a stamp. */
|
|
707
|
+
.demo-tag { background:color-mix(in srgb, var(--warn) 12%, var(--surface)); color:var(--warn); border:1px solid currentColor; border-radius:99px;
|
|
615
708
|
padding:.05rem .55rem; font-size:.75rem; font-weight:600; letter-spacing:.02em; white-space:nowrap; }`
|
|
616
709
|
: ''}
|
|
617
710
|
/* Honest, and out of the way of the fleet: three of these stacked at full padding pushed
|
|
618
711
|
the table below the fold on a laptop, which is its own kind of hidden. */
|
|
619
|
-
.
|
|
620
|
-
|
|
712
|
+
/* One box, worn by whichever line is up. The replay banner and the line saying the page is
|
|
713
|
+
live take turns in the same place in the flow, so their box is declared once: two rules
|
|
714
|
+
would only ever have to disagree by a pixel of padding for entering a replay to move the
|
|
715
|
+
whole page down, which is the jump this pair exists to remove. The minimum is there
|
|
716
|
+
because only one of the two carries a button, and a button's own line box is taller than
|
|
717
|
+
a line of this text. */
|
|
718
|
+
.warn, .live-state { border:1px solid currentColor; border-radius:var(--r-sm);
|
|
719
|
+
padding:.4rem .7rem; margin:.3rem 0; font-size:.8rem; line-height:1.45; }
|
|
720
|
+
/* And the floor on the two that take turns, never on the box: put on the shared rule it grew
|
|
721
|
+
the offline banner and the noscript warning by 5.5px each — a page redrawn to settle an
|
|
722
|
+
argument between two other elements. It clears the taller of the pair's own contents, which
|
|
723
|
+
is the banner's button: 12px of text in a line box its padding and border take past 21px,
|
|
724
|
+
against 18.56px for a line of the live text. */
|
|
725
|
+
.replaying-note, .live-state { min-height:1.5rem; }
|
|
726
|
+
/* An accent down the left edge instead of a frame at full intensity. What these say is true
|
|
727
|
+
and worth keeping — "1 session reports a status tarmac does not know" — and it is not
|
|
728
|
+
urgent: a full-width amber box above the fleet made the first thing a reader saw a
|
|
729
|
+
warning about a footnote. The mark stays, the shout goes. */
|
|
730
|
+
.warn { background:var(--warnbg); color:var(--fg); border:0;
|
|
731
|
+
border-left:3px solid var(--warn); border-radius:0 var(--r-sm) var(--r-sm) 0; }
|
|
732
|
+
.warn strong { color:var(--warn); }
|
|
621
733
|
.warn:last-of-type { margin-bottom:.9rem; }
|
|
734
|
+
/* The live half: the same box with none of the alarm. Grey on nothing, its border spent on
|
|
735
|
+
holding the size rather than on drawing one — what is normal reads as chrome, and the page
|
|
736
|
+
raises its voice only for the minute that is not now. */
|
|
737
|
+
.live-state:not([hidden]) { display:flex; align-items:baseline; gap:.6rem; flex-wrap:wrap; }
|
|
738
|
+
.live-state { color:var(--dim); border-color:transparent; }
|
|
739
|
+
.live-state strong { font-weight:600; }
|
|
740
|
+
body.replaying .live-state { display:none; }
|
|
741
|
+
/* On the map and nowhere else, like the scrubber it belongs to: the table and the curves have
|
|
742
|
+
no replay, so a line telling them apart from one answers a question their reader cannot
|
|
743
|
+
ask. Written on the body rather than shipped conditionally, so one shell serves all three. */
|
|
744
|
+
body:not([data-view="map"]) #live-state { display:none; }
|
|
622
745
|
/* The footnote: same words, none of the weight. Dim, small, below the fleet and with no box
|
|
623
746
|
around it, because what it carries is true rather than urgent — the threshold that dated a
|
|
624
747
|
reading, the payload shapes nobody has captured yet. It reads as chrome to someone
|
|
625
748
|
scanning their sessions and as an answer to someone who came looking for it. */
|
|
626
|
-
|
|
749
|
+
/* 72ch and a rule above it. At 95ch the paragraph ran the width of a 1440px screen, which is
|
|
750
|
+
past the measure anybody reads at, and the page ended on a grey slab. A hairline and a
|
|
751
|
+
column make it the footnote it always was. */
|
|
752
|
+
.note { color:var(--dim); font-size:.75rem; line-height:1.5; margin:1.5rem 0 0; max-width:72ch;
|
|
753
|
+
border-top:1px solid var(--line); padding-top:.75rem; }
|
|
754
|
+
/* One rule for the block, not one per note: two paragraphs each with a line over them is a
|
|
755
|
+
table of contents. */
|
|
756
|
+
/* The margin between two of them is also the clearance between two tap targets: each summary
|
|
757
|
+
draws an overlay .45rem above and below itself, so anything under .9rem here has the first
|
|
758
|
+
note's own bottom edge opening the second. Pinned by test/phone-view. */
|
|
759
|
+
#fleet-notes .note + .note { border-top:0; padding-top:0; margin-top:.95rem; }
|
|
760
|
+
.note code { background:var(--surface-2); border:1px solid var(--line); border-radius:4px; padding:.05rem .3rem; }
|
|
761
|
+
/* The fold. The fact is the summary and stays on the page; what follows from it is one press
|
|
762
|
+
away. The marker is the browser's own — a disclosure a reader already knows how to work
|
|
763
|
+
beats a caret this page would have had to teach. */
|
|
764
|
+
/* The padding is not decoration: it is the half of the 44px thumb target that can be drawn.
|
|
765
|
+
Spent on the overlay instead, the overlay reaches so far past the summary that two stacked
|
|
766
|
+
notes trade taps — the tabs' 7px bug, on a control set in the smallest type here. */
|
|
767
|
+
.note summary { font-size:.75rem; padding:.4rem 0; cursor:pointer; }
|
|
768
|
+
.note summary:focus-visible { outline:2px solid var(--wait); outline-offset:2px; border-radius:4px; }
|
|
769
|
+
details.note[open] summary { margin-bottom:.3rem; }
|
|
627
770
|
/* Two marks, one weight: a reading that has gone cold, and a reading picked out of several
|
|
628
771
|
that were not about the same window. Both say the number beside them may not be what the
|
|
629
772
|
reader takes it for, so neither may end up quieter than the other. */
|
|
630
773
|
.stale, .mixed { color:var(--warn); font-weight:600; }
|
|
631
|
-
.
|
|
774
|
+
/* The one place on the page that had no panel. overflow hidden is also what clips the 3px
|
|
775
|
+
state accent on the first cell to the panel's corners. */
|
|
776
|
+
.wrap { overflow-x:auto; background:var(--surface); border:1px solid var(--line);
|
|
777
|
+
border-radius:var(--r-lg); box-shadow:var(--shadow-1), var(--edge); }
|
|
632
778
|
table { border-collapse:collapse; width:100%; min-width:44rem; }
|
|
633
|
-
|
|
634
|
-
|
|
779
|
+
/* A shaded head and a row that answers the pointer: the difference between a <table> and a
|
|
780
|
+
view. The head is the nested surface, one step off the panel it sits in. */
|
|
781
|
+
th { text-align:left; font-weight:600; font-size:.7rem; text-transform:uppercase; letter-spacing:.08em;
|
|
782
|
+
color:var(--dim); padding:.45rem .6rem; background:var(--surface-2);
|
|
783
|
+
border-bottom:1px solid var(--line); white-space:nowrap; }
|
|
784
|
+
th:first-child, td:first-child { padding-left:.85rem; }
|
|
785
|
+
th:last-child, td:last-child { padding-right:.85rem; }
|
|
635
786
|
td { padding:.5rem .6rem; border-bottom:1px solid var(--line); white-space:nowrap; }
|
|
787
|
+
/* The panel draws the bottom edge; a last row that draws its own puts two lines there. */
|
|
788
|
+
tbody tr:last-child td { border-bottom:0; }
|
|
789
|
+
/* Only where there is a pointer to hover with. On a touchscreen :hover sticks to whatever was
|
|
790
|
+
tapped last, which would leave one row shaded for as long as the reader looks at it. */
|
|
791
|
+
@media (hover:hover) and (pointer:fine) { tbody tr:hover td { background:var(--surface-2); } }
|
|
636
792
|
td.num { font-variant-numeric:tabular-nums; }
|
|
793
|
+
/* ── the face of a number ────────────────────────────────────────────────────────────
|
|
794
|
+
The values, and only the values. Not a project, not a session name, not a model, not a
|
|
795
|
+
label: a dashboard set entirely in mono is a terminal dump, and the discipline of putting
|
|
796
|
+
the instrument face on the measurements alone is the whole of what separates the two.
|
|
797
|
+
.why is deliberately absent — it holds words ("no reading"), not a reading. */
|
|
798
|
+
.pct, td.num, .gauge .num, .chart-stat, #replay-at, .key .k-val, .asof {
|
|
799
|
+
font-family:var(--mono); font-variant-numeric:tabular-nums; letter-spacing:0; }
|
|
637
800
|
.dim { color:var(--dim); }
|
|
638
801
|
/* Shape + word + border, so the state survives a reader who cannot tell our two hues
|
|
639
802
|
apart, and a print. */
|
|
@@ -657,6 +820,10 @@ export function renderPage(fleet, view = 'table', { historyEnabled = false, demo
|
|
|
657
820
|
.bar > i { display:block; height:100%; border-radius:99px; background:var(--dim); }
|
|
658
821
|
.empty { color:var(--dim); }
|
|
659
822
|
.freshness { margin-left:auto; color:var(--dim); font-size:.8rem; font-variant-numeric:tabular-nums; }
|
|
823
|
+
/* A line break in the header, and nothing else: it exists only on a phone, where the six
|
|
824
|
+
things up there have to be told which two rows they belong to. Off at every other width,
|
|
825
|
+
where they fit on one. */
|
|
826
|
+
.hdr-break { display:none; }
|
|
660
827
|
.pulse { display:inline-block; width:.4rem; height:.4rem; border-radius:99px; background:var(--busy); margin-right:.4rem; vertical-align:middle; }
|
|
661
828
|
/* The failing state is carried by the banner's words; the dashed frame only repeats it. */
|
|
662
829
|
body.failing .pulse { background:var(--warn); }
|
|
@@ -664,10 +831,15 @@ export function renderPage(fleet, view = 'table', { historyEnabled = false, demo
|
|
|
664
831
|
.offline strong { white-space:nowrap; }
|
|
665
832
|
/* The tabs, and what they hide. The shell owns the choice — not the fragment — so a poll
|
|
666
833
|
that swaps the fleet underneath cannot put the reader back on a view they left. */
|
|
667
|
-
|
|
834
|
+
/* A segmented control: a sunk track with a raised chip on the tab you are reading, rather
|
|
835
|
+
than three words of which one has a ring round it. The chip is DECORATION — --line-strong
|
|
836
|
+
and the shadow are both under the 3:1 a border would need to identify a control on its
|
|
837
|
+
own, so the ink and the weight stay the signal, exactly as before. */
|
|
838
|
+
nav { display:flex; gap:2px; background:var(--surface-2); border:1px solid var(--line);
|
|
839
|
+
border-radius:var(--r-pill); padding:2px; }
|
|
668
840
|
nav a { color:var(--dim); text-decoration:none; font-size:.8rem; font-weight:600; text-transform:uppercase;
|
|
669
|
-
letter-spacing:.06em; padding:.
|
|
670
|
-
nav a[aria-current="page"] { color:var(--fg);
|
|
841
|
+
letter-spacing:.06em; padding:.2rem .6rem; border-radius:var(--r-pill); border:0; }
|
|
842
|
+
nav a[aria-current="page"] { color:var(--fg); background:var(--surface); box-shadow:var(--shadow-1); }
|
|
671
843
|
/* A finger is not a cursor. Every control on this page is a pill sized for a pointer that
|
|
672
844
|
lands on a single pixel — about 26px of box against the 44 a thumb is asked to hit — and
|
|
673
845
|
the fix cannot be more padding: that would redraw the page for everyone to solve a problem
|
|
@@ -685,9 +857,15 @@ export function renderPage(fleet, view = 'table', { historyEnabled = false, demo
|
|
|
685
857
|
Map, is 50), so a horizontal inset buys nothing — and at .3rem against a .15rem gap between
|
|
686
858
|
the tabs it made their two overlays overlap by 7px, where a tap meant for Table landed on
|
|
687
859
|
Map because Map's pseudo paints later. */
|
|
688
|
-
nav a, .replay button, .replaying-note button, .hist-range button, .to-now, .key
|
|
860
|
+
nav a, .replay button, .replaying-note button, .hist-range button, .to-now, .key,
|
|
861
|
+
.note summary { position:relative; }
|
|
689
862
|
@media (pointer: coarse) {
|
|
690
863
|
nav a::after, .replay button::after { content:''; position:absolute; inset:-.7rem 0; }
|
|
864
|
+
/* The footnote's fold is a control like the rest of them. Its inset is the smallest here
|
|
865
|
+
because two of them can stand one above the other and the margin between them is the
|
|
866
|
+
whole of their clearance: what 44px needs beyond the summary's own box is bought in
|
|
867
|
+
padding above, not in overlay. */
|
|
868
|
+
.note summary::after { content:''; position:absolute; inset:-.45rem 0; }
|
|
691
869
|
.replaying-note button::after { content:''; position:absolute; inset:-.85rem 0; }${HISTORY_TOUCH_CSS} }
|
|
692
870
|
body[data-view="table"] .view-map { display:none; }
|
|
693
871
|
body[data-view="map"] .view-table { display:none; }
|
|
@@ -698,12 +876,17 @@ ${HISTORY_CSS}
|
|
|
698
876
|
the fleet is what the page is about, and these two numbers are the weather it flies in.
|
|
699
877
|
Laid out with flex behind the same :not([hidden]) guard the replay containers carry —
|
|
700
878
|
the replayed pair ships hidden, and a display in a stylesheet beats the attribute. */
|
|
879
|
+
/* Separated from the tabs by a rule rather than by a gap the eye reads as another gap: the
|
|
880
|
+
header holds two groups that have nothing to do with each other, and a line is the cheapest
|
|
881
|
+
way to say so. In the header only — the replayed pair below the banner has no neighbour to
|
|
882
|
+
be divided from. */
|
|
701
883
|
.limits:not([hidden]) { display:flex; gap:1rem; flex-wrap:wrap; align-items:center; }
|
|
884
|
+
header .limits:not([hidden]) { padding-left:.9rem; border-left:1px solid var(--line); }
|
|
702
885
|
.gauge { display:flex; align-items:baseline; gap:.35rem; font-size:.8rem; }
|
|
703
886
|
/* Not upper-cased, alone among the small labels on this page: "5H" is not an hour, and a
|
|
704
887
|
unit that has been shouted reads as a different unit. */
|
|
705
888
|
.gauge .lbl { color:var(--dim); font-weight:600; letter-spacing:.04em; }
|
|
706
|
-
.gauge .num { font-variant-numeric:tabular-nums; font-weight:
|
|
889
|
+
.gauge .num { font-variant-numeric:tabular-nums; font-weight:600; }
|
|
707
890
|
.gauge .reset { color:var(--dim); }
|
|
708
891
|
/* Same bargain as the row bars: a glance at a magnitude, in the quiet ink of a secondary
|
|
709
892
|
fact, beside the number that is the authority. Its own class rather than .bar — that one
|
|
@@ -746,21 +929,79 @@ ${HISTORY_CSS}
|
|
|
746
929
|
label for the button rather than for the pair, and take width from the slider to do it. */
|
|
747
930
|
.replay .replay-name { flex-basis:100%; font-size:.7rem; font-weight:700; letter-spacing:.07em;
|
|
748
931
|
text-transform:uppercase; color:var(--dim); }
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
.replay
|
|
932
|
+
/* The one filled button on the whole dashboard, and the only place the page spends maximum
|
|
933
|
+
contrast: everything else here is a reading, and this is the single thing a reader is
|
|
934
|
+
invited to press. */
|
|
935
|
+
.replay button { font:inherit; font-size:.8rem; font-weight:600; color:var(--bg); background:var(--fg);
|
|
936
|
+
border:1px solid var(--fg); border-radius:var(--r-pill); padding:.2rem .95rem; cursor:pointer; }
|
|
937
|
+
.replay button:hover:not(:disabled) { opacity:.88; }
|
|
938
|
+
/* Pressed, and it looks it. The WORD on this button says which of the two it does next —
|
|
939
|
+
"Pause" while the day walks — which is right and is also the one thing a glance cannot
|
|
940
|
+
catch: a screenshot of a stopped replay reads exactly like a screenshot of a running one.
|
|
941
|
+
The attribute carries the other fact, the one that can be painted. */
|
|
942
|
+
/* Inverted, now that the resting button is the filled one. The pair is what it always was —
|
|
943
|
+
the WORD says which of the two the next press does, the ATTRIBUTE says which of them is
|
|
944
|
+
happening, and only the second survives a screenshot — so the two paints simply swapped
|
|
945
|
+
ends. Stopped is the solid invitation; running is the same button hollowed out. */
|
|
946
|
+
#play[data-playing="true"] { background:var(--surface); color:var(--fg); border-color:var(--fg); }
|
|
947
|
+
.replay button:disabled { opacity:.4; cursor:default; }
|
|
948
|
+
/* The handle. A range input is drawn by the browser until appearance:none hands its two
|
|
949
|
+
shadow parts over — and what the browser drew was a fat grey groove belonging to no page,
|
|
950
|
+
on the one control a reader of this view spends the most time touching. Both engines are
|
|
951
|
+
spelled out because they name the same two parts differently and neither falls back to the
|
|
952
|
+
other; the values are one pair, so a thumb retuned in one is retuned in both.
|
|
953
|
+
The rail is the track's own hue, the thumb the page's ink, ringed in the background so it
|
|
954
|
+
reads as an object ON the rail rather than a lump of it. */
|
|
955
|
+
/* accent-color goes: it was the last thing on this page a browser drew for us, and on the one
|
|
956
|
+
screen anybody will screenshot. --p is the share of the range already walked, written by the
|
|
957
|
+
script on every input and every step of a play — it is what makes the bar FILL as the day
|
|
958
|
+
runs, which is the whole of the motion in a recording of this view. Firefox has a track
|
|
959
|
+
part for that and needs no variable; webkit has none, so the fill is a gradient stop. */
|
|
960
|
+
.replay input[type="range"] { flex:1; min-width:10rem; -webkit-appearance:none; appearance:none;
|
|
961
|
+
background:transparent; height:1.5rem; margin:0; cursor:pointer; accent-color:auto; }
|
|
962
|
+
.replay input[type="range"]::-webkit-slider-runnable-track { height:6px; border-radius:var(--r-pill);
|
|
963
|
+
background:linear-gradient(to right, var(--dim) 0 var(--p,0%), var(--line) var(--p,0%) 100%); }
|
|
964
|
+
.replay input[type="range"]::-moz-range-track { height:6px; border-radius:var(--r-pill); background:var(--line); }
|
|
965
|
+
.replay input[type="range"]::-moz-range-progress { height:6px; border-radius:var(--r-pill); background:var(--dim); }
|
|
966
|
+
/* The margin is what centres a webkit thumb on its track: that engine lays the thumb out from
|
|
967
|
+
the top of the track box, so half the difference of the two heights is what is owed back.
|
|
968
|
+
Ringed in --fg rather than filled with it: 18:1 against the page, which is what identifies
|
|
969
|
+
the handle — the track's own edge is nowhere near the 3:1 a control would need. */
|
|
970
|
+
.replay input[type="range"]::-webkit-slider-thumb { -webkit-appearance:none; appearance:none;
|
|
971
|
+
width:16px; height:16px; margin-top:-5px; border-radius:var(--r-pill);
|
|
972
|
+
background:var(--surface); border:2px solid var(--fg); box-shadow:var(--shadow-1); cursor:grab; }
|
|
973
|
+
.replay input[type="range"]::-moz-range-thumb { width:16px; height:16px; border-radius:var(--r-pill);
|
|
974
|
+
background:var(--surface); border:2px solid var(--fg); box-shadow:var(--shadow-1); cursor:grab; }
|
|
975
|
+
.replay input[type="range"]:active::-webkit-slider-thumb { cursor:grabbing; }
|
|
976
|
+
.replay input[type="range"]:active::-moz-range-thumb { cursor:grabbing; }
|
|
977
|
+
.replay input[type="range"]:disabled { opacity:.45; cursor:default; }
|
|
978
|
+
/* What appearance:none took away and the page owes back: a control that can be reached by
|
|
979
|
+
tab and not seen once it is there is a control a keyboard reader loses. The hue is the one
|
|
980
|
+
this page already spends on "a human is being waited for", which is what a focus ring is. */
|
|
981
|
+
.replay input[type="range"]:focus-visible, .replay button:focus-visible,
|
|
982
|
+
.replaying-note button:focus-visible { outline:2px solid var(--wait); outline-offset:2px; }
|
|
753
983
|
/* The two things the reader has to be able to read while dragging: the minute under the
|
|
754
984
|
handle, and what the whole range covers. Tabular, so neither jitters as it counts. */
|
|
755
985
|
#replay-at { font-variant-numeric:tabular-nums; font-weight:600; }
|
|
986
|
+
/* One line, and the pointer is told there is more behind it. What came off this line is a
|
|
987
|
+
paragraph of standing prose about the record, which is now the title and a span for a
|
|
988
|
+
screen reader — moved, never dropped: it is what keeps an ungrouped map from reading as a
|
|
989
|
+
rendering that broke. */
|
|
756
990
|
.replay .covers { flex-basis:100%; color:var(--dim); font-size:.75rem; }
|
|
991
|
+
.replay .covers[title] { cursor:help; }
|
|
757
992
|
/* The banner wears the warning style on purpose: a page showing a past minute as though it were the
|
|
758
993
|
fleet is the worst thing this dashboard could do, so it wears the loudest thing it has. */
|
|
759
994
|
/* Sticky, because the handle is at the bottom of a map that can be taller than the
|
|
760
995
|
viewport: a reader dragging with the "this is the past" banner scrolled off the top is
|
|
761
996
|
a reader the banner is not warning. */
|
|
997
|
+
/* The one box on this page still allowed to shout. The banners above it gave up their frame
|
|
998
|
+
for a left accent; this one keeps the whole frame, takes the deeper shadow and lifts off
|
|
999
|
+
the page — because a dashboard showing a past minute as though it were the fleet is the
|
|
1000
|
+
worst thing this product could do. */
|
|
762
1001
|
.replaying-note:not([hidden]) { display:flex; align-items:baseline; gap:.6rem; flex-wrap:wrap;
|
|
763
|
-
position:sticky; top:0; z-index:1;
|
|
1002
|
+
position:sticky; top:0; z-index:1;
|
|
1003
|
+
border:1px solid var(--warn); border-left-width:3px; border-radius:var(--r-sm);
|
|
1004
|
+
box-shadow:var(--shadow-2); }
|
|
764
1005
|
.replaying-note button { font:inherit; font-size:.75rem; font-weight:600; color:inherit;
|
|
765
1006
|
background:transparent; border:1px solid currentColor; border-radius:99px;
|
|
766
1007
|
padding:.05rem .7rem; cursor:pointer; }
|
|
@@ -779,7 +1020,28 @@ ${HISTORY_CSS}
|
|
|
779
1020
|
flat grid this view was before, which is the honest drawing of what it holds. */
|
|
780
1021
|
.map { gap:.9rem; }
|
|
781
1022
|
.map.berths { display:flex; flex-wrap:wrap; align-items:flex-start; }
|
|
782
|
-
|
|
1023
|
+
/* One cell per node, and every cell the same: the replay is the one surface where the fleet
|
|
1024
|
+
changes under a still hand — each position of the handle is another minute, and sessions
|
|
1025
|
+
come and go between two of them. A grid whose rows are the size of what is in them redraws
|
|
1026
|
+
the page at every step of a scrub, which is what made a drag look like a page breaking.
|
|
1027
|
+
The floor has to clear the TALLEST card the record can produce, or it is decoration: a row
|
|
1028
|
+
is minmax(floor,auto), so one node taller than the floor pushes its own row and every row
|
|
1029
|
+
under it — the jump, by the other road. The card that decides it is a waiting session:
|
|
1030
|
+
a dial, a name, a caption clamped to two lines and the two numbers under it, 214px in
|
|
1031
|
+
Chrome. One value at every width: the columns narrow on a phone and the cards do not, so a
|
|
1032
|
+
floor cut to match them would sit under the cards it is supposed to hold up. */
|
|
1033
|
+
/* 14rem and not 13.5: the floor is measured against the tallest card the record can draw,
|
|
1034
|
+
and C5's dial takes that card from 215.9px to 223.9. Left at 13.5 it would clear its own
|
|
1035
|
+
floor and push its row and every row under it at the minute it appears — the jump this
|
|
1036
|
+
grid exists to remove, re-entered by a stroke width. The two are one change. */
|
|
1037
|
+
.map.flat { display:grid; grid-template-columns:repeat(auto-fill,minmax(10.5rem,1fr));
|
|
1038
|
+
grid-auto-rows:minmax(14rem,auto); }
|
|
1039
|
+
/* And what is in a cell sits in the middle of it. A session card hung from the top left 67px
|
|
1040
|
+
of white under its last line — 31% of the cell — which inside a bordered box reads as a
|
|
1041
|
+
render that failed rather than as air. The agent already had this from #170; the cards
|
|
1042
|
+
were the half that never got it. Where a cell is the height of its own content, which is
|
|
1043
|
+
every card in a berth, the rule changes nothing. */
|
|
1044
|
+
.map.flat .node { justify-content:center; }
|
|
783
1045
|
/* The berth: a frame around the nodes read in one directory, and the label is the whole of
|
|
784
1046
|
what it claims. Quiet on purpose — a hairline and a caption in the grey the rest of the
|
|
785
1047
|
page uses for a heading, because the loud thing on this view is a session's state, and a
|
|
@@ -796,7 +1058,13 @@ ${HISTORY_CSS}
|
|
|
796
1058
|
letter-spacing:.06em; color:var(--dim); }
|
|
797
1059
|
/* The cards side by side at their own width, wrapping inside the frame when the directory
|
|
798
1060
|
holds more of them than the row can take. */
|
|
799
|
-
.
|
|
1061
|
+
/* flex-start, not stretch. Two cards with captions of different lengths were drawn to one
|
|
1062
|
+
height and the shorter one carried 18.2px of white under its last line — C11.1's problem
|
|
1063
|
+
at berth scale. Each card takes its own height instead. The DIALS stay level, which is the
|
|
1064
|
+
line a reader scans a row along, and that is why a berth is not centred the way a replay
|
|
1065
|
+
cell is: filled cards with a shadow read as two objects of two heights, not as one that
|
|
1066
|
+
failed to fill. */
|
|
1067
|
+
.berth-cards { display:flex; flex-wrap:wrap; gap:.6rem; align-items:flex-start; }
|
|
800
1068
|
.berth-cards .node { width:10.5rem; }
|
|
801
1069
|
/* And the strips docked underneath, full width of the frame, one under the other: a strip is
|
|
802
1070
|
a line of text, and a line of text in a column half a card wide is an ellipsis where the
|
|
@@ -804,33 +1072,69 @@ ${HISTORY_CSS}
|
|
|
804
1072
|
directory's background work, under the terminals someone is sitting at — and NOT because
|
|
805
1073
|
one of those terminals dispatched it, which nothing here knows. */
|
|
806
1074
|
.berth-strips { display:flex; flex-direction:column; gap:.4rem; margin-top:.6rem; }
|
|
807
|
-
|
|
1075
|
+
/* A card standing on the berth's floor rather than a second hairline drawn inside a first.
|
|
1076
|
+
Three planes now — floor, berth, card — where there used to be one white and two identical
|
|
1077
|
+
borders. The berth keeps no fill and no shadow of its own: a shadow inside a shadow reads
|
|
1078
|
+
as neither. */
|
|
1079
|
+
.node { background:var(--surface); border:1px solid var(--line); border-radius:var(--r-md);
|
|
1080
|
+
box-shadow:var(--shadow-1), var(--edge); padding:.8rem .85rem .7rem;
|
|
808
1081
|
display:flex; flex-direction:column; align-items:center; text-align:center; }
|
|
809
|
-
|
|
810
|
-
|
|
1082
|
+
/* A wash of the state's own hue at 4%, under the border that already carries it. It invents
|
|
1083
|
+
no information — same hue, same node, quieter than the edge — and it is what makes "these
|
|
1084
|
+
four are working" readable across a berth without reading a single word. */
|
|
1085
|
+
.node[data-state="busy"] { border-color:color-mix(in srgb, var(--busy) 45%, var(--line));
|
|
1086
|
+
background:color-mix(in srgb, var(--busy) 4%, var(--surface)); }
|
|
1087
|
+
.node[data-state="waiting"] { border-color:color-mix(in srgb, var(--wait) 45%, var(--line));
|
|
1088
|
+
background:color-mix(in srgb, var(--wait) 4%, var(--surface)); }
|
|
811
1089
|
/* An agent is not a smaller session — it is a strip. It was a card at three quarters scale,
|
|
812
1090
|
which put a dial on a session that has no terminal to draw a statusline frame with: a ring
|
|
813
1091
|
that can never fill, captioned with the words of a fault someone could go and repair. The
|
|
814
1092
|
honest form is the one the table already speaks in — text on a line, left-aligned, its
|
|
815
1093
|
state in the same glyph and in a three-pixel accent down the left edge. */
|
|
816
|
-
/*
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1094
|
+
/* Behind the scrubber an agent fills its cell like every other node. It kept its own height
|
|
1095
|
+
for as long as the grid's rows did — a strip at half a card, sitting at the top of its
|
|
1096
|
+
row — and that is exactly what made the map dance: an agent appearing between two minutes
|
|
1097
|
+
of a scrub moved every dial under it. The SHAPE stays different, which is the honest part
|
|
1098
|
+
(no dial, no arc that could never fill, its text left-aligned); the CELL is the same. */
|
|
1099
|
+
/* Its text sits in the middle of whatever box it is given, which in the flat grid is the
|
|
1100
|
+
height of a dial: hung from the top of one, two lines of text read as a cell that failed
|
|
1101
|
+
to draw. Docked in a berth the box is the text's own height and this does nothing. */
|
|
1102
|
+
.node[data-role="agent"] { align-items:stretch; justify-content:center; text-align:left;
|
|
1103
|
+
padding:.5rem .7rem .55rem; border-radius:var(--r-sm);
|
|
826
1104
|
/* The box goes back to the neutral line the tinted rule above gave it: the accent is
|
|
827
1105
|
the channel that carries state here, and a strip outlined in its hue as well was
|
|
828
1106
|
the same fact said twice, in two weights, on a shape half the size of a card. */
|
|
829
1107
|
border-color:var(--line); border-left-width:3px; border-left-color:var(--dim); }
|
|
1108
|
+
/* The recessed fill belongs to the DOCKED strip, not to the agent. In a berth it is an object
|
|
1109
|
+
nested under the cards and reads as one; behind the scrubber it is a cell beside other
|
|
1110
|
+
cells, and --surface-2 there sinks it in light and lifts it in dark — the same object read
|
|
1111
|
+
two opposite ways depending on the theme. In the grid it takes --surface, the shadow and
|
|
1112
|
+
the 4% state wash like its neighbours, and the SHAPE goes on being what tells it apart. */
|
|
1113
|
+
.berth-strips .node[data-role="agent"] { background:var(--surface-2); box-shadow:none; }
|
|
830
1114
|
.node[data-role="agent"][data-state="busy"] { border-left-color:var(--busy); }
|
|
831
1115
|
.node[data-role="agent"][data-state="waiting"] { border-left-color:var(--wait); }
|
|
832
1116
|
.node[data-role="agent"][data-state="unknown"] { border-left-color:var(--warn); }
|
|
833
1117
|
.node[data-role="agent"] .who { margin-top:0; width:100%; }
|
|
1118
|
+
/* A cell is 10.5rem wide, and every caption on it is one nowrap line: cut to that column, a
|
|
1119
|
+
replayed agent at 320px drew its project as "a…" while the word BACKGROUND beside it kept
|
|
1120
|
+
all of its own, and a waiting reason — the one caption this page calls why a node is not
|
|
1121
|
+
working — lost two thirds of itself. The cell has height to spare and no width to give, so
|
|
1122
|
+
they wrap DOWN it instead. Scoped to the flat grid: a strip docked in a berth is a band the
|
|
1123
|
+
width of its frame, and the prompt on it has no length limit — wrapping that one down its
|
|
1124
|
+
berth is the paragraph the ellipsis is there to prevent.
|
|
1125
|
+
|
|
1126
|
+
Two lines, then an ellipsis. Free to wrap as far as it likes, a caption is a card free to
|
|
1127
|
+
grow — and a card taller than the row floor takes its row and every row under it, which is
|
|
1128
|
+
the jump this grid exists to remove, re-entered through the fix for the clipping. A waiting
|
|
1129
|
+
reason has no length limit anywhere in this codebase: the source publishes what it likes
|
|
1130
|
+
and sessions.ts copies it through. */
|
|
1131
|
+
.map.flat .node .sub { white-space:normal; overflow-wrap:anywhere;
|
|
1132
|
+
display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; line-clamp:2; overflow:hidden; }
|
|
1133
|
+
.map.flat .node[data-role="agent"] .who { flex-wrap:wrap; }
|
|
1134
|
+
/* And the label that says an agent is not a terminal stays beside the name it qualifies. The
|
|
1135
|
+
margin that pushes it to the far end of a strip's one line drops it, alone and
|
|
1136
|
+
right-aligned, onto a second line the moment that line is allowed to wrap. */
|
|
1137
|
+
.map.flat .node[data-role="agent"] .kind { margin-left:0; }
|
|
834
1138
|
/* A strip's project, which since the berths is the REPLAY's business alone: a live strip
|
|
835
1139
|
prints none — the frame around it says the directory — and behind the scrubber there is no
|
|
836
1140
|
frame, and the project is the only name the ring kept. The rule stayed when the markup
|
|
@@ -843,6 +1147,16 @@ ${HISTORY_CSS}
|
|
|
843
1147
|
want the day it grows one, and unprefixed they would take it. */
|
|
844
1148
|
.node .kind { margin-left:auto; font-size:.6rem; font-weight:700; text-transform:uppercase;
|
|
845
1149
|
letter-spacing:.08em; color:var(--dim); }
|
|
1150
|
+
/* The reading on a strip, drawn. The bar is the table's own — same track, same fill, same
|
|
1151
|
+
refusal to be coloured by a state or a threshold — and the number beside it is what is
|
|
1152
|
+
authoritative, in the face every other number on this page wears. The word stays: a bar
|
|
1153
|
+
says a magnitude and never which one. */
|
|
1154
|
+
.node .ctx { display:inline-flex; align-items:center; gap:.3rem; white-space:nowrap; }
|
|
1155
|
+
.node .bar { display:inline-block; width:2.75rem; height:.3rem; border-radius:var(--r-pill);
|
|
1156
|
+
background:var(--line); vertical-align:middle; margin-right:0; }
|
|
1157
|
+
.node .bar > i { display:block; height:100%; border-radius:var(--r-pill); background:var(--dim); }
|
|
1158
|
+
.node .ctx-pct { font-family:var(--mono); font-variant-numeric:tabular-nums; font-weight:600;
|
|
1159
|
+
font-size:.8rem; color:var(--fg); }
|
|
846
1160
|
/* The prompt a background session was named after — the strip's own line, now that the berth
|
|
847
1161
|
around it carries the directory. One line, clipped: it is a sentence somebody typed, and
|
|
848
1162
|
it is the only thing on the strip that has no length limit. */
|
|
@@ -851,24 +1165,29 @@ ${HISTORY_CSS}
|
|
|
851
1165
|
/* Said, not shown: the four glyphs differ in silhouette, so a reader who cannot separate
|
|
852
1166
|
two hues still has the state — but a screen reader is handed a bullet and nothing else. */
|
|
853
1167
|
.sr { position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); white-space:nowrap; }
|
|
854
|
-
.
|
|
1168
|
+
/* 6rem. On a post the ring IS the product, and at 5.5 with a 5px stroke it was a progress
|
|
1169
|
+
graphic. Ships WITH the 14rem row floor above — the taller card is what makes that floor
|
|
1170
|
+
necessary. */
|
|
1171
|
+
.dial { position:relative; width:6rem; height:6rem; }
|
|
855
1172
|
.dial svg { width:100%; height:100%; display:block; overflow:visible; }
|
|
856
|
-
|
|
1173
|
+
/* Thicker, and the ring becomes a dial rather than a progress graphic. Purely CSS: the
|
|
1174
|
+
percentage is carried by stroke-dasharray off DIAL_R, which no stroke width touches. */
|
|
1175
|
+
.track { fill:none; stroke:var(--line); stroke-width:6.5; }
|
|
857
1176
|
/* Butt caps, not round: a rounded cap adds half a stroke width at each end, which draws a
|
|
858
1177
|
1% reading at three times its extent. The prettier cap overstates every small number. */
|
|
859
|
-
.arc { fill:none; stroke:var(--dim); stroke-width:5; }
|
|
1178
|
+
.arc { fill:none; stroke:var(--dim); stroke-width:6.5; }
|
|
860
1179
|
.node[data-state="busy"] .arc { stroke:var(--busy); }
|
|
861
1180
|
/* A reading past the freshness threshold is drawn as what it is: thin, faded, and in the
|
|
862
1181
|
warning hue — never the solid arc of a live one. Its EXTENT stays true, because the
|
|
863
1182
|
number is still the truth of an earlier moment; and a dash pattern here would overwrite
|
|
864
1183
|
stroke-dasharray, which is what carries the percentage. */
|
|
865
1184
|
.node[data-reading="stale"] .arc, .node[data-reading="undated"] .arc {
|
|
866
|
-
stroke:var(--warn); stroke-width:
|
|
1185
|
+
stroke:var(--warn); stroke-width:3.5; opacity:.7; }
|
|
867
1186
|
/* A replayed reading. Its EXTENT is what the record vouches for; its age is the one thing
|
|
868
1187
|
the ring never kept, so it may not wear the solid arc that means "as current as a reading
|
|
869
1188
|
gets" — nor the warning hue of a stale one, which would claim the opposite. Between the
|
|
870
1189
|
two: full colour, a shade lighter, and no date underneath it. */
|
|
871
|
-
.node[data-reading="undatable"] .arc { stroke-width:
|
|
1190
|
+
.node[data-reading="undatable"] .arc { stroke-width:5.5; opacity:.85; }
|
|
872
1191
|
/* Nothing was measured. Keyed on the measurement and never on the age of the file: a
|
|
873
1192
|
solid empty ring is what a session measured at 0% wears, and the two must not match. */
|
|
874
1193
|
.track.unmeasured { stroke-dasharray:2 6; stroke-linecap:round; }
|
|
@@ -894,8 +1213,11 @@ ${HISTORY_CSS}
|
|
|
894
1213
|
@media (prefers-reduced-motion: reduce) { .halo { animation:none; opacity:.35; transform:scale(1.18); } }
|
|
895
1214
|
.val { position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
|
|
896
1215
|
font-variant-numeric:tabular-nums; }
|
|
897
|
-
|
|
898
|
-
|
|
1216
|
+
/* 650 was a weight the sans had and the system mono faces do not — asked for it, they round
|
|
1217
|
+
back to 400 and the number came out lighter than before. 600 is a weight they carry. The
|
|
1218
|
+
negative tracking goes with the sans it was measured on. */
|
|
1219
|
+
.pct { font-size:1.55rem; font-weight:600; }
|
|
1220
|
+
.pct i { font-style:normal; font-size:.62em; font-weight:500; color:var(--dim); }
|
|
899
1221
|
.why { font-size:.68rem; color:var(--dim); line-height:1.2; max-width:4.4rem; }
|
|
900
1222
|
.why b { display:block; font-size:1.25rem; font-weight:400; }
|
|
901
1223
|
.who { margin-top:.5rem; display:flex; align-items:baseline; gap:.3rem; max-width:100%; }
|
|
@@ -915,6 +1237,9 @@ ${HISTORY_CSS}
|
|
|
915
1237
|
.sub.waiting-for { color:var(--wait); font-weight:600; }
|
|
916
1238
|
.asof { font-size:.72rem; color:var(--dim); font-variant-numeric:tabular-nums; margin-top:.15rem; }
|
|
917
1239
|
.asof.stale { color:var(--warn); font-weight:600; }
|
|
1240
|
+
/* The columns narrow here; the row floor does not follow them. A card at 320px is taller than
|
|
1241
|
+
one at 1280, not shorter — its captions have less width and more lines — so a floor cut to
|
|
1242
|
+
the narrow column would sit under every card on the page and hold nothing up. */
|
|
918
1243
|
@media (max-width: 30rem) { .map.flat { grid-template-columns:repeat(auto-fill,minmax(8.5rem,1fr)); } .map { gap:.6rem; } }
|
|
919
1244
|
|
|
920
1245
|
/* Below this the table stops being a table. What replaces it is the strip described down at
|
|
@@ -923,7 +1248,38 @@ ${HISTORY_CSS}
|
|
|
923
1248
|
as nothing at all — and the labels that go are the ones whose value says what it is on its
|
|
924
1249
|
own, with the exceptions named where they are given a word back. */
|
|
925
1250
|
@media (max-width: 46rem) {
|
|
926
|
-
body { padding
|
|
1251
|
+
body { padding:.75rem .75rem 1.25rem; }
|
|
1252
|
+
/* ── the header, in two lines ──────────────────────────────────────────────────────
|
|
1253
|
+
Six objects wrapping as they pleased put five rows of chrome — a name, a badge, tabs,
|
|
1254
|
+
two gauges and the age of the reading — above the first session on a 390px screen: the
|
|
1255
|
+
fleet started a third of the way down a page that is about the fleet.
|
|
1256
|
+
|
|
1257
|
+
Three now, and the first two are DECLARED rather than left to the wrap: the name and the
|
|
1258
|
+
tabs, then the account. The break is a real element in the markup between those two
|
|
1259
|
+
groups, zero-height and full-width — NOT "order" on the items around it, which would be
|
|
1260
|
+
this sheet moving two readings past each other to get a line break. Nothing here is
|
|
1261
|
+
reordered: what the page shows is the order the markup is in.
|
|
1262
|
+
|
|
1263
|
+
Three and not two, and the missing one is arithmetic rather than taste. The account's own
|
|
1264
|
+
row is 255px of "5h 62% resets in 59m 7d 43% resets in 4d 11h" and the age of the
|
|
1265
|
+
reading is 92 more, against 346 of usable width: the pair is 7px over, and the reset
|
|
1266
|
+
countdowns are the only words on this page that appear nowhere else — a phone that
|
|
1267
|
+
dropped them would be a phone that cannot tell you when your window opens. So the age
|
|
1268
|
+
takes the third row whole rather than a fact being spent on the second.
|
|
1269
|
+
|
|
1270
|
+
A demo badge takes the width it takes and pushes the tabs down, which is a fourth row on
|
|
1271
|
+
a demo and none on a serve: it is the one element here whose job is to be in the way. */
|
|
1272
|
+
header { gap:.3rem .5rem; padding:.5rem .6rem; margin-bottom:.75rem; }
|
|
1273
|
+
.hdr-break { display:block; flex-basis:100%; height:0; }
|
|
1274
|
+
/* The rule between the two groups is what the break already says at this width. */
|
|
1275
|
+
header .limits:not([hidden]) { gap:.8rem; padding-left:0; border-left:0; }
|
|
1276
|
+
.freshness { font-size:.72rem; }
|
|
1277
|
+
.gauge { font-size:.72rem; }
|
|
1278
|
+
/* The rail goes where the row bar goes, three rules below, and for the same reason: the
|
|
1279
|
+
number beside it is the authority, the bar is a second telling of it, and a phone has no
|
|
1280
|
+
width for the second telling. What it says in words — "62%", "resets in 1h" — is every
|
|
1281
|
+
word it said before. */
|
|
1282
|
+
.gauge .rail { display:none; }
|
|
927
1283
|
/* The summary's ISO stamp, spent. It is the widest thing on that line and the header two
|
|
928
1284
|
lines above already says the same fact in the words a reader uses — "updated 3s ago",
|
|
929
1285
|
counted by the shell whether or not a poll ever lands. Hidden rather than dropped: the
|
|
@@ -938,17 +1294,21 @@ ${HISTORY_CSS}
|
|
|
938
1294
|
them. The negative margin gives it the page's own gutters back, so the bar reaches the
|
|
939
1295
|
edges of the phone and the rule above it reads as an edge rather than a floating line.
|
|
940
1296
|
|
|
941
|
-
The
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
range — nothing replayed here is dated, and the past is drawn ungrouped — and they
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
reader is staring at one
|
|
1297
|
+
The line under the handle stays. Hiding it for the length of a replay was the obvious way
|
|
1298
|
+
to keep the bar short, and it silently undid the fix coversRange and KEEPS argue for
|
|
1299
|
+
below: two of the things that line says are standing properties of the RECORD, not of the
|
|
1300
|
+
range — nothing replayed here is dated, and the past is drawn ungrouped — and they are in
|
|
1301
|
+
the reader's view precisely because they had lived "nowhere the reader can see it", where
|
|
1302
|
+
an ungrouped map reads as a rendering that broke. A phone replaying is exactly when a
|
|
1303
|
+
reader is staring at one, and a phone has no hover to reach a title with: the two words
|
|
1304
|
+
are on the line itself for that reason, and only their paragraph is behind it. */
|
|
948
1305
|
body.replaying .replay:not([hidden]) { position:sticky; bottom:0; z-index:3;
|
|
949
1306
|
background:var(--bg); border-top:1px solid var(--line);
|
|
950
1307
|
padding:.55rem .75rem .8rem; margin:1rem -.75rem 0; }
|
|
951
|
-
.
|
|
1308
|
+
/* The panel goes with the table. Below this width the rows ARE cards, and a panel around
|
|
1309
|
+
a column of cards is a frame drawn round a frame — the thing the two planes were
|
|
1310
|
+
introduced to stop. */
|
|
1311
|
+
.wrap { overflow-x:visible; background:transparent; border:0; border-radius:0; box-shadow:none; }
|
|
952
1312
|
table, tbody { display:block; }
|
|
953
1313
|
table { min-width:0; }
|
|
954
1314
|
thead { display:none; }
|
|
@@ -965,8 +1325,12 @@ ${HISTORY_CSS}
|
|
|
965
1325
|
The cell steps out of the layout entirely, with display:contents, so the row is the flex
|
|
966
1326
|
container and every VALUE is one of its items. Anything else puts a box between the row
|
|
967
1327
|
and the thing being placed, and the order below would have nothing to order. */
|
|
1328
|
+
/* And the strip becomes the card the desktop panel used to hold: filled, on the floor, with
|
|
1329
|
+
the same hairline and the same shadow as a node on the map. Without the fill it would be
|
|
1330
|
+
the one surface on the page still transparent over grey. */
|
|
968
1331
|
tr { display:flex; flex-wrap:wrap; align-items:baseline; column-gap:.4rem; row-gap:.05rem;
|
|
969
|
-
|
|
1332
|
+
background:var(--surface); box-shadow:var(--shadow-1), var(--edge);
|
|
1333
|
+
border:1px solid var(--line); border-left-width:3px; border-radius:var(--r-md);
|
|
970
1334
|
padding:.5rem .75rem .55rem; margin-bottom:.55rem; }
|
|
971
1335
|
/* white-space on the CELL, and not on the row: the desktop rule being undone is
|
|
972
1336
|
td { white-space:nowrap }, and an explicit declaration on the cell beats anything the row
|
|
@@ -1002,11 +1366,6 @@ ${HISTORY_CSS}
|
|
|
1002
1366
|
background session named after its prompt, a name with no length limit. Not the exotic
|
|
1003
1367
|
case: when nothing in the fleet calls itself interactive, every row is drawn as a card. */
|
|
1004
1368
|
.berth-cards .node { flex:1 1 8.5rem; width:auto; min-width:0; }
|
|
1005
|
-
/* A strip sharing a phone's width with a card is an ellipsis where the prompt was — the
|
|
1006
|
-
one line saying what this agent was told to do is the first thing a narrow column takes
|
|
1007
|
-
away. It spans the row instead, like the cells below it. The berth docks its own strips
|
|
1008
|
-
full width at every size, so what this rule is left covering is the REPLAY's flat grid. */
|
|
1009
|
-
.node[data-role="agent"] { grid-column:1 / -1; }
|
|
1010
1369
|
/* Line one: who, and in what state. The project leads and carries the weight; the session
|
|
1011
1370
|
name travels beside it in the page's grey. That order is deliberate and it is a red line
|
|
1012
1371
|
— a background session is NAMED AFTER ITS PROMPT, and a prompt set as the heading of a
|
|
@@ -1087,6 +1446,10 @@ ${HISTORY_CSS}
|
|
|
1087
1446
|
td[data-label="Model"] .v:has(.dim)::before { content:'· model '; }
|
|
1088
1447
|
td[data-label="Effort"] .v:has(.dim)::before { content:'· effort '; }
|
|
1089
1448
|
td[data-label="Cost"] .v:has(.dim)::before { content:'· cost '; }
|
|
1449
|
+
/* The table's strip only. The map's bar survives this on specificity — ".node .bar" above
|
|
1450
|
+
is 0,2,0 against this rule's 0,1,0, and a media query adds none — so an agent's reading
|
|
1451
|
+
is still drawn on the one screen it was reported missing from. No repeat of it here: a
|
|
1452
|
+
rule that changes nothing is a rule the next reader has to prove harmless. */
|
|
1090
1453
|
.bar { display:none; }
|
|
1091
1454
|
${HISTORY_PHONE_CSS} }
|
|
1092
1455
|
</style>
|
|
@@ -1108,6 +1471,11 @@ ${HISTORY_PHONE_CSS} }
|
|
|
1108
1471
|
<a href="/map"${view === 'map' ? ' aria-current="page"' : ''}>Map</a>
|
|
1109
1472
|
<a href="/history"${view === 'history' ? ' aria-current="page"' : ''}>History</a>
|
|
1110
1473
|
</nav>
|
|
1474
|
+
<!-- Where the header folds on a phone. An element and not a pseudo, because the alternative
|
|
1475
|
+
is "order" on the two groups around it — this page's sheet may move a control and never
|
|
1476
|
+
a reading, and both of those are readings. Empty, so there is nothing in it for anyone
|
|
1477
|
+
to be read. -->
|
|
1478
|
+
<span class="hdr-break" aria-hidden="true"></span>
|
|
1111
1479
|
<!-- The account's two windows, page-level because that is what they are: a limit belongs to
|
|
1112
1480
|
the account every session below is spending from, not to any one of them. Their VALUES
|
|
1113
1481
|
come up from the fragment on every poll (the script's limits-src copy), so the header
|
|
@@ -1123,6 +1491,22 @@ ${HISTORY_PHONE_CSS} }
|
|
|
1123
1491
|
<strong>⚠ refresh failing</strong> — nothing on this page has moved since the time in the header.
|
|
1124
1492
|
<span id="why"></span>
|
|
1125
1493
|
</div>
|
|
1494
|
+
<!-- Which of the two fleets is on screen, said in the place the banner below will stand. The
|
|
1495
|
+
banner used to be inserted into the flow the moment a reader took hold of the handle and
|
|
1496
|
+
removed again when they let go, so the page dropped a line on the way in and rose one on
|
|
1497
|
+
the way out — at the exact moment a reader is comparing two minutes of it. The space is
|
|
1498
|
+
spent either way now, and a page that only speaks up when it is showing the past is a page
|
|
1499
|
+
that says nothing on the way back. Up with the scrubber and for the same reason: with no
|
|
1500
|
+
record there is no second state to be telling this one apart from. -->
|
|
1501
|
+
<!-- Served up, unlike the controls below it. It is not one of them: it says the page is showing
|
|
1502
|
+
the fleet now, which is true of a served page before any script runs and true of one where
|
|
1503
|
+
none ever will, and the way BACK from a replay is a button that lives in the banner. Shipped
|
|
1504
|
+
hidden it was the fault this pair exists to remove — the map paints, the record lands a
|
|
1505
|
+
moment later, and a line appears and pushes the whole fleet down. -->
|
|
1506
|
+
<div class="live-state" id="live-state">
|
|
1507
|
+
<strong>● live</strong>
|
|
1508
|
+
<span>— the fleet as the header dates it.</span>
|
|
1509
|
+
</div>
|
|
1126
1510
|
<!-- The one claim on this page that could be a lie, so it is the loudest element on it and it
|
|
1127
1511
|
carries the minute it is showing. Hidden until a script raises it: with no script there
|
|
1128
1512
|
is no replay, and a banner about one would be a warning about nothing. -->
|
|
@@ -1154,9 +1538,15 @@ ${HISTORY_PHONE_CSS} }
|
|
|
1154
1538
|
<!-- "Replay", and nothing about how much of the day it holds: the range is the record's to
|
|
1155
1539
|
state, in the sentence below, which is built around never calling ten minutes a day. -->
|
|
1156
1540
|
<span class="replay-name">Replay</span>
|
|
1157
|
-
|
|
1541
|
+
<!-- The word says what the next press does; the attribute says what is happening now, which
|
|
1542
|
+
is the half a stylesheet can paint and a glance can catch. -->
|
|
1543
|
+
<button type="button" id="play" data-playing="false">Play</button>
|
|
1158
1544
|
<input type="range" id="scrub" min="0" max="0" step="1" value="0" disabled aria-label="Replay position">
|
|
1159
1545
|
<div class="covers" id="covers"></div>
|
|
1546
|
+
<!-- What the line above could not fit, for a reader who cannot hover a title: the standing
|
|
1547
|
+
properties of the record — nothing replayed is dated, and the past is drawn ungrouped.
|
|
1548
|
+
Its own element rather than a child of the line, which the script rewrites wholesale. -->
|
|
1549
|
+
<span class="sr" id="covers-note"></span>
|
|
1160
1550
|
</div>
|
|
1161
1551
|
${view === 'history' ? renderHistoryView({ historyEnabled, demo }) : ''}
|
|
1162
1552
|
<script>${pageScript(view)}</script>${view === 'history' ? `\n<script>${historyScript()}</script>` : ''}
|
|
@@ -1217,6 +1607,28 @@ function pageScript(view) {
|
|
|
1217
1607
|
var off = document.getElementById('offline'), why = document.getElementById('why');
|
|
1218
1608
|
var limits = document.getElementById('limits');
|
|
1219
1609
|
var last = Date.now(), failing = false, inFlight = false, since = 0, gen = 0;
|
|
1610
|
+
// ── the footnotes, across a swap ────────────────────────────────────────────────────
|
|
1611
|
+
// The folds live in the fragment this script replaces every five seconds, so a reader who
|
|
1612
|
+
// opens one gets about two seconds of it before a new one is built shut — a note less
|
|
1613
|
+
// readable folded than it was as a paragraph, which is the opposite of the change. What was
|
|
1614
|
+
// opened is remembered by id and put back on the other side of every swap.
|
|
1615
|
+
//
|
|
1616
|
+
// Capture, because "toggle" does not bubble: it fires on the <details> and nowhere else, so
|
|
1617
|
+
// a listener on the container only hears it on the way down. Delegated rather than bound to
|
|
1618
|
+
// each note, because the notes are exactly what the swap destroys.
|
|
1619
|
+
var openNotes = {};
|
|
1620
|
+
live.addEventListener('toggle', function (ev) {
|
|
1621
|
+
var t = ev && ev.target;
|
|
1622
|
+
if (!t || !t.id || t.id.indexOf('note-') !== 0) return;
|
|
1623
|
+
if (t.open) openNotes[t.id] = 1; else delete openNotes[t.id];
|
|
1624
|
+
}, true);
|
|
1625
|
+
function reopenNotes() {
|
|
1626
|
+
for (var k in openNotes) {
|
|
1627
|
+
if (!Object.prototype.hasOwnProperty.call(openNotes, k)) continue;
|
|
1628
|
+
var d = document.getElementById(k);
|
|
1629
|
+
if (d) d.open = true;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1220
1632
|
// How many polls in a row have come back with nothing usable, and when the last of them was.
|
|
1221
1633
|
// On a phone the page is read on a radio, and one dropped request is a tunnel rather than an
|
|
1222
1634
|
// outage — the banner frames the table off and says the fleet cannot be read, which is the
|
|
@@ -1303,6 +1715,7 @@ function pageScript(view) {
|
|
|
1303
1715
|
if (body.trim() === '') throw new Error('The server answered with an empty page.');
|
|
1304
1716
|
if (!mineStill()) return;
|
|
1305
1717
|
live.innerHTML = body;
|
|
1718
|
+
reopenNotes();
|
|
1306
1719
|
// The account's gauges, lifted out of the fragment and into the header where they
|
|
1307
1720
|
// belong. Here rather than in the fragment's own place on the page because a limit is
|
|
1308
1721
|
// the account's and not a session's; here rather than in the shell alone because the
|
|
@@ -1348,6 +1761,7 @@ function pageScript(view) {
|
|
|
1348
1761
|
|
|
1349
1762
|
var replay = document.getElementById('replay'), scrub = document.getElementById('scrub');
|
|
1350
1763
|
var playBtn = document.getElementById('play'), covers = document.getElementById('covers');
|
|
1764
|
+
var coversNote = document.getElementById('covers-note');
|
|
1351
1765
|
var rview = document.getElementById('replay-view'), rmap = document.getElementById('replay-map');
|
|
1352
1766
|
var rmeta = document.getElementById('replay-meta'), note = document.getElementById('replaying');
|
|
1353
1767
|
var rlimits = document.getElementById('replay-limits');
|
|
@@ -1390,50 +1804,86 @@ function pageScript(view) {
|
|
|
1390
1804
|
return hhmm(t) + (new Date(t).getUTCDate() === new Date(ref).getUTCDate() ? '' : ' yesterday');
|
|
1391
1805
|
}
|
|
1392
1806
|
|
|
1393
|
-
// What the range covers, in the record's own terms
|
|
1807
|
+
// What the range covers, in the record's own terms and in ONE line: the span, how many
|
|
1808
|
+
// readings are in it, and how many minutes have none. Never "a day" — that is the size of the
|
|
1394
1809
|
// ring, and a serve ten minutes old has seen ten minutes.
|
|
1395
|
-
|
|
1810
|
+
//
|
|
1811
|
+
// One line because this is the line under the hand of whoever is dragging the handle, and it
|
|
1812
|
+
// was three of technical prose. What came off it is below, in KEEPS, and it is MOVED rather
|
|
1813
|
+
// than dropped: the standing properties of the record are the honest part of this feature.
|
|
1814
|
+
function coversRange() {
|
|
1396
1815
|
var n = record.samples.length;
|
|
1397
|
-
if (n === 0) {
|
|
1398
|
-
// A record empty because every reading FAILED is not a record that has just started, and
|
|
1399
|
-
// this was the one branch that threw that away: ten hours of a collector that could not
|
|
1400
|
-
// run read exactly like a serve thirty seconds old.
|
|
1401
|
-
return record.missed
|
|
1402
|
-
? 'Nothing recorded — this serve started at ' + hhmm(record.since) + ' UTC and '
|
|
1403
|
-
+ record.missed + ' minute' + (record.missed === 1 ? '' : 's') + ' were due and never read.'
|
|
1404
|
-
: 'Nothing recorded yet — this serve started at ' + hhmm(record.since) + ' UTC'
|
|
1405
|
-
+ ' and takes a reading every ' + Math.round(record.cadence / 1000) + 's.';
|
|
1406
|
-
}
|
|
1407
1816
|
var last = record.samples[n - 1].t;
|
|
1408
|
-
return 'Covering ' + edge(record.since, last) + ' – ' + hhmm(last) + ' UTC'
|
|
1409
|
-
|
|
1410
|
-
// away comes back, so the sentence names the last time this page asked rather than a
|
|
1411
|
-
// moment it may be hours past.
|
|
1412
|
-
+ ', as this page last had it — ' + n + ' reading' + (n === 1 ? '' : 's')
|
|
1817
|
+
return 'Covering ' + edge(record.since, last) + ' – ' + hhmm(last) + ' UTC · '
|
|
1818
|
+
+ n + ' reading' + (n === 1 ? '' : 's')
|
|
1413
1819
|
// A gap that says it is a gap is not a gap. The handle steps through readings, not
|
|
1414
|
-
// through minutes, and a record with holes in it is not a smooth walk
|
|
1415
|
-
|
|
1416
|
-
+
|
|
1417
|
-
// The
|
|
1418
|
-
//
|
|
1419
|
-
//
|
|
1420
|
-
//
|
|
1421
|
-
//
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1820
|
+
// through minutes, and a record with holes in it is not a smooth walk — which is why this
|
|
1821
|
+
// one clause stayed on the visible line while the paragraph below it went.
|
|
1822
|
+
+ (record.missed ? ' · ' + record.missed + ' minute' + (record.missed === 1 ? '' : 's') + ' with no reading' : '')
|
|
1823
|
+
// The two words the paragraph below explains, kept where every reader meets them. A title
|
|
1824
|
+
// has no hover on a touchscreen and no keyboard, and a hidden span is for a screen reader:
|
|
1825
|
+
// moving the whole of it would have left the sighted phone reader — the one this page
|
|
1826
|
+
// argues about, staring at an ungrouped map — with neither half, and an ungrouped map that
|
|
1827
|
+
// says nothing about being ungrouped reads as a rendering that broke.
|
|
1828
|
+
+ ' · undated, ungrouped';
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// The two standing properties of the record, off the line and never out of reach: in the
|
|
1832
|
+
// title for a pointer, and in a span only a screen reader meets. Both are things this page
|
|
1833
|
+
// would otherwise be caught not saying — the ring never kept how old a reading was, and it
|
|
1834
|
+
// never kept the directory a node was read in, so a replayed map is undated and ungrouped.
|
|
1835
|
+
// Shown less and told nothing, a reader reads an ungrouped map as a rendering that broke.
|
|
1836
|
+
var KEEPS = 'The record keeps each reading, not how old that reading was, so nothing replayed'
|
|
1837
|
+
+ ' here is dated. It keeps a project name and never the directory a node was read in, so the'
|
|
1838
|
+
+ ' past is drawn ungrouped, in the order the sample carries.'
|
|
1839
|
+
// The third of them, and the one that qualifies the range itself rather than what is drawn
|
|
1840
|
+
// from it: the record is asked for again only when a tab that has been away comes back, so
|
|
1841
|
+
// the minute this range ends on is the last one this page ASKED for. A map left open on a
|
|
1842
|
+
// desk all afternoon would otherwise read as a record that stops where the fleet did.
|
|
1843
|
+
+ ' The range ends where this page last asked for the record, not at this minute.';
|
|
1844
|
+
|
|
1845
|
+
// The line, and what it could not fit. A message that IS the whole of what there is to say
|
|
1846
|
+
// carries no second copy of itself: a tooltip repeating the line it sits on is how a reader
|
|
1847
|
+
// learns to ignore the next one.
|
|
1848
|
+
function say(line, more) {
|
|
1849
|
+
covers.textContent = line;
|
|
1850
|
+
coversNote.textContent = more;
|
|
1851
|
+
if (more === '') covers.removeAttribute('title');
|
|
1852
|
+
else covers.setAttribute('title', more);
|
|
1428
1853
|
}
|
|
1429
1854
|
|
|
1855
|
+
// What there is to say when there is no range: a message that is the whole of itself, and
|
|
1856
|
+
// carries none of KEEPS — there is nothing replayed for those properties to be true of.
|
|
1857
|
+
//
|
|
1858
|
+
// A record empty because every reading FAILED is not a record that has just started, and this
|
|
1859
|
+
// was the one branch that threw that away: ten hours of a collector that could not run read
|
|
1860
|
+
// exactly like a serve thirty seconds old.
|
|
1861
|
+
function emptyText() {
|
|
1862
|
+
return record.missed
|
|
1863
|
+
? 'Nothing recorded — this serve started at ' + hhmm(record.since) + ' UTC and '
|
|
1864
|
+
+ record.missed + ' minute' + (record.missed === 1 ? '' : 's') + ' were due and never read.'
|
|
1865
|
+
: 'Nothing recorded yet — this serve started at ' + hhmm(record.since) + ' UTC'
|
|
1866
|
+
+ ' and takes a reading every ' + Math.round(record.cadence / 1000) + 's.';
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
// Nothing here touches the state line, and that is the rule rather than an omission: it is
|
|
1870
|
+
// served up, and it says the page is showing the fleet as the header dates it — true of a
|
|
1871
|
+
// record with a day in it, of one a serve is too young to have taken, and of one that could
|
|
1872
|
+
// not be read at all. Hidden on the empty answer it removed a line the server had already
|
|
1873
|
+
// painted: a serve samples on an interval with no leading call, so every page opened on a
|
|
1874
|
+
// fresh one is answered with an empty sample list for a minute, and the fleet jumped up
|
|
1875
|
+
// 42px, 33ms in.
|
|
1430
1876
|
function ready() {
|
|
1431
1877
|
var n = record.samples.length;
|
|
1432
1878
|
replay.hidden = false;
|
|
1433
|
-
|
|
1879
|
+
say(n === 0 ? emptyText() : coversRange(), n === 0 ? '' : KEEPS);
|
|
1434
1880
|
scrub.max = String(n === 0 ? 0 : n - 1);
|
|
1435
1881
|
scrub.disabled = n === 0;
|
|
1436
1882
|
playBtn.disabled = n === 0;
|
|
1883
|
+
// The record grows by a reading a minute, so the same index is a smaller share of it every
|
|
1884
|
+
// time this runs. Refilled here, or a handle nobody has touched since the last poll draws
|
|
1885
|
+
// the fraction of an hour ago.
|
|
1886
|
+
fill();
|
|
1437
1887
|
}
|
|
1438
1888
|
|
|
1439
1889
|
// Revealed, not hidden, when the record cannot be had: a scrubber that silently never
|
|
@@ -1442,7 +1892,7 @@ function pageScript(view) {
|
|
|
1442
1892
|
replay.hidden = false;
|
|
1443
1893
|
scrub.disabled = true;
|
|
1444
1894
|
playBtn.disabled = true;
|
|
1445
|
-
|
|
1895
|
+
say(said, '');
|
|
1446
1896
|
}
|
|
1447
1897
|
|
|
1448
1898
|
function load() {
|
|
@@ -1502,7 +1952,15 @@ function pageScript(view) {
|
|
|
1502
1952
|
+ '<span class="project">' + esc(x.project) + '</span>'
|
|
1503
1953
|
+ '<span class="kind">' + esc(x.kind) + '</span></div>'
|
|
1504
1954
|
+ (state === 'waiting' && x.waitingFor ? '<div class="sub waiting-for">' + esc(x.waitingFor) + '</div>' : '')
|
|
1505
|
-
|
|
1955
|
+
// The same fragment the server writes for a live strip, in the browser's copy of the
|
|
1956
|
+
// renderer: a bar for the magnitude, the number beside it, and the word that says which
|
|
1957
|
+
// quantity the bar is about. Clamped, because the track sits in a fixed box and a fill
|
|
1958
|
+
// wider than it paints over the text next to it.
|
|
1959
|
+
+ (pct === null
|
|
1960
|
+
? ''
|
|
1961
|
+
: '<div class="sub"><span class="ctx">ctx <span class="bar"><i style="width:'
|
|
1962
|
+
+ Math.max(0, Math.min(100, pct)) + '%"></i></span>'
|
|
1963
|
+
+ '<span class="ctx-pct">' + pct + '%</span></span></div>')
|
|
1506
1964
|
+ (typeof x.costUsd === 'number' ? '<div class="sub">$' + x.costUsd.toFixed(2) + '</div>' : '')
|
|
1507
1965
|
+ '</article>';
|
|
1508
1966
|
}
|
|
@@ -1632,12 +2090,32 @@ function pageScript(view) {
|
|
|
1632
2090
|
: '$' + cost.toFixed(2) + (reporting < n ? ' (' + reporting + '/' + n + ' reporting cost)' : ''));
|
|
1633
2091
|
}
|
|
1634
2092
|
|
|
2093
|
+
/**
|
|
2094
|
+
* How much of the record is behind the handle, as the track's own gradient reads it.
|
|
2095
|
+
*
|
|
2096
|
+
* Webkit gives a range input two shadow parts and no third one for the walked half, so the
|
|
2097
|
+
* fill is a stop in the track's background and this is where the stop is. Firefox has
|
|
2098
|
+
* ::-moz-range-progress and needs none of it, which is why the value is a token rather than a
|
|
2099
|
+
* background written from here: one number, two engines, and the sheet decides what each
|
|
2100
|
+
* does with it.
|
|
2101
|
+
*
|
|
2102
|
+
* A record of one reading has a max of 0 and nothing to divide by. Infinity and NaN are both
|
|
2103
|
+
* answers a browser throws the whole gradient away for, leaving a track that never fills for
|
|
2104
|
+
* anybody — so a span of nothing is nought per cent, which is where the handle is.
|
|
2105
|
+
*/
|
|
2106
|
+
function fill() {
|
|
2107
|
+
var m = Number(scrub.max);
|
|
2108
|
+
var share = m > 0 ? (Number(scrub.value) / m) * 100 : 0;
|
|
2109
|
+
scrub.style.setProperty('--p', (share >= 0 && share <= 100 ? share : 0).toFixed(2) + '%');
|
|
2110
|
+
}
|
|
2111
|
+
|
|
1635
2112
|
function draw(i) {
|
|
1636
2113
|
var s = record && record.samples[i];
|
|
1637
2114
|
if (!s) return;
|
|
1638
2115
|
at = i;
|
|
1639
2116
|
replaying = true;
|
|
1640
2117
|
scrub.value = String(i);
|
|
2118
|
+
fill();
|
|
1641
2119
|
// The handle's own value is an index, so a reader who cannot see the banner would be read
|
|
1642
2120
|
// "3" while the fleet on screen is three hours old. The minute travels with the handle.
|
|
1643
2121
|
var minute = hhmm(s.t) + ' UTC';
|
|
@@ -1658,6 +2136,10 @@ function pageScript(view) {
|
|
|
1658
2136
|
function stopPlay() {
|
|
1659
2137
|
if (playing) { clearInterval(playing); playing = null; }
|
|
1660
2138
|
playBtn.textContent = 'Play';
|
|
2139
|
+
// Both halves, every time: the word for the press that comes next, the attribute for what
|
|
2140
|
+
// is happening now. Set here rather than only where a reader clicks, because the walk also
|
|
2141
|
+
// ends on its own at the last reading.
|
|
2142
|
+
playBtn.setAttribute('data-playing', 'false');
|
|
1661
2143
|
}
|
|
1662
2144
|
|
|
1663
2145
|
// Back to now, in one gesture, with nothing of the past left behind a hidden attribute.
|
|
@@ -1682,6 +2164,7 @@ function pageScript(view) {
|
|
|
1682
2164
|
// has played nothing.
|
|
1683
2165
|
draw(at < 0 || at >= record.samples.length - 1 ? 0 : at);
|
|
1684
2166
|
playBtn.textContent = 'Pause';
|
|
2167
|
+
playBtn.setAttribute('data-playing', 'true');
|
|
1685
2168
|
playing = setInterval(function () {
|
|
1686
2169
|
// It stops at the end rather than looping back: a day that restarts on its own is a
|
|
1687
2170
|
// day whose beginning and end are impossible to tell apart.
|
|
@@ -1866,10 +2349,25 @@ function renderNode({ row: r, role, state, reading, measured, pulse }) {
|
|
|
1866
2349
|
// around it and the table a column header over it, and a bare `61%` under a line of prompt
|
|
1867
2350
|
// reads as how much of the prompt is done. Each part is dropped on its own field being
|
|
1868
2351
|
// null — a snapshot with no turn behind it has a model in it and no percentage.
|
|
1869
|
-
|
|
2352
|
+
//
|
|
2353
|
+
// The reading is DRAWN now, in the bar the table's Context column already speaks — a
|
|
2354
|
+
// magnitude at a glance beside the number that is the authority, track in --line and fill
|
|
2355
|
+
// in --dim, coloured by neither the state nor a threshold. What it is not is a small dial:
|
|
2356
|
+
// an arc at that size cannot be read (5% and 15% draw the same silhouette), and a ring on
|
|
2357
|
+
// an agent is the claim #170 removed. A strip stays a line of text, now with 44x5px of
|
|
2358
|
+
// graphic on it.
|
|
2359
|
+
//
|
|
2360
|
+
// Which is why the percentage leaves the escaped list: `published` puts `esc` over every
|
|
2361
|
+
// one of its members, and markup through it would come back as text.
|
|
2362
|
+
const ctx = pct === null
|
|
2363
|
+
? ''
|
|
2364
|
+
: `<span class="ctx">ctx <span class="bar"><i style="width:${Math.max(0, Math.min(100, pct))}%"></i></span>` +
|
|
2365
|
+
`<span class="ctx-pct">${pct}%</span></span>`;
|
|
2366
|
+
const meta = [r.model, r.effort]
|
|
1870
2367
|
.filter((v) => v !== null && v !== '')
|
|
1871
2368
|
.map(esc)
|
|
1872
2369
|
.join(' · ');
|
|
2370
|
+
const published = ctx === '' ? meta : meta === '' ? ctx : `${ctx} · ${meta}`;
|
|
1873
2371
|
return `<article class="node" data-role="${role}" data-state="${state}" data-reading="${reading}">
|
|
1874
2372
|
<div class="who"><span class="shape" aria-hidden="true">${SHAPE[state]}</span><span class="sr">${esc(stateWord(state, r))}</span><span class="prompt">${esc(r.name)}</span><span class="kind">${esc(r.kind)}</span></div>
|
|
1875
2373
|
${state === 'waiting' && r.waitingFor ? `<div class="sub waiting-for">${esc(r.waitingFor)}</div>` : ''}
|