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 (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Product header */}
|
{/* 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-between">
|
<div className="px-4 py-3 border-b border-border bg-surface-container-low shrink-0 flex items-center justify-end">
|
||||||
<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>
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{user?.active_product_id !== id && (
|
{user?.active_product_id !== id && (
|
||||||
<ActivateProductButton productId={id} isDemo={isDemo} redirectTo={`/products/${id}`} />
|
<ActivateProductButton productId={id} isDemo={isDemo} redirectTo={`/products/${id}`} />
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ import { toast } from 'sonner'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||||
import { PanelNavBar } from '@/components/shared/panel-nav-bar'
|
|
||||||
import { useSelectionStore } from '@/stores/selection-store'
|
import { useSelectionStore } from '@/stores/selection-store'
|
||||||
import { usePlannerStore } from '@/stores/planner-store'
|
import { usePlannerStore } from '@/stores/planner-store'
|
||||||
import { useBacklogStore } from '@/stores/backlog-store'
|
import { useBacklogStore } from '@/stores/backlog-store'
|
||||||
|
|
@ -330,92 +329,87 @@ export function PbiList({ productId, isDemo }: PbiListProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<PanelNavBar
|
<div className="flex items-center justify-end gap-2 px-4 py-2 border-b border-border bg-surface-container-low shrink-0">
|
||||||
title="Product Backlog"
|
{filterPriority !== 'all' && (
|
||||||
actions={
|
<button
|
||||||
<>
|
onClick={() => setFilterPriority('all')}
|
||||||
{filterPriority !== 'all' && (
|
className="flex items-center gap-1 text-xs text-primary hover:underline"
|
||||||
<button
|
aria-label="Wis prioriteitsfilter"
|
||||||
onClick={() => setFilterPriority('all')}
|
>
|
||||||
className="flex items-center gap-1 text-xs text-primary hover:underline"
|
<Badge className={cn('text-xs', PRIORITY_COLORS[filterPriority])}>
|
||||||
aria-label="Wis prioriteitsfilter"
|
{PRIORITY_LABELS[filterPriority]}
|
||||||
>
|
</Badge>
|
||||||
<Badge className={cn('text-xs', PRIORITY_COLORS[filterPriority])}>
|
<span>×</span>
|
||||||
{PRIORITY_LABELS[filterPriority]}
|
</button>
|
||||||
</Badge>
|
)}
|
||||||
<span>×</span>
|
{filterStatus !== 'all' && (
|
||||||
</button>
|
<button
|
||||||
)}
|
onClick={() => setFilterStatus('all')}
|
||||||
{filterStatus !== 'all' && (
|
className="flex items-center gap-1 text-xs text-primary hover:underline"
|
||||||
<button
|
aria-label="Wis statusfilter"
|
||||||
onClick={() => setFilterStatus('all')}
|
>
|
||||||
className="flex items-center gap-1 text-xs text-primary hover:underline"
|
<Badge className={cn('text-xs', PBI_STATUS_COLORS[filterStatus])}>
|
||||||
aria-label="Wis statusfilter"
|
{PBI_STATUS_LABELS[filterStatus]}
|
||||||
>
|
</Badge>
|
||||||
<Badge className={cn('text-xs', PBI_STATUS_COLORS[filterStatus])}>
|
<span>×</span>
|
||||||
{PBI_STATUS_LABELS[filterStatus]}
|
</button>
|
||||||
</Badge>
|
)}
|
||||||
<span>×</span>
|
<Popover>
|
||||||
</button>
|
<PopoverTrigger
|
||||||
)}
|
render={
|
||||||
<Popover>
|
<Button variant="outline" size="sm" className="h-7 text-xs">
|
||||||
<PopoverTrigger
|
{`Filters${activeFilterCount > 0 ? ` (${activeFilterCount})` : ''}`}
|
||||||
render={
|
</Button>
|
||||||
<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"
|
||||||
<PopoverContent align="end" className="w-72 space-y-4">
|
options={SORT_OPTIONS}
|
||||||
<FilterPills
|
value={sortMode}
|
||||||
label="Sorteren op"
|
onChange={setSortMode}
|
||||||
options={SORT_OPTIONS}
|
/>
|
||||||
value={sortMode}
|
<FilterPills
|
||||||
onChange={setSortMode}
|
label="Prioriteit"
|
||||||
/>
|
options={PRIORITY_OPTIONS}
|
||||||
<FilterPills
|
value={filterPriority}
|
||||||
label="Prioriteit"
|
onChange={setFilterPriority}
|
||||||
options={PRIORITY_OPTIONS}
|
/>
|
||||||
value={filterPriority}
|
<FilterPills
|
||||||
onChange={setFilterPriority}
|
label="Status"
|
||||||
/>
|
options={STATUS_OPTIONS}
|
||||||
<FilterPills
|
value={filterStatus}
|
||||||
label="Status"
|
onChange={setFilterStatus}
|
||||||
options={STATUS_OPTIONS}
|
/>
|
||||||
value={filterStatus}
|
<div className="flex justify-end pt-1 border-t border-border">
|
||||||
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}>
|
|
||||||
<Button
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs"
|
||||||
disabled={isDemo}
|
disabled={activeFilterCount === 0}
|
||||||
onClick={() => !isDemo && setDialogState({ mode: 'create', productId, defaultPriority: 2 })}
|
onClick={() => {
|
||||||
|
setFilterPriority('all')
|
||||||
|
setFilterStatus('all')
|
||||||
|
setSortMode('priority')
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
+ PBI
|
Wis filters
|
||||||
</Button>
|
</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">
|
<div className="flex-1 overflow-y-auto">
|
||||||
{pbis.length === 0 ? (
|
{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
|
### State machine
|
||||||
|
|
||||||
```
|
```
|
||||||
QUEUED → CLAIMED → RUNNING → DONE
|
QUEUED → CLAIMED (snapshot capture) → RUNNING → DONE
|
||||||
→ FAILED
|
→ FAILED
|
||||||
→ CANCELLED (door user)
|
→ 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
|
### ClaudeJob model
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
@ -1134,6 +1137,7 @@ claude_jobs
|
||||||
status: ClaudeJobStatus (QUEUED|CLAIMED|RUNNING|DONE|FAILED|CANCELLED)
|
status: ClaudeJobStatus (QUEUED|CLAIMED|RUNNING|DONE|FAILED|CANCELLED)
|
||||||
claimed_by_token_id (FK → api_tokens, nullable)
|
claimed_by_token_id (FK → api_tokens, nullable)
|
||||||
claimed_at, started_at, finished_at
|
claimed_at, started_at, finished_at
|
||||||
|
plan_snapshot: String? — bevroren snapshot van task.implementation_plan bij claim
|
||||||
branch, summary, error
|
branch, summary, error
|
||||||
@@index([user_id, status])
|
@@index([user_id, status])
|
||||||
@@index([task_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?
|
claimed_at DateTime?
|
||||||
started_at DateTime?
|
started_at DateTime?
|
||||||
finished_at DateTime?
|
finished_at DateTime?
|
||||||
|
plan_snapshot String?
|
||||||
branch String?
|
branch String?
|
||||||
summary String?
|
summary String?
|
||||||
error String?
|
error String?
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue