docs: create roadmap (4 phases)

This commit is contained in:
2026-03-23 19:51:36 +01:00
parent 1f5df8c36a
commit 112c17a701
4 changed files with 451 additions and 25 deletions

View File

@@ -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*

80
.planning/ROADMAP.md Normal file
View File

@@ -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 | - |

63
.planning/STATE.md Normal file
View File

@@ -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

283
CLAUDE.md Normal file
View File

@@ -0,0 +1,283 @@
<!-- GSD:project-start source:PROJECT.md -->
## 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
<!-- GSD:project-end -->
<!-- GSD:stack-start source:codebase/STACK.md -->
## 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
<!-- GSD:stack-end -->
<!-- GSD:conventions-start source:CONVENTIONS.md -->
## 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 `<Noun>Handler`: `WebhookHandler`, `UpdatesHandler`, `DismissHandler`, `TagsHandler`, `TagByIDHandler`, `TagAssignmentHandler`
- Test functions use `Test<FunctionName>_<Scenario>`: `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
<!-- GSD:conventions-end -->
<!-- GSD:architecture-start source:ARCHITECTURE.md -->
## 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:architecture-end -->
<!-- GSD:workflow-start source:GSD defaults -->
## 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.
<!-- GSD:workflow-end -->
<!-- GSD:profile-start -->
## 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.
<!-- GSD:profile-end -->