Phase A inventor deliverable: 3 candidate shapes (Tiles + view switcher, Project rows table, 3-pane workspace), recommendation = Tiles, and m's chip-picker decisions captured in §7. Scope locked for coder gate: Tiles default, Pinned ∪ active ∪ open-work as 'current', 3 tabs (Tiles/Tasks/Events) — Activity deferred, Stale folded into Quiet under Tiles. No code touched. Coder shift only after head's go/no-go.
28 KiB
Dashboard overhaul — Phase 5h design
Status: Phase A (design). No code touched. Awaiting m's go/no-go via head.
Branch: mai/fuller/phase-5h-phase-a-design.
Author: fuller (inventor), 2026-05-26.
Source request, verbatim: "The Dashboard could really use an overhaul as well. Of course we should continue to use unified filters but the dashboard should allow all kinds of views and easily show a helpful overview over my current projects."
§1 — Current state diagnosis
/dashboard today is a 5-card stream renderer. The shape, with row counts derived from the live data:
| Card | What it shows | Sort | Cap |
|---|---|---|---|
| Open tasks | Every open VTODO across all linked CalDAV lists, bucketed Overdue/Today/Tomorrow/Week/No-due | overdue→due asc→prio→summary | 30 |
| Events | Every VEVENT next 7d, grouped by day | start asc | 50 |
| Open issues | Every open Gitea issue across all linked repos | updated_at desc | 30 |
| Recent documents | Every dated item_link in the last 30d |
event_date desc | 30 |
| Stale projects | mai-managed items with quiet repo + 0 open tasks + 0 open issues | stale_days desc | 20 |
Filter strip: tag, mgmt, has chips reusing TreeFilter. 60s cache keyed by encoded filter. ?refresh=1 busts. Empty cards collapse to one-liners when no filter is active. Inline VTODO ✓/✎/× on the Tasks card.
What works (don't throw away)
- Aggregation pipeline (
internal/aggregate.Aggregator) — solid, cached, fan-out workers. Any new shape should ride on the same Todos/Events/Issues/Docs primitives. - Filter parity with
/,/timeline,/calendar,/graph. The chip vocabulary is a real asset; m said keep it. - Cache shape — 60s TTL keyed by filter is fine. Tasks-card writeback already invalidates correctly.
- Inline VTODO writeback is genuinely useful — one ✓ click clears a row without leaving the page.
- Stale card as a "consider archiving" candidate list. Tiny, surfaces a question that's otherwise hidden.
What doesn't fit m's request
- Task-centric, not project-centric. m sees "every open task" / "every open issue" as a giant flat stream. He cannot scan the list and answer "how is project X doing?" without ctrl-F. He explicitly asked for a project overview.
- No notion of "current". Every active project contributes equally. Of 47 active items, m's likely day-to-day focus is ~5-10 (the ones with recent activity + open work). The dashboard buries those signals.
- One view, no switcher. m said "all kinds of views". Today it's the 5-card stream and that's it.
- Pinning is underused. Only 2 items are pinned (
dev,projax) — pinning today has no visible payoff anywhere, so m doesn't bother. A project-centric dashboard would give pins a job. - Stale card surfaces non-actionable noise. Once m sees a project once and decides "not now, not archive", the row stays forever. No snooze, no dismiss. Becomes wallpaper.
- Per-project signals are scattered. To see paliad's status m has to scan five separate cards for paliad rows. Should be one tile.
What's already settled (don't redesign)
- Unified filter chips (
?q,?tag,?mgmt,?has,?public) — preserved verbatim. - Server-rendered Go
html/template+ HTMX. No JS framework. - Mobile + desktop both via the new sidebar (≥768px) / bottom-nav (≤767px) layout.
- No file uploads, no new schema columns without head sign-off, no CLI surface.
§2 — m's mental model: what "current projects" means
Reading the live data (mcp__projax__list_items):
- 47 active items, of which 7 are root areas (
admin,dev,finances,health,home,social,work, plus the syntheticpublicparent). - 14 items are public —
dev.fdbck,dev.flexsiebels,dev.goldi,dev.imagen,dev.mai,dev.mclone,dev.mvoice,dev.otto,dev.mcompete,dev.mlex,dev.upc-kommentar,dev.youpcorg,health.sports.manjin,work.paliad. These are clearly the projects m considers portfolio-worthy — code projects he wants the world to see. - Only 2 items are pinned:
dev(root area) andprojax(this very project). Pinning is structurally there but visually invisible, so it's not used. That's an opportunity, not a constraint — if pins surface clearly, m starts using them. - Activity signal is reachable cheaply: every mai-managed item has a
gitea-repolink → repoupdated_at(already cached for the Stale card). Every linked CalDAV list has VTODO/VEVENT counts (already aggregated). A project is "alive" if any of: open VTODO, open issue, repo touched in last 14d, dated link in last 14d.
So "current projects" isn't a fixed predicate — it's a small ordered subset. A workable definition for v1 is the union of:
- Pinned (m's explicit "this matters now" signal — promote pins to UI-visible),
- Active recently (repo activity OR open VTODO with due ≤ 14d OR dated link in last 14d),
- At risk (any overdue VTODO, or open issues with stale
updated_at).
Everything else — the 30+ projects that are technically active but not on m's plate this week — should be reachable but not on the primary surface. A "show all active" toggle handles them.
Crucially: the definition of "current" is configurable. The chip strip already lets m narrow by tag / mgmt / has-links. The new dashboard adds an implicit "current" prefilter on top of that, with a chip to lift it.
§3 — Candidate shapes
Three viable directions. For each: sketch, signals, view switcher behaviour, filter integration, tradeoffs, cost.
Candidate A — Project Tiles + view switcher
Sketch (desktop, ~1200px main column, 3-column tile grid):
┌─ Dashboard ──────────────────────────────────────────────────────────┐
│ [Tiles] [Tasks] [Events] [Activity] · tag▾ mgmt▾ has▾ · ↻ updated 2m │
│ ◇ pinned + active ○ show all active │
├──────────────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ ★ paliad │ │ ★ projax │ │ ★ youpcorg │ │
│ │ work · live │ │ dev · this │ │ dev · live │ │
│ │ 4 open · 2! │ │ 7 open │ │ 1 open │ │
│ │ • next call │ │ • 5h dash │ │ • 1600+ judg │ │
│ │ 2d ago │ │ now │ │ 4d ago │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ flexsiebels │ │ otto │ │ mai │ │
│ │ dev · live │ │ dev · live │ │ dev · live │ │
│ │ 2 open │ │ 9 open · 3! │ │ 5 open │ │
│ │ • home page │ │ • i64 deploy │ │ • #218 mAi │ │
│ │ 1d ago │ │ 3h ago │ │ 6h ago │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ Quiet (12) ▾ Archived (3) ▾ │
└──────────────────────────────────────────────────────────────────────┘
Per-tile signals (top→bottom):
- Title + pin star (click to toggle). Primary path under title (
work.paliad). Live URL pill ifpublic_live_urlis set. - Counts row:
N open(open VTODOs across linked calendars),M!(overdue), optionalK issues(open Gitea issues). Color: overdue red, otherwise default. Click navigates to/i/<path>/. - Top signal line: the single most-relevant next thing — either the soonest-due open VTODO, or the most-recently-updated open issue, or last commit summary. One line, truncated.
- Last-activity stamp:
2d ago/3h ago/now. Derived from max(repo.updated_at, latest_vtodo_modified, latest_dated_link). - Optional micro-bar at the bottom — colored stripe showing the ratio overdue/today/week tasks. Skip for v1 if cluttered.
View switcher (top tab strip, 4 views, same filter + scope chips apply across all):
- Tiles (default) — the grid above. Project-centric.
- Tasks — the existing Open-tasks card + Events card, full-width. Task-centric, today's view. Same inline ✓/✎/× writeback.
- Events — the existing Events card alone, full-width with bigger day headers. The "what's on my calendar" view.
- Activity — a chronological feed merging recent commits (repo
updated_atper linked repo), closed issues, completed VTODOs, dated docs. "What just happened across my projects." This is genuinely new; deferable to v2 if scope-tight.
Scope chip (next to filter chips, top-row): ◇ pinned + active (default — the "current projects" prefilter) ↔ ○ show all active. Single radio. Lifts the implicit "current" filter so m can browse the long tail.
Filter integration: existing tag / mgmt / has / public chips narrow the project set across all four views. URL stays a single /dashboard?view=tiles&tag=work&scope=current so links bookmark cleanly. Default view (tiles) + default scope (current) elided from URL.
Tradeoffs:
- ✅ Best match for m's explicit ask. "Helpful overview over my current projects" maps almost literally to the tile grid.
- ✅ Pinning gets a visible job. Stars on tiles make the feature legible.
- ✅ View switcher gives m the "all kinds of views" he asked for, without losing today's task-centric value (it's the
Taskstab). - ✅ Per-tile signals tell a story at a glance. Scrolling 8 tiles is faster than scrolling 30 unrelated task rows.
- ❌ Tile signals require a per-project rollup the current
dashboardPayloaddoesn't compute. Moderate aggregation work — group existing TodoRow/IssueRow/repo-updated by item.ID. - ❌ The Activity view is the most novel and the highest risk; can be cut from v1 if needed (3 tabs is fine).
- ❌ Tiles trade information density for legibility. A power user wanting "everything everywhere" has to flip to Tasks view.
Implementation cost: moderate.
dashboard.gogrows a per-project rollup (group existing rows byItem.ID, computeLastActivity = max(repoUpdated, dueSoonest, eventSoonest, docLatest)).- New
dashboardProjectstruct + tiles template partial. - View-switcher = a URL
?view=param routing into one of 4 template partials. Each partial re-uses the existing*_sectiontemplates wherever possible. - Scope chip = one boolean URL param + a
IsCurrent(item, rollup, now)predicate. - Pin toggle = a POST handler that flips
Pinnedand re-renders. The detail page already has the field; surface it on the tile.
Candidate B — Project Rows + signal columns
Sketch (desktop, full-width table):
┌─ Dashboard ──────────────────────────────────────────────────────────────┐
│ [Compact] [Detailed] [Kanban] · tag▾ mgmt▾ has▾ · ◇ pinned + active │
├──────────────────────────────────────────────────────────────────────────┤
│ Project Path Open Over Issues Last Status │
│ ─────────────────────────────────────────────────────────────────────────│
│ ★ paliad work.paliad 4 2 3 2d ago live │
│ ★ projax dev.projax 7 0 1 now dev │
│ ★ youpcorg dev.youpcorg 1 0 2 4d ago live │
│ ★ otto dev.otto 9 3 5 3h ago dev │
│ mai dev.mai 5 1 8 6h ago dev │
│ flexsiebels dev.flex… 2 0 1 1d ago live │
│ fdbck dev.fdbck 0 0 0 12d ago live │
│ ─────────────────────────────────────────────────────────────────────────│
│ Quiet (12) ▾ Archived (3) ▾ │
└──────────────────────────────────────────────────────────────────────────┘
Per-row signals: pin star, title, primary path, open-task count, overdue count, open-issue count, last-activity relative, status pill. Each column sortable (column-header click flips sort, persisted in URL).
View switcher:
- Compact (default) — the table above.
- Detailed — the current 5-card layout, preserved verbatim. "Take me back to the old dashboard."
- Kanban-by-status — three columns:
Active,Stale,At risk(overdue or open-issue-heavy). Cards in each column = same per-row signals condensed.
Filter integration: identical to Candidate A — chips narrow the project set across all three views.
Tradeoffs:
- ✅ Highest information density. m sees ~25 projects without scrolling on a wide screen.
- ✅ Sortable columns are a "spreadsheet" superpower — "show me everything ordered by overdue count desc" is one click.
- ✅ Includes a "back to old layout" via the Detailed view — zero regret for muscle memory.
- ✅ Same per-project rollup work as Candidate A, so impl-cost largely overlaps.
- ❌ Tables feel less like a "dashboard" and more like a report. The visual texture is monotone — no big numbers, no color, no story per row. Less daily-driver, more weekly review.
- ❌ Mobile is uglier — a 6-column table degrades into a tall card list, losing the table's compactness advantage.
- ❌ Kanban view as proposed is shallow — three status columns is just a filter dimension dressed up. Not a real flow.
Implementation cost: moderate, mostly overlapping Candidate A. Sortable headers add ~50 lines of JS (or server-side via URL ?sort=col,dir).
Candidate C — Workspace-style 3-pane
Sketch (desktop, fixed 3-column layout):
┌─ Dashboard · tag▾ mgmt▾ has▾ · ↻ updated 2m ago ────────────────────────┐
│ ┌─ FOCUS ──────────┐ ┌─ TODAY ────────────┐ ┌─ ACTIVITY ───────────────┐│
│ │ ★ paliad │ │ Overdue (2) │ │ otto · i64 deployed 3h ││
│ │ 4 · 2! 2d │ │ • paliad/call 1d │ │ projax · 5h commit 4h ││
│ │ ★ projax │ │ • otto/i65 2d │ │ mai · #218 closed 6h ││
│ │ 7 · 0 now │ │ Today (5) │ │ youpcorg · scrape 1d ││
│ │ ★ youpcorg │ │ • paliad/spec │ │ flex · #45 merged 1d ││
│ │ 1 · 0 4d │ │ • projax/dash │ │ paliad · push 2d ││
│ │ ★ otto │ │ ⋮ │ │ ⋮ ││
│ │ 9 · 3! 3h │ │ Events (3) │ │ ││
│ │ ───────── │ │ • 14:00 paliad-call│ │ ││
│ │ Pinned (4) │ │ • 16:30 sport │ │ ││
│ │ + Pin a project │ │ • Tue 09:00 standup│ │ ││
│ └──────────────────┘ └────────────────────┘ └─────────────────────────┘│
└──────────────────────────────────────────────────────────────────────────┘
Three fixed panes (each independently scrollable):
- FOCUS (left, ~25%) — pinned-only project tiles. Vertical strip. Star-toggle to pin/unpin. The "what matters this week" surface. Empty state has a "+ Pin a project" affordance.
- TODAY (middle, ~40%) — the existing Open-tasks + Events cards condensed, no per-project cards. Overdue → Today → Tomorrow → Events for today/tomorrow.
- ACTIVITY (right, ~35%) — chronological feed: recent commits + issue updates + completed VTODOs + dated docs, newest first. The "what happened recently" view.
View switcher: none. The layout is the layout. Mobile collapses to a vertical stack (FOCUS → TODAY → ACTIVITY).
Filter integration: chips narrow all three panes simultaneously (filter to tag=work and FOCUS shows only pinned work-projects, TODAY shows only work-tagged tasks, ACTIVITY shows only work-tagged events).
Tradeoffs:
- ✅ Strongest "workspace" feel — closer to Linear / Notion than to a stream reader. Most "dashboard-y".
- ✅ FOCUS pane is the most direct expression of "show me my current projects" and forces pinning to do real work.
- ✅ No view-switcher means no decision fatigue — open it, see everything that matters in three panes.
- ❌ No view-switcher also means no answer to "I want a different view" — directly contradicts m's "all kinds of views" ask.
- ❌ Three-pane on desktop only. Mobile becomes a vertical scroll that's not meaningfully different from today's stack. The whole shape is desktop-first.
- ❌ Requires pinning to be useful, but pinning is currently unused. Bootstrap problem — m has to invest before he sees value.
- ❌ Highest implementation cost: three new layouts (pane shells + responsive collapse + per-pane templates) and the brand-new Activity feed.
Implementation cost: large. Three new template surfaces, the Activity aggregator is new code, and the responsive mobile collapse needs care.
§4 — Recommendation: Candidate A (Project Tiles + view switcher)
A best matches m's three signals in order:
- "Helpful overview over my current projects" → the Tiles grid is literally that. One tile per project, scannable, the most-relevant next thing surfaced per tile.
- "All kinds of views" → the view switcher gives 3-4 different shapes (Tiles / Tasks / Events / Activity), all sharing the same filter + scope, all bookmarkable. C has no switcher and B's "Detailed" view is just nostalgia for today's layout.
- "Continue to use unified filters" → same chip strip as today, extended with a
scope=current|allchip that's the only addition. No filter vocabulary changes.
B is the runner-up, and a strong runner-up — its sortable table is genuinely useful for weekly-review, and its "Detailed" tab is a comfortable escape hatch. But the first impression of a table-of-projects on the daily-driver surface is "report," not "dashboard," and m opened the request with the word helpful. Tiles feel more helpful at a glance.
C has the best architectural shape (no mode-switching, everything visible) but flunks the "all kinds of views" ask outright, and depends on pinning that m hasn't adopted. The FOCUS pane idea is good enough that it shows up inside Candidate A's Tiles view as the "pinned-first sort" — we keep the strongest signal from C without paying its cost.
Recommended scope-cut for v1 (if cost is a worry): ship Tiles + Tasks + Events tabs only. Defer Activity to a follow-up phase. That's 3 tabs, the first of which is new and the other two are partial reuses of today's templates.
§5 — Implementation plan (if greenlit)
Files touched
web/dashboard.go- Add
dashboardProjectrollup struct:{Item, OpenTasks, Overdue, OpenIssues, LastActivity, NextSignal, IsLive}. - Add
collectProjectRollups(items, todos, events, issues, repos)— groups the existing rows byItem.ID, computesLastActivity = max(repo.updated_at, last_modified_vtodo, latest_dated_link, last_event), picksNextSignal(soonest-due VTODO summary, else latest issue title, else latest commit summary). - Add
IsCurrent(rollup, now)predicate: pinned OR (open tasks > 0) OR (open issues > 0) OR (LastActivity within 14d) OR (any overdue VTODO). - Extend
handleDashboardto read?view=(tiles | tasks | events | activity, default tiles) and?scope=(current | all, default current). Both elide from URL when default. - Cache key extends to include
view+scopeso/dashboard?view=tasksis a separate cache entry — keeps the 60s TTL meaningful per shape.
- Add
web/templates/dashboard.tmpl— top tab strip + scope chip + per-view partial dispatch.web/templates/dashboard_tiles.tmpl(new) — tile grid + "Quiet (N) ▾" collapsible footer.web/templates/dashboard_section.tmpl— retained as thetasksview's partial (the existing 5-card layout, minus Stale which moves under Quiet).web/templates/dashboard_events.tmpl(new, lightweight) — the existing Events card surface promoted to a full-tab view with bigger day headers.web/static/style.css—.dash-tilesgrid (CSS grid, auto-fill,minmax(280px, 1fr)),.tileshell,.tile-counts,.tile-signal,.tile-stamp. Mobile breakpoint stacks tiles 1-column ≤ 600px, 2-col 600–900px, 3-col ≥ 900px.web/dashboard_pin.go(new) —handleDashboardPinPOSTs?id=<uuid>&pin=true|false, callsStore.SetPin([id], bool), invalidates dashboard cache, re-renders.store/store.go— addSetPin(ctx, ids []string, pinned bool) errorif not already there (mirrorsSetPublic).
Activity view (if v1): web/dashboard_activity.go — merges recent gitea commits (new: lift commit lister out of detail page), recent closed issues, recent completed VTODOs, recent dated docs. Sort by event_time desc. Cap 50. Cache piggybacks on dashboard's 60s.
Commit slicing
- Rollup + tile data model, no UI change. Adds
dashboardProject+collectProjectRollups+ tests against a fixture set of items+todos+issues.view=tilesnot wired yet. - Tiles tab + view switcher chrome.
?view=tiles(default),?view=tasksfalls through to today's layout. Tab strip indashboard.tmpl. CSS for tiles. - Scope chip (
?scope=current|all) +IsCurrentpredicate + "Quiet (N) ▾" expandable footer that lists collapsed projects. - Pin toggle on tile +
handleDashboardPin+ cache invalidation. - Events tab as its own URL —
?view=eventsrenders the events partial standalone. - (Optional, v2-mergeable) Activity tab: aggregator extension + template + tab entry.
- Mobile polish: media-query breakpoints for tile grid, touch-friendly pin star, drawer fits.
- Design.md addendum: a new "Dashboard overhaul (Phase 5h)" section documenting the final shape, the URL contract, and the rollup definition.
Each commit on this branch is independently revertable. The Tasks tab (today's layout) survives the whole sequence — if m hates Tiles, switching to Tasks gets him back his familiar dashboard with zero data loss.
Test coverage
dashboard_rollup_test.go— fixture-driven: feed in items + synthetic todos/issues, assertdashboardProjectfields. Cover edge cases: project with 0 links, project with multiple linked repos, overdue counting.dashboard_view_test.go— request/dashboard?view=tiles,/dashboard?view=tasks,/dashboard?view=events; assert the right template selected + the expected sections present.dashboard_scope_test.go— assertscope=currentfilters out the right items;scope=allincludes them.dashboard_pin_test.go— POST flip → SetPin called → cache invalidated → re-render shows star state.- Existing
dashboard_test.go,dashboard_events_test.go,dashboard_edit_test.gokeep passing unchanged — the Tasks tab is the same surface.
Deploy strategy
Standard projax flow: push to mai/fuller/phase-5h-* worktree-branch as commits land, head merges to main per slice, Gitea webhook → Dockerfile → https://projax.msbls.de/healthz SHA check. The view-switcher default of tiles means the first deploy that lands flips m's daily surface — but the Tasks tab is one click away, so m has a fallback the moment he wants it.
Rollback if it doesn't feel right
Three safety levers, in order of cheapness:
- Change the default tab: flip
viewdefault fromtilestotasksin one line ofhandleDashboard. m sees today's dashboard, the Tiles view still exists for opt-in. - Hide the tab strip: comment out the tab-strip template block. View URL params still work but no UI to switch.
- Revert the merge commits: each phase is a clean merge, so
git revert -m 1 <sha>per slice cleanly unwinds.
A week of dogfood after Phase 5h ships is the natural soak. If m still likes today's dashboard better after 7 days, lever 1.
§6 — Open questions (the 4 worth chip-asking)
These are decisions with 2-4 mutually-exclusive options. The remaining design choices have sensible defaults and can be settled in implementation chat.
- Default tab on
/dashboard— Tiles (new) or Tasks (today's layout preserved)? - "Current" definition — pinned ∪ recently-active (recommended), or activity-only, or pinned-only, or pinned ∪ open-work?
- Activity tab in v1 — ship it now (4th tab), or defer to a follow-up phase (3 tabs at launch)?
- Stale card fate — fold into Quiet section under Tiles, keep as its own card on the Tasks tab, or retire entirely (replaced by the LastActivity stamp on each tile)?
§7 — m's decisions (2026-05-26)
Picks via AskUserQuestion, all four matching the inventor's recommendation:
- Q1 (Default tab): Tiles. New project-tile grid is the landing surface. Tasks tab one click away.
- Q2 (Current = ?): Pinned ∪ recently-active ∪ open-work. Generous union — catches everything plausibly relevant without forcing m to pin first. Quiet long-tail collapses into a "Quiet (N) ▾" fold under the grid.
- Q3 (Activity tab): Defer to v2. Phase 5h ships 3 tabs (Tiles / Tasks / Events). Activity feed is the most novel piece and can land as a follow-up after Tiles has dogfood.
- Q4 (Stale card): Fold into Quiet under Tiles. Per-tile
LastActivitystamp carries the staleness signal; the "consider archiving?" nudge migrates to the Quiet fold's framing. The standalone Stale card retires.
No inventor↔m disagreements; no reasoning notes needed. All four picks tighten the recommended scope to "Tiles-first, 3 tabs, no separate Stale card" — a smaller phase than the doc's optional-Activity v1 sketch.
Scope-locked summary for the coder gate
If head greenlights the coder shift, the brief is:
- Rollup data model +
IsCurrent(rollup, now)= pinned OR (open tasks > 0) OR (open issues > 0) OR (LastActivity within 14d). - Tab strip on
/dashboardwith three tabs: Tiles (default), Tasks (today's 5-card layout minus Stale), Events (events card promoted to its own surface). - Tile grid with per-project signals + pin star + "Quiet (N) ▾" fold containing both
IsCurrent=falseprojects AND the Stale candidates. - URL contract:
?view=tiles|tasks|events(defaulttiles, elided),?scope=current|all(defaultcurrent, elided). Existing chip vocabulary unchanged. - Cache key extends to
(filter, view, scope). Pin toggle invalidates dashboard cache. - Tests cover rollup math + view routing + scope predicate + pin flip.
- Mobile: 1-col ≤ 600px, 2-col 600–900px, 3-col ≥ 900px tile grid; tab strip stays at top.
- Design.md addendum documenting the final shape lands in the same branch.
Activity tab + sortable-table view + multi-pane workspace are explicit non-goals for Phase 5h. Revisit after dogfood if Tiles proves the model.