Batch promotes an entire OS version (metadata + all iso/squashfs/img assets
across all platforms) from one registry to another, mirroring the existing
package promote command.
Allows promoting a package version from one registry to another by
fetching package info from the source and re-signing/publishing each
s9pk variant to the destination.
The btrfs check --readonly command can produce false positives. Skip it
for the preen strategy and only run the repair path when aggressive
repair is requested.
Migrate registry metrics from PostgreSQL/sqlx to embedded SQLite via
rusqlite. Add new metrics CLI subcommands (summary, users, downloads)
with i18n support. Record os_version in user activity and package
download requests. Remove old PostgreSQL schema and setup script.
Replace GitHub Actions artifact downloads with registry-based OS image
retrieval via start-cli. Add publish-tunnel subcommand, registry_url
helper, and remove old S3/RUN_ID workflows. Also clean up old deb
versions in publish-deb.sh before copying new ones.
The hairpin NAT check uses Linux-specific APIs (bind_device, raw fd
conversion). Extract it into a separate function with #[cfg(target_os)]
so the entire block is excluded on non-Linux platforms, rather than
guarding only the unsafe block.
The sed-based platform extraction was greedy, turning "x86_64" into "64".
Replace with explicit platform list iteration. Exclude raspberrypi from
deploy. Re-enable raspberrypi as a platform choice for builds.
fix: give StartOS UI interface a non-empty id
The iface object in StartOsUiComponent had id: '' (empty string).
Any plugin whose action calls sdk.serviceInterface.get() with
that id triggers an RPC to the host with an empty
serviceInterfaceId, which Rust's ServiceInterfaceId type rejects
via its ID regex (^[a-z0-9]+(-[a-z0-9]+)*$).
The container runtime appends the method name to every error
message as "${msg}@${method}", so the empty-string failure
surfaces in the UI as:
Action Failed: Deserialization Error: Invalid ID: @get-service-interface
Setting id: 'startos-ui' makes it a valid, stable identifier
that passes the regex and accurately names the interface.
The POSTROUTING MASQUERADE rules in forward-port failed to handle two
hairpin scenarios:
1. Host-to-target hairpin (OUTPUT DNAT): when sip is a WAN IP (tunnel
case), the old rule matched `-s sip` but the actual source of
locally-originated packets is a local interface IP, not the WAN IP.
Fix: use `-m addrtype --src-type LOCAL -m conntrack --ctorigdst sip`
to match any local source while tying the rule to the specific sip.
2. Same-subnet self-hairpin (PREROUTING DNAT): when a WireGuard peer
connects to itself via the tunnel's public IP, traffic is DNAT'd back
to the peer. Without MASQUERADE the response takes a loopback shortcut,
bypassing the tunnel server's conntrack and breaking NAT reversal.
Fix: add `-s dip/dprefix -d dip` to masquerade same-subnet traffic,
which also subsumes the old bridge_subnet rule.
Also bind the hairpin detection socket to the gateway interface and local
IP for consistency with the echoip client.
Using Ipv4Addr::UNSPECIFIED (0.0.0.0) as the local address with
SO_BINDTODEVICE caused bind(0.0.0.0:0) to fail with "Address in use"
on interfaces where port 443 was already in use. Binding to the
gateway's actual IPv4 address instead still forces IPv4 DNS filtering
while avoiding the kernel-level conflict.
fix: correct false breakage detection for flavored packages and config changes
Two bugs caused the UI to incorrectly warn about dependency breakages:
1. dryUpdate (version path): Flavored package versions (e.g. #knots:27.0.0:0)
failed exver.satisfies() against flavorless ranges (e.g. >=26.0.0) due to
flavor mismatch. Now checks the manifest's `satisfies` declarations,
matching the pattern already used in DepErrorService. Added `satisfies`
field to PackageVersionInfo so it's available from registry data.
2. checkConflicts (config path): fast-json-patch's compare() treated missing
keys as conflicts (add ops) and used positional array comparison, diverging
from the backend's conflicts() semantics. Replaced with a conflicts()
function that mirrors core/src/service/action.rs — missing keys are not
conflicts, and arrays use set-based comparison.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: unified restart notification with reason-specific messaging
Replace statusInfo.updated (bool) with serverInfo.restart (nullable enum)
to unify all restart-needed scenarios under a single PatchDB field.
Backend sets the restart reason in RPC handlers for hostname change (mdns),
language change, kiosk toggle, and OS update download. Init clears it on
boot. The update flow checks this field to prevent updates when a restart
is already pending.
Frontend shows a persistent action bar with reason-specific i18n messages
instead of per-feature restart dialogs. For .local hostname changes, the
existing "open new address" dialog is preserved — the restart toast
appears after the user logs in on the new address.
Also includes migration in v0_4_0_alpha_23 to remove statusInfo.updated
and initialize serverInfo.restart.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix broken styling and improve settings layout
* refactor: move restart field from ServerInfo to ServerStatus
The restart reason belongs with other server state (shutting_down,
restarting, update_progress) rather than on the top-level ServerInfo.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix PR comment
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Aiden McClelland <me@drbonez.dev>
The BIOS_BOOT_TYPE_GUID constant had the wrong value, so
find_bios_boot_partition never matched the actual BIOS boot partition
created by the gpt crate. This caused it to appear as an available
backup target.
Split poll_ip_info into two phases: write IP info (addresses, subnets,
gateway, DNS, NTP) to the watch immediately, then fetch WAN IP in a
second pass. Previously the echoip HTTP fetch (5s timeout per URL)
blocked the write and was repeatedly cancelled by D-Bus signals during
interface activation, preventing the gateway from ever appearing.
Replace PolicyRoutingCleanup Drop with gc_policy_routing. The old Drop
spawned async route flushes that raced with new apply_policy_routing
calls when the watcher restarted on device_added, wiping freshly-created
routing tables for existing interfaces like eth0. Now policy routing is
managed idempotently by apply_policy_routing, and stale rules are
garbage-collected at the start of each watcher iteration.
- Fix parseInt callback in container-runtime to avoid extra map arguments
- Use proper error propagation in list_service_interfaces instead of unwrap_or_default
- Handle non-plain objects by reference in deepEqual