- Add XBPS build and sign steps to CI workflow
- Add XBPS publication as independent gate (requires manual trigger)
- Track format availability in release notes
- Update release-footer.md with XBPS install instructions
- Add --available/--withheld arguments to extract_changelog.py
- Attach XBPS artifacts to Gitea release
- Clean up XBPS signing key material after use
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Deliver end-to-end native monitoring path under runit with existing
CLI/TUI controls and truthful status, preserving systemd behavior.
Changes:
- Add init system abstraction layer (src/fenris/init_system.py) that
detects systemd vs runit and provides unified interface for timer
control, on-demand collection, and service state queries
- Create runit service files (units/runit/) with completion-relative
5-minute cadence, 2-minute boot delay, bounded execution (90s),
no catch-up, and serialized runs via flock
- Update monitor.py to use abstraction layer instead of direct systemctl
- Update status.py to use abstraction layer for service state queries
- Update all packaging scripts (deb, rpm) for init-system-aware setup
- Update Makefile to install runit service files alongside systemd units
- Add 46 tests for init system abstraction layer
Spec: ADR 0008, §8.4, §8.5, §8.6, §8.7, §8.8
Closes#84
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- Change default from ~/.ssh/id_rsa to ~/.ssh/id_xbps
- Add comment explaining key separation
- Update script documentation to match
Addresses code review finding: default signing key should not be
the user's personal SSH authentication key.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
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>
Implement fenris status as the read-only CLI twin of the TUI,
composing from the observation store and allow-listed systemctl
properties per spec section 8.8.
New module src/fenris/status.py:
- Freshness grading with shared constants (section 8.9, LC-10)
- Configuration error from direct config reads (section 8.3, LC-4)
- Store fault / newer-schema exact phrases (section 9.4-9.5, FL-4/FL-5)
- Drive anomalies as ordinary facts (section 9.7, FL-7)
- Four separate service facts (section 7.3, LC-9, CI-2)
- Projection recomputed on read, never stored (section 6.10)
- Retired command rejection with migration pointers (section 8.8)
- Six disclosures via --disclosures flag (section 6.11, CI-4)
Updated fenris.py:
- Replaced old cmd_status with new status module integration
- Added retired command handlers (start/stop/run)
- Added global --device flag rejection
Tests: 43 new, 182 total passing, zero regressions.
Closes#27.
Hour classification (PR-4):
- Powered-off: POH delta < 90% of wall-clock span
- Active: DUW delta >= 256 MiB
- Idle: powered on + sampled + below active threshold
- Unknown: unsampled without POH evidence
- Four splits sum to exactly wall_clock_seconds
- Disabled time is never an hour state
Monitoring periods (FL-8):
- ensure_period_open: opens period at run moment if none exists
- close_period: closes with end cause
- is_inside_period: checks timestamp against period bounds
- Never backdated; wall-clock outside periods excluded from denominator
Day aggregates (ST-4, PR-5):
- Derived monotonically from hour rows
- UTC-bounded; no 23/25-hour days
- Coverage: known seconds / period wall-clock
- Gap hours inside periods contribute unknown seconds
- Hours outside periods excluded entirely
- No absent hour interpolated/estimated/fabricated (FL-3)
Raw sample pruning (ST-5):
- Prunes samples older than 14 days
- Hour observations and day aggregates retained indefinitely
Closes#22
- Header now shows the Bongbetic icon + light/dark wordmark (linked to
bongbetic.com) next to FENRIS — no more lonely "F" placeholder.
- Footer gets the little b_glyph monogram for that extra polish.
- Wires up real favicons (ico + 16/32) and apple-touch-icon + webmanifest
so the tab and bookmarks actually look like Bongbetic.
- Vendors the missing brand bits: favicon set, apple icon, 192px icons,
hi-res wordmark and contact sheet.
- Rewrite README in a playful, plain-English voice and add Bongbetic
wordmarks/glyph/icons so Gitea renders the brand nicely (light/dark).
- Vendor assets/bongbetic-brand/* from /mnt/Toto/Documents/bongbetic/Logo.
- Remove plan-dash-changes.md from tracking (internal only) and gitignore it.