Commit graph

177 commits

Author SHA1 Message Date
5830f8887a Merge pull request 'dev_gateway' (#22) from dev_gateway into main
All checks were successful
CI / lint (push) Successful in 26s
CI / test (push) Successful in 1m59s
CI / validate-json (push) Successful in 24s
CI / markdown-links (push) Successful in 23s
Build ISO / build-iso (push) Successful in 20m15s
Reviewed-on: #22
2026-10-07 16:34:47 +02:00
9076b85304 test: simulate permission errors in wireguard tests instead of chmod 000
All checks were successful
CI / lint (pull_request) Successful in 29s
CI / validate-json (pull_request) Successful in 23s
CI / markdown-links (pull_request) Successful in 21s
CI / test (pull_request) Successful in 1m47s
CI runs pytest as root, which ignores directory permissions, so the
0o000-directory tests didn't hit the error path: the enable test went on
to call the real systemctl and failed, and the status test passed only
by accident. Monkeypatch the PermissionError instead, and stub reload so
the enable test can never shell out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 16:24:51 +02:00
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