Release Fenris 0.5.0 with Chalktone dashboard and dotted activity plots

This commit is contained in:
xavierk
2026-09-19 11:13:37 +05:30
parent 8bac26836a
commit e49aac8569
23 changed files with 1083 additions and 623 deletions
+64 -61
View File
@@ -40,19 +40,13 @@ from fenris.status import (
)
from fenris.tui import (
FenrisTuiApp,
DailyBarGraph,
HistoryGraph,
_format_remaining,
_query_drive_health,
_query_daily_graph_data,
_query_hourly_graph_data,
_RANGE_OPTIONS,
_RANGE_DEFAULT,
_BAR_HEIGHT,
_GLYPH_ALLOCATED,
_GLYPH_UNALLOCATED,
_GLYPH_GAP,
_GLYPH_ZERO,
_GLYPH_PARTIAL,
_MIN_WIDTH,
_MIN_HEIGHT,
)
@@ -365,15 +359,15 @@ class TestDenseScreen:
}):
async with app.run_test():
strip = str(app.query_one("#service-strip").render()).lower()
rail = str(app.query_one("#quit-rail").render())
usage = app.query_one("#usage-history")
rail = str(app.query_one("#action-rail").render())
usage = app.query_one("#activity-panel")
service = app.query_one("#service-strip")
quit_rail = app.query_one("#quit-rail")
quit_rail = app.query_one("#action-rail")
assert "continuity" in strip
assert "monitoring: active in background · persists across reboots" in strip
assert "r resume — enable monitoring and future boots" in strip
assert "r Resume" in str(app.query_one("#action-rail").render())
assert "q quit" not in strip
assert rail == "q Quit TUI"
assert "q Quit TUI" in rail
assert usage.region.y < service.region.y < quit_rail.region.y
assert usage.region.bottom <= service.region.y
assert service.region.bottom <= quit_rail.region.y
@@ -405,13 +399,12 @@ class TestDenseScreen:
main_grid = app.query_one("#main-grid")
assert str(main_grid.styles.layout) == "<grid>"
assert main_grid.has_class("paused")
assert len(main_grid.styles.grid_rows) == 6
banner = str(paused_banner.render()).lower()
assert "monitoring: paused — deliberate disable" in banner
assert "paused time is excluded from your usage habit · resume: fenris monitor resume" in banner
assert paused_banner.region.height >= 5
assert paused_banner.region.y < app.query_one("#usage-history").region.y
assert app.query_one("#usage-history").region.bottom <= app.query_one(
assert paused_banner.region.height >= 2
assert paused_banner.region.y < app.query_one("#activity-panel").region.y
assert app.query_one("#activity-panel").region.bottom <= app.query_one(
"#service-strip"
).region.y
screenshot = app.export_screenshot()
@@ -423,11 +416,6 @@ class TestDenseScreen:
assert "monitoring: does not start on next boot" in str(
app.query_one("#service-strip").render()
).lower()
dashboard_scroll = app.query_one("#dashboard-scroll")
assert dashboard_scroll.max_scroll_y > 0
dashboard_scroll.focus()
await pilot.press("end")
assert dashboard_scroll.scroll_y == dashboard_scroll.max_scroll_y
footer_text = " ".join(
"".join(
ElementTree.fromstring(app.export_screenshot()).itertext()
@@ -490,23 +478,23 @@ class TestDenseScreen:
async with app.run_test() as pilot:
headline = str(app.query_one("#headline-band").render())
# Identity now shows Fenris by Bongbetic (issue #79)
assert "Fenris by Bongbetic" in headline
assert auth_notice in headline
assert "Fenris by Bongbetic" in str(app.query_one("#identity").render())
assert auth_notice in str(app.query_one("#auth-notice").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)
assert auth_notice not in str(app.query_one("#headline-band").render())
assert not app.query_one("#auth-notice").display
await pilot.pause(0.25)
assert auth_notice not in str(app.query_one("#headline-band").render())
assert not app.query_one("#auth-notice").display
fresh_app = FenrisTuiApp(
store_path=tmp_path / "nonexistent.db",
refresh_interval_s=0.2,
)
async with fresh_app.run_test():
assert auth_notice in str(fresh_app.query_one("#headline-band").render())
assert auth_notice in str(fresh_app.query_one("#auth-notice").render())
# ---------------------------------------------------------------------------
@@ -600,7 +588,7 @@ class TestFirstRun:
strip = str(app.query_one("#service-strip").render())
assert "no observations yet" in headline.lower()
assert "r resume — enable monitoring and future boots" in headline.lower()
assert "r resume — enable monitoring and future boots" in strip.lower()
assert "r resume" in str(app.query_one("#action-rail").render()).lower()
# ---------------------------------------------------------------------------
@@ -864,15 +852,15 @@ class TestQueryHourlyGraphData:
# ---------------------------------------------------------------------------
# DailyBarGraph widget unit tests (issue #75)
# HistoryGraph widget unit tests (issue #75)
# ---------------------------------------------------------------------------
class TestDailyBarGraph:
class TestHistoryGraph:
def test_empty_data(self, tmp_path):
"""Empty data shows awaiting message."""
app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db")
# We test the widget directly via the app's compose
graph = DailyBarGraph()
graph = HistoryGraph()
# Simulate setting empty data
from textual.app import App as TextualApp
class _TestApp(TextualApp):
@@ -884,14 +872,14 @@ class TestDailyBarGraph:
def test_range_default(self):
"""Default range is 14 days."""
graph = DailyBarGraph()
graph = HistoryGraph()
assert graph.range_days == _RANGE_DEFAULT
assert graph.selected_index == -1
assert graph.view_mode == "daily"
def test_trim_to_range(self):
"""Data is trimmed to the selected range."""
graph = DailyBarGraph()
graph = HistoryGraph()
data = [{"day": "2026-09-%02d" % d, "total_bytes": d * 100}
for d in range(1, 31)]
graph._all_day_data = data
@@ -902,7 +890,7 @@ class TestDailyBarGraph:
def test_range_all_fits(self):
"""When data fits within range, all days are shown."""
graph = DailyBarGraph()
graph = HistoryGraph()
data = [{"day": "2026-09-%02d" % d, "total_bytes": d * 100}
for d in range(1, 8)]
graph._all_day_data = data
@@ -911,7 +899,7 @@ class TestDailyBarGraph:
assert len(graph._day_data) == 7
def test_scale_includes_allocated_and_unallocated_writes(self):
graph = DailyBarGraph()
graph = HistoryGraph()
graph._all_day_data = [{
"day": "2026-09-16",
"total_bytes": 800,
@@ -927,7 +915,7 @@ class TestDailyBarGraph:
# ---------------------------------------------------------------------------
class TestBarGraphTUI:
"""Headless tests for the interactive bar graph."""
"""Headless tests for the interactive history plot."""
@pytest.mark.asyncio
async def test_graph_renders_with_data(self, tmp_path):
@@ -943,20 +931,21 @@ class TestBarGraphTUI:
app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test(size=(80, 24)) as pilot:
await pilot.press("v", "v")
graph = app.query_one("#usage-history")
assert isinstance(graph, DailyBarGraph)
assert isinstance(graph, HistoryGraph)
assert len(graph._day_data) > 0
# Legend should be visible
legend = str(app.query_one("#bar-legend").render())
assert "Alloc" in legend
assert "Unalloc" in legend
assert "Zero" in legend
range_label = str(app.query_one("#bar-range").render())
assert "14 days" in range_label
assert "UTC" in range_label
plotted = str(app.query_one("#bar-render").render())
assert "Writes (" in plotted
assert "Day UTC" in plotted
assert "→" in plotted
assert "Writes (" in legend
assert "UTC" in range_label
assert "─" in plotted
@pytest.mark.asyncio
async def test_arrow_selection(self, tmp_path):
@@ -972,6 +961,7 @@ class TestBarGraphTUI:
app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test(size=(80, 24)) as pilot:
await pilot.press("v", "v")
graph = app.query_one("#usage-history")
app.set_focus(graph)
await pilot.pause()
@@ -1002,6 +992,7 @@ class TestBarGraphTUI:
app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test(size=(80, 24)) as pilot:
await pilot.press("v", "v")
graph = app.query_one("#usage-history")
app.set_focus(graph)
await pilot.pause()
@@ -1043,6 +1034,7 @@ class TestBarGraphTUI:
app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test(size=(80, 24)) as pilot:
await pilot.press("v", "v")
graph = app.query_one("#usage-history")
app.set_focus(graph)
await pilot.pause()
@@ -1079,6 +1071,7 @@ class TestBarGraphTUI:
app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test(size=(80, 24)) as pilot:
await pilot.press("v", "v")
graph = app.query_one("#usage-history")
app.set_focus(graph)
await pilot.pause()
@@ -1114,8 +1107,9 @@ class TestBarGraphTUI:
"""Empty store shows awaiting message in graph."""
app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db")
async with app.run_test(size=(80, 24)) as pilot:
await pilot.press("v", "v")
graph = app.query_one("#usage-history")
assert isinstance(graph, DailyBarGraph)
assert isinstance(graph, HistoryGraph)
render = str(app.query_one("#bar-render").render())
assert "awaiting" in render.lower()
@@ -1124,8 +1118,9 @@ class TestBarGraphTUI:
"""Graph pane has a border title."""
app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db")
async with app.run_test(size=(80, 24)) as pilot:
await pilot.press("v", "v")
graph = app.query_one("#usage-history")
assert graph.border_title == "Usage history"
assert app.query_one("#activity-panel").border_title == "Drive activity"
@pytest.mark.asyncio
async def test_glyphs_in_legend(self, tmp_path):
@@ -1141,13 +1136,11 @@ class TestBarGraphTUI:
app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test(size=(80, 24)) as pilot:
await pilot.press("v", "v")
await pilot.pause()
legend = str(app.query_one("#bar-legend").render())
assert _GLYPH_ALLOCATED in legend
assert _GLYPH_UNALLOCATED in legend
assert _GLYPH_GAP in legend
assert _GLYPH_ZERO in legend
assert _GLYPH_PARTIAL in legend
for meaning in ("Writes", "Unalloc", "Gap", "Zero", "Partial"):
assert meaning in legend
# ---------------------------------------------------------------------------
@@ -1172,6 +1165,7 @@ class TestConstrainedLayout:
app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test(size=(80, 24)) as pilot:
await pilot.press("v", "v")
# All four regions should be visible
assert app.query_one("#headline-band") is not None
assert app.query_one("#usage-history") is not None
@@ -1180,8 +1174,8 @@ class TestConstrainedLayout:
# Graph should not be in constrained mode
assert not app._is_constrained_mode
# Constrained summary should be hidden
summary = app.query_one("#constrained-summary")
assert str(summary.styles.display) == "none"
summary = app.query_one("#bar-render")
assert "Graph needs" not in str(summary.render())
@pytest.mark.asyncio
async def test_constrained_below_80_width(self, tmp_path):
@@ -1198,13 +1192,14 @@ class TestConstrainedLayout:
app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test(size=(79, 24)) as pilot:
await pilot.press("v", "v")
# Should be in constrained mode
assert app._is_constrained_mode
# Main grid should have constrained class
main_grid = app.query_one("#main-grid")
assert main_grid.has_class("constrained")
# Constrained summary should be visible
summary = app.query_one("#constrained-summary")
summary = app.query_one("#bar-render")
summary_text = str(summary.render())
assert "Graph needs ≥80×24" in summary_text
assert "days" in summary_text
@@ -1225,10 +1220,11 @@ class TestConstrainedLayout:
app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test(size=(80, 23)) as pilot:
await pilot.press("v", "v")
# Should be in constrained mode
assert app._is_constrained_mode
# Constrained summary should be visible
summary = app.query_one("#constrained-summary")
summary = app.query_one("#bar-render")
summary_text = str(summary.render())
assert "Graph needs ≥80×24" in summary_text
@@ -1247,11 +1243,12 @@ class TestConstrainedLayout:
app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test(size=(79, 24)) as pilot:
await pilot.press("v", "v")
# Start in constrained mode
assert app._is_constrained_mode
graph = app.query_one("#usage-history")
# Select a day while constrained
graph.selected_index = 5
# Select a day through the visible graph while constrained.
await pilot.press(*(["left"] * 8))
# Resize to normal — pilot.resize_terminal changes terminal size
await pilot.resize_terminal(80, 24)
@@ -1285,6 +1282,7 @@ class TestConstrainedLayout:
app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test(size=(80, 24)) as pilot:
await pilot.press("v", "v")
# Start in normal mode
assert not app._is_constrained_mode
graph = app.query_one("#usage-history")
@@ -1303,7 +1301,7 @@ class TestConstrainedLayout:
# Should be in constrained mode
assert app._is_constrained_mode
# Constrained summary should show selected day context
summary = app.query_one("#constrained-summary")
summary = app.query_one("#bar-render")
summary_text = str(summary.render())
assert "Graph needs ≥80×24" in summary_text
# Selected day context should survive
@@ -1314,8 +1312,9 @@ class TestConstrainedLayout:
"""Constrained summary shows awaiting message for empty store."""
app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db")
async with app.run_test(size=(79, 24)) as pilot:
await pilot.press("v", "v")
assert app._is_constrained_mode
summary = app.query_one("#constrained-summary")
summary = app.query_one("#bar-render")
summary_text = str(summary.render())
assert "Graph needs ≥80×24" in summary_text
assert "awaiting" in summary_text.lower()
@@ -1335,24 +1334,25 @@ class TestConstrainedLayout:
app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test(size=(79, 24)) as pilot:
await pilot.press("v", "v")
# All regions should exist
assert app.query_one("#headline-band") is not None
assert app.query_one("#drive-health") is not None
assert app.query_one("#service-strip") is not None
assert app.query_one("#quit-rail") is not None
assert app.query_one("#action-rail") is not None
# Headline should contain projection
headline = str(app.query_one("#headline-band").render())
assert "remaining" in headline.lower() or "projection" in headline.lower()
# Service strip should have actions
strip = str(app.query_one("#service-strip").render())
strip = str(app.query_one("#action-rail").render())
assert "p Pause" in strip
assert "r Resume" in strip
assert "q quit" not in strip # Quit is in quit-rail
assert "q Quit TUI" in strip
# Quit rail should be visible
rail = str(app.query_one("#quit-rail").render())
rail = str(app.query_one("#action-rail").render())
assert "Quit" in rail
@pytest.mark.asyncio
@@ -1368,6 +1368,7 @@ class TestConstrainedLayout:
app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test(size=(79, 24)) as pilot:
await pilot.press("v", "v")
strip = str(app.query_one("#service-strip").render())
# Status should be visible even with long reasons
assert "stale" in strip.lower() or "freshness" in strip.lower()
@@ -1387,6 +1388,7 @@ class TestConstrainedLayout:
app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test(size=(79, 24)) as pilot:
await pilot.press("v", "v")
# Quit should still work
await pilot.press("q")
assert not app.is_running
@@ -1396,6 +1398,7 @@ class TestConstrainedLayout:
"""Theme and motion toggles work in constrained mode."""
app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db")
async with app.run_test(size=(79, 24)) as pilot:
await pilot.press("v", "v")
assert app._is_constrained_mode
# Toggle theme
await pilot.press("t")
@@ -1415,8 +1418,8 @@ class TestConstrainedLayout:
assert _MIN_HEIGHT == 24
def test_is_constrained_with_terminal_width(self, tmp_path):
"""DailyBarGraph._is_constrained checks terminal width."""
graph = DailyBarGraph()
"""HistoryGraph._is_constrained checks terminal width."""
graph = HistoryGraph()
# Widget-level check (no terminal width)
assert graph._is_constrained() is False
# Terminal width below minimum