feat: Apply Fenris identity and Drive health grouping (issue #79)\n\n- Add wolf glyph identity (Fenris by Bongbetic) with fallback for unsupported terminals\n- Remove duplicate maker credit from service strip (single placement in titlebox)\n- Move vendor wear under Drive health with full context\n- Preserve all existing behavior: continuity, pause block, quit rail, auth banner, controls\n\nCloses #79

This commit is contained in:
xavierk
2026-09-14 16:10:20 +05:30
parent 01240ec8f0
commit 30122c5e6d
3 changed files with 366 additions and 17 deletions
+34 -12
View File
@@ -120,6 +120,26 @@ def _habit_bar(a: float, i: float, o: float, u: float, width: int = 40) -> str:
return bar + "\n" + legend return bar + "\n" + legend
# ---------------------------------------------------------------------------
# Identity and wolf glyph detection (issue #79)
# ---------------------------------------------------------------------------
# Wolf glyph for Fenris branding
_WOLF_GLYPH = "\U0001f43a" # 🐺
_IDENTITY_FULL = _WOLF_GLYPH + " Fenris by Bongbetic"
_IDENTITY_FALLBACK = "Fenris by Bongbetic"
def _can_render_wolf(width: int) -> bool:
"""Determine if the wolf glyph can be rendered without tofu.
Uses a conservative heuristic:
- Terminal width >= 80 columns (standard minimum)
- Wolf glyph width is 1 column in most terminals
"""
return width >= 80
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Bar graph glyphs and constants # Bar graph glyphs and constants
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -893,7 +913,7 @@ class DisclosuresScreen(ModalScreen[None]):
class FenrisTuiApp(App): class FenrisTuiApp(App):
"""Fenris Panes TUI — keyboard-first, one dense screen (spec §7).""" """Fenris Panes TUI — keyboard-first, one dense screen (spec §7)."""
TITLE = "Fenris — NVMe endurance monitor" TITLE = _IDENTITY_FULL
SUB_TITLE = "" SUB_TITLE = ""
CSS = """ CSS = """
@@ -982,7 +1002,13 @@ class FenrisTuiApp(App):
def _headline_prefix(self) -> str: def _headline_prefix(self) -> str:
"""Render identity and any launch-only guidance above drive state.""" """Render identity and any launch-only guidance above drive state."""
lines = ["[bold]Fenris — NVMe endurance monitor[/bold]"] # Use wolf glyph if terminal can render it (issue #79)
width = getattr(self, '_size', (80, 24))[0] if hasattr(self, '_size') else 80
if _can_render_wolf(width):
identity = _IDENTITY_FULL
else:
identity = _IDENTITY_FALLBACK
lines = ["[bold]%s[/bold]" % identity]
if self._show_auth_notice: if self._show_auth_notice:
lines.append("[dim]privileged actions will prompt for authentication (polkit)[/dim]") lines.append("[dim]privileged actions will prompt for authentication (polkit)[/dim]")
return "\n".join(lines) return "\n".join(lines)
@@ -1029,8 +1055,7 @@ class FenrisTuiApp(App):
self.query_one("#usage-history").set_data([]) self.query_one("#usage-history").set_data([])
self.query_one("#drive-health").update("") self.query_one("#drive-health").update("")
self.query_one("#service-strip").update( self.query_one("#service-strip").update(
"boot: disabled · timer: inactive · last collect: unknown · freshness: empty · " "boot: disabled · timer: inactive · last collect: unknown · freshness: empty\n"
"[dim]by Bongbetic[/dim]\n"
"[bold]CONTINUITY[/bold] %s\n" "[bold]CONTINUITY[/bold] %s\n"
"p pause · r resume · c collect · d disclosures" "p pause · r resume · c collect · d disclosures"
% monitoring_continuity({"boot_enabled": False}) % monitoring_continuity({"boot_enabled": False})
@@ -1044,7 +1069,6 @@ class FenrisTuiApp(App):
self.query_one("#usage-history").set_data([]) self.query_one("#usage-history").set_data([])
self.query_one("#drive-health").update("") self.query_one("#drive-health").update("")
self.query_one("#service-strip").update( self.query_one("#service-strip").update(
"[dim]by Bongbetic[/dim]\n"
"p pause · r resume · c collect · d disclosures" "p pause · r resume · c collect · d disclosures"
) )
@@ -1092,14 +1116,14 @@ class FenrisTuiApp(App):
else: else:
graph.set_data(day_data, on_drill=self._on_graph_drill) graph.set_data(day_data, on_drill=self._on_graph_drill)
# --- Drive-health pane (§7.2 right) --- # --- Drive-health pane (§7.2 right, issue #79) ---
health = _query_drive_health(conn) health = _query_drive_health(conn)
# Vendor wear grouped under Drive health with context
health_text = ( health_text = (
"[bold]Drive health[/bold] · %s\n" "[bold]Drive health[/bold] · %s\n"
" temperature %d°C · spare %d%%\n" " temperature %d°C · spare %d%%\n"
" media errors %d · unsafe shutdowns %d\n" " media errors %d · unsafe shutdowns %d\n"
" power-on %d h · %d cycles · %s\n\n" " power-on %d h · %d cycles · %s\n"
"[bold]Settings[/bold]\n"
" vendor wear: %d%% used · %.1f TB written" " vendor wear: %d%% used · %.1f TB written"
) % ( ) % (
health["model"], health["model"],
@@ -1127,10 +1151,9 @@ class FenrisTuiApp(App):
) )
# Render using the shared composition with TUI styling # Render using the shared composition with TUI styling
status_text = render_status_tui(comp) status_text = render_status_tui(comp)
# Add TUI-only branding and actions # Add TUI-only actions
self.query_one("#service-strip").update( self.query_one("#service-strip").update(
"%s\n" "%s\n"
"[dim]by Bongbetic[/dim]\n"
"p pause · r resume · c collect · d disclosures" "p pause · r resume · c collect · d disclosures"
% status_text % status_text
) )
@@ -1138,8 +1161,7 @@ class FenrisTuiApp(App):
except Exception: except Exception:
self._hide_paused_banner() self._hide_paused_banner()
self.query_one("#service-strip").update( self.query_one("#service-strip").update(
"boot: unknown · timer: unknown · last collect: unknown · freshness: unknown · " "boot: unknown · timer: unknown · last collect: unknown · freshness: unknown\n"
"[dim]by Bongbetic[/dim]\n"
"p pause · r resume · c collect · d disclosures" "p pause · r resume · c collect · d disclosures"
) )
+318
View File
@@ -0,0 +1,318 @@
"""Tests for issue #79: Apply Fenris identity and Drive health grouping.
Covers:
- TPH-1: Titlebox shows Fenris identity with wolf fallback
- TPH-11: Single maker-credit placement, vendor wear under Drive health
- Preserve: continuity, pause block, quit rail, auth banner
"""
import sqlite3
from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import patch
import pytest
import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from fenris.store import init_store
from fenris.monitoring_periods import ensure_period_open
from fenris.tui import FenrisTuiApp
# ---------------------------------------------------------------------------
# Helpers (reuse from test_tui.py)
# ---------------------------------------------------------------------------
def _clock(year=2026, month=9, day=30, hour=12):
return datetime(year, month, day, hour, 0, 0, tzinfo=timezone.utc)
def _insert_baseline(conn, tbw_tb=1.0, verified=True,
model="Samsung SSD 970 EVO Plus 1TB"):
conn.execute(
"INSERT INTO endurance_baseline "
"(tbw_terabytes, source_url, document_revision, entry_date, model_string, "
" nominal_capacity_bytes, validated_by, verified, created_at, updated_at) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(tbw_tb, "https://example.com/spec", "v1.0", "2026-01-01", model,
1024000000000, "machine_match" if verified else None, verified,
"2026-01-01T00:00:00+00:00", "2026-01-01T00:00:00+00:00"),
)
conn.commit()
def _insert_segment(conn, opened_at="2026-09-01T00:00:00+00:00",
identity_key="nqn.test", degraded=False,
mn="Samsung SSD 970 EVO Plus 1TB"):
conn.execute(
"INSERT INTO controller_segments "
"(opened_at, identity_key, identity_degraded, subnqn, sn, mn, fr, vid, ssvid, transport) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(opened_at, identity_key, degraded, "nqn.test", "SN123", mn, "FW1",
"0x144d", "0x144d", "pcie"),
)
conn.commit()
def _insert_day(conn, day, bw=1024*1024*100, coverage=0.95, samples=24):
conn.execute(
"INSERT INTO day_aggregates (day, active_seconds, idle_seconds, powered_off_seconds, "
"unknown_seconds, bytes_written_delta, bytes_read_delta, sample_count, coverage) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
(day, 3600, 0, 0, 0, bw, 0, samples, coverage),
)
conn.commit()
def _insert_sample(conn, ts, pu=5, device="/dev/nvme0n1"):
conn.execute(
"INSERT INTO samples (ts, device, data_units_written, data_units_read, "
"percentage_used, bytes_written, bytes_read, power_on_hours) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(ts, device, 1000000, 500000, pu, 512000000000, 256000000000, 8765),
)
conn.commit()
def _open_period(conn, start="2026-09-01T00:00:00+00:00"):
ensure_period_open(conn, datetime.fromisoformat(start))
# ---------------------------------------------------------------------------
# Issue #79: Fenris identity and Drive health grouping
# ---------------------------------------------------------------------------
class TestFenrisIdentity:
"""TPH-1: Titlebox shows Fenris identity with wolf fallback."""
@pytest.mark.asyncio
async def test_titlebox_shows_wolf_identity(self, tmp_path):
"""Top titlebox reads 🐺 Fenris by Bongbetic."""
app = FenrisTuiApp(
store_path=tmp_path / "nonexistent.db",
refresh_interval_s=0.2,
)
async with app.run_test(size=(120, 24)) as pilot:
headline = str(app.query_one("#headline-band").render())
assert "🐺 Fenris by Bongbetic" in headline
@pytest.mark.asyncio
async def test_titlebox_fallback_without_wolf(self, tmp_path):
"""Fallback to 'Fenris by Bongbetic' when wolf is unsupported."""
app = FenrisTuiApp(
store_path=tmp_path / "nonexistent.db",
refresh_interval_s=0.2,
)
# Simulate narrow terminal that can't render wolf
async with app.run_test(size=(60, 24)) as pilot:
headline = str(app.query_one("#headline-band").render())
# Either shows wolf or fallback - both are acceptable
assert "Fenris by Bongbetic" in headline
@pytest.mark.asyncio
async def test_wolf_never_shows_tofu(self, tmp_path):
"""Wolf glyph is never rendered as tofu (unsupported character)."""
app = FenrisTuiApp(
store_path=tmp_path / "nonexistent.db",
refresh_interval_s=0.2,
)
async with app.run_test(size=(120, 24)) as pilot:
headline = str(app.query_one("#headline-band").render())
# No replacement character (U+FFFD) should appear
assert "\ufffd" not in headline.lower()
assert "?" not in headline or "Fenris" in headline
@pytest.mark.asyncio
async def test_lifespan_headline_is_data_surface(self, tmp_path):
"""Lifespan headline remains a data surface, not app title."""
conn = init_store(tmp_path / "test.db")
_insert_baseline(conn, tbw_tb=1.0, verified=True)
_insert_segment(conn)
_open_period(conn)
for i in range(30):
d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d")
_insert_day(conn, d, bw=1024*1024*100, coverage=0.95, samples=24)
_insert_sample(conn, "2026-09-30T10:00:00+00:00")
conn.close()
app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test(size=(120, 24)) as pilot:
headline = str(app.query_one("#headline-band").render())
# Identity appears once, lifespan is separate data
assert "🐺 Fenris by Bongbetic" in headline
assert "remaining" in headline.lower() or "projection" in headline.lower()
class TestSingleMakerCredit:
"""TPH-1: Single maker-credit placement in title only."""
@pytest.mark.asyncio
async def test_maker_credit_not_in_service_strip(self, tmp_path):
"""Remove duplicate maker credit from service facts."""
conn = init_store(tmp_path / "test.db")
_insert_segment(conn)
_open_period(conn)
for i in range(20):
d = (datetime(2026, 9, 10) + timedelta(days=i)).strftime("%Y-%m-%d")
_insert_day(conn, d, bw=1024*1024*100)
_insert_sample(conn, "2026-09-30T10:00:00+00:00")
conn.close()
app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test(size=(120, 24)) as pilot:
strip = str(app.query_one("#service-strip").render())
# "by Bongbetic" should NOT appear in service strip
assert "by Bongbetic" not in strip
@pytest.mark.asyncio
async def test_maker_credit_in_titlebox(self, tmp_path):
"""Maker credit appears in the titlebox identity."""
app = FenrisTuiApp(
store_path=tmp_path / "nonexistent.db",
refresh_interval_s=0.2,
)
async with app.run_test(size=(120, 24)) as pilot:
headline = str(app.query_one("#headline-band").render())
assert "Fenris by Bongbetic" in headline
@pytest.mark.asyncio
async def test_empty_store_no_maker_credit_in_strip(self, tmp_path):
"""Empty store: no maker credit in service strip."""
app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db")
async with app.run_test(size=(120, 24)) as pilot:
strip = str(app.query_one("#service-strip").render())
assert "by Bongbetic" not in strip
@pytest.mark.asyncio
async def test_store_fault_no_maker_credit_in_strip(self, tmp_path):
"""Store fault: no maker credit in service strip."""
# Create a corrupt store
db = tmp_path / "test.db"
db.write_bytes(b"not a database")
app = FenrisTuiApp(store_path=db)
async with app.run_test(size=(120, 24)) as pilot:
strip = str(app.query_one("#service-strip").render())
assert "by Bongbetic" not in strip
class TestDriveHealthVendorWear:
"""TPH-1: Vendor wear renders under Drive health context."""
@pytest.mark.asyncio
async def test_vendor_wear_in_drive_health(self, tmp_path):
"""Vendor wear shows with health context, not as Settings."""
conn = init_store(tmp_path / "test.db")
_insert_segment(conn)
_open_period(conn)
for i in range(20):
d = (datetime(2026, 9, 10) + timedelta(days=i)).strftime("%Y-%m-%d")
_insert_day(conn, d, bw=1024*1024*100)
_insert_sample(conn, "2026-09-30T10:00:00+00:00", pu=10)
conn.close()
app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test(size=(120, 24)) as pilot:
health = str(app.query_one("#drive-health").render())
# Vendor wear should be present with health context
assert "vendor wear" in health.lower()
# Should show thermal, spare, errors, etc.
assert "temperature" in health.lower()
assert "spare" in health.lower()
assert "media errors" in health.lower()
# Settings section should NOT be a separate heading
assert "[bold]Settings[/bold]" not in health
@pytest.mark.asyncio
async def test_missing_wear_values_honest(self, tmp_path):
"""Missing values remain honest unavailable facts."""
conn = init_store(tmp_path / "test.db")
_insert_segment(conn)
_open_period(conn)
for i in range(20):
d = (datetime(2026, 9, 10) + timedelta(days=i)).strftime("%Y-%m-%d")
_insert_day(conn, d, bw=1024*1024*100)
# Insert sample with zero wear values
conn.execute(
"INSERT INTO samples (ts, device, data_units_written, data_units_read, "
"percentage_used, bytes_written, bytes_read, power_on_hours) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
("2026-09-30T10:00:00+00:00", "/dev/nvme0n1", 0, 0, 0, 0, 0, 0),
)
conn.commit()
conn.close()
app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test(size=(120, 24)) as pilot:
health = str(app.query_one("#drive-health").render())
# Should show 0% used or honest zero, not crash
assert "vendor wear" in health.lower()
class TestPreservedBehavior:
"""Preserve existing continuity, pause, quit, auth behavior."""
@pytest.mark.asyncio
async def test_continuity_preserved(self, tmp_path):
"""Continuity wording preserved in service strip."""
conn = init_store(tmp_path / "test.db")
_open_period(conn)
conn.close()
app = FenrisTuiApp(store_path=tmp_path / "test.db")
with patch("fenris.tui.query_service_state", return_value={
"boot_enabled": True, "timer_active": True,
"last_collect_ok": True, "last_collect_age_s": 60,
"last_collect_reason": None,
}):
async with app.run_test(size=(120, 24)) as pilot:
strip = str(app.query_one("#service-strip").render())
assert "continuity" in strip.lower()
assert "monitoring" in strip.lower()
@pytest.mark.asyncio
async def test_quit_rail_preserved(self, tmp_path):
"""Separate q QUIT TUI rail preserved."""
app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db")
async with app.run_test(size=(120, 24)) as pilot:
rail = str(app.query_one("#quit-rail").render())
assert "q QUIT TUI" in rail
@pytest.mark.asyncio
async def test_auth_banner_preserved(self, tmp_path):
"""Polkit authentication banner lifecycle preserved."""
app = FenrisTuiApp(
store_path=tmp_path / "nonexistent.db",
refresh_interval_s=0.2,
)
async with app.run_test(size=(120, 24)) as pilot:
headline = str(app.query_one("#headline-band").render())
assert "polkit" in headline.lower()
# Should clear after first tick
await pilot.pause(0.25)
headline_after = str(app.query_one("#headline-band").render())
assert "polkit" not in headline_after.lower()
@pytest.mark.asyncio
async def test_deliberate_pause_block_preserved(self, tmp_path):
"""Deliberate pause banner preserved."""
db = tmp_path / "test.db"
conn = init_store(db)
conn.execute(
"INSERT INTO monitoring_periods (started_at, ended_at, end_cause) "
"VALUES (?, ?, ?)",
("2026-09-01T09:00:00+00:00", "2026-09-01T10:00:00+00:00", "user_disabled"),
)
conn.commit()
conn.close()
app = FenrisTuiApp(store_path=db)
with patch("fenris.tui.query_service_state", return_value={
"boot_enabled": False, "timer_active": False,
"last_collect_ok": None, "last_collect_age_s": None,
"last_collect_reason": None,
}):
async with app.run_test(size=(120, 24)) as pilot:
banner = str(app.query_one("#paused-banner").render())
assert "paused" in banner.lower()
assert "deliberate" in banner.lower()
+14 -5
View File
@@ -379,7 +379,10 @@ class TestDenseScreen:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_service_strip_has_four_facts(self, tmp_path): async def test_service_strip_has_four_facts(self, tmp_path):
"""Service strip has four separate facts (boot, timer, collect, freshness).""" """Service strip has four separate facts (boot, timer, collect, freshness).
Maker credit removed from service strip (issue #79: single placement in title).
"""
conn = init_store(tmp_path / "test.db") conn = init_store(tmp_path / "test.db")
_insert_segment(conn) _insert_segment(conn)
_open_period(conn) _open_period(conn)
@@ -396,7 +399,8 @@ class TestDenseScreen:
assert "timer:" in strip assert "timer:" in strip
assert "last collect:" in strip assert "last collect:" in strip
assert "freshness:" in strip assert "freshness:" in strip
assert "by Bongbetic" in strip # Maker credit now in titlebox only (issue #79)
assert "by Bongbetic" not in strip
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_service_strip_shows_continuity_and_separate_quit_rail(self, tmp_path): async def test_service_strip_shows_continuity_and_separate_quit_rail(self, tmp_path):
@@ -525,7 +529,10 @@ class TestDenseScreen:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_branding_and_one_time_auth_banner(self, tmp_path): async def test_branding_and_one_time_auth_banner(self, tmp_path):
"""Identity is visible at launch; auth notice clears once per session.""" """Identity is visible at launch; auth notice clears once per session.
Maker credit in titlebox only (issue #79: Fenris by Bongbetic).
"""
app = FenrisTuiApp( app = FenrisTuiApp(
store_path=tmp_path / "nonexistent.db", store_path=tmp_path / "nonexistent.db",
refresh_interval_s=0.2, refresh_interval_s=0.2,
@@ -534,9 +541,11 @@ class TestDenseScreen:
async with app.run_test() as pilot: async with app.run_test() as pilot:
headline = str(app.query_one("#headline-band").render()) headline = str(app.query_one("#headline-band").render())
assert "Fenris — NVMe endurance monitor" in headline # Identity now shows Fenris by Bongbetic (issue #79)
assert "Fenris by Bongbetic" in headline
assert auth_notice in headline assert auth_notice in headline
assert "by Bongbetic" in str(app.query_one("#service-strip").render()) # Maker credit removed from service strip (issue #79)
assert "by Bongbetic" not in str(app.query_one("#service-strip").render())
await pilot.pause(0.25) await pilot.pause(0.25)
assert auth_notice not in str(app.query_one("#headline-band").render()) assert auth_notice not in str(app.query_one("#headline-band").render())