The live socket never made a REST call, so the seven-day refresh
token expired while the chart still looked fine. A deploy then
could not log in. Ping user preferences every six hours, and when
the grant is already dead offer a one-click reconnect that writes
the token on the existing callback.
Trendlines project into the whitespace beyond the newest candle, but the time
axis stopped there, so a converging pair could be seen without knowing when it
converges. Lightweight Charts only labels times present on its scale, so the
chart now carries whitespace points past the last bar: no value, nothing drawn,
but the axis has something to label and timeToCoordinate answers out there.
Future times repeat the most recent bar interval, which is what timeAtIndex
already does for the projections themselves. That drifts across the daily halt
and the weekend; agreeing with the projected line matters more than abstract
accuracy, and session-accurate projection needs server-side session rules the
client does not have.
Two bugs surfaced while measuring it. Padding meant to be five bars measured as
sixty-seven, because the interval came from the gap between the final two bars;
barInterval now takes a median over recent bars and ignores a ragged tail.
And that gap was two seconds on a one-minute chart because Yahoo stamps its
in-progress candle with the time of the request, while the poller emitted
anything newer than the last thing it sent. Every poll therefore appended a new
"1m" bar seconds after the previous one, interleaved with the real ones — live
in production, which is still on Yahoo. Timestamps are bucketed on parse, and
the final candle is emitted unclosed so it revises the current minute rather
than entering the aggregator and adding its volume to every higher timeframe
again on each poll. Verified live: eight consecutive bars, all aligned, all
sixty seconds apart.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Producing a real alert to check the wiring showed #47 twice in one Events row:
once as the badge the browser draws from the `number` field, and again at the
start of the message, because the number had been prefixed onto the shared
string.
ntfy carries plain text and has nowhere else to put a number or a timestamp, so
those belong in a push body built for it. The browser already receives `number`
and `at` as fields and formats its own local time, so its message stays clean.
Alert now carries both: `message` for a screen, `push` for a phone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Alerts get a number, assigned server-side and shown in both the push and the
Events list, so a notification on a phone can be matched to a row on a screen
when several fire together. It could not come from the browser: that counter
restarts on reload and differs between tabs. It is persisted next to the
cooldown state, because numbering restarting after a deploy would collide with a
phone's existing notification history — which changed that file from a list to
an object, with the loader still reading the old shape.
Pushes now carry a timestamp in the configured zone rather than the server's.
ALERT_TIMEZONE defaults to America/Chicago; containers run UTC, and a push
reading 02:14 to someone seeing 21:14 costs a translation every time. The
browser already formats its own times locally and is unchanged.
Confluence zones are one line each, ordered by price rather than by proximity,
so the list reads top to bottom the way the chart does and all of them fit on
screen — sixteen zones in 394px, about 25px each, where each previously took a
four-line block. Ordering is a display concern only: the server still returns
them nearest-first, which is what the alert path wants.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Uploading a capture required a token; retrieving one did not. That was a
deliberate capability-URL design with a test asserting it, and the reasoning
held: it lets whoever is debugging fetch a capture without the chart password.
Changed because of what a capture contains. getDisplayMedia returns a picture of
someone's screen, and preferCurrentTab is a preference rather than a constraint,
so a mis-click shares a different window. An unguessable id stops guessing but
not leakage: capability URLs escape through proxy logs, browser history and
pasted links.
Retrieval now uses the dependency the rest of the API uses, which already
accepts the session cookie — so a logged-in browser needs nothing extra, which
was the condition for making this change at all. An agent on the server reads
the capture directory directly; one working over HTTP sends the API token.
Both handlers moved from meta.py to routes.py. meta.py is the deliberately open
router — health, version, login, logout — and a screenshot endpoint did not
belong there. The existing test now asserts 401 without credentials, and a new
one covers the browser path: log in, then retrieve with only the cookie.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The app is past being built and into being changed continually, but the
documents still read as a project being executed: the plan opened by telling its
audience to work top-to-bottom, and §13 listed M0 through M10 as a queue when
all of them shipped days ago.
The milestones stay, marked as shipped. Their "Done when" criteria describe
correct behaviour and several have become tests, so they are worth more as a
specification of working subsystems than they would be archived. If one stops
matching reality, that is a bug in the document.
AGENTS.md now says when to update each, because both decay unless it is part of
finishing the work rather than tidying afterwards. The plan changes when a
decision changes. The log gains an entry when a fix was not obvious — the bar
being "would this have saved someone an hour", not every fix, because a log of
trivia stops being read and takes the useful entries down with it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>