Commit Graph

163 Commits

Author SHA1 Message Date
admin 8b8c04a487 fix: P0+P1 critical bug fixes across controller (24 files)
Concurrency fixes:
- Deep-copy stacks in GetStack/GetStacks to prevent shared state mutation (C04)
- Add per-state mutex to watchdog pathProbeState (C05)
- Guard MetricsCollector.Start() with sync.Once against double-start (C06)
- Hold diskJobMu across entire raw mount operation (C07)
- Add mutex to SetEncryptionKey (C08), MigrateEncryption write lock (H03)
- Use sync.Once for sync.Stop() channel close (H08)
- Set syncing=true before releasing lock in TriggerSync (H09)
- Deep-copy lastDBDump/lastBackup in GetFullStatus (H11)
- Add WaitGroup for stderr goroutine in MigrateDrive (H19)
- Add mutex to SetBackupRunningCheck (M18)

Security fixes:
- Validate Bearer token against Hub API key in CSRF middleware (H16)
- Validate backup paths start with expected prefix in RemoveStack (M12)
- Guard uuid[:8] slice with length check (H20)
- Parse fstab fields exactly for mount target matching (H21)

Bug fixes:
- Use decrypted env vars for compose deploy (C01)
- Log decrypt failures in DecryptMap instead of swallowing (C02)
- Move Deployed=false inside lock in runComposeDeploy (C03)
- Fix activeDrives() to skip disconnected drives (H02)
- Fix Snapshot() stderr extraction from exec.ExitError (H01)
- Check unlockCmd.Run() error in restic (H01)
- Buffer template rendering via bytes.Buffer (H07)
- Thread context.Context through cloudflare client (H10)
- Fix leaf-name collision detection in cross-drive backup (H15)
- Add nil check for crossDriveRunner (H17)
- Use strings.TrimSpace instead of slice on command output (H18)
- Make SaveAppConfig atomic with write-to-tmp+rename (H04)
- Pass encKey on deploy failure SaveAppConfig (H05)
- Fix IPv6 address format in TCP health probe

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 13:39:45 +01:00
admin 2ad743b66f v0.30.2: Report geo-restriction + logo/favicon update + Hub geo auth
- Add GeoRestrictionReport to report types and builder, so Hub can
  display geo-blocking status on customer detail pages
- Update all 5 BuildReport() call sites with new geoRestriction param
- Add /api/geo/ to selfUpdateAuthMiddleware (Hub Bearer token auth)
- Replace embedded logo SVG with updated logo.svg (white text variant)
- Add FelhomFaviconSVG constant + /static/favicon.svg route
- Update layout.html and catchall.html favicon links

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 12:42:51 +01:00
admin e61e164cf7 docs: bump README version to v0.30.1
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 12:05:57 +01:00
admin 9ed5e78c45 fix: remove custom block response from WAF rules (CF free plan)
Cloudflare Free plan doesn't support custom response body in block
rules. Use plain block action which returns CF's default 403 page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 12:03:01 +01:00
admin e1fb85240b feat: geo-restriction via Cloudflare WAF custom rules
Add country-based access control managed through the Settings page.
Global allow-list with per-app overrides, searchable country selector,
automatic sync to Cloudflare WAF on settings change / deploy / remove,
plus periodic 6-hour verification.

New package: internal/cloudflare/ (client, zone, waf, countries, geosync)
New API: /api/geo/* (6 endpoints) + /api/stacks/{name}/geo/override

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 11:58:22 +01:00
admin 4c5d430b1a feat: controller-side HTTP/TCP health probes
Add network-level health probing from the controller to deployed apps.
The controller probes containers over the shared Docker network and
overrides stack state to "unhealthy" if the service isn't responding.

Three probe types: http (any response = alive), api (validates status
code and body content), tcp (port reachability). Configured per-app
via healthcheck: section in .felhom.yml. Runs every minute, per-app
interval defaults to 5 minutes.

This replaces Docker-level healthchecks for distroless images (e.g.
Vikunja) that lack shell utilities, and complements existing Docker
healthchecks for other apps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 11:11:21 +01:00
admin 077640d9bb feat: dynamic logo from synced assets + SVG favicon
Logo handler now checks Hub-synced assets first, falling back to
embedded SVG. Added SVG favicon to layout and catchall templates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 09:34:38 +01:00
admin 0c687ae280 fix: remove stale git lock files before catalog sync
Catalog sync could fail permanently if the container was killed mid-fetch,
leaving behind .git/shallow.lock (or index.lock, HEAD.lock). Now cleaned
up automatically before each git fetch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 20:09:38 +01:00
admin 44f7fd2f19 feat: encrypt sensitive values in app.yaml with AES-256-GCM
Passwords and secrets from deploy fields (type: password/secret) are now
encrypted at rest in app.yaml using a per-node 32-byte key. Values stored
as ENC:base64(nonce+ciphertext), decrypted transparently for docker-compose
and web UI. Key included in infra backup bundle for disaster recovery.
Existing plaintext values migrated automatically on startup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 19:12:24 +01:00
admin 38eaae29aa fix: password field value, masked post-deploy creds, initial pw note
- Fix password fields showing empty after deployment: now reads value
  from DeployedFieldValues (app.yaml env) instead of only .Default
- Post-deploy card: passwords are masked with reveal + copy buttons
  instead of showing plaintext
- Settings page: deployed password fields show "initial password" hint
  explaining the value won't update if changed in the app
- Hide Generate button on settings page for already-deployed apps
- Added EMAIL to username-detection heuristic for credential display

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 18:40:38 +01:00
admin eb2207fb62 feat: password fields with masked input, reveal toggle, confirmation
- Password deploy fields now use type=password (masked by default)
- Added eye toggle button to reveal/hide password and confirm fields
- Added confirmation field below each password input
- Generate button fills both password and confirmation fields
- Form validation checks password confirmation matches before deploy
- Confirmation field only shown for new deployments (not already deployed)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 16:33:33 +01:00
admin bfab1e102f feat: show actual credentials on post-deploy success page
Instead of a generic "default creds" message, the post-deploy card now
reads actual username/password values from the deploy form fields and
displays them in a table. Filters out internal DB passwords and secret
keys, showing only user-facing credentials (admin user, admin password).
Falls back to metadata defaultCreds for apps without typed deploy fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 16:17:47 +01:00
admin 4edc974404 fix: show actual timestamps in debug log viewer
The Naplóviewer was showing relative times like '3586mp' (seconds ago)
which were also negative due to timezone mismatch — parseLine used
time.Parse (UTC) but log.LstdFlags outputs local time. Now:
- parseLine uses time.ParseInLocation with time.Local
- fmtTime JS shows absolute HH:MM:SS (or MM-DD HH:MM:SS for old entries)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 16:02:28 +01:00
admin 46c220fd8f fix: show filebrowser subdomain link on stacks page
Protected stacks like filebrowser have no .felhom.yml or app.yaml,
so the subdomain lookup found nothing. Added well-known subdomain
fallback map for programmatically managed protected stacks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 15:53:20 +01:00
admin 0a85b5cd69 fix: add json template function for post-deploy data embedding
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 15:29:31 +01:00
admin a30f4c0234 feat: post-deploy info card with app link, first steps, and credentials
After successful deploy, shows a rich info card instead of auto-redirecting
to the apps list. Includes direct app link, first steps from catalog metadata,
default credentials info, and link to settings page for password reveal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 15:27:33 +01:00
admin e737704e68 fix: skip stopped apps in telemetry to avoid zero-value averages on hub
Deployed-but-stopped apps were included in telemetry reports with all-zero
memory/CPU values, dragging down hub-side averages. Now isStackRunning()
filters to only running/starting/unhealthy/restarting states.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 15:05:39 +01:00
admin d3f7e39d6d fix: catch-all middleware allow localhost for healthcheck, drop certresolver
CatchAllMiddleware was intercepting Docker healthcheck requests (Host:
localhost) and internal API calls, returning 404 instead of passing
through. Also removed certresolver from catch-all Traefik router to
avoid cert provisioning issues with HostRegexp(.+).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 14:15:49 +01:00
admin df165f7ef0 feat: catch-all page for stopped apps, deploy controls, dashboard open button
Stopped/undeployed app subdomains now show a branded page instead of
Traefik 404. Deploy settings page gains start/stop/restart controls.
Dashboard shows "Megnyitás" button for running apps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 13:38:53 +01:00
admin aaf479356a fix(stacks): RestartStack now uses up -d with env vars
Previously used bare "docker compose restart" which doesn't inject
env vars or pick up template changes. Now matches StartStack behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 12:17:23 +01:00
admin 563cf07ec8 feat(deploy): async compose-up for instant UI feedback (v0.28.2)
Deploy API now returns immediately after validation + config save.
docker compose up -d runs in a background goroutine so the UI shows
progress during image pulls instead of blocking for 30-60s.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 12:08:08 +01:00
admin 4a6ab4d61c feat(debug): add Telemetria teszt section to debug page (v0.28.1)
- New GET /api/debug/telemetry endpoint runs full telemetry pipeline on-demand
- GetTelemetryPreview callback added to DebugCallbacks, wired in main.go
- BuildAppTelemetryForDebug() exported wrapper in report/telemetry.go
- Debug page: new collapsible section with per-app table (memory, CPU, log errors/warnings, issues) and raw JSON viewer
- Available regardless of hub configuration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 11:09:06 +01:00
admin 05ecd65412 feat(telemetry): add per-app metrics and log telemetry to hub reports (v0.28.0)
- New internal/metrics/telemetry.go: MetricsStore.GetContainerTelemetry()
  aggregates container memory/CPU from SQLite over the last 15 min
- New internal/metrics/logscanner.go: ScanContainerLogs() scans docker logs
  for errors/warnings, deduplicates via fingerprinting (strips timestamps,
  replaces 6+ digit numbers, hex strings, UUIDs)
- New internal/report/telemetry.go: buildAppTelemetrySection() assembles
  per-stack AppTelemetry by aggregating container metrics and log summaries
- internal/report/types.go: added AppTelemetry field to Report struct plus
  AppTelemetry type with memory/CPU/log fields and LogIssue references
- internal/report/builder.go: calls buildAppTelemetrySection() in BuildReport()
- Backward-compatible: old Hub versions silently ignore app_telemetry field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 10:46:27 +01:00
admin ac5e3cb14e fix(monitoring): read used_mem_mb from nested system object in API response
The /api/system/info response wraps SystemInfo under data.system,
not directly under data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 10:13:08 +01:00
admin ad4c005e01 v0.27.3: Use real system memory everywhere, add monitoring memory bar
Deploy page, pre-start check, and deploy validation now use actual
/proc/meminfo usage instead of declared mem_request sums. New
GetMemoryMB() helper for lightweight real-time memory reads. Monitoring
page gains a stacked memory distribution bar showing per-container
usage, OS overhead, and free memory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 10:06:03 +01:00
admin c33247abc1 docs: update README for v0.27.2 architectural changes
- Memory validation: stopped apps excluded from CommittedMemory()
- Pre-start memory check (409 Conflict) on stack start
- hungarian_ui metadata field in resources
- USB badge on storage cards
- Manual Tier2 triggers now push infra backup to Hub
- showAlert() replaces native alert() for copyable error text

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 09:40:21 +01:00
admin e99067ca60 v0.27.2 — copyable error popups, Tier2 hub reporting, memory bar fixes, new labels
- Replace native alert() with custom showAlert() modal (text selectable)
- Manual Tier2 backup now pushes infra backup to Hub
- CommittedMemory() excludes stopped/exited apps
- Pre-start memory check blocks start if insufficient RAM
- Add hungarian_ui metadata field + "Magyar felület" badge
- Add "USB" badge on storage cards in settings page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 09:28:29 +01:00
admin b8ab9264f4 fix: SyncFileBrowserMounts reads domain from controller config instead of missing .env
The filebrowser stack has no .env file — domain is baked into compose labels
by docker-setup.sh. The sync always bailed with a WARN and storage paths
were never applied to FileBrowser's config.yaml or docker-compose.yml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 18:01:43 +01:00
admin 64072f1936 style: subdomain suffix as plain text instead of bordered box
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 15:48:44 +01:00
admin 002c388f9f fix(deploy): polish subdomain field UI
- Remove "Automatikusan generálva" badge from domain field (it's not
  generated, it's the customer's configured domain)
- Shrink subdomain input width (8rem) so the .domain suffix appears
  directly next to it on the same line
- Suppress redundant "Az alkalmazás aldomainje" description hint for
  subdomain fields (the warning hint is sufficient)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 15:21:01 +01:00
admin 66817709ad v0.27.0 — user-configurable app subdomains
Users can now customize the subdomain for each app during deployment
instead of using a fixed value. The deploy page shows an editable text
input with the default pre-filled and the base domain as a suffix.

New "subdomain" deploy field type with DNS-safe format validation,
reserved name blocklist, and uniqueness check across deployed stacks.
Locked after deploy — changing requires Remove + Redeploy.

Backward compatible: InjectMissingFields() auto-fills SUBDOMAIN from
.felhom.yml defaults for existing deployed apps on next sync/restart.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 15:06:22 +01:00
admin f7556b0dad v0.26.2 — show full app URL on deploy page
Domain field now displays subdomain.base_domain (e.g. wiki.demo-felhom.eu)
instead of just the base domain, matching the app card display.
Applies to both pre-deploy and post-deploy settings pages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 13:51:39 +01:00
admin f95f570670 v0.26.1 — show auto-generated values on deploy page
- Pre-generate domain + secret field values when deploy page loads,
  so user sees actual domain and masked passwords (with reveal button)
  before deploying. Same values submitted as hidden inputs → saved to app.yaml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 13:42:15 +01:00
admin 7abd1c5954 v0.26.0: Storage namespace felhom-data/ + test node wipe script
All felhom-managed data on external drives now lives under felhom-data/
subdirectory, cleanly separating controller data from user files.

- backup/paths.go: add FelhomDataDir constant, update 8 path helpers
- stacks/delete.go: add local felhomDataDir constant (circular import
  boundary), update ProtectedHDDPaths + GetStackBackupData
- storage/migrate_drive.go: import backup pkg, fix conflict check, verify,
  rsync excludes (felhom-data/backups/*/restic/), size estimation
- storage/migrate.go: import backup pkg, fix DB dump paths
- web/handlers.go: fix legacy 'storage' path -> backup.AppDataDir()
- storage/format_linux.go: create felhom-data/ instead of storage/
- storage/attach_linux.go: create felhom-data/ instead of storage/
- scripts/felhom-wipe.sh: new multi-level test node wipe script
  (soft/controller/full/nuclear)
- CHANGELOG.md, controller/README.md, scripts/README.md: updated docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 10:10:51 +01:00
admin 7f48786312 v0.25.0 — Debug page: operator testing & diagnostics dashboard
Debug-mode-only dashboard (/debug) with 8 collapsible sections:
system diagnostics, notification testing, backup triggers, storage
simulation, hub & connectivity, self-update dry-run, DR/setup wizard,
and in-memory log viewer. Migrates debug dump from API router to web
server. Adds ring buffer log capture, storage disconnect simulation,
event history tracking, and cross-drive/self-update test methods.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 20:18:57 +01:00
admin be7803c0ac v0.24.0 — Pre-testing observability: debug logging, diagnostic dump, startup self-test
- Add [DEBUG] logging across all modules (backup, storage, sync, selfupdate,
  monitor, notify, report, assets, setup) gated behind logging.level: "debug"
- Add /api/debug/dump endpoint returning full controller state JSON (debug only)
- Add startup self-test validating 9 subsystems (Docker, dirs, storage, hub,
  restic repos, metrics DB) with pass/warn/fail summary
- New packages: internal/selftest, internal/util
- Constructor/signature changes: debug bool params, logger params on
  RunHealthCheck and BuildReport, smart watchdog probe logging

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 18:32:26 +01:00
admin 6f02536243 updated version in readme 2026-02-21 17:16:26 +01:00
admin fc97199fe2 restore.html fix 2026-02-21 17:05:04 +01:00
admin 51617f55d0 Fix build: rename _ to r in handler signatures for executeTemplate
dashboardHandler, stacksHandler, monitoringHandler used blank identifier
for the request param but now call executeTemplate(w, r, ...).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:40:40 +01:00
admin 02650e3202 v0.23.0 — CSRF protection on all browser-facing POST endpoints
Controller:
- internal/web/csrf.go (new): CsrfProtect middleware, csrfToken/csrfField helpers
- auth.go: per-session CSRF token (csrfToken field, csrfTokenForSession method)
- server.go: executeTemplate wrapper auto-injects CSRFField+CSRFToken
- main.go: wire CsrfProtect on all routes; bump to v0.23.0
- handlers.go, storage_handlers.go, handler_restore.go: executeTemplate
- All templates: CSRFField in forms, meta csrf-token, csrfHeaders() JS helper,
  fetch calls updated; sendBeacon→fetch+keepalive in storage_attach.html

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:38:56 +01:00
admin aa167b43f5 updated readme 2026-02-21 15:45:40 +01:00
admin 538d367cc4 feat(controller): Hub asset syncer for logos and screenshots
Add internal/assets package that downloads and caches app assets from
Hub API with SHA-256 change detection. Assets resolve from synced cache
first, falling back to baked-in directory. Daily sync schedule +
on-demand POST /api/assets/sync endpoint.

Config: assets.sync_enabled + assets.sync_schedule (default 05:00)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 15:29:23 +01:00
admin a5fec20d31 fix: setup wizard logo not loading
The logo handler tried os.ReadFile() on a non-existent filesystem path.
The SVG only exists as an embedded string constant in the web package.
Export FelhomLogoSVG and serve it directly in the setup handler.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 15:08:45 +01:00
admin 296fdbfdcb v0.22.1: Fix setup wizard bugs (detection, CSRF panic, version display, IP)
- NeedsSetup: only check for empty customer.id (not "demo-felhom")
- renderError: pass *http.Request to ensureCSRFToken (was nil → panic)
- Welcome template: remove redundant "v" prefix from version display
- IP detection: read HOST_IP env var for Docker container awareness
- docker-setup.sh: inject HOST_IP into generated docker-compose.yml
- Add logging for Hub config download in setup wizard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 13:30:32 +01:00
admin 6eb75204b6 v0.22.0: First-run setup wizard, local infra backup, hub verification
New controller features:
- Web-based setup wizard replaces docker-setup.sh interactive config
  - Dual listener: :8080 (Traefik) + :8081 (direct HTTP for LAN)
  - Drive scanner finds .felhom-infra-backup/ on all block devices
  - Hub recovery pull (GET /api/v1/recovery/{id}) with retrieval password
  - Fresh install: Hub config download or manual wizard
  - CSRF protection, state persistence, Hungarian UI
- Local infra backup written to all connected drives after each backup cycle
  - .felhom-infra-backup/backup.json + metadata.json with SHA256 checksum
- Hub verification: parse customer_blocked from report push response
  - Limited mode after 7 days without verification
- Recovery info page on Settings + recovery-info.txt file generation
- Pending events queue: DR events sent to Hub on next report push
- docker-setup.sh v6.0.0: removed interactive wizard, minimal controller.yaml only

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 12:33:17 +01:00
admin e217c3a445 v0.21.3: Push infra backup after config apply, fix double-v in events
After successful config apply, immediately push infra backup to Hub
so the config sync status updates right away. Also fix startup event
message that showed "vv0.21.2" instead of "v0.21.3".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 20:26:17 +01:00
admin 7953f657cc v0.21.2: Fix config apply on Docker bind mounts
os.Rename() fails with "device or resource busy" on bind-mounted files.
Fall back to direct os.WriteFile when rename fails.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 20:11:33 +01:00
admin 7d69d96cf3 Remove ping_uuids from example config, update architecture diagram
- Comment out ping_uuids section in controller.yaml.example (deprecated)
- Architecture diagram: remove status.felhom.eu, update to Hub event system
- Mark Healthchecks references as deprecated throughout README

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 19:56:12 +01:00
admin e9dcba2473 v0.21.1: Add GET /api/config endpoint for live config content
New endpoint returns raw controller.yaml content (text/yaml) for Hub
live diff and pull operations. Same auth as other config endpoints.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 19:26:47 +01:00
admin 8aebbb8902 feat: Hub monitoring takeover — event push system + config cleanup (v0.21.0)
Replace external Healthchecks.io with Hub-native event system. Controller
now pushes structured events via POST /api/v1/event with typed detail
structs. Hub handles dead man's switch, notification dispatch, and cooldowns.

Phase 5: PushEvent() core method, 21 event types, expanded notification
settings (11 toggles), Hub connection monitoring on dashboard, alerts.
Phase 6: Deprecation log for ping UUIDs, pinger kept for transition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 18:53:21 +01:00