# Architecture **Analysis Date:** 2026-03-23 ## Pattern Overview **Overall:** Monolithic Go HTTP server with embedded React SPA frontend **Key Characteristics:** - 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 **HTTP Layer (Handlers):** - 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` **Storage Layer (SQLite):** - 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 **Entry Point / Wiring:** - 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` **Frontend SPA:** - 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 **Webhook Ingestion:** 1. DIUN sends `POST /webhook` with JSON payload containing image update event 2. `WebhookHandler` in `pkg/diunwebhook/diunwebhook.go` validates the `Authorization` header (if `WEBHOOK_SECRET` is set) using constant-time comparison 3. JSON body is decoded into `DiunEvent` struct; `image` field is required 4. `UpdateEvent()` acquires `mu.Lock()`, executes `INSERT OR REPLACE` into `updates` table (keyed on `image`), sets `received_at` to current time, resets `acknowledged_at` to `NULL` 5. Returns `200 OK` **Dashboard Polling:** 1. React SPA (`useUpdates` hook in `frontend/src/hooks/useUpdates.ts`) polls `GET /api/updates` every 5 seconds 2. `UpdatesHandler` in `pkg/diunwebhook/diunwebhook.go` queries `updates` table with `LEFT JOIN` on `tag_assignments` and `tags` 3. Returns `map[string]UpdateEntry` as JSON (keyed by image name) 4. Frontend groups entries by tag, displays in `TagSection` components with `ServiceCard` children **Acknowledge (Dismiss):** 1. User clicks acknowledge button on a `ServiceCard` 2. Frontend sends `PATCH /api/updates/{image}` via `useUpdates.acknowledge()` 3. Frontend performs optimistic update on local state 4. `DismissHandler` sets `acknowledged_at = datetime('now')` for matching image row **Tag Management:** 1. Tags are fetched once on mount via `useTags` hook (`GET /api/tags`) 2. Create: `POST /api/tags` with `{ name }` -- tag names must be unique (409 on conflict) 3. Delete: `DELETE /api/tags/{id}` -- cascades to `tag_assignments` via FK constraint 4. Assign: `PUT /api/tag-assignments` with `{ image, tag_id }` -- `INSERT OR REPLACE` 5. Unassign: `DELETE /api/tag-assignments` with `{ image }` 6. Drag-and-drop in frontend uses `@dnd-kit/core`; `DndContext.onDragEnd` calls `assignTag()` which performs optimistic UI update then fires API call **State Management:** - **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: - `useUpdates` (`frontend/src/hooks/useUpdates.ts`): holds `UpdatesMap`, loading/error state, polling countdown - `useTags` (`frontend/src/hooks/useTags.ts`): holds `Tag[]`, provides create/delete callbacks - No global state library (no Redux, Zustand, etc.) -- state is passed via props from `App.tsx` ## Key Abstractions **DiunEvent:** - 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 **UpdateEntry:** - 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) **Tag:** - 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 **Go Server:** - 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 **Frontend SPA:** - 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')`) **Webhook Endpoint:** - 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 **Mutex-based serialization:** - 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 **HTTP Server:** - Standard `net/http` server handles requests concurrently via goroutines - Graceful shutdown with 15-second timeout on SIGINT/SIGTERM ## Error Handling **Strategy:** Return appropriate HTTP status codes with plain-text error messages; log errors server-side via `log.Printf` **Backend Patterns:** - 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 **Frontend Patterns:** - `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 **Logging:** Standard library `log` package. Logs webhook receipt, decode errors, storage errors. No structured logging or log levels beyond `log.Printf` and `log.Fatalf`. **Validation:** Manual validation in each handler. No validation library or middleware. Each handler checks HTTP method, decodes body, validates required fields individually. **Authentication:** Optional token-based auth on webhook endpoint only. `WEBHOOK_SECRET` env var compared via `crypto/subtle.ConstantTimeCompare` against `Authorization` header. No auth on API endpoints (`/api/*`). **CORS:** Not configured. Frontend is served from the same origin as the API, so CORS is not needed in production. Vite dev server proxies `/api` and `/webhook` to `localhost:8080`. **Database Migrations:** Inline in `InitDB()`. Uses `CREATE TABLE IF NOT EXISTS` for initial schema and `ALTER TABLE ADD COLUMN` (error silently ignored) for adding `acknowledged_at` to existing databases. --- *Architecture analysis: 2026-03-23*