Diun Web Updater — mobile web UI for one-click container updates#1
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
/api/diun/webhook(bearer-token auth). Your existing Discord notifier is unaffected — this is an additional notifier.docker compose pull+up -dfor that service, streaming logs live over SSE, then records history and clears the badge.Stack
better-sqlite3,dockerode+docker composeCLI. Single-password signed-cookie auth (no user DB, no auth library).docker-cli+docker-cli-compose),docker-compose.ymlwith the required same-path stacks mount.Built in reviewed work packages
WP0scaffold + API contract ·WP1docker/reconcile (33 tests) ·WP2webhook + reconciliation API (39 tests) ·WP3auth + SSE + update routes (47 tests) ·WP4frontend core ·WP5history/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)
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