// Home page — 9 persona variants. Project Owner has 2 layout variations. // Each variant uses the same data primitives but emphasizes different things. // ── Shared sub-blocks ──────────────────────────────────────────────── const ActivityRow = ({ a }) => (
{PERSON_BY_ID[a.who].name} {a.verb} {a.what}
{a.when}
); const QuickAction = ({ icon, label, hint, onClick, accent }) => ( ); const InboxRow = ({ icon, tone = 'neutral', title, meta, action, onClick }) => (
{ if (onClick) e.currentTarget.style.background = 'var(--apm-bg-sunken)'; }} onMouseLeave={e => { if (onClick) e.currentTarget.style.background = 'transparent'; }}>
{title}
{meta}
{action}
); // ── 1. PROJECT OWNER home (variant A: action-first) ───────────────── const HomeOwner = ({ persona, variant = 'a', goto }) => { const myProjects = projectsByOwner(persona.id); const dueGates = myProjects.filter(p => p.health === 'at-risk' || p.progress > 50 && p.progress < 100); const myTickets = ticketsByAssignee(persona.id); if (variant === 'b') { // Variant B — focus mode, single-column timeline of "what's next" return (
{/* 1 */}
1
Gate response due · today
RA/QA flagged 2 conditions on PEARL Fit-Coach MVP
Jenna Rowe needs your response on consent-scope coverage and adverse-event routing before the gate review tomorrow.
{/* 2 */}
2
Component change · this week
Consent Ledger split is breaking — vote on impact for your 2 dependent projects
Migrating to clinical-care-only consent will require a re-prompt flow in PEARL Fit-Coach and Patient Consent Service.
{/* 3 */}
3
Idea routing · no rush
Tomás logged “Auto-summarize visit notes” — does it belong in your Q-shape work?
14 upvotes. The idea touches charting and patient comms — your call whether it merges, gets promoted, or gets parked.
You also have {myTickets.length} open tickets and {myProjects.length} projects.
); } // Variant A — full dashboard return ( p.initiative)).size} initiatives`} subtitle="Gate reviews, change requests touching your components, and ideas in your space all flow here. The home is read-only — actions live on the project pages." action={
} /> {/* Top stats */}
} />
{/* Two-col */}
{/* My projects */}
goto('projects')}>View all } />
Project
Phase
Health
Tickets
Target
{myProjects.slice(0, 6).map((p, i) => (
goto('project', { id: p.id })} style={{ display: 'grid', gridTemplateColumns: '1.7fr 0.9fr 0.7fr 0.7fr 1fr', padding: 'var(--apm-row-pad) var(--apm-card-pad)', alignItems: 'center', cursor: 'pointer', borderBottom: i < myProjects.slice(0,6).length - 1 ? '1px solid var(--apm-line)' : 'none', transition: 'background .1s', }} onMouseEnter={e => e.currentTarget.style.background = 'var(--apm-bg-sunken)'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
{p.name}
{p.code} · {INI_BY_ID[p.initiative].name}
{p.openTickets}
{p.target}
))}
{/* Gates */}
goto('project', { id: 'proj_fitcoach_mvp', tab: 'gates' })}>Open} onClick={() => goto('project', { id: 'proj_fitcoach_mvp', tab: 'gates' })} /> goto('project', { id: 'proj_audit_pipe', tab: 'gates' })}>Open} onClick={() => goto('project', { id: 'proj_audit_pipe', tab: 'gates' })} /> goto('project', { id: 'proj_consent' })}>View} />
{/* Right rail */}
goto('__new_project__')} />
{APM_ACTIVITY.slice(0, 6).map((a, i) => )}
); }; // ── 2. ADMIN home ──────────────────────────────────────────────────── const HomeAdmin = ({ persona, goto }) => ( }>Invite user} />
} /> Review} /> Start} /> View} />
{APM_SENSITIVITY.map(s => (
{s.desc}
{[42, 31, 8, 5, 27][APM_SENSITIVITY.indexOf(s)]} projects
))}
{APM_ACTIVITY.slice(0, 5).map((a, i) => )}
); // ── 3. STAGE APPROVER (Security) home ─────────────────────────────── const HomeApprover = ({ persona, goto }) => (
{[ { proj: 'proj_fitcoach_mvp', gate: 'HIPAA/PII', age: '6 hr', prio: 'high' }, { proj: 'proj_consent', gate: 'HIPAA/PII', age: '1 day', prio: 'high' }, { proj: 'proj_doctrl', gate: 'Security', age: '2 day', prio: 'med' }, { proj: 'proj_audit_pipe', gate: 'Security', age: '3 day', prio: 'med' }, ].map((row, i, arr) => { const p = PROJ_BY_ID[row.proj]; return (
goto('project', { id: p.id, tab: 'gates' })} style={{ display: 'grid', gridTemplateColumns: '40px 1fr 140px 120px 120px', padding: 'var(--apm-row-pad) var(--apm-card-pad)', alignItems: 'center', gap: 12, cursor: 'pointer', borderBottom: i < arr.length - 1 ? '1px solid var(--apm-line)' : 'none', transition: 'background .1s', }} onMouseEnter={e => e.currentTarget.style.background = 'var(--apm-bg-sunken)'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}> {row.prio === 'high' ? 'High' : 'Med'}
{p.name}
{p.code} · owner {PERSON_BY_ID[p.owner].name} · {p.sensitivity.map(s => )}
open {row.age}
); })}
Any project handling PHI
Must use comp_consent v1.2+ and emit to comp_audit. Re-confirm at Go-Live.
Any external partner integration
Pen-test on file within 12 months. mTLS only. Logs land in audit pipeline.
); // ── 4. PORTFOLIO MANAGER home ─────────────────────────────────────── const HomeManager = ({ persona, goto }) => ( }>New initiative} /> {/* Initiatives strip */}
{APM_INITIATIVES.map(ini => { const tone = ini.health >= 75 ? 'var(--apm-ok)' : ini.health >= 60 ? 'var(--apm-warn)' : 'var(--apm-risk)'; return ( goto('initiative', { id: ini.id })}>
{ini.code}
{ini.name}
{ini.summary}
{projectsByInitiative(ini.id).length} projects
{fmtMoney(ini.spent)} / {fmtMoney(ini.budget)}
target {ini.target}
Health {ini.health}
); })}
{[ { team: 'Clinical Apps', load: 92 }, { team: 'Platform', load: 78 }, { team: 'Regulatory', load: 64 }, { team: 'IT Support', load: 48 }, ].map((t,i,a) => (
{t.team}
{t.load}%
))}
Open} /> Plan} /> Open} />
); // ── 5. RA/QA home ──────────────────────────────────────────────────── const HomeRaqa = ({ persona, goto }) => (
} />
{APM_PROJECTS.filter(p => p.raqa === persona.id && (p.phase === 'RA/QA' || p.phase === 'HIPAA/PII')).map((p, i, a) => (
goto('project', { id: p.id, tab: 'gates' })} style={{ padding: 'var(--apm-row-pad) var(--apm-card-pad)', cursor: 'pointer', borderBottom: i < a.length - 1 ? '1px solid var(--apm-line)' : 'none', }} onMouseEnter={e => e.currentTarget.style.background = 'var(--apm-bg-sunken)'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
{p.name}
{p.code} · owner {PERSON_BY_ID[p.owner].name}
{p.sensitivity.map(s => )}
))}
{[ { label: 'CER documents on file', value: '34 / 36', tone: 'warn' }, { label: 'Training records up-to-date', value: '92%', tone: 'ok' }, { label: 'Adverse-event signals', value: '3 escalated', tone: 'risk' }, { label: 'External audits next 90d', value: '2 (BSI, FDA)', tone: 'info' }, ].map((r, i, a) => (
{r.label}
{r.value}
))}
); // ── 6. HELPDESK home ──────────────────────────────────────────────── const HomeHelpdesk = ({ persona, goto }) => ( }>New ticket} />
goto('tickets')}>All } /> {APM_TICKETS.slice(0, 6).map((t, i, a) => (
goto('tickets', { id: t.id })} style={{ display: 'grid', gridTemplateColumns: '60px 1fr 130px 80px 80px', padding: 'var(--apm-row-pad) var(--apm-card-pad)', alignItems: 'center', gap: 12, cursor: 'pointer', borderBottom: i < a.length - 1 ? '1px solid var(--apm-line)' : 'none', }} onMouseEnter={e => e.currentTarget.style.background = 'var(--apm-bg-sunken)'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}> {t.priority}
{t.title}
{PROJ_BY_ID[t.project].name} · {t.age}
{t.status} {t.assignee ? : }
))}
{APM_HELP.filter(h => h.status === 'open').map((h,i,a) => (
{h.title}
asked by {PERSON_BY_ID[h.from].name} · {h.age}
))}
"Where do I find the clinic PIN reset flow?" appeared 8 times in 30 days.
); // ── 7. DEVOPS home ────────────────────────────────────────────────── const HomeDevops = ({ persona, goto }) => (
} />
{componentsByOwner(persona.id).map((c,i,a) => (
goto('components', { id: c.id })} style={{ padding: 'var(--apm-row-pad) var(--apm-card-pad)', cursor: 'pointer', borderBottom: i < a.length - 1 ? '1px solid var(--apm-line)' : 'none', display: 'grid', gridTemplateColumns: '1.2fr 1fr 100px 100px 80px', alignItems: 'center', gap: 12, }} onMouseEnter={e => e.currentTarget.style.background = 'var(--apm-bg-sunken)'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
{c.name}
{c.summary}
{c.sensitivity.map(s => )}
{c.stability}
{c.version}
{c.dependents.length} deps
))}
{APM_CHANGES.filter(c => COMP_BY_ID[c.component].owner === persona.id || c.status === 'pending').slice(0,3).map(c => ( goto('components', { id: c.component, change: c.id })}>
{c.breaking ? 'Breaking' : 'Compatible'} {COMP_BY_ID[c.component].name}
{c.title}
{c.description}
{c.impact.length} dependent projects · submitted by {PERSON_BY_ID[c.submitter].name} · {c.submitted}
))}
); // ── 8. EXECUTIVE home ─────────────────────────────────────────────── const HomeExec = ({ persona, goto }) => ( {/* Briefings */}
{APM_BRIEFINGS.map(b => { const tone = b.kind === 'risk' ? 'risk' : b.kind === 'milestone' ? 'ok' : b.kind === 'decision' ? 'accent' : 'info'; return (
{b.kind}
{b.title}
{b.body}
{b.when}
); })}
{/* Portfolio strip */} goto('portfolio')}>Full view } />
{APM_INITIATIVES.map(ini => { const tone = ini.health >= 75 ? 'var(--apm-ok)' : ini.health >= 60 ? 'var(--apm-warn)' : 'var(--apm-risk)'; return ( goto('initiative', { id: ini.id })}>
{ini.code}
{ini.name}
{ini.health}
health
{fmtMoney(ini.spent)} / {fmtMoney(ini.budget)} · {ini.target}
); })}
); // ── 9. TEAM MEMBER home ───────────────────────────────────────────── const HomeMember = ({ persona, goto }) => ( } onClick={() => goto('ideas', { compose: true })}>Submit an idea} />
{APM_TICKETS.filter((_,i) => i < 4).map((t, i, a) => (
goto('tickets', { id: t.id })} style={{ padding: 'var(--apm-row-pad) var(--apm-card-pad)', cursor: 'pointer', borderBottom: i < a.length - 1 ? '1px solid var(--apm-line)' : 'none', display: 'flex', alignItems: 'center', gap: 12, }} onMouseEnter={e => e.currentTarget.style.background = 'var(--apm-bg-sunken)'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}> {t.priority}
{t.title}
{PROJ_BY_ID[t.project].name} · {t.age}
))}
{APM_IDEAS.filter(i => i.submitter === persona.id || persona.id === 'p_tomas').slice(0,3).map((idea, i, a) => (
{idea.title}
{idea.upvotes} upvotes · {idea.submitted}
{idea.status}
))}
); // ── Router ─────────────────────────────────────────────────────────── const Home = ({ persona, route, goto }) => { const v = route.params?.variant || 'a'; switch (persona.role) { case 'admin': return ; case 'owner': return ; case 'approver': return ; case 'manager': return ; case 'raqa': return ; case 'helpdesk': return ; case 'devops': return ; case 'exec': return ; case 'member': return ; default: return ; } }; // Alias so root file can call const HomePage = ({ persona, variant, goto, openChat, openCompose }) => ( ); Object.assign(window, { Home, HomePage });