furtka/assets/www/settings/index.html
Daniel Maksymilian Syrnicki c080764c7e
All checks were successful
Build ISO / build-iso (push) Successful in 17m5s
CI / lint (push) Successful in 27s
CI / test (push) Successful in 40s
CI / validate-json (push) Successful in 25s
CI / markdown-links (push) Successful in 12s
fix(furtka): move assets/ to repo top level so Caddy + systemd find it
Root cause of today's 403 on a fresh install: assets/ lived inside the
Python package at furtka/assets/, so the resource-manager tarball
extracted to /opt/furtka/versions/<ver>/furtka/assets/. But Caddyfile
has `root * /opt/furtka/current/assets/www`, systemd units point at
/opt/furtka/current/assets/bin/furtka-status, and the install-time
`systemctl link /opt/furtka/current/assets/systemd/*.service` expected
the top-level layout. All three found nothing:

- Caddy → 403 Forbidden (empty/missing document root)
- systemctl link → silent no-op, nothing ever linked into
  /etc/systemd/system/
- furtka-api.service + furtka-reconcile.service → "inactive" because
  they were never registered

Nothing in the Python package ever imported furtka.assets — these are
shell scripts, HTML/CSS, systemd units, and a Caddyfile, which is
config data, not package data. Promoting assets/ to the repo root
matches how it's referenced everywhere downstream and eliminates the
path mismatch.

Changes:
- git mv furtka/assets assets
- iso/build.sh: tarball-staging step now also `cp -a "$REPO_ROOT/assets"`
  so the tarball ships ./assets at its root, and the live-ISO copy
  reads from $REPO_ROOT/assets instead of $REPO_ROOT/furtka/assets.
- scripts/build-release-tarball.sh: same for release tarballs.
- webinstaller/app.py: _resolve_assets_dir's dev fallback walks one
  level up to REPO_ROOT/assets/.
- tests/test_webinstaller_assets.py: ASSETS constant updated.

Tests still green (150/150) because both paths were fs-level — no
code imports changed. Next ISO build will land assets at the path
everything downstream expects.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 15:26:10 +02:00

203 lines
8.2 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Settings · Furtka</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="/style.css">
</head>
<body>
<main class="wrap">
<nav class="nav">
<a class="brand" href="/">Furtka</a>
<div class="nav-links">
<a href="/">Home</a>
<a href="/apps">Apps</a>
<a href="/settings/" aria-current="page">Settings</a>
</div>
</nav>
<h1>Settings</h1>
<p class="lede">What this box knows about itself.</p>
<section>
<h2>About this box</h2>
<div class="card">
<dl class="kv">
<dt>Hostname</dt><dd id="set-hostname"></dd>
<dt>IP address</dt><dd id="set-ip"></dd>
<dt>Furtka version</dt><dd id="set-version"></dd>
<dt>Kernel</dt><dd id="set-kernel"></dd>
<dt>RAM</dt><dd id="set-ram"></dd>
<dt>Docker</dt><dd id="set-docker"></dd>
<dt>Uptime</dt><dd id="set-uptime"></dd>
</dl>
</div>
</section>
<section>
<h2>Furtka updates</h2>
<div class="card">
<dl class="kv">
<dt>Installed</dt><dd id="upd-current"></dd>
<dt>Latest available</dt><dd id="upd-latest"></dd>
</dl>
<div class="update-actions">
<button id="check-updates-btn" class="secondary">Check for updates</button>
<button id="apply-update-btn" hidden>Update now</button>
</div>
<p id="update-status" class="hint"></p>
</div>
</section>
<section>
<h2>Appearance</h2>
<div class="card">
<dl class="kv">
<dt>Theme</dt><dd>Follows your system setting</dd>
<dt>Language</dt><dd>English</dd>
</dl>
</div>
</section>
<section>
<h2>Coming next</h2>
<div class="coming">
<p class="hint">Controls we're building — follow progress on <a href="https://furtka.org">furtka.org</a>.</p>
<a href="https://furtka.org/#planned">Reboot</a>
<a href="https://furtka.org/#planned">Shut down</a>
<a href="https://furtka.org/#planned">Change hostname</a>
<a href="https://furtka.org/#planned">Backup</a>
<a href="https://furtka.org/#planned">User accounts</a>
<a href="https://furtka.org/#planned">Remote access</a>
</div>
</section>
<footer>
<p>Furtka · <a href="https://furtka.org">furtka.org</a></p>
</footer>
</main>
<script>
async function refresh() {
try {
const r = await fetch('/status.json', { cache: 'no-store' });
if (!r.ok) return;
const s = await r.json();
document.getElementById('set-hostname').textContent = s.hostname || '—';
document.getElementById('set-ip').textContent = s.ip_primary || '—';
document.getElementById('set-version').textContent = s.furtka_version || '—';
document.getElementById('set-kernel').textContent = s.kernel || '—';
document.getElementById('set-ram').textContent = s.ram_total || '—';
document.getElementById('set-docker').textContent = s.docker_version || '—';
document.getElementById('set-uptime').textContent = s.uptime || '—';
document.getElementById('upd-current').textContent = s.furtka_version || '—';
} catch (e) {
/* next tick will retry */
}
}
refresh();
setInterval(refresh, 15000);
// --- Furtka updates -----------------------------------------------
const STAGE_LABELS = {
downloading: 'Downloading release…',
verifying: 'Verifying signature…',
extracting: 'Unpacking update…',
swapping: 'Switching to new version…',
restarting: 'Restarting services…',
done: 'Update complete — reloading…',
rolled_back: 'Update failed, rolled back to the previous version',
rolled_back_manual: 'Rolled back manually',
};
let pollHandle = null;
const statusEl = document.getElementById('update-status');
const checkBtn = document.getElementById('check-updates-btn');
const applyBtn = document.getElementById('apply-update-btn');
function setStatus(msg, isError = false) {
statusEl.textContent = msg;
statusEl.style.color = isError ? 'var(--danger)' : 'var(--muted)';
}
checkBtn.addEventListener('click', async () => {
checkBtn.disabled = true;
const original = checkBtn.textContent;
checkBtn.textContent = 'Checking…';
setStatus('');
try {
const r = await fetch('/api/furtka/update/check', { method: 'POST' });
const data = await r.json();
if (!r.ok) {
setStatus(data.error || `HTTP ${r.status}`, true);
return;
}
document.getElementById('upd-latest').textContent = data.latest || '—';
if (data.update_available) {
applyBtn.hidden = false;
applyBtn.textContent = `Update to ${data.latest}`;
setStatus(`Update available: ${data.current}${data.latest}`);
} else {
applyBtn.hidden = true;
setStatus('Already up to date');
}
} catch (e) {
setStatus(`Network error: ${e.message}`, true);
} finally {
checkBtn.disabled = false;
checkBtn.textContent = original;
}
});
applyBtn.addEventListener('click', async () => {
applyBtn.disabled = true;
checkBtn.disabled = true;
setStatus('Starting update…');
try {
const r = await fetch('/api/furtka/update/apply', { method: 'POST' });
const data = await r.json();
if (r.status === 409) {
setStatus('Another update is already running — watching it', true);
} else if (!r.ok) {
setStatus(data.error || `HTTP ${r.status}`, true);
applyBtn.disabled = false;
checkBtn.disabled = false;
return;
}
// Poll /update-state.json (served by Caddy, unaffected by the
// API restart the updater is about to trigger) every 2s.
pollHandle = setInterval(pollUpdateState, 2000);
} catch (e) {
setStatus(`Network error: ${e.message}`, true);
applyBtn.disabled = false;
checkBtn.disabled = false;
}
});
async function pollUpdateState() {
try {
const r = await fetch('/update-state.json', { cache: 'no-store' });
if (!r.ok) return;
const s = await r.json();
const label = STAGE_LABELS[s.stage] || `Stage: ${s.stage}`;
setStatus(label, s.stage === 'rolled_back');
if (s.stage === 'done') {
clearInterval(pollHandle);
setTimeout(() => location.reload(), 5000);
} else if (s.stage === 'rolled_back') {
clearInterval(pollHandle);
if (s.reason) {
setStatus(`${label}${s.reason}`, true);
}
applyBtn.disabled = false;
checkBtn.disabled = false;
}
} catch (e) {
/* keep polling; restart blip expected */
}
}
</script>
</body>
</html>