Why the long notes that used to sit in engine/dashboard.ts live here instead. val.town caps a
single file at 80,000 characters and answers a push over that with HTTP 400,
body/content must NOT have more than 80000 characters. On 14 August 2026 dashboard.ts reached
80,910 and the push was refused — so Kim's status page went on serving the OLD deployed copy,
including a sentence a prior audit had already proved false, while the corrected file sat in the
tree unable to land. The cap was rediscovered by that 400, which is the expensive way to find it;
scripts/deploy.mjs now checks every file it is about to push and says the number first.
Four narrative comment blocks were moved here VERBATIM to make the room. Each site keeps a
one-line pointer. Nothing was edited, nothing was deleted, only relocated — the [was: ...]
incidents below are the record of what this page once got wrong, and they are load-bearing.
This is the same move NOTES-v1.4.md records for main.ts (R39 there). A SECOND notes file rather
than more of that one, so two files at their ceiling cannot collide in the same document.
Deployed as an inert file on the val, registered in scripts/deploy.mjs's TYPES map, so it sits
next to the code it explains.
Moved verbatim from engine/dashboard.ts (was lines 162-185). The pointer left at the site names
the three holes; this is the evidence behind each one.
// ESCAPING, PLUS THE TWO THINGS ESCAPING DOES NOT COVER.
//
// Every name and every dog name on this page is typed by a stranger: her booking form is public,
// and the account already holds a self-service "help test" contact with future bookings. So this
// is a hostile input path, not a formatting helper.
//
// 1. BIDI CONTROLS. U+202A-U+202E and U+2066-U+2069 reorder the text AROUND them when the browser
// draws the line. None of them is < > & " or ', so all five escapes miss them entirely, and a
// name of "Kim Refund 24.05$ eeF" renders on Kim's phone reading right-to-left through
// the rest of the cell. Dropped rather than escaped: they have no legitimate use in a dog's
// name, and a visible mojibake would be worse than a silent removal.
// 2. LENGTH. There is no cap anywhere between the CRM and this page, and no word-break rule in
// the stylesheet, so one 10,000-character name with no spaces in it pushes the table wider
// than the 720px wrapper and the phone view is unusable. Truncated with an ellipsis, which
// still shows enough of the name to recognise who it is.
//
// The two are SEPARATE functions on purpose. The length cap belongs only on fields a stranger
// types; putting it on esc() itself cut the card's own generated sentences off at 120 characters,
// including the one that tells Kim this is not a list of people who owe her money. Three tests
// caught that within a minute of it being written, which is what they are for.
// 3. VALUES THAT ARE NOT TEXT. `String(x ?? "")` renders an object as "[object Object]" and a NaN
// as "NaN", and both went onto Kim's page looking like something her CRM had said. Only a
// string or a real number is renderable; anything else is nothing, and the copy around each
// cell already knows how to say "not recorded".
Moved verbatim from engine/dashboard.ts (was lines 378-393). The pointer left at the site keeps
the headline number; this is how it was established.
// PROVEN LIVE 4 Aug 2026, and this replaces a capped scan that was quietly wrong.
//
// The old version walked six pages of ALL 5,695 opportunities and filtered client side. Two
// things were wrong with that. It flagged itself truncated, which suppressed the funnel and
// put an "at least this many" caveat on the tile. And it undercounted: it found 2 new
// customers in the last 7 days when the true figure is 5, because the ones it missed were
// created long ago and only reached the Customer stage this week, so they sat deep in a list
// ordered by creation date.
//
// Three facts make a complete scan affordable, each verified against the live account:
// 1. `pipeline_stage_id` IS honoured server side. Customer holds 1,513 records, not 5,695.
// 2. Pagination is strictly newest-first by createdAt, so page count is knowable up front.
// 3. meta.total is returned on page 1, so the rest can be fetched at once rather than in
// series. Sequentially those 16 pages cost 9.4s; in parallel they cost about one.
// No sort parameter exists (six spellings probed, all 422), which is why the whole stage has
// to be read: someone can reach Customer this week having been created a year ago.
Moved verbatim from engine/dashboard.ts (was lines 530-543). The pointer left at the site states
the rule; this is the incident it came from.
// THREE-VALUED ON PURPOSE, because two values have nowhere to put "we could not tell".
//
// [was: `const practice = run?.testMode !== false`, one boolean, which turned every unknown into
// a positive claim of practice. The mode row at the bottom of this page was gated on `runKnown`
// and so said "Practice or live: could not check" correctly. The paragraph two rows below it was
// gated on `stateKnown`, which is a DIFFERENT read. So: let the run-state or kill-switch read
// fail while the state read succeeds, with the sequence empty, and this page printed "In
// practice mode ... has never sent to or processed one of your actual inquiries" on a live
// system, two lines under a box admitting it could not check. That is the same false sentence
// Kim was shown from 11 to 14 August, reached through a second door. `modeKnown` is now the one
// gate on every sentence that names the mode, and an unknown mode renders as unknown, never as
// practice. Note it also requires an actual boolean: a run blob that reads fine but has never
// been written (`run === null`) used to come out as practice, and would now otherwise come out
// as live, which is the same bug pointing the other way.]
Moved verbatim from engine/dashboard.ts (was lines 981-995). The pointer left at the site keeps
the watchdog constraint inline, because that one binds anyone editing this copy. The reasoning
underneath it is here.
// THE EMPTY CASE, AND WHICH EMPTY IT IS. This paragraph used to be chosen by `realPeople === 0`
// alone, so it said "in practice mode ... has never sent to or processed one of your actual
// inquiries" whenever the sequence happened to be empty — including now, when the system is
// live and has messaged two real people. It was hidden only by the accident that one of them
// is still enrolled. The moment the last person leaves (the remaining one reaches 21 days
// around 1 September, or books) it would have come back, on a live system, in her words, as a
// flat lie. Empty and PRACTICE and empty and LIVE are different facts and now read differently.
//
// AND SO IS EMPTY AND WE-CANNOT-TELL, which is the third branch below and the reason `practice`
// stopped being a bare boolean. This whole block is chosen by `stateKnown`; the mode is a
// different read, and when that read fails the branch above was reached with `practice` sitting
// at its default of true. One failed read of a blob this paragraph does not even name was
// enough to print the practice sentence on a live system. There is no default now: an unknown
// mode gets its own words and they claim nothing. (Those words must not contain the phrase the
// watchdog scans this page for, so they say "in practice or for real" instead.)