kanban-dev-webapp/docs/superpowers/specs/2026-09-21-phase-10-gantt-plan-redesign-design.md
Dave 450aea5974 docs(plan): Phase 10 spec — Gantt redesign for /plan
Redesign the planning page as a project-grouped Gantt:
parent rows are cards (start_date..target_date), indented
children are subtasks (gaining optional start_date and target_date).
No new collections. cards gains an optional assignee text field.
Replaces the Phase 5 Puzzle + Timeline tabs at /plan.

Out of scope: drag-to-reschedule, subtask dates edited on the
card page, connector lines, status/priority filters.
2026-09-21 15:43:40 +02:00

14 KiB

Phase 10 — Gantt plan redesign

Date: 2026-09-21 Status: Draft for review Phase: 10 Depends on: Phase 5 (planning and roadmap view), the existing cards / subtasks schema Supersedes: The Puzzle + Timeline tabs of the Phase 5 /plan page. The data they read (start_date, target_date on cards) is reused; only the view and a small schema add ship here.


1. Purpose

Replace the current /plan screen — its Now/Next/Later grouping and its client-grouped timeline do not work for the developer — with a real Gantt where each row is one of the developer's own work items, indented subtasks sit under it, and every bar represents a duration.

The inspiration is a reference Gantt (dark navy palette, monthly header above weekly ticks, parent bars in a saturated tone with their subtasks in a lighter tint). The reference is treated as layout, colour direction, and shape only — none of its domain language ("Planning / Packaging / Marketing") is carried over. What ships is keyed to the existing projects → cards → subtasks tree.

The result answers two questions the current page answers badly:

  • "How long will this take, and how does it sit against the rest of the work?" — every card bar shows duration.
  • "What does this card actually involve?" — the subtasks under it are visible without leaving the planning page.

2. Data — the only schema add

Two new fields, both optional and idempotent through ensureXxxFields-style helpers:

subtasks.start_date (date, optional) subtasks.target_date (date, optional)

The existing subtasks collection is currently { card, title, done, position, created, updated } and acts as the checklist on the card detail page. The new date fields extend that record so the same row can both be a checklist item and appear on the Gantt when it has dates. A subtask with neither date set is invisible on the Gantt, exactly like the Phase 5 timeline's "undated lane" treatment — except here we count them once per project section instead of giving them a row.

cards.assignee (text, optional, max ~120)

A free-text label rendered above the parent bar. Empty by default; nothing in the rest of the app reads it. The Phase 5 timeline never had this and the reference image's "Bob Johnson / Henry Leblanc" labels read as human assignments, so the field exists to support that look. One developer means it will usually be empty in practice; that is intentional, not underbuilt.

Everything else — cards.start_date, cards.target_date, cards.title, cards.project, the existing subtasks.card relation, the existing (card, position) index — is reused without change.

No new collections. No new rules. The Phase 1 read rules already scope a client manager to their own client's work; that carries over as-is.

3. The new /plan page

3.1 Vertical structure

For every active project, in client-then-project alphabetical order, render one project section with this shape:

┌──────────────────────────────────────────────────────────────────────┐
│ ●  Project Name                                  Client · 12 cards  │  ← section header
├──────────────────────────────────────────────────────────────────────┤
│ MONTHS             January                          February        │  ← month row
│ WEEKS   W1   W2    W3   W4   W1   W2   W3   W4   W1   W2   W3   W4   │  ← week row
├───────────────────────────────────┬──────────────────────────────────┤
│ Card title                        │ ████████                        │  ← card row
│   Subtask one                     │   ████                           │  ← subtask row
│   Subtask two                     │       ████                       │  ← subtask row
│ Card title without dates          │ — not scheduled —                │  ← undated card row
│ Card title                        │ ████                             │  ← card row
├───────────────────────────────────┴──────────────────────────────────┤
│ 2 cards not scheduled · 3 subtasks not scheduled    [Add dates]      │  ← footer
└──────────────────────────────────────────────────────────────────────┘

Two or more project sections stack vertically. A horizontal divider separates them.

3.2 Inside one project section

One flat row array drives both the left label column and the right timeline track, indexed identically. That is the same invariant the existing TimelineView already relies on (one row array, two views) — it guarantees a row's label and its bar cannot drift, and it makes subtask indentation a matter of CSS padding rather than a parallel data structure. The array entries are:

  • { kind: "card", card, datedSubtasks: Subtask[] } — renders the parent bar from card.start_date → card.target_date and one indented subtask bar per dated subtask.
  • { kind: "undated", card } — renders the undated card row. Position below the dated rows within the same project section.

datedSubtasks is pre-filtered at loader time to subtasks where start_date or target_date is set; subtasks without dates are counted into the section footer instead.

3.3 The bars

  • Parent card bar: bg-client-brand with the client's brand colour (already on clients.brand_color, today rendered as the row dot). Rounded ends, same height across all cards (12 px). If the card has no assignee, no label is drawn above the bar.
  • Subtask bars: same colour, opacity 0.5 (a single Tailwind opacity modifier against the parent's coloured background; avoids a second CSS variable per project). Inset to the right of the parent bar by padding-left: 1.5rem.
  • Card bar without dates: not drawn. The label column shows an em-dash and the text "not scheduled".

3.4 Header

The header above the timeline reuses monthTicks and weekTicks from src/lib/roadmap/timeline.ts. Month label sits centred above the weeks it spans. Each W1..W4 segment is one quarter-month. The today marker (vertical line + "Today" pill) is reused unchanged.

3.5 Section header

The project section header is a single row above the timeline header inside the same panel:

  • A 2 px coloured bar at the top edge in the project client's brand colour (this becomes the section's only visual brand cue and removes the need for a separate icon column).
  • The project name in larger type on the left.
  • The client name + active card count on the right.

3.6 Empty and sparse cases

  • No active projects at all — the page shows the existing "No active projects" tile. Same wording as before so it doesn't read as a new feature.
  • No dated cards and no dated subtasks — the page shows a single "Nothing scheduled yet" panel with the same copy as Phase 5 §4.2. We do not invent an undated lane for the Gantt.
  • A section with only undated cards — section still renders, timeline area collapses to a single row per undated card with "not scheduled" text. The section footer counts them.

3.7 Interactions (this phase)

Read-only.

  • Clicking a card bar → opens /p/[client]/[project]/c/[id] (existing).
  • Clicking a subtask bar → opens the parent card; the existing card detail page renders subtasks, and the subtask with start_date/target_date set gets a small "scheduled" indicator next to its checkbox (text only, no Gantt duplicated on the card page).
  • Hovering a bar shows a tooltip with the card title, the client, and the explicit date range ("5 Mar → 19 Mar").
  • Keyboard: every bar is a role="button" with tabIndex=0, focusable. Enter / Space opens the card. The focus ring uses the existing --brand token so it reads against the dark background.

Editing dates happens on the card detail page or the subtask detail panel, as today. Drag-to-reschedule is out of scope for this phase — see §6.

4. Loader

loadRoadmap is replaced by a new loadPlan returning the structure the view needs in one server-side pass:

type PlanSection = {
  project: { id: string; name: string; slug: string };
  client:  { id: string; name: string; slug: string; brandColor: string };
  cards: PlanCard[];
};

type PlanCard = {
  id: string;
  title: string;
  priority: CardPriority;
  startDate: string | null;
  targetDate: string | null;
  assignee: string;
  href: string;
  datedSubtasks: PlanSubtask[];
  undatedSubtaskCount: number;
};

type PlanSubtask = {
  id: string;
  title: string;
  done: boolean;
  startDate: string | null;
  targetDate: string | null;
};

Implementation notes:

  • One PocketBase trip per collection (projects, clients, cards, subtasks), in the same shape as loadRoadmap today.
  • Sort: cards within a project by start_date ASC NULLS LAST, then by target_date ASC NULLS LAST, then by title. Subtasks within a card by position ASC.
  • Scope: read with the caller's own PocketBase token. The Phase 1 read rules already constrain a client manager to their own client's projects.
  • datedSubtasks is filtered to rows where either date is present; the rest are counted.

The existing RoadmapCard type and src/lib/roadmap/band.ts are dropped from the /plan page but kept (used by the dashboard tile if applicable — to confirm before deletion in the plan). The new loader does not need band.

5. Files touched

  • scripts/setup-remote.ts — add ensureSubtaskDateFields (adds start_date, target_date) and ensureCardAssigneeField (adds assignee). Both follow the ensureXxxFields idempotent pattern (Phase 8c background-md, Phase 9a proposal fields).
  • src/lib/roadmap/load.ts — add loadPlan() alongside loadRoadmap, or rename loadRoadmap → loadPlan and update callers if the dashboard tile does not need the legacy band/shape. Choice deferred to the implementation plan.
  • src/app/plan/page.tsx — replace the RoadmapTabs body with GanttPlanView. Keep the TopBar title "Planning".
  • src/components/roadmap/gantt-plan-view.tsx — new. Single section component; does the heavy lifting (row array, row alignment, month/week ticks via existing helpers).
  • src/components/roadmap/project-section.tsx — new. One project's section, including its header.
  • src/components/roadmap/gantt-bar.tsx — new. One bar (card or subtask variant).
  • Tests: src/lib/roadmap/load.test.ts for the loader; tests/plan.test.ts for the page integration.

6. Out of scope

  • Drag-to-reschedule. The Phase 5 audit-log shape (owner moving a client-owned target_date) applies here too; adding drag to a denser widget without the activity log in place would be a regression. A future phase can wire drag with the same audit-log shape as Phase 5 §5.
  • Subtask editing on the Gantt. Dates are edited on the card page or subtask panel, as today.
  • Filtering by client, priority, or status. Not in the request.
  • Connector lines between parent and child bars (the reference Gantt has them; the indented alignment reads cleanly without them, and the dark palette makes 1px rules tricky to get right).
  • Hover-only tooltips that pop out of the bar (we use a native title attribute for now).
  • Removing the legacy RoadmapTabs / TimelineView / PuzzleView / band-view files. If the dashboard still uses them, leave them. If not, the plan may delete them. This keeps the diff small and reversible.

7. Migration

ensureSubtaskDateFields and ensureCardAssigneeField are idempotent. Re-running npm run setup-remote over the live instance adds the fields without affecting existing rows. No backfill: existing rows simply show as unscheduled on the new page, exactly the way Phase 5's "undated lane" handled the same case.

8. Acceptance criteria

  1. The /plan page loads for the owner and shows one section per active project, sorted client-then-project alphabetical.
  2. A client manager sees only their own client's project sections.
  3. Within a section, dated cards render as parent bars coloured with the client's brand colour. Their dated subtasks render as indented lighter-tint bars below them.
  4. A card with an assignee shows that label above its bar; a card without one shows nothing above the bar.
  5. Cards without dates show as "not scheduled" rows in the same section, below the dated rows, and are counted in the section footer.
  6. Subtasks without dates are invisible on the Gantt but counted in the section footer.
  7. The header shows month labels and W1..W4 segments using the existing monthTicks / weekTicks helpers. A vertical "Today" line + pill marks the current date.
  8. Clicking a card or subtask bar opens the existing card detail page.
  9. Every bar is keyboard-focusable and activates on Enter / Space.
  10. With no dated cards or subtasks across any project, the page shows a single "Nothing scheduled yet" panel.
  11. With no active projects at all, the page shows the existing "No active projects" tile.
  12. The schema additions survive a re-run of scripts/setup-remote.ts without duplicating fields.
  13. The existing /p/[client]/[project]/c/[id] page still renders subtasks correctly when a subtask has dates set.
  14. Unit tests cover the new loader shape. Integration tests cover the page rendering against the demo data set.

9. Risks

  • Background colour contrast. Subtasks use opacity-50 against the project's brand colour, which is already opaque. If the brand colour is light (e.g. #ffffff or close to it), the subtask bar may be hard to read. Mitigation: a follow-up phase can clamp luminance or use a derived darker tint per project; not worth blocking Phase 10 on.
  • Wide projects. A project with many dated cards and subtasks becomes a long vertical list. Mitigated by the existing overflow-x-auto on the timeline track; the label column stays the same width. No change in this phase.
  • Subtasks as both checklist and planning. A subtask with done = true but a target_date in the future shows as a completed-but-scheduled row. We draw it with a strikethrough on its title. No rule changes; the user can choose to ignore either signal.