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>
154 lines
6.4 KiB
HTML
154 lines
6.4 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>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="/" aria-current="page">Home</a>
|
|
<a href="/apps">Apps</a>
|
|
<a href="/settings/">Settings</a>
|
|
</div>
|
|
</nav>
|
|
<header>
|
|
<h1>Welcome to Furtka</h1>
|
|
<p class="lead">Your home server is ready.</p>
|
|
<p class="host">Running on <code id="hostname">—</code></p>
|
|
</header>
|
|
|
|
<section>
|
|
<h2>Your apps</h2>
|
|
<div id="apps-section" class="grid-apps">
|
|
<a class="app-tile" href="/apps"><span class="name">Loading…</span></a>
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>System status</h2>
|
|
<div class="tiles">
|
|
<div class="tile">
|
|
<span class="label">Uptime</span>
|
|
<span class="value" id="uptime">—</span>
|
|
</div>
|
|
<div class="tile">
|
|
<span class="label">Docker</span>
|
|
<span class="value" id="docker">—</span>
|
|
</div>
|
|
<div class="tile">
|
|
<span class="label">Free disk</span>
|
|
<span class="value" id="disk">—</span>
|
|
</div>
|
|
</div>
|
|
<p class="updated">Updated <span id="updated">—</span></p>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>Coming next</h2>
|
|
<div class="coming">
|
|
<p class="hint">Features we're building — follow progress on <a href="https://furtka.org">furtka.org</a>.</p>
|
|
<a href="https://furtka.org/#planned">Photos</a>
|
|
<a href="https://furtka.org/#planned">Smart home</a>
|
|
<a href="https://furtka.org/#planned">Media streaming</a>
|
|
<a href="https://furtka.org/#planned">Multiple boxes</a>
|
|
<a href="https://furtka.org/#planned">Secure link</a>
|
|
<a href="https://furtka.org/#planned">User accounts</a>
|
|
</div>
|
|
</section>
|
|
|
|
<footer>
|
|
<p>Furtka · <a href="https://furtka.org">furtka.org</a></p>
|
|
</footer>
|
|
</main>
|
|
|
|
<script>
|
|
// Hostname + install metadata — written once at install time to
|
|
// /var/lib/furtka/furtka.json (see _furtka_json_cmd in the installer).
|
|
// Separate from status.json because these facts don't change between
|
|
// refresh ticks.
|
|
let HOSTNAME = "";
|
|
const FALLBACK_ICON = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-7l-2-2H5a2 2 0 0 0-2 2z"/></svg>';
|
|
|
|
function esc(s) {
|
|
const d = document.createElement('div');
|
|
d.textContent = s == null ? '' : String(s);
|
|
return d.innerHTML;
|
|
}
|
|
|
|
async function loadFurtkaJson() {
|
|
try {
|
|
const r = await fetch('/furtka.json', { cache: 'no-store' });
|
|
if (!r.ok) return;
|
|
const f = await r.json();
|
|
HOSTNAME = f.hostname || "";
|
|
const el = document.getElementById('hostname');
|
|
if (el) el.textContent = HOSTNAME || '—';
|
|
} catch (e) { /* no-op */ }
|
|
}
|
|
|
|
function primaryAction(app) {
|
|
// Only fileshare has a direct "open" link today. Future apps with
|
|
// HTTP endpoints would surface a URL here; everything else falls
|
|
// back to the /apps manage page.
|
|
if (app.name === 'fileshare' && HOSTNAME) {
|
|
return { href: `smb://${HOSTNAME}.local/files`, label: 'Open files' };
|
|
}
|
|
return { href: '/apps', label: 'Manage →' };
|
|
}
|
|
|
|
async function renderApps() {
|
|
const target = document.getElementById('apps-section');
|
|
try {
|
|
const r = await fetch('/api/apps', { cache: 'no-store' });
|
|
if (!r.ok) throw new Error('api');
|
|
const apps = (await r.json()).filter(a => a.ok !== false);
|
|
if (!apps.length) {
|
|
target.innerHTML = '<a class="app-tile" href="/apps">' +
|
|
'<span class="name">No apps yet</span>' +
|
|
'<span class="cta">Install your first app →</span></a>';
|
|
return;
|
|
}
|
|
target.innerHTML = apps.map(a => {
|
|
const icon = a.icon_svg || FALLBACK_ICON;
|
|
const { href, label } = primaryAction(a);
|
|
return `<a class="app-tile" href="${esc(href)}">
|
|
<div class="icon">${icon}</div>
|
|
<span class="name">${esc(a.display_name || a.name)}</span>
|
|
<span class="cta">${esc(label)}</span>
|
|
</a>`;
|
|
}).join('');
|
|
} catch (e) {
|
|
target.innerHTML = '<a class="app-tile" href="/apps">' +
|
|
'<span class="name">Manage apps</span>' +
|
|
'<span class="cta">Open →</span></a>';
|
|
}
|
|
}
|
|
|
|
async function refresh() {
|
|
try {
|
|
const r = await fetch('/status.json', { cache: 'no-store' });
|
|
if (!r.ok) return;
|
|
const s = await r.json();
|
|
document.getElementById('uptime').textContent = s.uptime || '—';
|
|
document.getElementById('docker').textContent = s.docker_version || '—';
|
|
document.getElementById('disk').textContent = s.disk_free || '—';
|
|
document.getElementById('updated').textContent = s.updated_at || '—';
|
|
} catch (e) {
|
|
/* next tick will retry */
|
|
}
|
|
}
|
|
|
|
// furtka.json must land first so renderApps can build the SMB link
|
|
// with the real hostname. If it 404s (very early in boot) the
|
|
// primary-action falls back to "Manage →".
|
|
loadFurtkaJson().then(renderApps);
|
|
refresh();
|
|
setInterval(refresh, 15000);
|
|
</script>
|
|
</body>
|
|
</html>
|