furtka/furtka/api.py

623 lines
23 KiB
Python
Raw Normal View History

feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
# ruff: noqa: E501 — _HTML below is a literal HTML/CSS/JS payload; wrapping
# its lines hurts readability and the rendered output is what matters here.
"""Tiny HTTP API + management UI for the Furtka resource manager.
Single stdlib http.server process, no Flask/no third-party deps so we don't
have to pip-install anything on the target. Caddy reverse-proxies /apps and
/api from :80 to here.
Security: NO AUTH. Bound to 127.0.0.1 by default; the Caddy proxy makes it
LAN-reachable. Anyone on the LAN can install/remove apps. The UI shouts this
out at the top. Auth lands when Authentik does.
"""
import json
import re
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
from http.server import BaseHTTPRequestHandler, HTTPServer
from furtka import dockerops, installer, reconciler
from furtka.manifest import ManifestError, load_manifest
from furtka.paths import apps_dir, bundled_apps_dir
from furtka.scanner import scan
_ICON_MAX_BYTES = 16 * 1024
_UNSAFE_SVG_PATTERNS = (
re.compile(r"<script", re.IGNORECASE),
re.compile(r"javascript:", re.IGNORECASE),
re.compile(r"\bon[a-z]+\s*=", re.IGNORECASE),
)
def _read_icon_svg(app_dir, icon_name):
"""Return an SVG string safe to inline into the /apps response, or None.
Inlined rather than served via a separate endpoint so the /apps page
renders in one round-trip (Doherty Threshold). The trust model: icons
come from bundled apps we ship in the ISO or from apps the operator
installed via the (auth-less-by-design) API so the realistic threat
is a malformed file, not an attacker. Filter the obvious script /
event-handler vectors for defense in depth and let the browser render
the rest.
"""
if not app_dir or not icon_name:
return None
path = app_dir / icon_name
try:
if not path.is_file() or path.stat().st_size > _ICON_MAX_BYTES:
return None
data = path.read_text(encoding="utf-8")
except OSError:
return None
data = data.strip()
if data.startswith("<?xml"):
end = data.find("?>")
if end == -1:
return None
data = data[end + 2 :].lstrip()
if not data.startswith("<svg"):
return None
if any(p.search(data) for p in _UNSAFE_SVG_PATTERNS):
return None
return data
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
_HTML = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Furtka Apps</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="/style.css">
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
</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" aria-current="page">Apps</a>
<a href="/settings/">Settings</a>
</div>
</nav>
<h1>Furtka Apps</h1>
<p class="lede">Install or remove resource-manager apps on this Furtka box.</p>
<div class="warn">No authentication on this UI yet. Anyone on your LAN can install or remove apps. Don't expose this to the wider internet.</div>
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
<h2>Installed</h2>
<div id="installed"></div>
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
<h2>Available to install</h2>
<div id="available"></div>
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
<details class="log-details">
<summary>Last action</summary>
<pre id="log">(none yet)</pre>
</details>
</main>
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
<div id="modal-backdrop" class="modal-backdrop" role="dialog" aria-modal="true">
<div class="modal">
<h3 id="modal-title"></h3>
<div id="modal-long" class="long"></div>
<div id="modal-error" class="error"></div>
<form id="modal-form" onsubmit="return false;"></form>
<div class="modal-actions">
<button type="button" class="secondary" id="modal-cancel">Cancel</button>
<button type="button" id="modal-submit">Install</button>
</div>
</div>
</div>
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
<script>
function esc(s) {
const d = document.createElement('div');
d.textContent = s == null ? '' : String(s);
return d.innerHTML;
}
// Fallback when an app doesn't ship a parseable icon.svg. Simple
// stroked folder currentColor so the tile's accent tint applies.
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 appIcon(a) {
// `a.icon_svg` is already sanitized server-side (see _read_icon_svg).
return `<div class="app-icon">${a.icon_svg || FALLBACK_ICON}</div>`;
}
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
const modal = {
backdrop: document.getElementById('modal-backdrop'),
title: document.getElementById('modal-title'),
long: document.getElementById('modal-long'),
form: document.getElementById('modal-form'),
error: document.getElementById('modal-error'),
submit: document.getElementById('modal-submit'),
cancel: document.getElementById('modal-cancel'),
current: null, // { name, action: 'install' | 'edit' }
};
modal.cancel.addEventListener('click', () => closeModal());
modal.backdrop.addEventListener('click', (e) => { if (e.target === modal.backdrop) closeModal(); });
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal(); });
function closeModal() {
modal.backdrop.classList.remove('open');
modal.form.innerHTML = '';
modal.error.classList.remove('show');
modal.error.textContent = '';
modal.current = null;
}
async function openSettingsDialog(name, action) {
const r = await fetch(`/api/apps/${encodeURIComponent(name)}/settings`);
if (!r.ok) {
document.getElementById('log').textContent =
`[settings ${name}] HTTP ${r.status}\\n` + await r.text();
return;
}
const data = await r.json();
modal.current = { name, action };
modal.title.textContent = data.display_name || data.name;
modal.long.textContent = data.description_long || data.description || '';
modal.long.style.display = modal.long.textContent ? '' : 'none';
modal.submit.textContent = action === 'install' ? 'Install' : 'Save and restart';
if (!data.settings.length) {
// No form fields treat as simple confirm.
modal.form.innerHTML = '<p class="hint">No settings to configure.</p>';
} else {
modal.form.innerHTML = data.settings.map(s => {
const id = `field-${esc(s.name)}`;
const value = action === 'edit' && s.type === 'password' ? '' : esc(s.value || '');
const placeholder = action === 'edit' && s.type === 'password' ? 'Leave blank to keep current' : '';
return `
<div class="field">
<label for="${id}">${esc(s.label)}${s.required ? '<span class="req">*</span>' : ''}</label>
${s.description ? `<div class="hint">${esc(s.description)}</div>` : ''}
<input
id="${id}"
name="${esc(s.name)}"
type="${s.type === 'password' ? 'password' : s.type === 'number' ? 'number' : 'text'}"
value="${value}"
placeholder="${esc(placeholder)}"
${s.required && action === 'install' ? 'required' : ''}
autocomplete="off"
spellcheck="false">
</div>`;
}).join('');
}
modal.backdrop.classList.add('open');
const first = modal.form.querySelector('input');
if (first) first.focus();
}
modal.submit.addEventListener('click', submitModal);
async function submitModal() {
if (!modal.current) return;
const { name, action } = modal.current;
const values = {};
for (const input of modal.form.querySelectorAll('input')) {
// In edit mode, skip password fields left blank server keeps existing.
if (action === 'edit' && input.type === 'password' && input.value === '') continue;
values[input.name] = input.value;
}
modal.submit.disabled = true;
const original = modal.submit.textContent;
modal.submit.textContent = action === 'install' ? 'Installing…' : 'Saving…';
modal.error.classList.remove('show');
try {
const url = action === 'install'
? '/api/apps/install'
: `/api/apps/${encodeURIComponent(name)}/settings`;
const body = action === 'install' ? { name, settings: values } : { settings: values };
const r = await fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body),
});
const data = await r.json();
document.getElementById('log').textContent =
`[${action} ${name}] HTTP ${r.status}\\n` + JSON.stringify(data, null, 2);
if (!r.ok) {
modal.error.textContent = data.error || `HTTP ${r.status}`;
modal.error.classList.add('show');
modal.submit.disabled = false;
modal.submit.textContent = original;
return;
}
closeModal();
await refresh();
} catch (e) {
modal.error.textContent = `Network error: ${e.message}`;
modal.error.classList.add('show');
modal.submit.disabled = false;
modal.submit.textContent = original;
}
}
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
async function refresh() {
const [installed, available] = await Promise.all([
fetch('/api/apps').then(r => r.json()),
fetch('/api/bundled').then(r => r.json()),
]);
document.getElementById('installed').innerHTML = installed.length
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
? installed.map(a => {
const hasSettings = a.has_settings;
return `
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
<div class="app">
<div class="left">
${appIcon(a)}
<div class="meta">
<span class="name">${esc(a.display_name || a.name)} <small>${esc(a.version || '')}</small></span>
<span class="desc">${esc(a.description || a.error || '')}</span>
</div>
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
</div>
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
<div class="buttons">
${hasSettings ? `<button data-op="edit" data-name="${esc(a.name)}">Settings</button>` : ''}
<button class="secondary" data-op="update" data-name="${esc(a.name)}">Update</button>
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
<button class="secondary" data-op="reinstall" data-name="${esc(a.name)}">Reinstall</button>
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
<button class="danger" data-op="remove" data-name="${esc(a.name)}">Remove</button>
</div>
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
</div>`;
}).join('')
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
: '<div class="empty">No apps installed yet.</div>';
document.getElementById('available').innerHTML = available.length
? available.map(a => `
<div class="app">
<div class="left">
${appIcon(a)}
<div class="meta">
<span class="name">${esc(a.display_name || a.name)} <small>${esc(a.version || '')}</small></span>
<span class="desc">${esc(a.description || '')}</span>
</div>
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
</div>
<button data-op="install" data-name="${esc(a.name)}">Install</button>
</div>`).join('')
: '<div class="empty">No bundled apps left to install.</div>';
for (const btn of document.querySelectorAll('button[data-op]')) {
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
btn.addEventListener('click', () => handleButton(btn.dataset.op, btn.dataset.name, btn));
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
}
}
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
async function handleButton(op, name, btn) {
if (op === 'install' || op === 'edit') {
openSettingsDialog(name, op === 'install' ? 'install' : 'edit');
return;
}
// Reinstall + update + remove are direct actions, no form.
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
btn.disabled = true;
const original = btn.textContent;
const labels = { reinstall: 'Reinstalling…', update: 'Checking…', remove: 'Removing…' };
btn.textContent = labels[op] || 'Working…';
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
try {
const urls = {
reinstall: '/api/apps/install',
remove: '/api/apps/remove',
update: `/api/apps/${encodeURIComponent(name)}/update`,
};
const r = await fetch(urls[op], {
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name}),
});
const data = await r.json();
let header = `[${op} ${name}] HTTP ${r.status}`;
if (op === 'update' && r.ok) {
header += data.updated
? ` updated ${data.services.length} service(s)`
: ' — already up to date';
}
document.getElementById('log').textContent = header + '\\n' + JSON.stringify(data, null, 2);
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
} catch (e) {
document.getElementById('log').textContent = `[${op} ${name}] network error: ${e.message}`;
}
btn.textContent = original;
await refresh();
}
refresh();
</script>
</body>
</html>
"""
def _manifest_summary(m, app_dir=None):
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
return {
"name": m.name,
"display_name": m.display_name,
"version": m.version,
"description": m.description,
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
"description_long": m.description_long,
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
"ports": list(m.ports),
"icon": m.icon,
"icon_svg": _read_icon_svg(app_dir, m.icon),
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
"has_settings": bool(m.settings),
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
}
def _list_installed():
out = []
for r in scan(apps_dir()):
if r.ok:
d = _manifest_summary(r.manifest, r.path)
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
d["ok"] = True
out.append(d)
else:
out.append({"name": r.path.name, "ok": False, "error": r.error})
return out
def _list_bundled():
installed_names = {r.path.name for r in scan(apps_dir()) if r.ok}
bundled = bundled_apps_dir()
if not bundled.exists():
return []
out = []
for entry in sorted(bundled.iterdir()):
if not entry.is_dir() or entry.name in installed_names:
continue
manifest_path = entry / "manifest.json"
if not manifest_path.exists():
continue
try:
m = load_manifest(manifest_path)
except ManifestError:
continue
out.append(_manifest_summary(m, entry))
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
return out
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
def _load_manifest_for(name):
"""Return (manifest, env_values, installed_bool) for an installed or bundled app.
Returns (None, None, False) if the name doesn't resolve anywhere.
"""
target = apps_dir() / name
if target.exists() and (target / "manifest.json").exists():
try:
m = load_manifest(target / "manifest.json")
except ManifestError:
return None, None, False
values = installer.read_env_values(target / ".env")
return m, values, True
bundled = bundled_apps_dir() / name
if bundled.exists() and (bundled / "manifest.json").exists():
try:
m = load_manifest(bundled / "manifest.json")
except ManifestError:
return None, None, False
env_example = bundled / ".env.example"
values = installer.read_env_values(env_example) if env_example.exists() else {}
return m, values, False
return None, None, False
def _do_get_settings(name):
m, values, installed = _load_manifest_for(name)
if m is None:
return 404, {"error": f"{name!r} not found"}
settings_out = []
for s in m.settings:
# Never return password values back to the client — user either keeps
# the current value (blank input means "don't change") or types a new one.
if s.type == "password":
current = ""
else:
current = values.get(s.name, s.default if s.default is not None else "")
settings_out.append(
{
"name": s.name,
"label": s.label,
"description": s.description,
"type": s.type,
"required": s.required,
"default": s.default,
"value": current,
}
)
return 200, {
"name": m.name,
"display_name": m.display_name,
"description": m.description,
"description_long": m.description_long,
"installed": installed,
"settings": settings_out,
}
def _do_install(name, settings=None):
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
try:
src = installer.resolve_source(name)
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
target = installer.install_from(src, settings=settings)
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
except installer.InstallError as e:
return 400, {"error": str(e)}
actions = reconciler.reconcile(apps_dir())
payload = {
"installed": str(target),
"actions": [{"kind": a.kind, "target": a.target, "detail": a.detail} for a in actions],
}
# 207 Multi-Status — install copy succeeded but reconcile had per-app errors.
return (207 if reconciler.has_errors(actions) else 200, payload)
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
def _do_update_settings(name, settings):
"""Write settings into an installed app's .env and kick off a reinstall.
Only works for already-installed apps use /api/apps/install for fresh
installs (since bundled-app folders under /opt/... are read-only).
"""
target = apps_dir() / name
if not target.exists():
return 404, {"error": f"{name!r} is not installed"}
try:
installer.update_env(name, settings)
except installer.InstallError as e:
return 400, {"error": str(e)}
actions = reconciler.reconcile(apps_dir())
return (
207 if reconciler.has_errors(actions) else 200,
{
"updated": name,
"actions": [{"kind": a.kind, "target": a.target, "detail": a.detail} for a in actions],
},
)
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
def _do_remove(name):
target = apps_dir() / name
if not target.exists():
return 404, {"error": f"{name!r} is not installed"}
compose_warning = None
try:
dockerops.compose_down(target, name)
except dockerops.DockerError as e:
compose_warning = str(e)
try:
installer.remove(name)
except installer.InstallError as e:
return 500, {"error": str(e)}
return 200, {"removed": name, "compose_warning": compose_warning}
def _do_update(name):
"""Pull newer container images for an installed app; restart if any changed.
Behaviour:
- 404 if the app isn't installed.
- Always runs `docker compose pull` first (cheap no-op when nothing to
fetch, touches the network).
- For each service, compares the container's running image ID against
the post-pull local image ID. If a service's image advanced, runs
`docker compose up -d` so compose recreates the affected containers
in place.
- Returns {updated: bool, services: [{service, from, to}], ...}.
"""
target = apps_dir() / name
if not target.exists():
return 404, {"error": f"{name!r} is not installed"}
try:
dockerops.compose_pull(target, name)
tags = dockerops.compose_image_tags(target, name)
changes = []
for service, tag in tags.items():
running = dockerops.running_container_image_id(target, name, service)
local = dockerops.local_image_id(tag)
if running and local and running != local:
changes.append({"service": service, "from": running, "to": local, "tag": tag})
if changes:
dockerops.compose_up(target, name)
except dockerops.DockerError as e:
return 502, {"error": str(e)}
return 200, {"app": name, "updated": bool(changes), "services": changes}
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
def _parse_settings_body(payload):
"""Extract and coerce the settings dict from a JSON body. Returns dict or None."""
s = payload.get("settings")
if s is None:
return None
if not isinstance(s, dict):
return False # sentinel — caller should reject
out = {}
for k, v in s.items():
if not isinstance(k, str):
return False
out[k] = "" if v is None else str(v)
return out
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
class _Handler(BaseHTTPRequestHandler):
def _json(self, status, payload):
body = json.dumps(payload).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def _html(self, status, body):
b = body.encode()
self.send_response(status)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(b)))
self.end_headers()
self.wfile.write(b)
def do_GET(self): # noqa: N802 — http.server convention
if self.path in ("/", "/apps", "/apps/"):
return self._html(200, _HTML)
if self.path == "/api/apps":
return self._json(200, _list_installed())
if self.path == "/api/bundled":
return self._json(200, _list_bundled())
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
# /api/apps/<name>/settings
if self.path.startswith("/api/apps/") and self.path.endswith("/settings"):
name = self.path[len("/api/apps/") : -len("/settings")]
if "/" in name or not name:
return self._json(400, {"error": "invalid app name"})
status, body = _do_get_settings(name)
return self._json(status, body)
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
self._json(404, {"error": "not found"})
def do_POST(self): # noqa: N802
length = int(self.headers.get("Content-Length", "0"))
raw = self.rfile.read(length) if length else b""
try:
payload = json.loads(raw.decode()) if raw else {}
except (UnicodeDecodeError, json.JSONDecodeError):
return self._json(400, {"error": "invalid JSON body"})
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
if not isinstance(payload, dict):
return self._json(400, {"error": "body must be a JSON object"})
# Per-app settings update: /api/apps/<name>/settings
if self.path.startswith("/api/apps/") and self.path.endswith("/settings"):
name = self.path[len("/api/apps/") : -len("/settings")]
if "/" in name or not name:
return self._json(400, {"error": "invalid app name"})
settings = _parse_settings_body(payload)
if settings is False or settings is None:
return self._json(400, {"error": "missing or invalid 'settings' object"})
status, body = _do_update_settings(name, settings)
return self._json(status, body)
# Per-app image update: /api/apps/<name>/update
if self.path.startswith("/api/apps/") and self.path.endswith("/update"):
name = self.path[len("/api/apps/") : -len("/update")]
if "/" in name or not name:
return self._json(400, {"error": "invalid app name"})
status, body = _do_update(name)
return self._json(status, body)
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
name = payload.get("name")
if not isinstance(name, str) or not name:
return self._json(400, {"error": "missing or empty 'name' field"})
if self.path == "/api/apps/install":
feat(furtka): in-browser app settings + ISO recovery-path fixes End-to-end VM test today (2026-04-15) validated the resource manager golden path but exposed four things blocking "dein-Vater-tauglich": no way to configure an app without SSH+editor, no openssh, no nano, keyboard stuck on US, and a samba healthcheck that cried wolf. Resource-manager side: - Manifest schema gains optional `settings` list (name/label/ description/type/required/default) and `description_long`. - Bundled-app install opens a form rendered from the manifest; submit carries values to `POST /api/apps/install` which writes them into the new app's `.env` before the placeholder check runs. - Installed apps grow an "Einstellungen" button that merges a partial settings dict into the existing `.env` (unsubmitted password fields = keep current), then reconciles to restart. - New endpoints: `GET/POST /api/apps/<name>/settings`. Passwords are never returned to the client. - Fileshare manifest declares its SMB_USER/SMB_PASSWORD settings in German with help text. ISO side (so the next build is actually usable on the TTY): - Add `openssh` to the package list + `sshd` to enabled services. `archinstall: true` in 4.x did not install openssh-server. - Add `nano` — `vim` was the only editor pitched at users, which is brutal for first-timers (and was missing anyway). - Keyboard layout follows the installer language (`de→de`, `pl→pl`, `en→us`) instead of hardcoded `us`. A German user couldn't type `/` or `-` at the console, making even `sudo nano` painful. - Disable the dperson/samba healthcheck in the compose override — it timed out on every probe while the share itself worked fine. 19 new tests (manifest parsing + settings-merge + two new API endpoints over live HTTP); 94 total, format + lint clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:00:02 +02:00
settings = _parse_settings_body(payload)
if settings is False:
return self._json(400, {"error": "'settings' must be an object"})
status, body = _do_install(name, settings=settings)
feat(furtka): web UI + HTTP API for app install/remove Adds the management UI Daniel asked for end-of-session. Goes beyond the original MVP scope (plan punted UI to v2) but the architecture already supports it cleanly: stdlib http.server only, no new deps. - furtka.api: minimal HTTP server. GET / serves a self-contained HTML page (dark-mode card list, vanilla JS, no build step). GET /api/apps + /api/bundled return JSON. POST /api/apps/{install, remove} accept {"name": "..."} and call the same installer + reconciler the CLI uses, so the placeholder-secret refusal and per-app reconcile isolation flow through unchanged. - furtka.cli: new `furtka serve` subcommand. Imports api lazily so `furtka app list` / `reconcile` startup stays zero-cost. - webinstaller: new furtka-api.service (Type=simple, restart on failure, after reconcile). Caddyfile gets two new handle blocks to reverse-proxy /api and /apps to localhost:7000. Landing page's "App store coming soon" tile becomes a real "Manage installed apps →" link to /apps. - Bound to 127.0.0.1 by default; Caddy makes it LAN-reachable. The UI shouts a "no auth, anyone on your LAN can install/remove" warning at the top — Authentik integration is the proper fix later. UX wrinkle worth noting: a placeholder-rejected install leaves the app in /var/lib/furtka/apps/<name>/ (so the user can edit .env in place). To re-trigger after editing, the Installed list now shows both Reinstall and Remove buttons. 10 new tests: helper functions (list_installed, list_bundled with hide-already-installed), install/remove endpoints with the no_docker fixture, and two real-socket urllib smoke tests that boot the actual HTTPServer on an ephemeral port and round-trip GET / + POST. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:23:46 +02:00
elif self.path == "/api/apps/remove":
status, body = _do_remove(name)
else:
status, body = 404, {"error": "not found"}
self._json(status, body)
def log_message(self, fmt, *args): # noqa: A003
# Quiet — systemd journal already records the access via Caddy's log.
pass
def serve(host: str = "127.0.0.1", port: int = 7000) -> None:
"""Run the API server. Blocks forever; exits on SIGINT/SIGTERM."""
server = HTTPServer((host, port), _Handler)
print(f"Furtka API listening on http://{host}:{port}/")
try:
server.serve_forever()
except KeyboardInterrupt:
pass
finally:
server.server_close()