Implement Amber/Nord/High Contrast theme presets with XDG user-scoped
persistence and reduced motion toggle. Covers TPH-10 and preference
integration with TPH-2.
- preferences.py: safe load/save with XDG_CONFIG_HOME/fenris/preferences.json
- themes.py: three Textual Theme objects with graph colour roles
- TUI: t cycles presets, m toggles reduced motion, both persist across restart
- Status composition receives reduced_motion from preferences
- 56 new tests covering persistence, themes, TUI integration, CLI isolation
- All 590 existing tests continue to pass
- Fix store dir mode: 2770 (per tmpfiles.d, matches test_store_group_access)
- Fix WAL/SHM survival: dpkg removes ephemeral SQLite files from
package-owned dirs; assert they are gone rather than present
- Fix opensuse migration_guard: use zypper instead of dnf
- Fix quote escaping in _setup_store_and_config
465 core tests pass. Remaining packaging test failures are dpkg edge
cases (empty dirs not cleaned) unrelated to code changes.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Replace all hardcoded docker commands with _container_cmd() helper
function that auto-detects podman or docker runtime.
Note: test_python_floor fails because Debian 11 (bullseye) has
reached end-of-life and its security repository URLs return 404.
This is a test infrastructure issue, not a code issue.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Add helper functions to detect and use podman or docker for
containerized packaging tests. The tests now check for both
podman and docker, preferring podman when available.
Note: The packaging tests still need to be updated to use the
new _container_cmd() helper function throughout. Currently only
the helper functions have been updated.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Headline and scenario rates now describe exact evidence-supported
monitored spans anchored at the latest published usage-evidence
endpoint T, not clock_now. Reader refresh alone never moves T or
dilutes rates.
- Add horizon_reasons field to ScenarioRange for specific unavailability facts
- Modify _compute_horizon_rate to use exact trailing 7/28/90×86400-second starts from T
- Show specific reasons for affected horizons (e.g., "starts before earliest data")
- Update TUI and CLI to display horizon-specific reasons
- Add 6 new tests for evidence-anchored projection rates
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Replace the static sparkline with an interactive block-glyph bar graph
that supports writes-only daily bars, range switching (7/14/28/90 days),
day selection, and hourly drill-down. No plotting dependency.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- Schema migration 1-2: add segment_id to samples, unattributed bytes to day_aggregates
- Collector now derives hour observations and day aggregates from sample pairs
- Cross-hour deltas tracked as unattributed (no proportional allocation)
- Display states: 0 samples -> awaiting first, 1 sample -> awaiting another
- Monitoring period ensured open on each collection run
- Derivation failures preserve prior history
Closes#73
- open_store_readonly(): stat() PermissionError (non-group user on the
2750 store dir) now maps to StoreFault so status/TUI degrade instead
of crashing with a traceback.
- init_store(): chmod db + -wal/-shm group rw after WAL setup — SQLite
WAL readers need write access to sidecars even for mode=ro opens.
- Store dir 2750 → 2770 (tmpfiles + make install) and UMask=002 on the
collect unit so root-created files stay group-accessible.
- rpm %post upgrade path re-runs systemd-tmpfiles --create to correct
placement modes on existing machines.
Bump to 0.3.3.
Fresh installs shipped a config template with no store_path key while
collector.py demanded one via get_store_path() — every first collect
crashed with KeyError 'store_path'. Resolve to the packaged default
(/var/lib/fenris/observations.db) when absent, document the key in the
template, and cover the fresh-install path with regression tests.
Bump to 0.3.2.
Implements the full release flow: a single script builds both deb and rpm
packages, signs the RPM payload, generates and clearsigns SHA256SUMS, uploads
to the Gitea package registry (deb to bookworm/jammy/noble pools, rpm to the
fenris group), creates a Gitea release entry with notes, and attaches all
artifacts.
Key changes:
- scripts/release.sh: new release script with --dry-run and --publish modes
- tests/test_release.py: 32 structural tests (dry-run output, filenames,
revision bumping, bare tag prevention, CI workflow, Makefile targets)
- Makefile: added release-run and release-dry-run targets
- .gitea/workflows/release.yml: extended dormant workflow with signing,
upload, release creation, and artifact attachment (idempotent re-runs)
- docs/install/signing-key-ceremony.md: added one-time live probe section
documenting throwaway package publish, apt/dnf verification, and cleanup
Acceptance criteria met:
- One release command performs build, sign, publish, and attach
- Dry-run mode prints every command; tests assert output without network
- Revision bumping on 409 (same-version rebuilds increment release number)
- Dormant CI workflow replicates the flow (queues harmlessly without runner)
- Live probe documented with throwaway package end-to-end
- No bare tags: release API creates tag atomically with release entry
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Migration runbook at docs/install/migrate-from-makeinstall.md covers the
mandatory remove-then-install path, why over-install is forbidden, no-move
continuity guarantees, and reset-to-dormant expectations.
Acceptance criteria MG-1 through MG-4 added to the install criteria section.
Containerized tests verify no-move continuity: existing group makes sysusers
a no-op, existing store dir makes tmpfiles a no-op, hand-edited config
survives as a non-database file, and store schema is caught up by the
upgrade-path migration. Dead code from a prior merge removed.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Replace the thin test_removal_semantics with comprehensive per-operation
tests covering all five acceptance criteria:
- deb remove keeps config, store (DB + WAL sidecars + backup), and group
- deb purge removes config, store, backup, and group
- rpm erase preserves modified config as .rpmsave
- rpm erase removes unmodified config
- store files never deleted except by purge
- dedicated test: sanctioned disable never runs on upgrade (parametrized
across all deb + rpm targets)
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Add comprehensive test coverage for upgrade semantics:
- 12 Python unit tests for store migration (forward-only, downgrade
refusal, idempotent behavior) across migrate_to_latest, init_store,
and open_store_readonly
- Enhanced packaging upgrade test to verify all five acceptance
criteria: snapshot before migration, store not rebuilt, config
survival, timer/removal no-ops during upgrade
- RPM-specific test for config file preservation (noreplace conffile)
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Add test_python_floor to the containerized packaging matrix:
- Sub-check 1: Ubuntu 22.04 (Python 3.10) installs successfully,
confirming the floor is met on the oldest supported deb target.
- Sub-check 2: Debian 11 (Python 3.9) fails to configure due to
unmet python3 (>= 3.10) dependency, verifying clean failure below floor.
Fix Makefile check-python gate to enforce Python >= 3.10, matching the
nfpm depends declaration.
Full compatibility matrix is now green: 17 packaging tests (dormant
install, migration guard, upgrade semantics, removal semantics, and
Python floor) pass across all four targets (Debian 12, Ubuntu 22.04,
Ubuntu 24.04, Fedora 40).
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- Fix nfpm.yaml ghost directory to include `group: fenris` so RPM metadata
matches the tmpfiles.d-created ownership (root:fenris 2750)
- Add RPM-native ownership assertions: store dir reported as package-owned
via `rpm -qf`, store contents verified as never owned by the package
- Add RPM conffile assertion: `rpm -qc` verifies fenris.conf is listed
- Unify store dir stat assertion across both formats (deb and rpm both
assert mode 2750 root:fenris)
- Remove unused `distro` parameter from `_find_package()`
All 16 packaging tests pass across the full matrix (3 deb + 1 rpm × 4 scenarios).
All 289 non-packaging tests pass.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- Copy packages to /pkg/ instead of /tmp/ to avoid tmpfs masking in
docker run --tmpfs /tmp, which hid packages needed at runtime by the
migration guard and upgrade tests
- Add version faking for deb upgrade test: sed the dpkg status to show
version 0.2.0 so dpkg -i treats the reinstall as an upgrade and
postinst receives the old-version argument
- For RPM upgrade test: extract and manually invoke the post scriptlet
with $1=2 (upgrade arguments), since faking a different version in
the binary RPM database is not practical
- Parameterize migration guard and upgrade assertions with pkg_name
(and version) instead of hardcoding filenames
All 16 packaging tests now pass across debian:bookworm, ubuntu:22.04,
ubuntu:24.04, and fedora:40.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- Upgrade path restarts only fenris-collect.timer, not fenris-collect.service
(spec §7: "a running oneshot finishes on its old interpreter")
- Remove redundant deb depends override in nfpm.yaml (top-level is sufficient)
- Remove common.sh sourcing — scripts are self-contained to avoid path
dependency when dpkg/rpm run them from /var/lib/dpkg/info/
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>