From 04c7f3c45acf3184c70e6c8bdd2115d52216b760 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 14 Feb 2026 21:27:44 -0800 Subject: [PATCH] Deploy Frigate NVR stack with Mosquitto, Ntfy, and frigate-notify (#190) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Deploy a cloud-free NVR stack for the GableCam (ReoLink Elite Floodlight at 192.168.1.159): - **Mosquitto** — shared MQTT broker in `mqtt` namespace (cluster-internal, no auth) - **Ntfy** — self-hosted push notifications in `ntfy` namespace, exposed at `ntfy.tail8d86e.ts.net` / `ntfy.ops.eblu.me` - **Frigate** — NVR with GableCam via HTTP-FLV, ONNX CPU detection, NFS recordings on sifaka, exposed at `nvr.tail8d86e.ts.net` / `nvr.ops.eblu.me` - **frigate-notify** — bridges Frigate detection events (person, car, dog, cat) to Ntfy alerts via MQTT Also includes: - Prometheus scrape target for Frigate metrics - Grafana dashboard for Frigate (status, inference speed, FPS, CPU/memory, storage) - Caddy reverse proxy entries for `nvr.ops.eblu.me` and `ntfy.ops.eblu.me` ## Prerequisites - [ ] Create NFS share `frigate` on sifaka (`/volume1/frigate`, RW for indri) - [ ] Create 1Password item "Reolink Floodlight Camera" in `blumeops` vault with `username` and `password` fields ## Deployment (after merge) ```bash argocd app sync apps argocd app sync mosquitto argocd app sync ntfy argocd app sync frigate argocd app sync grafana-config argocd app sync prometheus mise run provision-indri -- --tags caddy mise run services-check ``` ## Verification - [ ] Mosquitto pod running, accepting connections on 1883 - [ ] Ntfy web UI accessible at `ntfy.ops.eblu.me` - [ ] Frigate web UI at `nvr.ops.eblu.me` showing GableCam live feed - [ ] Object detection working (ONNX, person/car/dog/cat) - [ ] Recordings appearing in NFS share on sifaka - [ ] frigate-notify sending detection alerts to Ntfy - [ ] Prometheus scraping Frigate metrics - [ ] Grafana dashboard showing Frigate data Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/190 --- ansible/roles/caddy/defaults/main.yml | 8 +- argocd/apps/frigate.yaml | 18 + argocd/apps/mosquitto.yaml | 18 + argocd/apps/ntfy.yaml | 18 + .../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 +- .../manifests/frigate/configmap-notify.yaml | 31 ++ argocd/manifests/frigate/configmap.yaml | 71 +++ .../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/homepage/values.yaml | 4 +- 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/configmap.yaml | 13 + argocd/manifests/ntfy/deployment.yaml | 54 ++ argocd/manifests/ntfy/ingress-tailscale.yaml | 26 + argocd/manifests/ntfy/kustomization.yaml | 9 + argocd/manifests/ntfy/service.yaml | 13 + argocd/manifests/prometheus/configmap.yaml | 6 + .../prometheus/ingress-tailscale.yaml | 2 +- .../teslamate/ingress-tailscale.yaml | 2 +- .../manifests/torrent/ingress-tailscale.yaml | 2 +- .../changelog.d/deploy-frigate-nvr.feature.md | 1 + docs/how-to/plans/completed/completed.md | 1 + .../operationalize-reolink-camera.md | 33 +- mise-tasks/services-check | 6 + 39 files changed, 1134 insertions(+), 25 deletions(-) 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/configmap.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 rename docs/how-to/plans/{ => completed}/operationalize-reolink-camera.md (88%) diff --git a/ansible/roles/caddy/defaults/main.yml b/ansible/roles/caddy/defaults/main.yml index 15bdd8a..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 @@ -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/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 new file mode 100644 index 0000000..ed357ad --- /dev/null +++ b/argocd/manifests/frigate/configmap-notify.yaml @@ -0,0 +1,31 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: frigate-notify-config + namespace: frigate +data: + config.yml: | + frigate: + server: http://frigate:5000 + public_url: https://nvr.ops.eblu.me + + 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" + + ntfy: + enabled: true + 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}}" diff --git a/argocd/manifests/frigate/configmap.yaml b/argocd/manifests/frigate/configmap.yaml new file mode 100644 index 0000000..629f8ee --- /dev/null +++ b/argocd/manifests/frigate/configmap.yaml @@ -0,0 +1,71 @@ +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 IP is reserved in UX7 DHCP config + gablecam: + - "rtsp://{FRIGATE_CAMERA_USER}:{FRIGATE_CAMERA_PASSWORD}@192.168.1.159:554/h264Preview_01_main" + gablecam_sub: + - "rtsp://{FRIGATE_CAMERA_USER}:{FRIGATE_CAMERA_PASSWORD}@192.168.1.159:554/h264Preview_01_sub" + + 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 + stationary: + max_frames: + default: 1500 + objects: + car: 150 + objects: + track: [person, car, dog, cat, bird] + + detectors: + 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 + 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..34c50ef --- /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.16.4-standard-arm64 + 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/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 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/configmap.yaml b/argocd/manifests/ntfy/configmap.yaml new file mode 100644 index 0000000..584eba1 --- /dev/null +++ b/argocd/manifests/ntfy/configmap.yaml @@ -0,0 +1,13 @@ +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 + attachment-cache-dir: /var/cache/ntfy/attachments + attachment-total-size-limit: 1G + attachment-file-size-limit: 10M + attachment-expiry-duration: 24h diff --git a/argocd/manifests/ntfy/deployment.yaml b/argocd/manifests/ntfy/deployment.yaml new file mode 100644 index 0000000..90c1f11 --- /dev/null +++ b/argocd/manifests/ntfy/deployment.yaml @@ -0,0 +1,54 @@ +--- +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", "--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: + 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: config + configMap: + name: ntfy-config + - 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..851171f --- /dev/null +++ b/argocd/manifests/ntfy/kustomization.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: ntfy +resources: + - configmap.yaml + - 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/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" 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..f5f3960 --- /dev/null +++ b/docs/changelog.d/deploy-frigate-nvr.feature.md @@ -0,0 +1 @@ +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 diff --git a/mise-tasks/services-check b/mise-tasks/services-check index 60dac49..020e177 100755 --- a/mise-tasks/services-check +++ b/mise-tasks/services-check @@ -80,6 +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.ops.eblu.me/v1/health" +check_http "Frigate" "https://nvr.ops.eblu.me/api/version" echo "" echo "Public services (via Fly.io):" @@ -99,6 +101,10 @@ 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" +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:"