Commit graph

175 commits

Author SHA1 Message Date
2f6c31a26e style: ruff format tests/test_wireguard.py
Some checks failed
CI / lint (pull_request) Successful in 27s
CI / validate-json (pull_request) Successful in 23s
CI / markdown-links (pull_request) Successful in 23s
CI / test (pull_request) Failing after 1m42s
Fixes the CI format check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 16:14:24 +02:00
2f7a02f1d1 Merge remote-tracking branch 'origin/main' into dev_gateway
Some checks failed
CI / lint (pull_request) Failing after 28s
CI / validate-json (pull_request) Successful in 23s
CI / markdown-links (pull_request) Successful in 21s
CI / test (pull_request) Failing after 1m45s
# Conflicts:
#	tests/test_api.py
2026-10-07 16:11:54 +02:00
d9a10f76ea fix: reuse the saved gateway registration on connect retry
Some checks failed
CI / lint (pull_request) Failing after 29s
CI / validate-json (pull_request) Successful in 23s
CI / markdown-links (pull_request) Successful in 23s
CI / test (pull_request) Failing after 1m28s
run_connect() saved the registration before bringing the tunnel up, but
a retry after a tunnel failure registered again anyway, leaving the
first box and its wg-easy peer orphaned on the gateway. Reuse the saved
registration when it's for the same gateway, and refuse a different
gateway until the box is disconnected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:19:34 +02:00
e251eb831a feat: group installed-app actions into an overflow menu
With the internet toggle, a published app showed up to seven buttons and
squeezed its description. Only Open stays visible; Settings, the
expose/stop-exposing toggle, Update, Reinstall and Remove move into a
"⋯" menu, and the public URL shows as a link under the description.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:13:39 +02:00
0490f49f85 fix: don't show gateway as disconnected when the session expired
Sessions are in-memory, so a furtka-api restart logs the user out and
/api/gateway/status returns 401. The settings page ignored that and kept
its default "not connected" panel visible. Both panels now start hidden
and a 401 redirects to /login.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:13:39 +02:00
f95bfcb56b fix: install wireguard-tools from the webinstaller's package list
30ee044 added it to archinstall/user_configuration.json, but the
webinstaller builds its own package list in build_archinstall_config(),
so installed boxes had no wg-quick@.service and gateway connect failed
with "Unit wg-quick@furtka-gw.service does not exist".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:13:39 +02:00
91aa33b891 feat(brand): Mark M is the Furtka logo — website, installer, box UI, console, boot splash
All checks were successful
CI / lint (pull_request) Successful in 30s
CI / validate-json (pull_request) Successful in 24s
CI / markdown-links (pull_request) Successful in 16s
CI / test (pull_request) Successful in 1m47s
CI / lint (push) Successful in 28s
CI / test (push) Successful in 1m56s
CI / validate-json (push) Successful in 24s
CI / markdown-links (push) Successful in 21s
Deploy site / deploy (push) Successful in 5s
Build ISO / build-iso (push) Successful in 20m27s
The logo vote on furtka.org closed with M 29 · R 13 (42 votes).

- website/tools/brand-assets.py generates every brand file from the
  logo-assets.py geometry: favicons (small-size tier, accent with a
  dark-mode variant) for website / webinstaller / on-box UI, 180 px
  apple-touch-icon, 1200x630 default share card, 640x480 syslinux splash,
  and docs/brand/ (mark, small mark, lockups, 20-column console art)
- website: header mark, favicon, touch icon, default og:image; /logo/ now
  shows the final result (static counts, winner highlighted, no voting
  UI or poll.js); homepage link reads 'meet our new logo'
- webinstaller: M in the header + tab icon
- on-box UI: .brand mark via CSS mask in brand red (not the UI's green),
  favicon on every page, brand line on the login page
- console: M art above the text of /etc/issue on the installed box
  (furtka-welcome) and the live ISO (furtka-update-issue)
- ISO: build.sh swaps releng's Arch splash for the Furtka one
- README shows the lockup

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wq1hzUp4ZmCKHDuLKCnu73
2026-09-28 15:08:49 +02:00
0cad2e05c8 feat(website): download page for the ISO, homepage with download CTA and 3-step how-it-works
- /download/ and /de/download/: big download button, file name, size and
  release date, requirements (64-bit PC, UEFI or BIOS, >=16 GB disk that
  gets erased, USB stick, network cable with internet, second device),
  five install steps (Etcher / Rufus DD mode / dd, boot menu, Secure Boot
  off, proksi.local:5000, wizard, pull stick), checksum + verify commands
  when the release has one, alpha notice, link to all releases
- the release data comes from the Forgejo API at build time
  (partials/release.html): newest release with an .iso asset, falling back
  to params.version if the lookup fails; getresource cache disabled so a
  build never pins an old release
- homepage: status chip follows the latest release, 'Download ISO' primary
  CTA + 'Source code' ghost CTA, numbered how-it-works cards, download band
  before the closer
- releases now upload furtka-<ver>.iso.sha256, and release.yml rebuilds the
  site after publishing so the download page follows each release

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wq1hzUp4ZmCKHDuLKCnu73
2026-09-28 14:52:59 +02:00
4c5bfe3aef Merge pull request 'fix: bug scan — API hardening, safer disk picker, updater rollback, friendlier installer' (#19) from fix/bug-scan-hw-compat into main
All checks were successful
CI / lint (push) Successful in 28s
CI / test (push) Successful in 1m54s
CI / validate-json (push) Successful in 24s
CI / markdown-links (push) Successful in 16s
Deploy site / deploy (push) Successful in 4s
Build ISO / build-iso (push) Successful in 21m17s
Reviewed-on: #19
2026-09-28 14:06:09 +02:00
b7b66fe136 docs(changelog): bug-scan fixes under Unreleased
All checks were successful
CI / lint (pull_request) Successful in 29s
CI / validate-json (pull_request) Successful in 24s
CI / markdown-links (pull_request) Successful in 15s
CI / test (pull_request) Successful in 1m42s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wq1hzUp4ZmCKHDuLKCnu73
2026-09-28 13:27:28 +02:00
7151d411c7 feat(installer): friendlier wizard, retry after a failed install, no boot stall on multi-NIC boards
- error state gets a 'Try again' button (settings are kept) plus links back
  to the disk / settings pages; the page reloads into it instead of only
  recolouring the bar
- names are lowercased instead of rejected; system account names (root,
  caddy, docker, …) are refused with a clear message
- plain wording: 'Server name' with a live http://<name>.local preview,
  'Pick a disk' with an explicit erase warning, a readable confirm table
  (language label, disk size/type) and a Back button
- the password hint says it is also the SSH / console password; the
  language hint says the wizard itself is English
- wizard pages redirect to the progress page while an install is running
- progress page: aria-live phase, aria-valuenow updates, 'lost connection'
  note, 'fine to close this page'
- .local fallback on the reboot page: router device list, IP may change
- live console banner says a network cable with internet is required
- target gets a systemd-networkd-wait-online --any drop-in: boards with a
  second unplugged NIC (or Ethernet + Wi-Fi) waited up to 2 min at boot

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wq1hzUp4ZmCKHDuLKCnu73
2026-09-28 13:21:55 +02:00
9f9e627678 fix(install,catalog): recover from interrupted installs, don't strand sync on network/disk errors
- an unfinished install-state.json whose runner is gone (free lock, >60 s
  old — reboot, OOM kill, runner never started) is marked 'error' instead
  of blocking every later install with 409 until someone fixes it over SSH
- read_state in install_runner/updater tolerates unreadable and non-object
  files
- Forgejo API + release downloads wrap OSError and HTTPException (socket
  timeouts, resets, disk full) — the update/catalog check endpoints used to
  drop the connection on a flaky network
- catalog sync records any failure as the 'error' stage, not just
  CatalogError

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wq1hzUp4ZmCKHDuLKCnu73
2026-09-28 13:15:10 +02:00
22c2245ac4 fix(updater,auth): atomic symlink flip, roll back on any failure, fail closed on a damaged users.json
updater:
- /opt/furtka/current is swapped via rename() instead of unlink+symlink,
  so a power cut can't leave the box without a current version
- OSError after the flip (disk full writing the Caddyfile) now rolls back
  too; rollback restores the previous Caddyfile and reloads caddy +
  daemon-reload
- the health check keeps polling on read timeouts / RemoteDisconnected
  instead of escaping and skipping the rollback
- run_update writes an 'error' stage for any failure (download, sha,
  disk full); the settings page shows it instead of spinning on
  'Downloading…'

auth:
- users.json that exists but is empty/corrupt/unreadable raises instead of
  reading as 'no admin yet' — before, a truncated file made /login an open
  create-admin form for the whole LAN. /login answers 503 with SSH recovery
  steps instead
- save_users fsyncs before the rename
- first-run admin creation is serialised (threaded server)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wq1hzUp4ZmCKHDuLKCnu73
2026-09-28 13:11:42 +02:00
20f6398abf fix(api): refuse path-like app names, validate settings before writing .env, harden the HTTP server
- app names from URL paths and JSON bodies must match APP_NAME_RE; installer
  remove()/update_env() check it too. Before, {"name": ".."} or "/" sent
  remove() into shutil.rmtree on /var/lib/furtka or the root filesystem
- manifests must carry a valid app name as well
- settings are validated in memory (placeholders, system paths, line breaks)
  before .env is written — a rejected /etc used to be saved anyway
- values containing $ are single-quoted so compose doesn't expand them
  ("pa$word" became "pa"); line breaks are refused (env injection)
- ThreadingHTTPServer so status polls keep answering during slow requests;
  state-changing POSTs still run one at a time
- 1 MiB body cap, 30 s socket timeout, bad Content-Length → 400
- POSTs with Sec-Fetch-Site other than same-origin are refused (apps on
  other ports of the same host count as same-site for SameSite=Strict)
- unexpected exceptions return a JSON 500 instead of a dropped connection
- UI pages send the browser to /login on a 401 instead of showing an empty
  app list after an API restart

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wq1hzUp4ZmCKHDuLKCnu73
2026-09-28 13:04:09 +02:00
8bcbaec621 fix(installer): safer disk picker, single install at a time, surface silent archinstall failures
- drives: classify by lsblk TRAN/ROTA (SATA SSDs were labelled HDD and lost
  to bigger spinning disks); drop zram, eMMC boot/rpmb areas, read-only and
  0-byte disks; grey out disks under 16 GB; rank USB disks last; warn about
  >2 TB disks on BIOS (MBR); boot-stick fallback via archiso kernel args
  when bootmnt is gone; timeouts on lsblk/smartctl/blkid
- step 2 only accepts a disk that was actually offered
- /install/run refuses a second concurrent install and disables the button
- archinstall's exit code is tracked: OOM kills / signals now show as errors
  instead of a progress bar stuck forever
- disk-probe exceptions land in the install log instead of a bare 500

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 12:53:24 +02:00
87dcb369db chore(website): make logo-assets.py pass ruff (unused var, E501 noqa, format)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 12:53:24 +02:00
905845187d fix: thread wg-easy v15's preshared key into the box's wg-quick config
furtka-gateway's wg-easy v15 client (wgeasy_v15.py) generates a
PresharedKey per WireGuard client by default, which the server enforces
even if the box's own conf omits it — dropping it here breaks the
handshake silently. Include it in [Peer] when present; v14 has no
preshared_key field at all, so this stays a no-op for that path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3jVwAqESRjPdW9P9nvfem
2026-09-19 13:34:45 +02:00
722dac4843 feat(website): randomise which logo comes first on /logo/ per visit
Some checks failed
CI / lint (push) Failing after 29s
CI / validate-json (push) Successful in 23s
CI / markdown-links (push) Successful in 14s
Deploy site / deploy (push) Successful in 4s
CI / test (push) Successful in 1m29s
Hugo sorts map keys, so `range (dict "m" … "r" …)` always put M first
and gave it the "first thing on the page" vote. The template now ranges
over an ordered list (front-matter `order:` can pin it), and a tiny
inline script on poll pages coin-flips the visual order before first
paint, applied via CSS `order` on every M/R pair and remembered in
sessionStorage so a reload or a vote doesn't shuffle the page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y8kPxizsExc95ZhvfJmaCw
2026-08-27 12:43:46 +02:00
c3c59b4bea feat(website): share card for /logo/ (og:image) so links to the vote get a preview
Some checks failed
CI / lint (push) Failing after 26s
CI / validate-json (push) Successful in 22s
CI / markdown-links (push) Successful in 14s
Deploy site / deploy (push) Successful in 4s
CI / test (push) Successful in 1m27s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MScAinbyMdeNc7H2BZdnnG
2026-08-26 14:33:28 +02:00
30ee044657 Add wireguard-tools to the installed-system package list
A freshly-installed box had no wg/wg-quick binary and no wg-quick@.service
unit, so wireguard.set_tunnel_enabled() would fail as soon as a box tried
to connect to a furtka-gateway. iso/overlay/packages.extra is unaffected —
that's the live-boot ISO's own package set, unrelated to what actually
lands on the installed disk.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0142mP6EqCkQKmR1xCp7zfmZ
2026-08-26 14:25:15 +02:00
58a99a0d9e fix(website): sharper console banners on /logo/ (30 columns, coverage-sampled), no line gaps or phone text inflation
Some checks failed
CI / lint (push) Failing after 26s
CI / validate-json (push) Successful in 23s
CI / markdown-links (push) Successful in 14s
Deploy site / deploy (push) Successful in 4s
CI / test (push) Successful in 1m26s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MScAinbyMdeNc7H2BZdnnG
2026-08-26 14:04:47 +02:00
22ce03d1f0 fix(website): build on Hugo 0.147 again (site.Data), chown site trees even when the CI build fails
Some checks failed
CI / lint (push) Failing after 31s
CI / validate-json (push) Successful in 22s
CI / test (push) Successful in 1m23s
Deploy site / deploy (push) Successful in 5s
CI / markdown-links (push) Successful in 14s
hugo.Data is not available in the alpine Hugo the deploy job uses; the
failed render left the webroot half-cleaned and root-owned because the
chown only ran after a successful build. It is an EXIT trap now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MScAinbyMdeNc7H2BZdnnG
2026-08-26 13:35:40 +02:00
5d827e5eab feat(website): logo showcase on /logo/, poll link + current feature list on the homepage
Some checks failed
CI / lint (push) Failing after 25s
CI / test (push) Successful in 1m28s
CI / validate-json (push) Successful in 24s
CI / markdown-links (push) Successful in 14s
Deploy site / deploy (push) Failing after 5s
Both marks now appear everywhere they would have to work: five sizes
down to the real 16 px tab icon, horizontal and stacked lockups, browser
tab + site header, phone tiles, console banner + boot splash, social
card, and four colour grounds — generated from one geometry source by
website/tools/logo-assets.py (outputs committed under data/logo and
static/img/logo). The homepage links to the vote and its 'what works
today' list catches up with 26.8–26.17: password login, online app
catalog, app dependencies, opt-in HTTPS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MScAinbyMdeNc7H2BZdnnG
2026-08-26 13:30:10 +02:00
aaba14731c fix(poll): resolve the client address via nginx real_ip, never from X-Forwarded-For
All checks were successful
CI / lint (push) Successful in 33s
CI / test (push) Successful in 2m7s
CI / validate-json (push) Successful in 25s
CI / markdown-links (push) Successful in 16s
A client-supplied X-Forwarded-For could mint a fresh device hash per
request. nginx now trusts only the edge hop (192.168.178.164) when
unwinding the chain and passes the result as X-Real-IP; the service
uses nothing else. setup-poll.sh --reset wipes the vote table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MScAinbyMdeNc7H2BZdnnG
2026-08-26 13:21:19 +02:00
9fd9d1b7d1 ci: stop ops/** from triggering the ISO build; hand site trees back to the VM user after CI deploy
All checks were successful
CI / lint (push) Successful in 25s
CI / test (push) Successful in 1m38s
CI / validate-json (push) Successful in 24s
CI / markdown-links (push) Successful in 15s
Deploy site / deploy (push) Successful in 4s
Build ISO / build-iso (push) Successful in 18m59s
The runner job runs as root, which left /srv/furtka-site and
/var/www/furtka.org root-owned and broke the manual deploy.sh path.
Runner capacity on forge-runner-01 raised to 2 alongside this, so the
site deploy no longer queues behind an ISO build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MScAinbyMdeNc7H2BZdnnG
2026-08-26 13:12:16 +02:00
2fbafd5c77 feat(website): logo poll at /logo/ with cookie-free vote counter
Some checks failed
CI / test (push) Waiting to run
Build ISO / build-iso (push) Successful in 18m1s
CI / lint (push) Successful in 24s
CI / markdown-links (push) Successful in 18s
CI / validate-json (push) Successful in 21s
Deploy site / deploy (push) Has been cancelled
Two marks (M and R) side by side, EN + DE, with the site-header and real
16 px favicon context for each. Votes go to ops/poll/pollsvc.py, a stdlib
Python service behind nginx at /api/poll/ (SQLite, systemd DynamicUser,
rate-limited). Devices are counted once via a salted hash of IP + user
agent; the privacy pages document it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MScAinbyMdeNc7H2BZdnnG
2026-08-26 12:57:23 +02:00
8b07b57242 fix(updater): start newly linked timers and link units via current; bench test report
All checks were successful
CI / lint (pull_request) Successful in 24s
CI / test (pull_request) Successful in 1m18s
CI / validate-json (pull_request) Successful in 22s
CI / markdown-links (pull_request) Successful in 14s
After the 26.19 → 26.20 self-update the bench had furtka-welcome.timer
linked + enabled but inactive (dead): enable only arms a timer for the
next boot. The link also pointed at /opt/furtka/versions/<ver>/ instead
of /opt/furtka/current/, unlike the installer-created links, so it would
have stopped following later updates and rollbacks. Link via current,
enable, start.

Adds docs/hardware-test-2026-08.md (timeline, five findings with root
causes / fixes / validation status, what worked, open items) and a
'Real hardware' section in iso/README.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MScAinbyMdeNc7H2BZdnnG
2026-08-26 09:46:13 +02:00
8e281fa4a6 chore: release 26.20-alpha
All checks were successful
CI / lint (pull_request) Successful in 24s
CI / test (pull_request) Successful in 1m19s
CI / validate-json (pull_request) Successful in 21s
CI / markdown-links (pull_request) Successful in 14s
Build ISO / build-iso (push) Successful in 18m15s
Deploy site / deploy (push) Successful in 4s
CI / lint (push) Successful in 23s
CI / test (push) Successful in 1m17s
CI / validate-json (push) Successful in 21s
CI / markdown-links (push) Successful in 13s
Release / release (push) Successful in 12m2s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MScAinbyMdeNc7H2BZdnnG
2026-08-25 17:08:25 +02:00
32ba522d59 fix(catalog): retry the boot-time sync and record failures as an error stage
On dual-stack LANs network-online.target is satisfied by the IPv6 SLAAC
address about a second before DHCPv4 lands, so the first
furtka-catalog-sync run died with 'Network is unreachable' and the next
attempt was the daily timer (OnBootSec=10min + up to 6 h jitter). The
state file stayed on 'checking' and the apps page showed 'checking…'
with nothing running — first hardware bench, 2026-08-25.

- furtka-catalog-sync.service: Restart=on-failure every 20 s, up to 8
  times in 15 min
- sync_catalog() writes stage 'error' + message on CatalogError, then
  re-raises (body moved to _sync_locked())
- apps page shows 'last sync failed: <error>' for that stage

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MScAinbyMdeNc7H2BZdnnG
2026-08-25 16:38:06 +02:00
60404ae643 fix(installer): put the 'pull the USB stick' step before the restart button
The hint lived on the rebooting page and in a confirm() popup — i.e. after
the click, with the machine restarting 3 s later. On the first hardware
bench the box promptly booted the stick again. The done state now shows a
two-step list (pull the stick → restart) above the button, and the button
says what it assumes: 'USB stick is out — restart now'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MScAinbyMdeNc7H2BZdnnG
2026-08-25 16:25:25 +02:00
5ddda5302a fix(installer): release the target disk before archinstall partitions it
The live ISO auto-activates LVM/md/swap found on attached drives. The
first hardware bench's SSD still carried a Proxmox VE 'pve' VG, so six
device-mapper nodes sat on sda3 and archinstall died at 12 % with
'unable to inform the kernel of the change ... in use'.

install_run now calls diskprep.release_disk() first: swapoff/umount
leaves-first, dmsetup remove / mdadm --stop for every dm/md node stacked
on the disk (retrying until the stack is gone), wipefs -a on partitions
and disk, blockdev --rereadpt. Steps are logged at the top of the install
log (spawn_archinstall opens it in append mode now). The progress page
names this failure explicitly if it still happens.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MScAinbyMdeNc7H2BZdnnG
2026-08-25 15:18:39 +02:00
0e05bef667 fix(console): always show the IP fallback on the welcome banner
The /etc/issue banner on both the live ISO and the installed system was a
oneshot after network-online.target that simply dropped the http://<ip>
line when DHCP hadn't delivered yet — so a slow NIC (first hardware bench,
2026-08-24) left the screen offering only proksi.local. On the ISO,
releng's root autologin on tty1 also made agetty --reload a no-op.

- furtka-issue.timer (ISO) / furtka-welcome.timer (target) re-run the
  banner script every 5 s; services drop RemainAfterExit + network wait
- fallback line is always present: every global IPv4, or a 'no IP yet —
  check cable / DHCP' hint; only rewrites + reloads on change
- build.sh removes releng's getty@tty1 autologin drop-in; banner tells
  users a shell is root + Enter away

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MScAinbyMdeNc7H2BZdnnG
2026-08-25 15:18:39 +02:00
f88412d9e8 Add gateway-connect UI and per-app internet-exposure toggle
Settings page gets a "Furtka Gateway" section: connect (gateway URL +
registration token + box name, polls /api/gateway/connect/status the same
way the update-apply flow polls /update-state.json) and disconnect, plus
a status readout (gateway URL, tunnel state). Removed "Remote access"
from the "Coming next" list now that it exists.

The Apps page's installed-app rows get an "Expose to Internet" control,
shown only when the manifest declares internet.viable — fetched lazily
per app (loadInternetSlots()) since the publish state isn't part of the
main /api/apps listing. Toggling calls the existing
GET/POST /api/apps/<name>/internet endpoints; the subdomain prompt uses a
plain window.prompt() rather than extending the settings modal, since
this needs a live network round-trip and a returned public URL that the
generic settings form has no way to express.

Verified end-to-end in a real browser against a running furtka serve +
a stub gateway server (login, publish, unpublish, connect-with-missing-
systemd-run, disconnect, and the client-side required-field validation),
not just by reading the generated HTML/JS.
2026-08-24 14:57:11 +02:00
91f2d6304c Fix wireguard.status()/set_tunnel_enabled() crash on unreadable conf path
Found by actually running furtka serve and clicking through the new
Settings UI: /api/gateway/status crashed the whole request with a raw
PermissionError traceback because conf_path.is_file() doesn't swallow
every OSError (confirmed directly, not assumed) when the parent directory
isn't accessible. A read-only status check must degrade to "not
configured" instead of 500ing; set_tunnel_enabled() now raises a clean
WireguardError instead of leaking an unhandled exception.

Also gives wireguard.py an env-var-overridable default path
(FURTKA_WIREGUARD_CONF / default_conf_path()), matching every other
runtime path in the codebase, instead of a single hardcoded constant.
2026-08-24 14:57:11 +02:00
e88b57f74d Add gateway connect/disconnect and per-app internet-toggle API endpoints
POST /api/gateway/connect dispatches registration + tunnel setup via
systemd-run (same async+state-file+poll idiom as install/update/catalog-
sync), backed by a new `furtka gateway connect-bg` internal CLI
subcommand and furtka.gateway.run_connect()/write_state()/read_state()/
acquire_lock(). GET /api/gateway/status and /api/gateway/connect/status
round out polling; POST /api/gateway/disconnect tears the tunnel down and
deregisters synchronously (a plain HTTP call plus one systemctl action,
same posture as _do_update_settings).

GET/POST /api/apps/<name>/internet is the per-app "expose to internet"
toggle: publishes/unpublishes a route via the connected gateway's
/v1/routes API (furtka.gateway.publish_route/unpublish_route, new) and
tracks published state in paths.internet_routes_file(). Deliberately its
own endpoint rather than a boolean settings[] type — it needs a live
network round-trip and returns a public URL, which the generic settings
modal has no way to express.

_manifest_summary()/cli.py's `app list --json` both gain the internet
field, mechanically identical to how requires was added.
2026-08-24 13:35:44 +02:00
557d410eca Add gateway registration client and WireGuard tunnel management
furtka.gateway: register/rotate-token/deregister against a furtka-gateway's
/v1/boxes/* API (stdlib urllib.request, same convention as
_release_common.py/updater.py). No local keypair generation — the
gateway's registration response carries the full WireGuard interface
config including the private key, since stock wg-easy always generates
the keypair itself (see furtka-gateway's wgeasy.py). Config persists to
paths.gateway_config_file() (0600, atomic write, same pattern as
auth.save_users).

furtka.wireguard: direct structural mirror of https.py's
snippet-write-plus-reload pattern for the wg-quick@furtka-gw systemd unit
— write/remove the .conf, flip the unit, revert the conf on failure so a
bad config can't leave the tunnel wedged.

paths.py gains gateway_config_file() and internet_routes_file()
(FURTKA_GATEWAY_CONFIG / FURTKA_INTERNET_ROUTES_FILE), following the
existing env-var-override convention.
2026-08-24 13:13:25 +02:00
43bcf3c4e5 Add internet.viable manifest field and its catalog-side privileged gate
New Manifest.internet field (viable: bool, port: int | None), parsed by
_parse_internet() following _parse_settings/_parse_requires's existing
shape. When viable, port must be one of the manifest's own declared ports
— resolves which entry is the exposable web UI for apps declaring several
(e.g. a DNS service exposing both a resolver port and an admin UI port).

catalog.py's release validation additionally rejects any internet.viable
app whose docker-compose.yaml sets privileged: true. Deliberately not a
network_mode check: a furtka-gateway reaches a host-networked container's
port exactly like a bridge-published one — the real risk is a compromised
internet-facing container also having host device/kernel access, which
privileged: true grants outright.
2026-08-24 13:08:55 +02:00
e1968facef chore: release 26.19-alpha
All checks were successful
Build ISO / build-iso (push) Successful in 19m56s
Deploy site / deploy (push) Successful in 4s
CI / lint (push) Successful in 23s
CI / test (push) Successful in 1m18s
CI / validate-json (push) Successful in 21s
CI / markdown-links (push) Successful in 13s
Release / release (push) Successful in 12m36s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFapVxVPvJJXwngkdbndxn
2026-08-23 16:05:43 +02:00
bf36b7621b docs: changelog entries for the dependency-hook install fixes
Some checks failed
CI / lint (pull_request) Successful in 33s
CI / test (pull_request) Successful in 1m20s
CI / validate-json (pull_request) Successful in 23s
CI / markdown-links (pull_request) Successful in 15s
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
CI / validate-json (push) Waiting to run
CI / markdown-links (push) Waiting to run
Build ISO / build-iso (push) Has been cancelled
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 15:29:08 +02:00
c2c3cdb1ed fix(cli): fire on_install hooks on named CLI installs
`furtka app install <name>` copied app folders (install_plan) then ran a
bare reconcile, which fires on_start but never on_install — so a CLI
install of a consumer brought its provider up without ever provisioning
it (no account, empty MQTT_* in the consumer .env). Route named installs
through install_runner.run_install (writing the plan file first), the same
docker phase the API dispatches, so providers come up before consumers and
on_install hooks run. Path installs keep the copy+reconcile dev/test path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 13:03:33 +02:00
0615d92c8d fix(install): copy app subdirectories so dependency hooks ship
install_from() only copied top-level files, so an app's scripts/ folder —
where provider on_install/on_start dependency hooks live — never reached
/var/lib/furtka/apps/<app>/. Every hooked dependency therefore failed at
reconcile with "hook ... missing in provider". Copy subdirectories too
(rmtree+copytree so a reinstall drops files removed upstream).

Found during the first real end-to-end run of the mosquitto+zigbee2mqtt
dependency pair on a test VM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 13:03:33 +02:00
1155f1d4ba chore: release 26.18-alpha
All checks were successful
Build ISO / build-iso (push) Successful in 18m44s
Deploy site / deploy (push) Successful in 4s
CI / lint (push) Successful in 28s
CI / test (push) Successful in 1m28s
CI / validate-json (push) Successful in 24s
CI / markdown-links (push) Successful in 14s
Release / release (push) Successful in 12m5s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 16:38:54 +02:00
1a2b817eb8 fix(deps): give on_start hooks the consumer's stored credentials
All checks were successful
CI / lint (pull_request) Successful in 31s
CI / test (pull_request) Successful in 1m27s
CI / validate-json (pull_request) Successful in 25s
CI / markdown-links (pull_request) Successful in 54s
Build ISO / build-iso (push) Successful in 18m35s
CI / lint (push) Successful in 29s
CI / test (push) Successful in 1m20s
CI / validate-json (push) Successful in 25s
CI / markdown-links (push) Successful in 54s
The reconciler handed an on_start dependency hook only
FURTKA_CONSUMER_APP/_VERSION, so it had no way to learn the consumer's
existing secrets. That made the feature's own headline use case —
re-create a provider account (e.g. an MQTT user) after a wipe with the
same password the consumer already holds — impossible unless the
provider stashed a copy itself.

on_start now also receives the consumer's .env values, namespaced under
FURTKA_CONSUMER_ENV_<KEY> (UPPER_SNAKE_CASE keys only, so a hand-edited
.env can't produce a malformed --env arg). on_start stays read-only
toward the consumer: unlike on_install, its stdout is intentionally not
merged back into the consumer's .env.

Surfaced building the first real provider/consumer catalog pair
(mosquitto + zigbee2mqtt) in daniel/furtka-apps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 23:45:42 +02:00
b725bf1773 chore: ruff format
All checks were successful
Build ISO / build-iso (push) Successful in 18m3s
CI / lint (push) Successful in 27s
CI / test (push) Successful in 1m22s
CI / validate-json (push) Successful in 25s
CI / markdown-links (push) Successful in 13s
Release / release (push) Successful in 12m13s
Whitespace-only — `ruff check` was green when 26.17-alpha shipped but
I forgot to run `ruff format`, so the CI format-check job went red on
the release commit. Runtime artifacts are unaffected (release.yml
doesn't gate on lint); this just re-greens the main baseline going
forward.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 20:10:04 +02:00
8e1f817d85 feat(apps): app-to-app dependencies with install + start hooks
Some checks failed
Build ISO / build-iso (push) Successful in 21m39s
CI / lint (push) Failing after 28s
CI / test (push) Successful in 1m29s
CI / validate-json (push) Successful in 24s
CI / markdown-links (push) Successful in 14s
Release / release (push) Successful in 12m2s
Manifests gain an optional `requires` array. Each entry points at
another app and may declare `on_install` + `on_start` hook scripts
that live in the *provider's* folder and run inside its container
via `docker compose exec`. Hook stdout (KEY=VALUE + optional
FURTKA_JSON: sentinel) gets merged into the consumer's .env; the
placeholder-secret check re-runs over the merged file. Provider apps
that aren't installed get auto-installed first (topo order, cycle
detection, explicit UI confirm). Removing an app is blocked while
other installed apps require it. Reconcile now visits apps in
dependency order so consumers' on_start hooks fire against already-up
providers; per-app error isolation skips just the offending consumer's
compose_up.

Release 26.17-alpha.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:39:10 +02:00
863ffa9737 chore: release 26.16-alpha
All checks were successful
Build ISO / build-iso (push) Successful in 18m12s
Deploy site / deploy (push) Successful in 3s
CI / lint (push) Successful in 28s
CI / test (push) Successful in 1m21s
CI / validate-json (push) Successful in 24s
CI / markdown-links (push) Successful in 13s
Release / release (push) Successful in 12m13s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:59:27 +02:00
ee132712be docs: sync READMEs with 26.15 HTTPS opt-in + boot-USB filter
All checks were successful
Build ISO / build-iso (push) Successful in 24m38s
CI / lint (push) Successful in 1m1s
CI / test (push) Successful in 2m42s
CI / validate-json (push) Successful in 58s
CI / markdown-links (push) Successful in 28s
- README roadmap: Local HTTPS Phase 1 entry now reflects the 26.15
  opt-in model (default off, toggle in /settings) instead of the
  26.4 auto-trust story.
- README + iso/README: boot-USB filtering is no longer a TODO; both
  files now describe the implemented `findmnt`/`PKNAME` behaviour.
- iso/README rough edges: drop the boot-USB bullet (closed) and
  re-word the wizard-still-HTTP-only bullet to match the 26.15 toggle
  flow (it was a stale dup of the same line under it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 12:09:33 +02:00
1193504a1e perf(site): gzip CSS, JS, SVG and fonts on the furtka.org nginx
Default nginx only gzips text/html, so the homepage HTML was the only
asset coming back compressed. The ~600 KB three.min.js bundle (and the
hashed CSS) were being shipped uncompressed across the public openresty
proxy. `gzip_types` now covers css/js/json/xml/svg/woff2.

Needs `sudo ops/nginx/setup-vm.sh` on forge-runner-01 to take effect —
the site-deploy workflow only rebuilds Hugo, it doesn't touch the
nginx config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 12:09:26 +02:00
65d48c92f8 feat(installer): filter the boot USB out of the install drive picker
On bare-metal installs, `lsblk` reports the USB stick the live ISO
booted from as TYPE=disk, so it showed up in the drive picker
alongside the real install target — a user could in theory pick the
USB they had just booted from. `findmnt /run/archiso/bootmnt` resolves
the boot partition and `lsblk -no PKNAME` walks it up to the parent
disk; that disk is dropped before scoring. On a normal box neither
file nor mountpoint exist and the picker is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 12:09:19 +02:00
aa7dea0528 feat(site): pimp homepage with animated 3D background and scroll reveals
Some checks failed
CI / lint (push) Successful in 1m24s
CI / test (push) Successful in 2m24s
CI / validate-json (push) Successful in 57s
CI / markdown-links (push) Successful in 29s
Deploy site / deploy (push) Successful in 7s
Build ISO / build-iso (push) Failing after 14m59s
Adopts the visual feel of Pascal's prototype while keeping Furtka's
voice, brand palette, and bilingual structure intact.

What changed
- Three.js wireframe torus-knot behind the hero, color/opacity tied
  to the existing --accent / --scene-opacity CSS vars so light and
  dark modes both work without a scene re-init.
- Scroll-driven camera zoom + core scale + tilt; canvas opacity fades
  past hero so feature cards stay readable.
- GSAP + ScrollTrigger reveal hero on load and stagger feature cards
  in as they enter the viewport. Lenis smooths scroll.
- "What works today" / "What's coming next" lists move from markdown
  bullets into front-matter arrays and render as scroll-reveal cards
  (7 + 4 cards, EN/DE parallel; copy is 1:1 from the original lists).
- Hero scaled up: gradient text on the wordmark (fg → accent),
  drop-shadow glow in dark mode, brighter lede color.
- Primary CTA -> /releases listing on Forgejo (Forgejo has no
  /releases/latest), with a pulsing glow + arrow slide on hover.
- Version bump 26.8-alpha -> 26.15-alpha to match the actual release.

Performance / a11y
- Vendor JS (Three.js r128, GSAP 3.12.2 + ScrollTrigger, Lenis 1.0.33)
  vendored locally under assets/js/vendor/ - no third-party CDN at
  runtime. ~728 KB total, fingerprinted via Hugo's pipeline with SRI.
- Canvas + scripts gated to homepage only ({{ if .IsHome }}); the
  Impressum/Datenschutz pages stay plain.
- prefers-reduced-motion: scene + GSAP early-return, CSS forces cards
  to their resting state. No-JS users see all content.
- All scripts deferred so first paint isn't blocked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 16:14:21 +02:00