feat: establish tokenized kanban design foundation

This commit is contained in:
zenchantlive 2026-02-11 18:38:51 -08:00
parent d82452b89c
commit ce2010fd92
18 changed files with 1544 additions and 162 deletions

View file

@ -4,8 +4,8 @@
{"id":"bb-29x.3","title":"Record parser and realtime performance baseline against PRD targets","description":"Measure parse latency and update propagation using realistic sample sizes and document outcomes.","acceptance_criteria":"Performance report exists with methodology and observed timings.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:18.3210495-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:18.3210495-08:00","labels":["benchmark","perf"],"dependencies":[{"issue_id":"bb-29x.3","depends_on_id":"bb-29x","type":"parent-child","created_at":"2026-02-11T17:12:18.3220949-08:00","created_by":"zenchantlive"},{"issue_id":"bb-29x.3","depends_on_id":"bb-29x.2","type":"blocks","created_at":"2026-02-11T17:12:39.4534943-08:00","created_by":"zenchantlive"}]}
{"id":"bb-29x.4","title":"Document operational runbook and boundary rationale","description":"Write architecture docs covering scanner policy, bd bridge behavior, and consistency guardrails for future maintainers.","acceptance_criteria":"Runbook documents startup, troubleshooting, and boundary rules.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:19.1385778-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:19.1385778-08:00","labels":["docs","runbook"],"dependencies":[{"issue_id":"bb-29x.4","depends_on_id":"bb-29x","type":"parent-child","created_at":"2026-02-11T17:12:19.1402086-08:00","created_by":"zenchantlive"},{"issue_id":"bb-29x.4","depends_on_id":"bb-29x.2","type":"blocks","created_at":"2026-02-11T17:12:39.9591458-08:00","created_by":"zenchantlive"}]}
{"id":"bb-6aj","title":"Project Registry and Multi-Project Scanner","description":"Support multiple Windows project roots using profile-scoped registry storage and safe discovery scanning tuned for developer machines.","acceptance_criteria":"Projects can be added/removed/listed and discovered via scanner with deterministic normalization.","status":"open","priority":0,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:47.7205517-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:11:47.7205517-08:00","labels":["multi-project","scanner"],"dependencies":[{"issue_id":"bb-6aj","depends_on_id":"bb-92d","type":"blocks","created_at":"2026-02-11T17:12:19.6374139-08:00","created_by":"zenchantlive"}]}
{"id":"bb-6aj.1","title":"Persist project registry in %USERPROFILE%\\\\.beadboard\\\\projects.json","description":"Implement read/write management for registry file in user profile path, isolated from repository files and safe for local machine usage.","acceptance_criteria":"Registry file is created lazily and survives app restarts.","status":"in_progress","priority":0,"issue_type":"task","assignee":"agent-a","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:48.5403111-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:43:31.2075726-08:00","labels":["config","registry"],"dependencies":[{"issue_id":"bb-6aj.1","depends_on_id":"bb-6aj","type":"parent-child","created_at":"2026-02-11T17:11:48.5419102-08:00","created_by":"zenchantlive"}]}
{"id":"bb-6aj.2","title":"Implement registry API for add/remove/list operations","description":"Expose robust API endpoints with path validation and normalized identity checks to prevent duplicates.","acceptance_criteria":"API supports add, remove, list and returns clear validation errors.","status":"open","priority":0,"issue_type":"task","assignee":"agent-a","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:49.3542564-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:43:31.7174748-08:00","labels":["api","registry"],"dependencies":[{"issue_id":"bb-6aj.2","depends_on_id":"bb-6aj","type":"parent-child","created_at":"2026-02-11T17:11:49.3558158-08:00","created_by":"zenchantlive"},{"issue_id":"bb-6aj.2","depends_on_id":"bb-6aj.1","type":"blocks","created_at":"2026-02-11T17:12:26.7117348-08:00","created_by":"zenchantlive"}]}
{"id":"bb-6aj.1","title":"Persist project registry in %USERPROFILE%\\\\.beadboard\\\\projects.json","description":"Implement read/write management for registry file in user profile path, isolated from repository files and safe for local machine usage.","acceptance_criteria":"Registry file is created lazily and survives app restarts.","status":"closed","priority":0,"issue_type":"task","assignee":"agent-a","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:48.5403111-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:53:17.2085722-08:00","closed_at":"2026-02-11T17:53:17.2085722-08:00","close_reason":"Implemented %USERPROFILE%/.beadboard/projects.json registry persistence with Windows-safe normalization and dedupe.","labels":["config","registry"],"dependencies":[{"issue_id":"bb-6aj.1","depends_on_id":"bb-6aj","type":"parent-child","created_at":"2026-02-11T17:11:48.5419102-08:00","created_by":"zenchantlive"}]}
{"id":"bb-6aj.2","title":"Implement registry API for add/remove/list operations","description":"Expose robust API endpoints with path validation and normalized identity checks to prevent duplicates.","acceptance_criteria":"API supports add, remove, list and returns clear validation errors.","status":"closed","priority":0,"issue_type":"task","assignee":"agent-a","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:49.3542564-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:53:23.9298353-08:00","closed_at":"2026-02-11T17:53:23.9298353-08:00","close_reason":"Implemented /api/projects GET/POST/DELETE with validation, normalization, and registry integration.","labels":["api","registry"],"dependencies":[{"issue_id":"bb-6aj.2","depends_on_id":"bb-6aj","type":"parent-child","created_at":"2026-02-11T17:11:49.3558158-08:00","created_by":"zenchantlive"},{"issue_id":"bb-6aj.2","depends_on_id":"bb-6aj.1","type":"blocks","created_at":"2026-02-11T17:12:26.7117348-08:00","created_by":"zenchantlive"}]}
{"id":"bb-6aj.3","title":"Build scanner with profile-root default and depth/ignore controls","description":"Scan %USERPROFILE% and user-defined roots for .beads directories with bounded recursion and ignore patterns to protect performance.","acceptance_criteria":"Scanner discovers projects without traversing entire drives by default.","status":"open","priority":0,"issue_type":"task","assignee":"agent-c","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:50.1925005-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:43:32.4095636-08:00","labels":["performance","scanner"],"dependencies":[{"issue_id":"bb-6aj.3","depends_on_id":"bb-6aj","type":"parent-child","created_at":"2026-02-11T17:11:50.1940841-08:00","created_by":"zenchantlive"},{"issue_id":"bb-6aj.3","depends_on_id":"bb-6aj.1","type":"blocks","created_at":"2026-02-11T17:12:27.2225981-08:00","created_by":"zenchantlive"}]}
{"id":"bb-6aj.3.1","title":"Add explicit full-drive scan mode for C:/D: by user action","description":"Provide an opt-in scan mode for full drive enumeration while retaining safe defaults and progress reporting expectations.","acceptance_criteria":"Full-drive scan is only activated explicitly, never by default startup logic.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:51.0244174-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:11:51.0244174-08:00","labels":["optional","scanner"],"dependencies":[{"issue_id":"bb-6aj.3.1","depends_on_id":"bb-6aj.3","type":"parent-child","created_at":"2026-02-11T17:11:51.0259617-08:00","created_by":"zenchantlive"}]}
{"id":"bb-6aj.4","title":"Implement aggregate project issue context model","description":"Define normalized project identity payload attached to every issue for cross-project Kanban, timeline, and session views.","acceptance_criteria":"Aggregated read output always includes stable project metadata.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:51.8518922-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:11:51.8518922-08:00","labels":["aggregation","data-model"],"dependencies":[{"issue_id":"bb-6aj.4","depends_on_id":"bb-6aj","type":"parent-child","created_at":"2026-02-11T17:11:51.8534893-08:00","created_by":"zenchantlive"},{"issue_id":"bb-6aj.4","depends_on_id":"bb-6aj.2","type":"blocks","created_at":"2026-02-11T17:12:27.7270195-08:00","created_by":"zenchantlive"}]}
@ -27,11 +27,11 @@
{"id":"bb-tpc.2","title":"Add debounce/coalescing and transient lock handling for file change bursts","description":"Coalesce rapid updates from agent activity and handle temporary read lock contention without surfacing noisy errors.","acceptance_criteria":"Burst writes produce stable event cadence and no hard failures from temporary locks.","status":"open","priority":0,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:54.315119-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:11:54.315119-08:00","labels":["stability","watcher"],"dependencies":[{"issue_id":"bb-tpc.2","depends_on_id":"bb-tpc","type":"parent-child","created_at":"2026-02-11T17:11:54.3172104-08:00","created_by":"zenchantlive"},{"issue_id":"bb-tpc.2","depends_on_id":"bb-tpc.1","type":"blocks","created_at":"2026-02-11T17:12:28.7308524-08:00","created_by":"zenchantlive"}]}
{"id":"bb-tpc.3","title":"Implement SSE events API endpoint with heartbeat and event IDs","description":"Create SSE route supporting keepalive heartbeats and resumable event consumption patterns for browser clients.","acceptance_criteria":"SSE stream remains alive and clients can reconnect automatically.","status":"open","priority":0,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:55.1518352-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:11:55.1518352-08:00","labels":["api","sse"],"dependencies":[{"issue_id":"bb-tpc.3","depends_on_id":"bb-tpc","type":"parent-child","created_at":"2026-02-11T17:11:55.1533991-08:00","created_by":"zenchantlive"},{"issue_id":"bb-tpc.3","depends_on_id":"bb-tpc.2","type":"blocks","created_at":"2026-02-11T17:12:29.2599782-08:00","created_by":"zenchantlive"}]}
{"id":"bb-tpc.4","title":"Build frontend SSE client with scoped React Query invalidation","description":"Consume server events and invalidate only affected query keys, limiting unnecessary re-fetches in multi-project mode.","acceptance_criteria":"Changed project views refresh while unrelated views remain stable.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:56.0008015-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:11:56.0008015-08:00","labels":["frontend","react-query"],"dependencies":[{"issue_id":"bb-tpc.4","depends_on_id":"bb-tpc","type":"parent-child","created_at":"2026-02-11T17:11:56.0024218-08:00","created_by":"zenchantlive"},{"issue_id":"bb-tpc.4","depends_on_id":"bb-tpc.3","type":"blocks","created_at":"2026-02-11T17:12:29.768818-08:00","created_by":"zenchantlive"}]}
{"id":"bb-trz","title":"Kanban Experience (Baseline Dashboard)","description":"Ship a production-ready Kanban baseline inspired by prototype behavior but backed by real Beads project data and strict typing.","acceptance_criteria":"Users can inspect and filter live Beads issues through stable Kanban workflows.","status":"open","priority":1,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:56.8115491-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:11:56.8115491-08:00","labels":["kanban","ui"],"dependencies":[{"issue_id":"bb-trz","depends_on_id":"bb-92d","type":"blocks","created_at":"2026-02-11T17:12:20.6480287-08:00","created_by":"zenchantlive"}]}
{"id":"bb-trz.1","title":"Implement Kanban column layout for Beads statuses","description":"Render columns for open, in_progress, blocked, deferred, and closed with responsive behavior and clear status counts.","acceptance_criteria":"All statuses map correctly and render with stable ordering.","status":"in_progress","priority":1,"issue_type":"task","assignee":"agent-b","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:57.6278082-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:43:33.1065408-08:00","labels":["columns","kanban"],"dependencies":[{"issue_id":"bb-trz.1","depends_on_id":"bb-trz","type":"parent-child","created_at":"2026-02-11T17:11:57.6288535-08:00","created_by":"zenchantlive"},{"issue_id":"bb-trz.1","depends_on_id":"bb-92d.4","type":"blocks","created_at":"2026-02-11T17:12:30.2796473-08:00","created_by":"zenchantlive"}]}
{"id":"bb-trz.2","title":"Build bead cards with priority/type/labels/assignee/dependency metadata","description":"Design compact cards exposing the most actionable issue metadata while preserving readability at high board density.","acceptance_criteria":"Cards show id, priority, type, labels, assignee, and dependency indicators.","status":"in_progress","priority":1,"issue_type":"task","assignee":"agent-b","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:58.4435327-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:43:33.8093251-08:00","labels":["cards","kanban"],"dependencies":[{"issue_id":"bb-trz.2","depends_on_id":"bb-trz","type":"parent-child","created_at":"2026-02-11T17:11:58.4450798-08:00","created_by":"zenchantlive"},{"issue_id":"bb-trz.2","depends_on_id":"bb-trz.1","type":"blocks","created_at":"2026-02-11T17:12:30.7837277-08:00","created_by":"zenchantlive"}]}
{"id":"bb-trz.3","title":"Implement detail slide-out panel with full issue metadata","description":"Add focused issue detail panel showing description, timestamps, dependencies, and lifecycle fields used by power users.","acceptance_criteria":"Selecting a card opens detail panel with complete issue context.","status":"in_progress","priority":1,"issue_type":"task","assignee":"agent-b","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:59.2746013-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:43:34.4946163-08:00","labels":["details","kanban"],"dependencies":[{"issue_id":"bb-trz.3","depends_on_id":"bb-trz","type":"parent-child","created_at":"2026-02-11T17:11:59.2756402-08:00","created_by":"zenchantlive"},{"issue_id":"bb-trz.3","depends_on_id":"bb-trz.2","type":"blocks","created_at":"2026-02-11T17:12:31.2944-08:00","created_by":"zenchantlive"}]}
{"id":"bb-trz.4","title":"Add search/filter/stats controls for status/type/priority/labels","description":"Provide fast filtering and at-a-glance counts, including critical issue indicators, for daily planning and triage workflows.","acceptance_criteria":"Search and filters apply consistently across board and counts.","status":"in_progress","priority":1,"issue_type":"task","assignee":"agent-b","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:00.0927161-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:43:35.1849967-08:00","labels":["filters","stats"],"dependencies":[{"issue_id":"bb-trz.4","depends_on_id":"bb-trz","type":"parent-child","created_at":"2026-02-11T17:12:00.0942721-08:00","created_by":"zenchantlive"},{"issue_id":"bb-trz.4","depends_on_id":"bb-trz.2","type":"blocks","created_at":"2026-02-11T17:12:31.798413-08:00","created_by":"zenchantlive"}]}
{"id":"bb-trz","title":"Kanban Experience (Baseline Dashboard)","description":"Ship a production-ready Kanban baseline inspired by prototype behavior but backed by real Beads project data and strict typing.","acceptance_criteria":"Users can inspect and filter live Beads issues through stable Kanban workflows.","status":"closed","priority":1,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:56.8115491-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:56:51.4226568-08:00","closed_at":"2026-02-11T17:56:51.4226568-08:00","close_reason":"Kanban epic complete for tracer bullet 1","labels":["kanban","ui"],"dependencies":[{"issue_id":"bb-trz","depends_on_id":"bb-92d","type":"blocks","created_at":"2026-02-11T17:12:20.6480287-08:00","created_by":"zenchantlive"}]}
{"id":"bb-trz.1","title":"Implement Kanban column layout for Beads statuses","description":"Render columns for open, in_progress, blocked, deferred, and closed with responsive behavior and clear status counts.","acceptance_criteria":"All statuses map correctly and render with stable ordering.","status":"closed","priority":1,"issue_type":"task","assignee":"agent-b","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:57.6278082-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:56:50.8105288-08:00","closed_at":"2026-02-11T17:56:50.8105288-08:00","close_reason":"Tracer bullet 1 Kanban baseline implemented and verified","labels":["columns","kanban"],"dependencies":[{"issue_id":"bb-trz.1","depends_on_id":"bb-trz","type":"parent-child","created_at":"2026-02-11T17:11:57.6288535-08:00","created_by":"zenchantlive"},{"issue_id":"bb-trz.1","depends_on_id":"bb-92d.4","type":"blocks","created_at":"2026-02-11T17:12:30.2796473-08:00","created_by":"zenchantlive"}]}
{"id":"bb-trz.2","title":"Build bead cards with priority/type/labels/assignee/dependency metadata","description":"Design compact cards exposing the most actionable issue metadata while preserving readability at high board density.","acceptance_criteria":"Cards show id, priority, type, labels, assignee, and dependency indicators.","status":"closed","priority":1,"issue_type":"task","assignee":"agent-b","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:58.4435327-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:56:50.8141656-08:00","closed_at":"2026-02-11T17:56:50.8141656-08:00","close_reason":"Tracer bullet 1 Kanban baseline implemented and verified","labels":["cards","kanban"],"dependencies":[{"issue_id":"bb-trz.2","depends_on_id":"bb-trz","type":"parent-child","created_at":"2026-02-11T17:11:58.4450798-08:00","created_by":"zenchantlive"},{"issue_id":"bb-trz.2","depends_on_id":"bb-trz.1","type":"blocks","created_at":"2026-02-11T17:12:30.7837277-08:00","created_by":"zenchantlive"}]}
{"id":"bb-trz.3","title":"Implement detail slide-out panel with full issue metadata","description":"Add focused issue detail panel showing description, timestamps, dependencies, and lifecycle fields used by power users.","acceptance_criteria":"Selecting a card opens detail panel with complete issue context.","status":"closed","priority":1,"issue_type":"task","assignee":"agent-b","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:59.2746013-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:56:50.8161639-08:00","closed_at":"2026-02-11T17:56:50.8161639-08:00","close_reason":"Tracer bullet 1 Kanban baseline implemented and verified","labels":["details","kanban"],"dependencies":[{"issue_id":"bb-trz.3","depends_on_id":"bb-trz","type":"parent-child","created_at":"2026-02-11T17:11:59.2756402-08:00","created_by":"zenchantlive"},{"issue_id":"bb-trz.3","depends_on_id":"bb-trz.2","type":"blocks","created_at":"2026-02-11T17:12:31.2944-08:00","created_by":"zenchantlive"}]}
{"id":"bb-trz.4","title":"Add search/filter/stats controls for status/type/priority/labels","description":"Provide fast filtering and at-a-glance counts, including critical issue indicators, for daily planning and triage workflows.","acceptance_criteria":"Search and filters apply consistently across board and counts.","status":"closed","priority":1,"issue_type":"task","assignee":"agent-b","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:00.0927161-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:56:50.8186688-08:00","closed_at":"2026-02-11T17:56:50.8186688-08:00","close_reason":"Tracer bullet 1 Kanban baseline implemented and verified","labels":["filters","stats"],"dependencies":[{"issue_id":"bb-trz.4","depends_on_id":"bb-trz","type":"parent-child","created_at":"2026-02-11T17:12:00.0942721-08:00","created_by":"zenchantlive"},{"issue_id":"bb-trz.4","depends_on_id":"bb-trz.2","type":"blocks","created_at":"2026-02-11T17:12:31.798413-08:00","created_by":"zenchantlive"}]}
{"id":"bb-u6f","title":"Agent Session Views and Metrics","description":"Group work by agent session and actor fields to provide auditability and practical productivity insights for asynchronous coding workflows.","acceptance_criteria":"Session-based summaries and detail views are available per project and aggregate contexts.","status":"open","priority":2,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:12.5083912-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:12.5083912-08:00","labels":["agents","sessions"],"dependencies":[{"issue_id":"bb-u6f","depends_on_id":"bb-tpc","type":"blocks","created_at":"2026-02-11T17:12:23.1727361-08:00","created_by":"zenchantlive"}]}
{"id":"bb-u6f.1","title":"Extract and normalize session identity fields from issue data","description":"Derive session grouping from closed_by_session, assignee, and created_by with robust fallback semantics.","acceptance_criteria":"Issues are consistently assigned to session buckets when data exists.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:13.3239834-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:13.3239834-08:00","labels":["agents","data"],"dependencies":[{"issue_id":"bb-u6f.1","depends_on_id":"bb-u6f","type":"parent-child","created_at":"2026-02-11T17:12:13.3255058-08:00","created_by":"zenchantlive"}]}
{"id":"bb-u6f.2","title":"Build session list and detail views for claimed/completed/open outcomes","description":"Present session-level issue outcomes and navigation for operational review and accountability.","acceptance_criteria":"Users can inspect session summaries and drill into individual session issue sets.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:14.1559358-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:14.1559358-08:00","labels":["agents","ui"],"dependencies":[{"issue_id":"bb-u6f.2","depends_on_id":"bb-u6f","type":"parent-child","created_at":"2026-02-11T17:12:14.157502-08:00","created_by":"zenchantlive"},{"issue_id":"bb-u6f.2","depends_on_id":"bb-u6f.1","type":"blocks","created_at":"2026-02-11T17:12:37.9045555-08:00","created_by":"zenchantlive"}]}

View file

@ -4,6 +4,13 @@ import path from 'node:path';
const nextConfig: NextConfig = {
reactStrictMode: true,
outputFileTracingRoot: path.join(process.cwd()),
webpack(config, { dev }) {
if (dev) {
// Avoid intermittent Windows ENOENT errors from webpack filesystem pack cache.
config.cache = false;
}
return config;
},
};
export default nextConfig;

1170
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,9 +9,10 @@
"start": "next start",
"lint": "next lint",
"typecheck": "tsc --noEmit",
"test": "node --test tests/bootstrap.test.mjs && node --import tsx --test tests/lib/parser.test.ts && node --import tsx --test tests/lib/pathing.test.ts && node --import tsx --test tests/lib/kanban.test.ts && node --import tsx --test tests/lib/read-issues.test.ts && node --test tests/guards/no-direct-jsonl-write.test.mjs"
"test": "node --test tests/bootstrap.test.mjs && node --import tsx --test tests/lib/parser.test.ts && node --import tsx --test tests/lib/pathing.test.ts && node --import tsx --test tests/lib/kanban.test.ts && node --import tsx --test tests/lib/read-issues.test.ts && node --test tests/guards/no-direct-jsonl-write.test.mjs && node --test tests/guards/no-inline-style-in-kanban.test.mjs"
},
"dependencies": {
"framer-motion": "^11.18.2",
"next": "15.5.7",
"react": "19.2.1",
"react-dom": "19.2.1"
@ -20,6 +21,9 @@
"@types/node": "^22.10.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"autoprefixer": "^10.4.24",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.17",
"tsx": "^4.21.0",
"typescript": "^5.7.2"
}

6
postcss.config.js Normal file
View file

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

46
src/app/globals.css Normal file
View file

@ -0,0 +1,46 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--color-bg: #0a111f;
--color-surface: #111c31;
--color-surface-muted: #17263f;
--color-surface-raised: #1b2e4a;
--color-text-strong: #f5f8ff;
--color-text-body: #cfdae8;
--color-text-muted: #8da1bd;
--color-border-soft: rgba(130, 152, 185, 0.24);
--color-border-strong: rgba(167, 188, 218, 0.44);
--status-open: #60a5fa;
--status-progress: #fbbf24;
--status-blocked: #fb7185;
--status-deferred: #94a3b8;
--status-closed: #34d399;
--priority-p0: #f43f5e;
--priority-p1: #f59e0b;
--priority-p2: #38bdf8;
--priority-p3: #94a3b8;
--priority-p4: #64748b;
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
min-height: 100%;
}
body {
background:
radial-gradient(circle at 15% 8%, rgba(56, 189, 248, 0.2), transparent 32%),
radial-gradient(circle at 80% 12%, rgba(167, 139, 250, 0.16), transparent 38%),
linear-gradient(180deg, #060c17 0%, #0a111f 40%, #0d1525 100%);
color: var(--color-text-body);
font-family: 'Segoe UI', Inter, system-ui, sans-serif;
}

View file

@ -1,5 +1,6 @@
import type { Metadata } from 'next';
import type { ReactNode } from 'react';
import './globals.css';
export const metadata: Metadata = {
title: 'BeadBoard',

View file

@ -1,5 +1,7 @@
'use client';
import { AnimatePresence } from 'framer-motion';
import { KANBAN_STATUSES } from '../../lib/kanban';
import type { BeadIssue } from '../../lib/types';
@ -11,26 +13,32 @@ interface KanbanBoardProps {
onSelect: (issue: BeadIssue) => void;
}
const STATUS_META: Record<(typeof KANBAN_STATUSES)[number], { label: string; dot: string }> = {
open: { label: 'Open', dot: 'bg-sky-300' },
in_progress: { label: 'In Progress', dot: 'bg-amber-300' },
blocked: { label: 'Blocked', dot: 'bg-rose-300' },
deferred: { label: 'Deferred', dot: 'bg-slate-300' },
closed: { label: 'Done', dot: 'bg-emerald-300' },
};
export function KanbanBoard({ columns, selectedIssueId, onSelect }: KanbanBoardProps) {
return (
<section
style={{
display: 'grid',
gridTemplateColumns: 'repeat(5, minmax(220px, 1fr))',
gap: '0.8rem',
overflowX: 'auto',
}}
>
<section className="grid min-w-[980px] grid-cols-5 gap-3 xl:min-w-0 xl:grid-cols-5">
{KANBAN_STATUSES.map((status) => (
<div key={status} style={{ background: '#edf2f7', borderRadius: 14, padding: '0.65rem', minHeight: 320 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.55rem' }}>
<strong style={{ fontSize: '0.85rem', color: '#1f2937' }}>{status}</strong>
<span style={{ fontSize: '0.8rem', color: '#475569' }}>{columns[status].length}</span>
<div key={status} className="rounded-2xl border border-border-soft bg-surface-muted/55 p-2.5">
<div className="mb-2 flex items-center justify-between">
<strong className="inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.14em] text-text-body">
<span className={`h-2 w-2 rounded-full ${STATUS_META[status].dot}`} />
{STATUS_META[status].label}
</strong>
<span className="font-mono text-xs text-text-muted">{columns[status].length}</span>
</div>
<div style={{ display: 'grid', gap: '0.55rem' }}>
{columns[status].map((issue) => (
<KanbanCard key={issue.id} issue={issue} selected={selectedIssueId === issue.id} onSelect={onSelect} />
))}
<div className="grid gap-2">
<AnimatePresence initial={false}>
{columns[status].map((issue) => (
<KanbanCard key={issue.id} issue={issue} selected={selectedIssueId === issue.id} onSelect={onSelect} />
))}
</AnimatePresence>
</div>
</div>
))}

View file

@ -1,5 +1,7 @@
'use client';
import { motion } from 'framer-motion';
import type { BeadIssue } from '../../lib/types';
import { Chip } from '../shared/chip';
@ -10,38 +12,53 @@ interface KanbanCardProps {
onSelect: (issue: BeadIssue) => void;
}
function priorityClass(priority: number): string {
switch (priority) {
case 0:
return 'border-rose-300/50 bg-rose-400/20 text-rose-100';
case 1:
return 'border-amber-300/40 bg-amber-400/20 text-amber-100';
case 2:
return 'border-sky-300/40 bg-sky-400/20 text-sky-100';
case 3:
return 'border-slate-300/35 bg-slate-400/20 text-slate-100';
default:
return 'border-slate-400/35 bg-slate-500/20 text-slate-100';
}
}
export function KanbanCard({ issue, selected, onSelect }: KanbanCardProps) {
const selectedClass = selected
? 'border-cyan-300/70 bg-surface-raised/95 shadow-card'
: 'border-border-soft bg-surface/90 hover:border-border-strong hover:bg-surface-raised/90';
return (
<button
<motion.button
layout
transition={{ duration: 0.18, ease: 'easeOut' }}
type="button"
onClick={() => onSelect(issue)}
style={{
width: '100%',
textAlign: 'left',
border: selected ? '2px solid #0f766e' : '1px solid #d7dee8',
borderRadius: 12,
padding: '0.7rem',
background: '#ffffff',
cursor: 'pointer',
}}
className={`w-full cursor-pointer rounded-2xl border px-3 py-2.5 text-left transition ${selectedClass}`}
>
<div style={{ fontSize: '0.74rem', color: '#5e6b7a' }}>{issue.id}</div>
<div style={{ fontWeight: 700, color: '#0f1720', margin: '0.15rem 0 0.5rem' }}>{issue.title}</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.35rem', marginBottom: '0.45rem' }}>
<Chip>P{issue.priority}</Chip>
<div className="font-mono text-[11px] text-text-muted">{issue.id}</div>
<div className="mt-1 text-sm font-semibold leading-5 text-text-strong">{issue.title}</div>
<div className="mt-2 flex flex-wrap gap-1.5">
<span className={`inline-flex items-center rounded-full border px-2 py-1 font-mono text-[11px] font-semibold ${priorityClass(issue.priority)}`}>
P{issue.priority}
</span>
<Chip>{issue.issue_type}</Chip>
<Chip>deps {issue.dependencies.length}</Chip>
<Chip tone="status">deps {issue.dependencies.length}</Chip>
</div>
<div style={{ fontSize: '0.8rem', color: '#314152' }}>
<div className="mt-2 truncate font-mono text-xs text-cyan-100/80">
{issue.assignee ? `@${issue.assignee}` : 'unassigned'}
</div>
{issue.labels.length > 0 ? (
<div style={{ marginTop: '0.45rem', display: 'flex', flexWrap: 'wrap', gap: '0.35rem' }}>
<div className="mt-2 flex flex-wrap gap-1.5">
{issue.labels.slice(0, 3).map((label) => (
<Chip key={`${issue.id}-${label}`}>#{label}</Chip>
))}
</div>
) : null}
</button>
</motion.button>
);
}

View file

@ -1,5 +1,7 @@
'use client';
import { motion } from 'framer-motion';
import type { KanbanFilterOptions, KanbanStats } from '../../lib/kanban';
import { StatPill } from '../shared/stat-pill';
@ -11,49 +13,63 @@ interface KanbanControlsProps {
}
export function KanbanControls({ filters, stats, onFiltersChange }: KanbanControlsProps) {
const inputClass =
'rounded-xl border border-border-soft bg-surface-muted/65 px-3 py-2 text-sm text-text-strong outline-none transition placeholder:text-text-muted focus:border-cyan-300/60 focus:ring-2 focus:ring-cyan-300/25';
return (
<section style={{ display: 'grid', gap: '0.75rem' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.6rem' }}>
<section className="grid gap-3">
<motion.div layout className="flex flex-wrap gap-2.5">
<input
type="search"
value={filters.query ?? ''}
onChange={(event) => onFiltersChange({ ...filters, query: event.target.value })}
placeholder="Search by id/title/labels"
style={{ flex: 1, minWidth: 260, borderRadius: 10, border: '1px solid #cbd5e1', padding: '0.5rem 0.6rem' }}
className={`${inputClass} min-w-60 flex-1`}
/>
<input
type="text"
<select
value={filters.type ?? ''}
onChange={(event) => onFiltersChange({ ...filters, type: event.target.value })}
placeholder="Type (task/bug/feature)"
style={{ width: 190, borderRadius: 10, border: '1px solid #cbd5e1', padding: '0.5rem 0.6rem' }}
/>
<input
type="number"
min={0}
max={4}
className={`${inputClass} w-40`}
aria-label="Type filter"
>
<option value="">All types</option>
<option value="task">Task</option>
<option value="bug">Bug</option>
<option value="feature">Feature</option>
<option value="epic">Epic</option>
<option value="chore">Chore</option>
</select>
<select
value={filters.priority ?? ''}
onChange={(event) => onFiltersChange({ ...filters, priority: event.target.value })}
placeholder="Priority"
style={{ width: 110, borderRadius: 10, border: '1px solid #cbd5e1', padding: '0.5rem 0.6rem' }}
/>
<label style={{ display: 'inline-flex', alignItems: 'center', gap: '0.45rem', color: '#334155', fontSize: '0.9rem' }}>
className={`${inputClass} w-32`}
aria-label="Priority filter"
>
<option value="">All priorities</option>
<option value="0">P0</option>
<option value="1">P1</option>
<option value="2">P2</option>
<option value="3">P3</option>
<option value="4">P4</option>
</select>
<label className="inline-flex items-center gap-2 rounded-xl border border-border-soft bg-surface-muted/60 px-3 py-2 text-sm text-text-body">
<input
type="checkbox"
checked={filters.showClosed ?? false}
onChange={(event) => onFiltersChange({ ...filters, showClosed: event.target.checked })}
className="h-4 w-4 accent-cyan-400"
/>
Show closed
</label>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.6rem' }}>
</motion.div>
<motion.div layout className="flex flex-wrap gap-2">
<StatPill label="Total" value={stats.total} />
<StatPill label="Open" value={stats.open} />
<StatPill label="Active" value={stats.active} />
<StatPill label="Blocked" value={stats.blocked} />
<StatPill label="Done" value={stats.done} />
<StatPill label="P0" value={stats.p0} />
</div>
<StatPill label="P0" value={stats.p0} tone={stats.p0 > 0 ? 'critical' : 'default'} />
</motion.div>
</section>
);
}

View file

@ -1,5 +1,7 @@
'use client';
import { AnimatePresence, motion } from 'framer-motion';
import type { BeadIssue } from '../../lib/types';
import { Chip } from '../shared/chip';
@ -9,42 +11,66 @@ interface KanbanDetailProps {
}
export function KanbanDetail({ issue }: KanbanDetailProps) {
if (!issue) {
return (
<aside style={{ border: '1px solid #d7dee8', borderRadius: 14, padding: '0.9rem', background: '#ffffff' }}>
<strong style={{ color: '#0f1720' }}>Details</strong>
<p style={{ color: '#475569' }}>Select a card to inspect full issue details.</p>
</aside>
);
}
return (
<aside style={{ border: '1px solid #d7dee8', borderRadius: 14, padding: '0.9rem', background: '#ffffff' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.5rem', alignItems: 'start' }}>
<div>
<div style={{ fontSize: '0.76rem', color: '#475569' }}>{issue.id}</div>
<h2 style={{ margin: '0.1rem 0 0.3rem', fontSize: '1.2rem', color: '#0f1720' }}>{issue.title}</h2>
</div>
<Chip>{issue.status}</Chip>
</div>
{issue.description ? <p style={{ color: '#334155' }}>{issue.description}</p> : null}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.4rem', marginBottom: '0.5rem' }}>
<Chip>priority {issue.priority}</Chip>
<Chip>{issue.issue_type}</Chip>
<Chip>{issue.assignee ? `@${issue.assignee}` : 'unassigned'}</Chip>
<Chip>{issue.dependencies.length} dependencies</Chip>
</div>
<div style={{ fontSize: '0.84rem', color: '#334155' }}>
<div><strong>Created:</strong> {issue.created_at || '-'}</div>
<div><strong>Updated:</strong> {issue.updated_at || '-'}</div>
</div>
{issue.labels.length > 0 ? (
<div style={{ marginTop: '0.6rem', display: 'flex', flexWrap: 'wrap', gap: '0.35rem' }}>
{issue.labels.map((label) => (
<Chip key={`${issue.id}-${label}`}>#{label}</Chip>
))}
</div>
) : null}
</aside>
<AnimatePresence mode="wait" initial={false}>
{issue ? (
<motion.aside
key={issue.id}
initial={{ opacity: 0, x: 24 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 24 }}
transition={{ duration: 0.2, ease: 'easeOut' }}
className="rounded-2xl border border-border-soft bg-surface/90 p-4 shadow-panel"
>
<div className="flex items-start justify-between gap-2">
<div>
<div className="font-mono text-xs text-text-muted">{issue.id}</div>
<h2 className="mt-1 text-xl font-semibold text-text-strong">{issue.title}</h2>
</div>
<Chip tone="status">{issue.status}</Chip>
</div>
{issue.description ? <p className="mt-3 text-sm leading-6 text-text-body">{issue.description}</p> : null}
<div className="mt-3 flex flex-wrap gap-1.5">
<Chip tone="priority">priority {issue.priority}</Chip>
<Chip>{issue.issue_type}</Chip>
<Chip>{issue.assignee ? `@${issue.assignee}` : 'unassigned'}</Chip>
<Chip>{issue.dependencies.length} dependencies</Chip>
</div>
<dl className="mt-4 grid gap-1.5 text-sm text-text-body">
<div>
<dt className="inline font-semibold text-text-strong">Created:</dt>{' '}
<dd className="inline">{issue.created_at || '-'}</dd>
</div>
<div>
<dt className="inline font-semibold text-text-strong">Updated:</dt>{' '}
<dd className="inline">{issue.updated_at || '-'}</dd>
</div>
<div>
<dt className="inline font-semibold text-text-strong">Closed:</dt>{' '}
<dd className="inline">{issue.closed_at || '-'}</dd>
</div>
</dl>
{issue.labels.length > 0 ? (
<div className="mt-4 flex flex-wrap gap-1.5">
{issue.labels.map((label) => (
<Chip key={`${issue.id}-${label}`}>#{label}</Chip>
))}
</div>
) : null}
</motion.aside>
) : (
<motion.aside
key="empty-detail"
initial={{ opacity: 0, x: 12 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 12 }}
transition={{ duration: 0.18, ease: 'easeOut' }}
className="rounded-2xl border border-border-soft bg-surface/80 p-4"
>
<strong className="text-text-strong">Details</strong>
<p className="mt-1 text-sm text-text-muted">Select a card to inspect full issue details.</p>
</motion.aside>
)}
</AnimatePresence>
);
}

View file

@ -1,5 +1,6 @@
'use client';
import { motion } from 'framer-motion';
import { useMemo, useState } from 'react';
import type { KanbanFilterOptions } from '../../lib/kanban';
@ -33,27 +34,24 @@ export function KanbanPage({ issues }: KanbanPageProps) {
);
return (
<main
style={{
minHeight: '100vh',
padding: '1.2rem',
background: 'linear-gradient(140deg, #f8fafc, #edf3f7 50%, #f8fbf9)',
color: '#0f1720',
fontFamily: 'Segoe UI, system-ui, sans-serif',
}}
>
<header style={{ marginBottom: '0.9rem' }}>
<h1 style={{ margin: 0, fontSize: '1.9rem' }}>BeadBoard Kanban</h1>
<p style={{ margin: '0.35rem 0 0', color: '#475569' }}>Tracer Bullet 1 baseline from live `.beads/issues.jsonl`</p>
<main className="mx-auto min-h-screen max-w-[1680px] px-4 py-4 sm:px-6 sm:py-6">
<header className="mb-4 rounded-2xl border border-border-soft bg-surface/70 px-4 py-4 backdrop-blur md:px-5">
<p className="font-mono text-xs uppercase tracking-[0.14em] text-cyan-200/80">BeadBoard</p>
<h1 className="mt-1 text-2xl font-semibold text-text-strong sm:text-3xl">Kanban Dashboard</h1>
<p className="mt-2 text-sm text-text-muted">Tracer Bullet 1 from live `.beads/issues.jsonl` on Windows-native paths.</p>
</header>
<KanbanControls filters={filters} stats={stats} onFiltersChange={setFilters} />
<section style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) 320px', gap: '0.9rem', marginTop: '0.9rem' }}>
<KanbanBoard
columns={columns}
selectedIssueId={selectedIssue?.id ?? null}
onSelect={(issue) => setSelectedIssueId(issue.id)}
/>
<KanbanDetail issue={selectedIssue} />
<section className="mt-3 grid grid-cols-1 gap-3 xl:grid-cols-[minmax(0,1fr)_360px]">
<motion.div layout className="overflow-x-auto rounded-2xl border border-border-soft bg-surface/55 p-2">
<KanbanBoard
columns={columns}
selectedIssueId={selectedIssue?.id ?? null}
onSelect={(issue) => setSelectedIssueId(issue.id)}
/>
</motion.div>
<div className="xl:sticky xl:top-4 xl:self-start">
<KanbanDetail issue={selectedIssue} />
</div>
</section>
</main>
);

View file

@ -2,21 +2,18 @@ import type { ReactNode } from 'react';
interface ChipProps {
children: ReactNode;
tone?: 'default' | 'status' | 'priority';
}
export function Chip({ children }: ChipProps) {
const CHIP_TONE_CLASS: Record<NonNullable<ChipProps['tone']>, string> = {
default: 'border-border-soft bg-surface-muted/70 text-text-body',
status: 'border-cyan-300/25 bg-cyan-400/15 text-cyan-100',
priority: 'border-amber-300/25 bg-amber-400/15 text-amber-100',
};
export function Chip({ children, tone = 'default' }: ChipProps) {
return (
<span
style={{
border: '1px solid #d7dee8',
borderRadius: 999,
padding: '0.2rem 0.55rem',
fontSize: '0.75rem',
fontWeight: 600,
color: '#1f2a38',
background: '#f7fafc',
}}
>
<span className={`inline-flex items-center rounded-full border px-2 py-1 text-[11px] font-semibold ${CHIP_TONE_CLASS[tone]}`}>
{children}
</span>
);

View file

@ -1,21 +1,16 @@
interface StatPillProps {
label: string;
value: number;
tone?: 'default' | 'critical';
}
export function StatPill({ label, value }: StatPillProps) {
export function StatPill({ label, value, tone = 'default' }: StatPillProps) {
const valueToneClass = tone === 'critical' ? 'text-rose-300' : 'text-text-strong';
return (
<div
style={{
border: '1px solid #d7dee8',
borderRadius: 12,
padding: '0.6rem 0.8rem',
background: '#ffffff',
minWidth: 90,
}}
>
<div style={{ fontSize: '0.7rem', textTransform: 'uppercase', letterSpacing: '0.05em', color: '#5e6b7a' }}>{label}</div>
<div style={{ fontSize: '1.05rem', fontWeight: 700, color: '#0f1720' }}>{value}</div>
<div className="min-w-20 rounded-xl border border-border-soft bg-surface-muted/65 px-3 py-2">
<div className="font-mono text-[10px] uppercase tracking-[0.16em] text-text-muted">{label}</div>
<div className={`mt-0.5 text-lg font-semibold ${valueToneClass}`}>{value}</div>
</div>
);
}

View file

@ -10,24 +10,34 @@ export interface ReadIssuesOptions {
includeTombstones?: boolean;
}
export function resolveIssuesJsonlPathCandidates(projectRoot: string = process.cwd()): string[] {
const baseDir = path.resolve(projectRoot, '.beads');
const primary = canonicalizeWindowsPath(path.join(baseDir, 'issues.jsonl'));
const fallback = canonicalizeWindowsPath(path.join(baseDir, 'issues.jsonl.new'));
return [primary, fallback];
}
export function resolveIssuesJsonlPath(projectRoot: string = process.cwd()): string {
const absolute = path.resolve(projectRoot, '.beads', 'issues.jsonl');
return canonicalizeWindowsPath(absolute);
return resolveIssuesJsonlPathCandidates(projectRoot)[0];
}
export async function readIssuesFromDisk(options: ReadIssuesOptions = {}): Promise<BeadIssue[]> {
const issuesPath = resolveIssuesJsonlPath(options.projectRoot);
const candidates = resolveIssuesJsonlPathCandidates(options.projectRoot);
try {
const jsonl = await fs.readFile(issuesPath, 'utf8');
return parseIssuesJsonl(jsonl, {
includeTombstones: options.includeTombstones ?? false,
});
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return [];
for (const issuesPath of candidates) {
try {
const jsonl = await fs.readFile(issuesPath, 'utf8');
return parseIssuesJsonl(jsonl, {
includeTombstones: options.includeTombstones ?? false,
});
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
continue;
}
throw error;
}
throw error;
}
return [];
}

39
tailwind.config.ts Normal file
View file

@ -0,0 +1,39 @@
import type { Config } from 'tailwindcss';
const config: Config = {
content: ['./src/**/*.{ts,tsx}'],
theme: {
extend: {
fontFamily: {
ui: ['Segoe UI', 'Inter', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'Consolas', 'monospace'],
},
colors: {
bg: 'var(--color-bg)',
surface: {
DEFAULT: 'var(--color-surface)',
muted: 'var(--color-surface-muted)',
raised: 'var(--color-surface-raised)',
},
text: {
strong: 'var(--color-text-strong)',
body: 'var(--color-text-body)',
muted: 'var(--color-text-muted)',
},
border: {
soft: 'var(--color-border-soft)',
strong: 'var(--color-border-strong)',
},
},
boxShadow: {
card: '0 6px 18px rgba(15, 23, 42, 0.08)',
panel: '0 18px 42px rgba(15, 23, 42, 0.2)',
},
borderRadius: {
xl2: '1rem',
},
},
},
};
export default config;

View file

@ -0,0 +1,34 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import path from 'node:path';
const ROOT = process.cwd();
const TARGET_DIRS = ['src/components/kanban', 'src/components/shared'];
const INLINE_STYLE_PATTERN = /\bstyle\s*=\s*\{\s*\{/m;
async function collectTsxFiles(relativeDir) {
const absoluteDir = path.join(ROOT, relativeDir);
const entries = await fs.readdir(absoluteDir, { withFileTypes: true });
return entries
.filter((entry) => entry.isFile() && entry.name.endsWith('.tsx'))
.map((entry) => path.join(absoluteDir, entry.name));
}
test('kanban and shared components do not use inline style objects', async () => {
const files = (await Promise.all(TARGET_DIRS.map(collectTsxFiles))).flat();
const offenders = [];
for (const filePath of files) {
const content = await fs.readFile(filePath, 'utf8');
if (INLINE_STYLE_PATTERN.test(content)) {
offenders.push(path.relative(ROOT, filePath));
}
}
assert.deepEqual(
offenders,
[],
`Inline style objects found in: ${offenders.join(', ')}`,
);
});

View file

@ -4,7 +4,7 @@ import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { readIssuesFromDisk, resolveIssuesJsonlPath } from '../../src/lib/read-issues';
import { readIssuesFromDisk, resolveIssuesJsonlPath, resolveIssuesJsonlPathCandidates } from '../../src/lib/read-issues';
import { sameWindowsPath } from '../../src/lib/pathing';
test('resolveIssuesJsonlPath appends .beads/issues.jsonl using windows-safe pathing', () => {
@ -12,6 +12,12 @@ test('resolveIssuesJsonlPath appends .beads/issues.jsonl using windows-safe path
assert.equal(sameWindowsPath(resolved, 'C:/Repo/Project/.beads/issues.jsonl'), true);
});
test('resolveIssuesJsonlPathCandidates includes .jsonl and .jsonl.new fallback paths', () => {
const [primary, fallback] = resolveIssuesJsonlPathCandidates('C:/Repo/Project');
assert.equal(sameWindowsPath(primary, 'C:/Repo/Project/.beads/issues.jsonl'), true);
assert.equal(sameWindowsPath(fallback, 'C:/Repo/Project/.beads/issues.jsonl.new'), true);
});
test('readIssuesFromDisk parses JSONL issues from disk', async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'beadboard-read-'));
const beadsDir = path.join(root, '.beads');
@ -39,3 +45,19 @@ test('readIssuesFromDisk returns empty list when issues file does not exist', as
const issues = await readIssuesFromDisk({ projectRoot: root });
assert.deepEqual(issues, []);
});
test('readIssuesFromDisk falls back to issues.jsonl.new when issues.jsonl is missing', async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'beadboard-read-fallback-'));
const beadsDir = path.join(root, '.beads');
const fallbackPath = path.join(beadsDir, 'issues.jsonl.new');
await fs.mkdir(beadsDir, { recursive: true });
await fs.writeFile(
fallbackPath,
JSON.stringify({ id: 'bb-fallback', title: 'From fallback', status: 'open', priority: 2, issue_type: 'task' }),
'utf8',
);
const issues = await readIssuesFromDisk({ projectRoot: root });
assert.equal(issues.length, 1);
assert.equal(issues[0].id, 'bb-fallback');
});