Skip to content
Design notes Fictional demonstration data Review launcher

Checkpoint 0.1.10 · complete-design review · not final · source-only · browser review pending

MainsKeeper — complete design notes

What was built, where it came from, the behaviour that isn’t obvious from the screens, the decisions, the assumptions and the known issues. Start with the complete design review. Round 6 comes first, then round 5; the round-4 notes follow, corrected where later rounds changed them.

Nothing in rounds 4, 5 or 6 has been rendered or visually inspected, and there are no screenshots. You asked for render inspection and screenshots. Rendering is not permitted in this environment: pages were written from source, and opening, previewing or screenshotting them was not allowed. The review page lists an unverified visual checklist and the screenshot routes to capture. Contrast was calculated from token values, not measured on screen. Layout at 375 / 768 / 1024 / 1440 px is written into the CSS but has not been looked at. The first review pass should be visual.

Round 9 · checkpoint 0.1.10 — exact monitoring-gap intervals (source-only checkpoint)

Fixed: in 0.1.9 the series and aggregate found monitoring gaps on the 1-minute sample grid, then snapped each one to the first recorded stop it overlapped. A 30 s gap (disable, check in, re-enable, check in: 15:22:27–15:22:57) contained no sample, so the series and aggregate had no gap, the chart joined across it and availability said “No unknown time”. Two gaps 15 s apart (15:22:27–15:28:27, 15:28:42–15:34:12) produced a single sampled run and the second gap was lost. Now fixture.js takes the persisted intervals (monitorStops / monitorResumes → u.outages plus an open gap while disabled) as authoritative. Every gap goes into the series, power lanes, history() and aggregate unknown spans with exact boundaries. Chart segments split at each boundary, points inside a gap are dropped, and the observed interval between gaps keeps no invented points. Availability and hourly-bucket coverage come from exact intersections of the power-state intervals with the selected range (stateMinutes), in fractional minutes. Only the text is rounded (fmt.duration, new fmt.minutes), and a non-zero gap never reads “No unknown time”.

Round 8 · checkpoint 0.1.9 — persistent monitoring gaps (source-only checkpoint)

Fixed: every UPS-role-off window now keeps its own gap with stable boundaries — the last observation before monitoring stopped (recorded when the role-off applies at check-in, monitorStops) and the first observation after it resumed (recorded by the first reading after re-enable, monitorResumes). Before, store.js overwrote readingsAt on every reading and fixture.js kept only the latest window (.pop()), so a later reading stretched the gap (15:22:27–15:27:57 became …15:33:27) and a second cycle erased the first gap. Series, power lanes, aggregates (snapped to the first overlapping stop in 0.1.9 — superseded by exact intervals in 0.1.10, see round 9), history unknown spans and availability all use the same gap list, which is stored in the browser and survives reloads.

Round 7 · checkpoint 0.1.8 — source-only correction pass

Fixes to shared logic and copy only; the visual direction, feature scope and concepts 1–9 are unchanged. Browser QA was deferred to a separate browser-capable environment and does not block this checkpoint, but it does block acceptance. Routes and expected outcomes: review · 0.1.8 corrections.

Fixes

Witness-down coverage, Office Desk UPS (computed with MK.select.availability / aggregate)

What was exercised (source / Node checks — not browser verification)

Remaining limitations

Round 6 · checkpoint 0.1.7

This was a correction round. Visual direction and feature scope are frozen. The corrections live in shared logic (product/app/assets/, CONTRACT.md §18), and pages, onboarding and the CLI were migrated onto it. Pages no longer compute duplicates, ranges, unknown spans, freshness thresholds or platform strings themselves. Routes, steps and expected outcomes for every case are on the review page: 0.1.7 corrections.

Decisions

Network UPS identity
Identity = namespace | protocol | host | port | NUT UPS name (SNMP has no UPS name). The namespace is the collector’s override, else the site’s (broadwater-lan, home-lan, shop-lan, duplex-lan; created sites <id>-lan, e.g. cabin-lan). Loopback hosts are per machine. A host alone is never an identity. Hard duplicate (same identity, monitored or draft) is blocked: “Already monitored by bw-pi-01 as BW Office UPS”. Soft duplicate (a trustworthy serial matching another UPS at a different identity, same vendor and base model) is a question, never a block: “Possibly the same UPS as BW Rack UPS (serial match) — confirm”. Untrustworthy serials (empty, placeholders, one repeated character such as 000000000000, sequential runs, under 6 alphanumerics, equal to the model, or unknown vendor/model) decide nothing.
Collector availability and UPS-role gating
Enforced in the store, so buttons cannot bypass them. Saving drafts and disabled sources is always allowed. Testing needs a reporting collector; otherwise the test is queued (“Test queued — collector not reporting”) and runs at the next check-in. It is never shown as success or failure. Monitoring needs a reporting collector and an applied UPS role. Otherwise the choice is saved as “Configured — waiting for collector” (which takes precedence) or “Configured — waiting for UPS role”, and starts at the check-in where both hold. Readings start no earlier than the reconnect or role-application time. Review controls (Simulate collector reconnect / check-in / disconnect) stand in for the device. Each reconnect or first reading advances the demo clock 30 s.
History range semantics
A point is included if from ≤ observed time ≤ to. An interval is included if start ≤ to and end ≥ from, where end = the end time, else the last observed record, else now only while ongoing, else the start. There is no implicit “until now”. Replayed records sit at observation time, and receipt events at receive time. So 15:00–15:22 excludes yesterday’s Duplex episode (INC-0409), Home 06:42 and the Duplex 14:03–14:30 episode.
Unknown is data
Missing-evidence states (“No records — collector not reporting”, “No records”, “History arriving”, “Not monitored yet”, “Not reported yet — next report due by …”) travel through history, lanes, series, hourly aggregates, coverage and availability as unknown. They are never filled, joined or counted as up or down. Witness-down: nothing from home-studio / Office Desk UPS after 15:04:12. Site reachability (home-esp32-garage) stays separate and never stands in for Office Desk UPS power.
Freshness by cadence
I = the source’s effective reporting interval (10–600 s, default 30 s), g = max(30 s, I/2). On schedule ≤ I + g · Overdue ≤ 2I + g · “Stale · last known <time>” beyond that or when collector contact is lost. The absolute age and the interval are always shown (“read 7 min ago · every 10 min”). Heartbeat is judged separately: late after 2H, contact lost after max(5 min, 3H); bw-pi-01 uses a 15 s override. JSON ups[].freshness = {state, ageSeconds, expectedIntervalSeconds, allowanceSeconds}.
Platform-specific instructions
Every command, elevation note, config path, secret store and log location comes from MK.cli.platform for that collector’s platform. macOS: sudo, /Library/Application Support/MainsKeeper/collector.toml, Keychain, /Library/Logs/MainsKeeper/. Fedora / Pi: sudo, /etc/mainskeeper/collector.toml, /etc/mainskeeper/secrets.d/, journalctl -u mainskeeperd. Windows: an elevated terminal (Administrator PowerShell, no sudo), C:\ProgramData\MainsKeeper\collector.toml, DPAPI, Event Viewer. ESP32: no host CLI. Use the probe’s local setup page.
Checks
"$OD_NODE_BIN" product/app/assets/state-checks.js drives the store and selectors in Node and exits 1 on any failure. 136/136 pass.

Freshness badges that moved

Computed with MK.select.upsReadingFreshness for every fixture UPS in incident, calm, witness-down, ups-comms-lost and revoked, against the 0.1.6 rule (≤ 60 s Fresh · ≤ 5 min Delayed · > 5 min Stale).

Deviations reported by the builders

What was exercised

Remaining limitations and open questions

Round 5 · checkpoint 0.1.6

This round finished and connected the product built in round 4. There is no new visual direction and no new feature area. The identity is still Lattice (concept 8) with Datum’s (concept 7) equipment workflows and the site-first fleet overview.

What changed

Decisions

Store
Pages read the fixture merged with the store and keep no copies of their own. sessionStorage, ?ack= and page-local fixtures are gone. Each incident has one acknowledgment record {by, at, note}. Some review links change state when opened, for example incident.html?id=INC-0412&step=ack.
Datasets
“Demo fleet” and “Your account (first run)”. ?scenario=empty shows the account. Creating a site there makes the account the active dataset. Pages without state/scenario keep showing the account until you switch back or reset. The review bar shows the active dataset.
Parameter contract
Links carry state, scenario, theme and motion. They never carry auth, enroll, id, site, collector, ups or focus. Sign-in uses ?auth=, so it no longer collides with the review bar’s ?state=. Unknown ids render “not found”.
Configuration ownership
One table (MK.config.contract) serves collector configuration, the review and the CLI spec. Dashboard-managed settings go desired → pending → applied at check-in, or fail with a reason. Alert rules are server-side and apply immediately. Secrets stay on the collector (mainskeeper sources auth <source>, elevated per platform since 0.1.7; ESP32 uses its setup page), and the dashboard shows only Configured / Missing / Rejected; the dashboard credential-delivery flow was removed. Storage and logging are local and read-only in the dashboard. Only heartbeat_interval may be overridden locally: “Effective: 15 s (local override) · Desired in dashboard: 30 s” on bw-pi-01.
CLI grammar & elevation
One grammar everywhere: enroll --reenroll, sources discover, sources use. --replace, scan and select no longer appear. macOS, Fedora and Raspberry Pi OS use sudo. Windows says “Run in an elevated terminal (Administrator)”. [0.1.7: paths, secret stores and logs are platform-specific too; ESP32 has no CLI.] Read-only commands need no elevation. logs needs the mainskeeper group. status exits 0 whenever the query succeeds. Network exits: duplicate 1, unreachable 6, rejected secret 6, partial 10 on test, success 0.
Network setup
Protocol → endpoint → local credentials (status only; must be Configured before the test) → test → identify → duplicate check → use → readings. A UPS already monitored by another collector is never created twice. You choose to keep the existing one or cancel. [Superseded in 0.1.7: duplicates are decided by network identity (namespace | protocol | host | port | UPS name); a trustworthy serial match only asks for confirmation. See round-6 decisions.] NUT ships with MainsKeeper.
Alert and timestamp model
On battery notifies after 60 s on battery. Low runtime fires below a 10 min UPS estimate. Contact lost fires after the contact grace — max(5 min, 3 × the collector’s heartbeat interval); 5 min for the 30 s heartbeats in this fixture. UPS comms lost fires after 2 min while the collector reports. Backlog stalled fires after 30 min. Home 06:42:10–06:42:48 (38 s) is “Below 60 s delay — event only”. Labels: First evidence · Grace period started · Declared · Acknowledged · Recovered · Resolved. Resolved = recovered and stable for 5 min (INC-0411 resolved 15:07:06; INC-0412 15:36:00 in the walkthrough).
Backfill policy
“Historical backfill updates history at observation time and does not generate duplicate or obsolete notifications. Fresh evidence of an active condition remains alertable while a backlog drains. Ordinary transmission latency does not make a reading historical replay.” Replay = a record received more than 2 min after observation while the collector was reconnecting from its buffer. Normal latency is not replay.
History semantics
Site, UPS and range filters apply to charts, episodes and events. When you select a UPS, other UPSs’ episodes at the site appear only under “Site context”. 1 h, 6 h and 24 h use 1-minute data. 7 d uses hourly aggregates (min/max/mean, power-state share, coverage) and hatches hours with no data as “No records”. Example: Shop, 26 Sep 02:00–06:00.
Staleness
[Superseded in 0.1.7: freshness is relative to each source’s reporting interval (On schedule · Overdue · Stale · last known); see moved badges.] Round 5 rule: a reading up to 60 s old was Fresh, up to 5 min Delayed, then “Stale · last known <time>”. Collector contact is lost after the contact grace, max(5 min, 3 × heartbeat). A legend appears on UPS and collector detail.
Witness and sleep
Witness down (?scenario=witness-down): home-studio stops at 15:04:12 and INC-0415 is declared, while “Site still reachable: home-esp32-garage reporting”. This is distinct from site contact loss (Shop: all witnesses silent, cause unknown) and from a locally observed connectivity failure (Duplex 13:41: probe evidence). home-laptop is an ordinary workstation, asleep since 13:05: “Asleep (ordinary workstation) — no incident”. It is excluded from “4 of 6 expected collectors reporting · 1 workstation asleep”.
ESP32 values
~384 KB flash ring, ~6 h at 30 s, USB HID on ESP32-S3 with USB-OTG host, max 3 targets. All are labelled “Illustrative — validated board/UPS list pending”. Nothing implies arbitrary boards or UPSs are supported.

What was exercised

Remaining limitations and open questions

What was built (round 4, updated)

Provenance

You chose a direction from round 3. Concept names appear only in these notes, never in the product UI.

Non-obvious behaviour

Assumptions

Known issues and unresolved design assumptions (round 4, status in round 5)

Round-5 status is in brackets. Open round-5 items are listed under Remaining limitations and open questions.

Verification

Behaviour and data

CLI

Visual and foundation deviations

Link (from)Receiving page readsResult
sync.html?collector= (fleet overview, history sync section)collector, case, stepHonoured; unknown → not found
relationships.html?site= (site detail)site, focus, cov, linksHonoured; filters clusters; unknown → not found
relationships.html?focus= (UPS detail, collectors)focus=<kind>:<id>Honoured; unknown focus → message, nothing preselected
sources.html?collector=garage-pi-02 (enroll, welcome)collector, caseHonoured for created collectors; unknown → not found
collector-config.html?id=garage-pi-02 (enroll, welcome)idHonoured for created collectors; unknown → not found
network-source.html?collector=&from=welcome&return= (welcome step 7, sources)collector, source, from, returnHonoured; return accepts relative *.html only
sign-in.html?auth= (shell sign-out)auth, returnHonoured; state left to the review bar
maintenance.html?site= (incident detail)site, edit, new, date, start, endHonoured
history.html?site=&range=6h (site), ?ups=&range= (UPS), ?site=&ups=&from=&to=&sel= (incident)site, ups, range, from, to, sel, epHonoured
sources.html?collector= (collector, UPS detail)collectorHonoured
incidents.html?ack=, relationships.html?sel=, ups.html?runtime=low (round-4 links)—Retired; acknowledgments come from the store, focus replaces sel; review links updated
notifications.html?channel=, alert-rules.html?rule=, incident.html?id=&step=, sync.html?case=&step=as linkedHonoured (step only for open INC-0412)

What’s next

  1. Review visually: open every link in the product map in both themes and at phone and tablet widths, and record defects.
  2. Decide the round-5 open questions: the enroll and queue exit codes, “resolved after 5 min stable”, the 10.0.30.5 fixture endpoint, network draft clean-up, and the 7-day custom-range limit.
  3. Fix what review finds in a further checkpoint.
  4. Prepare the final handoff only after you accept the design. This package is a nonfinal complete-design review checkpoint, not a handoff.

Checkpoint 0.2.3

Battery Assurance

Additive scope on the Lattice/Datum product. Synthetic demo data only. Monitoring-only: no hardware-write controls. Current operation, evidence, policy age, and unresolved findings stay distinct. Battery replacement starts a new installation id; prior evidence is retained and does not qualify the new battery. Findings resolve only with documented support — not utility restore, ack, snooze, or replacement.

0.2.1 corrections: preserve occurrence dates (never fill from receipt); shared eligibility then age assessment; successive replacements keep prior retirement dates; calm/restored episode sync drops live “ongoing” evidence; History depletion copy is outcome-derived; resolution store rejects empty claims and keeps resolved history; assurance policy is authoritative via setAssurancePolicy; CLI assurance lives in cli-screens.js and survives build-examples.js.

0.2.2 corrections: infer / choose battery association (predates-installation never qualifies); investigating stays in UNRESOLVED_STATUSES and attention counts; resolve validates linked evidence and shows the authored observation; setAssurancePolicy true-patch preserves qualifyKinds; awaitingEvidence derived from composed state; date-only occurrence display; standalone manual History records with episodeId-only association.

0.2.3 corrections: History manual records normalize the unselected "all" site/UPS sentinel to null (same as sel.history); date-only evidence uses calendar-day interval overlap for range filters instead of treating the stored noon anchor as an exact time; exact timestamps keep the point rule; occurrence stays separate from recording/receipt.

Browser verification and screenshots skipped by user request. Visual and browser-interaction acceptance remain unverified.

Entry: review scenarios · CONTRACT.md §22 · assets/assurance.js · state-checks.js groups 10–12.