diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 59b1098..0ba3887 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -9,7 +9,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co go test -v -coverprofile=coverage.out -coverpkg=./... ./... # Run a single test -go test -v -run TestWebhookHandler ./test/diunwebhook/ +go test -v -run TestWebhookHandler ./pkg/diunwebhook/ # Run the app locally go run ./cmd/diunwebhook/ @@ -19,22 +19,44 @@ docker build -t diun-webhook-dashboard . # Run with Docker Compose docker compose up -d + +# Frontend (from frontend/ directory) +bun install # install deps +bun run dev # dev server on :5173 (proxies /api and /webhook to :8080) +bun run build # production build → frontend/dist/ ``` CI warns (but does not fail) when coverage drops below 80%. ## Architecture -The app is a minimal Go HTTP server that receives [DIUN](https://crazymax.dev/diun/) webhook events and exposes them via a JSON API and static HTML dashboard. All states are in-memory (no persistence). +The app is a Go HTTP server that receives [DIUN](https://crazymax.dev/diun/) webhook events and exposes them via a JSON API and a React SPA dashboard. Events are persisted to a SQLite database (`diun.db`) using `modernc.org/sqlite` (pure Go, no CGO). **Package layout:** -- `pkg/diunwebhook/` — core library: `DiunEvent` struct, in-memory `updates` map (guarded by `sync.Mutex`), and HTTP handlers (`WebhookHandler`, `UpdatesHandler`) -- `cmd/diunwebhook/main.go` — wires the handlers and static file server onto `net/http`'s default mux, listens on `:8080` -- `test/diunwebhook/` — external test package (`package diunwebhook_test`) that imports `pkg/diunwebhook`; uses `httptest` for handler tests -- `static/` — served verbatim at `/` +- `pkg/diunwebhook/` — core library: `DiunEvent`, `UpdateEntry`, and `Tag` structs; SQLite-backed storage (guarded by `sync.Mutex`); HTTP handlers (`WebhookHandler`, `UpdatesHandler`, `DismissHandler`, `TagsHandler`, `TagByIDHandler`, `TagAssignmentHandler`) +- `cmd/diunwebhook/main.go` — calls `InitDB()`, wires handlers onto `net/http`'s default mux, serves `./frontend/dist` at `/`, listens on `:8080` (overridable via `PORT` env var), graceful shutdown +- `pkg/diunwebhook/diunwebhook_test.go` — external test package (`package diunwebhook_test`); uses `httptest` for handler tests +- `pkg/diunwebhook/export_test.go` — test-only exports +- `frontend/` — React SPA (Bun + Vite + React 19 + Tailwind CSS + shadcn/ui + dnd-kit) + +**Database schema (SQLite):** +- `updates` — one row per image (PRIMARY KEY `image`), stores full event fields plus `received_at` and `acknowledged_at` +- `tags` — user-defined tag/group names (`id` INTEGER PK, `name` UNIQUE) +- `tag_assignments` — maps `image` → `tag_id` (FK with CASCADE delete) + +**API routes:** +- `POST /webhook` — accept a DIUN event +- `GET /api/updates` — return all events (with tag and acknowledged state) +- `PATCH /api/updates/{image}` — mark an event as acknowledged (dismiss) +- `GET /api/tags` — list all tags +- `POST /api/tags` — create a tag +- `DELETE /api/tags/{id}` — delete a tag (cascades to assignments) +- `PUT /api/tag-assignments` — assign an image to a tag +- `DELETE /api/tag-assignments` — unassign an image from its tag **Key data flow:** -1. DIUN POSTs JSON to `/webhook` → `WebhookHandler` decodes into `DiunEvent` → stored in `updates[event.Image]` (the latest event per image wins) -2. Dashboard JS polls `GET /api/updates` → `UpdatesHandler` returns a full map as JSON +1. DIUN POSTs JSON to `/webhook` → `WebhookHandler` decodes into `DiunEvent` → upserted into `updates` table (latest event per image wins, resets acknowledged state) +2. React SPA polls `GET /api/updates` every 5 s → `UpdatesHandler` returns map of `UpdateEntry` (includes event, received time, acknowledged flag, and optional tag) +3. User can dismiss updates via `PATCH /api/updates/{image}` and organize images into tag groups via the tag/assignment endpoints -**Test helpers exposed from the library package** (not part of the public API, only for tests): `GetUpdatesMap()`, `UpdatesReset()`, `UpdateEvent()`. +**Test helpers exposed from the library package** (not part of the public API, only for tests): `GetUpdatesMap()`, `UpdatesReset()`, `UpdateEvent()`, `ResetTags()`. diff --git a/README.md b/README.md index 86f61da..357aae2 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,23 @@ # DIUN Webhook Dashboard -A tiny Go web app that receives [DIUN](https://crazymax.dev/diun/) webhook events and shows the latest image updates in a simple UI. +A Go web app that receives [DIUN](https://crazymax.dev/diun/) webhook events and shows image updates in a modern dashboard. Events are persisted to SQLite so they survive restarts. - Receives DIUN webhooks at `POST /webhook` -- Serves a minimal dashboard at `/` -- Exposes a read-only API at `GET /api/updates` -- Stores events in memory (ephemeral) +- Serves a React SPA dashboard at `/` +- REST API for updates, tags, and acknowledgements +- Persistent storage via SQLite (`diun.db`) +- Tag/group system to organize images +- Dismiss (acknowledge) updates you've reviewed ## Quick start ### Run locally (Go 1.26+) ```bash -go run . +# Build the frontend first +cd frontend && bun install && bun run build && cd .. + +# Start the server +go run ./cmd/diunwebhook/ # open http://localhost:8080 ``` @@ -49,41 +55,62 @@ Expected JSON payload (simplified): ``` ## API -- `POST /webhook` — accept a DIUN event JSON body. -- `GET /api/updates` — return the latest events as a JSON object keyed by image name. -- `/` — static HTML dashboard. -Note: data is only kept in-memory and will be reset on restart. +| Method | Endpoint | Description | +|--------|----------|-------------| +| `POST` | `/webhook` | Accept a DIUN event JSON body | +| `GET` | `/api/updates` | Return all events (keyed by image) with tag and acknowledged state | +| `PATCH` | `/api/updates/{image}` | Mark an event as acknowledged (dismiss) | +| `GET` | `/api/tags` | List all tags | +| `POST` | `/api/tags` | Create a new tag | +| `DELETE` | `/api/tags/{id}` | Delete a tag (cascades to assignments) | +| `PUT` | `/api/tag-assignments` | Assign an image to a tag | +| `DELETE` | `/api/tag-assignments` | Unassign an image from its tag | ## Project Structure -- `cmd/diunwebhook/` — main application source and tests -- `static/` — static assets -- `Dockerfile`, `docker-compose.yml`, `go.mod`, `go.sum` — project config/build files +``` +cmd/diunwebhook/ — main application entrypoint +pkg/diunwebhook/ — core library (handlers, DB, models) +frontend/ — React SPA (Bun + Vite + React 19 + Tailwind + shadcn/ui) +.gitea/workflows/ — CI/CD workflows (Gitea Actions) +Dockerfile — 3-stage multi-stage build +docker-compose.yml — single-service compose file +``` ## Development -- Code: `main.go` -- Static assets: `static/` -- Container image: `Dockerfile` -## Production notes -- Behind a reverse proxy, ensure the app is reachable at `/webhook` from DIUN. -- Persistence is not implemented; hook up a store (e.g., BoltDB/Redis/Postgres) if you need durability. -- Consider adding auth, rate limiting, or a secret/token on the webhook endpoint if exposed publicly. +**Backend:** +```bash +go run ./cmd/diunwebhook/ +``` + +**Frontend (dev server with hot reload):** +```bash +cd frontend +bun install +bun run dev # http://localhost:5173, proxies API to :8080 +``` ## Testing Run unit tests and check coverage: ```bash -go test -v -cover +go test -v -coverprofile=coverage.out -coverpkg=./... ./... ``` -Aim for 80–90% coverage. Coverage below 80% will emit a warning in CI but will not fail the pipeline. +Aim for 80-90% coverage. Coverage below 80% will emit a warning in CI but will not fail the pipeline. ## CI/CD with Gitea Actions -A sample Gitea Actions workflow is provided in `.gitea/workflows/ci.yml` to automate build, test, and coverage checks. +- **CI** (`.gitea/workflows/ci.yml`): Runs on push/PR to `develop`. Checks formatting (`gofmt`), runs `go vet`, tests with coverage, and builds the binary. +- **Release** (`.gitea/workflows/release.yml`): Manual dispatch. Runs tests, bumps version, tags, builds and pushes Docker image to Gitea registry, creates a release with changelog. + +## Production notes +- Behind a reverse proxy, ensure the app is reachable at `/webhook` from DIUN. +- Data is persisted to `diun.db` in the working directory. Mount a volume to preserve data across container recreations. +- Consider adding auth, rate limiting, or a secret/token on the webhook endpoint if exposed publicly. ## License MIT — see `LICENSE`.