From d0360c1585964a2aaf7c0ab68d7707f2ced70da9 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 14 Feb 2026 12:39:02 -0800 Subject: [PATCH 01/20] Deploy Frigate NVR stack with Mosquitto, Ntfy, and frigate-notify Add four new services for cloud-free camera recording and alerting: - Mosquitto MQTT broker (shared service in mqtt namespace) - Ntfy push notifications (tailnet-accessible) - Frigate NVR with GableCam via HTTP-FLV, ONNX detection, NFS recordings - frigate-notify bridging detection events to Ntfy Also adds Prometheus scrape target, Grafana dashboard, and Caddy reverse proxy entries for nvr.ops.eblu.me and ntfy.ops.eblu.me. Co-Authored-By: Claude Opus 4.6 --- ansible/roles/caddy/defaults/main.yml | 6 + argocd/apps/frigate.yaml | 18 + argocd/apps/mosquitto.yaml | 18 + argocd/apps/ntfy.yaml | 18 + .../manifests/frigate/configmap-notify.yaml | 32 ++ argocd/manifests/frigate/configmap.yaml | 58 +++ .../manifests/frigate/deployment-notify.yaml | 34 ++ argocd/manifests/frigate/deployment.yaml | 80 +++ argocd/manifests/frigate/external-secret.yaml | 22 + .../manifests/frigate/ingress-tailscale.yaml | 26 + argocd/manifests/frigate/kustomization.yaml | 15 + argocd/manifests/frigate/pv-nfs.yaml | 22 + argocd/manifests/frigate/pvc-database.yaml | 13 + argocd/manifests/frigate/pvc-recordings.yaml | 15 + argocd/manifests/frigate/service.yaml | 19 + .../dashboards/configmap-frigate.yaml | 490 ++++++++++++++++++ .../grafana-config/kustomization.yaml | 1 + argocd/manifests/mosquitto/configmap.yaml | 10 + argocd/manifests/mosquitto/deployment.yaml | 47 ++ argocd/manifests/mosquitto/kustomization.yaml | 8 + argocd/manifests/mosquitto/service.yaml | 13 + argocd/manifests/ntfy/deployment.yaml | 48 ++ argocd/manifests/ntfy/ingress-tailscale.yaml | 26 + argocd/manifests/ntfy/kustomization.yaml | 8 + argocd/manifests/ntfy/service.yaml | 13 + argocd/manifests/prometheus/configmap.yaml | 6 + .../changelog.d/deploy-frigate-nvr.feature.md | 1 + 27 files changed, 1067 insertions(+) create mode 100644 argocd/apps/frigate.yaml create mode 100644 argocd/apps/mosquitto.yaml create mode 100644 argocd/apps/ntfy.yaml create mode 100644 argocd/manifests/frigate/configmap-notify.yaml create mode 100644 argocd/manifests/frigate/configmap.yaml create mode 100644 argocd/manifests/frigate/deployment-notify.yaml create mode 100644 argocd/manifests/frigate/deployment.yaml create mode 100644 argocd/manifests/frigate/external-secret.yaml create mode 100644 argocd/manifests/frigate/ingress-tailscale.yaml create mode 100644 argocd/manifests/frigate/kustomization.yaml create mode 100644 argocd/manifests/frigate/pv-nfs.yaml create mode 100644 argocd/manifests/frigate/pvc-database.yaml create mode 100644 argocd/manifests/frigate/pvc-recordings.yaml create mode 100644 argocd/manifests/frigate/service.yaml create mode 100644 argocd/manifests/grafana-config/dashboards/configmap-frigate.yaml create mode 100644 argocd/manifests/mosquitto/configmap.yaml create mode 100644 argocd/manifests/mosquitto/deployment.yaml create mode 100644 argocd/manifests/mosquitto/kustomization.yaml create mode 100644 argocd/manifests/mosquitto/service.yaml create mode 100644 argocd/manifests/ntfy/deployment.yaml create mode 100644 argocd/manifests/ntfy/ingress-tailscale.yaml create mode 100644 argocd/manifests/ntfy/kustomization.yaml create mode 100644 argocd/manifests/ntfy/service.yaml create mode 100644 docs/changelog.d/deploy-frigate-nvr.feature.md diff --git a/ansible/roles/caddy/defaults/main.yml b/ansible/roles/caddy/defaults/main.yml index 15bdd8a..9e2a7a4 100644 --- a/ansible/roles/caddy/defaults/main.yml +++ b/ansible/roles/caddy/defaults/main.yml @@ -76,6 +76,12 @@ caddy_services: - name: cv host: "cv.{{ caddy_domain }}" backend: "https://cv.tail8d86e.ts.net" + - name: nvr + host: "nvr.{{ caddy_domain }}" + backend: "https://nvr.tail8d86e.ts.net" + - name: ntfy + host: "ntfy.{{ caddy_domain }}" + backend: "https://ntfy.tail8d86e.ts.net" - name: sifaka host: "nas.{{ caddy_domain }}" backend: "http://sifaka:5000" diff --git a/argocd/apps/frigate.yaml b/argocd/apps/frigate.yaml new file mode 100644 index 0000000..a90f412 --- /dev/null +++ b/argocd/apps/frigate.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: frigate + namespace: argocd +spec: + project: default + source: + repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git + targetRevision: main + path: argocd/manifests/frigate + destination: + server: https://kubernetes.default.svc + namespace: frigate + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/argocd/apps/mosquitto.yaml b/argocd/apps/mosquitto.yaml new file mode 100644 index 0000000..976dd5c --- /dev/null +++ b/argocd/apps/mosquitto.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: mosquitto + namespace: argocd +spec: + project: default + source: + repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git + targetRevision: main + path: argocd/manifests/mosquitto + destination: + server: https://kubernetes.default.svc + namespace: mqtt + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/argocd/apps/ntfy.yaml b/argocd/apps/ntfy.yaml new file mode 100644 index 0000000..8846b7f --- /dev/null +++ b/argocd/apps/ntfy.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: ntfy + namespace: argocd +spec: + project: default + source: + repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git + targetRevision: main + path: argocd/manifests/ntfy + destination: + server: https://kubernetes.default.svc + namespace: ntfy + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/argocd/manifests/frigate/configmap-notify.yaml b/argocd/manifests/frigate/configmap-notify.yaml new file mode 100644 index 0000000..0fae36a --- /dev/null +++ b/argocd/manifests/frigate/configmap-notify.yaml @@ -0,0 +1,32 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: frigate-notify-config + namespace: frigate +data: + config.yml: | + frigate: + server: http://frigate:5000 + + alerts: + general: + title: "Frigate Alert" + filters: + labels: + allow: + - person + - car + - dog + - cat + min_score: 0.6 + + notif: + ntfy: + enabled: true + server: http://ntfy.ntfy.svc.cluster.local:80 + topic: frigate-alerts + headers: {} + + mqtt: + server: mosquitto.mqtt.svc.cluster.local + port: 1883 diff --git a/argocd/manifests/frigate/configmap.yaml b/argocd/manifests/frigate/configmap.yaml new file mode 100644 index 0000000..8503885 --- /dev/null +++ b/argocd/manifests/frigate/configmap.yaml @@ -0,0 +1,58 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: frigate-config + namespace: frigate +data: + config.yml: | + mqtt: + host: mosquitto.mqtt.svc.cluster.local + port: 1883 + + go2rtc: + streams: + gablecam: + - "ffmpeg:http://{FRIGATE_CAMERA_USER}:{FRIGATE_CAMERA_PASSWORD}@192.168.1.159/flv?port=1935&app=bcs&stream=channel0_main.bcs#video=copy#audio=copy#audio=opus" + gablecam_sub: + - "ffmpeg:http://{FRIGATE_CAMERA_USER}:{FRIGATE_CAMERA_PASSWORD}@192.168.1.159/flv?port=1935&app=bcs&stream=channel0_sub.bcs" + + cameras: + gablecam: + enabled: true + ffmpeg: + inputs: + - path: rtsp://127.0.0.1:8554/gablecam + input_args: preset-rtsp-restream + roles: [record] + - path: rtsp://127.0.0.1:8554/gablecam_sub + input_args: preset-rtsp-restream + roles: [detect] + detect: + enabled: true + width: 640 + height: 480 + objects: + track: [person, car, dog, cat] + + detectors: + onnx: + type: onnx + + record: + enabled: true + retain: + days: 3 + mode: all + alerts: + retain: + days: 30 + mode: active_objects + detections: + retain: + days: 14 + mode: motion + + snapshots: + enabled: true + retain: + default: 14 diff --git a/argocd/manifests/frigate/deployment-notify.yaml b/argocd/manifests/frigate/deployment-notify.yaml new file mode 100644 index 0000000..6273d71 --- /dev/null +++ b/argocd/manifests/frigate/deployment-notify.yaml @@ -0,0 +1,34 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frigate-notify + namespace: frigate +spec: + replicas: 1 + selector: + matchLabels: + app: frigate-notify + template: + metadata: + labels: + app: frigate-notify + spec: + containers: + - name: frigate-notify + image: ghcr.io/0x2142/frigate-notify:v0.3.5 + volumeMounts: + - name: config + mountPath: /app/config.yml + subPath: config.yml + resources: + requests: + memory: "32Mi" + cpu: "50m" + limits: + memory: "128Mi" + cpu: "100m" + volumes: + - name: config + configMap: + name: frigate-notify-config diff --git a/argocd/manifests/frigate/deployment.yaml b/argocd/manifests/frigate/deployment.yaml new file mode 100644 index 0000000..1a9fc39 --- /dev/null +++ b/argocd/manifests/frigate/deployment.yaml @@ -0,0 +1,80 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frigate + namespace: frigate +spec: + replicas: 1 + selector: + matchLabels: + app: frigate + template: + metadata: + labels: + app: frigate + spec: + containers: + - name: frigate + image: ghcr.io/blakeblackshear/frigate:0.15.1 + ports: + - containerPort: 5000 + name: http + - containerPort: 8554 + name: rtsp + - containerPort: 1984 + name: go2rtc + env: + - name: FRIGATE_CAMERA_USER + valueFrom: + secretKeyRef: + name: frigate-camera + key: username + - name: FRIGATE_CAMERA_PASSWORD + valueFrom: + secretKeyRef: + name: frigate-camera + key: password + volumeMounts: + - name: config + mountPath: /config/config.yml + subPath: config.yml + - name: recordings + mountPath: /media/frigate + - name: database + mountPath: /db + - name: shm + mountPath: /dev/shm + resources: + requests: + memory: "256Mi" + cpu: "200m" + limits: + memory: "2Gi" + cpu: "2000m" + livenessProbe: + httpGet: + path: /api/version + port: 5000 + initialDelaySeconds: 30 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /api/version + port: 5000 + initialDelaySeconds: 15 + periodSeconds: 10 + volumes: + - name: config + configMap: + name: frigate-config + - name: recordings + persistentVolumeClaim: + claimName: frigate-recordings + - name: database + persistentVolumeClaim: + claimName: frigate-database + - name: shm + emptyDir: + medium: Memory + sizeLimit: 256Mi diff --git a/argocd/manifests/frigate/external-secret.yaml b/argocd/manifests/frigate/external-secret.yaml new file mode 100644 index 0000000..abe2c92 --- /dev/null +++ b/argocd/manifests/frigate/external-secret.yaml @@ -0,0 +1,22 @@ +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: frigate-camera + namespace: frigate +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-blumeops + target: + name: frigate-camera + creationPolicy: Owner + data: + - secretKey: username + remoteRef: + key: Reolink Floodlight Camera + property: username + - secretKey: password + remoteRef: + key: Reolink Floodlight Camera + property: password diff --git a/argocd/manifests/frigate/ingress-tailscale.yaml b/argocd/manifests/frigate/ingress-tailscale.yaml new file mode 100644 index 0000000..f814b70 --- /dev/null +++ b/argocd/manifests/frigate/ingress-tailscale.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: frigate-tailscale + namespace: frigate + annotations: + tailscale.com/proxy-class: "default" + tailscale.com/proxy-group: "ingress" + gethomepage.dev/enabled: "true" + gethomepage.dev/name: "NVR" + gethomepage.dev/group: "Infrastructure" + gethomepage.dev/icon: "frigate.png" + gethomepage.dev/description: "Network video recorder" + gethomepage.dev/href: "https://nvr.ops.eblu.me" + gethomepage.dev/pod-selector: "app=frigate" +spec: + ingressClassName: tailscale + defaultBackend: + service: + name: frigate + port: + number: 5000 + tls: + - hosts: + - nvr diff --git a/argocd/manifests/frigate/kustomization.yaml b/argocd/manifests/frigate/kustomization.yaml new file mode 100644 index 0000000..0610eca --- /dev/null +++ b/argocd/manifests/frigate/kustomization.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: frigate +resources: + - external-secret.yaml + - configmap.yaml + - configmap-notify.yaml + - pv-nfs.yaml + - pvc-recordings.yaml + - pvc-database.yaml + - deployment.yaml + - deployment-notify.yaml + - service.yaml + - ingress-tailscale.yaml diff --git a/argocd/manifests/frigate/pv-nfs.yaml b/argocd/manifests/frigate/pv-nfs.yaml new file mode 100644 index 0000000..d3a592b --- /dev/null +++ b/argocd/manifests/frigate/pv-nfs.yaml @@ -0,0 +1,22 @@ +# NFS PersistentVolume for Frigate recordings +# Requires: NFS share on sifaka at /volume1/frigate with NFS permissions for indri +# +# To create on Synology: +# 1. Control Panel > Shared Folder > Create +# 2. Name: frigate, Location: Volume 1 +# 3. Control Panel > File Services > NFS > NFS Rules +# 4. Add rule for "frigate" share: Hostname=indri, Privilege=Read/Write, Squash=No mapping +apiVersion: v1 +kind: PersistentVolume +metadata: + name: frigate-recordings-nfs-pv +spec: + capacity: + storage: 2Ti + accessModes: + - ReadWriteMany + persistentVolumeReclaimPolicy: Retain + storageClassName: "" + nfs: + server: sifaka + path: /volume1/frigate diff --git a/argocd/manifests/frigate/pvc-database.yaml b/argocd/manifests/frigate/pvc-database.yaml new file mode 100644 index 0000000..040bda3 --- /dev/null +++ b/argocd/manifests/frigate/pvc-database.yaml @@ -0,0 +1,13 @@ +# PersistentVolumeClaim for Frigate SQLite database +# Uses minikube's default storage class for local provisioning +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: frigate-database + namespace: frigate +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi diff --git a/argocd/manifests/frigate/pvc-recordings.yaml b/argocd/manifests/frigate/pvc-recordings.yaml new file mode 100644 index 0000000..19c1be8 --- /dev/null +++ b/argocd/manifests/frigate/pvc-recordings.yaml @@ -0,0 +1,15 @@ +# PersistentVolumeClaim for Frigate recordings +# Binds to the NFS PV for sifaka:/volume1/frigate +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: frigate-recordings + namespace: frigate +spec: + accessModes: + - ReadWriteMany + storageClassName: "" + volumeName: frigate-recordings-nfs-pv + resources: + requests: + storage: 2Ti diff --git a/argocd/manifests/frigate/service.yaml b/argocd/manifests/frigate/service.yaml new file mode 100644 index 0000000..28034a4 --- /dev/null +++ b/argocd/manifests/frigate/service.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: frigate + namespace: frigate +spec: + selector: + app: frigate + ports: + - name: http + port: 5000 + targetPort: 5000 + - name: rtsp + port: 8554 + targetPort: 8554 + - name: go2rtc + port: 1984 + targetPort: 1984 diff --git a/argocd/manifests/grafana-config/dashboards/configmap-frigate.yaml b/argocd/manifests/grafana-config/dashboards/configmap-frigate.yaml new file mode 100644 index 0000000..63bd6ca --- /dev/null +++ b/argocd/manifests/grafana-config/dashboards/configmap-frigate.yaml @@ -0,0 +1,490 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-dashboard-frigate + namespace: monitoring + labels: + grafana_dashboard: "1" +data: + frigate.json: | + { + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [ + { + "options": { + "0": { "color": "red", "index": 0, "text": "DOWN" } + }, + "type": "value" + }, + { + "options": { + "1": { "color": "green", "index": 1, "text": "UP" } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "red", "value": null }, + { "color": "green", "value": 1 } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 4, "w": 4, "x": 0, "y": 0 }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "frigate_service_info", + "refId": "A" + } + ], + "title": "Frigate Status", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 15 }, + { "color": "red", "value": 30 } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { "h": 4, "w": 4, "x": 4, "y": 0 }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "frigate_detector_inference_speed_seconds{name=\"onnx\"} * 1000", + "refId": "A" + } + ], + "title": "Detector Inference", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 5 }, + { "color": "red", "value": 10 } + ] + }, + "unit": "fps" + }, + "overrides": [] + }, + "gridPos": { "h": 4, "w": 4, "x": 8, "y": 0 }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "frigate_camera_fps{camera_name=\"gablecam\"}", + "legendFormat": "{{type}}", + "refId": "A" + } + ], + "title": "Camera FPS", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [{ "color": "green", "value": null }] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 4, "w": 4, "x": 12, "y": 0 }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "frigate_detection_total_fps", + "refId": "A" + } + ], + "title": "Detection FPS", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [{ "color": "green", "value": null }] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 }, + "id": 5, + "options": { + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { "mode": "multi", "sort": "desc" } + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "frigate_cpu_usage_percent", + "legendFormat": "{{type}} - {{pid}}", + "refId": "A" + } + ], + "title": "CPU Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [{ "color": "green", "value": null }] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 4 }, + "id": 6, + "options": { + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { "mode": "multi", "sort": "desc" } + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "frigate_mem_usage_percent", + "legendFormat": "{{type}} - {{pid}}", + "refId": "A" + } + ], + "title": "Memory Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [{ "color": "green", "value": null }] + }, + "unit": "fps" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 12 }, + "id": 7, + "options": { + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { "mode": "multi", "sort": "desc" } + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "frigate_camera_fps{camera_name=\"gablecam\"}", + "legendFormat": "{{type}}", + "refId": "A" + } + ], + "title": "Camera FPS Over Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [{ "color": "green", "value": null }] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 12 }, + "id": 8, + "options": { + "legend": { + "calcs": ["lastNotNull"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { "mode": "multi", "sort": "desc" } + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "frigate_storage_used_bytes", + "legendFormat": "{{storage}}", + "refId": "A" + } + ], + "title": "Storage Usage", + "type": "timeseries" + } + ], + "refresh": "30s", + "schemaVersion": 38, + "tags": ["frigate", "nvr", "camera"], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Frigate NVR", + "uid": "frigate", + "version": 1, + "weekStart": "" + } diff --git a/argocd/manifests/grafana-config/kustomization.yaml b/argocd/manifests/grafana-config/kustomization.yaml index fa06560..18deacc 100644 --- a/argocd/manifests/grafana-config/kustomization.yaml +++ b/argocd/manifests/grafana-config/kustomization.yaml @@ -17,6 +17,7 @@ resources: - dashboards/configmap-postgresql.yaml - dashboards/configmap-services.yaml - dashboards/configmap-zot.yaml + - dashboards/configmap-frigate.yaml - dashboards/configmap-cv-apm.yaml - dashboards/configmap-docs-apm.yaml - dashboards/configmap-flyio.yaml diff --git a/argocd/manifests/mosquitto/configmap.yaml b/argocd/manifests/mosquitto/configmap.yaml new file mode 100644 index 0000000..c0cd870 --- /dev/null +++ b/argocd/manifests/mosquitto/configmap.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: mosquitto-config + namespace: mqtt +data: + mosquitto.conf: | + listener 1883 + allow_anonymous true + persistence false diff --git a/argocd/manifests/mosquitto/deployment.yaml b/argocd/manifests/mosquitto/deployment.yaml new file mode 100644 index 0000000..34dbc92 --- /dev/null +++ b/argocd/manifests/mosquitto/deployment.yaml @@ -0,0 +1,47 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mosquitto + namespace: mqtt +spec: + replicas: 1 + selector: + matchLabels: + app: mosquitto + template: + metadata: + labels: + app: mosquitto + spec: + containers: + - name: mosquitto + image: eclipse-mosquitto:2 + ports: + - containerPort: 1883 + name: mqtt + volumeMounts: + - name: config + mountPath: /mosquitto/config/mosquitto.conf + subPath: mosquitto.conf + resources: + requests: + memory: "32Mi" + cpu: "50m" + limits: + memory: "128Mi" + cpu: "100m" + livenessProbe: + tcpSocket: + port: 1883 + initialDelaySeconds: 5 + periodSeconds: 30 + readinessProbe: + tcpSocket: + port: 1883 + initialDelaySeconds: 3 + periodSeconds: 10 + volumes: + - name: config + configMap: + name: mosquitto-config diff --git a/argocd/manifests/mosquitto/kustomization.yaml b/argocd/manifests/mosquitto/kustomization.yaml new file mode 100644 index 0000000..5a9cfa1 --- /dev/null +++ b/argocd/manifests/mosquitto/kustomization.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: mqtt +resources: + - configmap.yaml + - deployment.yaml + - service.yaml diff --git a/argocd/manifests/mosquitto/service.yaml b/argocd/manifests/mosquitto/service.yaml new file mode 100644 index 0000000..7a66aa0 --- /dev/null +++ b/argocd/manifests/mosquitto/service.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: mosquitto + namespace: mqtt +spec: + selector: + app: mosquitto + ports: + - name: mqtt + port: 1883 + targetPort: 1883 diff --git a/argocd/manifests/ntfy/deployment.yaml b/argocd/manifests/ntfy/deployment.yaml new file mode 100644 index 0000000..144e698 --- /dev/null +++ b/argocd/manifests/ntfy/deployment.yaml @@ -0,0 +1,48 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ntfy + namespace: ntfy +spec: + replicas: 1 + selector: + matchLabels: + app: ntfy + template: + metadata: + labels: + app: ntfy + spec: + containers: + - name: ntfy + image: binwiederhier/ntfy:v2.11.0 + args: ["serve"] + ports: + - containerPort: 80 + name: http + volumeMounts: + - name: cache + mountPath: /var/cache/ntfy + resources: + requests: + memory: "32Mi" + cpu: "50m" + limits: + memory: "256Mi" + cpu: "200m" + livenessProbe: + httpGet: + path: /v1/health + port: 80 + initialDelaySeconds: 5 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /v1/health + port: 80 + initialDelaySeconds: 3 + periodSeconds: 10 + volumes: + - name: cache + emptyDir: {} diff --git a/argocd/manifests/ntfy/ingress-tailscale.yaml b/argocd/manifests/ntfy/ingress-tailscale.yaml new file mode 100644 index 0000000..fff1731 --- /dev/null +++ b/argocd/manifests/ntfy/ingress-tailscale.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ntfy-tailscale + namespace: ntfy + annotations: + tailscale.com/proxy-class: "default" + tailscale.com/proxy-group: "ingress" + gethomepage.dev/enabled: "true" + gethomepage.dev/name: "Ntfy" + gethomepage.dev/group: "Infrastructure" + gethomepage.dev/icon: "ntfy.png" + gethomepage.dev/description: "Push notifications" + gethomepage.dev/href: "https://ntfy.ops.eblu.me" + gethomepage.dev/pod-selector: "app=ntfy" +spec: + ingressClassName: tailscale + defaultBackend: + service: + name: ntfy + port: + number: 80 + tls: + - hosts: + - ntfy diff --git a/argocd/manifests/ntfy/kustomization.yaml b/argocd/manifests/ntfy/kustomization.yaml new file mode 100644 index 0000000..3e73104 --- /dev/null +++ b/argocd/manifests/ntfy/kustomization.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: ntfy +resources: + - deployment.yaml + - service.yaml + - ingress-tailscale.yaml diff --git a/argocd/manifests/ntfy/service.yaml b/argocd/manifests/ntfy/service.yaml new file mode 100644 index 0000000..7ed6ffb --- /dev/null +++ b/argocd/manifests/ntfy/service.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: ntfy + namespace: ntfy +spec: + selector: + app: ntfy + ports: + - name: http + port: 80 + targetPort: 80 diff --git a/argocd/manifests/prometheus/configmap.yaml b/argocd/manifests/prometheus/configmap.yaml index 0881d2e..39f18be 100644 --- a/argocd/manifests/prometheus/configmap.yaml +++ b/argocd/manifests/prometheus/configmap.yaml @@ -44,3 +44,9 @@ data: - job_name: "kube-state-metrics" static_configs: - targets: ["kube-state-metrics.monitoring.svc.cluster.local:8080"] + + # Frigate NVR metrics + - job_name: "frigate" + static_configs: + - targets: ["frigate.frigate.svc.cluster.local:5000"] + metrics_path: /api/metrics diff --git a/docs/changelog.d/deploy-frigate-nvr.feature.md b/docs/changelog.d/deploy-frigate-nvr.feature.md new file mode 100644 index 0000000..aa068db --- /dev/null +++ b/docs/changelog.d/deploy-frigate-nvr.feature.md @@ -0,0 +1 @@ +Deploy Frigate NVR stack: Frigate for camera recording/detection, Mosquitto MQTT broker, Ntfy for push notifications, and frigate-notify for detection alerting. GableCam connected via HTTP-FLV with ONNX CPU detection, NFS recordings on sifaka, and Grafana dashboard. -- 2.50.1 (Apple Git-155) From 7b17729085c034dcb19992492a18aff33f9397fb Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 14 Feb 2026 12:53:38 -0800 Subject: [PATCH 02/20] Address PR #190 review feedback - Add bird to tracked objects (catches escaped chickens/ducks) - Add DHCP reservation comment for GableCam IP - Remove explicit detect dimensions (Frigate auto-detects from stream) - Reorganize homepage groups: ArgoCD/Prometheus/PyPI to Infrastructure, CV/Docs/TeslaMate/Transmission to Services Co-Authored-By: Claude Opus 4.6 --- argocd/manifests/argocd/ingress-tailscale.yaml | 2 +- argocd/manifests/cv/ingress-tailscale.yaml | 2 +- argocd/manifests/devpi/ingress-tailscale.yaml | 2 +- argocd/manifests/docs/ingress-tailscale.yaml | 2 +- argocd/manifests/frigate/configmap-notify.yaml | 1 + argocd/manifests/frigate/configmap.yaml | 5 ++--- argocd/manifests/prometheus/ingress-tailscale.yaml | 2 +- argocd/manifests/teslamate/ingress-tailscale.yaml | 2 +- argocd/manifests/torrent/ingress-tailscale.yaml | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/argocd/manifests/argocd/ingress-tailscale.yaml b/argocd/manifests/argocd/ingress-tailscale.yaml index b105904..85393af 100644 --- a/argocd/manifests/argocd/ingress-tailscale.yaml +++ b/argocd/manifests/argocd/ingress-tailscale.yaml @@ -14,7 +14,7 @@ metadata: tailscale.com/proxy-group: "ingress" gethomepage.dev/enabled: "true" gethomepage.dev/name: "ArgoCD" - gethomepage.dev/group: "Misc" + gethomepage.dev/group: "Infrastructure" gethomepage.dev/icon: "argo-cd.png" gethomepage.dev/description: "GitOps CD" gethomepage.dev/href: "https://argocd.ops.eblu.me" diff --git a/argocd/manifests/cv/ingress-tailscale.yaml b/argocd/manifests/cv/ingress-tailscale.yaml index e2d321f..489f95a 100644 --- a/argocd/manifests/cv/ingress-tailscale.yaml +++ b/argocd/manifests/cv/ingress-tailscale.yaml @@ -10,7 +10,7 @@ metadata: tailscale.com/tags: "tag:k8s,tag:flyio-target" gethomepage.dev/enabled: "true" gethomepage.dev/name: "CV" - gethomepage.dev/group: "Misc" + gethomepage.dev/group: "Services" gethomepage.dev/icon: "mdi-file-document" gethomepage.dev/description: "Resume / CV" gethomepage.dev/href: "https://cv.eblu.me" diff --git a/argocd/manifests/devpi/ingress-tailscale.yaml b/argocd/manifests/devpi/ingress-tailscale.yaml index de26d90..474bf72 100644 --- a/argocd/manifests/devpi/ingress-tailscale.yaml +++ b/argocd/manifests/devpi/ingress-tailscale.yaml @@ -8,7 +8,7 @@ metadata: tailscale.com/proxy-group: "ingress" gethomepage.dev/enabled: "true" gethomepage.dev/name: "PyPI" - gethomepage.dev/group: "Misc" + gethomepage.dev/group: "Infrastructure" gethomepage.dev/icon: "pypi.png" gethomepage.dev/description: "PyPI cache" gethomepage.dev/href: "https://pypi.ops.eblu.me" diff --git a/argocd/manifests/docs/ingress-tailscale.yaml b/argocd/manifests/docs/ingress-tailscale.yaml index 4e54360..047e823 100644 --- a/argocd/manifests/docs/ingress-tailscale.yaml +++ b/argocd/manifests/docs/ingress-tailscale.yaml @@ -10,7 +10,7 @@ metadata: tailscale.com/tags: "tag:k8s,tag:flyio-target" gethomepage.dev/enabled: "true" gethomepage.dev/name: "Docs" - gethomepage.dev/group: "Misc" + gethomepage.dev/group: "Services" gethomepage.dev/icon: "mdi-book-open-page-variant" gethomepage.dev/description: "BlumeOps Documentation" gethomepage.dev/href: "https://docs.eblu.me" diff --git a/argocd/manifests/frigate/configmap-notify.yaml b/argocd/manifests/frigate/configmap-notify.yaml index 0fae36a..973f496 100644 --- a/argocd/manifests/frigate/configmap-notify.yaml +++ b/argocd/manifests/frigate/configmap-notify.yaml @@ -18,6 +18,7 @@ data: - car - dog - cat + - bird min_score: 0.6 notif: diff --git a/argocd/manifests/frigate/configmap.yaml b/argocd/manifests/frigate/configmap.yaml index 8503885..a52bd30 100644 --- a/argocd/manifests/frigate/configmap.yaml +++ b/argocd/manifests/frigate/configmap.yaml @@ -11,6 +11,7 @@ data: go2rtc: streams: + # GableCam IP is reserved in UX7 DHCP config gablecam: - "ffmpeg:http://{FRIGATE_CAMERA_USER}:{FRIGATE_CAMERA_PASSWORD}@192.168.1.159/flv?port=1935&app=bcs&stream=channel0_main.bcs#video=copy#audio=copy#audio=opus" gablecam_sub: @@ -29,10 +30,8 @@ data: roles: [detect] detect: enabled: true - width: 640 - height: 480 objects: - track: [person, car, dog, cat] + track: [person, car, dog, cat, bird] detectors: onnx: diff --git a/argocd/manifests/prometheus/ingress-tailscale.yaml b/argocd/manifests/prometheus/ingress-tailscale.yaml index 98bf4e4..7395e09 100644 --- a/argocd/manifests/prometheus/ingress-tailscale.yaml +++ b/argocd/manifests/prometheus/ingress-tailscale.yaml @@ -11,7 +11,7 @@ metadata: tailscale.com/tags: "tag:k8s,tag:flyio-target" gethomepage.dev/enabled: "true" gethomepage.dev/name: "Prometheus" - gethomepage.dev/group: "Misc" + gethomepage.dev/group: "Infrastructure" gethomepage.dev/icon: "prometheus.png" gethomepage.dev/description: "Metrics storage" gethomepage.dev/href: "https://prometheus.ops.eblu.me" diff --git a/argocd/manifests/teslamate/ingress-tailscale.yaml b/argocd/manifests/teslamate/ingress-tailscale.yaml index 7b1b565..dfafb17 100644 --- a/argocd/manifests/teslamate/ingress-tailscale.yaml +++ b/argocd/manifests/teslamate/ingress-tailscale.yaml @@ -8,7 +8,7 @@ metadata: tailscale.com/proxy-group: "ingress" gethomepage.dev/enabled: "true" gethomepage.dev/name: "TeslaMate" - gethomepage.dev/group: "Misc" + gethomepage.dev/group: "Services" gethomepage.dev/icon: "teslamate.png" gethomepage.dev/description: "Tesla data logger" gethomepage.dev/href: "https://tesla.ops.eblu.me" diff --git a/argocd/manifests/torrent/ingress-tailscale.yaml b/argocd/manifests/torrent/ingress-tailscale.yaml index 4d4e924..fe15dd5 100644 --- a/argocd/manifests/torrent/ingress-tailscale.yaml +++ b/argocd/manifests/torrent/ingress-tailscale.yaml @@ -9,7 +9,7 @@ metadata: tailscale.com/proxy-group: "ingress" gethomepage.dev/enabled: "true" gethomepage.dev/name: "Transmission" - gethomepage.dev/group: "Misc" + gethomepage.dev/group: "Services" gethomepage.dev/icon: "transmission.png" gethomepage.dev/description: "Torrent client" gethomepage.dev/href: "https://torrent.ops.eblu.me" -- 2.50.1 (Apple Git-155) From 15c932922d6dd2e1970c885c00857dc99a79d5d2 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 14 Feb 2026 12:55:20 -0800 Subject: [PATCH 03/20] Update homepage layout for new group structure Replace Misc group with Infrastructure and Services in the homepage layout configuration to match the reorganized ingress annotations. Co-Authored-By: Claude Opus 4.6 --- argocd/manifests/homepage/values.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/argocd/manifests/homepage/values.yaml b/argocd/manifests/homepage/values.yaml index c5f1357..7297098 100644 --- a/argocd/manifests/homepage/values.yaml +++ b/argocd/manifests/homepage/values.yaml @@ -199,5 +199,7 @@ config: style: column Content: style: column - Misc: + Infrastructure: + style: column + Services: style: column -- 2.50.1 (Apple Git-155) From 84badbc446e5f2be21812e6a695c9a3baf5d809f Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 14 Feb 2026 12:56:06 -0800 Subject: [PATCH 04/20] Rename hajimari to homepage in Caddy config Co-Authored-By: Claude Opus 4.6 --- ansible/roles/caddy/defaults/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/roles/caddy/defaults/main.yml b/ansible/roles/caddy/defaults/main.yml index 9e2a7a4..ce2235a 100644 --- a/ansible/roles/caddy/defaults/main.yml +++ b/ansible/roles/caddy/defaults/main.yml @@ -67,7 +67,7 @@ caddy_services: - name: navidrome host: "dj.{{ caddy_domain }}" backend: "https://dj.tail8d86e.ts.net" - - name: hajimari + - name: homepage host: "go.{{ caddy_domain }}" backend: "https://go.tail8d86e.ts.net" - name: docs -- 2.50.1 (Apple Git-155) From c76525573336f034c6c5a664ddf4374c8c9316a6 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 14 Feb 2026 13:06:22 -0800 Subject: [PATCH 05/20] Add mosquitto pod check to services-check Co-Authored-By: Claude Opus 4.6 --- mise-tasks/services-check | 1 + 1 file changed, 1 insertion(+) diff --git a/mise-tasks/services-check b/mise-tasks/services-check index 60dac49..075749f 100755 --- a/mise-tasks/services-check +++ b/mise-tasks/services-check @@ -99,6 +99,7 @@ check_service "grafana" "kubectl --context=minikube-indri -n monitoring get pods check_service "miniflux" "kubectl --context=minikube-indri -n miniflux get pods -l app=miniflux -o jsonpath='{.items[0].status.phase}' | grep -q Running" check_service "teslamate" "kubectl --context=minikube-indri -n teslamate get pods -l app=teslamate -o jsonpath='{.items[0].status.phase}' | grep -q Running" check_service "blumeops-pg" "kubectl --context=minikube-indri -n databases get pods -l cnpg.io/cluster=blumeops-pg -o jsonpath='{.items[0].status.phase}' | grep -q Running" +check_service "mosquitto" "kubectl --context=minikube-indri -n mqtt get pods -l app=mosquitto -o jsonpath='{.items[0].status.phase}' | grep -q Running" echo "" echo "ArgoCD app sync status:" -- 2.50.1 (Apple Git-155) From 7cdd7c71966b33406489a41bc6031b362220aef8 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 14 Feb 2026 13:09:53 -0800 Subject: [PATCH 06/20] Add ntfy checks to services-check Co-Authored-By: Claude Opus 4.6 --- mise-tasks/services-check | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mise-tasks/services-check b/mise-tasks/services-check index 075749f..b210df3 100755 --- a/mise-tasks/services-check +++ b/mise-tasks/services-check @@ -80,6 +80,7 @@ check_http "Transmission" "https://torrent.ops.eblu.me/" check_http "Immich" "https://photos.ops.eblu.me/" check_http "Navidrome" "https://dj.ops.eblu.me/" check_http "CV" "https://cv.ops.eblu.me/" +check_http "Ntfy" "https://ntfy.tail8d86e.ts.net/v1/health" echo "" echo "Public services (via Fly.io):" @@ -100,6 +101,7 @@ check_service "miniflux" "kubectl --context=minikube-indri -n miniflux get pods check_service "teslamate" "kubectl --context=minikube-indri -n teslamate get pods -l app=teslamate -o jsonpath='{.items[0].status.phase}' | grep -q Running" check_service "blumeops-pg" "kubectl --context=minikube-indri -n databases get pods -l cnpg.io/cluster=blumeops-pg -o jsonpath='{.items[0].status.phase}' | grep -q Running" check_service "mosquitto" "kubectl --context=minikube-indri -n mqtt get pods -l app=mosquitto -o jsonpath='{.items[0].status.phase}' | grep -q Running" +check_service "ntfy" "kubectl --context=minikube-indri -n ntfy get pods -l app=ntfy -o jsonpath='{.items[0].status.phase}' | grep -q Running" echo "" echo "ArgoCD app sync status:" -- 2.50.1 (Apple Git-155) From efc71c9f22e461c501740ed32723e09887dd58a5 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 14 Feb 2026 13:24:56 -0800 Subject: [PATCH 07/20] Add upstream relay config to ntfy for instant iOS push notifications Configures ntfy to forward poll requests through ntfy.sh for APNs delivery. Without this, iOS delays notifications by 20-30+ minutes. Free tier allows 250 messages/day (no account needed). Co-Authored-By: Claude Opus 4.6 --- argocd/manifests/ntfy/configmap.yaml | 9 +++++++++ argocd/manifests/ntfy/deployment.yaml | 8 +++++++- argocd/manifests/ntfy/kustomization.yaml | 1 + 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 argocd/manifests/ntfy/configmap.yaml diff --git a/argocd/manifests/ntfy/configmap.yaml b/argocd/manifests/ntfy/configmap.yaml new file mode 100644 index 0000000..a205db1 --- /dev/null +++ b/argocd/manifests/ntfy/configmap.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: ntfy-config + namespace: ntfy +data: + server.yml: | + base-url: https://ntfy.ops.eblu.me + upstream-base-url: https://ntfy.sh diff --git a/argocd/manifests/ntfy/deployment.yaml b/argocd/manifests/ntfy/deployment.yaml index 144e698..90c1f11 100644 --- a/argocd/manifests/ntfy/deployment.yaml +++ b/argocd/manifests/ntfy/deployment.yaml @@ -17,11 +17,14 @@ spec: containers: - name: ntfy image: binwiederhier/ntfy:v2.11.0 - args: ["serve"] + args: ["serve", "--config", "/etc/ntfy/server.yml"] ports: - containerPort: 80 name: http volumeMounts: + - name: config + mountPath: /etc/ntfy/server.yml + subPath: server.yml - name: cache mountPath: /var/cache/ntfy resources: @@ -44,5 +47,8 @@ spec: initialDelaySeconds: 3 periodSeconds: 10 volumes: + - name: config + configMap: + name: ntfy-config - name: cache emptyDir: {} diff --git a/argocd/manifests/ntfy/kustomization.yaml b/argocd/manifests/ntfy/kustomization.yaml index 3e73104..851171f 100644 --- a/argocd/manifests/ntfy/kustomization.yaml +++ b/argocd/manifests/ntfy/kustomization.yaml @@ -3,6 +3,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: ntfy resources: + - configmap.yaml - deployment.yaml - service.yaml - ingress-tailscale.yaml -- 2.50.1 (Apple Git-155) From 74a572084cca8f4851335ced51a0f9e65003b316 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 14 Feb 2026 13:30:59 -0800 Subject: [PATCH 08/20] Switch Frigate detector from ONNX to CPU (TFLite) ONNX detector was crashing due to missing model path config. CPU/TFLite works out of the box on ARM64 and is sufficient for single-camera detection of large objects. Co-Authored-By: Claude Opus 4.6 --- argocd/manifests/frigate/configmap.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/argocd/manifests/frigate/configmap.yaml b/argocd/manifests/frigate/configmap.yaml index a52bd30..8039826 100644 --- a/argocd/manifests/frigate/configmap.yaml +++ b/argocd/manifests/frigate/configmap.yaml @@ -34,8 +34,8 @@ data: track: [person, car, dog, cat, bird] detectors: - onnx: - type: onnx + cpu: + type: cpu record: enabled: true -- 2.50.1 (Apple Git-155) From 0a871a40b4d2333bfe1d4e41b87aa7a0b935949f Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 14 Feb 2026 13:37:25 -0800 Subject: [PATCH 09/20] Switch go2rtc streams from HTTP-FLV to RTSP Camera had HTTP/RTMP disabled. RTSP is the Frigate-recommended protocol for ReoLink cameras. Co-Authored-By: Claude Opus 4.6 --- argocd/manifests/frigate/configmap.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/argocd/manifests/frigate/configmap.yaml b/argocd/manifests/frigate/configmap.yaml index 8039826..c7b221f 100644 --- a/argocd/manifests/frigate/configmap.yaml +++ b/argocd/manifests/frigate/configmap.yaml @@ -13,9 +13,9 @@ data: streams: # GableCam IP is reserved in UX7 DHCP config gablecam: - - "ffmpeg:http://{FRIGATE_CAMERA_USER}:{FRIGATE_CAMERA_PASSWORD}@192.168.1.159/flv?port=1935&app=bcs&stream=channel0_main.bcs#video=copy#audio=copy#audio=opus" + - "rtsp://{FRIGATE_CAMERA_USER}:{FRIGATE_CAMERA_PASSWORD}@192.168.1.159:554/h264Preview_01_main" gablecam_sub: - - "ffmpeg:http://{FRIGATE_CAMERA_USER}:{FRIGATE_CAMERA_PASSWORD}@192.168.1.159/flv?port=1935&app=bcs&stream=channel0_sub.bcs" + - "rtsp://{FRIGATE_CAMERA_USER}:{FRIGATE_CAMERA_PASSWORD}@192.168.1.159:554/h264Preview_01_sub" cameras: gablecam: -- 2.50.1 (Apple Git-155) From ee141edeb0f7a005a8470f32ababdc05152e8395 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 14 Feb 2026 13:41:18 -0800 Subject: [PATCH 10/20] Fix frigate-notify config structure Use MQTT-only event collection (disable webapi), fix ntfy alert config nesting to match frigate-notify's expected format. Co-Authored-By: Claude Opus 4.6 --- .../manifests/frigate/configmap-notify.yaml | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/argocd/manifests/frigate/configmap-notify.yaml b/argocd/manifests/frigate/configmap-notify.yaml index 973f496..7f26f75 100644 --- a/argocd/manifests/frigate/configmap-notify.yaml +++ b/argocd/manifests/frigate/configmap-notify.yaml @@ -8,26 +8,21 @@ data: frigate: server: http://frigate:5000 + webapi: + enabled: false + + mqtt: + enabled: true + server: mosquitto.mqtt.svc.cluster.local + port: 1883 + clientid: frigate-notify + topic_prefix: frigate + alerts: general: title: "Frigate Alert" - filters: - labels: - allow: - - person - - car - - dog - - cat - - bird - min_score: 0.6 - notif: ntfy: enabled: true server: http://ntfy.ntfy.svc.cluster.local:80 topic: frigate-alerts - headers: {} - - mqtt: - server: mosquitto.mqtt.svc.cluster.local - port: 1883 -- 2.50.1 (Apple Git-155) From a3e19198a245e2ea25758eb4681ad26d546bccd8 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 14 Feb 2026 13:47:21 -0800 Subject: [PATCH 11/20] Enable attachments on ntfy for snapshot images frigate-notify sends detection snapshots as attachments, which requires ntfy to have attachment support configured. Co-Authored-By: Claude Opus 4.6 --- argocd/manifests/ntfy/configmap.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/argocd/manifests/ntfy/configmap.yaml b/argocd/manifests/ntfy/configmap.yaml index a205db1..584eba1 100644 --- a/argocd/manifests/ntfy/configmap.yaml +++ b/argocd/manifests/ntfy/configmap.yaml @@ -7,3 +7,7 @@ data: server.yml: | base-url: https://ntfy.ops.eblu.me upstream-base-url: https://ntfy.sh + attachment-cache-dir: /var/cache/ntfy/attachments + attachment-total-size-limit: 1G + attachment-file-size-limit: 10M + attachment-expiry-duration: 24h -- 2.50.1 (Apple Git-155) From 020c525ad83eee79f39d1a7375b0a482c597c0b0 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 14 Feb 2026 13:51:10 -0800 Subject: [PATCH 12/20] Add public_url to frigate-notify for notification links Clip/snapshot links in notifications were using the internal cluster URL (frigate:5000). Set public_url to nvr.ops.eblu.me so links work from phones. Co-Authored-By: Claude Opus 4.6 --- argocd/manifests/frigate/configmap-notify.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/argocd/manifests/frigate/configmap-notify.yaml b/argocd/manifests/frigate/configmap-notify.yaml index 7f26f75..f72951d 100644 --- a/argocd/manifests/frigate/configmap-notify.yaml +++ b/argocd/manifests/frigate/configmap-notify.yaml @@ -7,6 +7,7 @@ data: config.yml: | frigate: server: http://frigate:5000 + public_url: https://nvr.ops.eblu.me webapi: enabled: false -- 2.50.1 (Apple Git-155) From 0e431d921e791191239bb09c71964d05946b968e Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 14 Feb 2026 14:02:10 -0800 Subject: [PATCH 13/20] Add View Event action button to ntfy notifications Uses frigate-notify's EventLink template variable with ntfy's X-Actions header to link to the Frigate event page, which has a built-in player that handles H.265 transcoding. Co-Authored-By: Claude Opus 4.6 --- argocd/manifests/frigate/configmap-notify.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/argocd/manifests/frigate/configmap-notify.yaml b/argocd/manifests/frigate/configmap-notify.yaml index f72951d..b1ce6a3 100644 --- a/argocd/manifests/frigate/configmap-notify.yaml +++ b/argocd/manifests/frigate/configmap-notify.yaml @@ -27,3 +27,5 @@ data: enabled: true server: http://ntfy.ntfy.svc.cluster.local:80 topic: frigate-alerts + headers: + - "X-Actions: view, View Event, {{.Extra.EventLink}}" -- 2.50.1 (Apple Git-155) From 79e5673fb83a39fb1b2ec92e1718d6377489b66e Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 14 Feb 2026 14:06:31 -0800 Subject: [PATCH 14/20] Replace View Clip button with Open Event and Open Camera buttons View Clip linked to raw H.265 MP4 which doesn't play in browsers. Open Event links to Frigate's review page (built-in player handles transcoding), Open Camera links to the live camera view. Co-Authored-By: Claude Opus 4.6 --- argocd/manifests/frigate/configmap-notify.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/argocd/manifests/frigate/configmap-notify.yaml b/argocd/manifests/frigate/configmap-notify.yaml index b1ce6a3..735ac95 100644 --- a/argocd/manifests/frigate/configmap-notify.yaml +++ b/argocd/manifests/frigate/configmap-notify.yaml @@ -28,4 +28,4 @@ data: server: http://ntfy.ntfy.svc.cluster.local:80 topic: frigate-alerts headers: - - "X-Actions: view, View Event, {{.Extra.EventLink}}" + - "X-Actions: view, Open Event, {{.Extra.PublicURL}}/review?id={{.ID}}, clear=true; view, Open Camera, {{.Extra.PublicURL}}#/cameras/{{.Camera}}" -- 2.50.1 (Apple Git-155) From 6ac250531754cc1255dd613c2fce5745e05ab761 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 14 Feb 2026 14:17:50 -0800 Subject: [PATCH 15/20] Fix ntfy X-Actions header YAML format from string to map The Go type for headers is []map[string]string, so the YAML entry must be a map (- Key: "value") not a quoted string (- "Key: value"). The string format silently failed unmarshaling, causing the default "View Clip" button to always appear instead of custom actions. Also fix camera URL path (added / before # fragment). Co-Authored-By: Claude Opus 4.6 --- argocd/manifests/frigate/configmap-notify.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/argocd/manifests/frigate/configmap-notify.yaml b/argocd/manifests/frigate/configmap-notify.yaml index 735ac95..ed357ad 100644 --- a/argocd/manifests/frigate/configmap-notify.yaml +++ b/argocd/manifests/frigate/configmap-notify.yaml @@ -28,4 +28,4 @@ data: server: http://ntfy.ntfy.svc.cluster.local:80 topic: frigate-alerts headers: - - "X-Actions: view, Open Event, {{.Extra.PublicURL}}/review?id={{.ID}}, clear=true; view, Open Camera, {{.Extra.PublicURL}}#/cameras/{{.Camera}}" + - X-Actions: "view, Open Event, {{.Extra.PublicURL}}/review?id={{.ID}}, clear=true; view, Open Camera, {{.Extra.PublicURL}}/#/cameras/{{.Camera}}" -- 2.50.1 (Apple Git-155) From 256ba7658ef103670d21a1dabd6bdace531b6bd7 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 14 Feb 2026 20:15:49 -0800 Subject: [PATCH 16/20] Add frigate and frigate-notify to services-check Also update ntfy health check URL from Tailscale to Caddy. Co-Authored-By: Claude Opus 4.6 --- mise-tasks/services-check | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mise-tasks/services-check b/mise-tasks/services-check index b210df3..020e177 100755 --- a/mise-tasks/services-check +++ b/mise-tasks/services-check @@ -80,7 +80,8 @@ check_http "Transmission" "https://torrent.ops.eblu.me/" check_http "Immich" "https://photos.ops.eblu.me/" check_http "Navidrome" "https://dj.ops.eblu.me/" check_http "CV" "https://cv.ops.eblu.me/" -check_http "Ntfy" "https://ntfy.tail8d86e.ts.net/v1/health" +check_http "Ntfy" "https://ntfy.ops.eblu.me/v1/health" +check_http "Frigate" "https://nvr.ops.eblu.me/api/version" echo "" echo "Public services (via Fly.io):" @@ -102,6 +103,8 @@ check_service "teslamate" "kubectl --context=minikube-indri -n teslamate get pod check_service "blumeops-pg" "kubectl --context=minikube-indri -n databases get pods -l cnpg.io/cluster=blumeops-pg -o jsonpath='{.items[0].status.phase}' | grep -q Running" check_service "mosquitto" "kubectl --context=minikube-indri -n mqtt get pods -l app=mosquitto -o jsonpath='{.items[0].status.phase}' | grep -q Running" check_service "ntfy" "kubectl --context=minikube-indri -n ntfy get pods -l app=ntfy -o jsonpath='{.items[0].status.phase}' | grep -q Running" +check_service "frigate" "kubectl --context=minikube-indri -n frigate get pods -l app=frigate -o jsonpath='{.items[0].status.phase}' | grep -q Running" +check_service "frigate-notify" "kubectl --context=minikube-indri -n frigate get pods -l app=frigate-notify -o jsonpath='{.items[0].status.phase}' | grep -q Running" echo "" echo "ArgoCD app sync status:" -- 2.50.1 (Apple Git-155) From 95f9294fb6f82807ae3d1f3452780ea61bb1f61d Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 14 Feb 2026 20:25:46 -0800 Subject: [PATCH 17/20] Add stationary object timeout to stop alerts on parked cars Cars stop being tracked after ~30s stationary (150 frames at 5fps). Other objects get ~5 minutes (1500 frames) before being dropped. Co-Authored-By: Claude Opus 4.6 --- argocd/manifests/frigate/configmap.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/argocd/manifests/frigate/configmap.yaml b/argocd/manifests/frigate/configmap.yaml index c7b221f..fa20327 100644 --- a/argocd/manifests/frigate/configmap.yaml +++ b/argocd/manifests/frigate/configmap.yaml @@ -30,6 +30,11 @@ data: roles: [detect] detect: enabled: true + stationary: + max_frames: + default: 1500 + objects: + car: 150 objects: track: [person, car, dog, cat, bird] -- 2.50.1 (Apple Git-155) From f46806b45819edf10f296284579cf3cf086cfb52 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 14 Feb 2026 20:41:11 -0800 Subject: [PATCH 18/20] Upgrade Frigate from 0.15.1 to 0.16.4-standard-arm64 Switches to native ARM64 image (was likely running under Rosetta/QEMU). No config breaking changes for our setup (CPU detector, no audio, no TensorRT/ROCm). Co-Authored-By: Claude Opus 4.6 --- argocd/manifests/frigate/deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/argocd/manifests/frigate/deployment.yaml b/argocd/manifests/frigate/deployment.yaml index 1a9fc39..34c50ef 100644 --- a/argocd/manifests/frigate/deployment.yaml +++ b/argocd/manifests/frigate/deployment.yaml @@ -16,7 +16,7 @@ spec: spec: containers: - name: frigate - image: ghcr.io/blakeblackshear/frigate:0.15.1 + image: ghcr.io/blakeblackshear/frigate:0.16.4-standard-arm64 ports: - containerPort: 5000 name: http -- 2.50.1 (Apple Git-155) From a153843d7ea608e9090733ce6f292b16c8b51192 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 14 Feb 2026 21:20:12 -0800 Subject: [PATCH 19/20] Switch Frigate detector from CPU/TFLite to ONNX with YOLO-NAS-s Uses YOLO-NAS-s model exported at 320x320 resolution, stored on NFS at /media/frigate/models/yolo_nas_s.onnx. Significantly better detection accuracy than the default SSD MobileNet V1. Co-Authored-By: Claude Opus 4.6 --- argocd/manifests/frigate/configmap.yaml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/argocd/manifests/frigate/configmap.yaml b/argocd/manifests/frigate/configmap.yaml index fa20327..629f8ee 100644 --- a/argocd/manifests/frigate/configmap.yaml +++ b/argocd/manifests/frigate/configmap.yaml @@ -39,8 +39,17 @@ data: track: [person, car, dog, cat, bird] detectors: - cpu: - type: cpu + onnx: + type: onnx + + model: + model_type: yolonas + width: 320 + height: 320 + input_tensor: nchw + input_pixel_format: bgr + path: /media/frigate/models/yolo_nas_s.onnx + labelmap_path: /labelmap/coco-80.txt record: enabled: true -- 2.50.1 (Apple Git-155) From 4cc6759938cf7cc15cb68d058e91e82a2c06193e Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 14 Feb 2026 21:26:39 -0800 Subject: [PATCH 20/20] Move ReoLink camera plan to completed, update changelog Plan verified and marked complete with all checklist items checked (except IoT VLAN isolation which is a separate plan). Updated open questions with resolved decisions. Updated changelog fragment to reflect full scope of deployment. Co-Authored-By: Claude Opus 4.6 --- .../changelog.d/deploy-frigate-nvr.feature.md | 2 +- docs/how-to/plans/completed/completed.md | 1 + .../operationalize-reolink-camera.md | 33 ++++++++++--------- 3 files changed, 19 insertions(+), 17 deletions(-) rename docs/how-to/plans/{ => completed}/operationalize-reolink-camera.md (88%) diff --git a/docs/changelog.d/deploy-frigate-nvr.feature.md b/docs/changelog.d/deploy-frigate-nvr.feature.md index aa068db..f5f3960 100644 --- a/docs/changelog.d/deploy-frigate-nvr.feature.md +++ b/docs/changelog.d/deploy-frigate-nvr.feature.md @@ -1 +1 @@ -Deploy Frigate NVR stack: Frigate for camera recording/detection, Mosquitto MQTT broker, Ntfy for push notifications, and frigate-notify for detection alerting. GableCam connected via HTTP-FLV with ONNX CPU detection, NFS recordings on sifaka, and Grafana dashboard. +Deploy cloud-free NVR stack: Frigate 0.16.4 (ARM64) with ONNX/YOLO-NAS-s detection, Mosquitto MQTT broker, Ntfy self-hosted push notifications (with iOS APNs relay), and frigate-notify for detection alerting. GableCam (ReoLink Elite Floodlight) connected via RTSP with NFS recordings on sifaka, Grafana dashboard, Prometheus scraping, Homepage integration, and Caddy reverse proxies at nvr.ops.eblu.me and ntfy.ops.eblu.me. diff --git a/docs/how-to/plans/completed/completed.md b/docs/how-to/plans/completed/completed.md index 6f05a9b..3c349d1 100644 --- a/docs/how-to/plans/completed/completed.md +++ b/docs/how-to/plans/completed/completed.md @@ -14,3 +14,4 @@ Plans that have been fully implemented and verified. Kept for historical referen |------|-----------|-------------| | [[adopt-dagger-ci]] | 2026-02-11 | Adopt Dagger as CI/CD build engine (Phases 1–3) | | [[segment-home-network]] | 2026-02-14 | Manual three-network segmentation for UniFi Express 7 | +| [[operationalize-reolink-camera]] | 2026-02-15 | Deploy Frigate NVR stack with Mosquitto, Ntfy, and frigate-notify | diff --git a/docs/how-to/plans/operationalize-reolink-camera.md b/docs/how-to/plans/completed/operationalize-reolink-camera.md similarity index 88% rename from docs/how-to/plans/operationalize-reolink-camera.md rename to docs/how-to/plans/completed/operationalize-reolink-camera.md index 1ef26b6..f82ead3 100644 --- a/docs/how-to/plans/operationalize-reolink-camera.md +++ b/docs/how-to/plans/completed/operationalize-reolink-camera.md @@ -11,8 +11,9 @@ tags: # Plan: Operationalize ReoLink Camera -> **Status:** Planned (not yet executed) +> **Status:** Completed (2026-02-15) > **Depends on:** [[add-unifi-pulumi-stack]] — the camera must be on the IoT VLAN, isolated from the rest of the network. +> **PR:** #190 ## Background @@ -241,23 +242,23 @@ Camera settings to apply: enable RTSP and ONVIF, set "fluency first" encoding mo ## Verification Checklist -- [ ] Camera streams accessible via RTSP from services subnet -- [ ] Camera has no internet access (blocked at firewall) -- [ ] Frigate pod is running and showing live camera feed in web UI -- [ ] Recordings appearing in NFS share on sifaka -- [ ] Object detection working (person/vehicle detected in Frigate UI) -- [ ] Retention policy active (old recordings cleaned up automatically) -- [ ] Alerts firing on detection events -- [ ] Prometheus metrics visible in Grafana dashboard -- [ ] `mise run services-check` passes +- [x] Camera streams accessible via RTSP from services subnet +- [ ] Camera has no internet access (blocked at firewall) — pending IoT VLAN segmentation +- [x] Frigate pod is running and showing live camera feed in web UI +- [x] Recordings appearing in NFS share on sifaka +- [x] Object detection working (person/vehicle detected in Frigate UI) +- [x] Retention policy active (old recordings cleaned up automatically) +- [x] Alerts firing on detection events (ntfy push notifications with ~6s delivery) +- [x] Prometheus metrics visible in Grafana dashboard +- [x] `mise run services-check` passes -## Open Questions +## Open Questions (Resolved) -- **MQTT broker:** Is there an existing MQTT broker in the cluster, or does one need to be deployed? Mosquitto is lightweight and standard. -- **Home Assistant:** Frigate works standalone, but HA adds richer automation (e.g., turn on floodlight when person detected, arm/disarm by time of day). Evaluate whether to add HA as a future plan. -- **Sifaka NFS share sizing:** How much space to allocate on the NAS? Start with 2 TB and monitor. The hybrid retention strategy keeps this manageable. -- **Additional cameras:** If more cameras are added later, CPU detection may become a bottleneck. At that point, evaluate a Hailo-8L USB accelerator or a dedicated Frigate host (e.g., RPi5). -- **Floodlight automation:** The ReoLink HTTP API supports floodlight control. Could be automated to turn on when Frigate detects a person at night — but this requires either HA or a custom script listening to MQTT events. +- **MQTT broker:** Deployed Mosquitto (eclipse-mosquitto:2) in the `mqtt` namespace. Lightweight, anonymous access, cluster-internal only (no Caddy/ingress needed since MQTT is TCP, not HTTP). +- **Home Assistant:** Deferred. Frigate + frigate-notify + ntfy provides a complete pipeline without HA. +- **Sifaka NFS share sizing:** Allocated 2 TB. Hybrid retention (3d continuous, 30d alerts, 14d detections) keeps usage well within bounds. +- **Additional cameras:** Using ONNX/YOLO-NAS-s on CPU at ~535ms/frame, ~2 FPS detection. Adequate for single camera. Apple Silicon Detector (ASD) via ZMQ is the next upgrade path for better performance (~15ms via Neural Engine). Requires Frigate 0.17+. +- **Floodlight automation:** Deferred to future Home Assistant evaluation. ## Future Considerations -- 2.50.1 (Apple Git-155)