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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
frigate-notify sends detection snapshots as attachments, which
requires ntfy to have attachment support configured.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Adds docs/index.md, explanation/explanation.md, how-to/plans/plans.md,
and how-to/plans/completed/completed.md so AI sessions get full
awareness of all doc sections and in-flight plans.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reflect actual UX7 zone-based firewall UI, correct streaming port
(8096 not 443), note indri DHCP reservation, mark plan as completed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
- Abandon the UniFi Pulumi IaC approach after provider bugs caused a network outage (no-op update reset undeclared properties on the default LAN network)
- Remove untracked IaC artifacts (`pulumi/unifi/`, `mise-tasks/unifi-preview`, `mise-tasks/unifi-up`) locally
- Mark `add-unifi-pulumi-stack` plan as Abandoned with explanation
- Create new `segment-home-network` plan for manual three-network segmentation (Main/IoT/Guest) via UX7 web UI
- Rewrite UniFi reference card to remove all Pulumi/IaC references
- Update plan and how-to indexes
## Test plan
- [x] `docs-check-links` passes
- [x] `docs-check-index` passes
- [x] Pre-commit hooks pass
- [ ] Review segmentation plan for completeness before executing manually
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/189
## Summary
- Add new how-to guide (`connect-to-postgres.md`) with the `psql` command using `op read` for 1Password credentials
- Add "Database" section to the how-to index linking to the new guide
- Link the new guide from the PostgreSQL reference card's Related section
## Test plan
- [x] Verified `psql` connection works from gilbert using the documented command
- [ ] Review doc formatting and content
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/188
Chart 2.3.0 mounts credentials as a file with standard k8s base64
encoding. The old double-encoding workaround (credentials-base64 in
stringData) now produces invalid JSON. Use raw JSON (credentials-file)
instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
- Replace `op item get --fields` with `op read` for secrets (matches playbook and CLAUDE.md guidance)
- Change `tags: [<role>]` to `tags: <role>` to match actual playbook style
- Remove redundant `listen:` from handler example, add `changed_when: true`
- Name handler after specific service (e.g. `Restart <service>`) to match real roles
- Add `last-reviewed: 2026-02-13` frontmatter
## Also noted (not fixed here)
Two other docs still use the old `op item get` pattern:
- `docs/how-to/troubleshooting.md:72` (ArgoCD login command)
- `docs/how-to/gandi-operations.md:35` (Gandi token export)
These can be fixed in their own review cycles.
Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/185
## Summary
- Add `daemon.json` with `registry-mirrors` to the forgejo-runner ConfigMap, pointing DinD at `http://host.minikube.internal:5050`
- Mount `daemon.json` into the DinD sidecar at `/etc/docker/daemon.json` via `subPath`
- Docker Hub pulls during Dagger CI builds will now route through Zot's pull-through cache, reducing bandwidth and avoiding rate limits
## Deployment and Testing
- [ ] `argocd app sync forgejo-runner`
- [ ] Exec into DinD container: `docker info` should show the registry mirror
- [ ] Trigger a workflow build and check Zot logs for cache hits
Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/183
v3.2.0 build failed (GitHub download timeout), rolling back to
working image while it rebuilds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
- Move FORGEJO_URL, RUNNER_NAME, and RUNNER_LABELS from ExternalSecret template to deployment env vars
- ExternalSecret now only contains the actual secret (RUNNER_TOKEN)
- Image version changes in RUNNER_LABELS now trigger automatic pod rollouts
## Deployment
1. Merge this PR
2. `argocd app sync forgejo-runner` — the deployment spec change will auto-roll the pod
No manual restart needed — that's the whole point :)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/181
## Summary
- Install yq in the forgejo-runner container image for structured YAML editing
- Replace fragile `sed` regex patterns with `yq` in `build-blumeops.yaml` and `cv-deploy.yaml` workflows
## Deployment
1. Merge this PR
2. Tag and release forgejo-runner v3.1.0: `mise run container-tag-and-release forgejo-runner v3.1.0`
3. Update runner label in `argocd/manifests/forgejo-runner/external-secret.yaml` from `v3.0.2` to `v3.1.0`
4. Sync the forgejo-runner app: `argocd app sync forgejo-runner`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/180
Replace old Apps/Observability/Infrastructure layout entries with
Content and Misc to match the recategorized ingress annotations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ArgoCD's tailscale ingress was missed in the recategorization (filed as
service-tailscale.yaml instead of ingress-tailscale.yaml). Fix the group
annotation and rename the file to match the convention used by all other
services.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
- Replace the three homepage groups (Apps, Observability, Infrastructure) with two cleaner groups
- **Content**: Immich, Kiwix, Miniflux, DJ, Grafana
- **Misc**: CV, TeslaMate, Transmission, Docs, Prometheus, PyPI
## Deployment and Testing
- [ ] Sync affected ingresses via ArgoCD (all 11 services)
- [ ] Verify homepage shows the two new groups correctly
Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/179
## Summary
- Remove `match_all = true` from `flyio_nginx_cache_requests_total` in Alloy so the metric only counts requests that go through the proxy cache (excludes health checks with empty `cache_status`)
- Change dashboard queries from `rate(...[5m])` to `increase(...[$__range])` — aggregates over the full dashboard time window instead of a 5-minute sliding window, giving meaningful ratios for low-traffic static sites
- Add null/NaN value mapping to show "No traffic" in neutral color instead of blank/red
## Root cause
Health check requests from Fly.io hit the default nginx server block (no `proxy_cache`), producing entries with empty `upstream_cache_status`. With `match_all = true`, these were counted in the cache metric, diluting the Fly.io dashboard ratio. For APM dashboards, `rate()[5m]` on low-traffic sites with 24h cache validity almost always returns either all-HITs (100%) or no data (blank → red background).
## Deployment
- Fly.io proxy redeploy needed for Alloy config change
- ArgoCD sync for dashboard ConfigMap changes
## Test plan
- [ ] Redeploy Fly.io proxy
- [ ] Sync grafana-config in ArgoCD
- [ ] Verify CV APM cache hit ratio shows a real percentage (not 100%)
- [ ] Verify Docs APM shows "No traffic" in neutral color when idle, real ratio when visited
- [ ] Verify Fly.io proxy dashboard cache ratio excludes health checks
Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/177