17 KiB
Project
DiunDashboard
A web-based dashboard that receives DIUN webhook events and presents a persistent, visual overview of which Docker services have available updates. Built for self-hosters who use DIUN to monitor container images but need something better than dismissable push notifications — a place that nags you until you actually update.
Core Value: Reliable, persistent visibility into which services need updating — data never disappears, and the dashboard is the one place you trust to show the full picture.
Constraints
- Tech stack: Go backend + React frontend — established, no migration
- Database: Must support both SQLite (simple deploys) and PostgreSQL (robust deploys)
- Deployment: Docker-first, single-container with optional compose
- No CGO: Pure Go SQLite driver (modernc.org/sqlite) — must maintain this for easy cross-compilation
- Backward compatible: Existing users with SQLite databases should be able to upgrade without data loss
Technology Stack
Languages
- Go 1.26 - Backend HTTP server and all API logic (
cmd/diunwebhook/main.go,pkg/diunwebhook/diunwebhook.go) - TypeScript ~5.7 - Frontend React SPA (
frontend/src/) - SQL (SQLite dialect) - Inline schema DDL and queries in
pkg/diunwebhook/diunwebhook.go
Runtime
- Go 1.26 (compiled binary, no runtime needed in production)
- Bun (frontend build toolchain, uses
oven/bun:1-alpineDocker image) - Alpine Linux 3.18 (production container base)
- Go modules -
go.modat project root (module name:awesomeProject) - Bun -
frontend/bun.lockpresent for frontend dependencies - Bun -
docs/bun.lockpresent for documentation site dependencies
Frameworks
net/http(Go stdlib) - HTTP server, routing, and handler registration. No third-party router.- React 19 (
^19.0.0) - Frontend SPA (frontend/) - Vite 6 (
^6.0.5) - Frontend dev server and build tool (frontend/vite.config.ts) - Tailwind CSS 3.4 (
^3.4.17) - Utility-first CSS (frontend/tailwind.config.ts) - shadcn/ui - Component library (uses Radix UI primitives,
class-variance-authority,clsx,tailwind-merge) - Radix UI (
@radix-ui/react-tooltip^1.1.6) - Accessible tooltip primitives - dnd-kit (
@dnd-kit/core^6.3.1,@dnd-kit/utilities^3.2.2) - Drag and drop - Lucide React (
^0.469.0) - Icon library - simple-icons (
^16.9.0) - Brand/service icons - VitePress (
^1.6.3) - Static documentation site (docs/) - Go stdlib
testingpackage withhttptestfor handler tests - No frontend test framework detected
- Vite 6 (
^6.0.5) - Frontend bundler (frontend/vite.config.ts) - TypeScript ~5.7 (
^5.7.2) - Type checking (tsc -bruns beforevite build) - PostCSS 8.4 (
^8.4.49) with Autoprefixer 10.4 (^10.4.20) - CSS processing (frontend/postcss.config.js) @vitejs/plugin-react(^4.3.4) - React Fast Refresh for Vite
Key Dependencies
modernc.org/sqlitev1.46.1 - Pure-Go SQLite driver (no CGO required). Registered asdatabase/sqldriver named"sqlite".modernc.org/libcv1.67.6 - C runtime emulation for pure-Go SQLitemodernc.org/memoryv1.11.0 - Memory allocator for pure-Go SQLitegithub.com/dustin/go-humanizev1.0.1 - Human-readable formatting (indirect dep of modernc.org/sqlite)github.com/google/uuidv1.6.0 - UUID generation (indirect)github.com/mattn/go-isattyv0.0.20 - Terminal detection (indirect)golang.org/x/sysv0.37.0 - System calls (indirect)golang.org/x/expv0.0.0-20251023 - Experimental packages (indirect)react/react-dom^19.0.0- UI framework@dnd-kit/core^6.3.1- Drag-and-drop for tag assignmenttailwindcss^3.4.17- Stylingclass-variance-authority^0.7.1- shadcn/ui component variant managementclsx^2.1.1- Conditional CSS class compositiontailwind-merge^2.6.0- Tailwind class deduplication
Configuration
PORT- HTTP listen port (default:8080)DB_PATH- SQLite database file path (default:./diun.db)WEBHOOK_SECRET- Token for webhook authentication (optional; when unset, webhook is open)go.mod- Go module definition (moduleawesomeProject)frontend/vite.config.ts- Vite config with@path alias to./src, dev proxy for/apiand/webhookto:8080frontend/tailwind.config.ts- Tailwind with shadcn/ui theme tokens (dark mode viaclassstrategy)frontend/postcss.config.js- PostCSS with Tailwind and Autoprefixer pluginsfrontend/tsconfig.json- Project references totsconfig.node.jsonandtsconfig.app.json@resolves tofrontend/src/(configured infrontend/vite.config.ts)
Database
Platform Requirements
- Go 1.26+
- Bun (for frontend and docs development)
- No CGO required (pure-Go SQLite driver)
- Single static binary +
frontend/dist/static assets - Alpine Linux 3.18 Docker container
- Persistent volume at
/data/for SQLite database - Port 8080 (configurable via
PORT) - Gitea Actions with custom Docker image
gitea.jeanlucmakiola.de/makiolaj/docker-node-and-go(contains both Go and Node/Bun toolchains) GOTOOLCHAIN=localenv var set in CI
Conventions
Naming Patterns
- Package-level source files use the package name:
diunwebhook.go - Test files follow Go convention:
diunwebhook_test.go - Test-only export files:
export_test.go - Entry point:
main.goinsidecmd/diunwebhook/ - PascalCase for exported functions:
WebhookHandler,UpdateEvent,InitDB,GetUpdates - Handler functions are named
<Noun>Handler:WebhookHandler,UpdatesHandler,DismissHandler,TagsHandler,TagByIDHandler,TagAssignmentHandler - Test functions use
Test<FunctionName>_<Scenario>:TestWebhookHandler_BadRequest,TestDismissHandler_NotFound - PascalCase structs:
DiunEvent,UpdateEntry,Tag - JSON tags use snake_case:
json:"diun_version",json:"hub_link",json:"received_at" - Package-level unexported variables use short names:
mu,db,webhookSecret - Local variables use short idiomatic Go names:
w,r,err,res,n,e - Components: PascalCase
.tsxfiles:ServiceCard.tsx,AcknowledgeButton.tsx,Header.tsx,TagSection.tsx - Hooks: camelCase with
useprefix:useUpdates.ts,useTags.ts - Types: camelCase
.tsfiles:diun.ts - Utilities: camelCase
.tsfiles:utils.ts,time.ts,serviceIcons.ts - UI primitives (shadcn): lowercase
.tsxfiles:badge.tsx,button.tsx,card.tsx,tooltip.tsx - camelCase for regular functions and hooks:
fetchUpdates,useUpdates,getServiceIcon - PascalCase for React components:
ServiceCard,StatCard,AcknowledgeButton - Helper functions within components use camelCase:
getInitials,getTag,getShortName - Event handlers prefixed with
handle:handleDragEnd,handleNewGroupSubmit - PascalCase interfaces:
DiunEvent,UpdateEntry,Tag,ServiceCardProps - Type aliases: PascalCase:
UpdatesMap - Interface properties use snake_case matching the Go JSON tags:
diun_version,hub_link
Code Style
gofmtenforced in CI (formatting check fails the build)- No additional Go linter (golangci-lint) configured
go vetruns in CI- Standard Go formatting: tabs for indentation
- No ESLint or Prettier configured in the frontend
- No formatting enforcement in CI for frontend code
- Consistent 2-space indentation observed in all
.tsxand.tsfiles - Single quotes for strings in TypeScript
- No semicolons (observed in all frontend files)
- Trailing commas used in multi-line constructs
strict: trueintsconfig.app.jsonnoUnusedLocals: truenoUnusedParameters: truenoFallthroughCasesInSwitch: truenoUncheckedSideEffectImports: true
Import Organization
- The project module is aliased as
diunin bothmain.goand test files - The blank-import pattern
_ "modernc.org/sqlite"is used for the SQLite driver inpkg/diunwebhook/diunwebhook.go @/maps tofrontend/src/(configured invite.config.tsandtsconfig.app.json)
Error Handling
- Handlers use
http.Error(w, message, statusCode)for all error responses - Error messages are lowercase:
"bad request","internal error","not found","method not allowed" - Internal errors are logged with
log.Printfbefore returning HTTP 500 - Decode errors include context:
log.Printf("WebhookHandler: failed to decode request: %v", err) - Fatal errors in
main.gouselog.Fatalf errors.Is()used for sentinel error comparison (e.g.,http.ErrServerClosed)- String matching used for SQLite constraint errors:
strings.Contains(err.Error(), "UNIQUE") - API errors throw with HTTP status:
throw new Error(\HTTP ${res.status}`)` - Catch blocks use
console.errorfor logging - Error state stored in hook state:
setError(e instanceof Error ? e.message : 'Failed to fetch updates') - Optimistic updates used for tag assignment (update UI first, then call API)
Logging
- Startup messages:
log.Printf("Listening on :%s", port) - Warnings:
log.Println("WARNING: WEBHOOK_SECRET not set ...") - Request logging on success:
log.Printf("Update received: %s (%s)", event.Image, event.Status) - Error logging before HTTP error response:
log.Printf("WebhookHandler: failed to store event: %v", err) - Handler name prefixed to log messages:
"WebhookHandler: ...","UpdatesHandler: ..."
Comments
- Comments are sparse in the Go codebase
- Handler functions have short doc comments describing the routes they handle:
- Inline comments used for non-obvious behavior:
// Migration: add acknowledged_at to existing databases - No JSDoc/TSDoc in the frontend codebase
Function Design
- Each handler is a standalone
func(http.ResponseWriter, *http.Request) - Method checking done at the top of each handler (not via middleware)
- Multi-method handlers use
switch r.Method - URL path parameters extracted via
strings.TrimPrefix - Request bodies decoded with
json.NewDecoder(r.Body).Decode(&target) - Responses written with
json.NewEncoder(w).Encode(data)orw.WriteHeader(status) - Mutex (
mu) used around write operations to SQLite - Custom hooks return object with state and action functions
useCallbackwraps all action functionsuseEffectfor side effects (polling, initial fetch)- State updates use functional form:
setUpdates(prev => { ... })
Module Design
- Single package
diunwebhookexports all types and handler functions - No barrel files; single source file
diunwebhook.gocontains everything - Test helpers exposed via
export_test.go(only visible to_testpackages) - Named exports for all components, hooks, and utilities
- Default export only for the root
Appcomponent (export default function App()) - Type exports use
export interfaceorexport type @/components/ui/contains shadcn primitives (badge.tsx,button.tsx, etc.)
Git Commit Message Conventions
feat- new featuresfix- bug fixesdocs- documentation changeschore- maintenance tasks (deps, config)refactor- code restructuringstyle- UI/styling changestest- test additions
Architecture
Pattern Overview
- 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
- 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 - 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
- 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 - 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
- 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: - No global state library (no Redux, Zustand, etc.) -- state is passed via props from
App.tsx
Key Abstractions
- 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
- 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) - 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
- 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 - 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')) - 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
- 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 - Standard
net/httpserver handles requests concurrently via goroutines - Graceful shutdown with 15-second timeout on SIGINT/SIGTERM
Error Handling
- 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 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
GSD Workflow Enforcement
Before using Edit, Write, or other file-changing tools, start work through a GSD command so planning artifacts and execution context stay in sync.
Use these entry points:
/gsd:quickfor small fixes, doc updates, and ad-hoc tasks/gsd:debugfor investigation and bug fixing/gsd:execute-phasefor planned phase work
Do not make direct repo edits outside a GSD workflow unless the user explicitly asks to bypass it.
Developer Profile
Profile not yet configured. Run
/gsd:profile-userto generate your developer profile. This section is managed bygenerate-claude-profile-- do not edit manually.