Pattern: role home
Who uses it: every named role — a buyer, an approver, a warehouse supervisor — each seeing their own version. How often: the first screen after sign-in, and the one they come back to between tasks. What "done" looks like: the reader knows in one glance whether anything needs them, and leaves for the screen that resolves it. Not a launcher — a tile grid of every app someone could open is app launch; this shows live content about their work. And not a dashboard: the reporting dashboard monitors the business (is the period on track), a role home monitors my work (is anything waiting on me). Same widgets, different subject — and confusing the two is how a home screen turns into a chart wall nobody acts on.
The screen — one role's morning
Every number on this screen is about the reader — "awaiting your approval", "your 11 close tasks" — and every one of them is a link out. A role home that cannot be left is a wall; the measure of this screen is how fast people leave it for the screen that does the work.
One pattern, many roles
The same markup serves every role — only the card contents change, which is why this is one pattern and not one screen per job title. An approver's morning above; a warehouse supervisor's below:
Identical classes, identical grid, different content. Roles differ in what the server sends, never in the markup — a per-role screen fork is the thing this pattern exists to prevent.
Markup
<div class="bo-widget-grid" style="--bo-widget-min: 16rem">
<!-- The one card that must exist: what needs the reader, linking
into the inbox that holds the rows. -->
<section class="bo-widget bo-widget--span-2">
<div class="bo-widget__header">
<span class="bo-widget__title">Needs you</span>
<a class="bo-btn bo-btn--sm bo-btn--secondary" href="/inbox">Open inbox</a>
</div>
<div class="bo-widget__body">
<span class="bo-stat">
<span class="bo-stat__label">Awaiting your approval</span>
<span class="bo-stat__value">3</span>
<span class="bo-stat__delta bo-stat__delta--bad">1 over 2 days</span>
</span>
</div>
</section>
<!-- Progress card: the close, a run, an onboarding checklist. -->
<section class="bo-widget">
<div class="bo-widget__header"><span class="bo-widget__title">Period close</span></div>
<div class="bo-widget__body">
<!-- Native <progress>: value/max semantics and the progressbar
role come from the platform. Always pair with visible text. -->
<progress class="bo-progress" value="82" max="100"
aria-label="Period 2026-07 close progress"></progress>
</div>
</section>
</div> Anatomy
- Identity line — a byline naming who is signed in and in what role. On a screen whose whole content is "yours", the reader must be able to see whose.
- "Needs you" card — the one card that must exist, spanning two columns, counting what the inbox holds and linking straight into it. This card is a summary OF the inbox, never a second copy of its logic. This is the human-monitoring signal — the AI-manages/human-monitors split below names it explicitly.
- Stat cards — stats with a label, value, and a delta that carries its meaning in words plus a glyph (two-channel: "1 over 2 days" is the text, the tone is the echo).
- Progress card — a progress bar for anything with a finish line: the period close, a posting run, onboarding.
- Recent — links back to what the reader touched; the cheapest useful card in the set.
Data contract
| Request | Returns / on failure |
|---|---|
| GET /home | the grid shell with each card as its own fragment slot — the page must render before the slowest card resolves |
| GET /home/cards/:name | one card, loaded independently (hx-trigger="load"); one slow query must never hold the whole screen — the reporting dashboard's contract, same reason. On 5xx/timeout: that one card shows its own failure + retry (see States); the rest of the grid stays live |
| counts | every "needs you" number comes from the SAME query the inbox uses, not a parallel one — two sources for one number is how a home screen starts lying |
| per-role content | the server decides which cards a role gets; the client never hides cards it received, because a hidden card is still a leaked fact |
The AI-manages / human-monitors split, stated: this screen is almost entirely the human's side — it is the oversight surface for work agents are doing underneath. Its one machine-facing rule is the counts contract above: an agent that resolves an inbox row must move this screen's number with it, because a stale "3 awaiting" is worse than no number at all.
States
| State | What ships |
|---|---|
| Nothing needs you | the "needs you" card says so as a success — "Nothing is waiting on you" — matching the inbox's own empty state; the card stays, it never disappears (a missing card reads as a broken card) |
| Card still loading | a skeleton inside that card only; the rest of the screen is already usable |
| Card failed | that card shows the failure and a retry, and the others carry on — one dead query must not blank the home screen |
| New role / first day | cards render with real zeros and an explanation, never an empty grid: "No orders yet — your first PO will appear here" |
| Permission-limited role | the server simply does not send those cards; nothing on screen hints at what was withheld |
| No JS | fully functional — this page has no JS dependency; every card is static markup and there is no <script> block to lose |
Components used
complexity 2 of 4