Files
DiunDashboard/.planning/research/SUMMARY.md

18 KiB

Project Research Summary

Project: DiunDashboard — PostgreSQL milestone + UX improvements Domain: Self-hosted container image update monitoring dashboard (Go backend + React SPA) Researched: 2026-03-23 Confidence: HIGH (stack and architecture sourced from direct codebase analysis and verified package versions; features MEDIUM due to tool restrictions during research)

Executive Summary

DiunDashboard is a self-hosted Go + React dashboard that receives DIUN webhook events and presents a persistent, acknowledgeable list of container images with available updates. The current milestone covers two parallel tracks: (1) fixing active data-correctness bugs and adding PostgreSQL as an alternative to SQLite, and (2) delivering UX improvements users need before the tool is genuinely usable at scale (bulk dismiss, search/filter, new-update indicators). Both tracks have well-understood solutions rooted in established Go patterns — the engineering risk is low provided the work is sequenced correctly.

The recommended approach is a strict dependency-first build order. The SQLite data-integrity bugs (INSERT OR REPLACE silently deleting tag assignments, missing FK pragma) must be fixed before any other work because they undermine trust in the tool and will complicate the subsequent refactor if left in. The backend refactor — introducing a Store interface and a Server struct to replace package-level globals — is the foundational prerequisite for PostgreSQL support, parallel test execution, and reliable UX features. PostgreSQL is then a clean additive step: implement PostgresStore, wire the DATABASE_URL env var into main.go, and provide dialect-appropriate SQL in the new store file.

The primary risk is dialect leakage: SQLite-specific SQL (datetime('now'), INSERT OR REPLACE, ? placeholders, AUTOINCREMENT, PRAGMA) scattered across handler functions will silently break on PostgreSQL if the Store interface abstraction is not in place before any PostgreSQL code is written. Secondary risks are a missing versioned migration runner (which leaves existing user databases in an unknown state on upgrade) and bulk dismiss implemented as N sequential API calls rather than a single transactional endpoint. Both risks have well-documented mitigations and are easy to prevent if addressed in the correct phase.


Key Findings

The existing stack is largely correct and requires minimal additions. The PostgreSQL driver is github.com/jackc/pgx/v5/stdlib (v5.9.1, verified 2026-03-22) — the de-facto community standard. Its stdlib adapter makes it a drop-in for the existing *sql.DB code path; the native pgx interface is not needed. lib/pq is explicitly maintenance-only and must not be used. For schema migrations, github.com/golang-migrate/migrate/v4 (v4.19.1) supports both the project's modernc.org/sqlite (pure-Go, no CGO) and pgx/v5 backends via separately maintained sub-packages. The existing SQLite driver should be upgraded from v1.46.1 to v1.47.0.

Core technologies:

  • github.com/jackc/pgx/v5/stdlib v5.9.1: PostgreSQL driver — only viable current option; lib/pq is maintenance-only
  • github.com/golang-migrate/migrate/v4 v4.19.1: schema migrations — explicit modernc.org/sqlite support satisfies no-CGO constraint
  • modernc.org/sqlite v1.47.0: existing SQLite driver (upgrade from v1.46.1) — must remain for pure-Go cross-compilation

No ORM. No query-builder library. The query set is 8 operations across 3 tables; raw SQL per store implementation is simpler, easier to audit, and matches the existing project style.

Configuration: DATABASE_URL env var (when set, activates PostgreSQL mode). DB_PATH retained for SQLite. No separate DB_DRIVER variable needed.

Expected Features

The feature research produced a clear priority stack grounded in the documented concerns and self-hosted dashboard conventions. Data integrity is a prerequisite for everything else — broken data collapses user trust faster than any missing feature.

Must have (table stakes):

  • SQLite data integrity fix (UPSERT + FK pragma) — existing bug silently deletes tag assignments on every DIUN event
  • Bulk acknowledge: dismiss all + dismiss by group — O(n) clicking for 20+ images causes abandonment
  • Search + filter by image name, status, and tag — standard affordance for any list exceeding 10 items
  • New-update indicator (badge/counter) and page/tab title count — persistent visibility is the core value proposition
  • PostgreSQL support — required for users running Coolify or other Postgres-backed infrastructure

Should have (differentiators):

  • Toast notification on new update arrival during polling — shares implementation with new-update indicator
  • Sort order controls (newest first, by name, by registry) — pure frontend, no backend change
  • Light/dark theme toggle — low complexity, removes a known complaint
  • Drag handle always visible (accessibility) — currently hover-only, invisible on touch/keyboard
  • Optimistic UI rollback on tag assignment failure — current code has no error recovery path

Defer (v2+):

  • Data retention / auto-cleanup of acknowledged entries — real concern but not urgent for most users
  • Alternative tag assignment dropdown — drag-and-drop exists; dropdown is an accessibility improvement, not a blocker
  • Browser notification API — high UX risk, low reward vs. badge approach
  • Auto-grouping by Docker stack — requires Docker socket access; different scope entirely

Architecture Approach

The architecture follows a standard Go repository interface pattern. The current monolith (diunwebhook.go with package-level var db *sql.DB and var mu sync.Mutex) is extracted into a Store interface implemented by two concrete types (SQLiteStore, PostgresStore), with HTTP handlers moved to methods on a Server struct that holds a Store. This pattern eliminates global state, enables parallel tests without resets, and enforces a strict boundary: handlers never see SQL, store implementations never see HTTP.

Major components:

  1. Store interface (store.go) — contract for all persistence; 11 methods covering updates, tags, and assignments
  2. SQLiteStore (sqlite.go) — SQLite-specific SQL, sync.Mutex, SetMaxOpenConns(1), PRAGMA foreign_keys = ON
  3. PostgresStore (postgres.go) — PostgreSQL-specific SQL, pgx connection pool, no mutex, db.BeginTx for atomicity
  4. Server struct (server.go) — holds Store and secret; all HTTP handlers are methods on Server
  5. models.go — shared DiunEvent, UpdateEntry, Tag structs with no imports beyond stdlib
  6. main.go — sole location where backend is chosen (DATABASE_URL present → PostgreSQL, absent → SQLite)
  7. Frontend SPA — unchanged API contract; communicates with backend via /api/* only

Key pattern: SQLiteStore retains sync.Mutex; PostgresStore does not. These are structurally different and must not share a mutex.

Migration strategy: Separate DDL per dialect (migrations/sqlite/ and migrations/postgres/). Both embedded in the binary via //go:embed. A versioned schema_migrations table prevents re-running migrations on existing databases and makes upgrade failures visible.

Critical Pitfalls

  1. SQLite-specific SQL leaking into shared codedatetime('now'), INSERT OR REPLACE, ? placeholders, AUTOINCREMENT, and PRAGMA all fail on PostgreSQL. Prevention: Store interface forces all SQL into store files; handlers call named methods only; integration tests run both stores.

  2. INSERT OR REPLACE silently deleting tag assignments — SQLite implements this as DELETE + INSERT, which cascades to tag_assignments and erases the user's groupings on every DIUN event. Prevention: replace with INSERT ... ON CONFLICT(image) DO UPDATE SET ...; add PRAGMA foreign_keys = ON; add regression test asserting tag survives a second UpdateEvent call.

  3. Global package-level state blocks dual-DB without struct refactorvar db *sql.DB at package scope means there is only one DB handle; PostgreSQL cannot be added without introducing if dbType == "postgres" branches across every handler. Prevention: Server struct with injected Store must precede all PostgreSQL work.

  4. No versioned migration runner — silent ALTER TABLE with discarded errors leaves existing SQLite databases in an unknown state on upgrade. Prevention: schema_migrations version table; log every migration attempt; never swallow DDL errors.

  5. Bulk dismiss implemented as N sequential API calls — 30 acknowledged images = 30 round trips, 30 mutex acquisitions, 30 React re-renders with potential flickering and partial-state failure. Prevention: design POST /api/updates/acknowledge-bulk endpoint first; one call, one transaction, one state update.


Implications for Roadmap

Based on the dependency graph from feature and architecture research, the milestone decomposes into four phases. The ordering is non-negotiable: each phase is a prerequisite for the next.

Phase 1: Data Integrity Fixes

Rationale: The INSERT OR REPLACE bug is active in production and deletes user data on every DIUN event. Fixing it before the refactor means the bug-fix tests become the regression suite that validates the refactor did not regress behavior. No other work is credible until the data layer is correct. Delivers: Trustworthy persistence — tag assignments survive new DIUN events; FK enforcement works; acknowledged state is preserved correctly. Addresses: Table-stakes feature "Data integrity across restarts"; Pitfalls 2, 10 (timestamp fix can be included here). Avoids: Shipping the bug in both DB paths; losing the fix in refactor noise. Research flag: None needed — the fix is a 3-line SQL change with a clear regression test. Standard patterns apply.

Phase 2: Backend Refactor — Store Interface + Server Struct

Rationale: The global state architecture makes PostgreSQL support structurally impossible without this refactor. All subsequent work (PostgreSQL implementation, parallel test execution, safer UX features) depends on this change. The refactor must be behavior-neutral — all existing tests pass before PostgreSQL is introduced. Delivers: Store interface, SQLiteStore implementation, Server struct with constructor injection, models in models.go. Zero behavior change for existing SQLite users. Uses: Existing modernc.org/sqlite; database/sql standard library; no new dependencies. Implements: Core architecture pattern from ARCHITECTURE.md; eliminates Pitfall 3 (global state) and Pitfall 4 (migration runner) in one phase. Avoids: Introducing PostgreSQL and refactoring simultaneously (would make failures ambiguous). Research flag: None needed — this is a standard Go repository interface pattern with well-documented prior art.

Phase 3: PostgreSQL Support

Rationale: With the Store interface in place, adding PostgreSQL is additive: write PostgresStore, add pgx/v5/stdlib as a dependency, add DATABASE_URL to main.go and Docker Compose. The interface boundary guarantees no SQLite-specific SQL can appear in handlers. Delivers: PostgresStore implementing all Store methods with PostgreSQL dialect SQL; DATABASE_URL env var wired through main.go; separate dialect migration files; updated compose.dev.yml with optional postgres profile; documentation. Uses: github.com/jackc/pgx/v5/stdlib v5.9.1; github.com/golang-migrate/migrate/v4 v4.19.1; separate migrations/sqlite/ and migrations/postgres/ directories. Avoids: Pitfalls 1, 5, 8, 10, 11 — all are mitigated by the Store interface + per-dialect SQL + connection pool (no mutex) in PostgresStore. Research flag: Verify exact import path for pgx/v5/stdlib during implementation. The database/sql compatibility layer is standard but the import string should be confirmed against pkg.go.dev before coding.

Phase 4: UX Improvements

Rationale: These features are independent of the DB work but grouped together because they share the frontend codebase and several features share implementation logic (new-update indicator and toast notification use the same poll-comparison logic; bulk dismiss all and bulk dismiss by group share the same API endpoint design). Deferring UX until after the backend is correct means UX tests run against a trustworthy data layer. Delivers: Bulk acknowledge (all + by group) with a single backend endpoint (POST /api/updates/acknowledge-bulk); search and filter by name/status/tag (frontend-only); new-update badge/counter and page title count; light/dark theme toggle; drag handle always-visible fix; optimistic UI rollback with user-visible error on tag assignment failure. Uses: Existing React 19 + Tailwind + shadcn/ui stack; no new frontend dependencies expected. Avoids: Pitfalls 6, 7, 9 — optimistic rollback, bulk endpoint, accessible drag handle. Research flag: None needed for search/filter/theme/accessibility (standard patterns). The bulk acknowledge endpoint needs clear API contract design before frontend implementation begins — define the request/response shape first.

Phase Ordering Rationale

  • Phase 1 before Phase 2: Bug fix tests become the regression suite; the refactor cannot accidentally regress behavior it has not validated first.
  • Phase 2 before Phase 3: The Store interface is a structural prerequisite. PostgreSQL added to the monolith produces unmaintainable dialect branches.
  • Phase 3 before Phase 4 (or parallel after Phase 2): UX features are mostly frontend and do not depend on PostgreSQL. However, the bulk acknowledge endpoint (AcknowledgeAll, AcknowledgeByTag) must be in the Store interface, which is finalized in Phase 2. Phase 4 frontend work can start once Phase 2 is merged; Phase 3 and Phase 4 can proceed in parallel.
  • Never: Mix refactor and new feature in the same commit. Each phase should be independently reviewable and revertable.

Research Flags

Phases likely needing deeper research during planning:

  • Phase 3 (PostgreSQL): Verify pgx/v5/stdlib import path (github.com/jackc/pgx/v5/stdlib) against pkg.go.dev before adding the dependency. Confirm golang-migrate database/sqlite sub-package still uses modernc.org/sqlite (not mattn/go-sqlite3) in v4.19.1 — this was verified but should be re-confirmed at time of implementation.

Phases with standard patterns (skip research-phase):

  • Phase 1 (Bug fixes): 3-line SQL change with a clear regression test; no research needed.
  • Phase 2 (Refactor): Standard Go repository interface pattern; no research needed.
  • Phase 4 (UX): All features use existing stack (React, Tailwind, shadcn/ui); no new technologies introduced.

Confidence Assessment

Area Confidence Notes
Stack HIGH All versions verified via pkg.go.dev at time of research (2026-03-23); pgx v5.9.1 published 2026-03-22; golang-migrate v4.19.1 confirms modernc.org/sqlite
Features MEDIUM Feature priorities derived from direct codebase audit (CONCERNS.md, PROJECT.md) — HIGH confidence; competitive landscape analysis (Portainer, Uptime Kuma patterns) from training data only — MEDIUM
Architecture HIGH Based on direct analysis of pkg/diunwebhook/diunwebhook.go and cmd/diunwebhook/main.go; Store interface pattern is a well-established Go idiom with no ambiguity
Pitfalls HIGH (backend) / MEDIUM (frontend) Backend pitfalls sourced from direct code evidence (line numbers cited); PostgreSQL dialect differences from training knowledge — recommend verifying $1 placeholder syntax before implementation; frontend pitfalls sourced from direct code analysis

Overall confidence: HIGH

Gaps to Address

  • PostgreSQL $1 placeholder syntax: PITFALLS.md flags this as MEDIUM confidence from training knowledge. Verify against the pgx/v5/stdlib documentation before writing any PostgreSQL query strings.
  • golang-migrate CGO status at v4.19.1: Confirmed at research time that database/sqlite sub-package uses modernc.org/sqlite; re-confirm at implementation time that this has not changed in a patch release.
  • Competitive feature validation: The UX feature priorities are based on self-hosted dashboard patterns (Portainer, Uptime Kuma) from training data. If the roadmapper wants higher confidence on feature ordering, a quick review of current Portainer CE and Uptime Kuma changelogs would validate the bulk-dismiss and search/filter priorities.
  • golang-migrate vs hand-rolled migration runner: PITFALLS.md notes a 30-line hand-rolled runner is sufficient for this project's scale. STACK.md recommends golang-migrate. Either is valid — the roadmap phase should make a decision and commit to one approach before implementation begins to avoid scope creep.

Sources

Primary (HIGH confidence)

Secondary (MEDIUM confidence)

  • Training-data knowledge of pgx/v5/stdlib database/sql adapter pattern — standard approach, verify import path at implementation
  • Training-data knowledge of Portainer CE, Uptime Kuma, Dockcheck-web UX patterns — feature prioritization for self-hosted dashboards

Tertiary (LOW confidence)

  • None

Research completed: 2026-03-23 Ready for roadmap: yes