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
+40 -5
View File
@@ -16,6 +16,8 @@ from pathlib import Path
from typing import Any, Dict, Optional, Tuple
from .store import init_store, get_store_path
from .monitoring_periods import ensure_period_open
from .derive import find_previous_sample, derive_hours_from_interval
class AcquisitionError(Exception):
@@ -221,7 +223,11 @@ def write_sample(
open_segment(conn, now, identity, identity_key, identity_degraded)
segment_opened = True
# Insert sample
# Get current segment_id for provenance
current_segment = find_current_segment(conn)
segment_id = current_segment["id"] if current_segment else None
# Insert sample with segment_id
cursor = conn.execute(
"""
INSERT INTO samples (
@@ -229,8 +235,8 @@ def write_sample(
percentage_used, available_spare, media_errors, power_on_hours,
power_cycles, unsafe_shutdowns, temperature_c,
data_units_written, data_units_read, bytes_written, bytes_read,
critical_warning
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
critical_warning, segment_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
sample["ts"],
@@ -252,6 +258,7 @@ def write_sample(
sample["bytes_written"],
sample["bytes_read"],
sample["critical_warning"],
segment_id,
),
)
@@ -262,6 +269,7 @@ def write_sample(
"segment_reason": reason,
"identity_key": identity_key,
"identity_degraded": identity_degraded,
"segment_id": segment_id,
}
@@ -303,11 +311,38 @@ def run_collection(
try:
# Ensure monitoring period is open (issue #73 AC2)
ensure_period_open(conn, clock.utcnow())
# Validate invariants
validate_sample_invariants(sample, conn)
# Write sample
write_sample(sample, identity, conn, clock)
# Write sample and get segment info
seg_info = write_sample(sample, identity, conn, clock)
# Derive hour observations from interval with previous sample
try:
# Find the sample we just wrote
cursor = conn.execute("SELECT id FROM samples ORDER BY id DESC LIMIT 1")
current_id = cursor.fetchone()[0]
prev = find_previous_sample(conn, seg_info.get("segment_id"), current_id)
if prev is not None:
# Build current sample dict for derivation
current = {
"id": current_id,
"ts": sample["ts"],
"bytes_written": sample["bytes_written"],
"bytes_read": sample["bytes_read"],
"power_on_hours": sample["power_on_hours"],
"temperature_c": sample["temperature_c"],
"data_units_written": sample["data_units_written"],
"data_units_read": sample["data_units_read"],
}
derive_hours_from_interval(conn, prev, current)
except Exception:
# Derivation failure must not prevent sample persistence (issue #73 AC6)
pass
return {
"ok": True,