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>
166 lines
8.8 KiB
Markdown
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*
|