fix(web): real ops board, no fixture data, Guides - #94
Conversation
Stop showing fixture teams/capabilities as live catalogues. Default Operations to a drag-and-drop board that PATCHes real task status. Add Guides for product orientation (Muster vs SoR, empty states, usage).
📝 WalkthroughWalkthroughAdds a Guides route with shell and command-palette navigation, introduces selectable static guide content, connects Operations board drag-and-drop to task PATCH updates, and replaces fixture-backed Teams and Capabilities views with governed empty states. ChangesCompany OS surfaces
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/features/operations/operations-view.tsx (1)
63-96: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winStatus badge/board column loses fidelity for non-canonical statuses.
statusis computed from the narrowedrawStatus(toOperationalState(rawStatus)) instead of the originaltask.status. SinceasTaskStatuscollapses anything outside{backlog, ready, in_progress, review, done}to"backlog", any task actually in states likeblocked,waiting,failed,cancelled, orcompleted/closed/resolved(all explicitly handled bytoOperationalState) will be shown asqueuedand dropped into the Backlog column — misrepresenting its real state in both the board and the status badge used in list view/detail drawer.🐛 Proposed fix — derive the badge status from the original value
- const rawStatus = asTaskStatus(task.status); return { ... - status: toOperationalState(rawStatus), - rawStatus, + status: toOperationalState(task.status), + rawStatus: asTaskStatus(task.status),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/features/operations/operations-view.tsx` around lines 63 - 96, Update taskToBoardItem so the status field passed to toOperationalState is derived from the original task.status rather than the narrowed rawStatus from asTaskStatus. Preserve rawStatus for the canonical raw-status field, while allowing explicitly supported non-canonical values such as blocked, waiting, failed, cancelled, completed, closed, and resolved to retain their operational state in the board and status badge.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/features/guides/guides-view.tsx`:
- Around line 125-133: Add an aria-pressed attribute to each guide button in the
guide list, deriving its boolean value from the same active?.id === guide.id
condition used by className. Preserve the existing click behavior and styling
while exposing the selected guide state to assistive technology.
In `@apps/web/features/operations/operations-view.tsx`:
- Around line 220-311: Add a keyboard- and assistive-technology-operable status
control to each task card in the board rendering near the existing drag
handlers, such as a “Move to…” select or button group. Populate it from
TASK_COLUMNS, invoke the existing moveTask function with the task ID and
selected column ID, and retain the current drag-and-drop behavior.
- Around line 133-144: Prevent overlapping status updates for the same task in
moveTask by tracking task IDs with in-flight mutations and returning early when
that ID is already pending; clear the tracking state in all completion paths.
Also use updateTask.isPending to disable cards’ draggable behavior while a move
is active, providing feedback and preventing re-drag attempts.
---
Outside diff comments:
In `@apps/web/features/operations/operations-view.tsx`:
- Around line 63-96: Update taskToBoardItem so the status field passed to
toOperationalState is derived from the original task.status rather than the
narrowed rawStatus from asTaskStatus. Preserve rawStatus for the canonical
raw-status field, while allowing explicitly supported non-canonical values such
as blocked, waiting, failed, cancelled, completed, closed, and resolved to
retain their operational state in the board and status badge.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a580d492-4012-406e-94cc-6f4f96a6a650
📒 Files selected for processing (13)
apps/web/app/guides/page.tsxapps/web/components/os/company-os-shell.test.tsapps/web/components/os/company-os-shell.tsxapps/web/components/os/os-command-palette.tsxapps/web/features/capabilities/capabilities-view.tsxapps/web/features/guides/guides-view.test.tsapps/web/features/guides/guides-view.tsxapps/web/features/operations/operations-view.test.tsapps/web/features/operations/operations-view.tsxapps/web/features/teams/teams-view.tsxapps/web/lib/api/client.tsapps/web/lib/api/fixtures/fixtures.test.tsapps/web/lib/queries/hooks.ts
| <button | ||
| type="button" | ||
| onClick={() => setActiveId(guide.id)} | ||
| className={cn( | ||
| "flex w-full items-start gap-2 rounded-md px-2 py-2 text-left text-sm", | ||
| active?.id === guide.id | ||
| ? "bg-muted font-semibold text-foreground" | ||
| : "text-muted-foreground hover:bg-muted/60 hover:text-foreground", | ||
| )} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Expose the selected guide state to assistive technology.
The active guide is indicated only through className, so screen readers cannot identify the current selection. Add aria-pressed (or implement tabs with aria-selected) to each guide button.
Proposed fix
<button
type="button"
+ aria-pressed={active?.id === guide.id}
onClick={() => setActiveId(guide.id)}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <button | |
| type="button" | |
| onClick={() => setActiveId(guide.id)} | |
| className={cn( | |
| "flex w-full items-start gap-2 rounded-md px-2 py-2 text-left text-sm", | |
| active?.id === guide.id | |
| ? "bg-muted font-semibold text-foreground" | |
| : "text-muted-foreground hover:bg-muted/60 hover:text-foreground", | |
| )} | |
| <button | |
| type="button" | |
| aria-pressed={active?.id === guide.id} | |
| onClick={() => setActiveId(guide.id)} | |
| className={cn( | |
| "flex w-full items-start gap-2 rounded-md px-2 py-2 text-left text-sm", | |
| active?.id === guide.id | |
| ? "bg-muted font-semibold text-foreground" | |
| : "text-muted-foreground hover:bg-muted/60 hover:text-foreground", | |
| )} |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/features/guides/guides-view.tsx` around lines 125 - 133, Add an
aria-pressed attribute to each guide button in the guide list, deriving its
boolean value from the same active?.id === guide.id condition used by className.
Preserve the existing click behavior and styling while exposing the selected
guide state to assistive technology.
| async function moveTask(id: string, status: TaskStatusId) { | ||
| setMoveError(null); | ||
| const current = items.find((item) => item.id === id); | ||
| if (!current || current.rawStatus === status) return; | ||
| try { | ||
| await updateTask.mutateAsync({ id, status }); | ||
| } catch (error) { | ||
| setMoveError( | ||
| error instanceof Error ? error.message : "Could not update task status.", | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
No guard against overlapping status-update requests for the same task.
Dragging the same card again before a prior updateTask.mutateAsync resolves fires a second concurrent PATCH with no ordering guarantee — the completion order can differ from call order, so a stale response can arrive after a newer one and leave the wrong status persisted. The no-op check on line 136 uses the pre-mutation items snapshot and won't prevent this race.
🔒 Proposed fix — block re-drag while a move is in flight
async function moveTask(id: string, status: TaskStatusId) {
setMoveError(null);
+ if (updateTask.isPending) return;
const current = items.find((item) => item.id === id);
if (!current || current.rawStatus === status) return;Consider also disabling draggable on cards while updateTask.isPending is true so users get visual feedback instead of a silently dropped move.
Per TanStack Query, the order in which mutations are fulfilled may differ from the order of mutate function calls. Community discussion on the same pattern confirms PATCH requests must not overtake each other.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async function moveTask(id: string, status: TaskStatusId) { | |
| setMoveError(null); | |
| const current = items.find((item) => item.id === id); | |
| if (!current || current.rawStatus === status) return; | |
| try { | |
| await updateTask.mutateAsync({ id, status }); | |
| } catch (error) { | |
| setMoveError( | |
| error instanceof Error ? error.message : "Could not update task status.", | |
| ); | |
| } | |
| } | |
| async function moveTask(id: string, status: TaskStatusId) { | |
| setMoveError(null); | |
| if (updateTask.isPending) return; | |
| const current = items.find((item) => item.id === id); | |
| if (!current || current.rawStatus === status) return; | |
| try { | |
| await updateTask.mutateAsync({ id, status }); | |
| } catch (error) { | |
| setMoveError( | |
| error instanceof Error ? error.message : "Could not update task status.", | |
| ); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/features/operations/operations-view.tsx` around lines 133 - 144,
Prevent overlapping status updates for the same task in moveTask by tracking
task IDs with in-flight mutations and returning early when that ID is already
pending; clear the tracking state in all completion paths. Also use
updateTask.isPending to disable cards’ draggable behavior while a move is
active, providing feedback and preventing re-drag attempts.
| {mode === "board" && filtered.length > 0 ? ( | ||
| <div className="overflow-x-auto pb-2"> | ||
| <div className="grid min-w-max grid-cols-5 gap-3"> | ||
| {TASK_COLUMNS.map((column) => { | ||
| const columnItems = filtered.filter( | ||
| (item) => item.rawStatus === column.id, | ||
| ); | ||
| return ( | ||
| <section | ||
| key={column.id} | ||
| aria-label={`${column.label} column`} | ||
| className={cn( | ||
| "w-[17rem] rounded-md border bg-card", | ||
| dragOverColumn === column.id && | ||
| "border-[var(--color-accent)] bg-muted/40", | ||
| )} | ||
| onDragOver={(event) => { | ||
| event.preventDefault(); | ||
| event.dataTransfer.dropEffect = "move"; | ||
| setDragOverColumn(column.id); | ||
| }} | ||
| onDragLeave={() => | ||
| setDragOverColumn((current) => | ||
| current === column.id ? null : current, | ||
| ) | ||
| } | ||
| onDrop={(event) => { | ||
| event.preventDefault(); | ||
| setDragOverColumn(null); | ||
| const id = event.dataTransfer.getData("text/task-id"); | ||
| if (id) void moveTask(id, column.id); | ||
| }} | ||
| > | ||
| <header className="flex items-center gap-2 border-b border-border px-2 py-2"> | ||
| <div className="min-w-0 flex-1"> | ||
| <h2 className="text-xs font-semibold uppercase tracking-[0.06em]"> | ||
| {column.label} | ||
| </h2> | ||
| <p className="text-xs text-muted-foreground"> | ||
| {column.hint} | ||
| </p> | ||
| </div> | ||
| <Badge className="bg-muted text-muted-foreground"> | ||
| {columnItems.length} | ||
| </Badge> | ||
| </header> | ||
| <ul className="min-h-28 space-y-2 p-2"> | ||
| {columnItems.map((item) => ( | ||
| <li key={item.id}> | ||
| <article | ||
| draggable | ||
| onDragStart={(event) => { | ||
| event.dataTransfer.effectAllowed = "move"; | ||
| event.dataTransfer.setData("text/task-id", item.id); | ||
| }} | ||
| onClick={() => setSelectedId(item.id)} | ||
| className={cn( | ||
| "group w-full cursor-grab rounded-md border border-border bg-[var(--color-paper)] p-2 text-left active:cursor-grabbing", | ||
| selectedId === item.id && "border-[var(--color-accent)]", | ||
| )} | ||
| > | ||
| <div className="flex items-start gap-1.5"> | ||
| <GripVertical | ||
| className="mt-0.5 size-3.5 shrink-0 text-muted-foreground opacity-60 group-hover:opacity-100" | ||
| aria-hidden | ||
| /> | ||
| <div className="min-w-0 flex-1"> | ||
| <p className="text-xs font-semibold leading-snug"> | ||
| {item.title} | ||
| </p> | ||
| <div className="mt-1 flex flex-wrap gap-1"> | ||
| <SeverityBadge severity={item.severity} compact /> | ||
| <span className="text-xs text-muted-foreground"> | ||
| {item.systemOfRecord} | ||
| </span> | ||
| </div> | ||
| <p className="mt-1 text-xs text-muted-foreground"> | ||
| {item.ownerName ?? "Unassigned"} ·{" "} | ||
| {relativeTime(item.updatedAt)} | ||
| </p> | ||
| </div> | ||
| </div> | ||
| </article> | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| </section> | ||
| ); | ||
| })} | ||
| </div> | ||
| </div> | ||
| ) : null} |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Drag-and-drop is the only way to change task status — no keyboard-accessible fallback.
The board relies exclusively on native drag-and-drop (draggable, onDragStart/onDrop) to move cards between columns; there's no button, select, or other keyboard/assistive-technology-operable control to change a task's status anywhere in the view (list mode has none either). This blocks a core workflow for keyboard-only and screen-reader users.
Suggest adding a lightweight fallback, e.g. a per-card "Move to…" select or button group calling the same moveTask, so status changes remain possible without drag gestures.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/features/operations/operations-view.tsx` around lines 220 - 311, Add
a keyboard- and assistive-technology-operable status control to each task card
in the board rendering near the existing drag handlers, such as a “Move to…”
select or button group. Populate it from TASK_COLUMNS, invoke the existing
moveTask function with the task ID and selected column ID, and retain the
current drag-and-drop behavior.
Summary
PATCH /api/v1/tasks/:id./guidessection (nav + palette) explaining real vs empty data, SoR boundaries, and how to use Command/Operations/Approvals.Homelab note
Archived 28 synthetic-titled tasks on the private homelab so the queue is not full of e2e leftovers.
Test plan
apps/webunit testsSummary by CodeRabbit
New Features
Updates
Tests