Skip to content

Diun Web Updater — mobile web UI for one-click container updates#1

Merged
StrandedTurtle merged 9 commits into
mainfrom
claude/youthful-pasteur-2f6s72
Jun 23, 2026
Merged

Diun Web Updater — mobile web UI for one-click container updates#1
StrandedTurtle merged 9 commits into
mainfrom
claude/youthful-pasteur-2f6s72

Conversation

@StrandedTurtle

Copy link
Copy Markdown
Owner

What this is

A self-hosted, mobile-first web UI that lists your Docker containers, shows an update-available indicator, and updates a container with one tap (pull + recreate via its compose file). Manual only — no watchtower-style auto-updates. Built to replace the "Diun → Discord at 9am → open Dockge on mobile" workflow.

How it works

  • Diun's webhook notifier POSTs update events to /api/diun/webhook (bearer-token auth). Your existing Discord notifier is unaffected — this is an additional notifier.
  • The dashboard reconciles those events against live Docker state (each container's running digest), so the badge is self-correcting (updating elsewhere, e.g. Dockge, clears it).
  • Clicking Update runs docker compose pull + up -d for that service, streaming logs live over SSE, then records history and clears the badge.

Note: the original idea assumed a Diun REST API (DIUN_WEB, /api/v1/images) that does not exist — Diun only exposes a webhook notifier, a gRPC control API, Prometheus metrics, and a bbolt DB. This implementation uses the documented webhook path.

Stack

  • Server: Node 22 / Express (ESM), better-sqlite3, dockerode + docker compose CLI. Single-password signed-cookie auth (no user DB, no auth library).
  • Client: React + Vite PWA, dark/light theme, mobile bottom-nav, SSE live logs.
  • Packaging: 2-stage Dockerfile (docker-cli + docker-cli-compose), docker-compose.yml with the required same-path stacks mount.

Built in reviewed work packages

WP0 scaffold + API contract · WP1 docker/reconcile (33 tests) · WP2 webhook + reconciliation API (39 tests) · WP3 auth + SSE + update routes (47 tests) · WP4 frontend core · WP5 history/settings/light-theme/mobile-nav · review fixes · docs · CI.

Tests / CI

Server suite 47/47 (node --test: ref normalization, reconciliation, auth). Client builds clean. This PR adds a GitHub Actions workflow running both.

Key safety notes (see README)

  • The stacks dir must be mounted at the same path host:container (otherwise relative compose volumes break on recreate).
  • Holding the Docker socket is root-equivalent on the host — keep the app behind auth and on an internal network.

Not yet verified against a live Docker daemon

The build/CI environment has no Docker daemon, so docker compose pull/up, SSE during a real update, container listing, and volume-survival on recreate are covered only by unit tests + mocked boots. The README includes a 5-minute host smoke test to validate on a throwaway stack before trusting it on important ones.

🤖 Generated with Claude Code


Generated by Claude Code

strandedturtle and others added 9 commits June 22, 2026 15:24
Server (Express, ESM, Node 22): config with required-var validation,
better-sqlite3 schema + query helpers (update_events, update_history,
pinned), and a minimal bootable index.js (health check + static SPA
serving with /api-excluding fallback). Routes are stubbed for later WPs.

Client (Vite + React + vite-plugin-pwa): placeholder app that builds to
client/dist, manifest + generated PWA icons.

Infra: 2-stage Dockerfile (docker-cli + docker-cli-compose v2 plugin),
docker-compose.yml with the required same-path stacks mount, .env.example,
.dockerignore, README, and API_CONTRACT.md as the shared interface for all
later work packages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Lj6nYJQDtLaZFvvEQJGM4
reconcile.js: pure, dependency-free helpers — normalizeRef (canonical
registry/repo:tag with Hub-namespace vs registry-host disambiguation,
port-vs-tag handling, and digest stripping), isUpdateAvailable, and
digestsEqual. Covered by 33 unit tests (server/test/reconcile.test.js).

docker.js: dockerode-based reads (listContainers with currentDigest from
RepoDigests, compose info from labels with a STACKS_DIR fallback scan) and
updateContainer — compose-managed pull + up -d streamed via spawn with
argv arrays (shell:false, no interpolation), plus a best-effort standalone
pull + recreate fallback. Streams output line-by-line; returns digests and
status without touching the DB.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Lj6nYJQDtLaZFvvEQJGM4
React/Vite SPA built to API_CONTRACT.md: api.js fetch wrapper
(credentials:'include', ApiError), App with session-gated routing,
password-only AuthPage, Dashboard with pending-count badge and Update All,
and UpdateCard with pin toggle + expandable live log.

Update flow: useUpdateRunner wraps startUpdate + useSSE so the per-card
button and the sequential Update-All batch both resolve only on the
terminal SSE result event (not when the POST returns). Pin toggle sends the
raw image ref; the server normalizes it. Dark theme + mobile-first
responsive styling via CSS custom properties; history/settings/light-theme
deferred to WP5 (stubs in place).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Lj6nYJQDtLaZFvvEQJGM4
webhook.js: POST /api/diun/webhook with constant-time, length-guarded
bearer-token auth (DIUN_WEBHOOK_TOKEN); validates the payload, normalizes
the image ref, and records the event.

containers-service.js: pure buildContainerItems() merging live container
data with the latest unresolved event + pin state — computes
updateAvailable/availableDigest and flags already-applied refs for
resolution. Covered by 6 new unit tests (39 total passing).

routes/api.js: GET /api/containers (503 on docker-unavailable, applies
event resolution), GET /api/history(/:name), GET /api/pinned, POST /api/pin
and DELETE /api/pin/:ref (refs normalized server-side so raw and canonical
refs are equivalent).

index.js: mounts the webhook (public) and api routers, with markers for
WP3 to insert session-cookie auth before the api router and the SPA
fallback kept last.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Lj6nYJQDtLaZFvvEQJGM4
auth.js: single shared-password login with constant-time, length-guarded
comparison; signed httpOnly SameSite=Lax cookie carrying a server-validated
expiry; requireAuth middleware scoped to /api/* (static assets pass
through). login/logout/me handlers.

sse.js: in-memory per-container update session with full buffer + replay so
subscribers that connect after POST still get every log line and the final
result; 30s post-finish grace window; per-subscriber disconnect cleanup.

routes/update.js: POST /api/update/:name (existence check → 404/503/500,
409 if already running, fire-and-forget runner that records history and can
never crash the process) and GET /api/update/:name/stream.

index.js: cookie-parser now uses SESSION_SECRET; mounts authRouter →
requireAuth → apiRouter → updateRouter, keeping the webhook + health public
and the SPA fallback last. 47/47 tests passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Lj6nYJQDtLaZFvvEQJGM4
useTheme.js: shared module-level theme state (header + settings toggles
stay in sync without a context), reads localStorage then
prefers-color-scheme, applies data-theme to <html> from first paint.
themes.css gains a [data-theme="light"] block so every component re-themes
automatically.

HistoryPage + HistoryRow: paginated update log (Load more), client-side
name filter, expandable rows, UTC-aware relative timestamps.
SettingsPage: theme switch, pinned-image management (list + unpin), and an
About section with a server-health indicator (manual-only updates noted).
BottomNav: mobile 3-tab navigation, hidden at >=768px.

App.jsx now routes to the real pages and renders BottomNav; the WP4 stubs
are removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Lj6nYJQDtLaZFvvEQJGM4
On a successful update, resolve any pending Diun event for the image's
normalized ref. Previously the dashboard indicator only cleared when the
container's RepoDigest exactly matched the event digest, but Diun frequently
reports a manifest-list (multi-arch) digest while the running container
carries a platform-specific digest — so the badge could never clear after
updating such images. Now a successful pull+recreate always resolves it.

Also cap the history `limit` query param at 500 to bound page size.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Lj6nYJQDtLaZFvvEQJGM4
Add a step-by-step deployment walkthrough (env + secrets, the same-path
stacks mount with the why, build/start, Diun webhook config, shared network,
optional Cloudflare Tunnel), a usage guide for every screen (updates, pin,
history, settings, PWA install), a configuration reference, security notes,
a troubleshooting section, and known limitations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Lj6nYJQDtLaZFvvEQJGM4
Runs on pushes to main / claude/** and on pull requests: one job runs the
server test suite (node --test), another installs and builds the client.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Lj6nYJQDtLaZFvvEQJGM4
@StrandedTurtle StrandedTurtle merged commit cbdc75e into main Jun 23, 2026
4 checks passed
@StrandedTurtle StrandedTurtle deleted the claude/youthful-pasteur-2f6s72 branch June 23, 2026 12:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant