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>
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/httpstandard 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
dbandmuvariables) - 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/sqlitedriver,database/sqlstdlib - 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 asdiun) - 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:
- DIUN sends
POST /webhookwith JSON payload containing image update event WebhookHandlerinpkg/diunwebhook/diunwebhook.govalidates theAuthorizationheader (ifWEBHOOK_SECRETis set) using constant-time comparison- JSON body is decoded into
DiunEventstruct;imagefield is required UpdateEvent()acquiresmu.Lock(), executesINSERT OR REPLACEintoupdatestable (keyed onimage), setsreceived_atto current time, resetsacknowledged_attoNULL- Returns
200 OK
Dashboard Polling:
- React SPA (
useUpdateshook infrontend/src/hooks/useUpdates.ts) pollsGET /api/updatesevery 5 seconds UpdatesHandlerinpkg/diunwebhook/diunwebhook.goqueriesupdatestable withLEFT JOINontag_assignmentsandtags- Returns
map[string]UpdateEntryas JSON (keyed by image name) - Frontend groups entries by tag, displays in
TagSectioncomponents withServiceCardchildren
Acknowledge (Dismiss):
- User clicks acknowledge button on a
ServiceCard - Frontend sends
PATCH /api/updates/{image}viauseUpdates.acknowledge() - Frontend performs optimistic update on local state
DismissHandlersetsacknowledged_at = datetime('now')for matching image row
Tag Management:
- Tags are fetched once on mount via
useTagshook (GET /api/tags) - Create:
POST /api/tagswith{ name }-- tag names must be unique (409 on conflict) - Delete:
DELETE /api/tags/{id}-- cascades totag_assignmentsvia FK constraint - Assign:
PUT /api/tag-assignmentswith{ image, tag_id }--INSERT OR REPLACE - Unassign:
DELETE /api/tag-assignmentswith{ image } - Drag-and-drop in frontend uses
@dnd-kit/core;DndContext.onDragEndcallsassignTag()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. Thedbandmuvariables are package-level globals inpkg/diunwebhook/diunwebhook.go. - Frontend: React
useStatehooks in two custom hooks:useUpdates(frontend/src/hooks/useUpdates.ts): holdsUpdatesMap, loading/error state, polling countdownuseTags(frontend/src/hooks/useTags.ts): holdsTag[], 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
DiunEventwith 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]UpdateEntrykeyed by image name (UpdatesMaptype 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_assignmentsjoin table
Entry Points
Go Server:
- Location:
cmd/diunwebhook/main.go - Triggers:
go run ./cmd/diunwebhook/or Docker containerCMD ["./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.htmlfromfrontend/dist/(served by Go file server at/) - Responsibilities: Mount React app, force dark mode (
document.documentElement.classList.add('dark'))
Webhook Endpoint:
- Location:
POST /webhook->WebhookHandlerinpkg/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) inpkg/diunwebhook/diunwebhook.goguards all write operations to the database UpdateEvent(),DismissHandler,TagsHandler(POST),TagByIDHandler(DELETE), andTagAssignmentHandler(PUT/DELETE) all acquiremu.Lock()before writing- Read operations (
GetUpdates,TagsHandlerGET) do NOT acquire the mutex - SQLite connection is configured with
db.SetMaxOpenConns(1)to prevent concurrent write issues
HTTP Server:
- Standard
net/httpserver 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 Allowedfor wrong HTTP methods - Input validation: Return
400 Bad Requestfor missing/malformed fields - Authentication: Return
401 Unauthorizedif webhook secret doesn't match - Not found: Return
404 Not Foundwhen row doesn't exist (e.g., dismiss nonexistent image) - Conflict: Return
409 Conflictfor unique constraint violations (duplicate tag name) - Internal errors: Return
500 Internal Server Errorfor database failures - Fatal startup errors:
log.FatalfonInitDBfailure
Frontend Patterns:
useUpdates: catches fetch errors, stores error message in state, displays error banneruseTags: catches errors, logs toconsole.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