Capstone: TrackFlow Ops Dashboard
APIs don't answer phones — people do, and they answer faster when the screen in front of them is honest, fast, and readable from six feet away. This capstone is about building that screen well, with Gemini doing the labor of layout, fetch plumbing, and rendering while you direct the UX decisions.
Domain Briefing: The Ops Floor
The customer-service team at a wholesale distributor lives in a tool like this. A restaurant calls: "where's PO-88412?" The rep needs the answer in under five seconds: current state, carrier events, and — critically — whether the order is about to breach its SLA before the customer mentions it. The pain with bad ops dashboards is always the same three failures: stale data presented as fresh (the rep promises "it shipped" when it didn't), failure states that look like empty states (the API is down but the screen shows "no orders" — so the rep tells the customer there's no order), and mouse-only UIs that slow down power users who live on the keyboard. Your dashboard must defeat all three — they're rubric items.
Two reasons. Pedagogically: with no framework between you and the DOM, every concept (state, rendering, events, fetch lifecycles) is visible — and those concepts transfer to React/Vue/Svelte, while the reverse is not true. Practically: this capstone tests whether you can direct Gemini through UI work — layout, accessibility, async states — where the review skills differ from backend diffs: you verify with your eyes, the keyboard, and the network tab, not just pytest.
What You Build
trackflow-dashboard — three files (index.html, style.css, app.js) plus a pure-logic module, no build step, opened via a static server:
- Order board: table of POs with color-coded state chips, filterable by state and customer, sortable by promised ship date
- SLA banner: a persistent strip showing breached / at-risk counts; clicking filters the board
- Detail drawer: clicking a row (or pressing Enter on it) slides open the order: line items, full audit timeline, carrier tracking events
- Live-ness: auto-refresh every 30s with a visible "last updated" stamp; manual refresh button; distinct loading / error / empty states
- Data layer:
api.jswith two implementations behind one interface —live(fetches the TrackFlow API from the API capstone) andmock(deterministic in-memory data) selected by?mode=mock - Tested logic: the pure functions (SLA classification, filtering, sorting, event formatting) live in
logic.jsand run undernode --test— no browser needed
Executing This Capstone in VS Code / PyCharm (or WebStorm)
Everything in this project is built through Gemini Code Assist inside your IDE — never by copying finished code from elsewhere. Each phase uses some of these five moves; every paste-block below names its move in the header. (Gemini Code Assist works identically in WebStorm if that's your JS home — it's the same JetBrains plugin.)
- Create the project folder and open it: VS Code File → Open Folder (PyCharm/WebStorm: File → Open). New files via the Explorer panel / Project tool window.
- Confirm Gemini Code Assist is signed in: the ✦ icon in VS Code's Activity Bar opens the panel with no error badge (JetBrains: the Gemini tool window on the right edge greets you by account). Setup is module G01.
- Open the integrated terminal (
Ctrl+`in VS Code,Alt+F12in JetBrains) for git andnode --test. - Serve the page from a static server —
fetch()won't work fromfile://. Easiest:python -m http.server 8000in the integrated terminal, then openhttp://localhost:8000(or VS Code's Live Server extension / JetBrains' built-in preview). - After creating GEMINI.md in Phase 1, start a new chat so the layering rules load into context (the G05 lesson).
- Keep the browser DevTools open (F12) — console errors and the Network tab are the "evidence" you feed to
/fixin this capstone (the G03 habit, frontend edition).
| Move | VS Code | PyCharm / WebStorm |
|---|---|---|
| 1 · Spec-comment | Type the comment in the editor and pause — ghost text appears. Tab accepts, Ctrl+→ accepts word-by-word, Esc rejects (G02) | Same: type, pause, Tab; hover the suggestion to cycle alternatives |
| 2 · Chat / smart cmd | Click the ✦ icon in the Activity Bar → chat panel opens. Select code first to scope it; /explain /fix /doc /simplify work here (G03) | Gemini tool window (right edge) → Chat tab; same slash commands |
| 3 · Generate tests | Select the function → right-click → Gemini Code Assist → Generate unit tests — ask for node:test style in chat if it assumes Jest; save into tests/ (G04) | Select → right-click → Gemini → Generate unit tests |
| 4 · Agent ticket | In the chat input, switch the Agent toggle ON → paste the ticket → review the plan in the panel → Accept/Reject each diff → click Allow when it asks to run a command (G06) | Gemini tool window → Agent tab → paste the ticket; leave Auto-approve changes OFF; review diffs the same way |
| 5 · Headless CLI | Integrated terminal: gemini -p "..." — same login, same GEMINI.md rules as the IDE (G08) | |
Example: a block labeled "Move 4 · paste in Agent mode" means: open the Gemini panel, flip the Agent toggle (VS Code) or open the Agent tab (JetBrains), paste, and work the plan→diff→approve loop.
Architecture: Three Layers, Even in Vanilla
trackflow-dashboard/
├── index.html semantic skeleton only — no inline JS
├── style.css design tokens as CSS custom properties
├── js/
│ ├── api.js data layer: liveApi + mockApi behind one interface
│ ├── logic.js PURE functions: classifySla(order, now),
│ │ filterOrders(orders, criteria), sortOrders(...),
│ │ formatEvent(...) — no DOM, no fetch, no Date.now()
│ │ (time is always a parameter — that's what makes it testable)
│ └── app.js state + render + events; the ONLY file that touches the DOM
├── tests/
│ └── logic.test.mjs node --test, no browser required
└── GEMINI.md / .aiexclude / .gemini/styleguide.md
node --test and is the vanilla ancestor of every state-management pattern you'll later meet in frameworks.Build Phases
| Phase | What happens | Skills from |
|---|---|---|
| 1. Foundation | GEMINI.md + skeleton + tokens | G05, G02 |
| 2. Logic + data | Pure functions, tests, dual API layer | G02–G04 |
| 3. Render | Board, banner, drawer, async states | G02–G03 |
| 4. A11y + agent | Accessibility audit; dark mode via agent | G03, G06 |
| 5. Ship | PR review + eyes-on verification | G09 |
Frontend success is partly terminal output (node --test) and partly observable behavior — so the expected-output blocks below describe both. Your visual design will differ; the behaviors (which state shows when, where focus lands, what persists) must match exactly.
Foundation: rules, skeleton, tokens
New repo. GEMINI.md rules: the three-layer separation above; semantic HTML (table for the board, button for actions — never clickable divs); all colors/spacing via CSS custom properties in :root; every interactive element keyboard-reachable; loading/error/empty are three distinct UI states, never conflated. Then generate the skeleton from a spec-comment in index.html (Move 1: type an HTML comment describing the regions — header with search + refresh, SLA banner, filter bar, order table, drawer <aside> — pause for ghost text, accept section by section) and the token sheet in style.css the same way.
index.html as the active file): "/explain is this markup semantic? flag any div that should be a button, table, or nav".Logic and data: testable before visible
Build logic.js function-by-function with spec-comments. The heart is SLA classification — spec it precisely:
// classifySla(order, nowIso) -> "ON_TIME" | "AT_RISK" | "BREACHED" | "MET"
// MET: order reached SHIPPED/DELIVERED on or before promised_ship_date.
// BREACHED: not shipped and now is past end of promised_ship_date.
// AT_RISK: not shipped and promised_ship_date is today or tomorrow.
// ON_TIME: everything else. Treat dates as YYYY-MM-DD strings in local time.
// Pure function: never call Date.now() — now arrives as a parameter.
Generate tests (Move 3: select classifySla → right-click → Generate unit tests, then in chat demand "node:test suite for classifySla covering all four outcomes plus the boundary: promised date == today"), run node --test tests/ in the integrated terminal, and iterate. Then api.js: mockApi first (deterministic orders covering every state and SLA class — generate it headlessly with Gemini CLI like the other capstones), then liveApi with timeouts and typed errors ({kind: "network" | "http", status?}) so the UI can distinguish failure from emptiness.
PS> node --test tests/
✔ classifySla: MET when shipped on or before promised date
✔ classifySla: BREACHED when unshipped past promised date
✔ classifySla: AT_RISK when promised date is today (the boundary)
✔ classifySla: AT_RISK when promised date is tomorrow
✔ classifySla: ON_TIME otherwise
✔ filterOrders: state and customer filters combine (AND)
✔ filterOrders: empty criteria returns all orders
✔ sortOrders: promised date ascending, stable for ties
✔ formatEvent: ISO timestamp renders as local short form
ℹ tests 14 • suites 4 • pass 14 • fail 0 • duration_ms 92
node --test all green with zero browser involvement — if any logic test needs a DOM, the function is in the wrong file (the frontend version of G04's "failing tests indict the design").Render: where you review with your eyes
Build app.js in chat-sized increments: render the board from mock data → wire filters/sort (calling logic.js) → SLA banner → drawer → auto-refresh with the "last updated" stamp. Then force the three async states and look at each: mock mode with empty data (empty state: "No orders match" + a clear-filters action), live mode with the API stopped (error state: "Can't reach TrackFlow — data may be stale" + retry), and a throttled network (loading state visible, UI not frozen). Use chat to fix what you see, feeding it evidence G03-style: paste the console error or describe the misbehavior precisely.
http://localhost:8000/?mode=mock
-> board renders 12 orders · banner reads "⚠ 2 BREACHED · 3 AT RISK"
-> "Last updated 14:02:11" stamp ticks on every refresh
EMPTY (mock mode, filter to a customer with no orders)
-> "No orders match your filters" + [Clear filters] button
-> banner counts unchanged (they reflect ALL orders, not the filter)
ERROR (live mode, TrackFlow server stopped)
-> red strip: "Can't reach TrackFlow — data may be stale" + [Retry]
-> previous orders stay visible, marked stale — NOT a blank table
-> DevTools console: one handled error log, no uncaught exceptions
LOADING (DevTools → Network → throttle to Slow 3G, press Refresh)
-> skeleton rows or spinner visible; filters remain clickable;
the "Last updated" stamp does NOT advance until data lands
Generated frontend code loves catch (e) { orders = [] } — which renders an API outage as "no orders," the exact lie that gets a rep to tell a customer their order doesn't exist. Hunt for this pattern in every diff you accept; rejecting it is rubric item 3.
Accessibility audit, then an agent-built feature
First the audit — Move 2, attaching both files as context (the +/paperclip control in the chat box, or keep both open in editor tabs): "Audit index.html + app.js for keyboard and screen-reader accessibility: tab order, focus management when the drawer opens/closes (focus moves in, Escape closes, focus returns to the row), aria-live for the SLA banner and 'last updated', missing labels. List concrete fixes." Apply and verify by unplugging your mouse — complete a full task (find a breached order, open its drawer, read tracking, close) with keys alone. Then the G06 ticket:
Add dark/light theme support:
1. All colors already flow through CSS custom properties — add a light
theme as a [data-theme="light"] override block. Do NOT touch component CSS.
2. A theme toggle button in the header (aria-pressed, persists in localStorage,
defaults to the user's prefers-color-scheme).
3. Both themes must keep WCAG AA contrast for state chips and the SLA
banner — list the contrast ratios you chose in your plan.
Plan first.
KEYBOARD-ONLY TASK (mouse unplugged)
Tab order: search → refresh → theme toggle → SLA banner → filters → first row
Arrow/Tab to the breached row, press Enter
-> drawer opens, focus lands on the drawer heading
Tab through: line items → audit timeline → tracking events
Press Esc
-> drawer closes, focus RETURNS to the row you opened it from
THEME
Click toggle -> <html data-theme="light">; aria-pressed flips to "true"
DevTools console: localStorage.getItem("theme") -> "light"
Reload -> still light. Clear localStorage + reload
-> follows OS prefers-color-scheme
State chips and SLA banner readable in BOTH themes (AA: ≥4.5:1)
Ship: review gate plus the eyes-on checklist
Push to GitHub; .gemini/styleguide.md must-flags: DOM access outside app.js, Date.now() inside logic.js, clickable non-button elements, catch-blocks that render errors as empty states, hardcoded colors bypassing tokens. Open the theme feature as a PR, process the review (G09 discipline). Final verification is manual and honest: the three async states, the keyboard-only task, both themes, and ?mode=mock vs live against your running TrackFlow API.
Grading Rubric
Stretch Goals
- Optimistic transitions: add a "Mark Delivered" action in the drawer that calls the API's transition endpoint, updates the UI optimistically, and rolls back with a toast on 409.
- URL state: filters and the open drawer encoded in the query string, so a rep can paste a link to a colleague that opens the exact same view.
- Big-board mode: a read-only rotation view for the warehouse wall TV (larger type, auto-cycling pages, breaches always pinned).
- Framework port: rebuild app.js in React keeping logic.js and api.js byte-identical — the layering pays off when only one of three layers changes.
"Built an operations dashboard with layered vanilla-JS architecture (headlessly tested pure logic, swappable data layer), honest async states, full keyboard accessibility with managed focus, and WCAG AA dual themes; AI-assisted development with human-reviewed agent workflows."