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

8.8 KiB

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