Add git-based auto-update for Linux deployment

- install.sh now clones from git instead of copying files
- Service runs git pull on restart for automatic updates
- Support config.local.json for production settings (gitignored)
- kao.py prefers config.local.json when present

To update production: push changes, then 'sudo systemctl restart kao'

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 17:34:51 -06:00
parent 81c23308cc
commit 36aecb1fc9
4 changed files with 107 additions and 68 deletions

1
.gitignore vendored
View File

@@ -8,6 +8,7 @@ __pycache__/
# Runtime files # Runtime files
status.json status.json
config.local.json
# IDE # IDE
.vscode/ .vscode/

View File

@@ -2,12 +2,17 @@
A minimalist system status monitor that uses ASCII emotes to display server health on an old phone. 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) ![Status: Optimal](<https://img.shields.io/badge/status-(%20%5E___%5E)-brightgreen>)
## Why? ## 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. 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.
## TODO
- Figure out way to update installation in /opt/kao when git updates. Maybe run a git pull on service startup?
- Think about ways of implementing noises.
## Features ## Features
- **OLED-optimized** — Pure black background, saves battery - **OLED-optimized** — Pure black background, saves battery
@@ -34,24 +39,24 @@ Open http://localhost:5100 on your phone (use Fully Kiosk Browser for best resul
## Status Faces ## Status Faces
| State | Emote | Meaning | | State | Emote | Meaning |
|-------|-------|---------| | ------------ | ----------- | ------------------------- |
| Optimal | `( ^_^)` | All systems healthy | | Optimal | `( ^_^)` | All systems healthy |
| Warning | `( o_o)` | Something needs attention | | Warning | `( o_o)` | Something needs attention |
| Critical | `( x_x)` | Immediate action required | | Critical | `( x_x)` | Immediate action required |
| Notify | `( 'o')` | Transient notification | | Notify | `( 'o')` | Transient notification |
| Sleeping | `( -_-)zzZ` | Sleep mode active | | Sleeping | `( -_-)zzZ` | Sleep mode active |
| Disconnected | `( ?.?)` | Can't reach server | | Disconnected | `( ?.?)` | Can't reach server |
## Built-in Detectors ## Built-in Detectors
| Detector | Monitors | | Detector | Monitors |
|----------|----------| | -------------- | ----------------------------- |
| **disk_space** | Disk usage on all drives | | **disk_space** | Disk usage on all drives |
| **cpu** | CPU utilization | | **cpu** | CPU utilization |
| **memory** | RAM usage | | **memory** | RAM usage |
| **service** | Whether processes are running | | **service** | Whether processes are running |
| **network** | Host reachability (ping) | | **network** | Host reachability (ping) |
## Configuration ## Configuration
@@ -59,19 +64,19 @@ Edit `config.json` to enable/disable detectors and set thresholds:
```json ```json
{ {
"aggregator_url": "http://localhost:5100", "aggregator_url": "http://localhost:5100",
"detectors": [ "detectors": [
{ {
"name": "disk_space", "name": "disk_space",
"enabled": true, "enabled": true,
"script": "detectors/disk_space.py", "script": "detectors/disk_space.py",
"env": { "env": {
"CHECK_INTERVAL": "300", "CHECK_INTERVAL": "300",
"THRESHOLD_WARNING": "85", "THRESHOLD_WARNING": "85",
"THRESHOLD_CRITICAL": "95" "THRESHOLD_CRITICAL": "95"
} }
} }
] ]
} }
``` ```
@@ -91,6 +96,7 @@ curl -X POST http://localhost:5100/event \
- `ttl` — Auto-expire after N seconds (for heartbeat pattern) - `ttl` — Auto-expire after N seconds (for heartbeat pattern)
Clear an event: Clear an event:
```bash ```bash
curl -X POST http://localhost:5100/clear \ curl -X POST http://localhost:5100/clear \
-d '{"id": "my_check"}' -d '{"id": "my_check"}'
@@ -131,15 +137,15 @@ automation:
## API Reference ## API Reference
| Endpoint | Method | Description | | Endpoint | Method | Description |
|----------|--------|-------------| | --------- | ------ | ---------------------- |
| `/` | GET | Web UI | | `/` | GET | Web UI |
| `/status` | GET | Current state as JSON | | `/status` | GET | Current state as JSON |
| `/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 |
| `/sleep` | POST | Enter sleep mode | | `/sleep` | POST | Enter sleep mode |
| `/wake` | POST | Exit sleep mode | | `/wake` | POST | Exit sleep mode |
## Personality ## Personality

View File

@@ -1,6 +1,7 @@
#!/bin/bash #!/bin/bash
# Kao Installation Script # Kao Installation Script
# Installs to /opt/kao and sets up systemd service # Clones from git to /opt/kao and sets up systemd service
# Updates automatically via git pull on service restart
set -e set -e
@@ -17,7 +18,6 @@ echo " Kao Installer"
echo "====================================" echo "===================================="
echo "Install directory: $INSTALL_DIR" echo "Install directory: $INSTALL_DIR"
echo "Running as user: $CURRENT_USER" echo "Running as user: $CURRENT_USER"
echo "Source: $SCRIPT_DIR"
echo "" echo ""
# Check if running as root # Check if running as root
@@ -40,34 +40,58 @@ if ! command -v python3 &> /dev/null; then
exit 1 exit 1
fi fi
# Check source files exist # Check for git
if [ ! -f "$SCRIPT_DIR/kao.py" ]; then if ! command -v git &> /dev/null; then
echo "Error: Source files not found in $SCRIPT_DIR" echo "Error: git not found"
echo "Run this script from the Kao repository directory" echo "Install with: sudo apt install git"
exit 1 exit 1
fi fi
# Create install directory # Get git remote URL from source repo
echo "[1/5] Creating install directory..." if [ -d "$SCRIPT_DIR/.git" ]; then
mkdir -p "$INSTALL_DIR" REPO_URL=$(git -C "$SCRIPT_DIR" remote get-url origin 2>/dev/null || echo "")
cp "$SCRIPT_DIR/aggregator.py" "$INSTALL_DIR/" fi
cp "$SCRIPT_DIR/kao.py" "$INSTALL_DIR/"
cp "$SCRIPT_DIR/index.html" "$INSTALL_DIR/"
cp "$SCRIPT_DIR/config.json" "$INSTALL_DIR/"
cp "$SCRIPT_DIR/requirements.txt" "$INSTALL_DIR/"
cp -r "$SCRIPT_DIR/detectors" "$INSTALL_DIR/"
# Set ownership before venv creation if [ -z "$REPO_URL" ]; then
echo "Error: Could not detect git remote URL"
echo "Run this script from within the Kao git repository"
exit 1
fi
echo "Git remote: $REPO_URL"
echo ""
# Remove existing installation if present
if [ -d "$INSTALL_DIR" ]; then
echo "[1/6] Removing existing installation..."
systemctl stop kao.service 2>/dev/null || true
rm -rf "$INSTALL_DIR"
else
echo "[1/6] No existing installation found"
fi
# Clone repository (as root since /opt requires elevated permissions)
echo "[2/6] Cloning repository..."
git clone "$REPO_URL" "$INSTALL_DIR"
# Set ownership to target user (so they can git pull, create venv, etc.)
chown -R "$CURRENT_USER:$CURRENT_GROUP" "$INSTALL_DIR" chown -R "$CURRENT_USER:$CURRENT_GROUP" "$INSTALL_DIR"
# Create local config for production customization
echo "[3/6] Creating local config..."
if [ ! -f "$INSTALL_DIR/config.local.json" ]; then
cp "$INSTALL_DIR/config.json" "$INSTALL_DIR/config.local.json"
echo "Created config.local.json - edit this for production settings"
fi
# Create virtual environment as target user # Create virtual environment as target user
echo "[2/5] Creating virtual environment..." echo "[4/6] Creating virtual environment..."
sudo -u "$CURRENT_USER" python3 -m venv "$INSTALL_DIR/venv" sudo -u "$CURRENT_USER" python3 -m venv "$INSTALL_DIR/venv"
sudo -u "$CURRENT_USER" "$INSTALL_DIR/venv/bin/pip" install --upgrade pip -q sudo -u "$CURRENT_USER" "$INSTALL_DIR/venv/bin/pip" install --upgrade pip -q
sudo -u "$CURRENT_USER" "$INSTALL_DIR/venv/bin/pip" install -r "$INSTALL_DIR/requirements.txt" -q sudo -u "$CURRENT_USER" "$INSTALL_DIR/venv/bin/pip" install -r "$INSTALL_DIR/requirements.txt" -q
# Create systemd service # Create systemd service with git pull on start
echo "[3/5] Creating systemd service..." echo "[5/6] Creating systemd service..."
cat > "$SERVICE_FILE" << EOF cat > "$SERVICE_FILE" << EOF
[Unit] [Unit]
Description=Kao - System Status Monitor Description=Kao - System Status Monitor
@@ -78,6 +102,7 @@ Type=simple
User=$CURRENT_USER User=$CURRENT_USER
Group=$CURRENT_GROUP Group=$CURRENT_GROUP
WorkingDirectory=$INSTALL_DIR WorkingDirectory=$INSTALL_DIR
ExecStartPre=/usr/bin/git -C $INSTALL_DIR pull --ff-only
ExecStart=$INSTALL_DIR/venv/bin/python $INSTALL_DIR/kao.py ExecStart=$INSTALL_DIR/venv/bin/python $INSTALL_DIR/kao.py
Restart=always Restart=always
RestartSec=5 RestartSec=5
@@ -87,12 +112,9 @@ WantedBy=multi-user.target
EOF EOF
# Reload systemd and enable service # Reload systemd and enable service
echo "[4/5] Enabling service..." echo "[6/6] Enabling and starting service..."
systemctl daemon-reload systemctl daemon-reload
systemctl enable kao.service systemctl enable kao.service
# Start service
echo "[5/5] Starting Kao..."
systemctl start kao.service systemctl start kao.service
echo "" echo ""
@@ -103,9 +125,11 @@ echo ""
echo "Kao is now running at http://$(hostname -I | awk '{print $1}'):5100" echo "Kao is now running at http://$(hostname -I | awk '{print $1}'):5100"
echo "" echo ""
echo "Commands:" echo "Commands:"
echo " sudo systemctl status kao # Check status" echo " sudo systemctl status kao # Check status"
echo " sudo systemctl restart kao # Restart" echo " sudo systemctl restart kao # Restart (auto-pulls latest code)"
echo " sudo systemctl stop kao # Stop" echo " sudo systemctl stop kao # Stop"
echo " sudo journalctl -u kao -f # View logs" echo " sudo journalctl -u kao -f # View logs"
echo ""
echo "Config: $INSTALL_DIR/config.local.json"
echo "To update: push changes to git, then 'sudo systemctl restart kao'"
echo "" echo ""
echo "Config file: $INSTALL_DIR/config.json"

12
kao.py
View File

@@ -242,10 +242,18 @@ def main():
else: else:
config_path = sys.argv[1] config_path = sys.argv[1]
# Resolve config path # Resolve config path - prefer config.local.json if it exists
base_dir = Path(__file__).parent
config_path = Path(config_path) config_path = Path(config_path)
if not config_path.is_absolute(): if not config_path.is_absolute():
config_path = Path(__file__).parent / config_path # Check for local config override first
local_config = base_dir / "config.local.json"
if local_config.exists() and config_path.name == "config.json":
config_path = local_config
print(f"Using local config: {config_path}")
else:
config_path = base_dir / config_path
if not config_path.exists(): if not config_path.exists():
print(f"Config file not found: {config_path}") print(f"Config file not found: {config_path}")