diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 2325b87..ec9af7b 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -91,34 +91,34 @@ Which phases cover which requirements. Updated during roadmap creation. | Requirement | Phase | Status | |-------------|-------|--------| -| DATA-01 | — | Pending | -| DATA-02 | — | Pending | -| DATA-03 | — | Pending | -| DATA-04 | — | Pending | -| REFAC-01 | — | Pending | -| REFAC-02 | — | Pending | -| REFAC-03 | — | Pending | -| DB-01 | — | Pending | -| DB-02 | — | Pending | -| DB-03 | — | Pending | -| BULK-01 | — | Pending | -| BULK-02 | — | Pending | -| SRCH-01 | — | Pending | -| SRCH-02 | — | Pending | -| SRCH-03 | — | Pending | -| SRCH-04 | — | Pending | -| INDIC-01 | — | Pending | -| INDIC-02 | — | Pending | -| INDIC-03 | — | Pending | -| INDIC-04 | — | Pending | -| A11Y-01 | — | Pending | -| A11Y-02 | — | Pending | +| DATA-01 | Phase 1 | Pending | +| DATA-02 | Phase 1 | Pending | +| DATA-03 | Phase 1 | Pending | +| DATA-04 | Phase 1 | Pending | +| REFAC-01 | Phase 2 | Pending | +| REFAC-02 | Phase 2 | Pending | +| REFAC-03 | Phase 2 | Pending | +| DB-01 | Phase 3 | Pending | +| DB-02 | Phase 3 | Pending | +| DB-03 | Phase 3 | Pending | +| BULK-01 | Phase 4 | Pending | +| BULK-02 | Phase 4 | Pending | +| SRCH-01 | Phase 4 | Pending | +| SRCH-02 | Phase 4 | Pending | +| SRCH-03 | Phase 4 | Pending | +| SRCH-04 | Phase 4 | Pending | +| INDIC-01 | Phase 4 | Pending | +| INDIC-02 | Phase 4 | Pending | +| INDIC-03 | Phase 4 | Pending | +| INDIC-04 | Phase 4 | Pending | +| A11Y-01 | Phase 4 | Pending | +| A11Y-02 | Phase 4 | Pending | **Coverage:** - v1 requirements: 22 total -- Mapped to phases: 0 -- Unmapped: 22 ⚠️ +- Mapped to phases: 22 +- Unmapped: 0 --- *Requirements defined: 2026-03-23* -*Last updated: 2026-03-23 after initial definition* +*Last updated: 2026-03-23 after roadmap creation* diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md new file mode 100644 index 0000000..f19c8e1 --- /dev/null +++ b/.planning/ROADMAP.md @@ -0,0 +1,80 @@ +# Roadmap: DiunDashboard + +## Overview + +This milestone restores data trust and then extends the foundation. Phase 1 fixes active bugs that silently corrupt user data today. Phase 2 refactors the backend into a testable, interface-driven structure — the structural prerequisite for everything that follows. Phase 3 adds PostgreSQL as a first-class alternative to SQLite. Phase 4 delivers the UX features that make the dashboard genuinely usable at scale: bulk dismiss, search/filter, new-update indicators, and accessibility fixes. + +## Phases + +**Phase Numbering:** +- Integer phases (1, 2, 3): Planned milestone work +- Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED) + +Decimal phases appear between their surrounding integers in numeric order. + +- [ ] **Phase 1: Data Integrity** - Fix active SQLite bugs that silently delete tag assignments and suppress test failures +- [ ] **Phase 2: Backend Refactor** - Replace global state with Store interface + Server struct; prerequisite for PostgreSQL +- [ ] **Phase 3: PostgreSQL Support** - Add PostgreSQL as an alternative backend via DATABASE_URL, with versioned migrations +- [ ] **Phase 4: UX Improvements** - Bulk dismiss, search/filter, new-update indicators, and accessibility fixes + +## Phase Details + +### Phase 1: Data Integrity +**Goal**: Users can trust that their data is never silently corrupted — tag assignments survive new DIUN events, foreign key constraints are enforced, and test failures are always visible +**Depends on**: Nothing (first phase) +**Requirements**: DATA-01, DATA-02, DATA-03, DATA-04 +**Success Criteria** (what must be TRUE): + 1. A second DIUN event for the same image does not remove its tag assignment + 2. Deleting a tag removes all associated tag assignments (foreign key cascade enforced) + 3. An oversized webhook payload is rejected with a 413 response, not processed silently + 4. A failing assertion in a test causes the test run to report failure, not pass silently +**Plans**: TBD + +### Phase 2: Backend Refactor +**Goal**: The codebase has a clean Store interface and Server struct so the SQLite implementation can be swapped without touching HTTP handlers, enabling parallel test execution and PostgreSQL support +**Depends on**: Phase 1 +**Requirements**: REFAC-01, REFAC-02, REFAC-03 +**Success Criteria** (what must be TRUE): + 1. All existing tests pass with zero behavior change after the refactor + 2. HTTP handlers contain no SQL — all persistence goes through named Store methods + 3. Package-level global variables (db, mu, webhookSecret) no longer exist + 4. Schema changes are applied via versioned migration files, not ad-hoc DDL in application code +**Plans**: TBD + +### Phase 3: PostgreSQL Support +**Goal**: Users running PostgreSQL infrastructure can point DiunDashboard at a Postgres database via DATABASE_URL and the dashboard works identically to the SQLite deployment +**Depends on**: Phase 2 +**Requirements**: DB-01, DB-02, DB-03 +**Success Criteria** (what must be TRUE): + 1. Setting DATABASE_URL starts the app using PostgreSQL; omitting it falls back to SQLite with DB_PATH + 2. A fresh PostgreSQL deployment receives all schema tables via automatic migration on startup + 3. An existing SQLite user can upgrade to the new binary without any data loss or manual schema changes + 4. The app can be run with Docker Compose using an optional postgres service profile +**Plans**: TBD +**UI hint**: no + +### Phase 4: UX Improvements +**Goal**: Users can manage a large list of updates efficiently — dismissing many at once, finding specific images quickly, and seeing new arrivals without manual refreshes +**Depends on**: Phase 2 +**Requirements**: BULK-01, BULK-02, SRCH-01, SRCH-02, SRCH-03, SRCH-04, INDIC-01, INDIC-02, INDIC-03, INDIC-04, A11Y-01, A11Y-02 +**Success Criteria** (what must be TRUE): + 1. User can dismiss all pending updates with a single button click + 2. User can dismiss all pending updates within a specific tag group with a single action + 3. User can search by image name and filter by status, tag, and sort order without a page reload + 4. A badge/counter showing pending update count is always visible; the browser tab title reflects it (e.g., "DiunDash (3)") + 5. New updates arriving during active polling trigger a visible in-page toast, and updates seen for the first time since the user's last visit are visually highlighted + 6. The light/dark theme toggle is available and respects system preference; the drag handle for tag reordering is always visible without hover +**Plans**: TBD +**UI hint**: yes + +## Progress + +**Execution Order:** +Phases execute in numeric order: 1 → 2 → 3 → 4 + +| Phase | Plans Complete | Status | Completed | +|-------|----------------|--------|-----------| +| 1. Data Integrity | 0/? | Not started | - | +| 2. Backend Refactor | 0/? | Not started | - | +| 3. PostgreSQL Support | 0/? | Not started | - | +| 4. UX Improvements | 0/? | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md new file mode 100644 index 0000000..91b9ed4 --- /dev/null +++ b/.planning/STATE.md @@ -0,0 +1,63 @@ +# Project State + +## Project Reference + +See: .planning/PROJECT.md (updated 2026-03-23) + +**Core value:** Reliable, persistent visibility into which services need updating — data never disappears, and the dashboard is the one place you trust to show the full picture. +**Current focus:** Phase 1 — Data Integrity + +## Current Position + +Phase: 1 of 4 (Data Integrity) +Plan: 0 of ? in current phase +Status: Ready to plan +Last activity: 2026-03-23 — Roadmap created; ready to begin Phase 1 planning + +Progress: [░░░░░░░░░░] 0% + +## Performance Metrics + +**Velocity:** +- Total plans completed: 0 +- Average duration: — +- Total execution time: — + +**By Phase:** + +| Phase | Plans | Total | Avg/Plan | +|-------|-------|-------|----------| +| - | - | - | - | + +**Recent Trend:** +- Last 5 plans: — +- Trend: — + +*Updated after each plan completion* + +## Accumulated Context + +### Decisions + +Decisions are logged in PROJECT.md Key Decisions table. +Recent decisions affecting current work: + +- Fix SQLite bugs before any other work — data trust is the #1 priority; bug-fix tests become the regression suite for the refactor +- Backend refactor must be behavior-neutral — all existing tests must pass before PostgreSQL is introduced +- No ORM or query builder — raw SQL per store implementation; 8 operations across 3 tables is too small to justify a dependency +- `DATABASE_URL` present activates PostgreSQL; absent falls back to SQLite with `DB_PATH` — no separate `DB_DRIVER` variable + +### Pending Todos + +None yet. + +### Blockers/Concerns + +- Phase 3: Verify `pgx/v5/stdlib` import path against pkg.go.dev before writing PostgreSQL query strings +- Phase 3: Re-confirm `golang-migrate` v4.19.1 `database/sqlite` sub-package uses `modernc.org/sqlite` (not `mattn/go-sqlite3`) at implementation time + +## Session Continuity + +Last session: 2026-03-23 +Stopped at: Roadmap created; STATE.md initialized +Resume file: None diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4fdd780 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,283 @@ + +## Project + +**DiunDashboard** + +A web-based dashboard that receives DIUN webhook events and presents a persistent, visual overview of which Docker services have available updates. Built for self-hosters who use DIUN to monitor container images but need something better than dismissable push notifications — a place that nags you until you actually update. + +**Core Value:** Reliable, persistent visibility into which services need updating — data never disappears, and the dashboard is the one place you trust to show the full picture. + +### Constraints + +- **Tech stack**: Go backend + React frontend — established, no migration +- **Database**: Must support both SQLite (simple deploys) and PostgreSQL (robust deploys) +- **Deployment**: Docker-first, single-container with optional compose +- **No CGO**: Pure Go SQLite driver (modernc.org/sqlite) — must maintain this for easy cross-compilation +- **Backward compatible**: Existing users with SQLite databases should be able to upgrade without data loss + + + +## Technology Stack + +## Languages +- Go 1.26 - Backend HTTP server and all API logic (`cmd/diunwebhook/main.go`, `pkg/diunwebhook/diunwebhook.go`) +- TypeScript ~5.7 - Frontend React SPA (`frontend/src/`) +- SQL (SQLite dialect) - Inline schema DDL and queries in `pkg/diunwebhook/diunwebhook.go` +## Runtime +- Go 1.26 (compiled binary, no runtime needed in production) +- Bun (frontend build toolchain, uses `oven/bun:1-alpine` Docker image) +- Alpine Linux 3.18 (production container base) +- Go modules - `go.mod` at project root (module name: `awesomeProject`) +- Bun - `frontend/bun.lock` present for frontend dependencies +- Bun - `docs/bun.lock` present for documentation site dependencies +## Frameworks +- `net/http` (Go stdlib) - HTTP server, routing, and handler registration. No third-party router. +- React 19 (`^19.0.0`) - Frontend SPA (`frontend/`) +- Vite 6 (`^6.0.5`) - Frontend dev server and build tool (`frontend/vite.config.ts`) +- Tailwind CSS 3.4 (`^3.4.17`) - Utility-first CSS (`frontend/tailwind.config.ts`) +- shadcn/ui - Component library (uses Radix UI primitives, `class-variance-authority`, `clsx`, `tailwind-merge`) +- Radix UI (`@radix-ui/react-tooltip` `^1.1.6`) - Accessible tooltip primitives +- dnd-kit (`@dnd-kit/core` `^6.3.1`, `@dnd-kit/utilities` `^3.2.2`) - Drag and drop +- Lucide React (`^0.469.0`) - Icon library +- simple-icons (`^16.9.0`) - Brand/service icons +- VitePress (`^1.6.3`) - Static documentation site (`docs/`) +- Go stdlib `testing` package with `httptest` for handler tests +- No frontend test framework detected +- Vite 6 (`^6.0.5`) - Frontend bundler (`frontend/vite.config.ts`) +- TypeScript ~5.7 (`^5.7.2`) - Type checking (`tsc -b` runs before `vite build`) +- PostCSS 8.4 (`^8.4.49`) with Autoprefixer 10.4 (`^10.4.20`) - CSS processing (`frontend/postcss.config.js`) +- `@vitejs/plugin-react` (`^4.3.4`) - React Fast Refresh for Vite +## Key Dependencies +- `modernc.org/sqlite` v1.46.1 - Pure-Go SQLite driver (no CGO required). Registered as `database/sql` driver named `"sqlite"`. +- `modernc.org/libc` v1.67.6 - C runtime emulation for pure-Go SQLite +- `modernc.org/memory` v1.11.0 - Memory allocator for pure-Go SQLite +- `github.com/dustin/go-humanize` v1.0.1 - Human-readable formatting (indirect dep of modernc.org/sqlite) +- `github.com/google/uuid` v1.6.0 - UUID generation (indirect) +- `github.com/mattn/go-isatty` v0.0.20 - Terminal detection (indirect) +- `golang.org/x/sys` v0.37.0 - System calls (indirect) +- `golang.org/x/exp` v0.0.0-20251023 - Experimental packages (indirect) +- `react` / `react-dom` `^19.0.0` - UI framework +- `@dnd-kit/core` `^6.3.1` - Drag-and-drop for tag assignment +- `tailwindcss` `^3.4.17` - Styling +- `class-variance-authority` `^0.7.1` - shadcn/ui component variant management +- `clsx` `^2.1.1` - Conditional CSS class composition +- `tailwind-merge` `^2.6.0` - Tailwind class deduplication +## Configuration +- `PORT` - HTTP listen port (default: `8080`) +- `DB_PATH` - SQLite database file path (default: `./diun.db`) +- `WEBHOOK_SECRET` - Token for webhook authentication (optional; when unset, webhook is open) +- `go.mod` - Go module definition (module `awesomeProject`) +- `frontend/vite.config.ts` - Vite config with `@` path alias to `./src`, dev proxy for `/api` and `/webhook` to `:8080` +- `frontend/tailwind.config.ts` - Tailwind with shadcn/ui theme tokens (dark mode via `class` strategy) +- `frontend/postcss.config.js` - PostCSS with Tailwind and Autoprefixer plugins +- `frontend/tsconfig.json` - Project references to `tsconfig.node.json` and `tsconfig.app.json` +- `@` resolves to `frontend/src/` (configured in `frontend/vite.config.ts`) +## Database +## Platform Requirements +- Go 1.26+ +- Bun (for frontend and docs development) +- No CGO required (pure-Go SQLite driver) +- Single static binary + `frontend/dist/` static assets +- Alpine Linux 3.18 Docker container +- Persistent volume at `/data/` for SQLite database +- Port 8080 (configurable via `PORT`) +- Gitea Actions with custom Docker image `gitea.jeanlucmakiola.de/makiolaj/docker-node-and-go` (contains both Go and Node/Bun toolchains) +- `GOTOOLCHAIN=local` env var set in CI + + + +## Conventions + +## Naming Patterns +- Package-level source files use the package name: `diunwebhook.go` +- Test files follow Go convention: `diunwebhook_test.go` +- Test-only export files: `export_test.go` +- Entry point: `main.go` inside `cmd/diunwebhook/` +- PascalCase for exported functions: `WebhookHandler`, `UpdateEvent`, `InitDB`, `GetUpdates` +- Handler functions are named `Handler`: `WebhookHandler`, `UpdatesHandler`, `DismissHandler`, `TagsHandler`, `TagByIDHandler`, `TagAssignmentHandler` +- Test functions use `Test_`: `TestWebhookHandler_BadRequest`, `TestDismissHandler_NotFound` +- PascalCase structs: `DiunEvent`, `UpdateEntry`, `Tag` +- JSON tags use snake_case: `json:"diun_version"`, `json:"hub_link"`, `json:"received_at"` +- Package-level unexported variables use short names: `mu`, `db`, `webhookSecret` +- Local variables use short idiomatic Go names: `w`, `r`, `err`, `res`, `n`, `e` +- Components: PascalCase `.tsx` files: `ServiceCard.tsx`, `AcknowledgeButton.tsx`, `Header.tsx`, `TagSection.tsx` +- Hooks: camelCase with `use` prefix: `useUpdates.ts`, `useTags.ts` +- Types: camelCase `.ts` files: `diun.ts` +- Utilities: camelCase `.ts` files: `utils.ts`, `time.ts`, `serviceIcons.ts` +- UI primitives (shadcn): lowercase `.tsx` files: `badge.tsx`, `button.tsx`, `card.tsx`, `tooltip.tsx` +- camelCase for regular functions and hooks: `fetchUpdates`, `useUpdates`, `getServiceIcon` +- PascalCase for React components: `ServiceCard`, `StatCard`, `AcknowledgeButton` +- Helper functions within components use camelCase: `getInitials`, `getTag`, `getShortName` +- Event handlers prefixed with `handle`: `handleDragEnd`, `handleNewGroupSubmit` +- PascalCase interfaces: `DiunEvent`, `UpdateEntry`, `Tag`, `ServiceCardProps` +- Type aliases: PascalCase: `UpdatesMap` +- Interface properties use snake_case matching the Go JSON tags: `diun_version`, `hub_link` +## Code Style +- `gofmt` enforced in CI (formatting check fails the build) +- No additional Go linter (golangci-lint) configured +- `go vet` runs in CI +- Standard Go formatting: tabs for indentation +- No ESLint or Prettier configured in the frontend +- No formatting enforcement in CI for frontend code +- Consistent 2-space indentation observed in all `.tsx` and `.ts` files +- Single quotes for strings in TypeScript +- No semicolons (observed in all frontend files) +- Trailing commas used in multi-line constructs +- `strict: true` in `tsconfig.app.json` +- `noUnusedLocals: true` +- `noUnusedParameters: true` +- `noFallthroughCasesInSwitch: true` +- `noUncheckedSideEffectImports: true` +## Import Organization +- The project module is aliased as `diun` in both `main.go` and test files +- The blank-import pattern `_ "modernc.org/sqlite"` is used for the SQLite driver in `pkg/diunwebhook/diunwebhook.go` +- `@/` maps to `frontend/src/` (configured in `vite.config.ts` and `tsconfig.app.json`) +## Error Handling +- Handlers use `http.Error(w, message, statusCode)` for all error responses +- Error messages are lowercase: `"bad request"`, `"internal error"`, `"not found"`, `"method not allowed"` +- Internal errors are logged with `log.Printf` before returning HTTP 500 +- Decode errors include context: `log.Printf("WebhookHandler: failed to decode request: %v", err)` +- Fatal errors in `main.go` use `log.Fatalf` +- `errors.Is()` used for sentinel error comparison (e.g., `http.ErrServerClosed`) +- String matching used for SQLite constraint errors: `strings.Contains(err.Error(), "UNIQUE")` +- API errors throw with HTTP status: `throw new Error(\`HTTP ${res.status}\`)` +- Catch blocks use `console.error` for logging +- Error state stored in hook state: `setError(e instanceof Error ? e.message : 'Failed to fetch updates')` +- Optimistic updates used for tag assignment (update UI first, then call API) +## Logging +- Startup messages: `log.Printf("Listening on :%s", port)` +- Warnings: `log.Println("WARNING: WEBHOOK_SECRET not set ...")` +- Request logging on success: `log.Printf("Update received: %s (%s)", event.Image, event.Status)` +- Error logging before HTTP error response: `log.Printf("WebhookHandler: failed to store event: %v", err)` +- Handler name prefixed to log messages: `"WebhookHandler: ..."`, `"UpdatesHandler: ..."` +## Comments +- Comments are sparse in the Go codebase +- Handler functions have short doc comments describing the routes they handle: +- Inline comments used for non-obvious behavior: `// Migration: add acknowledged_at to existing databases` +- No JSDoc/TSDoc in the frontend codebase +## Function Design +- Each handler is a standalone `func(http.ResponseWriter, *http.Request)` +- Method checking done at the top of each handler (not via middleware) +- Multi-method handlers use `switch r.Method` +- URL path parameters extracted via `strings.TrimPrefix` +- Request bodies decoded with `json.NewDecoder(r.Body).Decode(&target)` +- Responses written with `json.NewEncoder(w).Encode(data)` or `w.WriteHeader(status)` +- Mutex (`mu`) used around write operations to SQLite +- Custom hooks return object with state and action functions +- `useCallback` wraps all action functions +- `useEffect` for side effects (polling, initial fetch) +- State updates use functional form: `setUpdates(prev => { ... })` +## Module Design +- Single package `diunwebhook` exports all types and handler functions +- No barrel files; single source file `diunwebhook.go` contains everything +- Test helpers exposed via `export_test.go` (only visible to `_test` packages) +- Named exports for all components, hooks, and utilities +- Default export only for the root `App` component (`export default function App()`) +- Type exports use `export interface` or `export type` +- `@/components/ui/` contains shadcn primitives (`badge.tsx`, `button.tsx`, etc.) +## Git Commit Message Conventions +- `feat` - new features +- `fix` - bug fixes +- `docs` - documentation changes +- `chore` - maintenance tasks (deps, config) +- `refactor` - code restructuring +- `style` - UI/styling changes +- `test` - test additions + + + +## Architecture + +## Pattern Overview +- Single Go binary serves both the JSON API and the static frontend assets +- All backend logic lives in one library package (`pkg/diunwebhook/`) +- SQLite database for persistence (pure-Go driver, no CGO) +- Frontend is a standalone React SPA that communicates via REST polling +- No middleware framework -- uses `net/http` standard library directly +## Layers +- Purpose: Accept HTTP requests, validate input, delegate to storage functions, return JSON responses +- Location: `pkg/diunwebhook/diunwebhook.go` (functions: `WebhookHandler`, `UpdatesHandler`, `DismissHandler`, `TagsHandler`, `TagByIDHandler`, `TagAssignmentHandler`) +- Contains: Request parsing, method checks, JSON encoding/decoding, HTTP status responses +- Depends on: Storage layer (package-level `db` and `mu` variables) +- Used by: Route registration in `cmd/diunwebhook/main.go` +- Purpose: Persist and query DIUN events, tags, and tag assignments +- Location: `pkg/diunwebhook/diunwebhook.go` (functions: `InitDB`, `UpdateEvent`, `GetUpdates`; inline SQL in handlers) +- Contains: Schema creation, migrations, CRUD operations via raw SQL +- Depends on: `modernc.org/sqlite` driver, `database/sql` stdlib +- Used by: HTTP handlers in the same file +- Purpose: Initialize database, configure routes, start HTTP server with graceful shutdown +- Location: `cmd/diunwebhook/main.go` +- Contains: Environment variable reading, mux setup, signal handling, server lifecycle +- Depends on: `pkg/diunwebhook` (imported as `diun`) +- Used by: Docker container CMD, direct `go run` +- Purpose: Display DIUN update events in an interactive dashboard with drag-and-drop grouping +- Location: `frontend/src/` +- Contains: React components, custom hooks for data fetching, TypeScript type definitions +- Depends on: Backend REST API (`/api/*` endpoints) +- Used by: Served as static files from `frontend/dist/` by the Go server +## Data Flow +- **Backend:** No in-memory state beyond the `sync.Mutex`. All data lives in SQLite. The `db` and `mu` variables are package-level globals in `pkg/diunwebhook/diunwebhook.go`. +- **Frontend:** React `useState` hooks in two custom hooks: +- No global state library (no Redux, Zustand, etc.) -- state is passed via props from `App.tsx` +## Key Abstractions +- Purpose: Represents a single DIUN webhook payload (image update notification) +- Defined in: `pkg/diunwebhook/diunwebhook.go` (Go struct), `frontend/src/types/diun.ts` (TypeScript interface) +- Pattern: Direct JSON mapping between Go struct tags and TypeScript interface +- Purpose: Wraps a `DiunEvent` with metadata (received timestamp, acknowledged flag, optional tag) +- Defined in: `pkg/diunwebhook/diunwebhook.go` (Go), `frontend/src/types/diun.ts` (TypeScript) +- Pattern: The API returns `map[string]UpdateEntry` keyed by image name (`UpdatesMap` type in frontend) +- Purpose: User-defined grouping label for organizing images +- Defined in: `pkg/diunwebhook/diunwebhook.go` (Go), `frontend/src/types/diun.ts` (TypeScript) +- Pattern: Simple ID + name, linked to images via `tag_assignments` join table +## Entry Points +- Location: `cmd/diunwebhook/main.go` +- Triggers: `go run ./cmd/diunwebhook/` or Docker container `CMD ["./server"]` +- Responsibilities: Read env vars (`DB_PATH`, `PORT`, `WEBHOOK_SECRET`), init DB, register routes, start HTTP server, handle graceful shutdown on SIGINT/SIGTERM +- Location: `frontend/src/main.tsx` +- Triggers: Browser loads `index.html` from `frontend/dist/` (served by Go file server at `/`) +- Responsibilities: Mount React app, force dark mode (`document.documentElement.classList.add('dark')`) +- Location: `POST /webhook` -> `WebhookHandler` in `pkg/diunwebhook/diunwebhook.go` +- Triggers: External DIUN instance sends webhook on image update detection +- Responsibilities: Authenticate (if secret set), validate payload, upsert event into database +## Concurrency Model +- A single `sync.Mutex` (`mu`) in `pkg/diunwebhook/diunwebhook.go` guards all write operations to the database +- `UpdateEvent()`, `DismissHandler`, `TagsHandler` (POST), `TagByIDHandler` (DELETE), and `TagAssignmentHandler` (PUT/DELETE) all acquire `mu.Lock()` before writing +- Read operations (`GetUpdates`, `TagsHandler` GET) do NOT acquire the mutex +- SQLite connection is configured with `db.SetMaxOpenConns(1)` to prevent concurrent write issues +- Standard `net/http` server handles requests concurrently via goroutines +- Graceful shutdown with 15-second timeout on SIGINT/SIGTERM +## Error Handling +- Method validation: Return `405 Method Not Allowed` for wrong HTTP methods +- Input validation: Return `400 Bad Request` for missing/malformed fields +- Authentication: Return `401 Unauthorized` if webhook secret doesn't match +- Not found: Return `404 Not Found` when row doesn't exist (e.g., dismiss nonexistent image) +- Conflict: Return `409 Conflict` for unique constraint violations (duplicate tag name) +- Internal errors: Return `500 Internal Server Error` for database failures +- Fatal startup errors: `log.Fatalf` on `InitDB` failure +- `useUpdates`: catches fetch errors, stores error message in state, displays error banner +- `useTags`: catches errors, logs to `console.error`, fails silently (no user-visible error) +- `assignTag`: uses optimistic update -- updates local state first, fires API call, logs errors to console but does not revert on failure +## Cross-Cutting Concerns + + + +## GSD Workflow Enforcement + +Before using Edit, Write, or other file-changing tools, start work through a GSD command so planning artifacts and execution context stay in sync. + +Use these entry points: +- `/gsd:quick` for small fixes, doc updates, and ad-hoc tasks +- `/gsd:debug` for investigation and bug fixing +- `/gsd:execute-phase` for planned phase work + +Do not make direct repo edits outside a GSD workflow unless the user explicitly asks to bypass it. + + + + + +## Developer Profile + +> Profile not yet configured. Run `/gsd:profile-user` to generate your developer profile. +> This section is managed by `generate-claude-profile` -- do not edit manually. +