# 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: `Authorization` header must match `WEBHOOK_SECRET` env var (when set) - Source: `pkg/diunwebhook/diunwebhook.go` lines 163-199 ## API Contracts ### Webhook Ingestion **`POST /webhook`** - Receive a DIUN event - Handler: `WebhookHandler` in `pkg/diunwebhook/diunwebhook.go` - Auth: `Authorization` header checked via constant-time compare against `WEBHOOK_SECRET` - Request body: ```json { "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` (missing `image` field or invalid JSON), `500 Internal Server Error` - Behavior: Upserts into `updates` table keyed by `image`. Replaces existing entry and resets `acknowledged_at` to NULL. ### Updates API **`GET /api/updates`** - List all tracked image updates - Handler: `UpdatesHandler` in `pkg/diunwebhook/diunwebhook.go` - Response: `200 OK` with JSON object keyed by image name: ```json { "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: `DismissHandler` in `pkg/diunwebhook/diunwebhook.go` - URL parameter: `{image}` is the full image name (URL-encoded) - Response: `204 No Content` on 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: `TagsHandler` in `pkg/diunwebhook/diunwebhook.go` - Response: `200 OK` with JSON array: ```json [{ "id": 1, "name": "production" }, { "id": 2, "name": "staging" }] ``` **`POST /api/tags`** - Create a new tag - Handler: `TagsHandler` in `pkg/diunwebhook/diunwebhook.go` - Request body: `{ "name": "production" }` - Response: `201 Created` with `{ "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: `TagByIDHandler` in `pkg/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_assignments` referencing this tag ### Tag Assignments API **`PUT /api/tag-assignments`** - Assign an image to a tag - Handler: `TagAssignmentHandler` in `pkg/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: `TagAssignmentHandler` in `pkg/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"))` in `cmd/diunwebhook/main.go` - Serves the production build of the React frontend ## Data Storage **Database:** - SQLite (file-based, single-writer) - Connection: `DB_PATH` env var (default `./diun.db`) - Driver: `modernc.org/sqlite` (pure Go, registered as `"sqlite"` in `database/sql`) - Max open connections: 1 (`db.SetMaxOpenConns(1)`) - Write concurrency: `sync.Mutex` in `pkg/diunwebhook/diunwebhook.go` **Schema:** ```sql -- 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()` in `pkg/diunwebhook/diunwebhook.go` - Uses `CREATE TABLE IF NOT EXISTS` for 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_SECRET` env var - Checked in `WebhookHandler` using `crypto/subtle.ConstantTimeCompare` against the `Authorization` header - When `WEBHOOK_SECRET` is empty, the webhook endpoint is unprotected (warning logged at startup) - Implementation: `pkg/diunwebhook/diunwebhook.go` lines 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 `log` package 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 targeting `develop` - Steps: `gofmt` check, `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_dispatch` with 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 (pulls `latest` from registry, mounts `diun-data` volume at `/data`) - `compose.dev.yml` - Local development (builds from Dockerfile) **Documentation Site:** - Separate `docs/Dockerfile` and `docs/nginx.conf` for 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 access - `DB_PATH` - Set to `/data/diun.db` in Docker for persistent volume mount - `PORT` - Override default port 8080 **Secrets:** - `WEBHOOK_SECRET` - Shared secret between DIUN and this app - `GITEA_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 `:5173` proxies `/api` and `/webhook` to `http://localhost:8080` (`frontend/vite.config.ts`) **Production:** - Go server serves `frontend/dist/` at `/` via `http.FileServer` - API and webhook routes are on the same origin (no CORS needed) **Polling:** - React SPA polls `GET /api/updates` every 5 seconds (no WebSocket/SSE) --- *Integration audit: 2026-03-23*