Skip to content

Facilities persona buildout — batch plan

Closing the loose ends and dependencies across the Facilities persona family, then finishing the personas whose surfaces are still stubs. Written mid-programme so a fresh session can pick up batch 3 without re-auditing.

Three of six facilities personas were complete; three had sidebar items that all landed on a pg-soon placeholder.

Persona State at audit
Facilities PM 7 surfaces, complete
Regional Facility Manager 8 surfaces, complete — the build reference
Estate Manager 4 surfaces, complete
VP of Facilities Overview only · 5 stubs
Facility Manager Overview + Facilities + record · 5 stubs
Station Manager Dashboard only · 5 stubs — out of scope, see below

The headline dependency was not a missing screen but an unimplementable spec. Estate Manager’s PRD assigns two decisions to the PM — FR-8 (accepting a location creates a Project) and FR-11 (only PM/VP advance a stage) — and the PM flow had nowhere to perform either. Estate Manager is built read-only on stage because the PM owns it, so the lifecycle was closed at both ends. In 2,119 lines facilities-pm mentioned “Expansion” once and “lease” twice.

  • VP monitors only. FR-11 names the VP alongside the PM, but the VP surface is a monitoring view: advancement stays PM-only, on facilities-pm’s project details page. Enforced structurally — no .pg-actions, no row click, no advance function in the file.
  • Station Manager is out of facilities. The remit stays; the flow will belong to a different department. Do not delete it and do not build its surfaces here. Which department is still open, so products.ts (category: 'facilities') and the README row are deliberately untouched.
  • Facility Manager becomes the cluster-tier origin for facilities-internal issues, now that Station Manager is leaving. A Station Manager in another department files into facilities as a Department Requester, which facilities-pm already models (“requests from clusters and departments”, plus a Department Requester persona in its PRD §12). The chain still closes.
Batch Scope State
1 PM Projects (FR-8 accept, FR-11 advance) + VP Expansion (monitor only) Done — fd9dd34, b249618 on atlas/facilities-projects-pm-vp
2 Station Manager Dropped — different department
3 Facility Manager — issues, requests, equipment, contractors, sops Done — see “Batch 3 outcome”
4 VP — regions, facilities, issues, reports (+ 2 review findings, below) Done — see “Batch 4 outcome”

products/atlas/flows/facilities-manager/index.html, 914 lines. Cluster admin (Ogun). Five navs land on pg-soon: issues, requests, equipment, contractors, sops.

Mirror facilities-regional-manager, not facilities-pm. The Regional Manager is the same shape one tier up (region vs cluster) and already carries every dataset this needs: ISSUES, REQUESTS, EQUIPMENT, CONTRACTORS, SOPS, FACILITIES, SEV, STAT_ISS, STAT_REQ, EQ_COND, ACK_ST, IC_*. facilities-pm is Super Admin and scopes wider than a cluster admin should.

Scope per the flows README: cluster health, issues, requests, equipment, contractors; SOPs read-only; requests forwarded to the PM.

What this flow already has: .pstat, .tbl-card, .pseg, .rec-grid, .card-eyebrow, .empty-wrap, .pager, .filt-select, .filt-search, .td-title, .td-sub, .dlg-overlay, .feed-row, toast(), rememberPage(), _PG_TOP. It has no esc(), dwRow(), openModal(), logRow(), .tl-step-*, .readonly-box or .note-composer — add only what a surface actually needs, copied verbatim from the Regional Manager.

There is no design.md for this flow yet. Write one in the same commit: the canon lint reads a flow’s design.md against its sibling index.html, and CLAUDE.md requires the doc to move with the flow.

Also fix while in this file (from the PR #73 design review)

Section titled “Also fix while in this file (from the PR #73 design review)”

Three pre-existing findings the CI design-review raised against this file. They are unrelated to what #73 changed, but this batch is already rewriting the flow, so fix them here rather than in a separate pass.

  1. :584 — the Facilities directory footer count is dead. The .tbl-foot <span class="lbl"> has no id, but :906 does document.getElementById('fac-foot'), which returns null and is swallowed by an if(f) guard. Filtering hides and shows rows while the count silently never moves. Add id="fac-foot". The Overview table next to it (#att-filter → #att-foot, :537,547,896) is the compliant version to match.
  2. :346 — .rt is a re-skin of .rec-panel. .rt{display:none} .rt.on{display:block} is used at :602,615,626,637,646,655 for .pseg sub-panels. The canonical .rec-panel/.rec-grid CSS is already defined verbatim in this file at :122-124 and never used. Rename every .rt/.rt on to .rec-panel/.rec-panel on and drop the .rt rule.
  3. :386-388 — .facts/.fact-lbl/.fact-val is a re-skin of .td-row. Used at :604-610 for the Facility details field rows. The canonical .td-row/.td-k/.td-ic/.td-v CSS is already in this file at :291-296 and never instantiated. Convert to .td-row rows, which also retires this flow’s share of the deferred .dw-row → .td-row migration.

Both re-skins are the exact pattern the canon calls out: a private class doing a canonical component’s job while the real CSS sits unused in the same file.

Batch 3 outcome — decisions that carry into batch 4

Section titled “Batch 3 outcome — decisions that carry into batch 4”

All three PR #73 findings fixed, five surfaces plus #pg-issue / #pg-request / #pg-standard details pages built, design.md written, pg-soon gone. Lint 0 on both files (explicit paths, 2 files confirmed). Four judgment calls worth reusing:

  • .dw-row was NOT imported; .td-row was used instead. The reference’s diRow() emits .dw-row, but finding 3 retires this flow’s share of that migration — importing it would have re-added what the batch just removed. The flow now instantiates zero .dw-row. .dw-status (a 3-line badge-group wrapper) was copied verbatim, because the triage explicitly rules that one fleet-wide: “one flow must not diverge alone”.
  • Table wrapping is invisible to an overflow check. With table-layout:fixed a too-narrow column wraps instead of overflowing, so scrollWidth - clientWidth reads 0 while cells are two lines high — batch 1’s measuring advice misses it. Measure instead by Range.getClientRects().length > 1 per text node. Five tables wrapped; each got a <colgroup> taking width from its narrow badge/numeric columns, plus a .tbl td{white-space:nowrap} + ellipsis guard. Batch 4’s finding 1 is the same defect — use the same fix and the same measurement.
  • The reference’s case-note <textarea> is unstyled (no .fld, no rule of its own), so it renders as a raw browser field in the wrong font. Copying .note-composer verbatim would have shipped that; the textarea is wrapped in the canon .fld block here.
  • Derived, never stored. Site health, its attention reason and every Overview KPI are computed from ISSUES / EQUIPMENT / REQUESTS. The old static markup had already drifted (Overview claimed 9 open issues, the directory column summed to 8; Ijoko passed “no high-severity issues” while holding a High one). Batch 4’s roll-ups should derive from the same data rather than restate it.

One known cosmetic consequence: at first load both needs-attention sites derive the same reason (“High-severity issue”), so the reason filter offers one real option until a state change differentiates them. That is the honest output of the data, not a hand-labelled distinction.

The bar moved mid-batch — batch 4 starts here

Section titled “The bar moved mid-batch — batch 4 starts here”

uat advanced 26 commits during batch 3 and raised the acceptance bar. Merged cleanly (only 7fc61ad, the fleet-wide login standardisation, touched this flow, and it is confined to the login block). What batch 4 must now satisfy:

  • CLAUDE.md ritual is six steps, not five. New step 6: load the flow and run site/scripts/flow-probe.js on every view, not just the ones you touched — flowViews().map(v => [v, flowProbe({view: v})]), all pass:true. It catches what no file-reader can: wrapped KPI labels, clipped nowrap headers, rows whose only target is a chevron, ragged text edges. Run it at 1440 — at a default ~730px pane it reports failures that are just the narrow viewport.
  • web-dead-pager is a new staged rule and the hardcoded <button disabled>‹ Prev</button> pager is now a violation over a JS-rendered tbody. facilities-manager had 7 grandfathered; batch 3’s new surfaces pushed it above baseline and turned them into 5 errors. All 12 removed — no surface paginates, so the .tbl-foot keeps only its derived count. facilities-vp still carries its own grandfathered count, so batch 4 will hit this the moment it adds a table.
  • The probe requires EVERY table row to be clickable (unconditional, once a table has rows). Purely informational tables fail. Equipment rows now open the site holding the item on its Equipment tab; a contractor is too thin for a details page, so its row opens a compact centred .dlg. Batch 4’s read-only roll-ups need a target per row — decide it up front rather than retrofitting.
  • Ratchet with an explicit path, never --all. --update-baseline only rewrites entries for files it actually linted, so lint-canon.mjs <file> --update-baseline graduated this flow to zero-tolerance and left the other 24 backlog entries alone. --all lints zero files on Windows and reports success, which here would have looked like a clean wipe.

products/atlas/flows/facilities-vp/index.html. Overview and Expansion are built; regions, facilities, issues and reports still land on pg-soon. Read-only executive roll-ups. Depends on batch 3 so the issue/request model is settled first.

Follow this flow’s own idioms, not facilities-pm’s: no esc() (trusted literals), a function-local set(), .r for right-aligned cells, and the empty state as a colspan row inside the tbody.

Also fix while in this file (from the PR #73 design review)

Section titled “Also fix while in this file (from the PR #73 design review)”
  1. :181 + row template :850 — the “Reason” column wraps to two lines in every region’s Sites needing attention table (.tbl td has no nowrap/ellipsis, and no <colgroup> widens it). Confirmed by the reviewer across three regions. Canon is one line per cell (HARD). Add a <colgroup> taking width from Severity and Open-since, plus nowrap/ellipsis.
  2. :852 — the “Every site is healthy” empty state omits .empty-ic. Every other .empty-wrap in this same file has the icon, as does Polaris’s emptyState() helper. Add the circle with an outline glyph before .empty-t.

Batch 4 outcome — the programme is complete

Section titled “Batch 4 outcome — the programme is complete”

Both findings fixed, four surfaces built (#pg-regions, #pg-facilities, #pg-issues, #pg-reports), the dormant #pg-region switched on, design.md written, the placeholder page gone. Lint 0 on both files; flow-probe passes on all six views at 1440; 442 cells measured with no wrap, no truncation, no overflow.

The monitor-only decision met the new probe head-on, and survived. The probe requires every table row to be clickable once a table has rows; batch 1 had enforced monitor-only as “no .pg-actions, no row click, no advance function”. Overview and Expansion both failed. Resolved by separating navigation from authority:

  • Every row now opens a read-only view. A site, a cluster, an issue and a project each open a centred .dlg whose footer holds Close plus a line naming who does act (“Stages are advanced by the Facilities PM”). No dialog has a primary button, and no advance/assign/approve/resolve function exists in the file — verified at runtime, not asserted. That is a stronger guarantee than the absent row click was, because it survives someone adding a row click later.
  • #pg-region was built, verified and deliberately unreachable, waiting on “the PM’s region-detail feature”. Batch 4’s Regions roll-up is that feature, so the row click was enabled rather than re-built.

Deriving from one SITES list found a real bug the reviewer had not. All 38 sites now live in one array and every roll-up computes from it. The hand-written per-cluster figures had drifted: South-East’s two clusters both read 88% when their own site counts give 75% and 100% — the region average, copied down a level. Same class as batch 3’s Overview/directory mismatch, so treat a stored count in these flows as suspect by default.

Cross-flow data was contradictory and is now aligned. The estate placed “Sango Retrofit Station” in the Ikeja cluster; facilities-manager has Sango as a Charge-Swap Station in Ogun, with Ijoko as Ogun’s second needs-attention site. Batch 4 adopts batch 3’s model, and ISS-301/ISS-302 keep their ids and titles. The dead REGIONS.clusters/REGIONS.sites arrays that held the contradiction were removed rather than left as stale data beside the derived version.

Still open, deliberately not fixed here: the VP names South-West’s clusters Ikeja / Ogun / Lekki while facilities-regional-manager names them Ogun / Lagos-Mainland / Lagos-Island. The individual site names match; only the cluster labels diverge. That is a fleet-wide reconciliation across three flows, not a batch-4 edit. Also note issue ages are per-flow snapshots (the VP shows Agbara at 5h, facilities-manager at 1h) — ids, titles and severities are the load-bearing part and those match.

The CI design-review on #73 returned 1 critical, 9 major, 6 minor across 7 files against a diff of 10 insertions and 15 deletions — a display-name rename plus a dead-shim removal. None of the findings were introduced by that diff. The reviewer audits whole files, so touching one line in seven files pulled every pre-existing finding in all seven into the report. This is the Layer 1 diff-scope trap reappearing at Layer 2.

Most findings are legitimate as canon observations, so they were routed to where the files are already being rewritten rather than bolted onto a rename:

Finding Routed to
facilities-manager ×3 (dead fac-foot, .rt, .facts) Batch 3 — fixed
facilities-vp ×2 (wrapping Reason column, missing .empty-ic) Batch 4 — fixed
facilities-station-manager ×5 Whoever moves that flow to its new department
starter-template — missing .tbl-foot on Batteries, #1a1a1a literals Standalone fix; #1a1a1a verified still present at :374,376
facilities-location-scout .dw-status, facilities-regional-manager .pg-actions status text, facilities-pm table <colgroup> Fleet-wide, own PR — .dw-status is used by every details page in those flows, so one flow must not diverge alone

It asks for starter-template’s sign-in to return to a peach var(--login-bg) ground with a .login-left/.login-right split. That is not drift. Two deliberate commits put it where it is:

  • fed28ea “revert login to the composed v1 scene, on a grey ground” — moved the ground from peach to --grey1 and explicitly retuned everything that had been tuned warm: texture dots #E2D3C2 → #DEDEE4, depth pool --amber-dim → --brand-dim, and all four shadow tints to neutral.
  • c8aa34e then added dark-theme grounding for that same scene.

The file matches that intent exactly (#scr-login{background:var(--grey1)}, .login-card{box-shadow:none}). Applying the fix would revert both commits. The rule the reviewer cites describes the split login that fed28ea intentionally replaced, so the canon reference is what is stale — not the file.

Traps hit in batch 1 — worth not repeating

Section titled “Traps hit in batch 1 — worth not repeating”

Lint passed on all of these; they were caught by reading the canon and measuring.

  • Never invent a class. I used .cell-main/.cell-sub; the real ones here are .td-title/.td-sub. Grep the target file before writing markup.
  • toast(msg,type) variants are default|error|gray|success|warning. 'warn' renders unstyled and silently.
  • No buttons inside table cells. Not a pattern any of the six facilities flows uses — all per-row actions are <tr onclick> into a details view, with create/edit/confirm in a centred .dlg. Two buttons in a cell also overflowed it.
  • Measure table width, do not eyeball it. Compare .tscroll scrollWidth - clientWidth against an existing reviewed table in the same flow (facilities-pm’s issues table sits at 8px). A 6th column collided “Target facility type” into “Stage” and clipped “Target handover” at 1440. Cut redundant columns instead of shrinking type: drop anything the row’s .td-sub already carries, and keep it searchable.
  • Shared components must not diverge per flow. .tl-step-* and the project dataset were copied verbatim so one project reads identically to all three personas. Same reason the login illustration’s warm neutrals were left alone — they are byte-identical across all six flows, so they are canon, not drift.

The file:// preview pane serves a stale snapshot and will not re-execute JS — new tabs and new filenames do not help. Serve over HTTP instead:

Terminal window
node -e "const http=require('http'),fs=require('fs'),path=require('path');http.createServer((q,s)=>{const p=path.join(process.cwd(),decodeURIComponent(q.url.split('?')[0]));fs.readFile(p,(e,d)=>{if(e){s.writeHead(404);return s.end('nf');}s.writeHead(200,{'Content-Type':p.endsWith('.html')?'text/html':'text/plain','Cache-Control':'no-store'});s.end(d);});}).listen(8099,()=>console.log('8099'))"

Then load http://localhost:8099/products/atlas/flows/<flow>/index.html, resize the viewport to 1440, and drive the surface. Also note the pane strips ?query, so ?page= deep links cannot be tested through it — call navTo() directly.

  • PR #73 (atlas/facilities-pm-parity) — design-review is red, triaged above. Its one blocking finding is a false positive; the rest are pre-existing and routed into batches 3 and 4. The rename itself is unaffected.
  • Station Manager’s destination department, which gates its products.ts category and README row.
  • .dw-row → canonical .td-row migration across the facilities flows. Deferred deliberately as a coordinated fleet-wide sweep, not a per-flow change. As of the follow-on below, facilities-pm instantiates .td-row in new markup while its existing dwRow call sites still render .dw-row, so the two helpers sit side by side there until the sweep runs.

Follow-on: the SOPs & Standards surface is removed (29 Jul 2026)

Section titled “Follow-on: the SOPs & Standards surface is removed (29 Jul 2026)”

The SOPs & Standards sidebar item and its library page are gone from all four personas that carried them: facilities-pm (which also loses the authoring dialog), facilities-manager, facilities-regional-manager and facilities-station-manager. The Estate Manager’s Requirement SOPs tab was deliberately left alone — it reads siting criteria, not operating standards, and its case reports are filed against them (FR-6).

Scope was nav item plus its page, not every SOP reference. Standards still appear where a record puts them in context: a site record’s SOPs tab, the requirement SOP a location is surveyed against, the SOP an issue names. What is gone is the library and, on the PM, the ability to author, version and publish.

Judgment calls worth keeping:

  • The Standards sidebar group label went with the item. An .sb-group heading over nothing is a collapsed-rail artifact, not a nav section.
  • #pg-standard stayed. The site record’s SOPs rows are its way in, and flow-probe requires every table row to be clickable, so deleting the details page would have forced those rows to become dead cells. It is a details page reached from a row now, which is how #pg-issue and #pg-request already work.
  • The back trail says Facilities, not Facility record. rememberPage() only tracks top-level pages, so back from a standard lands on the directory, exactly as it does for an issue opened from a record. The crumb was set to match the button rather than the other way round: two controls next to each other must not disagree.
  • facilities-regional-manager’s record SOPs panel had to be rebuilt to derive. It was three hardcoded rows with no onclick, harmless while a library existed and an orphaned #pg-standard once it did not. It now derives from SOPS, filters to the standards the site’s cluster is actually asked to acknowledge, and shows that cluster’s state.
  • The station manager’s “SOPs to read” KPI stayed. Acknowledging standards is still that role’s job, and the card lands on the same Core-tier placeholder its three siblings do. Flagged rather than removed, because dropping it would leave three cards in a four-track grid.

The PRD and the prototype now disagree on purpose. Authoring standards is still assigned to the PM in facilities-pm/PRD.md. Removing a surface does not retire a requirement, so the PRD was left as the record of it and design.md says so explicitly.

Pre-existing probe failures, surfaced but not fixed (later fixed — see the follow-on below). When the tab-removal landed, flow-probe still failed on facilities-regional-manager’s equipment and contractors views and the station manager’s dashboard, all with “every row clickable” — those renderers emitted <tr> with no onclick at HEAD, before this change. They were called out as a feature decision rather than part of removing a tab, then done in the next follow-on.

Follow-on: the PM dashboard stat row, and derived badges (29 Jul 2026)

Section titled “Follow-on: the PM dashboard stat row, and derived badges (29 Jul 2026)”

A tier-by-tier comparison of facilities-pm against facilities-vp turned up four contradictions, all of the same kind: a count typed into markup, next to a surface that computes the same thing correctly. Batches 3 and 4 derived everything they built; the two older surfaces were never swept.

Found Fixed
facilities-vp sidebar Issues & SLAs badge read 7; ISSUES holds 8, and the KPI and table foot both said 8 renderSbCounts() derives all three VP badges
facilities-pm Overview Open Issues read 17 against a 12-issue array card dropped from the dashboard; the sidebar badge now derives to unresolved-issue count
facilities-pm sidebar Facility Issues read 17 derived, so removing the KPI did not just move the wrong number one panel left
facilities-pm Overview restated counts the sidebar badges already carried Open Issues and Equipment replaced by Resolution velocity and Projects

renderOverviewKpis() is called by each surface renderer, so approving a resolution, deciding a request or accepting a location moves the dashboard without a reload — verified by driving each path, not by reading the code. projStats() is the one definition of the project counts, read by both the Projects stat row and the dashboard card.

Resolution velocity is derived from timestamps that were already there. The issue logs carry "x ago" strings and nothing else, so agoDays() parses those rather than adding a second and disagreeing clock. Filed to resolution approved across the three issues whose logs carry an approval gives a median of 3.0d; the reopened one counts, because its first resolution still took the time it took. The sub-line states the sample size on purpose.

Still open, and not fixable by editing a KPI. facilities-pm’s facilities array holds 12 sites while every headline on the flow says 38, which is why Facility Health stays typed at 92% · 35 of 38 and why it cannot be reconciled with the VP’s derived 82% · 31 of 38. Same root cause as the two flows disagreeing about which sites exist at all: of the PM’s 12 facilities only 5 appear in the VP’s 38-site estate with matching name, type and region, and the VP’s type histogram carries no Retrofit Station at all (Charge-Swap 21 · Workshop 6 · BAU 5 · Experience Centre 3 · Office 3) though the PM has two and the product ships a Retrofit Station Manager persona. Fixing that means deciding the canonical estate once and rebuilding both datasets from it.

Follow-on: reading the case report in Review location (29 Jul 2026)

Section titled “Follow-on: reading the case report in Review location (29 Jul 2026)”

FR-8 asks the PM to accept or send back a submitted location, and the whole basis for that call is the case report the Estate Manager filed against the requirement SOP (FR-6). The Review location .dlg named the report (REP-112 filed 21 Jul 2026 by Yusuf Idris) and stopped there, so the decision was being asked for without its evidence. A Read case report button now unfolds the report in place: its fields as .td-rows, the case notes in a .readonly-box, and the photo/video evidence as a captioned .media-grid.

Three judgment calls worth keeping:

  • Unfold in place, do not stack a second .dlg. The overlay z-index and closeModal’s “is any overlay still open” guard both allow stacking, but the flow’s Escape handler closes every open overlay at once, so a stacked report would take the half-typed Note to the Estate Manager with it. Unfolding also keeps the report and the note in one scroll, which matters because the note is written about the report. The panel folds away on every open, so the dialog still reads as a decision.
  • caseReport mirrors the Estate Manager’s REPORTS shape (title · status · notes · media) rather than inventing a PM-side model, on the same “one record reads identically at every tier” rule batches 3 and 4 followed.
  • Captions are held to one line. A 72px .media-tile fits about 13 characters and a wrapped caption covers half the thumbnail it labels. .media-cap got the same nowrap + ellipsis guard the tables use, and two names were shortened so nothing truncates at all. The reference flow wraps here; that is a defect, not canon.

Measurement trap, again, and the opposite way round from batch 4. Counting Range.getClientRects() per text node reported two lines for a caption that renders on one: under overflow:hidden the rects fragment at the clip boundary. And scrollWidth - clientWidth rounds to an integer, so it reported 0 overflow for a caption overflowing by 0.04px, which is still enough to trigger the ellipsis and eat two characters. What actually held: the element’s rendered height for wrapping (18px is one line, 36px is two), and the text node’s Range width against the padded content-box width for truncation.

Not ratcheted into a rule. A wrapped .media-cap is layout-only, so it would be a flow-probe check, and flow-probe has no baseline mechanism — the reference flow and every flow that copied it would start failing at once. It needs a staged canon-rules.mjs rule plus a baseline refresh, the way web-dead-pager was introduced, which is a fleet change and not this one’s to make.

Still open on this thread. The PM’s queue references REP-112 and REP-115, and the Estate Manager flow’s own REPORTS array stops at REP-108 and holds no report for Ifo corridor or the Agbara industrial belt. The two flows agree on the report’s shape but not yet on its existence, which is the same class of cross-flow drift batch 4 fixed for issue ids. Fixing it means editing the Estate Manager’s AREAS and REPORTS, so it is left out of this change.

Follow-on: the three “every row clickable” failures, fixed (30 Jul 2026)

Section titled “Follow-on: the three “every row clickable” failures, fixed (30 Jul 2026)”

The tab-removal follow-on left three probe failures standing, called out as feature decisions. This closes them, each with the target the tier already implied — not a uniform retrofit.

View Row now opens
facilities-regional-manager · equipment the site holding the item, on its Equipment tab — openRecordByName(site,'rt-equipment'), extending the record opener to take a tab, mirroring the cluster tier exactly
facilities-regional-manager · contractors a compact read-only .dlg (Close only) whose Clusters field links each covered cluster into its record via openCluster — the same shape the cluster tier’s contractor dialog uses
facilities-station-manager · dashboard a read-only issue peek .dlg (Close only); the station files and reads issues, it does not resolve them, so the dialog has no action

Three design points worth keeping:

  • The target followed each tier’s authority, not a template. Equipment is a facet of a site, so its row routes into the site record — there is no standalone equipment record at any tier. A contractor and a station issue are compact records with nowhere richer to go, so they open centred dialogs. Every new dialog is read-only: Close alone, no primary button, because neither the region tier (over a contractor) nor the station tier (over an issue it did not action) has authority to act there. Asserted at runtime, not inferred.
  • Data joins were checked before routing. All 12 equipment sites in the region flow resolve to a FACILITIES entry, so no equipment row lands on a missing record. The contractor clusters string (“All 3 clusters”, “Lagos-Mainland, Lagos-Island”) is parsed against CLUSTERS by name, with “all” expanding to every cluster.
  • The new dialogs were wired into each flow’s Escape handler. flow-probe’s “every dialog closes on Escape” check opens every .dlg-overlay and dispatches Escape; a dialog the handler does not know about fails it. Both handlers now close the new dialog alongside the role switcher.

The station-manager’s dashboard issue table was three static <tr> with a dead pager; it is now rendered from a small STN_ISSUES array so each row carries its own target and the count derives. Its ids reuse ISS-302 / ISS-304 from the cluster and region tiers so one issue reads identically everywhere; ISS-311 is a station-local follow-on. flow-probe now passes every view on all five facilities flows at 1440, and both edited flows lint 0 with no console errors. The station-manager has no design.md (it is “Core · full build to follow”), so only the regional flow’s doc was updated.

Layer-2 canon review, one finding fixed. An independent design review of the three changed flows (Layer 1 was already 0 errors) turned up a single re-skin: the station peek dialog first rendered its Severity / Status / Assignee / Age fields with .hc-row — the dashboard’s health-check row — because that was the only key/value-ish atom I had reached for. The flow already defines the canonical .td-row/.td-k/.td-ic/ .td-v (Polaris tdRow), so the fix was to add the tdRow helper and its four icon constants and render the fields with the real atom; .hc-row now appears only on the Pass/Fail health card where it belongs. The regional contractor dialog (.dw-row via diRow, matching that flow’s own detail rows) and facilities-pm were clean.