feat: plan_snapshot field on ClaudeJob + architecture doc (#23)
* feat: add plan_snapshot field to ClaudeJob schema Nullable String? column on claude_jobs captures the task's implementation_plan at claim time — immutable baseline for drift detection. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: update ClaudeJob lifecycle with plan_snapshot Document state machine snapshot capture/reset, plan_snapshot field rationale, and drift-detection baseline semantics. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: remove duplicate header labels on backlog page Both the product H1 + description in the page header and the "Product Backlog" panel-title in the PBI panel duplicated info already visible in the NavBar. Removed both, keeping the right-aligned action bars (activate/sprint/settings, plus filters/+PBI) intact. PanelNavBar component is unchanged — Stories and Taken panels keep their titles. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8877ea469d
commit
794f7afd2e
6 changed files with 89 additions and 94 deletions
|
|
@ -92,14 +92,8 @@ export default async function ProductBacklogPage({ params, searchParams }: Props
|
|||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Product header */}
|
||||
<div className="px-4 py-3 border-b border-border bg-surface-container-low shrink-0 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="font-medium text-foreground">{product.name}</h1>
|
||||
{product.description && (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">{product.description}</p>
|
||||
)}
|
||||
</div>
|
||||
{/* Product header — actions only; product-naam zit al in NavBar */}
|
||||
<div className="px-4 py-3 border-b border-border bg-surface-container-low shrink-0 flex items-center justify-end">
|
||||
<div className="flex items-center gap-3">
|
||||
{user?.active_product_id !== id && (
|
||||
<ActivateProductButton productId={id} isDemo={isDemo} redirectTo={`/products/${id}`} />
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import { toast } from 'sonner'
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { PanelNavBar } from '@/components/shared/panel-nav-bar'
|
||||
import { useSelectionStore } from '@/stores/selection-store'
|
||||
import { usePlannerStore } from '@/stores/planner-store'
|
||||
import { useBacklogStore } from '@/stores/backlog-store'
|
||||
|
|
@ -330,92 +329,87 @@ export function PbiList({ productId, isDemo }: PbiListProps) {
|
|||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<PanelNavBar
|
||||
title="Product Backlog"
|
||||
actions={
|
||||
<>
|
||||
{filterPriority !== 'all' && (
|
||||
<button
|
||||
onClick={() => setFilterPriority('all')}
|
||||
className="flex items-center gap-1 text-xs text-primary hover:underline"
|
||||
aria-label="Wis prioriteitsfilter"
|
||||
>
|
||||
<Badge className={cn('text-xs', PRIORITY_COLORS[filterPriority])}>
|
||||
{PRIORITY_LABELS[filterPriority]}
|
||||
</Badge>
|
||||
<span>×</span>
|
||||
</button>
|
||||
)}
|
||||
{filterStatus !== 'all' && (
|
||||
<button
|
||||
onClick={() => setFilterStatus('all')}
|
||||
className="flex items-center gap-1 text-xs text-primary hover:underline"
|
||||
aria-label="Wis statusfilter"
|
||||
>
|
||||
<Badge className={cn('text-xs', PBI_STATUS_COLORS[filterStatus])}>
|
||||
{PBI_STATUS_LABELS[filterStatus]}
|
||||
</Badge>
|
||||
<span>×</span>
|
||||
</button>
|
||||
)}
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs">
|
||||
{`Filters${activeFilterCount > 0 ? ` (${activeFilterCount})` : ''}`}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<PopoverContent align="end" className="w-72 space-y-4">
|
||||
<FilterPills
|
||||
label="Sorteren op"
|
||||
options={SORT_OPTIONS}
|
||||
value={sortMode}
|
||||
onChange={setSortMode}
|
||||
/>
|
||||
<FilterPills
|
||||
label="Prioriteit"
|
||||
options={PRIORITY_OPTIONS}
|
||||
value={filterPriority}
|
||||
onChange={setFilterPriority}
|
||||
/>
|
||||
<FilterPills
|
||||
label="Status"
|
||||
options={STATUS_OPTIONS}
|
||||
value={filterStatus}
|
||||
onChange={setFilterStatus}
|
||||
/>
|
||||
<div className="flex justify-end pt-1 border-t border-border">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
disabled={activeFilterCount === 0}
|
||||
onClick={() => {
|
||||
setFilterPriority('all')
|
||||
setFilterStatus('all')
|
||||
setSortMode('priority')
|
||||
}}
|
||||
>
|
||||
Wis filters
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<DemoTooltip show={isDemo}>
|
||||
<div className="flex items-center justify-end gap-2 px-4 py-2 border-b border-border bg-surface-container-low shrink-0">
|
||||
{filterPriority !== 'all' && (
|
||||
<button
|
||||
onClick={() => setFilterPriority('all')}
|
||||
className="flex items-center gap-1 text-xs text-primary hover:underline"
|
||||
aria-label="Wis prioriteitsfilter"
|
||||
>
|
||||
<Badge className={cn('text-xs', PRIORITY_COLORS[filterPriority])}>
|
||||
{PRIORITY_LABELS[filterPriority]}
|
||||
</Badge>
|
||||
<span>×</span>
|
||||
</button>
|
||||
)}
|
||||
{filterStatus !== 'all' && (
|
||||
<button
|
||||
onClick={() => setFilterStatus('all')}
|
||||
className="flex items-center gap-1 text-xs text-primary hover:underline"
|
||||
aria-label="Wis statusfilter"
|
||||
>
|
||||
<Badge className={cn('text-xs', PBI_STATUS_COLORS[filterStatus])}>
|
||||
{PBI_STATUS_LABELS[filterStatus]}
|
||||
</Badge>
|
||||
<span>×</span>
|
||||
</button>
|
||||
)}
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs">
|
||||
{`Filters${activeFilterCount > 0 ? ` (${activeFilterCount})` : ''}`}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<PopoverContent align="end" className="w-72 space-y-4">
|
||||
<FilterPills
|
||||
label="Sorteren op"
|
||||
options={SORT_OPTIONS}
|
||||
value={sortMode}
|
||||
onChange={setSortMode}
|
||||
/>
|
||||
<FilterPills
|
||||
label="Prioriteit"
|
||||
options={PRIORITY_OPTIONS}
|
||||
value={filterPriority}
|
||||
onChange={setFilterPriority}
|
||||
/>
|
||||
<FilterPills
|
||||
label="Status"
|
||||
options={STATUS_OPTIONS}
|
||||
value={filterStatus}
|
||||
onChange={setFilterStatus}
|
||||
/>
|
||||
<div className="flex justify-end pt-1 border-t border-border">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
disabled={isDemo}
|
||||
onClick={() => !isDemo && setDialogState({ mode: 'create', productId, defaultPriority: 2 })}
|
||||
disabled={activeFilterCount === 0}
|
||||
onClick={() => {
|
||||
setFilterPriority('all')
|
||||
setFilterStatus('all')
|
||||
setSortMode('priority')
|
||||
}}
|
||||
>
|
||||
+ PBI
|
||||
Wis filters
|
||||
</Button>
|
||||
</DemoTooltip>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
disabled={isDemo}
|
||||
onClick={() => !isDemo && setDialogState({ mode: 'create', productId, defaultPriority: 2 })}
|
||||
>
|
||||
+ PBI
|
||||
</Button>
|
||||
</DemoTooltip>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{pbis.length === 0 ? (
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 488 KiB After Width: | Height: | Size: 490 KiB |
|
|
@ -1120,12 +1120,15 @@ Developers kunnen vanuit de Task Detail Dialog een lokale Claude Code-sessie ins
|
|||
### State machine
|
||||
|
||||
```
|
||||
QUEUED → CLAIMED → RUNNING → DONE
|
||||
→ FAILED
|
||||
QUEUED → CLAIMED (snapshot capture) → RUNNING → DONE
|
||||
→ FAILED
|
||||
→ CANCELLED (door user)
|
||||
CLAIMED → QUEUED (stale claim cleanup, >30min)
|
||||
CLAIMED → QUEUED (stale claim cleanup, >30min; snapshot gewist)
|
||||
QUEUED → CLAIMED (re-claim na stale reset; snapshot refreshed)
|
||||
```
|
||||
|
||||
**Snapshot-rationale:** bij atomic claim schrijft `wait_for_job` de dan-actuele `task.implementation_plan` naar `claude_jobs.plan_snapshot`. Dit veld blijft bevroren terwijl de job loopt — ook als een gebruiker `update_task_plan` aanroept. Zo kan een toekomstige verify-tool drift detecteren tussen de baseline (snapshot) en de actuele plan. Jobs zonder snapshot (NULL) zijn aangemaakt vóór deze feature en worden als "no baseline" gemarkeerd.
|
||||
|
||||
### ClaudeJob model
|
||||
|
||||
```
|
||||
|
|
@ -1134,6 +1137,7 @@ claude_jobs
|
|||
status: ClaudeJobStatus (QUEUED|CLAIMED|RUNNING|DONE|FAILED|CANCELLED)
|
||||
claimed_by_token_id (FK → api_tokens, nullable)
|
||||
claimed_at, started_at, finished_at
|
||||
plan_snapshot: String? — bevroren snapshot van task.implementation_plan bij claim
|
||||
branch, summary, error
|
||||
@@index([user_id, status])
|
||||
@@index([task_id, status])
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "claude_jobs" ADD COLUMN "plan_snapshot" TEXT;
|
||||
|
|
@ -260,6 +260,7 @@ model ClaudeJob {
|
|||
claimed_at DateTime?
|
||||
started_at DateTime?
|
||||
finished_at DateTime?
|
||||
plan_snapshot String?
|
||||
branch String?
|
||||
summary String?
|
||||
error String?
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue