diff --git a/CLAUDE.md b/CLAUDE.md index 60cfbef..895c929 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,12 +25,9 @@ Sentry-Emote is a minimalist system status monitor designed for an old Pixel pho ## Quick Start ```bash -# Setup python -m venv venv source venv/bin/activate # or .\venv\Scripts\activate on Windows pip install -r requirements.txt - -# Run everything python sentry.py ``` @@ -38,7 +35,7 @@ UI available at http://localhost:5000 ## Configuration -Edit `config.json` to configure the aggregator URL, enable/disable detectors, and set thresholds. +Edit `config.json` to configure detectors: ```json { @@ -49,11 +46,7 @@ Edit `config.json` to configure the aggregator URL, enable/disable detectors, an "name": "cpu", "enabled": true, "script": "detectors/cpu.py", - "env": { - "CHECK_INTERVAL": "30", - "THRESHOLD_WARNING": "85", - "THRESHOLD_CRITICAL": "95" - } + "env": { "CHECK_INTERVAL": "30", "THRESHOLD_WARNING": "85", "THRESHOLD_CRITICAL": "95" } } ] } @@ -73,35 +66,43 @@ All detectors support: `AGGREGATOR_URL`, `CHECK_INTERVAL`, `THRESHOLD_WARNING`, ## API Endpoints -- `POST /event` — Register event: `{"id": "name", "priority": 1-4, "message": "optional", "ttl": optional_seconds}` -- `POST /clear` — Clear event: `{"id": "name"}` -- `GET /status` — Current state JSON -- `GET /events` — List active events +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/event` | POST | Register event: `{"id": "name", "priority": 1-4, "message": "optional", "ttl": seconds}` | +| `/clear` | POST | Clear event: `{"id": "name"}` | +| `/sleep` | POST | Enter sleep mode (for Home Assistant) | +| `/wake` | POST | Exit sleep mode | +| `/status` | GET | Current state JSON | +| `/events` | GET | List active events | ## Priority System Lower number = higher priority. Events with a `ttl` auto-expire (heartbeat pattern). -| Priority | State | Emote | Color | Behavior | -|----------|----------|----------|--------|----------| -| 1 | Critical | `( x_x)` | Red | Shaking animation | -| 2 | Warning | `( o_o)` | Yellow | Breathing animation | -| 3 | Notify | `( 'o')` | Blue | Popping animation, 10s default TTL | -| 4 | Optimal | `( ^_^)` | Green | Default when no events | +| Priority | State | Emote | Color | Animation | +|----------|-------|-------|-------|-----------| +| 1 | Critical | `( x_x)` | Red | shaking | +| 2 | Warning | `( o_o)` | Yellow | breathing | +| 3 | Notify | `( 'o')` | Blue | popping | +| 4 | Optimal | varies | Green | varies | -## Testing Events +## Personality System -```bash -# Warning with 30s TTL -curl -X POST -H "Content-Type: application/json" \ - -d '{"id":"test","priority":2,"message":"Test warning","ttl":30}' \ - http://localhost:5000/event +The optimal state cycles through emotes with paired animations every 5 minutes: -# Clear manually -curl -X POST -H "Content-Type: application/json" \ - -d '{"id":"test"}' \ - http://localhost:5000/clear -``` +| Emote | Animation | Vibe | +|-------|-----------|------| +| `( ^_^)` | breathing | calm | +| `( ᵔᴥᵔ)` | floating | dreamy | +| `(◕‿◕)` | bouncing | cheerful | +| `( ・ω・)` | swaying | curious | +| `( ˘▽˘)` | breathing | cozy | + +Additional states: +- **Idle expressions** (15% chance): `( -_^)`, `( ^_~)`, `( ᵕ.ᵕ)` with blink animation +- **Recovery celebration**: `\(^o^)/` with bounce for 5 seconds after issues resolve +- **Connection lost**: `( ?.?)` gray, searching animation +- **Sleep mode**: `( -_-)zzZ` dim, very slow breathing ## File Structure diff --git a/README.md b/README.md new file mode 100644 index 0000000..4f847c8 --- /dev/null +++ b/README.md @@ -0,0 +1,155 @@ +# Sentry-Emote + +A minimalist system status monitor that uses ASCII emotes to display server health on an old phone. + +![Status: Optimal](https://img.shields.io/badge/status-(%20%5E___%5E)-brightgreen) + +## Why? + +Turn an old phone (with its OLED screen) into a glanceable ambient display for your home server. Instead of graphs and numbers, see a happy face `( ^_^)` when things are good, and a worried face `( o_o)` when they're not. + +## Features + +- **OLED-optimized** — Pure black background, saves battery +- **Glanceable** — Know your server's status from across the room +- **Extensible** — Add custom detectors for any metric +- **Personality** — Rotating expressions, celebration animations, sleep mode +- **Home Assistant ready** — Webhook endpoints for automation + +## Quick Start + +```bash +# Clone and setup +git clone https://github.com/yourusername/sentry-emote.git +cd sentry-emote +python -m venv venv +source venv/bin/activate # Windows: .\venv\Scripts\activate +pip install -r requirements.txt + +# Run everything +python sentry.py +``` + +Open http://localhost:5000 on your phone (use Fully Kiosk Browser for best results). + +## Status Faces + +| State | Emote | Meaning | +|-------|-------|---------| +| Optimal | `( ^_^)` | All systems healthy | +| Warning | `( o_o)` | Something needs attention | +| Critical | `( x_x)` | Immediate action required | +| Notify | `( 'o')` | Transient notification | +| Sleeping | `( -_-)zzZ` | Sleep mode active | +| Disconnected | `( ?.?)` | Can't reach server | + +## Built-in Detectors + +| Detector | Monitors | +|----------|----------| +| **disk_space** | Disk usage on all drives | +| **cpu** | CPU utilization | +| **memory** | RAM usage | +| **service** | Whether processes are running | +| **network** | Host reachability (ping) | + +## Configuration + +Edit `config.json` to enable/disable detectors and set thresholds: + +```json +{ + "aggregator_url": "http://localhost:5000", + "detectors": [ + { + "name": "disk_space", + "enabled": true, + "script": "detectors/disk_space.py", + "env": { + "CHECK_INTERVAL": "300", + "THRESHOLD_WARNING": "85", + "THRESHOLD_CRITICAL": "95" + } + } + ] +} +``` + +## Custom Detectors + +Create your own detector by POSTing events to the aggregator: + +```bash +curl -X POST http://localhost:5000/event \ + -H "Content-Type: application/json" \ + -d '{"id": "my_check", "priority": 2, "message": "Something is wrong", "ttl": 120}' +``` + +- `id` — Unique identifier for this event +- `priority` — 1 (critical), 2 (warning), 3 (notify), 4 (optimal) +- `message` — What to display +- `ttl` — Auto-expire after N seconds (for heartbeat pattern) + +Clear an event: +```bash +curl -X POST http://localhost:5000/clear \ + -d '{"id": "my_check"}' +``` + +## Home Assistant Integration + +Add webhook commands to your Home Assistant config: + +```yaml +rest_command: + sentry_sleep: + url: "http://YOUR_SERVER:5000/sleep" + method: POST + sentry_wake: + url: "http://YOUR_SERVER:5000/wake" + method: POST +``` + +Trigger from automations: + +```yaml +automation: + - alias: "Sentry Sleep at Bedtime" + trigger: + platform: time + at: "23:00:00" + action: + service: rest_command.sentry_sleep + + - alias: "Sentry Wake in Morning" + trigger: + platform: time + at: "07:00:00" + action: + service: rest_command.sentry_wake +``` + +## API Reference + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/` | GET | Web UI | +| `/status` | GET | Current state as JSON | +| `/events` | GET | List all active events | +| `/event` | POST | Register an event | +| `/clear` | POST | Clear an event by ID | +| `/sleep` | POST | Enter sleep mode | +| `/wake` | POST | Exit sleep mode | + +## Personality + +The emote has personality! In optimal state it: + +- Rotates through happy faces every 5 minutes +- Occasionally winks `( -_^)` or blinks `( ᵕ.ᵕ)` +- Celebrates `\(^o^)/` when recovering from warnings +- Each face has its own animation (floating, bouncing, swaying) + +## License + +MIT diff --git a/aggregator.py b/aggregator.py index 20a88f8..6845e70 100644 --- a/aggregator.py +++ b/aggregator.py @@ -53,11 +53,29 @@ last_emote_change = 0 current_optimal_emote = OPTIMAL_EMOTES[0][0] current_optimal_animation = OPTIMAL_EMOTES[0][1] +# Sleep mode +is_sleeping = False +SLEEP_EMOTE = "( -_-)zzZ" +SLEEP_COLOR = "#333333" +SLEEP_ANIMATION = "sleeping" + def get_current_state(): """Determine current state based on active events.""" global previous_priority, celebrating_until, last_emote_change, current_optimal_emote, current_optimal_animation + # Sleep mode overrides everything + if is_sleeping: + return { + "current_state": "sleeping", + "active_emote": SLEEP_EMOTE, + "color": SLEEP_COLOR, + "animation": SLEEP_ANIMATION, + "message": "", + "active_events": [], + "last_updated": datetime.now().isoformat(timespec="seconds"), + } + now = time.time() with events_lock: @@ -196,6 +214,24 @@ def clear_event(): return jsonify({"error": "Event not found"}), 404 +@app.route("/sleep", methods=["POST"]) +def sleep_mode(): + """Enter sleep mode. For Home Assistant webhook.""" + global is_sleeping + is_sleeping = True + state = write_status() + return jsonify({"status": "sleeping", "current_state": state}), 200 + + +@app.route("/wake", methods=["POST"]) +def wake_mode(): + """Exit sleep mode. For Home Assistant webhook.""" + global is_sleeping + is_sleeping = False + state = write_status() + return jsonify({"status": "awake", "current_state": state}), 200 + + @app.route("/") def index(): """Serve the frontend.""" diff --git a/index.html b/index.html index 6c37522..df9e5e3 100644 --- a/index.html +++ b/index.html @@ -172,6 +172,22 @@ opacity: 0.8; } } + + /* Sleeping animation - very slow, subtle breathing */ + .sleeping { + animation: sleep 6s ease-in-out infinite; + } + + @keyframes sleep { + 0%, 100% { + opacity: 0.4; + transform: scale(1); + } + 50% { + opacity: 0.2; + transform: scale(0.98); + } + }