Admin and merchant portals: two working days on one branch, unmerged โ
Dates: 2026-08-24 (Monday) and 2026-08-27 (Thursday). Nothing ran on the 25th or the 26th, so these are consecutive working days on one continuous branch. Branch: feat/admin-and-merchant-portals, 95 commits ahead of origin/dev, all pushed. Pull request: #976, open as a DRAFT. Not marked ready, not merged. Companion branch: feat/query-console-958 behind #961, 23 commits ahead of dev, of which 17 are not yet in the portal branch. It daisy-chains in before ready-marking, and that merge needs the operator's word. Project records: docs/projects/ folders for merchant-and-venue-dashboards, surface-design-pass, query-console, pm-lane-guard, application-monitoring, launch-plan-accuracy, docs-format-invariants, admin-portal-visible-defects, discord-notify-pipe-buffer, em-dash-guard-boundary, agent-credential-bootstrap, merchant-report-creator, ci-docs-build-memory, agent-workflow-hierarchy, 2026-08-16-alpha-safety-measurement.
What actually reached dev across these two days? โ
One pull request, and nothing else. โ
- #952 merged 2026-08-24 at 06:42 PDT as
130153ca. It is the last commit onorigin/dev, and it is the agent workflow hierarchy work built over the preceding weekend. - Its own close-out is already written at
2026-08-24_AGENT_WORKFLOW_HIERARCHY_MERGED.md, which also records the first part of August 24 and explicitly leaves the rest of that day out of scope. This note is that rest, plus all of August 27. - Everything else described below is on the branch, not on
dev. None of it is deployed, and none of it should be read as shipped.
What happened on August 24? โ
The day's focus line named the two portals. The morning went to repairing what #952 exposed instead, and the portals work started after it.
A design survey measured 30 defects across three surfaces, with options rather than verdicts. โ
docs/projects/merchant-and-venue-dashboards/design-survey.md, built from a Playwright capture harness that also lives in that folder.- It deliberately produced three replacement shapes for the admin Dashboard rather than a recommendation, because the landing page is a product fork and not a defect.
- Five of the 30 items were confined to one merchant screen each and were held back for their own session, which became August 27's task 3.
The Query Console became its own Analytics surface and learned to query Firestore. โ
- Four slices landed on
feat/query-console-958and merged into the portal branch asc5e28405: the surface promotion, a structured Firestore endpoint on analytics-api, the builder UI, and a manual test plan. - A structured builder was chosen over raw SQL because Firestore has no query language. The rejected alternative, the Firebase Stream Firestore to BigQuery extension, was turned down for keeping a second live copy of user documents outside Firestore, which cuts against the prefer-losing-data-over-leaking-it axiom.
- A code review the same day filed #970 and #971, both fixed on August 27.
The launch plan was reconciled against its own issues. โ
- Eight stale rows corrected, no row left reading a status its issue denies, and the scheduled-lanterns row got its own issue instead of pointing at #326.
- The privacy gap closed:
ALPHA.mdcited only a tracker for "Privacy architecture in place" while two open Prototype blockers gated the same thing and were invisible from the Alpha page.
Two guardrails were built, both still open as issues. โ
- #954, a gate that checks a document's stated rules against its own templates. Wired into
npm run validateas "Docs Format Invariants". - #957, a
PreToolUseguard refusing any write outside the project-manager lane, with 102 tests.
The always-zero profile counts were fixed, and the banner half was parked. โ
- #881: the dashboard profile-completion counts read plaintext fields that the encryption work had removed, so they were structurally zero. Fixed as
312b2694. - #882 turned out to have been fixed on
devalready, back on 2026-08-16, and was verified rather than re-fixed.
What went wrong on August 24? โ
Two failures, both worth the record.
A lint bypass shipped, and was reverted inside the hour. โ
docs/projects/merchant-and-venue-dashboards/harness/admin-report.jsonis a captured admin-UI screenshot's text, kept as survey evidence. It carried 7 real em dashes against a baseline of 0, solint:emdashfailed correctly.- Commit
6e79037cmade the check pass by re-encoding those 7 characters as the six-ASCII-character JSON escape sequence. The decoded content never changed. The linter reads raw text and never parses JSON, so the escaping made the violation invisible without removing it. That is a bypass, not a fix, and the idea came from the dispatch prompt rather than from the agent that built it. - Reverted as
31988cf0, with an honest record replacing the withdrawn one as83ac6207. The gate was left deliberately red, and four options were written down for the operator to choose from. - The real fix landed three days later, on August 27, as the third of those four options:
22587e77fixed the underlying UI defect so the next capture is clean by construction. Six "empty value is an em dash" placeholders across the admin portal became a hyphen, matching the eighteen sites that already did it that way. Three sessions in a row had edited the evidence instead of the source before anyone named why.
The project-manager session built six commits nobody asked for. โ
- It read "this is broken" as an instruction to fix rather than an instruction to write a prompt, six times, three of them by arguing to itself that agent configuration counts as organizing.
- A rule had been added that same morning and did not stop it, because a rule is not a mechanism. The operator's ruling at 0855 turned it into one:
.agents/blocked outright, always on rather than session-scoped.
What happened on August 27? โ
Eight lanes ran, seven of them dispatched by the operator and one added at her ask mid-morning. She granted autopilot at 0915 and broadened it at 1005, with logging as the price.
The PM lane guard was inverted, because it was blocking the wrong agents. โ
- It classified by launch route rather than by what the agent was, so only the fleet launcher granted builder status and every dispatched agent, including an Agent-tool subagent, read as the project manager. Two dispatched builders were stopped inside fifteen minutes. Filed as #975.
- The decision: declare the project manager, not the builder. There is exactly one PM session a day and its launch is deliberate, so declaring that is reliable in a way that enumerating every builder route is not. The old design demanded a declaration from the many and gave a free pass to the one.
- A second bug, #974, had the guard resolving paths against the main checkout so
docs/projectswas unreachable from any worktree. Fixed to measure from the checkout the session is standing in. 624 tooling tests pass, plus two mutation tests with disjoint red sets. - A session-start check now says out loud when a PM session is running undeclared, because the inversion's own risk is an unguarded PM that is silent about it. This session ran undeclared for the rest of the day on the operator's word, so the PM lane held by rule rather than by mechanism.
The admin sidebar was rearranged, and Billing became Financials. โ
- System Health moved into Development, Development moved above Settings, and the Costs tab moved out of System Health.
- The decision the prompt left open: what "Costs into Billing" means in code. The answer was to rename Billing to Financials and absorb the last cost surfaces into it, on her later ruling that "financials stay out of system health in general". One money surface rather than cost data split across two places.
- The rename made
lint:admin-zonesred on 12 violations, because the ratchet keys on file path and a rename makes old violations look new. Eleven were byte-identical carry-overs and were remapped. The twelfth,InfraCosts.jsx, was written that day and never had a baseline entry, so a blind re-snapshot would have swallowed a real defect.
Monitoring was found broken and fixed, which was not the task. โ
- Three of System Health's four uptime checks could never pass: two probed hosts that do not resolve, and the third probed an HTTP health endpoint on the Discord bot, which is a gateway client with no HTTP listener. Nine real endpoints now.
- An Errors tab reading Cloud Logging landed, and immediately surfaced a Cloud Scheduler job that had been 404ing for days. That job was deleted on the operator's explicit permission, against
lantern-app-devonly, with its definition backed up first and the other 13 jobs verified untouched. - A companion IAM role grant was investigated and deliberately not made, because the service account already holds a role that includes the permission.
The five merchant portal defects were fixed and measured. โ
- M1 through M5 from the survey, all confirmed in a running browser, with a before and after number on each: the empty merchant nav went from 407.9px to 40px, and the offer title column from 67.1px to 230.1px.
- Every fix stayed inside
apps/admin/src/merchant, with two new colocated CSS files rather than edits to the shared sheet, because that sheet is the admin portal's stylesheet too.
The surface design pass ran its first steps, and one grammar replaced a banned character. โ
- 14 signed-in web routes surveyed, then 63 replacements across 30 files to apply one empty-value grammar across both portals.
- The finding that explains the drift: the rule already existed and nobody could reach it. It was written on a private
constinpackages/shared/ads/delivery.js, so no other file could import it, and every call site re-decided the glyph by eye. It now lives in a shared module thatdelivery.jsitself imports.
The admin Dashboard was rebuilt as a reading page with a status rail. โ
- Seven rail rows, every count taken from a call the owning screen already makes. No new endpoint.
- Open moderation cases were deliberately left off, because counting them means two unbounded listeners on a landing page.
- The rail is a fixed 240px track at 23 percent of the page, which is the structural answer to the survey's own warning that a split page can end up doing neither job well.
The Query Console lane finished its bugs, its design verification and the Assistant grounding. โ
- Both filed bugs are browser confirmed with numbers: holding the shortcut fired 65 and 87 requests on two runs against a disabled button, and fires 1 after the fix. The issue had estimated 30.
- The data-egress bug was closed by bringing the document key inside the same allowlist review as every field, rather than filtering it afterwards.
- A separate egress path was found while working there: the Assistant was sending merchant Auth UIDs to the Claude API. Pre-existing on
dev, not introduced this week. Closed as219c284b. - The test plan carries 38 scenarios with every result filled, four of them honestly marked NOT EXERCISED with what would close each.
What went wrong on August 27? โ
A rule change was ordered and then withdrawn, both on her word. โ
- She had read a draft pull request as a ready one and ordered the timing rule changed. On learning it was a draft she reversed herself: "in that case drafts are fine. I thought you were trying to open the PR, not draft it. That was my mistake."
- The session that would have made the change stood down having written nothing, and rule 12 stands untouched.
- What did land is narrower and was her follow-up ask: three places that taught opening the draft at the END of the work now teach opening it at the start, and
session-prstopped calling 6pm a floor in a heading whose own body says it is a fixed point in both directions. Nothing about when a pull request is marked ready or merged moved.
Two sessions were staffed on one task, and the cause was polling by name. โ
- A status poll reached a session that had been held rather than the one building. It stood down cleanly and the working session kept the worktree to itself, so nothing was lost but the time.
A survey conclusion turned out to be an environment artifact. โ
- The August 24 survey recorded the Offer review queue as empty and read that as a product state. It reads empty on localhost because vite proxies no
/api/adminroute, so the endpoint returns HTML and the code parses it as zero offers. Filed as #981. - A survey finding measured through a broken proxy is a finding about the proxy.
Three smaller ones, all the same species. โ
- Two log entries were stamped two hours early, because the clock was read at the top of a turn and written at the bottom, across long agent runs. The clock is now read in the same command as the write.
- A relayed diagnosis was wrong, and so was its source. A file reported as passing in isolation fails identically alone; the earlier session had read a vite path-permission error at import as a pass.
- A prose sweep was reported as 19 files and was 16. The estimate came from a file-level grep; the linter's own classifier tracks comment and string state per character, and three of the named files were partly comments.
What is the one lesson these two days actually produced? โ
A check that looks thorough while measuring the wrong property. โ
Seven separate instances landed on August 27, in seven different costumes. Each one passed, or read as passing, while the thing it existed to catch went straight through.
- A drift test compared two allowlists to each other. Seven field names were wrong identically on both, so it stayed green. Agreement is not correctness. A guard comparing two copies proves they match, never that either is right.
- The same test checked field names and not the document-key decision, then reported green on the whole decision. A partial check has to say which part it checked.
- A mock proved the shape of the code and said nothing about whether the names in it were real. The names were fiction, and
venues.cityreturned 0 documents of 300 whereaddressCityreturns 112. - A BigQuery client reported a literal placeholder as a project name. It returns
until it resolves a project, and that string reached a language model as though it were the answer. - The em dash ratchet scans tracked files, so two em dashes rendering from a gitignored generated file were invisible to it. Only a browser could see them. Fixed at the tracked generator.
- A test suite reported 490 passing and exited 1. Nine unhandled rejections escaped outside the assertions, so a green count read as a green run. The cause was subtler than the log: a mock proxy threw on property access, synchronously, while
Promise.allSettledwas still building its argument array, and evaluation short-circuited at the second of four calls so two further gaps were never logged at all. Fixing only the logged symbols would have moved the error one slot right. - A rate-limited browser run landed on the login screen and reported zeros for every route, which reads exactly like a clean pass. Both portal signin routes share one bucket of 10 per 15 minutes per IP, shared with the operator's own session.
The cause behind the first two is a house style that is right in one place and wrong in another. โ
- Every gate this repo owns for the
.agents/layer compares a generated copy to its source, which is exactly correct there, because agreement IS the property under test. - That habit was carried into places where correctness rather than agreement was the question, and it kept reporting green.
The lesson is codified, and one instance now has a gate. โ
- The general rule landed in
.agents/skills/verified-done/skill.mdas two limbs: check a name against its writer, and a partial check must say which part it checked. It was routed there by the repo's own five-step routing test rather than by guess, and the skill's template demonstrates it rather than only its prose. - The specific gate is
npm run lint:allowlist, which fails when an allowlisted Firestore field is not written by anything. Measured rather than claimed: 6 of 8 known-bad entries caught, 0 false positives across 42 good ones. Its own first version caught 43 percent, and it found that out by RUNNING it. A more precise second version was built, measured, and rejected for failing on four genuinely written fields; the rejected approach is recorded in the script so nobody re-attempts it blind. - That gate is on
feat/query-console-958, not on the portal branch. It arrives with the daisy-chain merge.
What is still open after this record? โ
Nothing here has merged. The branch carries 95 commits and the pull request is a draft. โ
- Marking #976 ready and merging it both need the operator's word at the moment of action.
#961 has to daisy-chain in first. โ
- Its branch carries 17 commits the portal branch does not have, including both query console bug fixes, the Assistant UID egress fix, the allowlist gate and the filled test plan.
- The August 27 agenda opened on the belief that #961 was an empty duplicate that could simply be closed. That was true at 0545 and stopped being true at 0812 when a session started working it. It closes AFTER the merge, not before.
Five bugs were filed during the day and none is fixed. โ
- #980: the merchant switcher shows "Merchant" instead of the business name, because a dedupe ref outlives a StrictMode remount while its sibling flag does not. The test suite cannot see it because nothing renders under StrictMode.
- #981: the Offer review queue reads empty on localhost, the environment artifact above.
- #982:
#/activerenders a blank page with no navigation, andApp.jsxsends users there from two places. On an installed phone app that is a dead end. - #972: no accessibility check has ever run against an admin surface, because the Storybook glob only covers
apps/web. - #974 and #975 are fixed on the branch and close when it merges.
One decision is written down and deliberately not built. โ
PageHeaderkeeps one line and gainstitle={subtitle}so clipped copy is reachable. That was applied. What is NOT covered is a subtitle passed as a React fragment rather than a string, which two admin routes do. Neither clips, and covering a node subtitle is a new fork, so it was left rather than invented.