@adrrr/tarmac 0.6.0 → 0.7.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 +58 -54
- package/dist/args.js +14 -8
- package/dist/cli.js +25 -0
- package/dist/render.js +30 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,30 +45,33 @@ atlas idle — fresh 8h ! Opus 5 high $0.00 8h
|
|
|
45
45
|
account 5h 17% resets in 2h 14m · 7d 42% resets in 3d 11h · as of 7m
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
That is the table with the status line chained. The last line
|
|
49
|
-
session
|
|
50
|
-
[window](docs/MANUAL.md#the-accounts-two-windows), so it is printed once
|
|
51
|
-
other reading here, and `— no reading` rather than `0%` when no snapshot
|
|
48
|
+
That is the table with the status line chained. The last line belongs to the account, not to
|
|
49
|
+
any one session. Every row above spends from the same five-hour and seven-day
|
|
50
|
+
[window](docs/MANUAL.md#the-accounts-two-windows), so it is printed once. It is dated like
|
|
51
|
+
every other reading here, and it reads `— no reading` rather than `0%` when no snapshot
|
|
52
|
+
carried one.
|
|
52
53
|
|
|
53
54
|
With nothing installed, the same command still lists every session, its state and its uptime,
|
|
54
|
-
straight from `claude agents --json`. The context column reads `— absent
|
|
55
|
-
cost fall to
|
|
55
|
+
straight from `claude agents --json`. The context column reads `— absent`. Model, effort and
|
|
56
|
+
cost fall to `—`. The line under the table counts how many sessions are covered.
|
|
56
57
|
|
|
57
58
|
Node ≥ 20. Zero runtime dependencies: no framework, no bundler, nothing to audit.
|
|
58
|
-
`--help`
|
|
59
|
-
|
|
59
|
+
`--help` and `--version` (`-v`) work everywhere. An option handed to a command that does not
|
|
60
|
+
read it is an error, never something quietly ignored. Every command, flag and route is in
|
|
60
61
|
[the manual](docs/MANUAL.md#commands-and-options).
|
|
61
62
|
|
|
62
63
|
## The map
|
|
63
64
|
|
|
64
|
-
`tarmac serve` puts the same fleet in the browser
|
|
65
|
-
climbing, and a banner the moment a refresh fails
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
that
|
|
71
|
-
|
|
65
|
+
`tarmac serve` puts the same fleet in the browser. Every session is a row, the ages keep
|
|
66
|
+
climbing, and a banner goes up the moment a refresh fails, so the table never goes stale in
|
|
67
|
+
silence.
|
|
68
|
+
|
|
69
|
+
It binds to loopback. It refuses any request whose `Host` is not loopback, and any request a
|
|
70
|
+
browser marks as coming from another origin, so your cwd paths and costs never leave the
|
|
71
|
+
machine. A client that sends no such mark, curl or a script, is left alone. A reverse proxy
|
|
72
|
+
presents a `Host` of its own, so `--trust-host <name>` names the one to let through;
|
|
73
|
+
[the manual](docs/MANUAL.md#putting-it-behind-a-reverse-proxy) says who that lets in. The
|
|
74
|
+
listening rules are [there too](docs/MANUAL.md#what-serve-listens-on).
|
|
72
75
|
|
|
73
76
|
<picture>
|
|
74
77
|
<source media="(prefers-color-scheme: dark)" srcset="docs/media/map-dark.png">
|
|
@@ -76,47 +79,47 @@ rules: [the manual](docs/MANUAL.md#what-serve-listens-on).
|
|
|
76
79
|
alt="The tarmac map, live. Four framed groups, one per working directory and labelled with its project: beacon, holding a session halted on a permission prompt; harbor, holding a session busy with its context arc at 90% and a background agent docked under the cards of that frame as a strip named after its prompt; quay, reporting a status tarmac does not know; and atlas, idle at 36%. A frame says only that its nodes were read in the same directory. Nothing inside one claims that any node dispatched another. The account's five-hour and seven-day gauges sit above them, and one warning above the fleet names the unrecognised status rather than filing it as idle.">
|
|
77
80
|
</picture>
|
|
78
81
|
|
|
79
|
-
The tab in the header swaps the table for the same fleet as nodes: one per session, the
|
|
80
|
-
its context, the shape by the name its state. A background agent has no terminal behind it
|
|
81
|
-
draw a frame with, so it gets a strip of text rather than a dial that could never fill. Both
|
|
82
|
+
The tab in the header swaps the table for the same fleet drawn as nodes: one per session, the
|
|
83
|
+
arc its context, the shape by the name its state. A background agent has no terminal behind it
|
|
84
|
+
to draw a frame with, so it gets a strip of text rather than a dial that could never fill. Both
|
|
82
85
|
views render the same reading into the same fragment, so they can never disagree.
|
|
83
86
|
|
|
84
87
|
The nodes are grouped by working directory: a frame per directory, labelled with its project,
|
|
85
|
-
the sessions inside it as cards and the agents docked underneath as strips.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
the sessions inside it as cards and the agents docked underneath as strips. A frame claims one
|
|
89
|
+
thing and no more: these nodes were read in the same directory. `claude agents --json`
|
|
90
|
+
publishes nothing that ties an agent to whoever dispatched it, so no label, no position and no
|
|
91
|
+
line inside a frame says one node asked for another. An agent whose directory matches no
|
|
92
|
+
session gets its own frame rather than somebody else's.
|
|
90
93
|
|
|
91
94
|
Above the fleet are the account's five-hour and seven-day
|
|
92
|
-
[gauges](docs/MANUAL.md#the-accounts-two-windows)
|
|
93
|
-
serve has seen. Drag it and the nodes render the fleet as it was at that minute
|
|
94
|
-
and the day walks past. Replayed,
|
|
95
|
-
name and never the directory it was read in, and a basename is not a directory. Two
|
|
96
|
-
of `atlas` answer to the same word, so a frame drawn on it would claim a shared
|
|
97
|
-
nobody can check.
|
|
95
|
+
[gauges](docs/MANUAL.md#the-accounts-two-windows). Under it is a scrubber over the day this
|
|
96
|
+
serve has seen. Drag it and the nodes render the fleet as it was at that minute. Press play
|
|
97
|
+
and the day walks past. Replayed, the nodes are drawn flat and unframed. The record keeps a
|
|
98
|
+
project name and never the directory it was read in, and a basename is not a directory. Two
|
|
99
|
+
checkouts of `atlas` answer to the same word, so a frame drawn on it would claim a shared
|
|
100
|
+
directory nobody can check.
|
|
98
101
|
|
|
99
102
|
The map follows the rules the table follows. A reading past the freshness threshold is drawn
|
|
100
|
-
thin, amber and dated. A percentage nobody measured is an empty dotted dial
|
|
101
|
-
|
|
102
|
-
snapshot for, nothing is drawn in its place
|
|
103
|
+
thin, amber and dated. A percentage nobody measured is an empty dotted dial, never a ring at
|
|
104
|
+
zero. Where nothing was published at all, such as the strip of an agent the join found no
|
|
105
|
+
snapshot for, nothing is drawn in its place: no dial, no dash. A word
|
|
103
106
|
`claude agents --json` printed that tarmac has no boolean for is shown as it came rather than
|
|
104
107
|
filed as `idle`, and raises one warning above the fleet naming it. The page is JSON
|
|
105
108
|
underneath: `GET /api/fleet` and `GET /api/history` serve exactly what the map draws. Details
|
|
106
|
-
in [the manual](docs/MANUAL.md#the-map).
|
|
109
|
+
are in [the manual](docs/MANUAL.md#the-map).
|
|
107
110
|
|
|
108
111
|
## Install
|
|
109
112
|
|
|
110
113
|
`install` changes the `statusLine` key of `~/.claude/settings.json`, and never on your
|
|
111
114
|
say-so alone. It prints the whole plan first, including the exact command that undoes it,
|
|
112
|
-
then waits for a typed word. `y` is not an answer
|
|
115
|
+
then waits for a typed word. `y` is not an answer. Scripts pass `--yes`, deliberately.
|
|
113
116
|
|
|
114
117
|
A status line you already had is wrapped, not replaced: its display stays byte-identical, and
|
|
115
118
|
`uninstall` names which of its four restore modes ran. Two files land under `~/.claude/`, the
|
|
116
119
|
wrapper and the `backup.json` that undoes it, and neither changes at runtime. The snapshots go
|
|
117
120
|
to `~/.local/state/tarmac/snapshots` (`$XDG_STATE_HOME` when set), because `~/.claude` is a
|
|
118
|
-
directory people commit. The plan, the restore modes and the cleanup of the layout that
|
|
119
|
-
snapshots inside `.claude
|
|
121
|
+
directory people commit. The plan, the restore modes and the cleanup of the old layout that
|
|
122
|
+
kept snapshots inside `.claude` are in
|
|
120
123
|
[the manual](docs/MANUAL.md#installing-safely-the-full-contract).
|
|
121
124
|
|
|
122
125
|
## Why it does not break
|
|
@@ -132,17 +135,18 @@ tarmac reads two things instead:
|
|
|
132
135
|
| `claude agents --json` | which sessions exist, busy, idle or waiting on you, cwd, uptime | a documented CLI surface (`--help`: *"Print active sessions … as a JSON array … for scripting"*) |
|
|
133
136
|
| the status line payload | context %, model, effort, cost | the JSON Claude Code hands to your own `statusLine.command` on every frame. Observed, not published as a schema |
|
|
134
137
|
|
|
135
|
-
That second
|
|
136
|
-
|
|
137
|
-
kind of missing it is: `absent` for a session no status
|
|
138
|
-
that has taken no turn yet, `drift` for a release that
|
|
139
|
-
stale reading keeps its value and gets dated with a
|
|
140
|
-
gets its version named *before* anything breaks,
|
|
141
|
-
state-by-state table is in
|
|
138
|
+
That second row is the honest caveat. tarmac does not promise the payload will hold still.
|
|
139
|
+
It promises that a measurement it could not take never renders as a confident `0`. What you
|
|
140
|
+
get instead is an em dash naming which kind of missing it is: `absent` for a session no status
|
|
141
|
+
line ever wrote for, `fresh` for one that has taken no turn yet, `drift` for a release that
|
|
142
|
+
moved the payload out from under us. A stale reading keeps its value and gets dated with a
|
|
143
|
+
`!`. A Claude Code build no fixture covers gets its version named *before* anything breaks,
|
|
144
|
+
and tarmac keeps reporting. The full state-by-state table is in
|
|
145
|
+
[the manual](docs/MANUAL.md#degradation-state-by-state).
|
|
142
146
|
|
|
143
147
|
## Configuration
|
|
144
148
|
|
|
145
|
-
Three numbers are
|
|
149
|
+
Three numbers here are judgement calls, so all three are yours to set, and behind a reverse
|
|
146
150
|
proxy the hosts `serve` answers to are yours as well. Everything else is deliberately not
|
|
147
151
|
configurable, and all of it works with no configuration at all.
|
|
148
152
|
|
|
@@ -151,22 +155,22 @@ configurable, and all of it works with no configuration at all.
|
|
|
151
155
|
| freshness threshold | `--stale-after 90s` \| `15m` \| `2h` | `10m` |
|
|
152
156
|
| port | `--port 8080` | `4477` |
|
|
153
157
|
| snapshots dir (read side) | `--snapshots-dir DIR` | the wrapper's frozen path when installed, else the XDG state directory |
|
|
154
|
-
| trusted hosts | `--trust-host HOST`, once per host | none
|
|
158
|
+
| trusted hosts | `--trust-host HOST`, once per host | none, so loopback only |
|
|
155
159
|
|
|
156
160
|
Each also has an environment variable and a key in `<home>/.claude/tarmac/config.json`.
|
|
157
|
-
Flag beats environment beats config file beats default, settled per setting
|
|
161
|
+
Flag beats environment beats config file beats default, settled per setting. `serve` opens by
|
|
158
162
|
printing each effective value and where it came from. Nothing is silently dropped or silently
|
|
159
163
|
corrected. A value that will not parse stops the run and says what it got, where it came from,
|
|
160
164
|
and what would have worked. Spellings, edge cases and the two health fields
|
|
161
|
-
`tarmac list --json` reports
|
|
165
|
+
`tarmac list --json` reports are in [the manual](docs/MANUAL.md#configuration).
|
|
162
166
|
|
|
163
167
|
## What it deliberately does not do
|
|
164
168
|
|
|
165
169
|
- **No inferred "waiting for you".** `agents --json` reports `waiting` with a reason, such as
|
|
166
170
|
a permission prompt or an open dialog, and tarmac draws exactly that, in the table and on
|
|
167
|
-
the map.
|
|
168
|
-
|
|
169
|
-
|
|
171
|
+
the map. It does not guess at the rest. A session that asked you a question in prose still
|
|
172
|
+
reports `idle`, because the only way to know better is to read a transcript, and this tool
|
|
173
|
+
does not read transcripts.
|
|
170
174
|
- **No history on disk.** `tarmac list` is a snapshot in time. A running `serve` holds the
|
|
171
175
|
last 24 hours of the readings it took itself, in memory, so the page can replay them. That
|
|
172
176
|
record reaches no further back than the serve that took it, and goes when it goes. None of
|
|
@@ -182,9 +186,9 @@ npm run build # flat JavaScript into dist/
|
|
|
182
186
|
node scripts/demo-fleet.ts # the invented fleet the captures above are taken of
|
|
183
187
|
```
|
|
184
188
|
|
|
185
|
-
Every capture on this page is taken of a fleet that does not exist
|
|
186
|
-
|
|
187
|
-
|
|
189
|
+
Every capture on this page is taken of a fleet that does not exist. A screenshot of a real
|
|
190
|
+
machine carries working directories, prompts and costs, and nothing real enters this repo.
|
|
191
|
+
What CI covers, which Node version develops and which one ships:
|
|
188
192
|
[the manual](docs/MANUAL.md#developing). Capturing the fixtures for a new Claude Code build:
|
|
189
193
|
[the manual](docs/MANUAL.md#capturing-a-new-claude-code-version).
|
|
190
194
|
|
package/dist/args.js
CHANGED
|
@@ -19,8 +19,13 @@ const OPTIONS = {
|
|
|
19
19
|
'--watch': 'watch',
|
|
20
20
|
'--yes': 'yes',
|
|
21
21
|
'--help': 'help',
|
|
22
|
+
'--version': 'version',
|
|
23
|
+
// The one short spelling this parser knows, and it is here because it is what people type
|
|
24
|
+
// before they read anything (#110). No other flag gets one: a letter is a scarce name, and
|
|
25
|
+
// `-h` for help would be the next request and the one after that.
|
|
26
|
+
'-v': 'version',
|
|
22
27
|
};
|
|
23
|
-
const FLAGS = new Set(['json', 'watch', 'yes', 'help']);
|
|
28
|
+
const FLAGS = new Set(['json', 'watch', 'yes', 'help', 'version']);
|
|
24
29
|
/** Options that ACCUMULATE rather than overwrite — passed twice, both values are kept. */
|
|
25
30
|
const LISTS = new Set(['trustHost']);
|
|
26
31
|
/**
|
|
@@ -29,14 +34,15 @@ const LISTS = new Set(['trustHost']);
|
|
|
29
34
|
* one level down — `tarmac serve --json` parsed cleanly and changed nothing, and from the
|
|
30
35
|
* outside that is indistinguishable from a server that decided to answer HTML anyway.
|
|
31
36
|
*
|
|
32
|
-
* `help` is in every set on purpose: `--help` is answerable whatever else was typed
|
|
37
|
+
* `help` is in every set on purpose: `--help` is answerable whatever else was typed, and
|
|
38
|
+
* `version` is there for the same reason — what build this is does not depend on the verb.
|
|
33
39
|
*/
|
|
34
40
|
const ACCEPTS = {
|
|
35
|
-
list: new Set(['staleAfter', 'snapshotsDir', 'home', 'claudeBin', 'json', 'watch', 'help']),
|
|
36
|
-
serve: new Set(['port', 'staleAfter', 'snapshotsDir', 'trustHost', 'home', 'claudeBin', 'help']),
|
|
37
|
-
install: new Set(['home', 'yes', 'help']),
|
|
38
|
-
uninstall: new Set(['home', 'yes', 'help']),
|
|
39
|
-
help: new Set(['help']),
|
|
41
|
+
list: new Set(['staleAfter', 'snapshotsDir', 'home', 'claudeBin', 'json', 'watch', 'help', 'version']),
|
|
42
|
+
serve: new Set(['port', 'staleAfter', 'snapshotsDir', 'trustHost', 'home', 'claudeBin', 'help', 'version']),
|
|
43
|
+
install: new Set(['home', 'yes', 'help', 'version']),
|
|
44
|
+
uninstall: new Set(['home', 'yes', 'help', 'version']),
|
|
45
|
+
help: new Set(['help', 'version']),
|
|
40
46
|
};
|
|
41
47
|
/** The commands a misplaced flag would have been right on — an error that points somewhere. */
|
|
42
48
|
function ownersOf(key) {
|
|
@@ -54,7 +60,7 @@ export function accepts(command, flag) {
|
|
|
54
60
|
return key !== undefined && ACCEPTS[command].has(key);
|
|
55
61
|
}
|
|
56
62
|
export function parseArgs(argv) {
|
|
57
|
-
const out = { command: 'list', port: null, staleAfter: null, snapshotsDir: null, trustHost: [], home: null, claudeBin: 'claude', json: false, watch: false, yes: false, help: false };
|
|
63
|
+
const out = { command: 'list', port: null, staleAfter: null, snapshotsDir: null, trustHost: [], home: null, claudeBin: 'claude', json: false, watch: false, yes: false, help: false, version: false };
|
|
58
64
|
let i = 0;
|
|
59
65
|
if (argv[0] && !argv[0].startsWith('-')) {
|
|
60
66
|
if (!COMMANDS.has(argv[0]))
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
//
|
|
8
8
|
// `install` / `uninstall` default to the home this process runs under, print what they are
|
|
9
9
|
// about to change, and proceed only on the typed verb (or `--yes`, in writing, for scripts).
|
|
10
|
+
import fs from 'node:fs';
|
|
10
11
|
import os from 'node:os';
|
|
11
12
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
12
13
|
import { parseArgs } from './args.js';
|
|
@@ -31,6 +32,7 @@ const USAGE = `tarmac — fleet observability for Claude Code
|
|
|
31
32
|
tarmac uninstall [--home DIR] [--yes]
|
|
32
33
|
restore the statusline exactly
|
|
33
34
|
|
|
35
|
+
--version, -v print the version of this build and exit — on any command
|
|
34
36
|
--watch redraw the table every 5s until ^C, dating every reading
|
|
35
37
|
--home whose .claude to read or change (default: this home)
|
|
36
38
|
--yes skip the typed confirmation — required when stdin is not a terminal
|
|
@@ -52,6 +54,22 @@ const USAGE = `tarmac — fleet observability for Claude Code
|
|
|
52
54
|
and by <home>/.claude/tarmac/config.json ({"staleAfterMs": …, "port": …,
|
|
53
55
|
"snapshotsDir": …, "trustHosts": […]}). \`serve\` prints which one won.
|
|
54
56
|
`;
|
|
57
|
+
/**
|
|
58
|
+
* What this build calls itself, read from the `package.json` that ships beside it — the one
|
|
59
|
+
* file npm puts in every tarball whatever `files` says, and the only place the number is
|
|
60
|
+
* written down. `dist/cli.js` sits one directory in exactly as `src/cli.ts` does, so a single
|
|
61
|
+
* relative path answers for the published CLI and for the suite, which runs the source.
|
|
62
|
+
*
|
|
63
|
+
* A version it could not read is not a version: rather than print an "unknown" nobody can act
|
|
64
|
+
* on, this throws and leaves through the same catch as every other refusal here.
|
|
65
|
+
*/
|
|
66
|
+
function version() {
|
|
67
|
+
const at = new URL('../package.json', import.meta.url);
|
|
68
|
+
const pkg = JSON.parse(fs.readFileSync(at, 'utf8'));
|
|
69
|
+
if (typeof pkg.version !== 'string' || pkg.version === '')
|
|
70
|
+
throw new Error(`${at.href} carries no version`);
|
|
71
|
+
return pkg.version;
|
|
72
|
+
}
|
|
55
73
|
try {
|
|
56
74
|
// Parsing is inside the try so that a refusal — a typo'd flag, a duration nobody can read,
|
|
57
75
|
// a config file with a key that does not exist — reaches the user as one line naming the
|
|
@@ -61,6 +79,13 @@ try {
|
|
|
61
79
|
process.stdout.write(USAGE);
|
|
62
80
|
process.exit(0);
|
|
63
81
|
}
|
|
82
|
+
// After `--help`, which answers the wider question, and before any command has begun: a
|
|
83
|
+
// `tarmac install --version` that printed a plan and then waited for a typed word would be
|
|
84
|
+
// a one-line question that hangs a script. Bare, so something other than a person can read it.
|
|
85
|
+
if (args.version) {
|
|
86
|
+
process.stdout.write(`${version()}\n`);
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
64
89
|
if (args.command === 'install' || args.command === 'uninstall') {
|
|
65
90
|
const home = args.home ?? os.homedir();
|
|
66
91
|
// The plan is computed first and printed whole: every refusal this operation has in it
|
package/dist/render.js
CHANGED
|
@@ -988,7 +988,19 @@ export function renderPage(fleet, view = 'table') {
|
|
|
988
988
|
word. min-width:0 for the same reason the berths carry it: a flex item's automatic
|
|
989
989
|
minimum is its min-content width, which without this is the whole unbroken string. */
|
|
990
990
|
td[data-label="Project"] .v { order:1; font-weight:600; min-width:0; overflow-wrap:anywhere; }
|
|
991
|
-
|
|
991
|
+
/* The basis is auto and not 0, which is what decides whether the name breaks INSIDE
|
|
992
|
+
itself. A basis of 0 puts it on the first line however little is left of one: at 390px
|
|
993
|
+
beside a 40-character project that is 44.6px of column, a session id cut across two
|
|
994
|
+
lines, the pill dropped alone underneath and a 63px strip standing at 105.7 — on
|
|
995
|
+
exactly the fleets whose checkout names are long. Its own width is the floor it wraps
|
|
996
|
+
at instead: beside the project where it fits, on the next line whole where it does not.
|
|
997
|
+
That floor is a trade, not a free fix: a name longer than one full line keeps a line of
|
|
998
|
+
its own at EVERY width, where a basis of 0 let it compress back beside the project as
|
|
999
|
+
the screen widened. The accepted side of #108: never cut a name mid-word, at the price
|
|
1000
|
+
of a taller strip for prompt-named agents.
|
|
1001
|
+
min-width:0 is what still lets it shrink once it is the widest thing on a line of its
|
|
1002
|
+
own, which is the wrap the rule above is written for. */
|
|
1003
|
+
td[data-label="Session"] .v { order:2; flex:1 1 auto; min-width:0; color:var(--dim);
|
|
992
1004
|
white-space:normal; overflow-wrap:anywhere; }
|
|
993
1005
|
/* The third value that can arrive as one unbroken token, and the one the two rules above
|
|
994
1006
|
missed: a waiting reason is FREE TEXT, so "permission prompt: /Users/…/foo.ts" is a path
|
|
@@ -1321,12 +1333,20 @@ function pageScript(view) {
|
|
|
1321
1333
|
}
|
|
1322
1334
|
|
|
1323
1335
|
function two(n) { return (n < 10 ? '0' : '') + n; }
|
|
1324
|
-
|
|
1336
|
+
// One page, one clock, and it is the one the summary line already dates the fleet on: an ISO
|
|
1337
|
+
// instant in UTC. Spelled on the machine's own clock, these minutes and that stamp are the
|
|
1338
|
+
// same fleet apparently living in two time zones, the first time anything shows both — an
|
|
1339
|
+
// export, a log line, a screenshot with the header in it. Every minute below carries UTC
|
|
1340
|
+
// where a reader meets it, because an unlabelled one reads as the reader's own clock and is
|
|
1341
|
+
// the same lie facing the other way.
|
|
1342
|
+
function hhmm(t) { var d = new Date(t); return two(d.getUTCHours()) + ':' + two(d.getUTCMinutes()); }
|
|
1325
1343
|
|
|
1326
1344
|
// A day-long ring can straddle one midnight, and "09:14 – 08:59" reads as a span running
|
|
1327
|
-
// backwards until the older edge says which day it is.
|
|
1345
|
+
// backwards until the older edge says which day it is. Which midnight is UTC's, like the
|
|
1346
|
+
// minutes it separates: counted otherwise, "yesterday" turns up between two minutes of one
|
|
1347
|
+
// UTC day and goes missing across the midnight it exists for.
|
|
1328
1348
|
function edge(t, ref) {
|
|
1329
|
-
return hhmm(t) + (new Date(t).
|
|
1349
|
+
return hhmm(t) + (new Date(t).getUTCDate() === new Date(ref).getUTCDate() ? '' : ' yesterday');
|
|
1330
1350
|
}
|
|
1331
1351
|
|
|
1332
1352
|
// What the range covers, in the record's own terms. Never "a day": that is the size of the
|
|
@@ -1338,13 +1358,13 @@ function pageScript(view) {
|
|
|
1338
1358
|
// this was the one branch that threw that away: ten hours of a collector that could not
|
|
1339
1359
|
// run read exactly like a serve thirty seconds old.
|
|
1340
1360
|
return record.missed
|
|
1341
|
-
? 'Nothing recorded — this serve started at ' + hhmm(record.since) + ' and '
|
|
1361
|
+
? 'Nothing recorded — this serve started at ' + hhmm(record.since) + ' UTC and '
|
|
1342
1362
|
+ record.missed + ' minute' + (record.missed === 1 ? '' : 's') + ' were due and never read.'
|
|
1343
|
-
: 'Nothing recorded yet — this serve started at ' + hhmm(record.since)
|
|
1363
|
+
: 'Nothing recorded yet — this serve started at ' + hhmm(record.since) + ' UTC'
|
|
1344
1364
|
+ ' and takes a reading every ' + Math.round(record.cadence / 1000) + 's.';
|
|
1345
1365
|
}
|
|
1346
1366
|
var last = record.samples[n - 1].t;
|
|
1347
|
-
return 'Covering ' + edge(record.since, last) + ' – ' + hhmm(last)
|
|
1367
|
+
return 'Covering ' + edge(record.since, last) + ' – ' + hhmm(last) + ' UTC'
|
|
1348
1368
|
// Not "when the page loaded": the record is asked for again when a tab that has been
|
|
1349
1369
|
// away comes back, so the sentence names the last time this page asked rather than a
|
|
1350
1370
|
// moment it may be hours past.
|
|
@@ -1579,8 +1599,9 @@ function pageScript(view) {
|
|
|
1579
1599
|
scrub.value = String(i);
|
|
1580
1600
|
// The handle's own value is an index, so a reader who cannot see the banner would be read
|
|
1581
1601
|
// "3" while the fleet on screen is three hours old. The minute travels with the handle.
|
|
1582
|
-
|
|
1583
|
-
|
|
1602
|
+
var minute = hhmm(s.t) + ' UTC';
|
|
1603
|
+
scrub.setAttribute('aria-valuetext', minute);
|
|
1604
|
+
atEl.textContent = minute;
|
|
1584
1605
|
rmeta.textContent = metaOf(s);
|
|
1585
1606
|
rmap.innerHTML = nodesOf(s);
|
|
1586
1607
|
// The account of that minute, in the place the live pair occupies — which the body class
|