A shopper's order page says "placed 2026-09-18 03:49:10 UTC" and the admin's order list says "2026-09-18 03:49:10" with no zone at all — the same instant, written two ways, neither of which a browser or a screen reader can turn into the reader's own time.
What the pages do today.datetime('now') stores UTC without a marker, and every page escapes that string straight into the markup: src/pages/checkout.ts:226 and :268 and src/pages/find.ts:18 append " UTC" by hand, src/pages/admin.ts:88, :172 and :431 do not, and the gift-card and stock ledgers print entry.at raw. There is no <time> element anywhere in src/pages/, so "3 minutes ago" is impossible without JavaScript and the app ships none.
Done looks like
[ ] src/when.ts is one small module: a SQLite timestamp in, and out comes both an ISO-8601 instant with its Z and the words to show — "just now", "6 minutes ago", "yesterday at 14:02 UTC", and an absolute "18 Sep 2026, 03:49 UTC" for anything older than a week. Pure functions with the clock passed in, so the tests do not sleep.
[ ] Every date in src/pages/ renders as <time datetime="2026-09-18T03:49:10Z">…</time>, escaped as everything else is, with the title attribute carrying the absolute form when the words are relative. No page appends " UTC" by hand any more.
[ ] The words are chosen server-side and the markup does not shift when they change length; no JavaScript is added and the page keeps script-src 'none'.
[ ] The terminal keeps its columns aligned and uses the same module for its short form, so a row reads the same in both places.
[ ] Tests that fail before the change: the order page, the receipt, the find-an-order list, the admin order list and the admin gift-card ledger each carry a <time datetime> whose value parses as a valid instant equal to the row; a stamp one minute old reads "just now" or "1 minute ago" and one a year old reads its absolute form; a malformed stamp in the database renders as itself, escaped, and does not throw.
Files I expect to change.src/when.ts (new), src/when.test.ts (new), src/pages/checkout.ts, src/pages/find.ts, src/pages/admin.ts, src/pages/giftcard.ts, src/tty.ts, src/server.test.ts.
Risks. SQLite's datetime('now') is UTC without a Z; parsing it with new Date(string) is what makes a timestamp jump by the host's offset, so the module appends the marker itself and a test pins that. This touches most of src/pages/, which is also where #35 rewrites style attributes into classes — that is why this waits for #35 rather than racing it. Relative words are a trap for an order placed seconds ago and rendered from a cache; nothing here is cached today, and it should stay no-store when it is.
Suggested crew.mason, with juniper reading the order page and the admin list on a phone.
A shopper's order page says "placed 2026-09-18 03:49:10 UTC" and the admin's order list says "2026-09-18 03:49:10" with no zone at all — the same instant, written two ways, neither of which a browser or a screen reader can turn into the reader's own time.
What the pages do today.
datetime('now')stores UTC without a marker, and every page escapes that string straight into the markup:src/pages/checkout.ts:226and:268andsrc/pages/find.ts:18append " UTC" by hand,src/pages/admin.ts:88,:172and:431do not, and the gift-card and stock ledgers printentry.atraw. There is no<time>element anywhere insrc/pages/, so "3 minutes ago" is impossible without JavaScript and the app ships none.Done looks like
src/when.tsis one small module: a SQLite timestamp in, and out comes both an ISO-8601 instant with itsZand the words to show — "just now", "6 minutes ago", "yesterday at 14:02 UTC", and an absolute "18 Sep 2026, 03:49 UTC" for anything older than a week. Pure functions with the clock passed in, so the tests do not sleep.src/pages/renders as<time datetime="2026-09-18T03:49:10Z">…</time>, escaped as everything else is, with thetitleattribute carrying the absolute form when the words are relative. No page appends " UTC" by hand any more.script-src 'none'.<time datetime>whose value parses as a valid instant equal to the row; a stamp one minute old reads "just now" or "1 minute ago" and one a year old reads its absolute form; a malformed stamp in the database renders as itself, escaped, and does not throw.Files I expect to change.
src/when.ts(new),src/when.test.ts(new),src/pages/checkout.ts,src/pages/find.ts,src/pages/admin.ts,src/pages/giftcard.ts,src/tty.ts,src/server.test.ts.Risks. SQLite's
datetime('now')is UTC without aZ; parsing it withnew Date(string)is what makes a timestamp jump by the host's offset, so the module appends the marker itself and a test pins that. This touches most ofsrc/pages/, which is also where #35 rewrites style attributes into classes — that is why this waits for #35 rather than racing it. Relative words are a trap for an order placed seconds ago and rendered from a cache; nothing here is cached today, and it should stayno-storewhen it is.Suggested crew. mason, with juniper reading the order page and the admin list on a phone.