{"id":48683581,"url":"https://github.com/embmeals/homelabbackend","last_synced_at":"2026-04-18T03:00:52.219Z","repository":{"id":348379080,"uuid":"1197369870","full_name":"embmeals/homelabBackend","owner":"embmeals","description":"Homelab monitoring dashboard — .NET 10, Hangfire, HTMX, Chart.js","archived":false,"fork":false,"pushed_at":"2026-04-06T18:00:36.000Z","size":598,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-11T03:40:00.391Z","etag":null,"topics":["dashboard","dotnet","hangfire","homelab","htmx","infrastructure","monitoring","razor-pages","self-hosted","sqlite"],"latest_commit_sha":null,"homepage":"https://homelab.emills.net","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/embmeals.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-31T14:26:57.000Z","updated_at":"2026-04-06T18:00:54.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/embmeals/homelabBackend","commit_stats":null,"previous_names":["embmeals/homelabbackend"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/embmeals/homelabBackend","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/embmeals%2FhomelabBackend","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/embmeals%2FhomelabBackend/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/embmeals%2FhomelabBackend/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/embmeals%2FhomelabBackend/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/embmeals","download_url":"https://codeload.github.com/embmeals/homelabBackend/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/embmeals%2FhomelabBackend/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31954736,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-18T00:39:45.007Z","status":"online","status_checked_at":"2026-04-18T02:00:07.018Z","response_time":103,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dashboard","dotnet","hangfire","homelab","htmx","infrastructure","monitoring","razor-pages","self-hosted","sqlite"],"created_at":"2026-04-11T03:39:53.553Z","updated_at":"2026-04-18T03:00:52.208Z","avatar_url":"https://github.com/embmeals.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Homelab Backend\n\nA .NET 10 application that monitors and manages a multi-machine homelab infrastructure. Built with Hangfire for job scheduling, Razor Pages + HTMX for a live-updating dashboard, and EF Core + SQLite for persistence.\n\n**Live at** [homelab.emills.net](https://homelab.emills.net) (protected by Cloudflare Access)\n\n![Dashboard](docs/screenshots/dashboard.png)\n![Architecture](docs/screenshots/architecture.png)\n\n## What It Does\n\nMonitors a 3-machine homelab (Mac server, Windows PC, Linux laptop) running Plex, Radarr, Sonarr, Prowlarr, qBittorrent, Syncthing, and 12+ Docker containers. Replaces a collection of shell scripts and a Python monitor with a single, always-on service.\n\n### Public Services\nThese are the services this app monitors — some are publicly accessible:\n\n| Service | Public URL                                         | Purpose |\n|---------|----------------------------------------------------|---------|\n| Overseerr | [requests.emills.net](https://requests.emills.net) | Request movies and TV shows |\n| Tautulli | [stats.emills.net](https://stats.emills.net)       | Plex viewing statistics |\n| Immich | [photos.emills.net](https://photos.emills.net)     | Photo library |\n| This Dashboard | [homelab.emills.net](https://homelab.emills.net)   | Monitoring dashboard |\n| Portainer | [docker.emills.net](https://docker.emills.net)     | Docker management |\n\n*Protected services require email OTP via Cloudflare Access.*\n\n## Media Pipeline\n\nThe core of the homelab is a fully automated media pipeline. Here's how a movie goes from request to screen:\n\n```\n You request a movie         Radarr searches for         Prowlarr feeds\n through Overseerr    →    the best quality release  →   indexer results\n (web UI)                   (Movies) / Sonarr (TV)       to Radarr/Sonarr\n\n         ↓                          ↓                          ↓\n\n qBittorrent downloads      Syncthing transfers         Radarr/Sonarr imports,\n behind Mullvad VPN    →    completed files from   →    renames, and organizes\n on the Windows PC          PC to Mac over LAN          into the media library\n\n         ↓                          ↓                          ↓\n\n Plex scans the library     Tautulli tracks viewing     Bazarr downloads\n and streams to any    →    stats and history      →    matching subtitles\n device on any network                                  automatically\n```\n\n**Why each service exists:**\n- **Overseerr** — clean request UI so friends/family can request without touching Radarr/Sonarr\n- **Radarr/Sonarr** — automates searching, downloading, and organizing movies/TV\n- **Prowlarr** — single place to manage indexers, syncs to Radarr + Sonarr\n- **qBittorrent** — download client, bound to Mullvad VPN for privacy\n- **Syncthing** — real-time file sync between PC (downloads) and Mac (storage)\n- **Plex** — media server with hardware transcoding, serves all devices\n- **Tautulli** — Plex analytics and viewing history\n- **Bazarr** — automated subtitle downloads for all media\n\n**Why the PC downloads instead of the Mac:** The VPN protects download traffic. Keeping it on the PC isolates it from the server — if the VPN drops, only the PC is affected. Syncthing handles the transfer seamlessly.\n\n## Dashboard\n\nThe dashboard updates every 30 seconds via HTMX partial swaps — no full page reloads.\n\n![Hangfire Jobs](docs/screenshots/hangfire.png)\n\n- **Service health cards** — response time and status for each service\n- **Download Pipeline** — real VPN + qBittorrent status via SSH to the PC\n- **Disk usage** — progress bars with warning (85%) and critical (95%) thresholds\n- **Torrent monitoring** — active downloads with stall/error detection\n- **Syncthing sync state** — file counts, pending transfers, errors\n- **Now Playing** — active Plex streams with user, device, and transcode status\n- **Media Library** — movie/show/episode counts and recently added titles\n- **Alerts banner** — aggregates critical and warning issues across all systems\n- **Quick Actions** — one-click Plex scan, restarts, DNS flush, VPN control\n- **Recent Events** — deduplicated service incidents in the last 24 hours\n- **Response time chart** — 24-hour Chart.js trend graph\n- **Uptime page** — 90-day per-service uptime with daily history bars\n\n### Recurring Jobs\n\n| Job | Frequency | What it checks                                                                 |\n|-----|-----------|--------------------------------------------------------------------------------|\n| Service Health | 5 min | HTTP ping 7 services, measure response time                                    |\n| Torrent Monitor | 5 min | SSH to PC, query qBit for stalled/errored downloads                            |\n| Syncthing | 5 min | Folder sync status via REST API                                                |\n| VPN Watchdog | 5 min | SSH to PC, detect Mullvad drop, auto-reconnect with cooldown                   |\n| Remediation | 5 min | Match recent pipeline findings to rules, dispatch known fixes with audit       |\n| Alert Notifications | 5 min | Route critical findings through the notification router                        |\n| Pipeline Health | 10 min | Drive mounts, qBit save path, arr queues, stale downloads, DNS, Plex libraries |\n| Docker Health | 10 min | Colima VM + all 13 container statuses                                          |\n| Tunnel Health | 15 min | HTTP check all Cloudflare Tunnel endpoints                                     |\n| Request Fulfillment | 15 min | Track Overseerr requests end-to-end, flag any stuck \u003e 48h                      |\n| Disk Space | 30 min | `df` on Saved_Media (5TB) and Backups (1.8TB)                                  |\n| Media Library Stats | 6 hours | Movie/show/episode counts from Radarr + Sonarr APIs                            |\n| Digest Flush | Daily 8 AM | Bundle warning-severity notifications into one email                           |\n| Backup Health | Daily 9 AM | SSH to Mac, verify backup age + expected artifacts                             |\n| Data Pruning | Daily 3 AM | Delete health check data older than 30 days across all tables                  |\n| Docker Update Reporter | Daily 3:15 AM | Dashboard note of Watchtower container updates                                 |\n| Subtitle Gap | Daily 4 AM | Find missing subtitles in Bazarr, trigger searches                             |\n| Gmail Cleanup | Daily 4:15 AM | Archive old promos/social, apply label rules                                   |\n| Immich Maintenance | Daily 4:30 AM | Photo library stats, duplicate detection report                                |\n| Disk Usage Trend | Weekly Sun 3:30 AM | 30-day growth projection, alert if filling within 4 weeks                      |\n| Library Integrity | Weekly Sun 5 AM | Reconcile Radarr/Sonarr against disk — orphans, ghosts, duplicates             |\n| Stale Media | Weekly Mon 3:45 AM | Flag unwatched media (\u003e180 days), optional auto-unmonitor                      |\n| Monthly SLO Report | Monthly 1st 8 AM | Per-service uptime, disk growth, library growth, remediation stats             |\n\n### Quick Actions\n\n| Action | What it does                                  |\n|--------|-----------------------------------------------|\n| Scan Plex | Trigger library rescan for new files          |\n| Restart Plex | Force-kill and restart Plex Media Server      |\n| Restart Radarr | Restart Radarr LaunchAgent                    |\n| Flush DNS | Clear macOS DNS cache                         |\n| Rescan Syncthing | Force folder re-check                         |\n| Clean Downloads | Delete stale files (\u003e24h) from PCDownloads    |\n| Start Downloads | Connect Mullvad VPN + start qBittorrent on PC |\n| Stop VPN | Disconnect Mullvad VPN                        |\n| Refresh Media Stats | Re-query Radarr/Sonarr for library counts     |\n\n## Infrastructure\n\n```\n┌─────────────────────────────────────────────────────┐\n│  Mac (M3 Pro) — Primary Server, always on           │\n│                                                     │\n│  Native: Plex, Radarr, Sonarr, Prowlarr, AdGuard    │\n│  Docker: Overseerr, Tautulli, Bazarr, Immich,       │\n│          Portainer, Kometa, Recyclarr, Watchtower   │\n│  Infra:  Cloudflare Tunnel, This Dashboard          │\n│  Storage: Saved_Media 5TB, Backups 1.8TB            │\n├─────────────────────────────────────────────────────┤\n│  Windows PC (RTX 4060 Ti) — Download Relay          │\n│                                                     │\n│  qBittorrent (VPN-bound), Syncthing, Mullvad VPN    │\n│  Downloads here, syncs to Mac, never stores media   │\n├─────────────────────────────────────────────────────┤\n│  Dell XPS 13 — On-demand dev/testing                │\n│                                                     │\n│  Ollama (local AI), Docker, Moonlight               │\n└─────────────────────────────────────────────────────┘\n```\n\n### Networking\n\n- All machines on the same LAN subnet\n- SSH key auth between all machines (Mac ↔ PC, Mac ↔ Dell, PC ↔ Dell)\n- Cloudflare Tunnel exposes services at `*.emills.net` with email OTP protection\n- Docker containers on a custom bridge network\n\n## Why Hangfire\n\nEvery job in this app used to wrap its `ExecuteAsync` in a catch-all `try-catch` that logged the error and moved on. The job would silently fail, Hangfire would mark it as succeeded, and the bug would go unnoticed.\n\nWhen we removed the catch-all try-catches and let exceptions propagate, Hangfire immediately surfaced a bug that had been hidden for weeks — `StaleMediaJob` was crashing because Tautulli returns Unix timestamps as JSON numbers, but the code called `GetString()` on them:\n\n![Hangfire Failed Job](docs/screenshots/hangfire-failed-job.png)\n\nHangfire gives you this for free: failed job visibility, stack traces, automatic retries (2 attempts), and manual requeue. Swallowing exceptions defeats all of it. The rule now: only catch exceptions at system boundaries (SSH, HTTP) for per-item isolation. Never wrap an entire job in a catch-all.\n\n## Notifications\n\nEvery job that used to send email directly now routes through an `INotificationRouter` that dispatches by severity:\n\n| Severity | Channels | When to use |\n|----------|----------|-------------|\n| `Critical` | Email + ntfy push | Paging events — service down, backup missing, VPN won't reconnect |\n| `Warning` | Daily digest (one email at 8 AM) | Non-urgent — stale media report, disk trends, request stuck \u003e 48h |\n| `Info` | Dashboard only | Informational — successful remediations, Docker update summaries |\n\nThis keeps the inbox quiet. Critical events page immediately on the phone via ntfy and arrive as email. Warnings accumulate in the `PendingDigestNotifications` table and flush once daily via `DigestFlushJob`. Info-level events land in logs and dashboard surfaces without touching email at all.\n\nntfy is self-hosted alongside the other homelab containers and exposed via Cloudflare Tunnel. If `Ntfy.BaseUrl` isn't configured, `NtfyChannel` silently no-ops and critical alerts still arrive via email.\n\n## Tech Stack\n\n- **.NET 10** — ASP.NET Core Minimal APIs + Razor Pages\n- **Hangfire** — recurring and fire-and-forget job scheduling (SQLite storage)\n- **EF Core** — SQLite with migrations for health check history\n- **HTMX** — live dashboard updates via partial view polling\n- **Chart.js + Luxon** — response time trend visualization\n- **Serilog** — structured logging with rolling file sink\n- **xUnit + Moq + FluentAssertions** — unit tests\n- **GitHub Actions** — CI on Ubuntu, deploy via self-hosted runner on Mac\n\n## Architecture\n\n```\n[Browser] ← HTMX 30s polling → [Razor Pages + Minimal APIs]\n                                          ↓\n                                    [Hangfire Jobs]\n                                 ↙     ↓      ↘\n                          [HTTP APIs] [SSH]  [SQLite]\n                           ↓    ↓      ↓       ↓\n                    Plex  Radarr  Mac   PC   health.db\n                    Sonarr Prowlarr  ↓    ↓   hangfire.db\n                    Overseerr     df  mullvad\n                    Tautulli    docker  qbit\n                    Syncthing  colima  torrents\n```\n\nThe app runs as a LaunchAgent on the Mac. When on macOS, shell commands execute locally. When running on Windows (dev), they SSH to the Mac. qBittorrent is accessed by SSH-ing to the PC and running curl locally (since qBit is bound to the VPN interface and unreachable over LAN).\n\n### Project layout\n\n```\nsrc/homelabBackend/\n├── Program.cs                  — 25-line composition root\n├── Startup/                    — extension methods called from Program.cs\n│   ├── ConfigurationRegistrations.cs   (IOptions\u003cT\u003e bindings)\n│   ├── PersistenceRegistrations.cs     (DbContext + Hangfire)\n│   ├── InfrastructureRegistrations.cs  (SSH, HTTP, Email, Notifications)\n│   ├── JobRegistrations.cs             (Hangfire job DI)\n│   ├── DashboardRegistrations.cs       (Razor + Hangfire dashboard)\n│   ├── StatusEndpoints.cs              (read-only /api/*)\n│   ├── ActionEndpoints.cs              (mutating /api/actions/*)\n│   └── RecurringJobSchedule.cs         (cron wiring)\n├── Jobs/                       — one file per Hangfire job + OnDemandActions\n├── Infrastructure/             — SSH, HTTP, Email, byte formatting\n│   └── Notifications/          — router, channels, severity enum\n├── Data/                       — EF Core entities + HomelabDbContext\n├── Models/\n│   ├── Configuration/          — IOptions\u003cT\u003e config types\n│   ├── Dtos/                   — external API + internal domain types\n│   └── ViewModels/             — Razor page models\n├── Migrations/                 — auto-generated EF migrations\n├── Pages/                      — Razor dashboard\n└── appsettings.json            — tracked placeholders; real keys in Development.json\n```\n\n## Setup\n\n```bash\n# Clone and restore\ngit clone https://github.com/embmeals/homelabBackend.git\ncd homelabBackend\ndotnet restore\n\n# Configure (copy and edit with your service URLs/keys)\ncp src/homelabBackend/appsettings.json src/homelabBackend/appsettings.Development.json\n\n# Run locally\ndotnet run --project src/homelabBackend\n\n# Run tests\ndotnet test\n\n# Deploy to Mac\nbash scripts/deploy-to-mac.sh\n```\n\n## Configuration\n\nAll values are configurable via `appsettings.json`:\n\n| Section | What it controls                                                           |\n|---------|----------------------------------------------------------------------------|\n| `HomelabServices` | Service URLs, health endpoints, API keys, per-service alert behavior       |\n| `QBittorrent` | qBit connection (URL, credentials)                                         |\n| `DiskCheck` | Drives to monitor, SSH hosts, warning/critical thresholds                  |\n| `Syncthing` | API URL, key, folder ID                                                    |\n| `Pipeline` | SSH hosts (Mac + PC), paths, Plex library section IDs                      |\n| `Notifications` | Gmail SMTP credentials + per-alert cooldown minutes                        |\n| `Ntfy` | Self-hosted ntfy base URL, topic, optional auth token                      |\n| `Tautulli` | Base URL + API key for Plex watch-history lookups                          |\n| `Bazarr` | Base URL + API key for subtitle-gap scanning                               |\n| `Immich` | Base URL + API key for photo library stats                                 |\n| `Overseerr` | Base URL + API key, stuck-threshold hours, page size                       |\n| `StaleMedia` | Unwatched-threshold days, auto-unmonitor toggle                            |\n| `TunnelHealth` | List of Cloudflare Tunnel endpoints to probe                               |\n| `BackupHealth` | SSH host, backups root path, warning/critical age, expected artifacts list |\n| `VpnWatchdog` | Enable toggle, reconnect cooldown minutes                                  |\n| `Remediation` | Enable toggle, lookback minutes, per-pattern rules (check → action)        |\n| `LibraryIntegrity` | SSH host, Movies/TV root paths                                             |\n| `GmailCleanup` | IMAP credentials, archive-age, label rules                                 |\n| `JobSchedules` | Cron expressions for all recurring jobs                                    |\n\nSecrets go in `appsettings.Development.json` (gitignored). The tracked `appsettings.json` uses placeholder values.\n\n## Deployment\n\nPush to `main` triggers the CI/CD pipeline:\n1. **CI** (Ubuntu) — restore, build, test\n2. **Deploy** (self-hosted Mac runner) — only runs if CI passes, publishes ARM64 binary, deploys via rsync (preserves config + DB), restarts LaunchAgent\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fembmeals%2Fhomelabbackend","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fembmeals%2Fhomelabbackend","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fembmeals%2Fhomelabbackend/lists"}