Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions static/dashboard/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ One line per version, newest first. Every REST-facing change bumps the version i
(the app surfaces it via `/system/dashboard/update-check` and the "Update available" pill), so this
file is the human record of what each bump enables. Keep entries short: `version - change (TICKET)`.

- **1.2.12** - Dashboard-update card back end (ADFA-5339, Phase 1 server half). New read-only `GET /system/dashboard/rebuild/log`: the last ~200 lines of `/var/log/dash-rebuild.log`, for the card's expandable "Details" (no file yet = empty log, not an error). `POST /system/dashboard/rebuild` now accepts `{ site: true }`: it refreshes the served landing page in the SAME run via `site-updater.sh`, from the same clone the rebuild's git fetch+reset refreshes, in finalize AFTER the core swap verifies live — so the site matches the new source. The site is a separate, versionless artifact: it never touches the reported version, and a site failure is logged, never a rollback of the (already-verified) core update. Both localhost-only. (ADFA-5339)
- **1.2.11** - `/auth/:service/session` mints the session **for the agent that asks** (ADFA-5361). Calibre-Web (Flask-Login) binds a session to a fingerprint of the User-Agent, so a session minted under dash-node's own agent was rejected on the WebView's first request: the identity was dropped, the `remember_token` deleted, and the card opened as the anonymous Guest — the "logged in as Admin" flash comes from the injected session and renders even then, which is why the auto-login looked like it worked. The route now forwards the caller's `User-Agent` through the whole login handshake (every request, not just the POST — the fingerprint is established on the first one), for Calibre-Web and Kolibri alike. The callers that consume the session themselves (downloads runner, `removeBook`) are unchanged. No User-Agent on the request degrades to the previous behaviour, logged. Same ticket: the books runner's private copy of the Calibre-Web login is gone — it never got the ADFA-5043 `remember_me` and was the drift this whole bug rode in on — so `getCalibreSession` is the one source. (ADFA-5361)
- **1.2.10** - In-proot content-service recovery (ADFA-5343, ADR-5343a §10). New `POST /system/service/:svc/restart`: runs `pdsm restart <svc>` in the one living proot to recover a content service wedged after an environment relaunch (orphaned off proot → `epoll_wait` ENOSYS), for the supported upstream services (mirrors `pdsm_installed_services`; `dash-node` excluded). A server-side watcher auto-heals a present-but-wedged content service (404 = not installed → left alone), cooldown-bounded; the app's future module-card Retry is the manual backstop hitting the same endpoint. Loopback-only, like all of `/k2go-api`. (ADFA-5343)
- **1.2.9** - Cancelable dashboard self-update (ADFA-5333). New `POST /system/dashboard/rebuild/cancel`: stops an in-flight rebuild cleanly while it is still **building** (git fetch + staging build + smoke test — none of which touch the live dashboard) by signaling the detached `setsid` session group; **refused during "promoting"** (the short dist-swap + restart window) so the swap is never interrupted mid-flight, and a no-op (409) when nothing is running. To support this, `tools/rebuild-dashboard.sh` now records its coarse phase (`building`/`promoting`) and its session-leader pid, and its cleanup trap fires on TERM/INT so a canceled run leaves no staging behind. Pairs with the app running the update in the background with a Cancel action (ADFA-5333). (ADFA-5333)
Expand Down
2 changes: 1 addition & 1 deletion static/dashboard/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dashboard-console",
"version": "1.2.11",
"version": "1.2.12",
"description": "",
"main": "index.js",
"scripts": {
Expand Down
35 changes: 32 additions & 3 deletions static/dashboard/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,11 @@ const REBUILD_STATUS_FILE = '/var/run/dash-rebuild.status';
const REBUILD_PHASE_FILE = '/var/run/dash-rebuild.phase';
const REBUILD_PID_FILE = '/var/run/dash-rebuild.pid';
const REBUILD_LOCK_DIR = '/var/run/dash-rebuild.lock';
// ADFA-5339: the rebuild script's own log; the card's expandable Details tails it. Read-only.
const REBUILD_LOG_FILE = '/var/log/dash-rebuild.log';
// How many trailing lines the Details tail returns. A whole rebuild is tens of lines, so this holds
// the entire log with headroom; the client replaces the panel each poll rather than tracking a cursor.
const REBUILD_LOG_TAIL = 200;
// ADFA-5051: the remote branch that update-check compares against AND the rebuild pulls. Defaults to
// mainline; set K2GO_DASH_BRANCH in the dash-node env to point a test box at a feature branch (e.g.
// to exercise the live self-update before merging to main) without touching code. Keep it unset in
Expand All @@ -273,26 +278,50 @@ apiRouter.get('/system/dashboard/rebuild/status', (_req: Request, res: Response)
res.json({ state });
});

// ADFA-5339: read-only tail of the rebuild log, for the card's expandable Details. Returns the last
// REBUILD_LOG_TAIL lines (a whole rebuild fits); the client replaces the panel each poll. No file yet
// (no rebuild ever ran) is not an error — it is an empty log. Localhost-only, like all of /k2go-api.
apiRouter.get('/system/dashboard/rebuild/log', (_req: Request, res: Response): void => {
res.set('Cache-Control', 'no-store');
let lines: string[] = [];
try {
const raw = fs.readFileSync(REBUILD_LOG_FILE, 'utf8');
// Split, drop a trailing empty line, keep the last N. Reading the whole file is fine at this
// size; if the log ever grows unbounded this is the place to switch to a byte-bounded tail.
const all = raw.split('\n');
if (all.length && all[all.length - 1] === '') all.pop();
lines = all.slice(-REBUILD_LOG_TAIL);
} catch { /* no log yet: leave lines empty */ }
res.json({ lines });
});

// Trigger a rebuild. Fire-and-forget: launches the orchestrator DETACHED and returns 202 at once;
// the app then polls /system/version + RestReadiness until the API is back on the new version.
apiRouter.post('/system/dashboard/rebuild', (_req: Request, res: Response): void => {
// ADFA-5339: an optional { site: true } also refreshes the served landing page in the same run. The
// site is a SEPARATE artifact with no version of its own — it is deployed by site-updater.sh from the
// same clone the rebuild's git fetch+reset refreshes, in finalize AFTER the core swap verifies live,
// so it matches the new source. It never touches the reported version; a site failure is logged and
// does NOT fail the (already-verified) core update. See ADFA-5339 §semantics.
apiRouter.post('/system/dashboard/rebuild', (req: Request, res: Response): void => {
let running = false;
try { running = fs.readFileSync(REBUILD_STATUS_FILE, 'utf8').trim() === 'running'; } catch { /* none */ }
if (running) { res.status(409).json({ error: 'a rebuild is already running' }); return; }
if (!fs.existsSync(REBUILD_SCRIPT)) { res.status(500).json({ error: 'rebuild script not found' }); return; }
const updateSite = (req.body as { site?: unknown })?.site === true;
try {
// setsid => own session, so `pdsm restart dash-node` inside the script can't kill this run.
// Pass the tracked branch so the REST rebuild and update-check always agree on the source.
const child = spawn('setsid', ['sh', REBUILD_SCRIPT], {
detached: true, stdio: 'ignore',
env: { ...process.env, K2GO_BRANCH: DASH_BRANCH },
// ADFA-5339: K2GO_SITE gates the finalize-time site deploy; absent/0 keeps the old behaviour.
env: { ...process.env, K2GO_BRANCH: DASH_BRANCH, K2GO_SITE: updateSite ? '1' : '0' },
});
child.unref();
// ADFA-5051: mark "running" synchronously here, before we answer. The detached script also sets
// it, but not until it starts — so a client that polls immediately could otherwise read the
// PREVIOUS run's "done"/"error" and report a false instant success. Writing it now closes that race.
try { fs.writeFileSync(REBUILD_STATUS_FILE, 'running'); } catch { /* best effort */ }
res.status(202).json({ ok: true, state: 'running' });
res.status(202).json({ ok: true, state: 'running', site: updateSite });
} catch (e: any) {
res.status(500).json({ error: e?.message || 'could not start rebuild' });
}
Expand Down
10 changes: 9 additions & 1 deletion static/site/site-updater.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,15 @@ DEST_DIR="/library/www/html/home"

printf "\n${CYAN}Deploying the landing site...${NC}\n"

[ -d "$SITE_SRC" ] || { printf "${RED}Source not found: $SITE_SRC${NC}\n"; exit 1; }
# ADFA-5339: validate the source is the REAL site BEFORE touching the destination. The mirror below
# is destructive (it wipes DEST first), so a mis-resolved SITE_SRC would delete the served site and
# copy the wrong tree with no rollback. `[ -d ]` is not enough — a wrong-but-existing dir (e.g. /root
# when SITE_SRC resolved empty) passes it; require the landing page's own index.html so only the
# actual site can proceed. Caught on device when this script was invoked with sh instead of bash.
[ -f "$SITE_SRC/index.html" ] || {
printf "${RED}Refusing to deploy: %s is not the site (no index.html). Nothing was changed.${NC}\n" "$SITE_SRC"
exit 1
}
mkdir -p "$DEST_DIR"

# Mirror: clear the destination, then copy via tar (proot-safe). Excludes this script and any *.sh.
Expand Down
15 changes: 15 additions & 0 deletions tools/rebuild-dashboard.sh
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,21 @@ if verify_live; then
# nginx reads /etc/nginx/conf.d, not /library/dashboard, so mirror the vhost then reload nginx.
[ -f "$LIVE/dash-node-nginx.conf" ] && { cp -f "$LIVE/dash-node-nginx.conf" "$NGINX_CONF_DIR/dash-node-nginx.conf"; chmod 0600 "$NGINX_CONF_DIR/dash-node-nginx.conf"; }
/usr/local/bin/pdsm restart nginx >>"$LOG" 2>&1 || log "warn: pdsm restart nginx returned non-zero"
# ADFA-5339: optionally refresh the served landing page, from the SAME clone the git fetch+reset
# above just refreshed, so it matches the new source. Runs only here — after the core swap has
# verified live — and is best-effort: the site is a separate, versionless artifact, so a failure is
# a warning, never a rollback of the core update that already succeeded. K2GO_SITE=1 opts in.
if [ "${K2GO_SITE:-0}" = "1" ]; then
SITE_UPDATER="$CLONE_DIR/static/site/site-updater.sh"
if [ -f "$SITE_UPDATER" ]; then
log "updating the served website (site-updater)"
# site-updater.sh is a bash script (uses ${BASH_SOURCE[0]}, arrays); run it with bash, not
# this sh — dash trips on the bash-isms and mis-resolves its own source dir (ADFA-5339).
bash "$SITE_UPDATER" >>"$LOG" 2>&1 || log "warn: website update failed (core update succeeded)"
else
log "warn: K2GO_SITE=1 but site-updater not found at $SITE_UPDATER (core update succeeded)"
fi
fi
log "rebuild complete"
rm -rf "$BACKUP"
set_status "done"
Expand Down
Loading