MainsKeeper Product & System Guide Edition 0.2.3

Chapter 5 · contracts

Interfaces and contracts

Boundaries between components: what crosses them, what stays local, and what is still illustrative.

Full catalog with producer/consumer fields: Interface catalog.

Monitoring-only boundary

Accepted MainsKeeper does not initiate UPS self-tests, discharge tests, shutdowns, reboots, outlet switching, or other hardware writes. Source connectivity tests are read-only and must not be confused with battery self-tests. Alert thresholds configure notifications only.
Enrollment sequence

Figure 6. Enrollment → first contact → first UPS reading

Takeaway: The sequence shows who sends the code, where the credential is stored, and that a denied enroll stores nothing. Endpoint names stay Proposed.

Status correction. The 10-minute code lifetime, and the split between approval, first contact, and the first reading, are Accepted behavior. Endpoint names, wire schemas, and API transport on this figure are Proposed. WDJB-MJHT is synthetic.
Configuration states

Figure 10. Configuration states — desired · pending · applied · failed

Takeaway: Pending leads separately to Applied or to Failed. A failure does not become Applied. Lost contact leaves the change Pending.

Illustrative exchanges

Schemas and endpoint names below are illustrative / Proposed unless noted as Accepted product behavior.

Example — telemetry record (illustrative)
{
  "schema": "mainskeeper.telemetry.reading/v1-illustrative",
  "collectorId": "bw-pi-01",
  "upsId": "ups-bw-rack",
  "observedAt": "2026-04-14T09:12:00-07:00",
  "receivedAt": "2026-04-14T09:12:02-07:00",
  "powerState": "battery",
  "chargePercent": { "support": "not-reported" },
  "runtimeEstimateMin": { "support": "not-reported" },
  "sequence": 4412
}

This is the walkthrough’s first on-battery observation, delivered while the WAN is still up. Charge and runtime are omitted on purpose. Missing measurements use unsupported or not-reported, never a fabricated 0. See the Broadwater outage walkthrough.

Example — replayed record (illustrative)
{
  "observedAt": "2026-04-14T09:41:00-07:00",
  "receivedAt": "2026-04-14T09:48:40-07:00",
  "flags": ["received-late", "replayed"],
  "powerState": "utility",
  "sequence": 4488,
  "note": "Return to utility. History at 09:41. Does not set recovery. The 09:12 row was already acked and is not sent again."
}

Receipt is more than two minutes after observation, during drain, so this is replay. It places the return to utility in history at 09:41 and ends the observed battery episode. It does not recover the on-battery incident, and it does not repeat the 09:13 notification. A separate fresh UPS reading after the 09:48 heartbeat does the recovery. Resolution is five stable minutes after that recovery. The replayed row keeps its original timestamp and does not overwrite the newer current state.

Example — enrollment exchange (behavior Accepted; HTTP Proposed)
# Collector
mainskeeper enroll --headless
# → short code WDJB-MJHT (design TTL 10 min), device polls for approval

# Operator (dashboard)
# approve code → site + roles + witness flag

# Collector receives credential material out-of-band from URL/logs
# First contact ≠ first UPS reading
Example — desired vs applied config
Desired (dashboard): heartbeat_interval = 30s
Local override:      heartbeat_interval = 15s
Effective text: "Effective: 15 s (local override) · Desired in dashboard: 30 s"
State lifecycle: desired → pending → applied | failed(reason)
Operational incident lifecycle

Figure 11. Operational incident lifecycle

Takeaway: Acknowledgement sits beside the state path. Recovery follows fresh evidence that the condition cleared. Resolution follows five minutes stable. First evidence and grace have no fixed order.