- dashboard: Vite + React + TypeScript + Tailwind CSS
- Always use TypeScript — never plain JS for new code
- Use
maketargets, never rawnpmcommands — seedashboard/Makefilefor all targets - Run
make typecheckto verify types,make formatto format after edits - Common targets:
make dev,make build,make qa,make docker-build,make typecheck
- Routes live in
src/routes/— the Vite plugin auto-generatessrc/routeTree.gen.tsat build/dev time; never edit that file manually src/routes/__root.tsx— root layout (Header, Footer, ConfigModal); shared across all pagessrc/routes/index.tsx— home page (/), service shortcut gridsrc/routes/status.tsx— status monitor (/status), existing status cards- Router is registered in
main.tsxviacreateRouter+RouterProvider; type registration viadeclare module '@tanstack/react-router' - Use
<Link to="/">for navigation — paths are type-checked at compile time - Use
useRouterState({ select: s => s.location.pathname })to read current path
@tanstack/hotkeysis framework-agnostic — use theuseHotkeywrapper atsrc/app/hooks/use-hotkey.tsuseHotkey(key, handler)registers viaHotkeyManagersingleton and cleans up on unmount- Home page services define a
shortcutfield insrc/features/home/services.ts; rendered as<kbd>hints on cards - Cast user-supplied strings to
Hotkeytype when callingmanager.register(key as Hotkey, ...)
services.ts—HOME_CATEGORIESarray of{ label, services[] }where each service hasname,url, optionalicon(resolves to/icons/services/<icon>.svg), optionalshortcuthome-page.tsx— renders category sections with a responsivegrid(auto-fill, 100–120 px columns); falls back to initial letter if no icon SVG exists- To add a service: add an entry to
HOME_CATEGORIESinservices.ts; add its SVG topublic/icons/services/if needed
- Keep solutions minimal — don't add features, refactors, or comments beyond what was asked
- No docstrings or type annotations on unchanged code
- Prefer editing existing files over creating new ones
- After editing files, run
make formatin the project you changed
- Every new user-facing feature ships with both a Cucumber scenario and an e2e spec — a scenario in
tests/bdd/features/*.featuresaying what the feature is for, and a Playwright spec intests/e2e/<area>/pinning the behaviour down. Not one or the other, and not "later" - Areas are
home/,status/andcommon/(header, navigation, anything shared) - Specs import
test/expectfromtests/e2e/fixture.ts, never straight from@playwright/test— the fixture draws the on-screen labelmake e2e-headruns on - Locate by role, label or placeholder; paths come from
ROUTES(@/app/routes), never a literal/status - Reuse a Cucumber step before writing a near-duplicate — the step file is the vocabulary, a second phrasing of the same action splits it
- A test never reaches the network: the status specs abort every request off localhost, so a suite result never depends on GitHub being up
- Pure logic (a filter, a formatter) gets a vitest test in
tests/as well — it runs in milliseconds and says which function broke - Before pushing:
make test,make e2e,make bdd.make e2e-headto watch a failure happen
- Brand/service logos go in
public/icons/<name>.svg(20×20 viewBox) so the browser can cache them - UI icons that need
currentColor(chevron, refresh, settings gear, etc.) stay inline in JSX — moving them to<img>breaks color theming - Icon filename defaults to
service.name.toLowerCase(); override withiconfield on theServiceobject
- Supported platforms:
atlassian|statusio|instatus|google-workspace atlassian— fetch/api/v2/summary.jsonstatusio— requiresstatusioId; fetchhttps://api.status.io/1.0/status/<id>instatus— fetch/summary.json+/components.jsongoogle-workspace— fetchhttps://www.google.com/appsstatus/dashboard/incidents.json- Before adding a service, verify the platform by checking network requests on the status page
- Sections:
'external'(default) |'homelab'
- Tooltips: use
<Tooltip>fromsrc/app/components/tooltip.tsx— supportsplacement="top" | "bottom"; black bg, white text, arrow pointer - Accordion sections: full-width clickable button with
aria-expanded+aria-controls/role="region"+aria-labelledby - Drag-and-drop:
@dnd-kit/sortablewithrectSortingStrategy; order persisted tolocalStorage - Collapse state persisted to
localStorage - Layout: CSS
columns-[320px](masonry) within each section — not a fixed grid
- Decorative SVGs get
aria-hidden="true" - Interactive elements need
aria-labelwhen icon-only - Modal closable with ESC key via
useEffectkeydown listener - Tooltips on truncated text and interactive controls
- Every new service must claim a unique host port — check
.docs/PORTS.mdbefore choosing, pick the next free one - Add the new port to
.docs/PORTS.mdand to the service's own README under## Ports - Suggest exposing the service via a new Cloudflare Zero Trust Tunnel subdomain pointing at
http://<pi-host>:<port>— remind the user to add it under Published application routes on the tunnel in the Cloudflare dashboard (Zero Trust → Networks → Tunnels)
- Docker: multi-stage build (
node:20-alpine→nginx:alpine) - Target:
linux/arm64(Raspberry Pi) make docker-buildto build the image- GitHub Actions:
.github/workflows/dashboard-build.yml
Skills live in .agents/skills/<name>/SKILL.md — the neutral path, read by both
Codex and opencode. .claude/skills is a symlink to that directory so Claude
Code finds the same files; don't "fix" it into a real folder, that splits the
skills in two.
- Never commit or push to
main— always branch + PR, squash-merge (e.g.gh pr merge <n> --squash --delete-branch) - Reviews come from Anthropic's managed Code Review (Claude GitHub App). Gemini Code Assist is no longer used — never comment
/gemini review - Reviews run automatically per the repo's Review Behavior setting. To request one explicitly, post a top-level PR comment:
@claude review(single run) or@claude review always(subscribe the PR to every push) - Findings land as inline comments tagged 🔴 Important / 🟡 Nit / 🟣 Pre-existing, plus a
Claude Code Reviewcheck run. The check never blocks merge - Replying to an inline finding does not notify Claude. To act on one, fix the code and push — a push-triggered re-review resolves the thread itself. Resolve a thread by hand only when rejecting a finding, and say why in the reply
- Repo-wide review tuning lives in
REVIEW.mdat the root; it outranksCLAUDE.mdfor review behaviour - Once findings are addressed and CI is green, squash-merge