infra: full IaC state after hf-pdocker-01 rebuild (2026-03-08)
Replaces initial Nomad/Consul/Terraform skeleton with current Docker-based infrastructure across all three nodes (hf-pdocker-01, hf-pdocker-02, bart). Key changes: - CLAUDE.md: updated service distribution reflecting hf-pdocker-01 restored - docs/plans/2026-03-08-hf-pdocker-01-rebuild.md: new rebuild plan doc - compose/bart/: ai-agents, guacamole, matrix, media, openvas, smarthome, watchtower - compose/hf-pdocker-02/: traefik, monitoring, adguard, gitea, semaphore, wikijs, unifi, vaultwarden, actual-budget, mcpo, netclaw, trivy, watchtower, n8n - compose/hf-pdocker-01/: frigate, frigate-notify, farm-agent - stacks/: all Swarm stack definitions (monitoring, traefik, smarthome, matrix, guacamole, media, ai-agents, frigate, gitea, semaphore, wikijs, unifi, etc.) - .gitignore: exclude .env credential files and acme.json certs - .env.example: placeholder credential files for all services - .claude/agents/: gitea-backup and other infrastructure agent configs - scripts/: frigate media cleanup and NAS sync scripts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
# Frigate Video Alerts via Matrix
|
||||
|
||||
## Summary
|
||||
|
||||
A lightweight Python container that listens for Frigate object detection events via MQTT, analyzes snapshots with Claude Vision (Haiku), and posts rich notifications to a dedicated Matrix room with inline images and AI-generated descriptions.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Frigate NVR (hf-pdocker-01)
|
||||
| MQTT: frigate/events
|
||||
v
|
||||
Mosquitto Broker (:1883)
|
||||
|
|
||||
v
|
||||
frigate-notify (Python container, hf-pdocker-01, host network)
|
||||
|-- Frigate API: GET /api/events/{id}/snapshot.jpg
|
||||
|-- Claude Vision API: analyze snapshot (Haiku)
|
||||
|-- Matrix API: upload image + send message
|
||||
v
|
||||
#video-alerts:hobbs.farm (@frigate:hobbs.farm bot)
|
||||
```
|
||||
|
||||
## Event Flow & Filtering
|
||||
|
||||
**MQTT topic:** `frigate/events`
|
||||
|
||||
**Filtering:**
|
||||
1. Only `type == "end"` events (complete detection, best snapshot)
|
||||
2. Only `label` in `["person", "car", "dog", "cat", "bird"]`
|
||||
3. Only `score >= 0.7`
|
||||
4. Only if `has_snapshot == true`
|
||||
5. Deduplication: skip duplicate event IDs within 30-second window per camera
|
||||
6. Rate limit: max 1 notification per camera per 60 seconds
|
||||
|
||||
## AI Vision Analysis
|
||||
|
||||
- Model: `claude-haiku-4-5-20251001` (~$0.001/event, ~$1.50/month at 50 events/day)
|
||||
- Prompt: "Briefly describe what's happening in this security camera image in 1-2 sentences. Focus on the people, vehicles, or animals and their activity. Be concise."
|
||||
- Max tokens: 150
|
||||
- Fallback: if Claude API fails, use structured description ("Person detected on {camera}")
|
||||
|
||||
## Matrix Message Format
|
||||
|
||||
Two messages per event:
|
||||
|
||||
1. **Image message** (`m.image`): snapshot uploaded to Matrix content repository
|
||||
2. **Text message** (`m.text` with HTML):
|
||||
```
|
||||
[camera_emoji] camera_name - Object - score%
|
||||
AI-generated description of the scene.
|
||||
|
||||
Link: http://192.168.86.192:5000/review?id={event_id}
|
||||
Timestamp
|
||||
```
|
||||
|
||||
**Bot:** `@frigate:hobbs.farm` (new dedicated account)
|
||||
**Room:** `#video-alerts:hobbs.farm` (new room, @jeremy:hobbs.farm invited)
|
||||
|
||||
## Container & Deployment
|
||||
|
||||
**Location:** `compose/hf-pdocker-01/frigate-notify/`
|
||||
|
||||
```
|
||||
compose/hf-pdocker-01/frigate-notify/
|
||||
docker-compose.yml
|
||||
.env
|
||||
Dockerfile
|
||||
app/
|
||||
main.py (~200 lines - MQTT listener, event processor)
|
||||
vision.py (~40 lines - Claude Vision API wrapper)
|
||||
matrix_client.py (~60 lines - image upload + message send)
|
||||
requirements.txt
|
||||
```
|
||||
|
||||
**Container config:**
|
||||
- Image: custom, from `python:3.12-slim`
|
||||
- Network: host
|
||||
- Memory: 256M (memswap_limit: 256M)
|
||||
- Stateless (no persistent storage)
|
||||
- Health endpoint: port 5199, returns JSON status
|
||||
|
||||
**Environment variables (.env):**
|
||||
- `ANTHROPIC_API_KEY`
|
||||
- `MATRIX_ACCESS_TOKEN`
|
||||
- `MATRIX_ROOM_ID`
|
||||
- `FRIGATE_URL=http://192.168.86.192:5000`
|
||||
- `MQTT_HOST=192.168.86.192`
|
||||
- `MQTT_PORT=1883`
|
||||
|
||||
## Error Handling
|
||||
|
||||
- **Claude API failure:** fall back to structured message, never drop alert
|
||||
- **Matrix API failure:** retry 3x with exponential backoff (1s, 2s, 4s)
|
||||
- **Snapshot fetch failure:** send text-only notification
|
||||
- **MQTT disconnect:** paho-mqtt auto-reconnects
|
||||
- **Invalid payload:** log and skip
|
||||
|
||||
## Observability
|
||||
|
||||
- Structured JSON logging to stdout
|
||||
- Health endpoint at `:5199/health` with MQTT status, event count, last event time
|
||||
- Added to Prometheus blackbox probes (http-pdocker01 job)
|
||||
- ContainerDown alert covers it automatically
|
||||
|
||||
## Decisions
|
||||
|
||||
- **Haiku over Sonnet:** 10x cheaper, fast enough for security descriptions
|
||||
- **host network:** Frigate, Mosquitto, Synapse all on LAN IPs, simplest routing
|
||||
- **aiohttp over matrix-nio:** lighter, no crypto dependencies needed for bot usage
|
||||
- **Two messages (image + text):** image renders natively across all Matrix clients
|
||||
- **End events only:** best snapshot quality, avoids duplicate notifications
|
||||
@@ -0,0 +1,713 @@
|
||||
# Frigate Video Alerts Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Build a Python container that listens for Frigate detection events via MQTT, analyzes snapshots with Claude Vision, and posts rich notifications to a Matrix room.
|
||||
|
||||
**Architecture:** A single Python container (`frigate-notify`) on hf-pdocker-01 subscribes to Frigate's MQTT events, filters for person/car/animal detections, fetches snapshots from Frigate's API, sends them to Claude Haiku for natural language description, then posts the snapshot image and description to a dedicated `#video-alerts` Matrix room via a new `@frigate:hobbs.farm` bot.
|
||||
|
||||
**Tech Stack:** Python 3.12, paho-mqtt, anthropic SDK, aiohttp, Docker Compose
|
||||
|
||||
**Design doc:** `docs/plans/2026-03-04-frigate-video-alerts-design.md`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Create Matrix bot account and room
|
||||
|
||||
This task runs on hf-pdocker-01 via SSH. No code files involved.
|
||||
|
||||
**Step 1: Register @frigate:hobbs.farm bot account**
|
||||
|
||||
Use the docker-swarm-ops agent to SSH to hf-pdocker-01 (192.168.86.192) and run:
|
||||
|
||||
```bash
|
||||
# Register the bot via Synapse admin API
|
||||
# First get an admin token (use @alertbot or register directly)
|
||||
docker exec $(docker ps -q -f name=matrix_synapse) register_new_matrix_user \
|
||||
-u frigate \
|
||||
-p "FrigateBot2026!" \
|
||||
-a \
|
||||
--no-admin \
|
||||
-c /data/homeserver.yaml \
|
||||
http://localhost:8008
|
||||
```
|
||||
|
||||
**Step 2: Login to get access token**
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://192.168.86.192:8008/_matrix/client/r0/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"type":"m.login.password","user":"frigate","password":"FrigateBot2026!"}'
|
||||
```
|
||||
|
||||
Save the `access_token` from the response.
|
||||
|
||||
**Step 3: Create #video-alerts room**
|
||||
|
||||
```bash
|
||||
curl -s -X POST "http://192.168.86.192:8008/_matrix/client/r0/createRoom" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer <TOKEN>" \
|
||||
-d '{
|
||||
"room_alias_name": "video-alerts",
|
||||
"name": "Video Alerts",
|
||||
"topic": "Frigate NVR detection alerts with AI descriptions",
|
||||
"visibility": "private",
|
||||
"invite": ["@jeremy:hobbs.farm"]
|
||||
}'
|
||||
```
|
||||
|
||||
Save the `room_id` from the response.
|
||||
|
||||
**Step 4: Send a test message to verify**
|
||||
|
||||
```bash
|
||||
curl -s -X PUT "http://192.168.86.192:8008/_matrix/client/r0/rooms/<ROOM_ID>/send/m.room.message/$(date +%s)" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer <TOKEN>" \
|
||||
-d '{"msgtype":"m.text","body":"Frigate video alerts bot is online."}'
|
||||
```
|
||||
|
||||
**Step 5: Store credentials in vault**
|
||||
|
||||
Use the vault-credential-manager agent to store:
|
||||
- Name: "Frigate Matrix Bot"
|
||||
- Username: @frigate:hobbs.farm
|
||||
- Password: FrigateBot2026!
|
||||
- Notes: access_token=<TOKEN>, room_id=<ROOM_ID>
|
||||
|
||||
**Step 6: Commit** (nothing to commit yet - this is infrastructure setup)
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Create the Python application files
|
||||
|
||||
**Files:**
|
||||
- Create: `compose/hf-pdocker-01/frigate-notify/app/requirements.txt`
|
||||
- Create: `compose/hf-pdocker-01/frigate-notify/app/vision.py`
|
||||
- Create: `compose/hf-pdocker-01/frigate-notify/app/matrix_client.py`
|
||||
- Create: `compose/hf-pdocker-01/frigate-notify/app/main.py`
|
||||
|
||||
**Step 1: Create requirements.txt**
|
||||
|
||||
```
|
||||
compose/hf-pdocker-01/frigate-notify/app/requirements.txt
|
||||
```
|
||||
|
||||
```text
|
||||
paho-mqtt==2.1.0
|
||||
anthropic>=0.40.0
|
||||
aiohttp>=3.9.0
|
||||
```
|
||||
|
||||
**Step 2: Create vision.py - Claude Vision wrapper**
|
||||
|
||||
```
|
||||
compose/hf-pdocker-01/frigate-notify/app/vision.py
|
||||
```
|
||||
|
||||
```python
|
||||
import base64
|
||||
import logging
|
||||
import anthropic
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PROMPT = (
|
||||
"Briefly describe what's happening in this security camera image in 1-2 sentences. "
|
||||
"Focus on the people, vehicles, or animals and their activity. Be concise."
|
||||
)
|
||||
|
||||
|
||||
def analyze_snapshot(client: anthropic.Anthropic, image_bytes: bytes) -> str | None:
|
||||
"""Send a snapshot to Claude Haiku for description. Returns None on failure."""
|
||||
try:
|
||||
b64 = base64.standard_b64encode(image_bytes).decode("utf-8")
|
||||
response = client.messages.create(
|
||||
model="claude-haiku-4-5-20251001",
|
||||
max_tokens=150,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"media_type": "image/jpeg",
|
||||
"data": b64,
|
||||
},
|
||||
},
|
||||
{"type": "text", "text": PROMPT},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
description = response.content[0].text.strip()
|
||||
logger.info("Vision analysis complete: %d chars", len(description))
|
||||
return description
|
||||
except Exception:
|
||||
logger.exception("Claude Vision API failed")
|
||||
return None
|
||||
```
|
||||
|
||||
**Step 3: Create matrix_client.py - Matrix image upload and messaging**
|
||||
|
||||
```
|
||||
compose/hf-pdocker-01/frigate-notify/app/matrix_client.py
|
||||
```
|
||||
|
||||
```python
|
||||
import logging
|
||||
import time
|
||||
import aiohttp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
RETRY_DELAYS = [1, 2, 4]
|
||||
|
||||
|
||||
class MatrixClient:
|
||||
def __init__(self, homeserver: str, token: str, room_id: str):
|
||||
self.homeserver = homeserver.rstrip("/")
|
||||
self.token = token
|
||||
self.room_id = room_id
|
||||
self.headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
async def _request(self, method: str, url: str, **kwargs) -> dict | None:
|
||||
for attempt, delay in enumerate(RETRY_DELAYS):
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.request(
|
||||
method, url, headers=self.headers, **kwargs
|
||||
) as resp:
|
||||
if resp.status < 300:
|
||||
return await resp.json()
|
||||
body = await resp.text()
|
||||
logger.warning(
|
||||
"Matrix API %s (attempt %d): %s", resp.status, attempt + 1, body
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Matrix request failed (attempt %d)", attempt + 1)
|
||||
if attempt < len(RETRY_DELAYS) - 1:
|
||||
await _sleep(delay)
|
||||
logger.error("All Matrix retries exhausted for %s", url)
|
||||
return None
|
||||
|
||||
async def upload_image(self, image_bytes: bytes, filename: str) -> str | None:
|
||||
url = f"{self.homeserver}/_matrix/media/v3/upload?filename={filename}"
|
||||
headers = {**self.headers, "Content-Type": "image/jpeg"}
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url, headers=headers, data=image_bytes) as resp:
|
||||
if resp.status < 300:
|
||||
data = await resp.json()
|
||||
mxc_uri = data.get("content_uri")
|
||||
logger.info("Uploaded image: %s", mxc_uri)
|
||||
return mxc_uri
|
||||
body = await resp.text()
|
||||
logger.error("Image upload failed %s: %s", resp.status, body)
|
||||
except Exception:
|
||||
logger.exception("Image upload failed")
|
||||
return None
|
||||
|
||||
async def send_image(self, mxc_uri: str, filename: str, size: int) -> dict | None:
|
||||
txn_id = str(int(time.time() * 1000))
|
||||
url = f"{self.homeserver}/_matrix/client/r0/rooms/{self.room_id}/send/m.room.message/{txn_id}"
|
||||
body = {
|
||||
"msgtype": "m.image",
|
||||
"body": filename,
|
||||
"url": mxc_uri,
|
||||
"info": {"mimetype": "image/jpeg", "size": size},
|
||||
}
|
||||
return await self._request("PUT", url, json=body)
|
||||
|
||||
async def send_text(self, plain: str, html: str) -> dict | None:
|
||||
txn_id = str(int(time.time() * 1000) + 1)
|
||||
url = f"{self.homeserver}/_matrix/client/r0/rooms/{self.room_id}/send/m.room.message/{txn_id}"
|
||||
body = {
|
||||
"msgtype": "m.text",
|
||||
"body": plain,
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": html,
|
||||
}
|
||||
return await self._request("PUT", url, json=body)
|
||||
|
||||
|
||||
async def _sleep(seconds: float):
|
||||
import asyncio
|
||||
await asyncio.sleep(seconds)
|
||||
```
|
||||
|
||||
**Step 4: Create main.py - MQTT listener and event processor**
|
||||
|
||||
```
|
||||
compose/hf-pdocker-01/frigate-notify/app/main.py
|
||||
```
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from threading import Thread
|
||||
|
||||
import anthropic
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
from vision import analyze_snapshot
|
||||
from matrix_client import MatrixClient
|
||||
|
||||
# --- Configuration ---
|
||||
MQTT_HOST = os.environ.get("MQTT_HOST", "192.168.86.192")
|
||||
MQTT_PORT = int(os.environ.get("MQTT_PORT", "1883"))
|
||||
FRIGATE_URL = os.environ.get("FRIGATE_URL", "http://192.168.86.192:5000")
|
||||
MATRIX_HOMESERVER = os.environ.get("MATRIX_HOMESERVER", "http://192.168.86.192:8008")
|
||||
MATRIX_ACCESS_TOKEN = os.environ["MATRIX_ACCESS_TOKEN"]
|
||||
MATRIX_ROOM_ID = os.environ["MATRIX_ROOM_ID"]
|
||||
HEALTH_PORT = int(os.environ.get("HEALTH_PORT", "5199"))
|
||||
|
||||
ALLOWED_LABELS = {"person", "car", "dog", "cat", "bird"}
|
||||
MIN_SCORE = 0.7
|
||||
RATE_LIMIT_SECONDS = 60
|
||||
DEDUP_WINDOW_SECONDS = 30
|
||||
|
||||
# --- Logging ---
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='{"time":"%(asctime)s","level":"%(levelname)s","module":"%(module)s","message":"%(message)s"}',
|
||||
stream=sys.stdout,
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# --- State ---
|
||||
recent_events: dict[str, float] = {}
|
||||
last_notify_per_camera: defaultdict[str, float] = defaultdict(float)
|
||||
events_processed = 0
|
||||
last_event_time: str | None = None
|
||||
mqtt_connected = False
|
||||
|
||||
|
||||
# --- Health endpoint ---
|
||||
class HealthHandler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if self.path == "/health":
|
||||
body = json.dumps({
|
||||
"status": "ok",
|
||||
"mqtt_connected": mqtt_connected,
|
||||
"events_processed": events_processed,
|
||||
"last_event": last_event_time,
|
||||
})
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(body.encode())
|
||||
else:
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass # suppress access logs
|
||||
|
||||
|
||||
def start_health_server():
|
||||
server = HTTPServer(("0.0.0.0", HEALTH_PORT), HealthHandler)
|
||||
thread = Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
logger.info("Health server started on port %d", HEALTH_PORT)
|
||||
|
||||
|
||||
# --- Snapshot fetching ---
|
||||
async def fetch_snapshot(event_id: str) -> bytes | None:
|
||||
import aiohttp
|
||||
url = f"{FRIGATE_URL}/api/events/{event_id}/snapshot.jpg"
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as resp:
|
||||
if resp.status == 200:
|
||||
data = await resp.read()
|
||||
logger.info("Fetched snapshot for %s: %d bytes", event_id, len(data))
|
||||
return data
|
||||
logger.warning("Snapshot fetch failed for %s: HTTP %d", event_id, resp.status)
|
||||
except Exception:
|
||||
logger.exception("Snapshot fetch error for %s", event_id)
|
||||
return None
|
||||
|
||||
|
||||
# --- Event processing ---
|
||||
async def process_event(event_data: dict, claude: anthropic.Anthropic, matrix: MatrixClient):
|
||||
global events_processed, last_event_time
|
||||
|
||||
event_type = event_data.get("type")
|
||||
if event_type != "end":
|
||||
return
|
||||
|
||||
after = event_data.get("after", {})
|
||||
event_id = after.get("id", "")
|
||||
camera = after.get("camera", "unknown")
|
||||
label = after.get("label", "")
|
||||
score = after.get("top_score", after.get("score", 0))
|
||||
has_snapshot = after.get("has_snapshot", False)
|
||||
|
||||
# --- Filtering ---
|
||||
if label not in ALLOWED_LABELS:
|
||||
return
|
||||
if score < MIN_SCORE:
|
||||
return
|
||||
if not has_snapshot:
|
||||
return
|
||||
|
||||
# Dedup
|
||||
now = time.time()
|
||||
if event_id in recent_events and now - recent_events[event_id] < DEDUP_WINDOW_SECONDS:
|
||||
return
|
||||
recent_events[event_id] = now
|
||||
|
||||
# Rate limit per camera
|
||||
if now - last_notify_per_camera[camera] < RATE_LIMIT_SECONDS:
|
||||
logger.info("Rate limited: %s on %s", label, camera)
|
||||
return
|
||||
last_notify_per_camera[camera] = now
|
||||
|
||||
# Cleanup old dedup entries
|
||||
cutoff = now - DEDUP_WINDOW_SECONDS * 2
|
||||
for eid in [k for k, v in recent_events.items() if v < cutoff]:
|
||||
del recent_events[eid]
|
||||
|
||||
logger.info("Processing event: %s on %s (%s, score=%.2f)", event_id, camera, label, score)
|
||||
|
||||
# --- Fetch snapshot ---
|
||||
snapshot = await fetch_snapshot(event_id)
|
||||
|
||||
# --- AI analysis ---
|
||||
description = None
|
||||
if snapshot:
|
||||
description = analyze_snapshot(claude, snapshot)
|
||||
if not description:
|
||||
description = f"{label.capitalize()} detected on {camera}."
|
||||
|
||||
# --- Format message ---
|
||||
score_pct = int(score * 100)
|
||||
ts = time.strftime("%I:%M:%S %p", time.localtime())
|
||||
date_str = time.strftime("%b %d, %Y", time.localtime())
|
||||
frigate_link = f"{FRIGATE_URL}/review?id={event_id}"
|
||||
|
||||
plain = f"{camera} - {label.capitalize()} - {score_pct}%\n{description}\n\nView in Frigate: {frigate_link}\n{ts} - {date_str}"
|
||||
html = (
|
||||
f"<b>{camera}</b> · {label.capitalize()} · {score_pct}%<br/>"
|
||||
f"{description}<br/><br/>"
|
||||
f'<a href="{frigate_link}">View in Frigate</a><br/>'
|
||||
f"<small>{ts} · {date_str}</small>"
|
||||
)
|
||||
|
||||
# --- Send to Matrix ---
|
||||
if snapshot:
|
||||
filename = f"{camera}_{event_id}.jpg"
|
||||
mxc_uri = await matrix.upload_image(snapshot, filename)
|
||||
if mxc_uri:
|
||||
await matrix.send_image(mxc_uri, filename, len(snapshot))
|
||||
|
||||
await matrix.send_text(plain, html)
|
||||
|
||||
events_processed += 1
|
||||
last_event_time = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
logger.info("Notification sent for %s on %s", label, camera)
|
||||
|
||||
|
||||
# --- MQTT ---
|
||||
def create_mqtt_client(loop: asyncio.AbstractEventLoop, claude: anthropic.Anthropic, matrix: MatrixClient):
|
||||
global mqtt_connected
|
||||
|
||||
def on_connect(client, userdata, flags, reason_code, properties):
|
||||
global mqtt_connected
|
||||
mqtt_connected = True
|
||||
client.subscribe("frigate/events")
|
||||
logger.info("MQTT connected, subscribed to frigate/events")
|
||||
|
||||
def on_disconnect(client, userdata, flags, reason_code, properties):
|
||||
global mqtt_connected
|
||||
mqtt_connected = False
|
||||
logger.warning("MQTT disconnected: %s", reason_code)
|
||||
|
||||
def on_message(client, userdata, msg):
|
||||
try:
|
||||
payload = json.loads(msg.payload)
|
||||
asyncio.run_coroutine_threadsafe(process_event(payload, claude, matrix), loop)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("Invalid JSON on %s", msg.topic)
|
||||
except Exception:
|
||||
logger.exception("Error processing MQTT message")
|
||||
|
||||
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
|
||||
client.on_connect = on_connect
|
||||
client.on_disconnect = on_disconnect
|
||||
client.on_message = on_message
|
||||
return client
|
||||
|
||||
|
||||
# --- Main ---
|
||||
def main():
|
||||
logger.info("Starting frigate-notify")
|
||||
|
||||
claude = anthropic.Anthropic()
|
||||
matrix = MatrixClient(MATRIX_HOMESERVER, MATRIX_ACCESS_TOKEN, MATRIX_ROOM_ID)
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
loop_thread = Thread(target=loop.run_forever, daemon=True)
|
||||
loop_thread.start()
|
||||
|
||||
start_health_server()
|
||||
|
||||
mqtt_client = create_mqtt_client(loop, claude, matrix)
|
||||
mqtt_client.connect(MQTT_HOST, MQTT_PORT)
|
||||
|
||||
def shutdown(signum, frame):
|
||||
logger.info("Shutting down...")
|
||||
mqtt_client.disconnect()
|
||||
loop.call_soon_threadsafe(loop.stop)
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGTERM, shutdown)
|
||||
signal.signal(signal.SIGINT, shutdown)
|
||||
|
||||
mqtt_client.loop_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add compose/hf-pdocker-01/frigate-notify/app/
|
||||
git commit -m "feat: add frigate-notify Python application
|
||||
|
||||
MQTT listener for Frigate events with Claude Vision analysis
|
||||
and Matrix notification delivery."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Create Dockerfile and docker-compose.yml
|
||||
|
||||
**Files:**
|
||||
- Create: `compose/hf-pdocker-01/frigate-notify/Dockerfile`
|
||||
- Create: `compose/hf-pdocker-01/frigate-notify/docker-compose.yml`
|
||||
- Create: `compose/hf-pdocker-01/frigate-notify/.env`
|
||||
|
||||
**Step 1: Create Dockerfile**
|
||||
|
||||
```
|
||||
compose/hf-pdocker-01/frigate-notify/Dockerfile
|
||||
```
|
||||
|
||||
```dockerfile
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY app/requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY app/ .
|
||||
|
||||
EXPOSE 5199
|
||||
|
||||
CMD ["python", "-u", "main.py"]
|
||||
```
|
||||
|
||||
**Step 2: Create docker-compose.yml**
|
||||
|
||||
```
|
||||
compose/hf-pdocker-01/frigate-notify/docker-compose.yml
|
||||
```
|
||||
|
||||
```yaml
|
||||
services:
|
||||
frigate-notify:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: frigate-notify
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
environment:
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
- MATRIX_ACCESS_TOKEN=${MATRIX_ACCESS_TOKEN}
|
||||
- MATRIX_ROOM_ID=${MATRIX_ROOM_ID}
|
||||
- FRIGATE_URL=http://192.168.86.192:5000
|
||||
- MQTT_HOST=192.168.86.192
|
||||
- MQTT_PORT=1883
|
||||
- MATRIX_HOMESERVER=http://192.168.86.192:8008
|
||||
mem_limit: 256M
|
||||
memswap_limit: 256M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -sf http://localhost:5199/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
```
|
||||
|
||||
**Step 3: Create .env file**
|
||||
|
||||
```
|
||||
compose/hf-pdocker-01/frigate-notify/.env
|
||||
```
|
||||
|
||||
```
|
||||
ANTHROPIC_API_KEY=<get from existing netclaw .env or vault>
|
||||
MATRIX_ACCESS_TOKEN=<from Task 1 step 2>
|
||||
MATRIX_ROOM_ID=<from Task 1 step 3>
|
||||
```
|
||||
|
||||
Note: Get the ANTHROPIC_API_KEY from the existing netclaw deployment:
|
||||
```bash
|
||||
ssh glyptic@192.168.86.100 "cat /mnt/docker-data/compose/hf-pdocker-02/netclaw/.env | grep ANTHROPIC"
|
||||
```
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add compose/hf-pdocker-01/frigate-notify/Dockerfile
|
||||
git add compose/hf-pdocker-01/frigate-notify/docker-compose.yml
|
||||
# Do NOT commit .env (contains secrets)
|
||||
git commit -m "feat: add frigate-notify Docker configuration
|
||||
|
||||
Standalone Compose service on hf-pdocker-01 with host networking,
|
||||
256M memory limit, health check on port 5199."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Build and deploy on hf-pdocker-01
|
||||
|
||||
This task uses the docker-swarm-ops agent to SSH to hf-pdocker-01.
|
||||
|
||||
**Step 1: Copy files to hf-pdocker-01**
|
||||
|
||||
SCP the entire `compose/hf-pdocker-01/frigate-notify/` directory to `/mnt/docker-data/compose/hf-pdocker-01/frigate-notify/` on hf-pdocker-01.
|
||||
|
||||
**Step 2: Build the container**
|
||||
|
||||
```bash
|
||||
cd /mnt/docker-data/compose/hf-pdocker-01/frigate-notify
|
||||
docker compose build
|
||||
```
|
||||
|
||||
**Step 3: Start the container**
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**Step 4: Verify container is running**
|
||||
|
||||
```bash
|
||||
docker ps --filter name=frigate-notify --format "table {{.Names}}\t{{.Status}}"
|
||||
```
|
||||
|
||||
Expected: `frigate-notify Up X seconds (health: starting)`
|
||||
|
||||
**Step 5: Check logs for successful startup**
|
||||
|
||||
```bash
|
||||
docker logs frigate-notify --tail 20
|
||||
```
|
||||
|
||||
Expected: Health server started, MQTT connected, subscribed to frigate/events.
|
||||
|
||||
**Step 6: Verify health endpoint**
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:5199/health | python3 -m json.tool
|
||||
```
|
||||
|
||||
Expected: `{"status": "ok", "mqtt_connected": true, "events_processed": 0, "last_event": null}`
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Add monitoring and verify end-to-end
|
||||
|
||||
**Files:**
|
||||
- Modify: `stacks/monitoring/prometheus.yml` - add health probe target
|
||||
|
||||
**Step 1: Add blackbox probe target**
|
||||
|
||||
In `stacks/monitoring/prometheus.yml`, add to the `http-pdocker01` targets list:
|
||||
|
||||
```yaml
|
||||
- http://192.168.86.192:5199/health # Frigate Notify
|
||||
```
|
||||
|
||||
**Step 2: Deploy updated prometheus config**
|
||||
|
||||
SCP to hf-pdocker-01, copy to NAS, hot-reload Prometheus:
|
||||
|
||||
```bash
|
||||
sudo cp /data/monitoring/prometheus.yml /mnt/docker-data/configs/prometheus/prometheus.yml
|
||||
# Reload inside container
|
||||
docker exec $(docker ps -q -f name=monitoring_prometheus) wget -qO- --post-data='' http://localhost:9090/-/reload
|
||||
```
|
||||
|
||||
**Step 3: Trigger a test detection**
|
||||
|
||||
Walk in front of a camera, or wait for an organic detection event. Check logs:
|
||||
|
||||
```bash
|
||||
docker logs frigate-notify --tail 30 -f
|
||||
```
|
||||
|
||||
Expected: Event processing log, snapshot fetch, vision analysis, Matrix notification sent.
|
||||
|
||||
**Step 4: Verify Matrix message received**
|
||||
|
||||
Check `#video-alerts:hobbs.farm` in Matrix for the snapshot image and AI-generated description.
|
||||
|
||||
**Step 5: Store credentials in vault**
|
||||
|
||||
Use vault-credential-manager agent to store the Frigate bot credentials.
|
||||
|
||||
**Step 6: Commit monitoring update**
|
||||
|
||||
```bash
|
||||
git add stacks/monitoring/prometheus.yml
|
||||
git commit -m "feat: add frigate-notify to Prometheus blackbox probes"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Update documentation and memory
|
||||
|
||||
**Step 1: Update MEMORY.md**
|
||||
|
||||
Add entry for frigate-notify service:
|
||||
- Container location, port, purpose
|
||||
- Matrix bot identity and room
|
||||
- Key config notes
|
||||
|
||||
**Step 2: Update CLAUDE.md service distribution**
|
||||
|
||||
Add frigate-notify to hf-pdocker-01 Compose services list.
|
||||
|
||||
**Step 3: Update alert-manager agent**
|
||||
|
||||
Add frigate-notify to the service list and probe targets documentation.
|
||||
|
||||
**Step 4: Commit documentation updates**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "docs: add frigate-notify to infrastructure documentation"
|
||||
```
|
||||
@@ -0,0 +1,182 @@
|
||||
# Farm Manager — Design Document
|
||||
|
||||
**Date**: 2026-03-05
|
||||
**Status**: Approved
|
||||
|
||||
## Overview
|
||||
|
||||
A web-based dashboard for managing Docker services across the 3-node homelab cluster. Provides full control (start/stop/restart/logs/pull) with service grouping for bulk operations.
|
||||
|
||||
## Architecture
|
||||
|
||||
Three components:
|
||||
|
||||
1. **Dashboard** — Static HTML/CSS/JS served by the API server. Dark theme, single page with service cards, group management, and slide-out log viewer.
|
||||
2. **API Server** — Python FastAPI on hf-pdocker-01:8888. Serves frontend, proxies Docker commands to node agents, manages group definitions in JSON.
|
||||
3. **Node Agent** — Python FastAPI on each node:8889. Mounts Docker socket, exposes REST API for container operations. Auto-detects Swarm vs Compose containers.
|
||||
|
||||
```
|
||||
Browser → Dashboard (hf-pdocker-01:8888)
|
||||
↓
|
||||
API Server
|
||||
↙ ↓ ↘
|
||||
Agent Agent Agent
|
||||
(01:8889) (02:8889) (bart:8889)
|
||||
↓ ↓ ↓
|
||||
Docker Docker Docker
|
||||
Socket Socket Socket
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### API Server (hf-pdocker-01:8888)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/nodes` | List all nodes with health status |
|
||||
| GET | `/api/services` | All containers across all nodes |
|
||||
| POST | `/api/services/{node}/{container}/start` | Start container |
|
||||
| POST | `/api/services/{node}/{container}/stop` | Stop container |
|
||||
| POST | `/api/services/{node}/{container}/restart` | Restart container |
|
||||
| GET | `/api/services/{node}/{container}/logs?tail=100` | Stream logs |
|
||||
| POST | `/api/services/{node}/{container}/pull` | Pull latest image |
|
||||
| GET | `/api/groups` | List all groups |
|
||||
| POST | `/api/groups` | Create group |
|
||||
| PUT | `/api/groups/{id}` | Edit group |
|
||||
| DELETE | `/api/groups/{id}` | Delete group |
|
||||
| POST | `/api/groups/{id}/start` | Start all services in group |
|
||||
| POST | `/api/groups/{id}/stop` | Stop all services in group |
|
||||
| POST | `/api/groups/{id}/restart` | Restart all services in group |
|
||||
|
||||
### Node Agent (each node:8889)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/containers` | List all containers with status |
|
||||
| POST | `/containers/{id}/start` | Start |
|
||||
| POST | `/containers/{id}/stop` | Stop |
|
||||
| POST | `/containers/{id}/restart` | Restart |
|
||||
| GET | `/containers/{id}/logs?tail=100` | Get logs |
|
||||
| POST | `/containers/{id}/pull` | Pull latest image |
|
||||
| GET | `/health` | Agent health check |
|
||||
|
||||
## Frontend UI
|
||||
|
||||
- **Sidebar**: Node list with health dots, group list with expand/collapse, "All Services" view, group management button
|
||||
- **Main area**: Grid of service cards (name, status badge, node, image, uptime, action buttons)
|
||||
- **Log viewer**: Slide-out right panel, last 200 lines, auto-scroll
|
||||
- **Group actions**: Toolbar with Start All / Stop All / Restart All, confirmation modal for destructive actions
|
||||
- **Group editor**: Modal with name field and checkbox service list
|
||||
- **Styling**: Dark theme, custom CSS variables, no framework
|
||||
|
||||
## Data Storage
|
||||
|
||||
### Groups (`/mnt/docker-data/configs/farm-manager/groups.json`)
|
||||
|
||||
```json
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"id": "ai-agents",
|
||||
"name": "AI Agents",
|
||||
"services": [
|
||||
{"node": "hf-pdocker-01", "container": "ai-agents_margaret"},
|
||||
{"node": "hf-pdocker-01", "container": "ai-agents_spirit"},
|
||||
{"node": "hf-pdocker-01", "container": "ai-agents_bob"},
|
||||
{"node": "hf-pdocker-01", "container": "ai-agents_clippy"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Node config (`config.json`)
|
||||
|
||||
```json
|
||||
{
|
||||
"nodes": [
|
||||
{"name": "hf-pdocker-01", "host": "192.168.86.192", "agent_port": 8889},
|
||||
{"name": "hf-pdocker-02", "host": "192.168.86.100", "agent_port": 8889},
|
||||
{"name": "bart", "host": "192.168.86.167", "agent_port": 8889}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Default Groups
|
||||
|
||||
| Group | Services |
|
||||
|-------|----------|
|
||||
| AI Agents | margaret, spirit, bob, clippy |
|
||||
| Smart Home | homeassistant, esphome, mosquitto |
|
||||
| Monitoring | grafana, prometheus, alertmanager, influxdb, blackbox-exporter |
|
||||
| Media | jellyfin, sonarr, radarr, sabnzbd, nzbhydra2, jellyseerr |
|
||||
| Matrix | synapse, synapse-db, wellknown |
|
||||
| Guacamole | guacamole, guacd, guacamole-db |
|
||||
| Dev Tools | gitea, gitea-db, semaphore, semaphore-db, wikijs, wikijs-db |
|
||||
| Security | vaultwarden, trivy, openvas |
|
||||
| Network | traefik, adguard, unifi, unifi-db |
|
||||
| Frigate | frigate, frigate-notify |
|
||||
|
||||
## Deployment
|
||||
|
||||
### Node Agent (all 3 nodes)
|
||||
|
||||
- Path: `/mnt/docker-data/compose/{node}/farm-agent/docker-compose.yml`
|
||||
- Image: built once, pushed to registry at 127.0.0.1:5050
|
||||
- Mounts: `/var/run/docker.sock`
|
||||
- Port: 8889
|
||||
- mem_limit: 128M
|
||||
|
||||
### API Server + Dashboard (hf-pdocker-01)
|
||||
|
||||
- Path: `/mnt/docker-data/compose/hf-pdocker-01/farm-manager/docker-compose.yml`
|
||||
- Port: 8888
|
||||
- Config: `/mnt/docker-data/configs/farm-manager/`
|
||||
- mem_limit: 256M
|
||||
|
||||
### Swarm Handling
|
||||
|
||||
Node agent auto-detects Swarm-managed containers and uses `docker service` API for Swarm services, `docker` API for Compose containers.
|
||||
|
||||
## Repo
|
||||
|
||||
- Gitea: `jeremy/farm-manager` (git.hobbs.farm)
|
||||
- Structure:
|
||||
|
||||
```
|
||||
farm-manager/
|
||||
├── agent/
|
||||
│ ├── main.py
|
||||
│ ├── docker_ops.py
|
||||
│ ├── requirements.txt
|
||||
│ └── Dockerfile
|
||||
├── server/
|
||||
│ ├── main.py
|
||||
│ ├── routes/
|
||||
│ ├── static/
|
||||
│ │ ├── index.html
|
||||
│ │ ├── style.css
|
||||
│ │ └── app.js
|
||||
│ ├── requirements.txt
|
||||
│ └── Dockerfile
|
||||
├── tests/
|
||||
│ ├── test_agent.py
|
||||
│ ├── test_server.py
|
||||
│ └── conftest.py
|
||||
├── config.json
|
||||
├── groups.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
| Layer | Scope | Tool |
|
||||
|-------|-------|------|
|
||||
| Unit | Agent Docker operations, group CRUD, API proxy logic | pytest + pytest-asyncio |
|
||||
| Integration | API server ↔ mock agent, group persistence | pytest + httpx TestClient |
|
||||
| Contract | Agent response schemas match dashboard expectations | pydantic models |
|
||||
|
||||
## Monitoring
|
||||
|
||||
- Prometheus blackbox probes: `:8888/health` and `:8889/health` on each node
|
||||
- No auth (internal LAN only)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,242 @@
|
||||
# Frigate-Notify v2 Design
|
||||
|
||||
## Summary
|
||||
|
||||
Evolve frigate-notify from a simple MQTT-to-Matrix alert forwarder into an intelligent notification system with known-object recognition, a web UI for managing identities and reviewing events, and configurable vision backends (local Ollama on a 3090 GPU or Claude Haiku cloud API).
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Reduce noise** — stop alerting on known family members, vehicles, and pets
|
||||
2. **Learn from real events** — tag objects from Frigate detection snapshots to build a recognition database over time
|
||||
3. **Local AI processing** — use a local 3090 GPU (192.168.86.196) for CLIP embeddings and vision LLM, reducing cloud API costs
|
||||
4. **Web interface** — standalone app for managing known objects, reviewing events, and configuring settings
|
||||
5. **Farm Manager integration** — sidebar link in Farm Manager pointing to the frigate-notify UI
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Frigate NVR ──MQTT──> Mosquitto Broker
|
||||
|
|
||||
v
|
||||
frigate-notify v2 (:5199)
|
||||
+-------------------------+
|
||||
| MQTT Listener |
|
||||
| Event Processor |
|
||||
| Web UI (HTML/CSS/JS) |
|
||||
| REST API (FastAPI) |
|
||||
| SQLite DB |
|
||||
+------+------+------+-----+
|
||||
| | |
|
||||
+------------+ | +------------+
|
||||
v v v
|
||||
clip-service (:5200) Ollama (:11434) Claude Haiku API
|
||||
192.168.86.196 192.168.86.196 cloud
|
||||
CLIP embeddings Vision LLM Vision fallback
|
||||
Scene descriptions Scene descriptions
|
||||
| |
|
||||
+-------+-----------+
|
||||
v
|
||||
GPU Machine (RTX 3090, 24GB VRAM)
|
||||
|
||||
frigate-notify v2
|
||||
|
|
||||
v
|
||||
Matrix API
|
||||
#video-alerts room
|
||||
(unknown objects only)
|
||||
|
||||
Farm Manager (:8888) ──sidebar link──> frigate-notify (:5199)
|
||||
```
|
||||
|
||||
## Known Object Recognition Pipeline
|
||||
|
||||
Each Frigate event goes through this flow:
|
||||
|
||||
```
|
||||
Event arrives (MQTT, type="end")
|
||||
-> Fetch snapshot from Frigate API
|
||||
-> Generate CLIP embedding via clip-service (192.168.86.196:5200)
|
||||
-> Compare against stored embeddings in SQLite
|
||||
-> High confidence match (>0.85): "known" -- log quietly, no alert
|
||||
-> Low confidence (<0.60): "unknown" -- send Matrix alert
|
||||
-> Ambiguous (0.60-0.85): send snapshot + reference images to Vision LLM
|
||||
-> LLM confirms match: "known" -- log quietly
|
||||
-> LLM says no match: "unknown" -- send Matrix alert
|
||||
```
|
||||
|
||||
Scene description (the AI-generated text in notifications) is handled separately and is configurable to use either Haiku or Ollama vision model.
|
||||
|
||||
### Fallback Chain
|
||||
|
||||
1. clip-service down: skip embedding matching, send all events to vision LLM
|
||||
2. Ollama down: fall back to Claude Haiku for everything
|
||||
3. Haiku down: structured text description ("Person detected on back_porch")
|
||||
|
||||
### Known Object Types
|
||||
|
||||
- **People** -- tagged by name, matched by face/body appearance
|
||||
- **Vehicles** -- tagged by description (e.g., "blue F-150"), matched by shape/color
|
||||
- **Pets** -- tagged by name, matched by appearance
|
||||
|
||||
### Learning Flow
|
||||
|
||||
1. Events stream in and show up in the web UI's "Recent Events" feed
|
||||
2. User taps an event and chooses "Tag as known" -> enters name/description -> selects type (person/vehicle/pet)
|
||||
3. System stores the CLIP embedding from that snapshot tied to that identity
|
||||
4. Multiple tagged snapshots of the same identity improve matching accuracy over time
|
||||
5. User can also "Confirm" or "Reject" the system's auto-matches to refine it
|
||||
|
||||
### Notification Behavior for Known Objects
|
||||
|
||||
- **Known objects**: logged quietly in the web UI, no Matrix notification
|
||||
- **Unknown objects**: full Matrix alert with snapshot + AI description (existing behavior)
|
||||
|
||||
## Web UI
|
||||
|
||||
Served at `:5199`, vanilla HTML/CSS/JS (consistent with Farm Manager). Same container as the API and event processor.
|
||||
|
||||
### Pages
|
||||
|
||||
| Page | Purpose |
|
||||
|------|---------|
|
||||
| Dashboard | Live feed of recent events. Each shows snapshot thumbnail, camera, label, score, known/unknown status, AI description. Filter by camera, label, status. |
|
||||
| Known Objects | Grid of all tagged identities. Click to see all associated snapshots, edit name, delete. Organized by type (people/vehicles/pets). |
|
||||
| Review Queue | Unreviewed events needing tagging. Quick-action buttons: "Tag as known", "Dismiss", "Confirm match". |
|
||||
| Settings | Vision backend toggle (Haiku/Ollama/both), confidence thresholds, rate limiting, camera enable/disable, Matrix notification preferences. |
|
||||
|
||||
## REST API
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `/api/events` | GET | List recent events, filterable |
|
||||
| `/api/events/{id}` | GET | Single event detail |
|
||||
| `/api/events/{id}/tag` | POST | Tag event as known object |
|
||||
| `/api/events/{id}/dismiss` | POST | Dismiss event |
|
||||
| `/api/known-objects` | GET | List all known identities |
|
||||
| `/api/known-objects/{id}` | GET/PUT/DELETE | Manage a known identity |
|
||||
| `/api/settings` | GET/PUT | Read/update configuration |
|
||||
| `/api/stats` | GET | Event counts, match rates |
|
||||
| `/health` | GET | Existing health endpoint (preserved) |
|
||||
|
||||
## Data Model
|
||||
|
||||
SQLite database at `/mnt/docker-data/configs/frigate-notify/frigate-notify.db`
|
||||
|
||||
### Tables
|
||||
|
||||
```sql
|
||||
known_objects
|
||||
id INTEGER PRIMARY KEY
|
||||
name TEXT NOT NULL -- "Jeremy", "Blue F-150", "Max the dog"
|
||||
type TEXT NOT NULL -- "person", "vehicle", "pet"
|
||||
action TEXT DEFAULT "quiet" -- "quiet" (log only) | "suppress" | "alert"
|
||||
created_at TIMESTAMP
|
||||
updated_at TIMESTAMP
|
||||
|
||||
embeddings
|
||||
id INTEGER PRIMARY KEY
|
||||
known_object_id INTEGER FK -> known_objects
|
||||
embedding BLOB -- CLIP vector (float32 array)
|
||||
snapshot_url TEXT -- Frigate snapshot URL for reference
|
||||
thumbnail BLOB -- Small JPEG for UI display
|
||||
created_at TIMESTAMP
|
||||
|
||||
events
|
||||
id INTEGER PRIMARY KEY
|
||||
frigate_event_id TEXT UNIQUE -- Frigate's event ID
|
||||
camera TEXT
|
||||
label TEXT -- person/car/dog/cat/bird
|
||||
score REAL
|
||||
snapshot BLOB -- JPEG thumbnail for UI
|
||||
embedding BLOB -- CLIP vector for this event
|
||||
match_status TEXT -- "known", "unknown", "ambiguous", "unreviewed"
|
||||
matched_object_id INTEGER FK -> known_objects (nullable)
|
||||
match_confidence REAL
|
||||
ai_description TEXT -- Scene description from Haiku/Ollama
|
||||
notified BOOLEAN DEFAULT 0 -- Was a Matrix alert sent?
|
||||
created_at TIMESTAMP
|
||||
|
||||
settings
|
||||
key TEXT PRIMARY KEY
|
||||
value TEXT -- JSON-encoded value
|
||||
```
|
||||
|
||||
### Storage Estimate
|
||||
|
||||
~50 events/day x 50KB thumbnail = ~2.5MB/day. With 90-day retention and auto-pruning, DB stays under 500MB.
|
||||
|
||||
## Ollama & CLIP Integration
|
||||
|
||||
### clip-service (new sidecar container)
|
||||
|
||||
Runs on 192.168.86.196 alongside Ollama. Lightweight FastAPI app that loads the CLIP model and exposes embedding endpoints.
|
||||
|
||||
```
|
||||
192.168.86.196 (GPU machine, RTX 3090)
|
||||
+-- Ollama (:11434) -- vision LLM (llava:13b)
|
||||
+-- clip-service (:5200) -- CLIP embedding generation
|
||||
POST /embed {image: base64} -> {embedding: [float...]}
|
||||
POST /similarity {embedding1, embedding2} -> {score: 0.0-1.0}
|
||||
```
|
||||
|
||||
- Uses `open_clip` library with ViT-L/14 model
|
||||
- GPU-accelerated, ~2GB VRAM, <50ms per image
|
||||
- Stateless -- frigate-notify stores the embeddings
|
||||
|
||||
### Ollama Vision Models
|
||||
|
||||
Pull on 192.168.86.196:
|
||||
- `llava:13b` -- scene description + ambiguous object matching
|
||||
- ~7.6GB free VRAM after CLIP (3090 has 24GB total)
|
||||
|
||||
### Vision Backend Configuration
|
||||
|
||||
User-configurable in Settings page:
|
||||
- **Scene description backend**: Haiku / Ollama / Ollama-primary-Haiku-fallback
|
||||
- **Recognition backend**: CLIP+Ollama / CLIP+Haiku / CLIP-only
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Python + FastAPI** -- replaces current raw HTTP server, serves API + static files
|
||||
- **SQLite** -- data storage (known objects, embeddings, events, settings)
|
||||
- **Vanilla HTML/CSS/JS** -- web UI, consistent with Farm Manager
|
||||
- **paho-mqtt** -- MQTT listener (kept from v1)
|
||||
- **aiohttp** -- Matrix client (kept from v1)
|
||||
- **open_clip** -- CLIP service sidecar on GPU machine
|
||||
- **numpy** -- embedding similarity calculations
|
||||
|
||||
## Deployment
|
||||
|
||||
- `frigate-notify` container: on same node as Frigate (hf-pdocker-01 when back)
|
||||
- `clip-service` container: on 192.168.86.196 (GPU machine), compose file on NAS
|
||||
- Ollama: already running on 192.168.86.196
|
||||
|
||||
### Compose Location
|
||||
|
||||
- frigate-notify: `/mnt/docker-data/compose/hf-pdocker-01/frigate-notify/`
|
||||
- clip-service: `/mnt/docker-data/compose/gpu-machine/clip-service/`
|
||||
|
||||
### Farm Manager Link
|
||||
|
||||
Add "Frigate Alerts" entry to Farm Manager sidebar config pointing to `http://<frigate-notify-host>:5199`. No code coupling.
|
||||
|
||||
## Migration from v1
|
||||
|
||||
All existing v1 functionality preserved:
|
||||
- MQTT listener and event filtering (labels, scores, dedup, rate limiting)
|
||||
- Frigate snapshot fetching
|
||||
- Claude Haiku vision analysis
|
||||
- Matrix notifications (image + text)
|
||||
- Health endpoint at `/health`
|
||||
|
||||
New features layer on top. The app can run in "v1 mode" (no CLIP service, no known objects) if the GPU machine is offline.
|
||||
|
||||
## Decisions
|
||||
|
||||
- **Standalone app with Farm Manager link** over built-in: independent deployment, no coupling
|
||||
- **Tag from events** over upload reference photos: uses real camera footage, less setup
|
||||
- **Quiet log for known objects** over full suppress: can review routine activity without being interrupted
|
||||
- **Hybrid CLIP + Vision LLM** over either alone: fast matching for clear cases, LLM accuracy for ambiguous
|
||||
- **User-configurable vision backend**: flexibility to use local or cloud, switch as needed
|
||||
- **SQLite** over PostgreSQL: single-file, no extra container, sufficient for this data volume
|
||||
- **Vanilla JS** over React/Vue: consistent with Farm Manager, no build step, simple
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,428 @@
|
||||
# hf-pdocker-01 Rebuild — Phase 1 Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Rebuild hf-pdocker-01 from fresh Ubuntu 24.04 to a fully operational Swarm manager with Frigate NVR, Farm Agent, and Frigate-Notify v2.
|
||||
|
||||
**Architecture:** Fresh Ubuntu 24.04 on NVMe, Docker CE, NFS-backed storage from Synology NAS, 5TB USB disk for Frigate recordings. Docker Swarm re-initialized as single manager with hf-pdocker-02 and bart rejoined as workers. Services deployed via Compose files already on NAS.
|
||||
|
||||
**Tech Stack:** Ubuntu 24.04, Docker CE, Docker Swarm, NFS v3, Intel VA-API/OpenVINO, Python 3.12 (containers)
|
||||
|
||||
**Access:** SSH to hf-pdocker-01 (192.168.86.192), user `glyptic`, password `&Rip8a115`
|
||||
|
||||
**Important:** All tasks execute via SSH from the WSL environment. Use the `docker-swarm-ops` agent for all remote operations.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Install System Prerequisites
|
||||
|
||||
**Target:** hf-pdocker-01 (192.168.86.192)
|
||||
|
||||
**Step 1: Install base packages**
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y nfs-common curl jq git rsync
|
||||
```
|
||||
|
||||
**Step 2: Install Docker CE**
|
||||
|
||||
```bash
|
||||
# Add Docker GPG key and repo
|
||||
sudo install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
sudo chmod a+r /etc/apt/keyrings/docker.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
```
|
||||
|
||||
**Step 3: Add glyptic to docker group**
|
||||
|
||||
```bash
|
||||
sudo usermod -aG docker glyptic
|
||||
```
|
||||
|
||||
**Step 4: Install Intel VA-API drivers for Frigate OpenVINO**
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y intel-media-va-driver-non-free vainfo
|
||||
```
|
||||
|
||||
**Step 5: Verify**
|
||||
|
||||
```bash
|
||||
docker --version # Expect: Docker version 28.x
|
||||
docker compose version # Expect: Docker Compose version v2.x
|
||||
vainfo # Expect: VA-API device listed (iHD driver, HD 630)
|
||||
ls /dev/dri/ # Expect: card0, renderD128
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Configure System Settings
|
||||
|
||||
**Target:** hf-pdocker-01 (192.168.86.192)
|
||||
|
||||
**Step 1: Set vm.swappiness to 10 (cluster standard)**
|
||||
|
||||
```bash
|
||||
echo "vm.swappiness=10" | sudo tee -a /etc/sysctl.conf
|
||||
sudo sysctl vm.swappiness=10
|
||||
```
|
||||
|
||||
**Step 2: Verify**
|
||||
|
||||
```bash
|
||||
sysctl vm.swappiness # Expect: vm.swappiness = 10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Configure NFS Mounts
|
||||
|
||||
**Target:** hf-pdocker-01 (192.168.86.192)
|
||||
|
||||
**Step 1: Create mount points**
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /mnt/docker-data /mnt/infrastructure
|
||||
```
|
||||
|
||||
**Step 2: Add NFS entries to fstab**
|
||||
|
||||
```bash
|
||||
echo '192.168.86.30:/volume1/Docker /mnt/docker-data nfs rw,soft,timeo=300,intr,_netdev,nfsvers=3 0 0' | sudo tee -a /etc/fstab
|
||||
echo '192.168.86.30:/volume1/Infrastructure /mnt/infrastructure nfs rw,hard,intr,_netdev 0 0' | sudo tee -a /etc/fstab
|
||||
```
|
||||
|
||||
**Step 3: Mount and verify**
|
||||
|
||||
```bash
|
||||
sudo mount -a
|
||||
ls /mnt/docker-data/configs/ # Expect: frigate, frigate-notify, etc.
|
||||
ls /mnt/infrastructure/margaret/ # Expect: openclaw.json, credentials/, etc.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Mount 5TB Frigate Recordings Disk
|
||||
|
||||
**Target:** hf-pdocker-01 (192.168.86.192)
|
||||
|
||||
**Step 1: Identify the disk and verify existing data**
|
||||
|
||||
```bash
|
||||
sudo blkid /dev/sda1 # Check filesystem type (expect ext4)
|
||||
sudo mkdir -p /mnt/frigate-recordings
|
||||
sudo mount /dev/sda1 /mnt/frigate-recordings
|
||||
ls /mnt/frigate-recordings/ # Expect: frigate recording directories
|
||||
```
|
||||
|
||||
**Step 2: Add to fstab using UUID for reliability**
|
||||
|
||||
```bash
|
||||
# Get UUID
|
||||
UUID=$(sudo blkid -s UUID -o value /dev/sda1)
|
||||
echo "UUID=$UUID /mnt/frigate-recordings ext4 defaults,_netdev 0 2" | sudo tee -a /etc/fstab
|
||||
```
|
||||
|
||||
**Step 3: Verify**
|
||||
|
||||
```bash
|
||||
df -h /mnt/frigate-recordings # Expect: ~4.5T disk
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Re-initialize Docker Swarm
|
||||
|
||||
**Target:** hf-pdocker-01 (192.168.86.192), then hf-pdocker-02 and bart
|
||||
|
||||
**Step 1: Initialize Swarm on hf-pdocker-01**
|
||||
|
||||
```bash
|
||||
# On hf-pdocker-01:
|
||||
docker swarm init --advertise-addr 192.168.86.192
|
||||
```
|
||||
|
||||
Save the worker join token from the output.
|
||||
|
||||
**Step 2: Clean up stale Swarm state on workers**
|
||||
|
||||
```bash
|
||||
# On hf-pdocker-02 (192.168.86.100):
|
||||
docker swarm leave --force
|
||||
|
||||
# On bart (192.168.86.167):
|
||||
docker swarm leave --force
|
||||
```
|
||||
|
||||
**Step 3: Rejoin workers**
|
||||
|
||||
```bash
|
||||
# On hf-pdocker-02:
|
||||
docker swarm join --token <WORKER_TOKEN> 192.168.86.192:2377
|
||||
|
||||
# On bart:
|
||||
docker swarm join --token <WORKER_TOKEN> 192.168.86.192:2377
|
||||
```
|
||||
|
||||
**Step 4: Verify cluster**
|
||||
|
||||
```bash
|
||||
# On hf-pdocker-01:
|
||||
docker node ls # Expect: 3 nodes, hf-pdocker-01 as Leader/Manager, others as Workers
|
||||
```
|
||||
|
||||
**Step 5: Recreate overlay networks**
|
||||
|
||||
```bash
|
||||
# On hf-pdocker-01:
|
||||
docker network create --driver overlay --attachable traefik-public
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Deploy Frigate NVR
|
||||
|
||||
**Target:** hf-pdocker-01 (192.168.86.192)
|
||||
|
||||
**Step 1: Update Frigate config MQTT address**
|
||||
|
||||
The Frigate config at `/mnt/docker-data/configs/frigate/config/config.yml` has MQTT pointed at `192.168.86.192:1883` (the old hf-pdocker-01 address). Mosquitto is currently running on bart (192.168.86.167) as a failover service.
|
||||
|
||||
```bash
|
||||
# On hf-pdocker-01, edit the config to point MQTT to bart:
|
||||
sed -i 's/host: 192.168.86.192/host: 192.168.86.167/' /mnt/docker-data/configs/frigate/config/config.yml
|
||||
```
|
||||
|
||||
Verify the change:
|
||||
```bash
|
||||
grep -A2 "^mqtt:" /mnt/docker-data/configs/frigate/config/config.yml
|
||||
# Expect: host: 192.168.86.167
|
||||
```
|
||||
|
||||
**Step 2: Deploy Frigate**
|
||||
|
||||
```bash
|
||||
cd /mnt/docker-data/compose/hf-pdocker-01/frigate
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**Step 3: Verify**
|
||||
|
||||
```bash
|
||||
docker ps --filter name=frigate --format "table {{.Names}}\t{{.Status}}" # Expect: Up
|
||||
curl -s http://localhost:5000/api/version # Expect: Frigate version response
|
||||
curl -s http://localhost:5000/api/stats | jq '.detectors' # Expect: openvino detector ~10ms inference
|
||||
```
|
||||
|
||||
Wait 1-2 minutes for cameras to initialize, then check:
|
||||
```bash
|
||||
curl -s http://localhost:5000/api/stats | jq '.cameras | keys' # Expect: list of camera names
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Deploy Farm Agent
|
||||
|
||||
**Target:** hf-pdocker-01 (192.168.86.192)
|
||||
|
||||
The existing farm-agent compose references `127.0.0.1:5050` (local registry, which is gone). We'll build the image directly from Gitea instead.
|
||||
|
||||
**Step 1: Clone farm-manager repo and build agent image**
|
||||
|
||||
```bash
|
||||
cd /tmp
|
||||
git clone http://192.168.86.100:3003/jeremy/farm-manager.git
|
||||
cd farm-manager/agent
|
||||
docker build -t farm-agent:latest .
|
||||
```
|
||||
|
||||
**Step 2: Update compose file to use local image**
|
||||
|
||||
```bash
|
||||
# Edit the compose file on NAS
|
||||
sed -i 's|image: 127.0.0.1:5050/farm-agent:latest|image: farm-agent:latest|' /mnt/docker-data/compose/hf-pdocker-01/farm-agent/docker-compose.yml
|
||||
```
|
||||
|
||||
**Step 3: Deploy**
|
||||
|
||||
```bash
|
||||
cd /mnt/docker-data/compose/hf-pdocker-01/farm-agent
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**Step 4: Verify**
|
||||
|
||||
```bash
|
||||
docker ps --filter name=farm-agent --format "table {{.Names}}\t{{.Status}}"
|
||||
curl -s http://localhost:8889/health # Expect: healthy response
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Deploy Frigate-Notify v2
|
||||
|
||||
**Target:** hf-pdocker-01 (192.168.86.192)
|
||||
|
||||
**Step 1: Clone frigate-notify repo**
|
||||
|
||||
```bash
|
||||
cd /tmp
|
||||
git clone http://192.168.86.100:3003/jeremy/frigate-notify.git
|
||||
```
|
||||
|
||||
**Step 2: Build the image**
|
||||
|
||||
```bash
|
||||
cd /tmp/frigate-notify
|
||||
docker build -t frigate-notify:latest .
|
||||
```
|
||||
|
||||
**Step 3: Create .env file with secrets**
|
||||
|
||||
The compose file needs `ANTHROPIC_API_KEY`, `MATRIX_ACCESS_TOKEN`, and `MATRIX_ROOM_ID`. Retrieve these from Vaultwarden or use known values.
|
||||
|
||||
Known values from MEMORY.md:
|
||||
- `MATRIX_ACCESS_TOKEN`: `syt_ZnJpZ2F0ZQ_uaKUheedpkQBqtCaldsa_2ifSTC` (bot: @frigate:hobbs.farm)
|
||||
- `MATRIX_ROOM_ID`: `!MTsFCQjnmLUNFGJePz:hobbs.farm`
|
||||
- `ANTHROPIC_API_KEY`: retrieve from vault or from bart's ai-agents .env
|
||||
|
||||
```bash
|
||||
cat > /mnt/docker-data/compose/hf-pdocker-01/frigate-notify/.env << 'EOF'
|
||||
ANTHROPIC_API_KEY=<retrieve from vault>
|
||||
MATRIX_ACCESS_TOKEN=syt_ZnJpZ2F0ZQ_uaKUheedpkQBqtCaldsa_2ifSTC
|
||||
MATRIX_ROOM_ID=!MTsFCQjnmLUNFGJePz:hobbs.farm
|
||||
EOF
|
||||
```
|
||||
|
||||
**Step 4: Update compose to use built image instead of build context, and fix IPs**
|
||||
|
||||
The compose file on NAS references `build: context: .` which won't work from NAS path. Update to use the locally built image. Also update MQTT and Matrix homeserver IPs since those services are on bart now.
|
||||
|
||||
Updated compose should have:
|
||||
- `image: frigate-notify:latest` (instead of `build:`)
|
||||
- `FRIGATE_URL=http://localhost:5000` (Frigate is now local on same host)
|
||||
- `MQTT_HOST=192.168.86.167` (Mosquitto on bart)
|
||||
- `MATRIX_HOMESERVER=http://192.168.86.167:8008` (Synapse on bart)
|
||||
|
||||
**Step 5: Deploy**
|
||||
|
||||
```bash
|
||||
cd /mnt/docker-data/compose/hf-pdocker-01/frigate-notify
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**Step 6: Verify**
|
||||
|
||||
```bash
|
||||
docker ps --filter name=frigate-notify --format "table {{.Names}}\t{{.Status}}"
|
||||
curl -s http://localhost:5199/health # Expect: healthy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: Restore Cron Jobs
|
||||
|
||||
**Target:** hf-pdocker-01 (192.168.86.192)
|
||||
|
||||
**Step 1: Verify scripts are accessible on NAS**
|
||||
|
||||
```bash
|
||||
ls -la /mnt/docker-data/scripts/sync-frigate-to-nas.sh
|
||||
ls -la /mnt/docker-data/scripts/cleanup-frigate-media.sh
|
||||
cat /mnt/docker-data/scripts/sync-frigate-to-nas.sh
|
||||
cat /mnt/docker-data/scripts/cleanup-frigate-media.sh
|
||||
```
|
||||
|
||||
**Step 2: Make scripts executable**
|
||||
|
||||
```bash
|
||||
chmod +x /mnt/docker-data/scripts/sync-frigate-to-nas.sh
|
||||
chmod +x /mnt/docker-data/scripts/cleanup-frigate-media.sh
|
||||
```
|
||||
|
||||
**Step 3: Install crontab entries**
|
||||
|
||||
```bash
|
||||
(crontab -l 2>/dev/null; echo "0 2 * * * /mnt/docker-data/scripts/sync-frigate-to-nas.sh") | crontab -
|
||||
(crontab -l 2>/dev/null; echo "0 3 * * 0 /mnt/docker-data/scripts/cleanup-frigate-media.sh") | crontab -
|
||||
```
|
||||
|
||||
**Step 4: Verify**
|
||||
|
||||
```bash
|
||||
crontab -l # Expect: both cron entries listed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 10: Monitoring Integration
|
||||
|
||||
**Target:** hf-pdocker-01 + hf-pdocker-02 (Prometheus)
|
||||
|
||||
**Step 1: Deploy Swarm global services (Telegraf + Node-exporter)**
|
||||
|
||||
On hf-pdocker-01, deploy the monitoring stacks that run globally:
|
||||
|
||||
```bash
|
||||
# Check if the stack files exist on NAS
|
||||
ls /mnt/docker-data/compose/hf-pdocker-01/ # Look for telegraf, node-exporter stacks
|
||||
```
|
||||
|
||||
If Telegraf and Node-exporter were Swarm stacks, redeploy them:
|
||||
```bash
|
||||
# These were global swarm services - redeploy the stacks
|
||||
docker stack deploy -c /path/to/telegraf-stack.yml telegraf
|
||||
docker stack deploy -c /path/to/node-exporter-stack.yml node-exporter
|
||||
```
|
||||
|
||||
**Step 2: Update Prometheus blackbox targets**
|
||||
|
||||
On hf-pdocker-02 (192.168.86.100), ensure Prometheus has blackbox probe targets for the new services:
|
||||
|
||||
Targets to verify/add:
|
||||
- `http://192.168.86.192:5000` (Frigate)
|
||||
- `http://192.168.86.192:5199/health` (Frigate-Notify)
|
||||
- `http://192.168.86.192:8889/health` (Farm Agent)
|
||||
|
||||
**Step 3: Hot-reload Prometheus**
|
||||
|
||||
```bash
|
||||
# On hf-pdocker-02:
|
||||
curl -X POST http://localhost:9095/-/reload
|
||||
```
|
||||
|
||||
**Step 4: Verify in Grafana**
|
||||
|
||||
Check Grafana at http://192.168.86.100:3001 — verify the new targets show as UP in the Prometheus targets page.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 Complete Checklist
|
||||
|
||||
After all tasks, verify:
|
||||
- [ ] Docker running on hf-pdocker-01
|
||||
- [ ] NFS mount at /mnt/docker-data working
|
||||
- [ ] 5TB disk mounted at /mnt/frigate-recordings
|
||||
- [ ] Swarm cluster: 3 nodes (1 manager + 2 workers)
|
||||
- [ ] Frigate running, cameras streaming, OpenVINO detector working
|
||||
- [ ] Farm Agent responding on :8889
|
||||
- [ ] Frigate-Notify v2 healthy on :5199
|
||||
- [ ] Cron jobs installed for sync + cleanup
|
||||
- [ ] Prometheus monitoring all new endpoints
|
||||
- [ ] All blackbox probes green
|
||||
|
||||
## Phase 2 (Future)
|
||||
|
||||
Migrate failover services back to hf-pdocker-01:
|
||||
- Traefik (from hf-pdocker-02)
|
||||
- Monitoring stack (from hf-pdocker-02)
|
||||
- AdGuard Home (from hf-pdocker-02)
|
||||
- Home Assistant, ESPHome, Mosquitto (from bart)
|
||||
- Matrix/Synapse (from bart)
|
||||
- Guacamole (from bart)
|
||||
- AI Agents (from bart)
|
||||
- Farm Manager API (new deploy)
|
||||
Reference in New Issue
Block a user