Files
DiunDashboard/CLAUDE.md

284 lines
17 KiB
Markdown

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