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:
2026-03-08 18:51:25 -05:00
parent b1d111a011
commit de1d9e1d93
102 changed files with 12634 additions and 3 deletions
@@ -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> &middot; {label.capitalize()} &middot; {score_pct}%<br/>"
f"{description}<br/><br/>"
f'<a href="{frigate_link}">View in Frigate</a><br/>'
f"<small>{ts} &middot; {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)