Files
DiunDashboard/.planning/codebase/ARCHITECTURE.md
Jean-Luc Makiola 96c4012e2f chore: add GSD codebase map with 7 analysis documents
Parallel analysis of tech stack, architecture, structure,
conventions, testing patterns, integrations, and concerns.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 19:13:23 +01:00

166 lines
8.8 KiB
Markdown

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