Live in the platform
Nancy's OR board — shipped 2026-09-30
This one is real. The OR-scoped manager lands on a board built from her own data, in the fuller layout — OR strip, the day's slate, the call as a table by role, then OR week, block-time usage and the open-shift pool. Nobody else's dashboard changed.
- The designs as images — start here — one page, full-width shots of the two live pages, no prose.
- Block weeks — OR and Clinic (live page) — 58 OR rules and 127 clinic rules grouped per provider, "every week" instead of repeating 1-5, AM + PM folded into one full day, released dates, and who may change a block. On the rail for anyone who can see the Blocks tab.
- Screenshot of her live board — 7 OR cases today, 38 this week, 170 open blocks in the next 90 days, the day's slate by site.
Verified in her own session: the case count on screen equals what
/api/v1/or-schedule/board returns, and the controls — a manager without the OR scope,
and the finance officer — render exactly as before. Her hero line now reads OR Supervisor
rather than the category word "Manager".
Live design previews
Nancy's OR board — previews
Standalone pages built from her live data. These are previews only — the block-week surfaces below are not in the app yet; the board above is.
- v2 — Nancy's board (current) — call schedule as a table, out-today and recent call-outs, OR week, block time, open shift report, and a hand-off row to the block-week page
- Block weeks — OR and Clinic (new page) — the block-week rules grouped per faculty with "Every week" instead of repeating 1-5, released dates with full day / AM / PM, who can change them and who gets emailed. Lifted off the homepage 2026-09-30 because those destinations already sit on the menu.
- v1 — dark version (earlier)
1 · The plan
title: Role-differentiated dashboards and access points (Unified Platform)
type: feat
status: active
date: 2026-09-30
origin: owner brief 2026-09-30 (chat) — "think about what each user should really have access to and what should be on their landing dashboard page"
Role-Differentiated Dashboards and Access Points
Overview
The platform is close to done. What remains is not features but fit: each person should land on a
dashboard that matches their job, and hold exactly the rights that job needs — no more.
Today the platform has five dashboard branches and three nav allow-lists, and the fit is wrong in
three ways: ten leadership accounts across four different jobs (OR, finance, staff scheduling,
faculty scheduling) share one dashboard; the big boss cannot see the money; and residents land on
a page with no evaluations on it. This plan assigns each role its own landing surface and closes the
access gaps that the audit below verifies.
Implementation happens on the live tree, not the git mirror. See Unit 0.
Problem Frame
Verified against the live platform on 2026-09-30 (see Current State). The dashboard chooses its layout
from role booleans in app/src/pages/Dashboard.tsx:
| Branch | Who lands here | What they see | ||
|---|---|---|---|---|
isAdmin | sfrasier | Hero band + 4 admin metrics + recent activity + admin focus + program links | ||
isFinance | Crystal | FinanceOfficerDashboard (KPI tape, cost centres, funds, transactions, exceptions, reports) | ||
| `isManager \ | \ | isStaff` | 10 managers + 29 staff | Hero band + self tape + CallWeek / OutToday / PendingDecisions / CoverageAlerts / StaffAbsence / OperationsActivity / RecentActivity |
isFaculty | 24 attendings | Faculty schedule + commitments + recent activity + faculty tiles | ||
isResident | 16 residents | GME fund + upcoming shifts + recent activity + resident tiles | ||
| fallback | Fellow, medical students, 6 contact-less rows | recent activity + program links |
So Nancy Solano (manager of the OR) and Winnie Wint (staff schedules) and ~29 staff land on the same
page, and nothing on that page is about the OR or about a staff roster.
Status — landed live 2026-09-30
Deployed to /var/www/unified and verified against the running app (not by description).
What a person now gets, rendered from a real session token — not the admin view-as preview:
| Person | Sidebar |
|---|---|
Nancy Solano (or) | Dashboard · View · Block Schedule · Edit · Pool · Activity History · Coverage Center · Reimbursement — and /schedule shows her View · Blocks · Edit · Pool |
Kelly Bottger (exec) | Dashboard · View · Edit · Pool · Activity History · Coverage Center · Reimbursement · Review Claims · Reporting · Reimbursement Profiles — and she LANDS on the finance board ("Kelly's finance desk") |
| Crystal Santos (unchanged) | Finance Dashboard · All Schedules · Call Schedule · Reimbursement · Review Claims · Reporting · Reimbursement Profiles |
| Winnie Wint (control, no scope) | Dashboard · View · Edit · Pool · Activity History · Coverage Center · Reimbursement — byte-identical to before |
Evidence: /api/v1/me returns managerScope for all four; SPA + server tsc clean; 11/11
roles.test.ts (incl. the negative and ordering cases); 9/9 dashboard/schedule smoke tests; 7 files
md5-verified after upload. Revert: _bak-rolescope-20260930-192542 (code),
_bak-dist-rolescope-20260930-192809 (built frontend).
Two things this turned up:
her app_access row has no ez_id, so the app renders "Email Not Linked" and no sidebar. This is
pre-existing and unrelated to this change (touched nothing about identity resolution; her scope
is null), but a manager locked out of the platform is worth fixing. Needs her EZ ID.
against the admin's own, so previewing Nancy yields managerScope = null and renders the generic
manager nav. Use a real token to review a scope-dependent surface.
Still open: Unit 3 (faculty eval management — gated on the /eval/:token submission proof),
Unit 5 (the differentiated dashboard *boards*: Nancy's OR board is still the operations dashboard),
Units 6–9. Access points are done; the landing *pages* are next.
Requirements Trace
Scope Boundaries
explicit-column + allow-list pattern already in the codebase is the pattern; extend it.
the resident evaluations card.
/finance Infor portal stays off-menu (its own header documents this as deliberate).Context & Research
Source of truth (verified 2026-09-30 — this one bit us)
| Tree | What it is |
|---|---|
/var/www/unified (VPS) | LIVE. Bind-mounted to /app in hermes-webui-gsga-unified-platform-1. This is where code must land. |
/workspace/projects/unified | Git mirror in the agent workspace. Lags live. |
md5sum differed on every file this plan touches:
DRIFT src/components/Layout.tsx local=1ade75d1… live=df507c31… DRIFT src/pages/Dashboard.tsx local=2f36d7f4… live=0f475d98… DRIFT src/lib/roles.ts local=98e6a3dc… live=30b1bacf… DRIFT src/pages/Admin.tsx local=1a4f3fa7… live=5755295c…
Editing the mirror and building it would ship a regression of the finance-officer work. **Unit 0 fixes
this before anything else.**
Relevant code and patterns
app/src/components/Layout.tsx — MANAGER_SECTIONS (l.133), FULL_SECTIONS (l.172), FINANCE_SECTIONS (l.261); selection order at l.295: isFinanceOfficerOnly ? FINANCE : isManagerOnly ? MANAGER : FULL.
Every item in an allow-list needs always: true or sectionHasItems() drops the whole section.
app/src/lib/roles.ts — canonicalRole, isManagerOnly, isFinanceOfficerOnly, canFileForOthers, isCallOutReporter, canRequestSwap, canViewProgress.
app/src/pages/Dashboard.tsx — the branch logic above. Bug: isManager (l.115) is category === 'Manager' || a.canEditSchedule — raw category, not canonicalRole(). Same for
isFaculty / isResident / isStaff. Fix while splitting the branch.
app/src/pages/Admin.tsx — FINANCE_TABS = ['review','reports','profiles']; allowedTabs at l.121.server/routes/admin.ts — requireFinance (l.120), requireFinanceOrReviewer (l.133), requireAdminOrEditorOrFinance (l.146).
server/db/migrations/042_finance_officer.sql — the template for adding a designation column.app/src/components/dashboard/FinanceOfficerDashboard.tsx (456 l.) — the model for a rich role dashboard.Current state (live DB, 2026-09-30)
unified.app_access: 93 rows, 12 flag columns (incl. is_manager, is_finance_officer).
unified.role_defaults: 8 categories. Precedence is row.col ?? defaults.col — a NULL in the row
inherits the category default, which is how the faculty eval gap below is hiding.
| Category | n | admin | mgr | fin | eff. edit | eff. evals | eff. review | eff. roster | eff. scl |
|---|---|---|---|---|---|---|---|---|---|
| Admin | 1 | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 |
| Manager | 11 | 0 | 10 | 1 | 10 | 0 | 0 | 11 | 10 |
| Staff | 29 | 0 | 0 | 0 | 0 | 0 | 0 | 26 | 29 |
| Faculty | 24 | 0 | 0 | 0 | 0 | 24 | 2 | 4 | 3 |
| Resident | 16 | 0 | 0 | 0 | 0 | 0 | 0 | 16 | 13 |
| Fellow | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
| Medical Student | 3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| Archived | 3 | 0 | 0 | 0 | 0 | 0 | 0 | 2 | 2 |
| *(no CRM contact)* | 6 | 0 | 0 | 0 | 0 | 0 | 0 | 6 | 6 |
Gaps found (this is the answer to "did I miss anyone?")
is_manager boolean covers OR, finance, staff scheduling and faculty scheduling; there is no scope column anywhere in the codebase (grep: no manager_scope
/ manager_domain). Nancy and Winnie get byte-identical navs. This is the root cause of R1 + R4.
is_finance_officer = 0; she gets the generic manager nav. R5 lost.My Evaluations / My Progress are nav-only.can_manage_evals = true through role_defaults.Faculty (their rows are NULL), so /eval-admin, /eval-tracker,
/eoy-admin, /eval-campaigns are reachable by all of them. Contradicts R3 as stated.
Verify before changing — see Risk R-2.
fconcepcion, acaceres, alopez, maleman, jsilva, jrodriguez). Real people with explicit flags, but no name/category → catch-all dashboard,
and they are hard to find in /admin users.
test-faculty, test-manager, test-resident@montefiore.org).
fellows (1 — Ariel Allen, currently fallback), medical students (3, all-false, nothing today),
the 6 NPs/staff who report absences (29 staff incl. Yvette Duchein, Paula Keatley, Rosa Heredia,
Tashana Ferguson, Sindhu Kanneth), archived (3 — one, sreese, still has a row), and the
/finance Infor portal (off-menu by design — confirm it stays that way).
Target model (proposed)
| Role | Who | Nav | Landing dashboard |
|---|---|---|---|
| Owner / Admin | sfrasier | full (unchanged) | admin command (unchanged) |
| Executive | Kelly Bottger | manager nav + Finance (review / reports / profiles) | Exec roll-up: money snapshot (Crystal's KPIs), coverage, eval completion, headcount |
| Finance officer | Crystal Santos | finance-only (shipped) | FinanceOfficerDashboard (shipped) |
| OR manager | Nancy Solano | OR group: schedule view, OR grid, block usage, pool, coverage, activity, own reimb | OR board: today's OR slate, block utilisation, open/uncovered cases, pool |
| Staff-schedule manager | *to be named* | staff schedule, schedule view, coverage, activity | Staff board: roster today, absences, coverage gaps |
| Faculty-schedule manager | *to be named* | schedule view/edit, call editor, call schedule, reports | Scheduler board: unfilled call/clinic slots, swap requests, publish state |
| Faculty | 24 attendings | unchanged, evals narrowed per R-2 decision | faculty tiles (unchanged) |
| Resident | 16 | unchanged | + My Evaluations card (due/completed), next shift, on-call, GME fund |
| NP / Staff | 29 | unchanged | staff self view: my absences, clinic today, coverage |
| Fellow | 1 | resident-like | resident-like |
| Medical student | 3 | minimal | minimal read-only |
| Archived | 3 | none | blocked |
Implementation Units
Goal: eliminate the drift so nothing built from the mirror regresses live work.
Files: whole repo (/var/www/unified → /workspace/projects/unified); note in docs/runbooks/.
Approach: rsync live → mirror excluding node_modules, dist, .env*, .pylibs; commit as a
baseline; record the md5 of every file this plan touches.
Verification: md5sum of Layout.tsx, Dashboard.tsx, roles.ts, Admin.tsx match on both trees.
manager_scope, is_eval_lead)Goal: make "which manager" expressible in data, exactly as is_manager / is_finance_officer are.
Files: Create server/db/migrations/043_manager_scope.sql; model on 042_finance_officer.sql.
Approach: manager_scope text (null | or | staff | faculty_schedules | exec) and
is_eval_lead boolean not null default false.
> Shipped 2026-09-30: manager_scope only. is_eval_lead was cut — it belongs to Unit 3,
> which is still gated, and an unused column is a promise the system does not keep. Add it with
> Unit 3. manager_scope also carries a CHECK constraint so a mistyped scope fails loudly
> instead of silently falling back to the generic manager surface. Backfill: Kelly exec, Nancy or; leave the other
eight managers null (= generic, today's behaviour). Print a before/after select in the migration
comment, as 042 does.
Test seam: migration is data-only; assert with a SQL query listing scope by email.
Verification: select email, manager_scope, is_eval_lead from unified.app_access order by 2 nulls last;
returns the expected four values with no unintended non-null scopes.
Goal: access.managerScope / access.isEvalLead reach the SPA without breaking the build.
Files: server/db/migrations/ (done in U1) · server/middleware/auth.ts (AccessFlags, ALL_FALSE,
SELECT list, row mapping) · app/src/types/index.ts · app/src/App.tsx GuardUser inline type.
Approach: the documented four-place rule — miss the App.tsx inline type and tsc fails with
TS2339. Predicates must return real booleans.
Test seam: /api/me shape — assert the two keys present for a scoped user.
Verification: npm run build in the container exits 0; /api/me returns both fields.
Goal: eval management belongs to the admin and designated eval leads, not 24 attendings.
Files: server/db/migrations/044_eval_scope.sql.
Approach: set role_defaults.Faculty.can_manage_evals = false, then grant explicitly to the eval
lead(s) + admin. Precondition: prove faculty submit evaluations through /eval/:token, which is
public and does not read canManageEvals — otherwise this removes a workflow.
Verification: effective-evals query from Current State returns only admin + eval leads; a faculty
account still submits an evaluation end-to-end via its token link.
Goal: R1, R4, R5 in the sidebar.
Files: app/src/components/Layout.tsx (MANAGER_SECTIONS keyed by scope; add EXEC_SECTIONS),
app/src/lib/roles.ts (isOrManagerOnly, isExecOnly, managerScope helper).
Approach: keep the allow-list pattern and selection order (finance → exec → manager → full); every
item keeps always: true. Do not switch to flag filtering.
Test seam: extract the section builders as pure functions so a test asserts item labels per role
without rendering.
Verification: rendered nav per role (playwright, /opt/webcapture): Nancy sees the OR group, Kelly
sees Finance, Crystal unchanged, sfrasier unchanged.
Goal: R1, R4, R6 — give each of the 40 people a page about their job.
Files: app/src/pages/Dashboard.tsx (split the isManager || isStaff branch; replace raw
category === tests with canonicalRole()), plus new
app/src/components/dashboard/OrManagerBoard.tsx, StaffBoard.tsx, SchedulerBoard.tsx,
ExecBoard.tsx, MyEvaluationsCard.tsx.
Approach: one variant selector dashboardVariantFor(user) returning a discriminated union, one
component per variant, each composed from existing cards + one or two new ones. Reuse ViewHeroBand,
ViewMetricTape, Card.
Test seam: dashboardVariantFor is pure — table-driven test over the 12 role shapes.
Verification: each role renders its own variant; Dashboard.smoke.test.tsx extended and green.
Goal: R6.
Files: app/src/components/dashboard/MyEvaluationsCard.tsx (new), app/src/pages/Dashboard.tsx.
Approach: read the resident's own eval status from the existing eval surface (resolve the exact
endpoint during implementation — /api/v1/eval-tracker is admin-gated, so a resident-scoped read is
needed). Empty state must be honest ("no evaluations due").
Test seam: card renders from a fixture; no embedded names.
Verification: a resident sees due/completed counts that match MyEval; numbers derive from the DB
at render time, never hardcoded.
Goal: G7, G8 — nobody is invisible.
Files: app/src/pages/admin/ users tab; app/src/pages/Admin.tsx as needed.
Approach: show manager_scope + effective flags (row ?? default) in the users grid; list
access rows with no CRM contact instead of silently dropping them.
Verification: the six contact-less emails appear with their flags; an admin can see why each
person has what they have without reading SQL.
Goal: the API never grants what the nav hides.
Files: server/routes/admin.ts (add requireOrManager / requireExec next to requireFinance),
server/routes/or-schedule.ts, server/routes/roster.ts.
Approach: only add a guard where a scoped role needs a read the current guard denies; leave every
admin-only route admin-only.
Verification: the 2×2 proof per role — scoped endpoint answers 200 for the scoped role, admin-only
endpoints 403, plus a control request that is not 403.
Goal: evidence, not assertions.
Files: extend /workspace/briefs/verify-finance-officer.py pattern to a per-role script.
Approach: for each of Nancy / Kelly / Crystal / a manager / a resident / a faculty / a staff:
rendered nav, dashboard variant, and the API matrix. Use the admin view-as rail for read-only
previews (admin must hold the designation being previewed).
Verification: one script, all roles, all green, output pasted into the handoff.
Worked example (what "done" looks like for Nancy)
uncovered/open cases, pool depth. Not the staff-ops page.
/api/v1/admin/users as her returns 403; the OR reads return 200.Risks & Dependencies
| Risk | Mitigation |
|---|---|
| R-1 Editing the stale mirror ships a regression of the shipped finance work | Unit 0 re-baselines first; md5-verify after every deploy |
R-2 Removing can_manage_evals from faculty breaks evaluation *submission* | Verify /eval/:token is independent of canManageEvals before Unit 3; if faculty need the admin surface, narrow only /eval-admin and /eoy-admin instead |
R-3 A new flag that isn't in all four places fails tsc and blocks the build | Unit 2 checklist; build in container before restart |
R-4 role_defaults change needs a container restart; app_access row changes do not | Restart after migrations; verify /api/me per role |
| R-5 Scope creep into a dashboard builder | Scope boundary above; Ponytail ruling below |
| R-6 Naming the staff-schedule / faculty-schedule managers is an owner decision | U4 ships the capability; assigning people is a data change the owner can make in /admin |
Dependencies: Unit 0 before all; U1 before U2/U3; U2 before U4/U5; U8 after U4 (needs to know the
visible surfaces); U9 last.
Ponytail scope ruling
a table), a separate OR analytics product, and any change to the /finance Infor portal.
manager_scope text column instead of four booleans; one variant selector functioninstead of branching in JSX; new boards composed from existing cards.
evaluations on landing — plus the two data integrity fixes (faculty eval over-grant, hidden accounts).
Open decisions (owner)
2 · Ideas (ce-ideate)
date: 2026-09-30
project: unified-platform
total_ideas: 5
picked_up: 1
Ideation: Unified Platform — dashboards and access
Context: the plan docs/plans/2026-09-30-001-feat-role-dashboards-and-access-plan.md covers the asks
the owner already made. These are the ideas *around* that work — grounded in the live state verified
2026-09-30 (93 access rows, row ?? role_defaults resolution, five dashboard branches, three nav
allow-lists).
Generated Ideas
1. Access ledger — "who can do what", with provenance
it came from an explicit app_access row or was inherited from role_defaults. This is exactly
how the faculty over-grant hid: 24 rows were NULL, so nothing on screen looked wrong while
can_manage_evals resolved true for every attending. It also directly serves the standing complaint
about data you can't see the source of.
two tables, no write path.
NULL and false is worse than nothing, because it recreates the bug it was built to catch.
2. Nav as a tested module, with a golden snapshot per role
as people. Move the builders into a pure module and assert a snapshot per role (12 shapes). Then
"managers must not see evals" is a failing test, not a bug report from a manager who found the
button.
here has repeatedly regressed through exactly the layer this covers.
3. Role preview in the view-as rail
the owner can flip through all twelve dashboards in about a minute without hunting for a representative
account. The owner judges visuals on the live artifact; this collapses the review loop.
narrowToActor already ANDs flags,so this is safe if the preview reuses the same contract.
render the same page, so it would show nothing. Sequence it after Unit 5.
4. Evaluations at a glance (program-wide completion matrix)
max 2×/AY, fresh pairs preferred) visible next to each cell. Eval season is the department's biggest
recurring deadline and chasing it is manual today.
/eval-tracker. Extend that page, do not add a sixth eval surface — thecodebase already has Evaluations / Evaluation Cycles / EOY / Campaigns and a seventh would be a
maintenance tax.
R-2 (who owns eval management) is settled. Building it first would enshrine the wrong audience.
5. "Since you were last here" delta on the landing page
coverage gap opened · 3 claims awaiting you · 1 eval due". This is what makes a dashboard worth a
second look — the owner's own test for a good page — and it makes daily login pay off for the
forty people who currently have no reason to return.
Also needs a decision on what counts as notable per role, or it becomes a second activity feed.
"notable" differs per role — an OR manager and a resident care about different deltas.
Rejected Ideas (for reference)
support surface, to solve a problem nobody reported. The role split is the fix.
pipeline and the roster studio. Nancy needs a page, not a product.
/finance Infor portal into the nav: its own header documents the off-menu decisionas deliberate — the department is not running it yet. Revisit when the Infor export lands.
cosmetic; the catch-all already renders something harmless.
Picked-Up Summary
(gated on eval-ownership decision), landing-page delta feed