Skip to content

feat(epics): Kanban board view with side-panel drill-down#8

Merged
DevHDI merged 2 commits into
mainfrom
feature/epic-kanban
May 8, 2026
Merged

feat(epics): Kanban board view with side-panel drill-down#8
DevHDI merged 2 commits into
mainfrom
feature/epic-kanban

Conversation

@DevHDI

@DevHDI DevHDI commented May 8, 2026

Copy link
Copy Markdown
Owner

Summary

Adds the V1 Kanban view for the Epics page that Brian and Evie validated, alongside the existing timeline. Three pieces, all on the same screen.

EpicsKanban

Three-column board: Planning (not-started), In Progress, Done. Each card carries the epic id, title, StatusBadge, {completed}/{total} stories counter, and a SegmentedProgressBar. Layout collapses gracefully on narrow viewports.

EpicStoriesSheet (drill-down)

Click a card → a shadcn Sheet slides in from the right. Two-state internal navigation:

  1. Stories list — header shows the epic header + progress bar, body lists stories with StatusBadge and task counters.
  2. Story detail — clicking a story swaps the body for the existing StoryDetailView (description, acceptance criteria, tasks). A "back to stories" button restores the list.

State resets to the list whenever the targeted epic changes — implemented by tracking the epic id during render (not in an effect), per the React 19 / Compiler guidance.

Toggle in EpicsBrowser

A Timeline / Board toggle appears above the epics list (pattern borrowed from the Stories page). Timeline stays the default, so existing user flows are unchanged. Board mode routes clicks to the Sheet rather than the in-page view: "story" navigation.

Reuse

No new shared primitives. The new components compose:

  • Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription
  • StoryDetailView, StatusBadge, SegmentedProgressBar
  • StaggeredList / StaggeredItem for the animation rhythm
  • Button + Columns3 / GanttChartSquare icons for the toggle

Test plan

  • pnpm test — 151/151 passing (no functional regression — UI-only change)
  • pnpm tsc --noEmit — clean
  • pnpm lint — clean (only the 2 pre-existing TanStack warnings)
  • Manual:
    • Open the Epics page → Timeline shows by default, identical to before
    • Click Board → 3 columns appear, epics dispatched by status
    • Click a card → Sheet opens with the epic header and stories list
    • Click a story → swaps to StoryDetailView, back button restores the list
    • Close Sheet, reopen on a different epic → lands on the list (not the previous story)
    • ESC / overlay click closes the Sheet
    • Keyboard: cards are focusable, Enter / Space activate them

DevHDI added 2 commits May 8, 2026 21:34
Adds a board layout to the Epics page alongside the existing timeline.

- `EpicsKanban` renders three columns — Planning (not-started), In
  Progress, Done — with one card per epic. Each card shows the epic id,
  title, status badge, story counter, and a segmented progress bar.
- `EpicStoriesSheet` is the side-panel drill-down: a shadcn Sheet that
  first lists the epic's stories, then swaps to `StoryDetailView` when
  a story is selected, with a "back to stories" affordance. Internal
  navigation resets whenever the targeted epic changes (derived during
  render rather than via an effect).
- `EpicsBrowser` gains a Timeline / Board toggle on the epics list.
  Timeline keeps the existing in-page navigation; Board opens the
  side-panel sheet on click. Timeline stays the default so existing
  flows are unchanged.

Reuses `Sheet`, `StoryDetailView`, `StatusBadge`, `SegmentedProgressBar`,
and the `StaggeredList` animation primitives, so the new view inherits
the project's visual language without new shared primitives.
Radix Dialog requires a Title to be mounted at all times for the
panel's accessible name. The previous code unmounted SheetTitle when
swapping the sheet body to the story-detail view, leaving the panel
without an accessible name and triggering a Radix warning.

Now a `<SheetTitle className="sr-only">Story {id}: {title}</SheetTitle>`
stays mounted in detail view alongside the visible "Back to stories"
button. The visible heading inside StoryDetailView remains the primary
on-screen title for sighted users; the sr-only one only services screen
readers and Radix's accessibility contract.
@DevHDI DevHDI merged commit bac255c into main May 8, 2026
2 checks passed
@DevHDI DevHDI deleted the feature/epic-kanban branch May 8, 2026 20:19
DevHDI added a commit that referenced this pull request May 13, 2026
* fix(api): handle malformed JSON on /api/revalidate as 400

await request.json() throws SyntaxError on empty bodies, wrong
Content-Type, or truncated payloads. The error was not caught, so the
endpoint returned an unstructured 500 instead of a 400, and bypassed
any error-monitoring middleware that expects structured 4xx
responses.

Wrap the parse in try/catch and return a clear 400 "Invalid JSON".

Addresses finding #8 from bmad-method-ui#5 review.

* fix(actions): revalidate cache only after successful refresh

refreshGitHubRepo called revalidateTag *before* the GitHub fetch.
When the fetch threw (network error, rate limit, 404), the cache was
already invalidated: subsequent renders fetched stale or empty data
with no recovery until a future successful refresh.

Move revalidateTag to the end of the function, after the GitHub fetch
and the prisma.repo.update both succeed. If either step fails, the
existing cached entry remains valid.

Addresses finding #6 from bmad-method-ui#5 review.

* fix(api): rate-limit /api/revalidate per client IP

The endpoint was protected by a shared secret with no rate limit of
its own. If the secret were ever obtained (logs, accidental commit,
compromised CI), an attacker could issue unlimited revalidateTag
calls to drive continuous cache invalidation and elevate upstream
load.

Add a per-IP limit using the existing in-memory checkRateLimit
helper: 30 requests per minute, which comfortably exceeds any
legitimate build/deploy cadence. The IP is read from x-forwarded-for
(first hop) with x-real-ip and a literal "unknown" fallback so a
missing header cannot bypass the bucket.

Defense in depth — the secret remains the primary protection.

Addresses finding #13 from bmad-method-ui#5 review.

* fix(api): move rate-limit post-auth with global key

The per-IP pre-auth limit added in 93023cf trusted the
x-forwarded-for header, which is client-controlled. In the very
threat the limit was meant to mitigate ("secret leaked"), the
attacker can rotate x-forwarded-for on every request and obtain a
fresh bucket each time, neutralising the cap.

Restructure the handler:

1. Validate the shared secret first. timingSafeEqual on SHA-256
   digests is computationally resistant to brute force, so no
   pre-auth quota is needed.
2. Apply a *post-auth* rate limit with a fixed key
   `revalidate:authenticated`. This caps total invalidations across
   all valid-secret holders, so even with a leaked secret an
   attacker is bounded to 30/min regardless of source IP.

Drop the now-unused getClientIp helper.

Addresses follow-up review on 93023cf.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant