Files
DiunDashboard/CLAUDE.md

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-alpine Docker image)
  • Alpine Linux 3.18 (production container base)
  • Go modules - go.mod at project root (module name: awesomeProject)
  • Bun - frontend/bun.lock present for frontend dependencies
  • Bun - docs/bun.lock present 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 testing package with httptest for 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 -b runs before vite 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/sqlite v1.46.1 - Pure-Go SQLite driver (no CGO required). Registered as database/sql driver named "sqlite".
  • modernc.org/libc v1.67.6 - C runtime emulation for pure-Go SQLite
  • modernc.org/memory v1.11.0 - Memory allocator for pure-Go SQLite
  • github.com/dustin/go-humanize v1.0.1 - Human-readable formatting (indirect dep of modernc.org/sqlite)
  • github.com/google/uuid v1.6.0 - UUID generation (indirect)
  • github.com/mattn/go-isatty v0.0.20 - Terminal detection (indirect)
  • golang.org/x/sys v0.37.0 - System calls (indirect)
  • golang.org/x/exp v0.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 assignment
  • tailwindcss ^3.4.17 - Styling
  • class-variance-authority ^0.7.1 - shadcn/ui component variant management
  • clsx ^2.1.1 - Conditional CSS class composition
  • tailwind-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 (module awesomeProject)
  • frontend/vite.config.ts - Vite config with @ path alias to ./src, dev proxy for /api and /webhook to :8080
  • frontend/tailwind.config.ts - Tailwind with shadcn/ui theme tokens (dark mode via class strategy)
  • frontend/postcss.config.js - PostCSS with Tailwind and Autoprefixer plugins
  • frontend/tsconfig.json - Project references to tsconfig.node.json and tsconfig.app.json
  • @ resolves to frontend/src/ (configured in frontend/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=local env 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.go inside cmd/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 .tsx files: ServiceCard.tsx, AcknowledgeButton.tsx, Header.tsx, TagSection.tsx
  • Hooks: camelCase with use prefix: useUpdates.ts, useTags.ts
  • Types: camelCase .ts files: diun.ts
  • Utilities: camelCase .ts files: utils.ts, time.ts, serviceIcons.ts
  • UI primitives (shadcn): lowercase .tsx files: 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

  • gofmt enforced in CI (formatting check fails the build)
  • No additional Go linter (golangci-lint) configured
  • go vet runs 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 .tsx and .ts files
  • Single quotes for strings in TypeScript
  • No semicolons (observed in all frontend files)
  • Trailing commas used in multi-line constructs
  • strict: true in tsconfig.app.json
  • noUnusedLocals: true
  • noUnusedParameters: true
  • noFallthroughCasesInSwitch: true
  • noUncheckedSideEffectImports: true

Import Organization

  • The project module is aliased as diun in both main.go and test files
  • The blank-import pattern _ "modernc.org/sqlite" is used for the SQLite driver in pkg/diunwebhook/diunwebhook.go
  • @/ maps to frontend/src/ (configured in vite.config.ts and tsconfig.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.Printf before returning HTTP 500
  • Decode errors include context: log.Printf("WebhookHandler: failed to decode request: %v", err)
  • Fatal errors in main.go use log.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.error for 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) or w.WriteHeader(status)
  • Mutex (mu) used around write operations to SQLite
  • Custom hooks return object with state and action functions
  • useCallback wraps all action functions
  • useEffect for side effects (polling, initial fetch)
  • State updates use functional form: setUpdates(prev => { ... })

Module Design

  • Single package diunwebhook exports all types and handler functions
  • No barrel files; single source file diunwebhook.go contains everything
  • Test helpers exposed via export_test.go (only visible to _test packages)
  • Named exports for all components, hooks, and utilities
  • Default export only for the root App component (export default function App())
  • Type exports use export interface or export type
  • @/components/ui/ contains shadcn primitives (badge.tsx, button.tsx, etc.)

Git Commit Message Conventions

  • feat - new features
  • fix - bug fixes
  • docs - documentation changes
  • chore - maintenance tasks (deps, config)
  • refactor - code restructuring
  • style - UI/styling changes
  • test - 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/http standard 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 db and mu variables)
  • 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/sqlite driver, database/sql stdlib
  • 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 as diun)
  • 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. The db and mu variables are package-level globals in pkg/diunwebhook/diunwebhook.go.
  • Frontend: React useState hooks 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 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)
  • 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

  • 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
  • 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'))
  • 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

  • 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
  • Standard net/http server handles requests concurrently via goroutines
  • Graceful shutdown with 15-second timeout on SIGINT/SIGTERM

Error Handling

  • 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
  • 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

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:quick for small fixes, doc updates, and ad-hoc tasks
  • /gsd:debug for investigation and bug fixing
  • /gsd:execute-phase for 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-user to generate your developer profile. This section is managed by generate-claude-profile -- do not edit manually.