refactor(tui): extract Activity readout and show local time

Move activity data assembly out of tui.py into activity_readout, behind
readout(conn, selection, now). Dashboard and fenris-monitor enable now show
local time with a timezone label, inspected points keep a secondary UTC line,
and the legacy UTC-relabelled day and hour branches are gone. Time formatting
(including freshness_age_human) lives in timefmt. Hour offsets now render as
+05:30 to match day labels.
This commit is contained in:
xavierk
2026-10-05 20:21:22 +05:30
parent ef61ececc1
commit 79ebb10dea
20 changed files with 1297 additions and 834 deletions
+794
View File
@@ -0,0 +1,794 @@
"""Activity readout: the ready-to-read account of an activity selection.
``readout(conn, selection, now)`` assembles everything the dashboard draws for
live intervals, local hours and local days from the observation store, with
every time expressed in the system's local timezone and labelled with it.
Stored evidence stays UTC; a secondary UTC line stays available when a point is
inspected. Nothing here imports Textual, so each rule is testable against a
SQLite fixture.
"""
from __future__ import annotations
import sqlite3
from dataclasses import dataclass
from datetime import date, datetime, timedelta, timezone
from typing import Any, Dict, List, Optional
from zoneinfo import ZoneInfo
from .activity_selection import (
HISTORY_RANGE_MAX,
ActivitySelection,
HistoryDayIdentity,
)
from .derive import _parse_ts
from .local_day import query_current_local_day, query_local_day_summary
from .status import CADENCE_DEFAULT_S
from .timefmt import (
local_clock,
local_zone_name,
tz_label,
utc_range_label,
utc_stamp_label,
)
LIVE_WINDOW_H = 3 # Rolling window for the live graph
LOCAL_ACTIVITY_STATE_LABELS = {
"so_far": "totals so far",
"incomplete": "incomplete",
"complete": "complete",
"zero": "measured zero",
"unavailable": "local activity unavailable",
}
@dataclass(frozen=True)
class ActivityReadout:
"""Everything the dashboard draws for one activity selection."""
view: str
timezone_name: str
timezone_label: str
local_date: str
days: tuple[Dict[str, Any], ...]
hours: tuple[Dict[str, Any], ...]
live: tuple[Dict[str, Any], ...]
health: Dict[str, Any]
local_day: Optional[Dict[str, Any]]
def readout(
conn: sqlite3.Connection,
selection: ActivitySelection,
now: datetime,
) -> ActivityReadout:
"""Assemble the readout for *selection* as of *now*."""
zone_name = local_zone_name()
local_date, local_day = selected_local_day(conn, selection, now, zone_name)
hours: List[Dict[str, Any]] = []
if selection.view == "day" and selection.browse_date is not None:
hours = _query_local_hourly_graph_data(conn, local_day, now)
return ActivityReadout(
view=selection.view,
timezone_name=zone_name,
timezone_label=tz_label(now, zone_name),
local_date=local_date,
days=tuple(_query_local_day_graph_data(
conn, now, zone_name, selected_date=selection.browse_date,
)),
hours=tuple(hours),
live=tuple(_query_live_graph_data(conn, now, zone_name)),
health=_query_drive_health(conn),
local_day=local_day,
)
def selected_local_day(
conn: sqlite3.Connection,
selection: ActivitySelection,
now: datetime,
zone_name: str | None = None,
) -> tuple[str, Optional[Dict[str, Any]]]:
"""Return the browsed (or current) local date and its stored summary."""
zone_name = zone_name or local_zone_name()
browse_date = selection.browse_date
local_date = browse_date or now.astimezone(ZoneInfo(zone_name)).date().isoformat()
try:
if browse_date is not None:
identity = selection.selected_history_day
summary = query_local_day_summary(
conn, browse_date,
identity.timezone if identity is not None else None, now,
)
else:
summary = query_current_local_day(conn, now, zone_name)
except Exception:
summary = None
if summary is not None:
summary = dict(summary, habit=_day_habit(conn, summary))
return local_date, summary
# ---------------------------------------------------------------------------
# Local-day evidence (ADR 0010)
# ---------------------------------------------------------------------------
def _query_local_day_graph_data(
conn: sqlite3.Connection,
now: datetime,
timezone_name: str | None = None,
days: int = HISTORY_RANGE_MAX,
selected_date: str | None = None,
) -> List[Dict[str, Any]]:
"""Read recorded local-day evidence, keeping unavailable dates visible."""
current_zone_name = timezone_name or local_zone_name()
current_zone = ZoneInfo(current_zone_name)
end_date = now.astimezone(current_zone).date()
start_date = end_date - timedelta(days=days - 1)
start_text, end_text = start_date.isoformat(), end_date.isoformat()
local_dates = {
(start_date + timedelta(days=offset)).isoformat()
for offset in range(days)
}
if selected_date is not None:
try:
date.fromisoformat(selected_date)
except ValueError:
selected_date = None
else:
local_dates.add(selected_date)
rows = conn.execute(
"SELECT local_date, tz_name FROM local_days "
"WHERE (local_date >= ? AND local_date <= ?) "
"OR local_date = ? ORDER BY local_date, id",
(start_text, end_text, selected_date or ""),
).fetchall()
recorded_zones: Dict[str, list[str]] = {}
for local_date, recorded_zone in rows:
recorded_zones.setdefault(local_date, [])
if recorded_zone not in recorded_zones[local_date]:
recorded_zones[local_date].append(recorded_zone)
result: List[Dict[str, Any]] = []
for local_date in sorted(local_dates):
zones = recorded_zones.get(local_date)
if not zones:
result.append({
"day": local_date,
"local_label": local_date,
"timezone_label": "",
"utc_label": "",
"identity": None,
"total_bytes": None,
"total_written": None,
"total_read": None,
"shared_bytes_written": 0,
"shared_bytes_read": 0,
"unallocated_bytes_written": 0,
"unallocated_bytes_read": 0,
"activity_state": "unavailable",
"is_zero": False,
"is_gap": True,
"is_partial": False,
})
continue
for recorded_zone in zones:
summary = query_local_day_summary(conn, local_date, recorded_zone, now)
if summary is None:
continue
identity = HistoryDayIdentity(
local_date,
summary["tz_name"],
summary["utc_start"],
summary["utc_end"],
)
written = summary["bytes_written"]
read = summary["bytes_read"]
state = summary["activity_state"]
result.append({
"day": local_date,
"local_label": local_date,
"timezone_label": "%s %s" % (
summary["tz_name"], summary["tz_offset"],
),
"utc_label": "%s → %s" % (
_parse_ts(summary["utc_start"]).strftime("%Y-%m-%d %H:%M"),
utc_stamp_label(_parse_ts(summary["utc_end"])),
),
"identity": identity,
"total_bytes": written,
"total_written": written,
"total_read": read,
"shared_bytes_written": summary["shared_bytes_written"],
"shared_bytes_read": summary["shared_bytes_read"],
"unallocated_bytes_written": summary["unallocated_bytes_written"],
"unallocated_bytes_read": summary["unallocated_bytes_read"],
"activity_state": state,
"is_zero": state == "zero",
"is_gap": written is None and read is None,
"is_partial": state in ("incomplete", "so_far"),
})
return result
def _query_local_hourly_graph_data(
conn: sqlite3.Connection,
summary: Dict[str, Any] | None,
now: datetime,
) -> List[Dict[str, Any]]:
"""Read complete UTC hour buckets inside recorded local-day boundaries."""
if summary is None:
return []
zone_name = summary["tz_name"]
day_start = _parse_ts(summary["utc_start"]).astimezone(timezone.utc)
day_end = _parse_ts(summary["utc_end"]).astimezone(timezone.utc)
current = now.astimezone(timezone.utc)
first_hour = day_start.replace(minute=0, second=0, microsecond=0)
if first_hour < day_start:
first_hour += timedelta(hours=1)
last_hour = day_end - timedelta(hours=1)
rows = conn.execute(
"SELECT hour, bytes_written_delta, bytes_read_delta, coverage, "
"sample_count, unknown_seconds, active_seconds, idle_seconds, "
"powered_off_seconds, temperature_min, temperature_max "
"FROM hour_observations WHERE hour >= ? AND hour <= ? ORDER BY hour",
(first_hour.isoformat(), last_hour.isoformat()),
).fetchall()
by_hour = {
_parse_ts(row[0]).astimezone(timezone.utc): row
for row in rows
}
result: List[Dict[str, Any]] = []
hour_start = first_hour
while hour_start + timedelta(hours=1) <= day_end:
hour_end = hour_start + timedelta(hours=1)
row = by_hour.get(hour_start)
is_future = hour_start > current
identity = hour_start.isoformat()
active = idle = off = 0
temp_min = temp_max = None
if row is None:
written = read = coverage = sample_count = 0
unknown_seconds = 0
is_gap = not is_future
else:
written = row[1] or 0
read = row[2] or 0
coverage = row[3] or 0.0
sample_count = row[4] or 0
unknown_seconds = row[5] or 0
active, idle, off = row[6] or 0, row[7] or 0, row[8] or 0
temp_min, temp_max = row[9], row[10]
is_gap = sample_count == 0 and unknown_seconds > 0
result.append({
"hour": identity,
"local_label": local_clock(hour_start, zone_name),
"local_end_label": local_clock(hour_end, zone_name),
"timezone_label": tz_label(hour_start, zone_name),
"utc_label": utc_stamp_label(hour_start),
"bytes_written": written,
"bytes_read": read,
"coverage": coverage,
"sample_count": sample_count,
"active_seconds": active,
"idle_seconds": idle,
"powered_off_seconds": off,
"unknown_seconds": unknown_seconds,
"temperature_min": temp_min,
"temperature_max": temp_max,
"is_zero": written == 0 and read == 0 and not is_gap,
"is_gap": is_gap,
"is_partial": (
hour_start <= current < hour_end
or coverage < 1.0
or unknown_seconds > 0
),
"is_future": is_future,
})
hour_start = hour_end
return result
def _day_habit(conn: sqlite3.Connection, summary: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Usage-habit split and thermal range of the complete hours in a local day."""
day_start = _parse_ts(summary["utc_start"]).astimezone(timezone.utc)
day_end = _parse_ts(summary["utc_end"]).astimezone(timezone.utc)
first_hour = day_start.replace(minute=0, second=0, microsecond=0)
if first_hour < day_start:
first_hour += timedelta(hours=1)
row = conn.execute(
"SELECT COUNT(*), COALESCE(SUM(active_seconds), 0), "
"COALESCE(SUM(idle_seconds), 0), COALESCE(SUM(powered_off_seconds), 0), "
"COALESCE(SUM(unknown_seconds), 0), MIN(temperature_min), "
"MAX(temperature_max) FROM hour_observations "
"WHERE hour >= ? AND hour <= ?",
(first_hour.isoformat(), (day_end - timedelta(hours=1)).isoformat()),
).fetchone()
if not row or not row[0]:
return None
return {
"active_seconds": row[1],
"idle_seconds": row[2],
"powered_off_seconds": row[3],
"unknown_seconds": row[4],
"temperature_min": row[5],
"temperature_max": row[6],
}
# ---------------------------------------------------------------------------
# UTC aggregates (store-level evidence; not shown as local days)
# ---------------------------------------------------------------------------
def _query_daily_graph_data(
conn: sqlite3.Connection,
window_end: Optional[datetime] = None,
) -> List[Dict[str, Any]]:
"""Query UTC day aggregates, with unallocated write evidence kept apart.
Returns one dict per UTC day with total/allocated/unallocated bytes,
coverage, evidence hours, and classification flags. The dashboard never
draws these as local days; local-day evidence is the only daily source.
"""
cursor = conn.execute(
"SELECT day, bytes_written_delta, unattributed_bytes_written, "
"coverage, sample_count, active_seconds, idle_seconds, "
"powered_off_seconds, unknown_seconds, "
"bytes_read_delta, unattributed_bytes_read "
"FROM day_aggregates ORDER BY day"
)
rows = cursor.fetchall()
by_day: Dict[str, Dict[str, Any]] = {}
for row in rows:
day = row[0]
bw_delta = row[1] or 0
unattributed_w = row[2] or 0
coverage = row[3] or 0.0
sample_count = row[4] or 0
active = row[5] or 0
idle = row[6] or 0
powered_off = row[7] or 0
unknown = row[8] or 0
br_delta = row[9] or 0
unattributed_r = row[10] or 0
total_written = bw_delta + unattributed_w
total_read = br_delta + unattributed_r
evidenced_hours = (active + idle + powered_off) // 3600
is_zero = total_written == 0 and total_read == 0
is_gap = (
sample_count == 0
and (active + idle + powered_off) == 0
and unknown > 0
)
is_partial = coverage < 0.5
by_day[day] = {
"day": day,
"local_label": day,
"total_bytes": total_written,
"total_written": total_written,
"total_read": total_read,
"allocated_bytes": bw_delta,
"unallocated_bytes": unattributed_w,
"allocated_read": br_delta,
"unallocated_read": unattributed_r,
"coverage": coverage,
"evidenced_hours": evidenced_hours,
"sample_count": sample_count,
"is_zero": is_zero,
"is_gap": is_gap,
"is_partial": is_partial,
}
if window_end is None:
return list(by_day.values())
end_date = window_end.astimezone(timezone.utc).date()
result: List[Dict[str, Any]] = []
for offset in range(HISTORY_RANGE_MAX - 1, -1, -1):
day = (end_date - timedelta(days=offset)).isoformat()
entry = by_day.get(day, {
"day": day,
"local_label": day,
"total_bytes": 0,
"total_written": 0,
"total_read": 0,
"allocated_bytes": 0,
"unallocated_bytes": 0,
"allocated_read": 0,
"unallocated_read": 0,
"coverage": 0.0,
"evidenced_hours": 0,
"sample_count": 0,
"is_zero": False,
"is_gap": True,
"is_partial": False,
})
if day == end_date.isoformat():
entry = dict(entry)
entry["is_partial"] = True
result.append(entry)
return result
def _query_hourly_graph_data(
conn: sqlite3.Connection,
day: str,
now: Optional[datetime] = None,
) -> List[Dict[str, Any]]:
"""Query hour observations for one UTC day.
Returns one dict per hour with bytes written/read, coverage, and flags.
"""
cursor = conn.execute(
"SELECT hour, bytes_written_delta, bytes_read_delta, coverage, sample_count, "
"active_seconds, idle_seconds, powered_off_seconds, unknown_seconds "
"FROM hour_observations "
"WHERE hour LIKE ? ORDER BY hour",
(day + "T%",),
)
rows = cursor.fetchall()
by_hour: Dict[int, Dict[str, Any]] = {}
for row in rows:
hour = row[0]
bw = row[1] or 0
br = row[2] or 0
coverage = row[3] or 0.0
sample_count = row[4] or 0
active = row[5] or 0
idle = row[6] or 0
powered_off = row[7] or 0
unknown = row[8] or 0
is_zero = bw == 0 and br == 0
local_label = hour[11:13] if len(hour) >= 13 else hour
hour_number = int(local_label)
by_hour[hour_number] = {
"hour": hour,
"local_label": local_label,
"bytes_written": bw,
"bytes_read": br,
"coverage": coverage,
"sample_count": sample_count,
"active_seconds": active,
"idle_seconds": idle,
"powered_off_seconds": powered_off,
"unknown_seconds": unknown,
"is_zero": is_zero,
"is_gap": False,
"is_partial": False,
"is_future": False,
}
current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
selected_date = datetime.fromisoformat(day).date()
result: List[Dict[str, Any]] = []
for hour_number in range(24):
hour_start = datetime.combine(
selected_date, datetime.min.time(), tzinfo=timezone.utc,
) + timedelta(hours=hour_number)
is_future = hour_start > current
entry = by_hour.get(hour_number)
if entry is None:
entry = {
"hour": hour_start.isoformat(),
"local_label": "%02d" % hour_number,
"bytes_written": 0,
"bytes_read": 0,
"coverage": 0.0,
"sample_count": 0,
"active_seconds": 0,
"idle_seconds": 0,
"powered_off_seconds": 0,
"unknown_seconds": 0,
"is_zero": False,
"is_gap": not is_future,
"is_partial": hour_start <= current < hour_start + timedelta(hours=1),
"is_future": is_future,
}
else:
entry["is_partial"] = hour_start <= current < hour_start + timedelta(hours=1)
entry["is_future"] = is_future
result.append(entry)
return result
# ---------------------------------------------------------------------------
# Live intervals (issue #91)
# ---------------------------------------------------------------------------
def _query_live_graph_data(
conn: sqlite3.Connection,
now: datetime,
zone_name: str | None = None,
) -> List[Dict[str, Any]]:
"""Query the last three hours of measured interval volumes from raw samples.
Each point represents the byte delta between two compatible consecutive
readings in the same controller segment. Points use actual elapsed
timestamps rather than pretending delayed runs are exact samples; gaps
and first-reading anchors are visually distinct. Labels are local, with
the timezone and a secondary UTC form alongside.
Returns one dict per interval, ordered oldest-first.
"""
zone_name = zone_name or local_zone_name()
window_start = (now - timedelta(hours=LIVE_WINDOW_H)).isoformat()
cursor = conn.execute(
"SELECT ts, bytes_written, bytes_read, segment_id, "
"temperature_c, critical_warning "
"FROM samples WHERE ts >= ? ORDER BY ts",
(window_start,),
)
rows = cursor.fetchall()
if len(rows) < 2:
return []
results: List[Dict[str, Any]] = []
for i in range(1, len(rows)):
prev_ts = _parse_ts(rows[i - 1][0])
curr_ts = _parse_ts(rows[i][0])
prev_bw = rows[i - 1][1] or 0
prev_br = rows[i - 1][2] or 0
curr_bw = rows[i][1] or 0
curr_br = rows[i][2] or 0
prev_seg = rows[i - 1][3]
curr_seg = rows[i][3]
elapsed_s = int((curr_ts - prev_ts).total_seconds())
is_gap = elapsed_s > (CADENCE_DEFAULT_S * 3) # more than 3× expected cadence
is_segment_boundary = prev_seg != curr_seg
bw_delta = max(0, curr_bw - prev_bw) if not is_segment_boundary else 0
br_delta = max(0, curr_br - prev_br) if not is_segment_boundary else 0
is_zero = bw_delta == 0 and br_delta == 0
results.append({
"start_ts": rows[i - 1][0],
"end_ts": rows[i][0],
"start_label": local_clock(prev_ts, zone_name),
"end_label": local_clock(curr_ts, zone_name),
"timezone_label": tz_label(curr_ts, zone_name),
"utc_label": utc_range_label(prev_ts, curr_ts),
"bytes_written": bw_delta,
"bytes_read": br_delta,
"elapsed_s": elapsed_s,
"temperature_c": rows[i][4],
"critical_warning": rows[i][5] or 0,
"is_gap": is_gap,
"is_zero": is_zero,
"is_segment_boundary": is_segment_boundary,
})
return results
# ---------------------------------------------------------------------------
# Drive health
# ---------------------------------------------------------------------------
def _query_drive_health(conn: sqlite3.Connection) -> Dict[str, Any]:
"""Query drive health data for the right pane."""
cursor = conn.execute(
"SELECT mn, sn, fr, temperature_c, available_spare, media_errors, "
"power_on_hours, power_cycles, unsafe_shutdowns, capacity_bytes, "
"percentage_used, data_units_written "
"FROM samples ORDER BY id DESC LIMIT 1"
)
row = cursor.fetchone()
if row is None:
return {
"model": "unknown",
"temp": 0,
"spare": 0,
"media_errors": 0,
"poh": 0,
"cycles": 0,
"unsafe_shutdowns": 0,
"capacity": "unknown",
"percentage_used": 0,
"written_tb": 0,
}
capacity = row[9]
capacity_str = "%d GB" % (capacity / 1e9) if capacity else "unknown"
written_tb = (row[11] * 512 * 1000) / 1e12 if row[11] else 0 # DUW to TB
return {
"model": row[0] or "unknown",
"temp": row[3] or 0,
"spare": row[4] or 0,
"media_errors": row[5] or 0,
"poh": row[6] or 0,
"cycles": row[7] or 0,
"unsafe_shutdowns": row[8] or 0,
"capacity": capacity_str,
"percentage_used": row[10] or 0,
"written_tb": written_tb,
}
# ---------------------------------------------------------------------------
# Readout text
# ---------------------------------------------------------------------------
def _gb(value: int | None) -> str:
return "%.3f GB" % ((value or 0) / 1e9)
def _duration(seconds: int) -> str:
if seconds < 3600:
return "%dm" % (seconds // 60)
return "%dh %02dm" % (seconds // 3600, (seconds % 3600) // 60)
def _habit_line(
active: int, idle: int, off: int, unknown: int,
temp_min: int | None, temp_max: int | None,
) -> str:
line = "active %s · idle %s · off %s · unknown %s" % (
_duration(active), _duration(idle), _duration(off), _duration(unknown),
)
if temp_min is not None and temp_max is not None:
line += " · %d–%d°C" % (temp_min, temp_max)
return line
def _evidence_lines(
shared_written: int, shared_read: int,
unallocated_written: int, unallocated_read: int,
) -> List[str]:
lines = []
if shared_written or shared_read:
lines.append("shared at midnight W %s · R %s" % (
_gb(shared_written), _gb(shared_read)))
if unallocated_written or unallocated_read:
lines.append("unallocated W %s · R %s" % (
_gb(unallocated_written), _gb(unallocated_read)))
return lines
def interval_readout_text(point: Dict[str, Any]) -> str:
"""Readout for one live interval: local time, volumes, thermal, warning, UTC."""
span = "%s → %s %s" % (
point.get("start_label", ""), point.get("end_label", ""),
point.get("timezone_label", ""),
)
utc = point.get("utc_label", "")
if point.get("is_segment_boundary"):
lines = [span + " · segment boundary", "W unavailable · R unavailable"]
else:
state_parts = []
if point.get("is_gap"):
state_parts.append("gap")
if point.get("is_zero") and not point.get("is_gap"):
state_parts.append("measured zero")
state = " · " + ", ".join(state_parts) if state_parts else ""
lines = ["[bold]%s[/bold] · W %s · R %s · %ds%s" % (
span, _gb(point.get("bytes_written")), _gb(point.get("bytes_read")),
point.get("elapsed_s", 0), state,
)]
extras = []
if point.get("temperature_c") is not None:
extras.append("%d°C" % point["temperature_c"])
if point.get("critical_warning"):
extras.append("drive critical warning 0x%02x" % point["critical_warning"])
if extras:
lines.append(" · ".join(extras))
if utc:
lines.append(utc)
return "\n".join(lines)
def hour_readout_text(
hour: Dict[str, Any],
local_date: str = "",
day: Dict[str, Any] | None = None,
) -> str:
"""Readout for one local hour, with day-level unallocated/shared evidence."""
day = day or {}
head = "%s %s" % (hour.get("local_label", ""), hour.get("timezone_label", ""))
notes = _evidence_lines(
day.get("shared_bytes_written", 0), day.get("shared_bytes_read", 0),
day.get("unallocated_bytes_written", 0), day.get("unallocated_bytes_read", 0),
)
if hour.get("is_gap") or hour.get("is_future"):
state = "future" if hour.get("is_future") else "gap"
lines = ["%s · %s" % (head, state), "W unavailable · R unavailable"]
lines += notes
else:
state = "incomplete" if hour.get("is_partial") else "measured"
lines = [
"[bold]%s[/bold] · %s" % (head, local_date),
"W %s · R %s · %d%% coverage · %s" % (
_gb(hour.get("bytes_written")), _gb(hour.get("bytes_read")),
hour.get("coverage", 0) * 100, state,
),
]
lines += notes
lines.append(_habit_line(
hour.get("active_seconds", 0), hour.get("idle_seconds", 0),
hour.get("powered_off_seconds", 0), hour.get("unknown_seconds", 0),
hour.get("temperature_min"), hour.get("temperature_max"),
))
if hour.get("utc_label"):
lines.append(hour["utc_label"])
return "\n".join(lines)
def day_readout_text(day: Dict[str, Any]) -> str:
"""Readout for one local day in the history view."""
evidence = _evidence_lines(
day.get("shared_bytes_written", 0), day.get("shared_bytes_read", 0),
day.get("unallocated_bytes_written", 0),
day.get("unallocated_bytes_read", 0),
)
if day.get("is_gap") and not evidence:
return "%s · local-day evidence unavailable" % day.get("day", "")
written = day.get("total_written")
read = day.get("total_read")
lines = [
"[bold]%s[/bold] · %s · %s" % (
day.get("day", ""), day.get("timezone_label", ""),
LOCAL_ACTIVITY_STATE_LABELS.get(
day.get("activity_state"), "local activity unavailable",
),
),
"W %s · R %s" % (
"unavailable" if written is None else _gb(written) + " known",
"unavailable" if read is None else _gb(read) + " known",
),
]
lines += evidence
if day.get("utc_label"):
lines.append(day["utc_label"])
return "\n".join(lines)
def local_day_text(local_date: str, local: Dict[str, Any] | None) -> str:
"""Text of the local-day totals strip for the browsed or current day."""
if local is None:
return (
"%s · local-day evidence unavailable\nW unavailable · R unavailable"
% local_date
)
written, read = local["bytes_written"], local["bytes_read"]
lines = [
"[bold]%s[/bold] · %s %s · %s" % (
local["local_date"], local["tz_name"], local["tz_offset"],
LOCAL_ACTIVITY_STATE_LABELS.get(
local["activity_state"], "local activity unavailable",
),
),
"W unavailable · R unavailable" if written is None or read is None
else "W %s known · R %s known" % (_gb(written), _gb(read)),
]
if local["shared_evidence_count"]:
lines.append("shared at midnight W %s · R %s" % (
_gb(local["shared_bytes_written"]), _gb(local["shared_bytes_read"])))
if local["unallocated_evidence_count"]:
lines.append("unallocated W %s · R %s" % (
_gb(local["unallocated_bytes_written"]),
_gb(local["unallocated_bytes_read"])))
habit = local.get("habit")
if habit:
lines.append(_habit_line(
habit["active_seconds"], habit["idle_seconds"],
habit["powered_off_seconds"], habit["unknown_seconds"],
habit["temperature_min"], habit["temperature_max"],
))
return "\n".join(lines)