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 <noreply@anthropic.com>
This commit is contained in:
2026-02-06 11:54:00 -06:00
parent fa0c16609d
commit c3ceb74ce8
6 changed files with 61 additions and 6 deletions

View File

@@ -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}` | | `/event` | POST | Register event: `{"id": "name", "priority": 1-4, "message": "optional", "ttl": seconds}` |
| `/clear` | POST | Clear event: `{"id": "name"}` | | `/clear` | POST | Clear event: `{"id": "name"}` |
| `/clear-all` | POST | Clear all active events |
| `/notify` | POST | Notification with optional customization (see below) | | `/notify` | POST | Notification with optional customization (see below) |
| `/sleep` | POST | Enter sleep mode | | `/sleep` | POST | Enter sleep mode |
| `/wake` | POST | Exit sleep mode | | `/wake` | POST | Exit sleep mode |

View File

@@ -188,6 +188,7 @@ automation:
| `/events` | GET | List all active events | | `/events` | GET | List all active events |
| `/event` | POST | Register an event | | `/event` | POST | Register an event |
| `/clear` | POST | Clear an event by ID | | `/clear` | POST | Clear an event by ID |
| `/clear-all` | POST | Clear all active events |
| `/notify` | POST | Simple notification `{"message": "", "duration": 5}` | | `/notify` | POST | Simple notification `{"message": "", "duration": 5}` |
| `/sleep` | POST | Enter sleep mode | | `/sleep` | POST | Enter sleep mode |
| `/wake` | POST | Exit 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 - Occasionally winks `( -_^)` or blinks `( ᵕ.ᵕ)` for a second or two
- Celebrates `\(^o^)/` when recovering from warnings - Celebrates `\(^o^)/` when recovering from warnings
- Each face has its own animation (floating, bouncing, swaying) - 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`): **Sound effects** (tap screen to enable, or use `?sound=on`):
- Warning: soft double-beep - Warning: soft double-beep

View File

@@ -226,6 +226,17 @@ def post_event():
return jsonify({"status": "ok", "current_state": state}), 200 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"]) @app.route("/clear", methods=["POST"])
def clear_event(): def clear_event():
""" """

View File

@@ -115,6 +115,7 @@ def main():
print() print()
active_alerts = set() active_alerts = set()
last_restart_counts = {}
while True: while True:
containers = get_container_status() containers = get_container_status()
@@ -137,15 +138,23 @@ def main():
event_id = f"docker_{name.replace('/', '_')}" event_id = f"docker_{name.replace('/', '_')}"
# Check for restarting state # Check restart count for running/restarting containers
if state == "restarting": # 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) 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)") send_event(event_id, 1, f"Container '{name}' restart loop ({restart_count}x)")
current_alerts.add(event_id) current_alerts.add(event_id)
else: elif new_restarts > 0:
send_event(event_id, 2, f"Container '{name}' restarting ({restart_count}x)") send_event(event_id, 2, f"Container '{name}' restarting ({restart_count}x)")
current_alerts.add(event_id) current_alerts.add(event_id)
else:
print(f"[OK] Container '{name}' is {state}")
# Check for exited/dead containers (warning) # Check for exited/dead containers (warning)
elif state in ("exited", "dead"): elif state in ("exited", "dead"):

View File

@@ -377,6 +377,11 @@
const prevClass = emoteEl.className; const prevClass = emoteEl.className;
const prevMsg = messageEl.textContent; 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! // Surprised face!
emoteEl.textContent = "( °o°)"; emoteEl.textContent = "( °o°)";
emoteEl.className = "popping"; emoteEl.className = "popping";

View File

@@ -14,7 +14,7 @@ info:
## TTL/Heartbeat Pattern ## TTL/Heartbeat Pattern
Events can have a TTL (time-to-live) that auto-expires them. Detectors typically send 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. heartbeat events that expire if not refreshed, indicating loss of communication.
version: 1.3.0 version: 1.4.0
license: license:
name: MIT name: MIT
@@ -65,6 +65,21 @@ paths:
schema: schema:
$ref: "#/components/schemas/Error" $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: /clear:
post: post:
summary: Clear an event summary: Clear an event
@@ -233,6 +248,19 @@ components:
current_state: current_state:
$ref: "#/components/schemas/Status" $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: ClearResponse:
type: object type: object
properties: properties: