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>
This commit is contained in:
255
.planning/codebase/INTEGRATIONS.md
Normal file
255
.planning/codebase/INTEGRATIONS.md
Normal file
@@ -0,0 +1,255 @@
|
||||
# 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*
|
||||
Reference in New Issue
Block a user