# Session: Continuous monitoring mode

Date: 2026-03-21

## What we built

Added `--watch` / `-w` flag for continuous monitoring mode. Runs checks on a configurable interval (default 30s), displays a live dashboard, and notifies on problems via terminal bell + OSC 9 desktop notifications.

Also unified the one-shot and monitoring output paths so both modes share the same rendering logic.

## Key decisions

### No TUI toolkit

Decided against adding a terminal UI library (e.g., rich, textual). The dashboard is a single screen redrawn periodically — `\033[2J\033[H` (clear + home) plus `print()` is sufficient. This preserves the zero-runtime-dependency constraint. If we ever need interactive controls or complex layouts, we can revisit.

### Structured error model: severity + kind, no category enum

Initially considered a `Category` enum (LOCAL, ISP, INTERNET, CONNECTIVITY) alongside `Severity`. Decided against it because:
- As we add more checks (DNS, wifi signal, bufferbloat, etc.), a fixed category enum would need constant expansion
- The "where" context is better captured in the description string, which is already descriptive
- Severity is what drives behavior (notifications, healthy/unhealthy state)

Each issue has a `kind` string (e.g., `"gateway-latency"`, `"isp-loss"`) for grouping, and `description` + `value` for display. The `message` property combines them: `"LAN: high latency to gateway (71 ms) — wifi congestion"`.

### Issue model: description + value, not a single message string

Split the human-readable message into `description` (stable across occurrences) and `value` (the specific measurement). This solved two problems:
- In monitoring mode, issues need to be grouped by kind — identical descriptions group naturally
- One-shot mode wants specific values, monitoring history wants the description without values cluttering the grouping

The `message` property reconstructs the full string by inserting the value before the em dash.

### Three monitoring states: stable / unstable / problem

Replaced the original OK/PROBLEM binary with three states:
- **Stable**: every check has been healthy, nothing to report
- **Problem**: current check has issues
- **Unstable**: current check is healthy, but problems were seen recently — the connection is flaky

This better matches the user's mental model. "It's fine now but it was bad 2 minutes ago" is meaningfully different from "it's always been fine."

### Persistent issue history

The dashboard shows every issue kind ever seen (within the rolling buffer), with count, last value, and timing. Active issues show `!!` prefix and "ongoing"; resolved ones show `  ` prefix and "Xm ago". This means transitioning from unstable back to problem doesn't lose the history of past issues.

### Notifications only on transition

Terminal bell + OSC 9 fire only on healthy-to-problem transitions. Repeating the bell every check cycle during a persistent problem would be unusable.

### Consistent output between modes

Both modes now use the same rendering path:
- Check modules (`gateway`, `internet`) own their `summary()` function for one-line status
- `_render_probes()` formats the summary lines
- Issue display uses the same formatting
- Status line appears in both modes
- Startup message in both modes ("checking..." / "running initial check...")

This eliminated `diagnose()`, `output.py`, and the `quiet` parameter on check modules. Checks are now pure data — they run probes and return results, with no print side effects.

### Labels: LAN and ISP/internet

Settled on "LAN:" and "ISP/internet:" as consistent labels across summary lines and issue descriptions. Initially had "Local network:" in issues vs "LAN:" in summaries vs "Internet:" in some places. Unified everything.

### Duration formatting

Two formatters: `_format_duration` for countdowns and session summaries (e.g., "2m 05s"), and `_format_ago` for issue history (seconds when under 1 minute, just minutes after — no one cares that an error was "2m 37s ago").

## Architectural direction

### Toward stateful check modules

The current check modules are stateless functions. The user expressed interest in making them stateful for monitoring mode — each check could:
- Decide its own run frequency (`should_run()`)
- Accumulate data across cycles
- Produce smarter summaries based on history

This would let future checks (DNS timing, wifi signal) run on different schedules and build up richer analysis without the monitor needing to understand each check's internals.

### Documentation as we go

Created `docs/metrics.md` covering RTT, packet loss, and jitter — what they are, how we measure them, and thresholds. Added a guideline to CLAUDE.md to update docs when changing metrics or diagnostic logic.

### Network condition simulation (TODO)

Noted the need for simulating network conditions in tests (packet loss, high latency, jitter) — possibly via Docker containers with `tc`/`netem`, network namespaces, or lightweight VMs. Currently all tests use mocking.

## Files changed

- `yslow/models.py` — added `Severity`, `Issue` (with kind/description/value/message), `CheckResult`
- `yslow/diagnosis.py` — refactored to return `list[Issue]`, removed `diagnose()`
- `yslow/monitor.py` — new: monitoring loop, state tracking, dashboard rendering, notifications
- `yslow/checks/gateway.py` — simplified to pure data, added `summary()`
- `yslow/checks/internet.py` — simplified to pure data, added `summary()`
- `yslow/__init__.py` — added argparse, unified one-shot and watch mode
- `yslow/output.py` — removed (dead code after consolidation)
- `docs/metrics.md` — new: measurement documentation
- `tests/test_diagnosis.py` — updated for `Issue` return type
- `tests/test_monitor.py` — new: monitor state, notification, and render tests
