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>
9.2 KiB
External Integrations
Analysis Date: 2026-03-23
APIs & External Services
DIUN (Docker Image Update Notifier):
- DIUN sends webhook POST requests when container image updates are detected
- Endpoint:
POST /webhook - SDK/Client: None (DIUN pushes to this app; this app is the receiver)
- Auth:
Authorizationheader must matchWEBHOOK_SECRETenv var (when set) - Source:
pkg/diunwebhook/diunwebhook.golines 163-199
API Contracts
Webhook Ingestion
POST /webhook - Receive a DIUN event
- Handler:
WebhookHandlerinpkg/diunwebhook/diunwebhook.go - Auth:
Authorizationheader checked via constant-time compare againstWEBHOOK_SECRET - Request body:
{
"diun_version": "4.28.0",
"hostname": "docker-host",
"status": "new",
"provider": "docker",
"image": "registry/org/image:tag",
"hub_link": "https://hub.docker.com/r/...",
"mime_type": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:abc123...",
"created": "2026-03-23T10:00:00Z",
"platform": "linux/amd64",
"metadata": {
"ctn_names": "container-name",
"ctn_id": "abc123",
"ctn_state": "running",
"ctn_status": "Up 2 days"
}
}
- Response:
200 OK(empty body) on success - Errors:
401 Unauthorized,405 Method Not Allowed,400 Bad Request(missingimagefield or invalid JSON),500 Internal Server Error - Behavior: Upserts into
updatestable keyed byimage. Replaces existing entry and resetsacknowledged_atto NULL.
Updates API
GET /api/updates - List all tracked image updates
- Handler:
UpdatesHandlerinpkg/diunwebhook/diunwebhook.go - Response:
200 OKwith JSON object keyed by image name:
{
"registry/org/image:tag": {
"event": { /* DiunEvent fields */ },
"received_at": "2026-03-23T10:00:00Z",
"acknowledged": false,
"tag": { "id": 1, "name": "production" } // or null
}
}
PATCH /api/updates/{image} - Dismiss (acknowledge) an update
- Handler:
DismissHandlerinpkg/diunwebhook/diunwebhook.go - URL parameter:
{image}is the full image name (URL-encoded) - Response:
204 No Contenton success - Errors:
405 Method Not Allowed,400 Bad Request,404 Not Found,500 Internal Server Error - Behavior: Sets
acknowledged_at = datetime('now')on the matching row
Tags API
GET /api/tags - List all tags
- Handler:
TagsHandlerinpkg/diunwebhook/diunwebhook.go - Response:
200 OKwith JSON array:
[{ "id": 1, "name": "production" }, { "id": 2, "name": "staging" }]
POST /api/tags - Create a new tag
- Handler:
TagsHandlerinpkg/diunwebhook/diunwebhook.go - Request body:
{ "name": "production" } - Response:
201 Createdwith{ "id": 1, "name": "production" } - Errors:
400 Bad Request(empty name),409 Conflict(duplicate name),500 Internal Server Error
DELETE /api/tags/{id} - Delete a tag
- Handler:
TagByIDHandlerinpkg/diunwebhook/diunwebhook.go - URL parameter:
{id}is integer tag ID - Response:
204 No Content - Errors:
405 Method Not Allowed,400 Bad Request(invalid ID),404 Not Found,500 Internal Server Error - Behavior: Cascading delete removes all
tag_assignmentsreferencing this tag
Tag Assignments API
PUT /api/tag-assignments - Assign an image to a tag
- Handler:
TagAssignmentHandlerinpkg/diunwebhook/diunwebhook.go - Request body:
{ "image": "registry/org/image:tag", "tag_id": 1 } - Response:
204 No Content - Errors:
400 Bad Request,404 Not Found(tag doesn't exist),500 Internal Server Error - Behavior:
INSERT OR REPLACE- reassigns if already assigned
DELETE /api/tag-assignments - Unassign an image from its tag
- Handler:
TagAssignmentHandlerinpkg/diunwebhook/diunwebhook.go - Request body:
{ "image": "registry/org/image:tag" } - Response:
204 No Content - Errors:
400 Bad Request,500 Internal Server Error
Static File Serving
GET / and all unmatched routes - Serve React SPA
- Handler:
http.FileServer(http.Dir("./frontend/dist"))incmd/diunwebhook/main.go - Serves the production build of the React frontend
Data Storage
Database:
- SQLite (file-based, single-writer)
- Connection:
DB_PATHenv var (default./diun.db) - Driver:
modernc.org/sqlite(pure Go, registered as"sqlite"indatabase/sql) - Max open connections: 1 (
db.SetMaxOpenConns(1)) - Write concurrency:
sync.Mutexinpkg/diunwebhook/diunwebhook.go
Schema:
-- Table: updates (one row per unique image)
CREATE TABLE IF NOT EXISTS updates (
image TEXT PRIMARY KEY,
diun_version TEXT NOT NULL DEFAULT '',
hostname TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT '',
provider TEXT NOT NULL DEFAULT '',
hub_link TEXT NOT NULL DEFAULT '',
mime_type TEXT NOT NULL DEFAULT '',
digest TEXT NOT NULL DEFAULT '',
created TEXT NOT NULL DEFAULT '',
platform TEXT NOT NULL DEFAULT '',
ctn_name TEXT NOT NULL DEFAULT '',
ctn_id TEXT NOT NULL DEFAULT '',
ctn_state TEXT NOT NULL DEFAULT '',
ctn_status TEXT NOT NULL DEFAULT '',
received_at TEXT NOT NULL,
acknowledged_at TEXT
);
-- Table: tags (user-defined grouping labels)
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
);
-- Table: tag_assignments (image-to-tag mapping, one tag per image)
CREATE TABLE IF NOT EXISTS tag_assignments (
image TEXT PRIMARY KEY,
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE
);
Migrations:
- Schema is created on startup via
InitDB()inpkg/diunwebhook/diunwebhook.go - Uses
CREATE TABLE IF NOT EXISTSfor all tables - One manual migration:
ALTER TABLE updates ADD COLUMN acknowledged_at TEXT(silently ignored if already present) - No formal migration framework; migrations are inline Go code
File Storage: Local filesystem only (SQLite database file)
Caching: None
Authentication & Identity
Webhook Authentication:
- Token-based via
WEBHOOK_SECRETenv var - Checked in
WebhookHandlerusingcrypto/subtle.ConstantTimeCompareagainst theAuthorizationheader - When
WEBHOOK_SECRETis empty, the webhook endpoint is unprotected (warning logged at startup) - Implementation:
pkg/diunwebhook/diunwebhook.golines 54-56, 163-170
User Authentication: None. The dashboard and all API endpoints (except webhook) are open/unauthenticated.
Monitoring & Observability
Error Tracking: None (no Sentry, Datadog, etc.)
Logs:
- Go stdlib
logpackage writing to stdout - Key log points: startup warnings, webhook receipt, errors in handlers
- No structured logging framework
CI/CD & Deployment
Hosting: Self-hosted via Docker on a Gitea instance at gitea.jeanlucmakiola.de
Container Registry: gitea.jeanlucmakiola.de/makiolaj/diundashboard
CI Pipeline (Gitea Actions):
- Config:
.gitea/workflows/ci.yml - Triggers: Push to
develop, PRs targetingdevelop - Steps:
gofmtcheck,go vet, tests with coverage (warn below 80%),go build - Runner: Custom Docker image with Go + Node/Bun toolchains
Release Pipeline (Gitea Actions):
- Config:
.gitea/workflows/release.yml - Trigger: Manual
workflow_dispatchwith semver bump choice (patch/minor/major) - Steps: Run full CI checks, compute new version tag, create git tag, build and push Docker image (versioned +
latest), create Gitea release with changelog - Secrets required:
GITEA_TOKEN,REGISTRY_TOKEN
Docker Build:
- Multi-stage Dockerfile at project root (
Dockerfile) - Stage 1:
oven/bun:1-alpine- Build frontend (bun install --frozen-lockfile && bun run build) - Stage 2:
golang:1.26-alpine- Build Go binary (CGO_ENABLED=0 go build) - Stage 3:
alpine:3.18- Runtime with binary + static assets, exposes port 8080
Docker Compose:
compose.yml- Production deploy (pullslatestfrom registry, mountsdiun-datavolume at/data)compose.dev.yml- Local development (builds from Dockerfile)
Documentation Site:
- Separate
docs/Dockerfileanddocs/nginx.conffor static site deployment via Nginx - Built with VitePress, served as static HTML
Environment Configuration
Required env vars (production):
- None strictly required (all have defaults)
Recommended env vars:
WEBHOOK_SECRET- Protect webhook endpoint from unauthorized accessDB_PATH- Set to/data/diun.dbin Docker for persistent volume mountPORT- Override default port 8080
Secrets:
WEBHOOK_SECRET- Shared secret between DIUN and this appGITEA_TOKEN- CI/CD pipeline (Gitea API access)REGISTRY_TOKEN- CI/CD pipeline (Docker registry push)
Webhooks & Callbacks
Incoming:
POST /webhook- Receives DIUN image update notifications
Outgoing:
- None
Frontend-Backend Communication
Dev Mode:
- Vite dev server on
:5173proxies/apiand/webhooktohttp://localhost:8080(frontend/vite.config.ts)
Production:
- Go server serves
frontend/dist/at/viahttp.FileServer - API and webhook routes are on the same origin (no CORS needed)
Polling:
- React SPA polls
GET /api/updatesevery 5 seconds (no WebSocket/SSE)
Integration audit: 2026-03-23