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
New `furtka catalog sync` pulls the latest daniel/furtka-apps release,
verifies its sha256, extracts under /var/lib/furtka/catalog/, and
atomically swaps into place — so apps can ship without cutting a new
Furtka core release. A daily timer (furtka-catalog-sync.timer, 10 min
post-boot + 24 h with ±6 h jitter) drives the sync; /apps gets a
manual "Sync apps catalog" button that kicks the same code path via a
detached systemd-run unit.
Layout of the new on-box tree:
/var/lib/furtka/catalog/ synced catalog (survives self-updates)
├── VERSION
└── apps/<name>/ ...
/var/lib/furtka/catalog-state.json sync stage + last version, UI-polled
/run/furtka/catalog.lock flock so timer + manual click can't race
Resolver precedence (furtka/sources.py): catalog wins over the bundled
seed (/opt/furtka/current/apps/, carried by the core release for offline
first-boot). Installed apps under /var/lib/furtka/apps/ are never auto-
swapped — user clicks Reinstall to move an existing install onto a
newer catalog version; settings merge-preserved via the existing
installer.install_from path.
New files:
- furtka/_release_common.py — shared Forgejo/tarball primitives lifted
from furtka/updater.py. Both modules now import from here; updater's
behaviour and public API unchanged.
- furtka/catalog.py — check_catalog(), sync_catalog() with staging +
manifest validation + atomic rename. Refuses bad sha256 / broken
manifests and leaves the live catalog intact on any failure path.
- furtka/sources.py — resolve_app_name() / list_available() abstraction
used by installer.resolve_source and api._list_available.
- assets/systemd/furtka-catalog-sync.{service,timer} — oneshot service
+ daily timer. Timer auto-enables on self-update via a one-line
addition to _link_new_units (fresh installs get enabled via the
webinstaller's _FURTKA_UNITS list).
API + UI:
- /api/bundled renamed internally to _list_available; endpoint stays as
a backcompat alias; /api/apps/available is the new canonical name.
Each list entry carries a `source` field ("catalog" | "bundled").
- POST /api/catalog/sync/check + /apply + GET /api/catalog/status.
- /apps page grows a catalog-status row + Sync button; poll loop
mirrors the Furtka self-update flow.
CLI: `furtka catalog sync [--check]` + `furtka catalog status` (both
support --json). Old `furtka app install` / `reconcile` / `update` /
`rollback` surfaces are unchanged.
Test gate: 194/170 baseline + 24 new tests covering catalog sync
(happy path, sha256 mismatch, invalid manifest, lock contention,
preserves-on-failure) + resolver precedence + api renames. ruff
check + format clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>