feat(ui): landing page redesign — apps grid + roadmap placeholders
Slice 4 of the on-box UI uplevel. The landing page is now the peak-end first impression after install: welcome + hostname chip, a "Your apps" tile grid consuming /api/apps (with the real icon and an app-specific primary action — fileshare gets smb://<host>.local/files, everything else falls back to Manage →), the existing system-status tiles kept intact, and a subtle "Coming next" row of text-only links that jump to the planned-features section on furtka.org. No dead tiles. The status script now also writes ip_primary, kernel, ram_total and a furtka_version read from /etc/furtka/version (TODO: pin that file at install time; for now it reports "dev"). The settings page will consume those in slice 5. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
358444839c
commit
c7ca6bfbb1
1 changed files with 88 additions and 5 deletions
|
|
@ -1,3 +1,7 @@
|
||||||
|
# ruff: noqa: E501 — inline HTML/CSS/JS payloads (_INDEX_HTML, _STYLE_CSS,
|
||||||
|
# _CADDYFILE, _FURTKA_STATUS_SH, etc.) round-trip verbatim to the installed
|
||||||
|
# system; wrapping them hurts readability and the rendered output is what
|
||||||
|
# matters.
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
@ -192,7 +196,14 @@ _INDEX_HTML = """\
|
||||||
<p class="host">Running on <code>__HOSTNAME__</code></p>
|
<p class="host">Running on <code>__HOSTNAME__</code></p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="status">
|
<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>
|
<h2>System status</h2>
|
||||||
<div class="tiles">
|
<div class="tiles">
|
||||||
<div class="tile">
|
<div class="tile">
|
||||||
|
|
@ -211,9 +222,17 @@ _INDEX_HTML = """\
|
||||||
<p class="updated">Updated <span id="updated">—</span></p>
|
<p class="updated">Updated <span id="updated">—</span></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="soon">
|
<section>
|
||||||
<h2>Apps</h2>
|
<h2>Coming next</h2>
|
||||||
<p><a href="/apps">Manage installed apps →</a></p>
|
<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>
|
</section>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
|
|
@ -222,6 +241,53 @@ _INDEX_HTML = """\
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const HOSTNAME = "__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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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') {
|
||||||
|
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() {
|
async function refresh() {
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/status.json', { cache: 'no-store' });
|
const r = await fetch('/status.json', { cache: 'no-store' });
|
||||||
|
|
@ -235,6 +301,8 @@ _INDEX_HTML = """\
|
||||||
/* next tick will retry */
|
/* next tick will retry */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderApps();
|
||||||
refresh();
|
refresh();
|
||||||
setInterval(refresh, 15000);
|
setInterval(refresh, 15000);
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -657,6 +725,10 @@ _STATUS_JSON_PLACEHOLDER = """\
|
||||||
"uptime": "starting…",
|
"uptime": "starting…",
|
||||||
"docker_version": "starting…",
|
"docker_version": "starting…",
|
||||||
"disk_free": "starting…",
|
"disk_free": "starting…",
|
||||||
|
"ip_primary": "",
|
||||||
|
"kernel": "",
|
||||||
|
"ram_total": "",
|
||||||
|
"furtka_version": "",
|
||||||
"updated_at": ""
|
"updated_at": ""
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
@ -679,6 +751,13 @@ else
|
||||||
docker_version=unavailable
|
docker_version=unavailable
|
||||||
fi
|
fi
|
||||||
disk_free=$(df -h / 2>/dev/null | awk 'NR==2 {print $4 " free of " $2}' || echo unknown)
|
disk_free=$(df -h / 2>/dev/null | awk 'NR==2 {print $4 " free of " $2}' || echo unknown)
|
||||||
|
ip_primary=$(ip -4 -o addr show scope global 2>/dev/null \
|
||||||
|
| awk '{print $4}' | cut -d/ -f1 | head -1 || true)
|
||||||
|
kernel=$(uname -r 2>/dev/null || echo unknown)
|
||||||
|
ram_total=$(free -h --si 2>/dev/null | awk '/^Mem:/ {print $2}' || echo unknown)
|
||||||
|
# TODO(furtka-version): wire into the installer so /etc/furtka/version
|
||||||
|
# gets pinned at install time. Until then the settings page shows "dev".
|
||||||
|
furtka_version=$(cat /etc/furtka/version 2>/dev/null || echo dev)
|
||||||
updated_at=$(date -Iseconds)
|
updated_at=$(date -Iseconds)
|
||||||
|
|
||||||
cat > "$tmp" <<EOF
|
cat > "$tmp" <<EOF
|
||||||
|
|
@ -687,6 +766,10 @@ cat > "$tmp" <<EOF
|
||||||
"uptime": "$uptime",
|
"uptime": "$uptime",
|
||||||
"docker_version": "$docker_version",
|
"docker_version": "$docker_version",
|
||||||
"disk_free": "$disk_free",
|
"disk_free": "$disk_free",
|
||||||
|
"ip_primary": "$ip_primary",
|
||||||
|
"kernel": "$kernel",
|
||||||
|
"ram_total": "$ram_total",
|
||||||
|
"furtka_version": "$furtka_version",
|
||||||
"updated_at": "$updated_at"
|
"updated_at": "$updated_at"
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue