Wire kanban board component to real boards API #6

Merged
forgejo_admin merged 3 commits from 3-kanban-board-ui into main 2026-03-13 20:37:39 +00:00
Contributor

Summary

Replace the static card list on /boards/[slug] with a full interactive kanban board ported from the playground prototype. Adds drag-and-drop (desktop + mobile), board tabs, note-type accent colors, optimistic API mutations, and a dark theme across the entire app.

Changes

  • src/lib/api.ts -- Add moveItem() function for PATCH /boards/{slug}/items/{id} and update apiFetch to accept RequestInit options
  • src/routes/api/boards/[slug]/items/[id]/+server.ts -- New proxy API route so browser-side code can call the backend move endpoint
  • src/routes/boards/[slug]/+page.server.ts -- Load both board detail AND boards list (for tabs) via Promise.all
  • src/routes/boards/[slug]/+page.svelte -- Full kanban rewrite: desktop HTML5 DnD, touch long-press drag, tap-to-move fallback, optimistic state updates, note-type color badges (plan/phase/issue/todo/project/repo), project/points/labels badges, Svelte 5 runes
  • src/routes/+layout.svelte -- Dark theme (bg-[#0a0a14], border-[#1a1a2e])
  • src/routes/+page.svelte -- Dark theme for root page
  • src/routes/boards/+page.svelte -- Dark theme for boards list
  • src/app.css -- Add overscroll-behavior and font smoothing

Test Plan

  • Navigate to /boards -- dark themed grid of board cards
  • Click a board -- kanban columns render with real items from API
  • Board tabs at top show all boards with item counts; clicking navigates
  • Desktop: drag a card between columns, verify column counts update
  • Mobile: long-press a card (300ms), drag to new column
  • Mobile: tap a card, tap a column header to move it (tap-to-move)
  • Verify PATCH call hits /api/boards/{slug}/items/{id} and persists
  • Cards show note_type badge with correct accent color
  • Cards show project badge, points badge, labels
  • npm run check -- 0 errors
  • npm run lint -- 0 errors
  • npm run build -- succeeds

Review Checklist

  • TypeScript check passes (npm run check -- 0 errors, 0 warnings)
  • ESLint passes (npm run lint -- 0 errors)
  • Production build succeeds (npm run build)
  • No inline <style> blocks -- all styles converted to Tailwind 4 utilities
  • Svelte 5 runes used throughout ($state, $derived, $effect, $props)
  • API proxy route prevents browser from calling internal cluster URL directly
  • Optimistic UI with rollback on API failure
  • Dark theme applied consistently across all pages
  • Closes #3
  • Playground source: html-playground/4-sprint-board/
## Summary Replace the static card list on `/boards/[slug]` with a full interactive kanban board ported from the playground prototype. Adds drag-and-drop (desktop + mobile), board tabs, note-type accent colors, optimistic API mutations, and a dark theme across the entire app. ## Changes - **`src/lib/api.ts`** -- Add `moveItem()` function for `PATCH /boards/{slug}/items/{id}` and update `apiFetch` to accept `RequestInit` options - **`src/routes/api/boards/[slug]/items/[id]/+server.ts`** -- New proxy API route so browser-side code can call the backend move endpoint - **`src/routes/boards/[slug]/+page.server.ts`** -- Load both board detail AND boards list (for tabs) via `Promise.all` - **`src/routes/boards/[slug]/+page.svelte`** -- Full kanban rewrite: desktop HTML5 DnD, touch long-press drag, tap-to-move fallback, optimistic state updates, note-type color badges (plan/phase/issue/todo/project/repo), project/points/labels badges, Svelte 5 runes - **`src/routes/+layout.svelte`** -- Dark theme (bg-[#0a0a14], border-[#1a1a2e]) - **`src/routes/+page.svelte`** -- Dark theme for root page - **`src/routes/boards/+page.svelte`** -- Dark theme for boards list - **`src/app.css`** -- Add overscroll-behavior and font smoothing ## Test Plan - [ ] Navigate to `/boards` -- dark themed grid of board cards - [ ] Click a board -- kanban columns render with real items from API - [ ] Board tabs at top show all boards with item counts; clicking navigates - [ ] Desktop: drag a card between columns, verify column counts update - [ ] Mobile: long-press a card (300ms), drag to new column - [ ] Mobile: tap a card, tap a column header to move it (tap-to-move) - [ ] Verify PATCH call hits `/api/boards/{slug}/items/{id}` and persists - [ ] Cards show note_type badge with correct accent color - [ ] Cards show project badge, points badge, labels - [ ] `npm run check` -- 0 errors - [ ] `npm run lint` -- 0 errors - [ ] `npm run build` -- succeeds ## Review Checklist - [x] TypeScript check passes (`npm run check` -- 0 errors, 0 warnings) - [x] ESLint passes (`npm run lint` -- 0 errors) - [x] Production build succeeds (`npm run build`) - [x] No inline `<style>` blocks -- all styles converted to Tailwind 4 utilities - [x] Svelte 5 runes used throughout ($state, $derived, $effect, $props) - [x] API proxy route prevents browser from calling internal cluster URL directly - [x] Optimistic UI with rollback on API failure - [x] Dark theme applied consistently across all pages ## Related - Closes #3 - Playground source: `html-playground/4-sprint-board/`
Wire kanban board component to real boards API
Some checks failed
ci/woodpecker/pr/woodpecker Pipeline failed
0bb3e5ce0e
Replace the static card list on /boards/[slug] with a full kanban board
featuring drag-and-drop (desktop HTML5 DnD + touch long-press + tap-to-move),
board tabs for navigation between boards, note-type accent colors, and a dark
theme matching the playground prototype.

- Add moveItem() to api.ts for PATCH /boards/{slug}/items/{id}
- Create /api/boards/[slug]/items/[id] proxy route for browser-side calls
- Update server loader to return both board detail and boards list
- Port drag-and-drop logic from playground with optimistic UI updates
- Convert all inline styles to Tailwind 4 utility classes
- Use Svelte 5 runes ($state, $derived, $effect, $props)
- Add card type differentiation (plan/phase/issue/todo/project/repo colors)
- Switch layout, root page, boards list to dark theme (bg-[#0a0a14])

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clean up unused board-tab-active class and static accentColor
Some checks failed
ci/woodpecker/pr/woodpecker Pipeline failed
eae0d87f26
Remove the class:board-tab-active directive that had no corresponding CSS
rule, and change accentColor from $derived to a plain const since it never
varies at runtime.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Contributor

PR #6 Review

BLOCKERS

1. Proxy error response leaks internal cluster URL to browser

In src/routes/api/boards/[slug]/items/[id]/+server.ts (line 26), the catch block forwards the raw error message to the client:

const message = err instanceof Error ? err.message : 'Failed to move item';
throw error(502, message);

The apiFetch function in src/lib/api.ts (line 54) constructs error messages that include the full internal URL:

throw new Error(`API request failed: ${res.status} ${res.statusText} (${url})`);

This means a failed PATCH will return "API request failed: 404 Not Found (http://pal-e-docs.pal-e-docs.svc.cluster.local:8000/boards/...)" directly to the browser. This leaks the internal Kubernetes service name, namespace, and port.

Fix: Replace with a generic message in the proxy catch block:

throw error(502, 'Failed to move item');

2. Proxy does not validate position field

In +server.ts line 22, body.position is passed through without validation:

position: body.position

While column is validated as a non-empty string (line 14-16), position accepts any value -- a string, object, array, or negative number would all pass through. The TypeScript type (position?: number) provides no runtime protection.

Fix: Add validation: position: typeof body.position === 'number' ? body.position : undefined

NITS

1. apiFetch header spread ordering is fragile

const res = await fetch(url, {
    headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
    ...init
});

Because ...init spreads after headers, any caller passing init.headers would silently replace (not merge) the default Accept and Content-Type headers. Currently no caller does this, but this is a latent footgun. Consider merging headers explicitly:

const { headers: extraHeaders, ...rest } = init ?? {};
const res = await fetch(url, {
    headers: { Accept: 'application/json', 'Content-Type': 'application/json', ...extraHeaders },
    ...rest
});

2. app.css uses a raw body style block instead of Tailwind

The requirement is "All Tailwind 4 -- no inline <style> blocks." The body block in src/app.css:

body {
    overscroll-behavior: none;
    -webkit-font-smoothing: antialiased;
}

This is a plain CSS rule, not inline <style> (so it technically passes), but it could use @layer base or Tailwind's @apply to stay idiomatic. Minor -- not blocking.

3. Ghost card has redundant box-shadow in both class and style

The touch ghost element (line 217-218 in the page.svelte) applies shadow-2xl via Tailwind class AND box-shadow: 0 8px 24px rgba(0,0,0,0.5) in the inline style. The inline style wins, making the Tailwind class dead code.

4. --tab-color CSS variable set but never used

In the board tabs (line 239), each tab sets --tab-color: {DEFAULT_COLOR} in its style attribute, but no Tailwind class or CSS rule references var(--tab-color). The playground used it in <style> selectors, but since those were converted to inline styles, the variable is orphaned.

5. Empty onkeydown handlers

Multiple elements have onkeydown={() => {}} (lines 290, 327). These exist to suppress a11y warnings about interactive elements needing keyboard handlers, but they provide no actual keyboard interaction. Consider adding Enter/Space handling for keyboard-only users, or at minimum add an aria-label to communicate the drag-and-drop nature. Non-blocking.

SOP COMPLIANCE

  • Branch named after issue -- 3-kanban-board-ui references Issue #3
  • PR body follows template -- Summary, Changes, Test Plan, Related sections all present
  • Related references plan -- Closes #3 present; playground source referenced
  • No secrets committed -- .env in .gitignore, no credentials in source
  • No scope creep -- all 8 changed files directly serve the kanban board feature
  • No inline <style> blocks -- all styles converted to Tailwind utilities
  • Svelte 5 runes used correctly -- $state, $derived, $effect, $props throughout

VERDICT: NOT APPROVED

Two blockers need fixes before merge:

  1. Internal URL leak in proxy error responses (security)
  2. Unvalidated position field passthrough in proxy (input validation)

The kanban implementation itself is solid -- faithful port from the playground with proper TypeScript typing, optimistic updates with rollback, and clean separation between server and browser code. The five nits above are non-blocking suggestions for follow-up.

## PR #6 Review ### BLOCKERS **1. Proxy error response leaks internal cluster URL to browser** In `src/routes/api/boards/[slug]/items/[id]/+server.ts` (line 26), the catch block forwards the raw error message to the client: ```typescript const message = err instanceof Error ? err.message : 'Failed to move item'; throw error(502, message); ``` The `apiFetch` function in `src/lib/api.ts` (line 54) constructs error messages that include the full internal URL: ```typescript throw new Error(`API request failed: ${res.status} ${res.statusText} (${url})`); ``` This means a failed PATCH will return `"API request failed: 404 Not Found (http://pal-e-docs.pal-e-docs.svc.cluster.local:8000/boards/...)"` directly to the browser. This leaks the internal Kubernetes service name, namespace, and port. **Fix:** Replace with a generic message in the proxy catch block: ```typescript throw error(502, 'Failed to move item'); ``` **2. Proxy does not validate `position` field** In `+server.ts` line 22, `body.position` is passed through without validation: ```typescript position: body.position ``` While `column` is validated as a non-empty string (line 14-16), `position` accepts any value -- a string, object, array, or negative number would all pass through. The TypeScript type (`position?: number`) provides no runtime protection. **Fix:** Add validation: `position: typeof body.position === 'number' ? body.position : undefined` ### NITS **1. `apiFetch` header spread ordering is fragile** ```typescript const res = await fetch(url, { headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, ...init }); ``` Because `...init` spreads after `headers`, any caller passing `init.headers` would silently replace (not merge) the default `Accept` and `Content-Type` headers. Currently no caller does this, but this is a latent footgun. Consider merging headers explicitly: ```typescript const { headers: extraHeaders, ...rest } = init ?? {}; const res = await fetch(url, { headers: { Accept: 'application/json', 'Content-Type': 'application/json', ...extraHeaders }, ...rest }); ``` **2. `app.css` uses a raw `body` style block instead of Tailwind** The requirement is "All Tailwind 4 -- no inline `<style>` blocks." The `body` block in `src/app.css`: ```css body { overscroll-behavior: none; -webkit-font-smoothing: antialiased; } ``` This is a plain CSS rule, not inline `<style>` (so it technically passes), but it could use `@layer base` or Tailwind's `@apply` to stay idiomatic. Minor -- not blocking. **3. Ghost card has redundant `box-shadow` in both class and style** The touch ghost element (line 217-218 in the page.svelte) applies `shadow-2xl` via Tailwind class AND `box-shadow: 0 8px 24px rgba(0,0,0,0.5)` in the inline style. The inline style wins, making the Tailwind class dead code. **4. `--tab-color` CSS variable set but never used** In the board tabs (line 239), each tab sets `--tab-color: {DEFAULT_COLOR}` in its style attribute, but no Tailwind class or CSS rule references `var(--tab-color)`. The playground used it in `<style>` selectors, but since those were converted to inline styles, the variable is orphaned. **5. Empty `onkeydown` handlers** Multiple elements have `onkeydown={() => {}}` (lines 290, 327). These exist to suppress a11y warnings about interactive elements needing keyboard handlers, but they provide no actual keyboard interaction. Consider adding Enter/Space handling for keyboard-only users, or at minimum add an `aria-label` to communicate the drag-and-drop nature. Non-blocking. ### SOP COMPLIANCE - [x] Branch named after issue -- `3-kanban-board-ui` references Issue #3 - [x] PR body follows template -- Summary, Changes, Test Plan, Related sections all present - [x] Related references plan -- `Closes #3` present; playground source referenced - [x] No secrets committed -- `.env` in `.gitignore`, no credentials in source - [x] No scope creep -- all 8 changed files directly serve the kanban board feature - [x] No inline `<style>` blocks -- all styles converted to Tailwind utilities - [x] Svelte 5 runes used correctly -- `$state`, `$derived`, `$effect`, `$props` throughout ### VERDICT: NOT APPROVED Two blockers need fixes before merge: 1. Internal URL leak in proxy error responses (security) 2. Unvalidated `position` field passthrough in proxy (input validation) The kanban implementation itself is solid -- faithful port from the playground with proper TypeScript typing, optimistic updates with rollback, and clean separation between server and browser code. The five nits above are non-blocking suggestions for follow-up.
Fix QA blockers: sanitize error response and validate position field
Some checks failed
ci/woodpecker/pr/woodpecker Pipeline failed
3fd24868fd
Blocker 1: The catch block was forwarding err.message to the browser,
which leaked internal Kubernetes service URLs from apiFetch errors.
Now returns a generic "Failed to update board item" message.

Blocker 2: body.position was passed through without runtime type
checking. Now validates that position is a number (or undefined)
before forwarding, and constructs a clean update object.

Closes #4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
forgejo_admin deleted branch 3-kanban-board-ui 2026-03-13 20:37:39 +00:00
Commenting is not possible because the repository is archived.
No reviewers
No milestone
No project
No assignees
1 participant
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ldraney/pal-e-app!6
No description provided.