feat(#73): publish trustworthy first usage history

- 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
This commit is contained in:
xavierk
2026-09-14 03:19:08 +05:30
parent f406285a0f
commit d790ff84c5
7 changed files with 1074 additions and 26 deletions
+24 -2
View File
@@ -331,7 +331,8 @@ def check_retired_flag(flag: str) -> Optional[str]:
def _format_projection(proj, freshness: str, service: Dict[str, Any],
drive_facts: List[str], config_error: Optional[str],
store_fault: Optional[str], newer_schema: Optional[str],
journal_hint: Optional[str]) -> str:
journal_hint: Optional[str],
sample_count: int = 0, day_count: int = 0) -> str:
"""Format the complete status output."""
lines = []
@@ -361,6 +362,16 @@ def _format_projection(proj, freshness: str, service: Dict[str, Any],
_append_service_facts(lines, service)
return "\n".join(lines)
# --- Single sample: awaiting another sample (issue #73 AC3) ---
# Only show awaiting state when there are no day aggregates (e.g., legacy import
# or hand-crafted stores can have 1 sample but sufficient day data for projection)
if sample_count <= 1 and day_count == 0:
lines.append("awaiting another sample")
lines.append("")
lines.append("Collecting usage data — the first projection requires at least two samples.")
_append_service_facts(lines, service)
return "\n".join(lines)
# --- Projection headline ---
headline = _format_headline(proj)
lines.append(headline)
@@ -598,6 +609,17 @@ def get_status(store_path: Optional[Path] = None, clock_now: Optional[datetime]
freshness = grade_freshness(newest_ts, clock_now)
# --- Sample count for single-sample state (issue #73 AC3) ---
sample_count = 0
day_count = 0
try:
cursor = conn.execute("SELECT COUNT(*) FROM samples")
sample_count = cursor.fetchone()[0]
cursor = conn.execute("SELECT COUNT(*) FROM day_aggregates")
day_count = cursor.fetchone()[0]
except sqlite3.Error:
pass
# Freshness age for the service fact
freshness_age_s = None
if newest_ts:
@@ -637,7 +659,7 @@ def get_status(store_path: Optional[Path] = None, clock_now: Optional[datetime]
# --- Compose output ---
result = _format_projection(
proj, freshness, service, drive_facts, config_error,
None, None, journal_hint,
None, None, journal_hint, sample_count, day_count,
)
conn.close()