Files
DiunDashboard/.planning/codebase/INTEGRATIONS.md
Jean-Luc Makiola 96c4012e2f chore: add GSD codebase map with 7 analysis documents
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>
2026-03-23 19:13:23 +01:00

256 lines
9.2 KiB
Markdown

# 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*