docs: create roadmap (4 phases)
This commit is contained in:
@@ -91,34 +91,34 @@ Which phases cover which requirements. Updated during roadmap creation.
|
|||||||
|
|
||||||
| Requirement | Phase | Status |
|
| Requirement | Phase | Status |
|
||||||
|-------------|-------|--------|
|
|-------------|-------|--------|
|
||||||
| DATA-01 | — | Pending |
|
| DATA-01 | Phase 1 | Pending |
|
||||||
| DATA-02 | — | Pending |
|
| DATA-02 | Phase 1 | Pending |
|
||||||
| DATA-03 | — | Pending |
|
| DATA-03 | Phase 1 | Pending |
|
||||||
| DATA-04 | — | Pending |
|
| DATA-04 | Phase 1 | Pending |
|
||||||
| REFAC-01 | — | Pending |
|
| REFAC-01 | Phase 2 | Pending |
|
||||||
| REFAC-02 | — | Pending |
|
| REFAC-02 | Phase 2 | Pending |
|
||||||
| REFAC-03 | — | Pending |
|
| REFAC-03 | Phase 2 | Pending |
|
||||||
| DB-01 | — | Pending |
|
| DB-01 | Phase 3 | Pending |
|
||||||
| DB-02 | — | Pending |
|
| DB-02 | Phase 3 | Pending |
|
||||||
| DB-03 | — | Pending |
|
| DB-03 | Phase 3 | Pending |
|
||||||
| BULK-01 | — | Pending |
|
| BULK-01 | Phase 4 | Pending |
|
||||||
| BULK-02 | — | Pending |
|
| BULK-02 | Phase 4 | Pending |
|
||||||
| SRCH-01 | — | Pending |
|
| SRCH-01 | Phase 4 | Pending |
|
||||||
| SRCH-02 | — | Pending |
|
| SRCH-02 | Phase 4 | Pending |
|
||||||
| SRCH-03 | — | Pending |
|
| SRCH-03 | Phase 4 | Pending |
|
||||||
| SRCH-04 | — | Pending |
|
| SRCH-04 | Phase 4 | Pending |
|
||||||
| INDIC-01 | — | Pending |
|
| INDIC-01 | Phase 4 | Pending |
|
||||||
| INDIC-02 | — | Pending |
|
| INDIC-02 | Phase 4 | Pending |
|
||||||
| INDIC-03 | — | Pending |
|
| INDIC-03 | Phase 4 | Pending |
|
||||||
| INDIC-04 | — | Pending |
|
| INDIC-04 | Phase 4 | Pending |
|
||||||
| A11Y-01 | — | Pending |
|
| A11Y-01 | Phase 4 | Pending |
|
||||||
| A11Y-02 | — | Pending |
|
| A11Y-02 | Phase 4 | Pending |
|
||||||
|
|
||||||
**Coverage:**
|
**Coverage:**
|
||||||
- v1 requirements: 22 total
|
- v1 requirements: 22 total
|
||||||
- Mapped to phases: 0
|
- Mapped to phases: 22
|
||||||
- Unmapped: 22 ⚠️
|
- Unmapped: 0
|
||||||
|
|
||||||
---
|
---
|
||||||
*Requirements defined: 2026-03-23*
|
*Requirements defined: 2026-03-23*
|
||||||
*Last updated: 2026-03-23 after initial definition*
|
*Last updated: 2026-03-23 after roadmap creation*
|
||||||
|
|||||||
80
.planning/ROADMAP.md
Normal file
80
.planning/ROADMAP.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# Roadmap: DiunDashboard
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This milestone restores data trust and then extends the foundation. Phase 1 fixes active bugs that silently corrupt user data today. Phase 2 refactors the backend into a testable, interface-driven structure — the structural prerequisite for everything that follows. Phase 3 adds PostgreSQL as a first-class alternative to SQLite. Phase 4 delivers the UX features that make the dashboard genuinely usable at scale: bulk dismiss, search/filter, new-update indicators, and accessibility fixes.
|
||||||
|
|
||||||
|
## Phases
|
||||||
|
|
||||||
|
**Phase Numbering:**
|
||||||
|
- Integer phases (1, 2, 3): Planned milestone work
|
||||||
|
- Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED)
|
||||||
|
|
||||||
|
Decimal phases appear between their surrounding integers in numeric order.
|
||||||
|
|
||||||
|
- [ ] **Phase 1: Data Integrity** - Fix active SQLite bugs that silently delete tag assignments and suppress test failures
|
||||||
|
- [ ] **Phase 2: Backend Refactor** - Replace global state with Store interface + Server struct; prerequisite for PostgreSQL
|
||||||
|
- [ ] **Phase 3: PostgreSQL Support** - Add PostgreSQL as an alternative backend via DATABASE_URL, with versioned migrations
|
||||||
|
- [ ] **Phase 4: UX Improvements** - Bulk dismiss, search/filter, new-update indicators, and accessibility fixes
|
||||||
|
|
||||||
|
## Phase Details
|
||||||
|
|
||||||
|
### Phase 1: Data Integrity
|
||||||
|
**Goal**: Users can trust that their data is never silently corrupted — tag assignments survive new DIUN events, foreign key constraints are enforced, and test failures are always visible
|
||||||
|
**Depends on**: Nothing (first phase)
|
||||||
|
**Requirements**: DATA-01, DATA-02, DATA-03, DATA-04
|
||||||
|
**Success Criteria** (what must be TRUE):
|
||||||
|
1. A second DIUN event for the same image does not remove its tag assignment
|
||||||
|
2. Deleting a tag removes all associated tag assignments (foreign key cascade enforced)
|
||||||
|
3. An oversized webhook payload is rejected with a 413 response, not processed silently
|
||||||
|
4. A failing assertion in a test causes the test run to report failure, not pass silently
|
||||||
|
**Plans**: TBD
|
||||||
|
|
||||||
|
### Phase 2: Backend Refactor
|
||||||
|
**Goal**: The codebase has a clean Store interface and Server struct so the SQLite implementation can be swapped without touching HTTP handlers, enabling parallel test execution and PostgreSQL support
|
||||||
|
**Depends on**: Phase 1
|
||||||
|
**Requirements**: REFAC-01, REFAC-02, REFAC-03
|
||||||
|
**Success Criteria** (what must be TRUE):
|
||||||
|
1. All existing tests pass with zero behavior change after the refactor
|
||||||
|
2. HTTP handlers contain no SQL — all persistence goes through named Store methods
|
||||||
|
3. Package-level global variables (db, mu, webhookSecret) no longer exist
|
||||||
|
4. Schema changes are applied via versioned migration files, not ad-hoc DDL in application code
|
||||||
|
**Plans**: TBD
|
||||||
|
|
||||||
|
### Phase 3: PostgreSQL Support
|
||||||
|
**Goal**: Users running PostgreSQL infrastructure can point DiunDashboard at a Postgres database via DATABASE_URL and the dashboard works identically to the SQLite deployment
|
||||||
|
**Depends on**: Phase 2
|
||||||
|
**Requirements**: DB-01, DB-02, DB-03
|
||||||
|
**Success Criteria** (what must be TRUE):
|
||||||
|
1. Setting DATABASE_URL starts the app using PostgreSQL; omitting it falls back to SQLite with DB_PATH
|
||||||
|
2. A fresh PostgreSQL deployment receives all schema tables via automatic migration on startup
|
||||||
|
3. An existing SQLite user can upgrade to the new binary without any data loss or manual schema changes
|
||||||
|
4. The app can be run with Docker Compose using an optional postgres service profile
|
||||||
|
**Plans**: TBD
|
||||||
|
**UI hint**: no
|
||||||
|
|
||||||
|
### Phase 4: UX Improvements
|
||||||
|
**Goal**: Users can manage a large list of updates efficiently — dismissing many at once, finding specific images quickly, and seeing new arrivals without manual refreshes
|
||||||
|
**Depends on**: Phase 2
|
||||||
|
**Requirements**: BULK-01, BULK-02, SRCH-01, SRCH-02, SRCH-03, SRCH-04, INDIC-01, INDIC-02, INDIC-03, INDIC-04, A11Y-01, A11Y-02
|
||||||
|
**Success Criteria** (what must be TRUE):
|
||||||
|
1. User can dismiss all pending updates with a single button click
|
||||||
|
2. User can dismiss all pending updates within a specific tag group with a single action
|
||||||
|
3. User can search by image name and filter by status, tag, and sort order without a page reload
|
||||||
|
4. A badge/counter showing pending update count is always visible; the browser tab title reflects it (e.g., "DiunDash (3)")
|
||||||
|
5. New updates arriving during active polling trigger a visible in-page toast, and updates seen for the first time since the user's last visit are visually highlighted
|
||||||
|
6. The light/dark theme toggle is available and respects system preference; the drag handle for tag reordering is always visible without hover
|
||||||
|
**Plans**: TBD
|
||||||
|
**UI hint**: yes
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
**Execution Order:**
|
||||||
|
Phases execute in numeric order: 1 → 2 → 3 → 4
|
||||||
|
|
||||||
|
| Phase | Plans Complete | Status | Completed |
|
||||||
|
|-------|----------------|--------|-----------|
|
||||||
|
| 1. Data Integrity | 0/? | Not started | - |
|
||||||
|
| 2. Backend Refactor | 0/? | Not started | - |
|
||||||
|
| 3. PostgreSQL Support | 0/? | Not started | - |
|
||||||
|
| 4. UX Improvements | 0/? | Not started | - |
|
||||||
63
.planning/STATE.md
Normal file
63
.planning/STATE.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Project State
|
||||||
|
|
||||||
|
## Project Reference
|
||||||
|
|
||||||
|
See: .planning/PROJECT.md (updated 2026-03-23)
|
||||||
|
|
||||||
|
**Core value:** Reliable, persistent visibility into which services need updating — data never disappears, and the dashboard is the one place you trust to show the full picture.
|
||||||
|
**Current focus:** Phase 1 — Data Integrity
|
||||||
|
|
||||||
|
## Current Position
|
||||||
|
|
||||||
|
Phase: 1 of 4 (Data Integrity)
|
||||||
|
Plan: 0 of ? in current phase
|
||||||
|
Status: Ready to plan
|
||||||
|
Last activity: 2026-03-23 — Roadmap created; ready to begin Phase 1 planning
|
||||||
|
|
||||||
|
Progress: [░░░░░░░░░░] 0%
|
||||||
|
|
||||||
|
## Performance Metrics
|
||||||
|
|
||||||
|
**Velocity:**
|
||||||
|
- Total plans completed: 0
|
||||||
|
- Average duration: —
|
||||||
|
- Total execution time: —
|
||||||
|
|
||||||
|
**By Phase:**
|
||||||
|
|
||||||
|
| Phase | Plans | Total | Avg/Plan |
|
||||||
|
|-------|-------|-------|----------|
|
||||||
|
| - | - | - | - |
|
||||||
|
|
||||||
|
**Recent Trend:**
|
||||||
|
- Last 5 plans: —
|
||||||
|
- Trend: —
|
||||||
|
|
||||||
|
*Updated after each plan completion*
|
||||||
|
|
||||||
|
## Accumulated Context
|
||||||
|
|
||||||
|
### Decisions
|
||||||
|
|
||||||
|
Decisions are logged in PROJECT.md Key Decisions table.
|
||||||
|
Recent decisions affecting current work:
|
||||||
|
|
||||||
|
- Fix SQLite bugs before any other work — data trust is the #1 priority; bug-fix tests become the regression suite for the refactor
|
||||||
|
- Backend refactor must be behavior-neutral — all existing tests must pass before PostgreSQL is introduced
|
||||||
|
- No ORM or query builder — raw SQL per store implementation; 8 operations across 3 tables is too small to justify a dependency
|
||||||
|
- `DATABASE_URL` present activates PostgreSQL; absent falls back to SQLite with `DB_PATH` — no separate `DB_DRIVER` variable
|
||||||
|
|
||||||
|
### Pending Todos
|
||||||
|
|
||||||
|
None yet.
|
||||||
|
|
||||||
|
### Blockers/Concerns
|
||||||
|
|
||||||
|
- Phase 3: Verify `pgx/v5/stdlib` import path against pkg.go.dev before writing PostgreSQL query strings
|
||||||
|
- Phase 3: Re-confirm `golang-migrate` v4.19.1 `database/sqlite` sub-package uses `modernc.org/sqlite` (not `mattn/go-sqlite3`) at implementation time
|
||||||
|
|
||||||
|
## Session Continuity
|
||||||
|
|
||||||
|
Last session: 2026-03-23
|
||||||
|
Stopped at: Roadmap created; STATE.md initialized
|
||||||
|
Resume file: None
|
||||||
283
CLAUDE.md
Normal file
283
CLAUDE.md
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
<!-- GSD:project-start source:PROJECT.md -->
|
||||||
|
## Project
|
||||||
|
|
||||||
|
**DiunDashboard**
|
||||||
|
|
||||||
|
A web-based dashboard that receives DIUN webhook events and presents a persistent, visual overview of which Docker services have available updates. Built for self-hosters who use DIUN to monitor container images but need something better than dismissable push notifications — a place that nags you until you actually update.
|
||||||
|
|
||||||
|
**Core Value:** Reliable, persistent visibility into which services need updating — data never disappears, and the dashboard is the one place you trust to show the full picture.
|
||||||
|
|
||||||
|
### Constraints
|
||||||
|
|
||||||
|
- **Tech stack**: Go backend + React frontend — established, no migration
|
||||||
|
- **Database**: Must support both SQLite (simple deploys) and PostgreSQL (robust deploys)
|
||||||
|
- **Deployment**: Docker-first, single-container with optional compose
|
||||||
|
- **No CGO**: Pure Go SQLite driver (modernc.org/sqlite) — must maintain this for easy cross-compilation
|
||||||
|
- **Backward compatible**: Existing users with SQLite databases should be able to upgrade without data loss
|
||||||
|
<!-- GSD:project-end -->
|
||||||
|
|
||||||
|
<!-- GSD:stack-start source:codebase/STACK.md -->
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
- Go 1.26 - Backend HTTP server and all API logic (`cmd/diunwebhook/main.go`, `pkg/diunwebhook/diunwebhook.go`)
|
||||||
|
- TypeScript ~5.7 - Frontend React SPA (`frontend/src/`)
|
||||||
|
- SQL (SQLite dialect) - Inline schema DDL and queries in `pkg/diunwebhook/diunwebhook.go`
|
||||||
|
## Runtime
|
||||||
|
- Go 1.26 (compiled binary, no runtime needed in production)
|
||||||
|
- Bun (frontend build toolchain, uses `oven/bun:1-alpine` Docker image)
|
||||||
|
- Alpine Linux 3.18 (production container base)
|
||||||
|
- Go modules - `go.mod` at project root (module name: `awesomeProject`)
|
||||||
|
- Bun - `frontend/bun.lock` present for frontend dependencies
|
||||||
|
- Bun - `docs/bun.lock` present for documentation site dependencies
|
||||||
|
## Frameworks
|
||||||
|
- `net/http` (Go stdlib) - HTTP server, routing, and handler registration. No third-party router.
|
||||||
|
- React 19 (`^19.0.0`) - Frontend SPA (`frontend/`)
|
||||||
|
- Vite 6 (`^6.0.5`) - Frontend dev server and build tool (`frontend/vite.config.ts`)
|
||||||
|
- Tailwind CSS 3.4 (`^3.4.17`) - Utility-first CSS (`frontend/tailwind.config.ts`)
|
||||||
|
- shadcn/ui - Component library (uses Radix UI primitives, `class-variance-authority`, `clsx`, `tailwind-merge`)
|
||||||
|
- Radix UI (`@radix-ui/react-tooltip` `^1.1.6`) - Accessible tooltip primitives
|
||||||
|
- dnd-kit (`@dnd-kit/core` `^6.3.1`, `@dnd-kit/utilities` `^3.2.2`) - Drag and drop
|
||||||
|
- Lucide React (`^0.469.0`) - Icon library
|
||||||
|
- simple-icons (`^16.9.0`) - Brand/service icons
|
||||||
|
- VitePress (`^1.6.3`) - Static documentation site (`docs/`)
|
||||||
|
- Go stdlib `testing` package with `httptest` for handler tests
|
||||||
|
- No frontend test framework detected
|
||||||
|
- Vite 6 (`^6.0.5`) - Frontend bundler (`frontend/vite.config.ts`)
|
||||||
|
- TypeScript ~5.7 (`^5.7.2`) - Type checking (`tsc -b` runs before `vite build`)
|
||||||
|
- PostCSS 8.4 (`^8.4.49`) with Autoprefixer 10.4 (`^10.4.20`) - CSS processing (`frontend/postcss.config.js`)
|
||||||
|
- `@vitejs/plugin-react` (`^4.3.4`) - React Fast Refresh for Vite
|
||||||
|
## Key Dependencies
|
||||||
|
- `modernc.org/sqlite` v1.46.1 - Pure-Go SQLite driver (no CGO required). Registered as `database/sql` driver named `"sqlite"`.
|
||||||
|
- `modernc.org/libc` v1.67.6 - C runtime emulation for pure-Go SQLite
|
||||||
|
- `modernc.org/memory` v1.11.0 - Memory allocator for pure-Go SQLite
|
||||||
|
- `github.com/dustin/go-humanize` v1.0.1 - Human-readable formatting (indirect dep of modernc.org/sqlite)
|
||||||
|
- `github.com/google/uuid` v1.6.0 - UUID generation (indirect)
|
||||||
|
- `github.com/mattn/go-isatty` v0.0.20 - Terminal detection (indirect)
|
||||||
|
- `golang.org/x/sys` v0.37.0 - System calls (indirect)
|
||||||
|
- `golang.org/x/exp` v0.0.0-20251023 - Experimental packages (indirect)
|
||||||
|
- `react` / `react-dom` `^19.0.0` - UI framework
|
||||||
|
- `@dnd-kit/core` `^6.3.1` - Drag-and-drop for tag assignment
|
||||||
|
- `tailwindcss` `^3.4.17` - Styling
|
||||||
|
- `class-variance-authority` `^0.7.1` - shadcn/ui component variant management
|
||||||
|
- `clsx` `^2.1.1` - Conditional CSS class composition
|
||||||
|
- `tailwind-merge` `^2.6.0` - Tailwind class deduplication
|
||||||
|
## Configuration
|
||||||
|
- `PORT` - HTTP listen port (default: `8080`)
|
||||||
|
- `DB_PATH` - SQLite database file path (default: `./diun.db`)
|
||||||
|
- `WEBHOOK_SECRET` - Token for webhook authentication (optional; when unset, webhook is open)
|
||||||
|
- `go.mod` - Go module definition (module `awesomeProject`)
|
||||||
|
- `frontend/vite.config.ts` - Vite config with `@` path alias to `./src`, dev proxy for `/api` and `/webhook` to `:8080`
|
||||||
|
- `frontend/tailwind.config.ts` - Tailwind with shadcn/ui theme tokens (dark mode via `class` strategy)
|
||||||
|
- `frontend/postcss.config.js` - PostCSS with Tailwind and Autoprefixer plugins
|
||||||
|
- `frontend/tsconfig.json` - Project references to `tsconfig.node.json` and `tsconfig.app.json`
|
||||||
|
- `@` resolves to `frontend/src/` (configured in `frontend/vite.config.ts`)
|
||||||
|
## Database
|
||||||
|
## Platform Requirements
|
||||||
|
- Go 1.26+
|
||||||
|
- Bun (for frontend and docs development)
|
||||||
|
- No CGO required (pure-Go SQLite driver)
|
||||||
|
- Single static binary + `frontend/dist/` static assets
|
||||||
|
- Alpine Linux 3.18 Docker container
|
||||||
|
- Persistent volume at `/data/` for SQLite database
|
||||||
|
- Port 8080 (configurable via `PORT`)
|
||||||
|
- Gitea Actions with custom Docker image `gitea.jeanlucmakiola.de/makiolaj/docker-node-and-go` (contains both Go and Node/Bun toolchains)
|
||||||
|
- `GOTOOLCHAIN=local` env var set in CI
|
||||||
|
<!-- GSD:stack-end -->
|
||||||
|
|
||||||
|
<!-- GSD:conventions-start source:CONVENTIONS.md -->
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
## Naming Patterns
|
||||||
|
- Package-level source files use the package name: `diunwebhook.go`
|
||||||
|
- Test files follow Go convention: `diunwebhook_test.go`
|
||||||
|
- Test-only export files: `export_test.go`
|
||||||
|
- Entry point: `main.go` inside `cmd/diunwebhook/`
|
||||||
|
- PascalCase for exported functions: `WebhookHandler`, `UpdateEvent`, `InitDB`, `GetUpdates`
|
||||||
|
- Handler functions are named `<Noun>Handler`: `WebhookHandler`, `UpdatesHandler`, `DismissHandler`, `TagsHandler`, `TagByIDHandler`, `TagAssignmentHandler`
|
||||||
|
- Test functions use `Test<FunctionName>_<Scenario>`: `TestWebhookHandler_BadRequest`, `TestDismissHandler_NotFound`
|
||||||
|
- PascalCase structs: `DiunEvent`, `UpdateEntry`, `Tag`
|
||||||
|
- JSON tags use snake_case: `json:"diun_version"`, `json:"hub_link"`, `json:"received_at"`
|
||||||
|
- Package-level unexported variables use short names: `mu`, `db`, `webhookSecret`
|
||||||
|
- Local variables use short idiomatic Go names: `w`, `r`, `err`, `res`, `n`, `e`
|
||||||
|
- Components: PascalCase `.tsx` files: `ServiceCard.tsx`, `AcknowledgeButton.tsx`, `Header.tsx`, `TagSection.tsx`
|
||||||
|
- Hooks: camelCase with `use` prefix: `useUpdates.ts`, `useTags.ts`
|
||||||
|
- Types: camelCase `.ts` files: `diun.ts`
|
||||||
|
- Utilities: camelCase `.ts` files: `utils.ts`, `time.ts`, `serviceIcons.ts`
|
||||||
|
- UI primitives (shadcn): lowercase `.tsx` files: `badge.tsx`, `button.tsx`, `card.tsx`, `tooltip.tsx`
|
||||||
|
- camelCase for regular functions and hooks: `fetchUpdates`, `useUpdates`, `getServiceIcon`
|
||||||
|
- PascalCase for React components: `ServiceCard`, `StatCard`, `AcknowledgeButton`
|
||||||
|
- Helper functions within components use camelCase: `getInitials`, `getTag`, `getShortName`
|
||||||
|
- Event handlers prefixed with `handle`: `handleDragEnd`, `handleNewGroupSubmit`
|
||||||
|
- PascalCase interfaces: `DiunEvent`, `UpdateEntry`, `Tag`, `ServiceCardProps`
|
||||||
|
- Type aliases: PascalCase: `UpdatesMap`
|
||||||
|
- Interface properties use snake_case matching the Go JSON tags: `diun_version`, `hub_link`
|
||||||
|
## Code Style
|
||||||
|
- `gofmt` enforced in CI (formatting check fails the build)
|
||||||
|
- No additional Go linter (golangci-lint) configured
|
||||||
|
- `go vet` runs in CI
|
||||||
|
- Standard Go formatting: tabs for indentation
|
||||||
|
- No ESLint or Prettier configured in the frontend
|
||||||
|
- No formatting enforcement in CI for frontend code
|
||||||
|
- Consistent 2-space indentation observed in all `.tsx` and `.ts` files
|
||||||
|
- Single quotes for strings in TypeScript
|
||||||
|
- No semicolons (observed in all frontend files)
|
||||||
|
- Trailing commas used in multi-line constructs
|
||||||
|
- `strict: true` in `tsconfig.app.json`
|
||||||
|
- `noUnusedLocals: true`
|
||||||
|
- `noUnusedParameters: true`
|
||||||
|
- `noFallthroughCasesInSwitch: true`
|
||||||
|
- `noUncheckedSideEffectImports: true`
|
||||||
|
## Import Organization
|
||||||
|
- The project module is aliased as `diun` in both `main.go` and test files
|
||||||
|
- The blank-import pattern `_ "modernc.org/sqlite"` is used for the SQLite driver in `pkg/diunwebhook/diunwebhook.go`
|
||||||
|
- `@/` maps to `frontend/src/` (configured in `vite.config.ts` and `tsconfig.app.json`)
|
||||||
|
## Error Handling
|
||||||
|
- Handlers use `http.Error(w, message, statusCode)` for all error responses
|
||||||
|
- Error messages are lowercase: `"bad request"`, `"internal error"`, `"not found"`, `"method not allowed"`
|
||||||
|
- Internal errors are logged with `log.Printf` before returning HTTP 500
|
||||||
|
- Decode errors include context: `log.Printf("WebhookHandler: failed to decode request: %v", err)`
|
||||||
|
- Fatal errors in `main.go` use `log.Fatalf`
|
||||||
|
- `errors.Is()` used for sentinel error comparison (e.g., `http.ErrServerClosed`)
|
||||||
|
- String matching used for SQLite constraint errors: `strings.Contains(err.Error(), "UNIQUE")`
|
||||||
|
- API errors throw with HTTP status: `throw new Error(\`HTTP ${res.status}\`)`
|
||||||
|
- Catch blocks use `console.error` for logging
|
||||||
|
- Error state stored in hook state: `setError(e instanceof Error ? e.message : 'Failed to fetch updates')`
|
||||||
|
- Optimistic updates used for tag assignment (update UI first, then call API)
|
||||||
|
## Logging
|
||||||
|
- Startup messages: `log.Printf("Listening on :%s", port)`
|
||||||
|
- Warnings: `log.Println("WARNING: WEBHOOK_SECRET not set ...")`
|
||||||
|
- Request logging on success: `log.Printf("Update received: %s (%s)", event.Image, event.Status)`
|
||||||
|
- Error logging before HTTP error response: `log.Printf("WebhookHandler: failed to store event: %v", err)`
|
||||||
|
- Handler name prefixed to log messages: `"WebhookHandler: ..."`, `"UpdatesHandler: ..."`
|
||||||
|
## Comments
|
||||||
|
- Comments are sparse in the Go codebase
|
||||||
|
- Handler functions have short doc comments describing the routes they handle:
|
||||||
|
- Inline comments used for non-obvious behavior: `// Migration: add acknowledged_at to existing databases`
|
||||||
|
- No JSDoc/TSDoc in the frontend codebase
|
||||||
|
## Function Design
|
||||||
|
- Each handler is a standalone `func(http.ResponseWriter, *http.Request)`
|
||||||
|
- Method checking done at the top of each handler (not via middleware)
|
||||||
|
- Multi-method handlers use `switch r.Method`
|
||||||
|
- URL path parameters extracted via `strings.TrimPrefix`
|
||||||
|
- Request bodies decoded with `json.NewDecoder(r.Body).Decode(&target)`
|
||||||
|
- Responses written with `json.NewEncoder(w).Encode(data)` or `w.WriteHeader(status)`
|
||||||
|
- Mutex (`mu`) used around write operations to SQLite
|
||||||
|
- Custom hooks return object with state and action functions
|
||||||
|
- `useCallback` wraps all action functions
|
||||||
|
- `useEffect` for side effects (polling, initial fetch)
|
||||||
|
- State updates use functional form: `setUpdates(prev => { ... })`
|
||||||
|
## Module Design
|
||||||
|
- Single package `diunwebhook` exports all types and handler functions
|
||||||
|
- No barrel files; single source file `diunwebhook.go` contains everything
|
||||||
|
- Test helpers exposed via `export_test.go` (only visible to `_test` packages)
|
||||||
|
- Named exports for all components, hooks, and utilities
|
||||||
|
- Default export only for the root `App` component (`export default function App()`)
|
||||||
|
- Type exports use `export interface` or `export type`
|
||||||
|
- `@/components/ui/` contains shadcn primitives (`badge.tsx`, `button.tsx`, etc.)
|
||||||
|
## Git Commit Message Conventions
|
||||||
|
- `feat` - new features
|
||||||
|
- `fix` - bug fixes
|
||||||
|
- `docs` - documentation changes
|
||||||
|
- `chore` - maintenance tasks (deps, config)
|
||||||
|
- `refactor` - code restructuring
|
||||||
|
- `style` - UI/styling changes
|
||||||
|
- `test` - test additions
|
||||||
|
<!-- GSD:conventions-end -->
|
||||||
|
|
||||||
|
<!-- GSD:architecture-start source:ARCHITECTURE.md -->
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
## Pattern Overview
|
||||||
|
- Single Go binary serves both the JSON API and the static frontend assets
|
||||||
|
- All backend logic lives in one library package (`pkg/diunwebhook/`)
|
||||||
|
- SQLite database for persistence (pure-Go driver, no CGO)
|
||||||
|
- Frontend is a standalone React SPA that communicates via REST polling
|
||||||
|
- No middleware framework -- uses `net/http` standard library directly
|
||||||
|
## Layers
|
||||||
|
- Purpose: Accept HTTP requests, validate input, delegate to storage functions, return JSON responses
|
||||||
|
- Location: `pkg/diunwebhook/diunwebhook.go` (functions: `WebhookHandler`, `UpdatesHandler`, `DismissHandler`, `TagsHandler`, `TagByIDHandler`, `TagAssignmentHandler`)
|
||||||
|
- Contains: Request parsing, method checks, JSON encoding/decoding, HTTP status responses
|
||||||
|
- Depends on: Storage layer (package-level `db` and `mu` variables)
|
||||||
|
- Used by: Route registration in `cmd/diunwebhook/main.go`
|
||||||
|
- Purpose: Persist and query DIUN events, tags, and tag assignments
|
||||||
|
- Location: `pkg/diunwebhook/diunwebhook.go` (functions: `InitDB`, `UpdateEvent`, `GetUpdates`; inline SQL in handlers)
|
||||||
|
- Contains: Schema creation, migrations, CRUD operations via raw SQL
|
||||||
|
- Depends on: `modernc.org/sqlite` driver, `database/sql` stdlib
|
||||||
|
- Used by: HTTP handlers in the same file
|
||||||
|
- Purpose: Initialize database, configure routes, start HTTP server with graceful shutdown
|
||||||
|
- Location: `cmd/diunwebhook/main.go`
|
||||||
|
- Contains: Environment variable reading, mux setup, signal handling, server lifecycle
|
||||||
|
- Depends on: `pkg/diunwebhook` (imported as `diun`)
|
||||||
|
- Used by: Docker container CMD, direct `go run`
|
||||||
|
- Purpose: Display DIUN update events in an interactive dashboard with drag-and-drop grouping
|
||||||
|
- Location: `frontend/src/`
|
||||||
|
- Contains: React components, custom hooks for data fetching, TypeScript type definitions
|
||||||
|
- Depends on: Backend REST API (`/api/*` endpoints)
|
||||||
|
- Used by: Served as static files from `frontend/dist/` by the Go server
|
||||||
|
## Data Flow
|
||||||
|
- **Backend:** No in-memory state beyond the `sync.Mutex`. All data lives in SQLite. The `db` and `mu` variables are package-level globals in `pkg/diunwebhook/diunwebhook.go`.
|
||||||
|
- **Frontend:** React `useState` hooks in two custom hooks:
|
||||||
|
- No global state library (no Redux, Zustand, etc.) -- state is passed via props from `App.tsx`
|
||||||
|
## Key Abstractions
|
||||||
|
- Purpose: Represents a single DIUN webhook payload (image update notification)
|
||||||
|
- Defined in: `pkg/diunwebhook/diunwebhook.go` (Go struct), `frontend/src/types/diun.ts` (TypeScript interface)
|
||||||
|
- Pattern: Direct JSON mapping between Go struct tags and TypeScript interface
|
||||||
|
- Purpose: Wraps a `DiunEvent` with metadata (received timestamp, acknowledged flag, optional tag)
|
||||||
|
- Defined in: `pkg/diunwebhook/diunwebhook.go` (Go), `frontend/src/types/diun.ts` (TypeScript)
|
||||||
|
- Pattern: The API returns `map[string]UpdateEntry` keyed by image name (`UpdatesMap` type in frontend)
|
||||||
|
- Purpose: User-defined grouping label for organizing images
|
||||||
|
- Defined in: `pkg/diunwebhook/diunwebhook.go` (Go), `frontend/src/types/diun.ts` (TypeScript)
|
||||||
|
- Pattern: Simple ID + name, linked to images via `tag_assignments` join table
|
||||||
|
## Entry Points
|
||||||
|
- Location: `cmd/diunwebhook/main.go`
|
||||||
|
- Triggers: `go run ./cmd/diunwebhook/` or Docker container `CMD ["./server"]`
|
||||||
|
- Responsibilities: Read env vars (`DB_PATH`, `PORT`, `WEBHOOK_SECRET`), init DB, register routes, start HTTP server, handle graceful shutdown on SIGINT/SIGTERM
|
||||||
|
- Location: `frontend/src/main.tsx`
|
||||||
|
- Triggers: Browser loads `index.html` from `frontend/dist/` (served by Go file server at `/`)
|
||||||
|
- Responsibilities: Mount React app, force dark mode (`document.documentElement.classList.add('dark')`)
|
||||||
|
- Location: `POST /webhook` -> `WebhookHandler` in `pkg/diunwebhook/diunwebhook.go`
|
||||||
|
- Triggers: External DIUN instance sends webhook on image update detection
|
||||||
|
- Responsibilities: Authenticate (if secret set), validate payload, upsert event into database
|
||||||
|
## Concurrency Model
|
||||||
|
- A single `sync.Mutex` (`mu`) in `pkg/diunwebhook/diunwebhook.go` guards all write operations to the database
|
||||||
|
- `UpdateEvent()`, `DismissHandler`, `TagsHandler` (POST), `TagByIDHandler` (DELETE), and `TagAssignmentHandler` (PUT/DELETE) all acquire `mu.Lock()` before writing
|
||||||
|
- Read operations (`GetUpdates`, `TagsHandler` GET) do NOT acquire the mutex
|
||||||
|
- SQLite connection is configured with `db.SetMaxOpenConns(1)` to prevent concurrent write issues
|
||||||
|
- Standard `net/http` server handles requests concurrently via goroutines
|
||||||
|
- Graceful shutdown with 15-second timeout on SIGINT/SIGTERM
|
||||||
|
## Error Handling
|
||||||
|
- Method validation: Return `405 Method Not Allowed` for wrong HTTP methods
|
||||||
|
- Input validation: Return `400 Bad Request` for missing/malformed fields
|
||||||
|
- Authentication: Return `401 Unauthorized` if webhook secret doesn't match
|
||||||
|
- Not found: Return `404 Not Found` when row doesn't exist (e.g., dismiss nonexistent image)
|
||||||
|
- Conflict: Return `409 Conflict` for unique constraint violations (duplicate tag name)
|
||||||
|
- Internal errors: Return `500 Internal Server Error` for database failures
|
||||||
|
- Fatal startup errors: `log.Fatalf` on `InitDB` failure
|
||||||
|
- `useUpdates`: catches fetch errors, stores error message in state, displays error banner
|
||||||
|
- `useTags`: catches errors, logs to `console.error`, fails silently (no user-visible error)
|
||||||
|
- `assignTag`: uses optimistic update -- updates local state first, fires API call, logs errors to console but does not revert on failure
|
||||||
|
## Cross-Cutting Concerns
|
||||||
|
<!-- GSD:architecture-end -->
|
||||||
|
|
||||||
|
<!-- GSD:workflow-start source:GSD defaults -->
|
||||||
|
## GSD Workflow Enforcement
|
||||||
|
|
||||||
|
Before using Edit, Write, or other file-changing tools, start work through a GSD command so planning artifacts and execution context stay in sync.
|
||||||
|
|
||||||
|
Use these entry points:
|
||||||
|
- `/gsd:quick` for small fixes, doc updates, and ad-hoc tasks
|
||||||
|
- `/gsd:debug` for investigation and bug fixing
|
||||||
|
- `/gsd:execute-phase` for planned phase work
|
||||||
|
|
||||||
|
Do not make direct repo edits outside a GSD workflow unless the user explicitly asks to bypass it.
|
||||||
|
<!-- GSD:workflow-end -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- GSD:profile-start -->
|
||||||
|
## Developer Profile
|
||||||
|
|
||||||
|
> Profile not yet configured. Run `/gsd:profile-user` to generate your developer profile.
|
||||||
|
> This section is managed by `generate-claude-profile` -- do not edit manually.
|
||||||
|
<!-- GSD:profile-end -->
|
||||||
Reference in New Issue
Block a user