From c3ceb74ce88feb8912b38458527276e9ed6c42a3 Mon Sep 17 00:00:00 2001 From: Spencer Grimes Date: Fri, 6 Feb 2026 11:54:00 -0600 Subject: [PATCH] Bump to v1.4.0: tap-to-dismiss, docker restart detection, cleanup thread fix Add /clear-all endpoint and wire it to the tap handler so tapping the display dismisses active warnings/critical alerts. Fix docker detector to track restart count deltas instead of relying on the transient "restarting" state. Wrap cleanup thread in try/except so it can't die silently and leave events stuck forever. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 1 + README.md | 3 ++- aggregator.py | 11 +++++++++++ detectors/docker.py | 17 +++++++++++++---- index.html | 5 +++++ openapi.yaml | 30 +++++++++++++++++++++++++++++- 6 files changed, 61 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ba29387..f4430b6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -75,6 +75,7 @@ All detectors support: `AGGREGATOR_URL`, `CHECK_INTERVAL`, `THRESHOLD_WARNING`, |----------|--------|-------------| | `/event` | POST | Register event: `{"id": "name", "priority": 1-4, "message": "optional", "ttl": seconds}` | | `/clear` | POST | Clear event: `{"id": "name"}` | +| `/clear-all` | POST | Clear all active events | | `/notify` | POST | Notification with optional customization (see below) | | `/sleep` | POST | Enter sleep mode | | `/wake` | POST | Exit sleep mode | diff --git a/README.md b/README.md index c72243e..c179bd8 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,7 @@ automation: | `/events` | GET | List all active events | | `/event` | POST | Register an event | | `/clear` | POST | Clear an event by ID | +| `/clear-all` | POST | Clear all active events | | `/notify` | POST | Simple notification `{"message": "", "duration": 5}` | | `/sleep` | POST | Enter sleep mode | | `/wake` | POST | Exit sleep mode | @@ -203,7 +204,7 @@ The emote has personality! In optimal state it: - Occasionally winks `( -_^)` or blinks `( ᵕ.ᵕ)` for a second or two - Celebrates `\(^o^)/` when recovering from warnings - Each face has its own animation (floating, bouncing, swaying) -- Reacts when tapped `( °o°)` and shows version info +- Reacts when tapped `( °o°)`, shows version info, and dismisses active alerts **Sound effects** (tap screen to enable, or use `?sound=on`): - Warning: soft double-beep diff --git a/aggregator.py b/aggregator.py index c6df809..6263c9a 100644 --- a/aggregator.py +++ b/aggregator.py @@ -226,6 +226,17 @@ def post_event(): return jsonify({"status": "ok", "current_state": state}), 200 +@app.route("/clear-all", methods=["POST"]) +def clear_all_events(): + """Clear all active events.""" + with events_lock: + count = len(active_events) + active_events.clear() + + state = write_status() + return jsonify({"status": "cleared", "count": count, "current_state": state}), 200 + + @app.route("/clear", methods=["POST"]) def clear_event(): """ diff --git a/detectors/docker.py b/detectors/docker.py index 269b33d..645856f 100644 --- a/detectors/docker.py +++ b/detectors/docker.py @@ -115,6 +115,7 @@ def main(): print() active_alerts = set() + last_restart_counts = {} while True: containers = get_container_status() @@ -137,15 +138,23 @@ def main(): event_id = f"docker_{name.replace('/', '_')}" - # Check for restarting state - if state == "restarting": + # Check restart count for running/restarting containers + # The "restarting" state is too transient to catch reliably, + # so we track count increases between checks instead + if state in ("running", "restarting"): restart_count = get_restart_count(name) - if restart_count >= RESTART_THRESHOLD: + prev_count = last_restart_counts.get(name, restart_count) + new_restarts = restart_count - prev_count + last_restart_counts[name] = restart_count + + if state == "restarting" or new_restarts >= RESTART_THRESHOLD: send_event(event_id, 1, f"Container '{name}' restart loop ({restart_count}x)") current_alerts.add(event_id) - else: + elif new_restarts > 0: send_event(event_id, 2, f"Container '{name}' restarting ({restart_count}x)") current_alerts.add(event_id) + else: + print(f"[OK] Container '{name}' is {state}") # Check for exited/dead containers (warning) elif state in ("exited", "dead"): diff --git a/index.html b/index.html index 88e8574..57c336d 100644 --- a/index.html +++ b/index.html @@ -377,6 +377,11 @@ const prevClass = emoteEl.className; const prevMsg = messageEl.textContent; + // Clear active warning/critical events + if (lastData && lastData.active_events && lastData.active_events.length > 0) { + fetch("/clear-all", { method: "POST" }); + } + // Surprised face! emoteEl.textContent = "( °o°)"; emoteEl.className = "popping"; diff --git a/openapi.yaml b/openapi.yaml index f130e0d..31ffd22 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -14,7 +14,7 @@ info: ## TTL/Heartbeat Pattern Events can have a TTL (time-to-live) that auto-expires them. Detectors typically send heartbeat events that expire if not refreshed, indicating loss of communication. - version: 1.3.0 + version: 1.4.0 license: name: MIT @@ -65,6 +65,21 @@ paths: schema: $ref: "#/components/schemas/Error" + /clear-all: + post: + summary: Clear all events + description: | + Clear all active events at once. Used by the frontend when the display + is tapped to dismiss warnings and critical alerts. + operationId: clearAllEvents + responses: + "200": + description: All events cleared + content: + application/json: + schema: + $ref: "#/components/schemas/ClearAllResponse" + /clear: post: summary: Clear an event @@ -233,6 +248,19 @@ components: current_state: $ref: "#/components/schemas/Status" + ClearAllResponse: + type: object + properties: + status: + type: string + example: "cleared" + count: + type: integer + description: Number of events that were cleared + example: 2 + current_state: + $ref: "#/components/schemas/Status" + ClearResponse: type: object properties: