← Back

Patch notes — first production deploy, fixes & Projects (2026-06-10 session 3)

Branch: main. This session took skui.io from “builds but never deployed” to live in production at https://skui.io, fixed the bugs that surfaced once it was actually running, and moved the Projects page into the admin/DB.


1 — Compose-based production deploy ✅

Problem: build.yml’s deploy job did a single docker run of only the web container with a named volume — no collector, no socket-proxy, and not the compose stack.

Fix (.gitea/workflows/build.yml): rewrote the deploy job to be compose-based:

  • scp compose.yml/storage/skui.io/compose.yml
  • SSH to deploy.skui.io as gitea-nextcloud: mkdir -p data, auto-detect the nginx network from nc-nginx-1 → export as NC_NETWORK, docker compose pull && up -d --remove-orphans, docker image prune -f.

Data volume → bind mount (compose.yml): web and collector now mount ./data:/data (resolves to /storage/skui.io/data) instead of the named skui-data volume, so the SQLite DB

  • uploads live in a host folder. Container runs as root → writes through regardless of host ownership (gitea-nextcloud:users).

No server .env needed: init_db.py seeds admin/skuiio; COLLECTOR_TOKEN defaults to skui-internal; NC_NETWORK is auto-detected.

2 — nginx routing + reload (in the nc repo) ✅

Problem: the live site 502’d. Two causes:

  1. nc/nginx/nginx.conf still proxied skui.io → the dead skui.io_site:80 container.
  2. After fixing the config, nginx kept serving the old in-memory config: nginx.conf is bind-mounted, so docker compose up -d nginx is a no-op when only the file changed.

Fixes:

  • Point the vhost at skui_web:8099 with lazy Docker DNS so nginx still boots if the upstream is briefly down:
    resolver 127.0.0.11 valid=30s;
    set $skui_upstream skui_web;
    proxy_pass http://$skui_upstream:8099;
    
  • nc/.gitea/workflows/deploy.yml now runs nginx -t && nginx -s reload after copying the config (the up -d no-op was the silent-502 culprit).

3 — Change-password 403 / 502 ✅ (two layered bugs)

Reported: changing the admin password returned 403 bad csrf, then 502.

  1. Missing CSRF tokenpassword_page was the only admin form rendered without the hidden <input name=csrf>, so every POST failed the generic CSRF check. Added the token.
  2. Latent crash, masked by #1 — once CSRF passed, the handler ran for the first time and crashed: require_user() returns the username string, but the handler indexed it as user["username"]TypeError → 502. Use the string directly.

Verified non-destructively: a wrong current password now returns “Current password is incorrect” (200), not 403/502.

4 — Container dismiss (no auto re-add) ✅

Problem: the collector INSERT OR IGNOREs every live container every ~30 s, so deleting one in admin just got re-added on the next push.

Fix: new tracked_containers.dismissed column (idempotent ALTER TABLE migration). “Remove” now sets dismissed=1, enabled=0, public=0, auto=0 — a tombstone the push can’t resurrect. Dismissed rows are filtered out of the admin list, status items, /api/*, and the dashboard count. Verified across repeated pushes on a temp DB.

5 — Projects moved to the DB ✅

Goal: add/remove projects from admin instead of editing data/projects.yaml + rebuilding.

  • New projects table (name, tagline, description, status, tags, url, sort, enabled), seeded from the former yaml (idempotent).
  • GET /api/projects (public) + q_projects() helper (tags split from comma-separated → list).
  • Admin CRUD at /admin/projects, placed between Notes and Working On (server nav + the public admin dropdown in sidebar-hydrate.js).
  • /projects/ hydrates its cards from /api/projects via sidebar-hydrate.js; the build-time yaml render stays as a no-JS fallback (DB is the source of truth).

Files touched

RepoFiles
skui.io.gitea/workflows/build.yml, compose.yml, server.py, scripts/init_db.py, layouts/projects/list.html, assets/js/sidebar-hydrate.js
ncnginx/nginx.conf, .gitea/workflows/deploy.yml

Follow-ups / notes

  • data/projects.yaml is now only a no-JS fallback; it will drift from the DB. Option to drop it and render an empty grid that JS fills.
  • Default admin password is still skuiio until changed via /admin/password.