Sprint: Verbeteren debug mode (#179)

* feat(PBI-49): add debugProps helper + Vitest test

Adds lib/debug.ts with debugProps(id, component, file) that returns
data-debug-id and data-debug-label attrs in dev mode, empty object in
production. Adds __tests__/lib/debug.test.ts covering both modes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(PBI-49): add debug-id pattern doc + CLAUDE.md reference

Adds docs/patterns/debug-id.md documenting the named-component boundary
rule (6 punten), helper-voorbeeld, skip-criteria en motivatie voor
handmatige pad-argumenten. Voegt verwijzing toe aan CLAUDE.md
patterns-tabel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(PBI-49): migrate 17 shared/ components to debugProps helper

Replace hardcoded data-debug-id + data-debug-label attribute pairs with
{...debugProps(id, component, file)} spread in all 17 components/shared/
files. Existing debug-ids preserved unchanged.

* feat(PBI-49): add debugProps to backlog/, sprint/, solo/ components

* feat(PBI-49): add debugProps to jobs/ + ideas/ components

* feat(PBI-49): add debugProps to products/ + settings/ + notifications/ components

* feat(PBI-49): add debugProps to admin/ + dashboard/ + dialogs/ + mobile/ + split-pane/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(PBI-49): use attr(data-debug-id) for debug tooltip in globals.css

* refactor(PBI-49): remove data-debug-label from debugProps helper + test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(PBI-49): strip unused component/file args from debugProps in shared/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to StatusBar, NavBar, PanelNavBar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to components/sprint/*

- new-sprint-dialog: __submit on submit button
- sprint-backlog: __list on SprintBacklogLeft + SprintBacklogRight scroll areas
- sprint-board-client: root wrapper div (display:contents) + __drag-overlay
- sprint-header: __title on goal button, __dates on dates button, __actions on action cluster
- sprint-run-controls: root on controls div, __start/__cancel on action buttons; __blockers-dialog on dialog content
- start-sprint-button: root on trigger button, __dialog on dialog content, __submit on submit button
- sync-active-sprint-cookie: no debug-id (returns null, side-effect only), comment added

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to components/backlog/*

* feat(PBI-49): add BEM sub-element data-debug-id to components/ideas/*

* feat(PBI-49): add BEM sub-element data-debug-id to components/dashboard/* + components/markdown.tsx

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to new-product-button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to components/solo/*

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-elements to nav-status-indicators

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to components/jobs/*

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to components/products/*

* feat(PBI-49): add BEM sub-element data-debug-id to components/notifications/*

- answer-modal: __content (scroll area), __submit (footer)
- notifications-bridge: skip comment (bridge, non-rendering wrapper)
- notifications-realtime-mount: skip comment (returns null)
- notifications-sheet: __header, __items (questions list)
- push-toggle: __switch (button), __label (button text) on subscribed/unsubscribed states

* feat(PBI-49): add BEM sub-element data-debug-id to components/settings/*

- leave-product-button: root only (single-button component)
- min-quota-editor: __input (number input), __save (save button)
- profile-editor: __username (bio/short-description input), __save (submit)
- role-manager: __roles (checkbox list), __add (save button)
- token-manager: __tokens (active tokens list), __generate (create button)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to admin, auth, dialogs, entity-dialog, mobile, split-pane

* docs(PBI-49): add debug-labels BEM pattern doc + CLAUDE.md entry

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-09 22:46:29 +02:00 committed by GitHub
parent ce43f7720a
commit d292e445d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
93 changed files with 600 additions and 218 deletions

View file

@ -8,6 +8,7 @@ import {
entityDialogFooterClasses,
entityDialogHeaderClasses,
} from '@/components/shared/entity-dialog-layout'
import { debugProps } from '@/lib/debug'
interface BatchEnqueueBlockerDialogProps {
open: boolean
@ -37,12 +38,12 @@ export function BatchEnqueueBlockerDialog({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent showCloseButton={false} className={entityDialogContentClasses}>
<DialogContent showCloseButton={false} className={entityDialogContentClasses} {...debugProps('batch-enqueue-blocker-dialog', 'BatchEnqueueBlockerDialog', 'components/solo/batch-enqueue-blocker-dialog.tsx')}>
<div className={entityDialogHeaderClasses}>
<DialogTitle className="text-xl font-semibold">Blokkade gedetecteerd</DialogTitle>
</div>
<div className="flex-1 overflow-y-auto px-6 py-6 space-y-6 text-sm text-foreground">
<div className="flex-1 overflow-y-auto px-6 py-6 space-y-6 text-sm text-foreground" data-debug-id="batch-enqueue-blocker-dialog__content">
<p>
{BLOCKER_REASON_LABELS[blockerReason]}:{' '}
<span className="font-medium">{blockerLabel}</span>.
@ -60,7 +61,7 @@ export function BatchEnqueueBlockerDialog({
<div className={entityDialogFooterClasses}>
<div className="flex justify-end gap-2">
<Button variant="ghost" onClick={onCancel}>
<Button variant="ghost" onClick={onCancel} data-debug-id="batch-enqueue-blocker-dialog__cancel">
Annuleer
</Button>
<TooltipProvider>
@ -71,6 +72,7 @@ export function BatchEnqueueBlockerDialog({
<Button
onClick={onConfirm}
disabled={noTasksBeforeBlocker}
data-debug-id="batch-enqueue-blocker-dialog__confirm"
>
{prefixCount === 1
? `Stuur ${prefixCount} taak tot aan blokkade`

View file

@ -4,6 +4,7 @@ import { useSoloStore } from '@/stores/solo-store'
import type { RealtimeStatus } from '@/stores/solo-store'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import { debugProps } from '@/lib/debug'
function RealtimeIndicator({
status,
@ -63,12 +64,14 @@ export function SoloNavStatusIndicators({
workerQuotaPct < minQuotaPct
return (
<div className="flex items-center gap-3 px-2">
<RealtimeIndicator
status={realtimeStatus}
showConnectingIndicator={showConnectingIndicator}
/>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<div className="flex items-center gap-3 px-2" {...debugProps('solo-nav-status-indicators', 'SoloNavStatusIndicators', 'components/solo/nav-status-indicators.tsx')}>
<span data-debug-id="nav-status-indicators__queue">
<RealtimeIndicator
status={realtimeStatus}
showConnectingIndicator={showConnectingIndicator}
/>
</span>
<div className="flex items-center gap-1 text-xs text-muted-foreground" data-debug-id="nav-status-indicators__running">
<span className={cn(
'size-2 rounded-full',
isStandby

View file

@ -1,4 +1,5 @@
import Link from 'next/link'
import { debugProps } from '@/lib/debug'
interface NoActiveSprintProps {
productId: string
@ -7,9 +8,9 @@ interface NoActiveSprintProps {
export function NoActiveSprint({ productId, productName }: NoActiveSprintProps) {
return (
<div className="flex flex-col items-center justify-center h-full gap-4 text-center px-6">
<div className="flex flex-col items-center justify-center h-full gap-4 text-center px-6" {...debugProps('no-active-sprint', 'NoActiveSprint', 'components/solo/no-active-sprint.tsx')}>
<div className="text-4xl text-muted-foreground">🏃</div>
<h2 className="text-lg font-medium text-foreground">Geen actieve sprint</h2>
<h2 className="text-lg font-medium text-foreground" data-debug-id="no-active-sprint__title">Geen actieve sprint</h2>
<p className="text-sm text-muted-foreground max-w-sm">
Er is nog geen actieve sprint voor <span className="font-medium text-foreground">{productName}</span>.
Start een sprint in het Sprint Board om hier je taken te zien.
@ -17,6 +18,7 @@ export function NoActiveSprint({ productId, productName }: NoActiveSprintProps)
<Link
href={`/products/${productId}/sprint`}
className="text-sm text-primary hover:underline"
data-debug-id="no-active-sprint__cta"
>
Naar Sprint Board
</Link>

View file

@ -1,4 +1,5 @@
import Link from 'next/link'
import { debugProps } from '@/lib/debug'
interface Product {
id: string
@ -12,7 +13,7 @@ interface ProductPickerProps {
export function ProductPicker({ products }: ProductPickerProps) {
return (
<div className="p-6 max-w-2xl mx-auto w-full">
<div className="p-6 max-w-2xl mx-auto w-full" {...debugProps('product-picker', 'ProductPicker', 'components/solo/product-picker.tsx')}>
<h1 className="text-xl font-medium text-foreground mb-2">Solo bord</h1>
<p className="text-sm text-muted-foreground mb-6">
Kies een product om je persoonlijke Kanban-bord te openen.
@ -26,7 +27,7 @@ export function ProductPicker({ products }: ProductPickerProps) {
</Link>
</div>
) : (
<div className="grid gap-2">
<div className="grid gap-2" data-debug-id="product-picker__items">
{products.map(product => (
<Link
key={product.id}

View file

@ -11,6 +11,7 @@
import { useSoloRealtime } from '@/lib/realtime/use-solo-realtime'
// render-loos — geen JSX-root, data-debug-id niet van toepassing
export function SoloRealtimeBridge({ productId }: { productId: string | null }) {
useSoloRealtime(productId)
return null

View file

@ -10,6 +10,7 @@ import { useSoloStore } from '@/stores/solo-store'
import { taskStatusToApi } from '@/lib/task-status'
import { previewEnqueueAllAction, enqueueClaudeJobsBatchAction } from '@/actions/claude-jobs'
import { BatchEnqueueBlockerDialog } from './batch-enqueue-blocker-dialog'
import { debugProps } from '@/lib/debug'
import { Button } from '@/components/ui/button'
import { DemoTooltip } from '@/components/shared/demo-tooltip'
import { SplitPane } from '@/components/split-pane/split-pane'
@ -200,8 +201,8 @@ export function SoloBoard({
}
return (
<div className="flex flex-col h-full p-4 gap-4 min-h-0">
<div className="flex items-start justify-between gap-4 shrink-0">
<div className="flex flex-col h-full p-4 gap-4 min-h-0" {...debugProps('solo-board', 'SoloBoard', 'components/solo/solo-board.tsx')}>
<div className="flex items-start justify-between gap-4 shrink-0" data-debug-id="solo-board__header">
<div className="min-w-0 flex items-center gap-3">
<DemoTooltip show={isDemo}>
<Button
@ -231,7 +232,7 @@ export function SoloBoard({
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="flex-1 min-h-0">
<div className="flex-1 min-h-0" data-debug-id="solo-board__columns">
<SplitPane
cookieKey={`solo-${productId}`}
defaultSplit={[33, 33, 34]}

View file

@ -3,6 +3,7 @@
import { useDroppable } from '@dnd-kit/core'
import { cn } from '@/lib/utils'
import { SoloTaskCard } from './solo-task-card'
import { debugProps } from '@/lib/debug'
import type { SoloTask } from './solo-board'
export const COLUMN_CONFIG = {
@ -40,13 +41,14 @@ export function SoloColumn({ status, tasks, isDemo, onTaskClick }: SoloColumnPro
'flex flex-col h-full rounded-lg border border-border overflow-hidden',
isOver && 'ring-2 ring-primary ring-inset',
)}
{...debugProps('solo-column', 'SoloColumn', 'components/solo/solo-column.tsx')}
>
<div className={cn('flex items-center gap-2 px-3 py-2', config.headerClass)}>
<div className={cn('flex items-center gap-2 px-3 py-2', config.headerClass)} data-debug-id="solo-column__header">
<span className="text-sm font-medium">{config.label}</span>
<span className="text-xs opacity-60 ml-auto">{tasks.length}</span>
</div>
<div className="flex-1 flex flex-col gap-2 p-2 overflow-y-auto min-h-[140px]">
<div className="flex-1 flex flex-col gap-2 p-2 overflow-y-auto min-h-[140px]" data-debug-id="solo-column__tasks">
{tasks.map(task => (
<SoloTaskCard
key={task.id}

View file

@ -10,6 +10,7 @@ import { JOB_STATUS_LABELS, JOB_STATUS_COLORS, JOB_STATUS_ACTIVE } from '@/compo
import { useSoloStore } from '@/stores/solo-store'
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip'
import type { SoloTask } from './solo-board'
import { debugProps } from '@/lib/debug'
const PRIORITY_BORDER: Record<number, string> = {
1: 'border-l-4 border-l-priority-critical',
@ -47,9 +48,10 @@ export function SoloTaskCard({ task, isDemo, onClick }: SoloTaskCardProps) {
isDemo ? 'cursor-pointer' : 'cursor-grab active:cursor-grabbing',
)}
{...(!isDemo ? { ...attributes, ...listeners } : {})}
{...debugProps('solo-task-card', 'SoloTaskCard', 'components/solo/solo-task-card.tsx')}
>
{/* Regel 1: taaknaam + task_code */}
<div className="flex items-start justify-between gap-2">
<div className="flex items-start justify-between gap-2" data-debug-id="solo-task-card__title">
<p className="text-sm text-foreground leading-snug flex-1">{task.title}</p>
{task.task_code && (
<TooltipProvider>
@ -106,7 +108,7 @@ export function SoloTaskCard({ task, isDemo, onClick }: SoloTaskCardProps) {
</div>
{/* Regel 4: story-info + job-badge */}
<div className="flex items-center justify-between gap-2 mt-0.5">
<div className="flex items-center justify-between gap-2 mt-0.5" data-debug-id="solo-task-card__status">
<p className="text-xs text-muted-foreground truncate flex-1">
{task.story_code && <span className="font-mono mr-1">{task.story_code}</span>}
{task.story_title}
@ -145,6 +147,7 @@ export function SoloTaskCardOverlay({ task }: { task: SoloTask }) {
'bg-surface-container rounded border border-primary px-3 py-2 shadow-xl opacity-90',
PRIORITY_BORDER[task.priority],
)}
{...debugProps('solo-task-card-overlay', 'SoloTaskCardOverlay', 'components/solo/solo-task-card.tsx')}
>
{/* Regel 1 */}
<div className="flex items-start justify-between gap-2">

View file

@ -19,6 +19,7 @@ import { useSoloStore } from '@/stores/solo-store'
import { enqueueClaudeJobAction, cancelClaudeJobAction } from '@/actions/claude-jobs'
import { cn } from '@/lib/utils'
import { getBranchUrl } from '@/lib/job-status-url'
import { debugProps } from '@/lib/debug'
import type { SoloTask } from './solo-board'
const STATUS_COLORS: Record<string, string> = {
@ -206,7 +207,7 @@ function TaskDetailContent({ task, productId, isDemo, repoUrl, onClose }: TaskDe
</p>
</div>
<div className={entityDialogBodyClasses}>
<div className={entityDialogBodyClasses} data-debug-id="task-detail-dialog__content">
{task.description && (
<div>
<p className="text-xs font-medium text-muted-foreground mb-1.5">Beschrijving</p>
@ -380,7 +381,7 @@ function TaskDetailContent({ task, productId, isDemo, repoUrl, onClose }: TaskDe
export function TaskDetailDialog({ task, productId, isDemo, repoUrl, onClose }: TaskDetailDialogProps) {
return (
<Dialog open={!!task} onOpenChange={(open) => { if (!open) onClose() }}>
<DialogContent showCloseButton={false} className={entityDialogContentClasses}>
<DialogContent showCloseButton={false} className={entityDialogContentClasses} {...debugProps('task-detail-dialog', 'TaskDetailDialog', 'components/solo/task-detail-dialog.tsx')}>
{task && (
<TaskDetailContent
key={task.id}

View file

@ -10,6 +10,7 @@ import { DemoTooltip } from '@/components/shared/demo-tooltip'
import { CodeBadge } from '@/components/shared/code-badge'
import { claimStoryAction } from '@/actions/stories'
import { cn } from '@/lib/utils'
import { debugProps } from '@/lib/debug'
export interface UnassignedStoryTask {
id: string
@ -156,12 +157,12 @@ export function UnassignedStoriesSheet({
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side="right">
<SheetContent side="right" {...debugProps('unassigned-stories-sheet', 'UnassignedStoriesSheet', 'components/solo/unassigned-stories-sheet.tsx')}>
<SheetHeader>
<SheetTitle>Openstaande stories</SheetTitle>
</SheetHeader>
<div className="flex-1 overflow-y-auto">
<div className="flex-1 overflow-y-auto" data-debug-id="unassigned-stories-sheet__content">
{stories.length === 0 ? (
<div className="flex items-center justify-center h-32">
<p className="text-sm text-muted-foreground text-center">
@ -169,7 +170,7 @@ export function UnassignedStoriesSheet({
</p>
</div>
) : (
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-2" data-debug-id="unassigned-stories-sheet__items">
{stories.map(story => (
<ClaimStoryRow
key={story.id}