{"id":48732309,"url":"https://github.com/mzac/apt-ui","last_synced_at":"2026-05-02T02:01:03.873Z","repository":{"id":346408473,"uuid":"1188880935","full_name":"mzac/apt-ui","owner":"mzac","description":"A docker container that will let you manage your apt package updates on all your Debian based systems through a simple GUI","archived":false,"fork":false,"pushed_at":"2026-05-01T15:56:49.000Z","size":874,"stargazers_count":3,"open_issues_count":15,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-01T17:14:41.160Z","etag":null,"topics":["apt","apt-get","claude","container","debian","docker","kubernetes","proxmox","raspberry-pi","raspbian","ubuntu"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mzac.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","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-22T17:54:37.000Z","updated_at":"2026-05-01T15:56:53.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/mzac/apt-ui","commit_stats":null,"previous_names":["mzac/apt-ui"],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/mzac/apt-ui","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mzac%2Fapt-ui","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mzac%2Fapt-ui/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mzac%2Fapt-ui/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mzac%2Fapt-ui/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mzac","download_url":"https://codeload.github.com/mzac/apt-ui/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mzac%2Fapt-ui/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32520156,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-02T01:12:54.858Z","status":"online","status_checked_at":"2026-05-02T02:00:05.923Z","response_time":132,"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":["apt","apt-get","claude","container","debian","docker","kubernetes","proxmox","raspberry-pi","raspbian","ubuntu"],"created_at":"2026-04-12T02:01:40.570Z","updated_at":"2026-05-02T02:01:03.846Z","avatar_url":"https://github.com/mzac.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ch1 align=\"center\"\u003e⬡ apt-ui\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cstrong\u003eSelf-hosted apt fleet manager — one dashboard, every server, real terminal output.\u003c/strong\u003e\u003cbr/\u003e\n  A focused alternative to AWX / Ansible Tower for Ubuntu, Debian, Raspbian, and Proxmox fleets.\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/mzac/apt-ui/actions/workflows/release.yml\"\u003e\u003cimg src=\"https://img.shields.io/github/actions/workflow/status/mzac/apt-ui/release.yml?branch=main\u0026label=build\u0026logo=github\" alt=\"Build\"/\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/mzac/apt-ui/security/code-scanning\"\u003e\u003cimg src=\"https://img.shields.io/badge/security-CodeQL-1f6feb?logo=github\" alt=\"CodeQL\"/\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/mzac/apt-ui/blob/main/LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/github/license/mzac/apt-ui?color=blue\" alt=\"License: MIT\"/\u003e\u003c/a\u003e\n  \u003cimg src=\"https://img.shields.io/maintenance/yes/2026\" alt=\"Maintained 2026\"/\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/mzac/apt-ui/releases/latest\"\u003e\u003cimg src=\"https://img.shields.io/github/v/release/mzac/apt-ui?label=release\u0026logo=github\" alt=\"Latest release\"/\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/mzac/apt-ui/commits/main\"\u003e\u003cimg src=\"https://img.shields.io/github/last-commit/mzac/apt-ui?logo=github\" alt=\"Last commit\"/\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/mzac/apt-ui/issues\"\u003e\u003cimg src=\"https://img.shields.io/github/issues/mzac/apt-ui\" alt=\"Open issues\"/\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/mzac/apt-ui/stargazers\"\u003e\u003cimg src=\"https://img.shields.io/github/stars/mzac/apt-ui?style=social\" alt=\"Stars\"/\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/python-3.12+-3776AB?logo=python\u0026logoColor=white\" alt=\"Python\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/fastapi-009688?logo=fastapi\u0026logoColor=white\" alt=\"FastAPI\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/sqlalchemy-async-D71F00?logo=sqlalchemy\u0026logoColor=white\" alt=\"SQLAlchemy\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/sqlite-003B57?logo=sqlite\u0026logoColor=white\" alt=\"SQLite\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/asyncssh-2f5786\" alt=\"asyncssh\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/APScheduler-orange\" alt=\"APScheduler\"/\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/react-18-61DAFB?logo=react\u0026logoColor=white\" alt=\"React\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/typescript-5-3178C6?logo=typescript\u0026logoColor=white\" alt=\"TypeScript\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/vite-7-646CFF?logo=vite\u0026logoColor=white\" alt=\"Vite\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/tailwind-06B6D4?logo=tailwindcss\u0026logoColor=white\" alt=\"Tailwind\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/zustand-state-2D3748\" alt=\"Zustand\"/\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/docker-2496ED?logo=docker\u0026logoColor=white\" alt=\"Docker\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/registry-ghcr.io-181717?logo=github\u0026logoColor=white\" alt=\"GHCR\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/arch-amd64%20%7C%20arm64-blue\" alt=\"Multi-arch\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/kubernetes-326CE5?logo=kubernetes\u0026logoColor=white\" alt=\"Kubernetes\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/tailscale-242424?logo=tailscale\u0026logoColor=white\" alt=\"Tailscale\"/\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"ARCHITECTURE.md\"\u003e📐 Architecture\u003c/a\u003e ·\n  \u003ca href=\"SECURITY.md\"\u003e🔒 Security\u003c/a\u003e ·\n  \u003ca href=\"CHANGELOG.md\"\u003e📋 Changelog\u003c/a\u003e ·\n  \u003ca href=\"https://github.com/mzac/apt-ui/releases\"\u003e📦 Releases\u003c/a\u003e\n\u003c/p\u003e\n\n---\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"images/main.png\" alt=\"apt-ui dashboard\" width=\"900\"/\u003e\n\u003c/p\u003e\n\n---\n\n\u003e 🤖 **This project was entirely written by [Claude](https://claude.ai) (Anthropic's AI assistant) via [Claude Code](https://claude.ai/code).** All code, configuration, and documentation — from the FastAPI backend and asyncssh integration to the React frontend and Docker setup — was generated through an iterative, conversation-driven development process with no manual coding.\n\n---\n\n## Why apt-ui\n\n**The fleet is the unit, not the host.** Most apt UIs are per-server. apt-ui treats your Ubuntu / Debian / Raspbian / Proxmox fleet as one thing — Check All, Upgrade All, Reboot All, Autoremove All all multiplexed into one terminal stream with per-server filter chips and live status. No more SSH'ing to twelve boxes to roll a security patch.\n\n**One container, zero agents.** Single Docker image (under 250 MB). Talks to managed servers over plain SSH — no daemons on the targets, no message bus, no Postgres, no Redis. The whole control plane is FastAPI + SQLite + APScheduler, designed to run on a Pi 4 and manage 50 servers comfortably.\n\n**Built for staged rollouts.** Tag servers with `ring:test` / `ring:prod` and auto-upgrade promotes through them in alphabetical ring order, aborting the rollout if any host fails. Maintenance windows block scheduled work outside approved hours. Pre/post-upgrade hooks let you take a BTRFS / ZFS snapshot first. Rolling reboot orchestrates kernel reboots in batches with reachability checks between them.\n\n**Security-aware, not just scheduling.** A daily CVE matcher annotates every pending package with USN / CVE-IDs sourced from the Ubuntu USN database. The fleet-wide CVE inventory pivots that data into \"which hosts are exposed to CVE-2025-XXXXX.\" A Prometheus `/metrics` endpoint feeds Grafana. Notifications cover daily summaries, weekly digests, security alerts, and reboot-required events across email / Telegram / Slack / webhook. Auth includes TOTP 2FA, scrypt-hashed API tokens, and admin / read-only RBAC.\n\n---\n\n## What's in the box\n\n\u003e One single-container control plane for an apt fleet — fleet-wide actions, scheduled automation, security visibility, and integrations to keep it honest.\n\n### 📦 Fleet management\n\n| | Feature | Highlights |\n|---|---|---|\n| 🗺 | **Dashboard \u0026 fleet view** | server card grid · update / security / reboot / autoremove counts · clickable filters · search across hostnames + tags |\n| 🏷 | **Groups \u0026 tags** | colour-coded groups (many-to-many) · freeform tags · auto-tagging by OS and virt type · ring tags drive staged rollouts |\n| ⚡ | **Fleet-wide actions** | Check All · Refresh All · Upgrade All · Autoremove All · Rolling Reboot — all multiplexed via WebSocket with per-server filter chips |\n| 📡 | **Reachability monitor** | TCP ping every 5 minutes (independent of SSH) · offline servers dimmed and banner-flagged · `is_reachable` + `last_seen` per server |\n| 🐳 | **Docker host detection** | identifies the host running the dashboard and blocks upgrades of container-runtime packages mid-flight |\n| 🔍 | **Fleet-wide package search** | five match modes (exact / contains / starts-with / ends-with / regex) · pivoted CVE-style table · diverged-version highlight |\n| ⚖️ | **Multi-server compare** | side-by-side installed-package inventory across any combination of servers · Diverged / Common / All filter modes |\n\n### 🔄 Update \u0026 upgrade\n\n| | Feature | Highlights |\n|---|---|---|\n| 📋 | **Upgradable list** | full version deltas · repo source · security flag · phased-update column · package descriptions on hover |\n| 🎯 | **Selective upgrade** | check the boxes for individual packages instead of upgrading everything |\n| 🐛 | **Dist-upgrade detection** | parallel `apt-get dist-upgrade --dry-run` surfaces new dependency packages and \"kept back\" rows that plain `upgrade` would skip |\n| 🖥 | **Live terminal** | WebSocket stream of `apt-get` output with carriage-return progress lines updating in place; ANSI colour preserved |\n| 📦 | **Package install** | search the apt cache and install new packages on any host from the UI |\n| 💿 | **.deb installs** | URL (validated, `wget`-pulled) or browser upload (SFTP'd via asyncssh) — both stream `dpkg -i` + `apt-get install -f` live |\n| 🧱 | **Templates** | named package sets applied to one or more hosts in one click — useful for provisioning identical roles |\n| 📌 | **Held packages** | per-package hold / unhold from the Packages tab; held-package chips with one-click ✕ unhold |\n| 📝 | **Apt sources editor** | tabbed editor for `/etc/apt/sources.list*` files · save / delete / create · \"Test with apt-get update\" streams live |\n\n### 🛡 Security\n\n| | Feature | Highlights |\n|---|---|---|\n| 🛡 | **CVE matcher** | daily Ubuntu USN sync · per-package severity-coloured 🛡 badge · USN + CVE links in tooltips |\n| 🚨 | **Fleet CVE inventory** | `/security` page pivots CVE → servers · severity / status / group filters · CSV export · nav badge with critical-CVE count |\n| 🔐 | **Per-server SSH keys** | Fernet-encrypted in DB · falls back to global `SSH_PRIVATE_KEY` or `SSH_AUTH_SOCK` |\n| 🛡 | **Auto security updates** | per-server `unattended-upgrades` toggle with shield-badge state · streams live SSH output when toggling |\n| 🔢 | **TOTP 2FA** | QR enrolment in Settings → Account · login flow asks for a 6-digit code when enabled |\n| 🔑 | **API tokens** | `aptui_\u003c32 url-safe bytes\u003e` format · scrypt-hashed · raw value shown only once · for `curl` / CI / scripts |\n| 👥 | **RBAC** | admin / read-only roles · `require_admin` on ~28 mutation endpoints · \"read-only\" badge in the nav |\n\n### ⏰ Automation \u0026 scheduling\n\n| | Feature | Highlights |\n|---|---|---|\n| 🗓 | **Scheduled checks** | configurable cron for fleet-wide update checks |\n| 🤖 | **Auto-upgrade** | optional hands-off upgrades on a schedule · concurrency cap · phased-update toggle · conffile-action choice |\n| 🚦 | **Maintenance windows** | global or per-server time windows where auto-upgrades are blocked · midnight-wrap · iCal feed for ops calendars |\n| 🪝 | **Pre/post-upgrade hooks** | shell commands run before / after every upgrade · pre-hook failure aborts · global or per-server scope |\n| 🎟 | **Staged rollout (rings)** | `ring:*` tags promote upgrades through environments in alphabetical order · per-batch failure aborts the rollout |\n| 🔁 | **Rolling reboot** | fleet-wide reboot of `reboot_required` servers in ring order with per-batch waits and reachability checks |\n| 🐧 | **Reboot-after-upgrade** | optional checkbox auto-reboots after a successful upgrade if `/var/run/reboot-required` exists |\n\n### 🔔 Notifications\n\n| | Channel | Notes |\n|---|---|---|\n| 📧 | **Email** | aiosmtplib · STARTTLS / SSL · HTML + text fallback |\n| ✈️ | **Telegram** | Bot API · auto-chunk for messages over 4 K |\n| 💬 | **Slack** | incoming webhook · Block Kit messages with header + section blocks |\n| 🪝 | **Webhook** | JSON POST · optional `X-Hub-Signature-256` HMAC-SHA256 |\n| | | |\n| 🗓 | **Events** | upgrade complete · upgrade error · security updates found · reboot required · daily summary · weekly digest |\n| 🎚 | **Per-channel × per-event toggles** | independently enable each event on each channel |\n| 📅 | **Weekly patch digest** | opt-in summary on a configurable cron · headline counters · by-server table · still-pending list · CVE summary · health flags |\n| 📜 | **Notification log** | every send recorded — channel, event, summary, success/failure |\n\n### 🔭 Visibility \u0026 reporting\n\n| | Feature | Highlights |\n|---|---|---|\n| 📜 | **Upgrade history** | per-server and fleet-wide log · filterable by server / status · full terminal output expandable per run |\n| 🔍 | **SSH audit log** | every command apt-ui dispatches recorded (command, exit, duration, 4 KB output excerpt) · sub-tab in History |\n| 🗒 | **dpkg log** | parses `/var/log/dpkg.log` + rotated `.gz` archives · filter by package / action / time |\n| 📊 | **Reports** | Patch Coverage · Upgrade Success Rate · Security SLA — all CSV-exportable |\n| 📈 | **Prometheus /metrics** | fleet-state counters / gauges for Grafana · optional `METRICS_TOKEN` bearer auth |\n| 🌐 | **Public /status.json** | opt-in fleet health snapshot for embedding · disabled by default |\n| 📅 | **iCal feed** | subscribable maintenance-window calendar at `/api/calendar.ics?token=…` |\n| 🕒 | **OS EOL countdown** | dashboard 🕒 badge when OS reaches end-of-life within 365 days · severity-coloured · ESM note for Ubuntu LTS |\n\n### 🧰 Server detail\n\n\u003e Each managed server gets its own page with tabs:\n\n`Packages` · `Upgrade` · `Health` · `Apt Repos` · `dpkg Log` · `History` · `Stats` · `Shell`\n\n| | Feature | Highlights |\n|---|---|---|\n| 🐧 | **OS detection** | Ubuntu · Debian · Raspbian · Armbian · Proxmox VE · Proxmox Backup Server · Proxmox Mail Gateway · bare-metal / VM / LXC / Docker via `systemd-detect-virt` |\n| 🔶 | **Proxmox VE awareness** | dedicated `pveupgrade` button · PVE-managed packages highlighted in the Packages tab |\n| 🏥 | **Health panel** | on-demand probe of `systemctl --failed`, last 20 boot-priority `journalctl` errors, recent reboot history · restart-service per failed unit |\n| 🍓 | **Raspberry Pi EEPROM** | firmware update detection for Pi 4 / 400 / CM4 / 5 · one-click apply |\n| 💾 | **Disk + boot health** | red badge when `/boot` free \u003c 100 MB or \u003c 10% · kernel install date with 60d / 180d age tinting |\n| 📸 | **Snapshot capability** | BTRFS / ZFS / LXC detected · banner with copy-pastable pre-hook command suggestion in the Upgrade tab |\n| ⚡ | **apt proxy** | detect + manage `apt-cacher-ng` proxy or `auto-apt-proxy` · live SSH output when toggling |\n\n### 🚀 Deployment\n\n| | Path | Status |\n|---|---|---|\n| 🐳 | **Docker Compose** | `docker compose up -d` — `docker-compose.ghcr.yml` pulls the prebuilt image |\n| ☸️ | **Kubernetes** | `k8s/deployment.yaml` — Deployment + ClusterIP Service + Longhorn PVC |\n| 🌐 | **Tailscale sidecar** | optional overlay — joins the container/pod to your tailnet · automatic HTTPS via `tailscale serve` |\n| 🛠 | **Build from source** | `./build-run.sh` — dev workflow with hot rebuild |\n| 🏗 | **Multi-arch images** | `linux/amd64` + `linux/arm64` published to GHCR every release |\n\n---\n\n## Quick start\n\n\u003e ⚠️ Requires Docker + Docker Compose v2 and SSH access to the target servers.\n\n### 1. Set up your `.env`\n\n```bash\ncat \u003e .env \u003c\u003cEOF\nSSH_PRIVATE_KEY=\"$(cat ~/.ssh/id_ed25519)\"\n\n# Optional but recommended — fixes JWT secret so sessions survive restarts\nJWT_SECRET=$(openssl rand -hex 32)\n\n# Optional overrides\n# TZ=America/Montreal\n# LOG_LEVEL=INFO\nEOF\n```\n\nThe key must be inside double quotes with literal newlines preserved (the heredoc above handles this).\n\n### 2a. Run from pre-built image (recommended)\n\n```bash\ndocker compose -f docker-compose.ghcr.yml up -d\n```\n\nTo pin to a specific release instead of `latest`, edit `docker-compose.ghcr.yml` and change the image tag, e.g. `ghcr.io/mzac/apt-ui:2026.05.01-03`.\n\n### 2b. Build from source\n\n```bash\n./build-run.sh\n```\n\nThe app will be available at **http://localhost:8111**.\n\nDefault login: `admin` / `admin` — **change this immediately** via Settings → Account.\n\n---\n\n## SSH authentication\n\nTwo approaches. Pick whichever fits your setup.\n\n### Option A — SSH directly as root (simplest)\n\nIf root has a password set the account is active and you can add your public key:\n\n```bash\n# Run on each managed server\nsudo mkdir -p /root/.ssh\nsudo cat ~/.ssh/id_ed25519.pub \u003e\u003e /root/.ssh/authorized_keys\nsudo chmod 600 /root/.ssh/authorized_keys\n```\n\nThen set `username = root` when adding each server in the dashboard. No sudo configuration required.\n\n### Option B — Regular user with passwordless sudo for apt-get\n\n```bash\n# Run on each managed server\necho \"youruser ALL=(ALL) NOPASSWD: /usr/bin/apt-get\" | sudo tee /etc/sudoers.d/apt-ui\n```\n\n### Key delivery\n\n| Mode | When to use |\n|---|---|\n| **Inline `SSH_PRIVATE_KEY`** | simplest; key must have no passphrase |\n| **`SSH_AUTH_SOCK` (agent)** | passphrase-protected key; forwards your host's agent into the container — the key never leaves your host |\n| **Per-server key in DB** | upload a dedicated key per managed server via the Add Server form; Fernet-encrypted at rest |\n\n---\n\n## Configuration\n\nAll runtime configuration (SMTP / Telegram / Slack / schedules / server list / users) is managed in the web UI and stored in the SQLite database at `/data/apt-ui.db`. **No restart required to change settings.**\n\n| Variable | Default | Description |\n|---|---|---|\n| `SSH_PRIVATE_KEY` | — | Full PEM content of the private key. Required unless using SSH agent. |\n| `SSH_AUTH_SOCK` | — | Path to SSH agent socket inside the container (e.g. `/run/ssh-agent.sock`). Alternative to `SSH_PRIVATE_KEY` — allows passphrase-protected keys. |\n| `JWT_SECRET` | random | JWT signing secret. Set to persist sessions across restarts. |\n| `ENCRYPTION_KEY` | — | Master key used to encrypt per-server SSH keys in the DB. Falls back to `JWT_SECRET`. |\n| `DATABASE_PATH` | `/data/apt-ui.db` | SQLite file path. |\n| `TZ` | `America/Montreal` | Timezone for scheduled jobs. |\n| `LOG_LEVEL` | `INFO` | Python log level. |\n| `ENABLE_TERMINAL` | `false` | Set `true` to enable the interactive SSH shell tab. Only enable for trusted users. |\n| `METRICS_TOKEN` | — | Optional bearer token to protect the `/metrics` endpoint. If unset the endpoint is unauthenticated. |\n| `STATUS_PAGE_PUBLIC` | `false` | Set `true` to enable the unauthenticated `/status.json` fleet health endpoint. |\n| `STATUS_PAGE_SHOW_NAMES` | `false` | Include server names (not hostnames) in `/status.json`. |\n| `STATUS_PAGE_TITLE` | `apt-ui Fleet Status` | Custom title returned by `/status.json`. |\n\n---\n\n## CLI tool\n\nAdmin operations can be run from inside the container:\n\n```bash\n# Reset password (interactive prompt)\ndocker compose exec apt-ui python -m backend.cli reset-password\n\n# Reset password inline\ndocker compose exec apt-ui python -m backend.cli reset-password --username admin --password newpass123\n\n# Create a user (admin by default; --readonly for non-admin)\ndocker compose exec apt-ui python -m backend.cli create-user --username zac --password mypass\n\n# List all users\ndocker compose exec apt-ui python -m backend.cli list-users\n```\n\n---\n\n## Tailscale\n\nThe dashboard can join your [Tailscale](https://tailscale.com) tailnet via an optional sidecar container. This gives you:\n\n- Secure remote access without exposing a port to the public internet\n- Automatic HTTPS with a Let's Encrypt cert via `tailscale serve`\n- Connection status (tailnet IP, hostname, DNS name) visible in Settings → Infrastructure\n- Works the same way in Kubernetes — the sidecar joins the pod to the tailnet\n\n### Enable Tailscale (Docker Compose)\n\nAdd to your `.env`:\n\n```\nTS_AUTHKEY=tskey-client-...   # generate at tailscale.com/settings/keys\nTS_HOSTNAME=apt-ui            # how it appears on your tailnet\n```\n\nRun with the overlay:\n\n```bash\ndocker compose -f docker-compose.yml -f docker-compose.tailscale.yml up -d\n```\n\nTailscale runs as a separate `tailscale/tailscale:latest` container, **not baked into the app image** — `docker compose pull` updates it independently of the app.\n\n### Enable `tailscale serve` (HTTPS on your tailnet)\n\n`tailscale serve` proxies HTTPS `:443` → app `:8000` and provisions a Let's Encrypt cert automatically for your node's DNS name (e.g. `apt-ui.your-tailnet.ts.net`).\n\nIn `docker-compose.tailscale.yml`, uncomment the two lines under the `tailscale` service:\n\n```yaml\n- TS_SERVE_CONFIG=/serve-config.json\n- ./tailscale-serve.json:/serve-config.json:ro\n```\n\nThe bundled `tailscale-serve.json` uses `${TS_CERT_DOMAIN}` which the Tailscale container resolves to your node's DNS name at runtime.\n\n---\n\n## Kubernetes\n\nA ready-to-use manifest is provided at [`k8s/deployment.yaml`](k8s/deployment.yaml):\n\n- 1-replica Deployment\n- ClusterIP Service on port 8000\n- PersistentVolumeClaim (Longhorn storage class — change if needed)\n- Secret references for `SSH_PRIVATE_KEY` and `JWT_SECRET`\n- Liveness + readiness probes against `GET /health`\n- Resource limits: 128–256 Mi RAM, 100m–500m CPU\n\n```bash\n# Create the secret\nkubectl create secret generic apt-ui-secrets \\\n  --from-literal=ssh-private-key=\"$(cat ~/.ssh/id_rsa)\" \\\n  --from-literal=jwt-secret=\"$(openssl rand -hex 32)\"\n\n# Deploy\nkubectl apply -f k8s/deployment.yaml\n```\n\nThe manifest also has a ready-to-uncomment Tailscale sidecar block — uncomment it and add the auth key to the secret:\n\n```bash\nkubectl create secret generic apt-ui-secrets \\\n  --from-literal=ssh-private-key=\"$(cat ~/.ssh/id_rsa)\" \\\n  --from-literal=jwt-secret=\"$(openssl rand -hex 32)\" \\\n  --from-literal=ts-authkey=\"tskey-client-...\"\n```\n\n---\n\n## Architecture\n\n```mermaid\nflowchart LR\n    SPA[\"🖥 React 18 SPA\u003cbr/\u003e10 pages · Zustand · Tailwind\"]\n\n    subgraph backend[\"🐳 apt-ui container — :8000\"]\n        direction TB\n        API[\"⚡ FastAPI\u003cbr/\u003e23 routers · 70+ REST endpoints\u003cbr/\u003e17 WebSocket streams\"]\n        WORKERS[\"⏰ APScheduler\u003cbr/\u003echeck_all · auto_upgrade · ping_all\u003cbr/\u003eweekly_digest · daily_summary · log_purge\"]\n        DB[(\"💾 SQLite\u003cbr/\u003e20 tables · 50+ migrations\u003cbr/\u003eFernet-encrypted SSH keys\")]\n        SSH[\"🔗 asyncssh\u003cbr/\u003eFresh connection per command\u003cbr/\u003ePer-server key → agent → global\"]\n    end\n\n    SERVERS[\"🖧 Managed Linux fleet\u003cbr/\u003eUbuntu · Debian · Raspbian · Armbian\u003cbr/\u003eProxmox VE / PBS / PMG · Raspberry Pi\"]\n\n    NOTIF[\"🔔 Notifications\u003cbr/\u003e📧 SMTP · ✈️ Telegram\u003cbr/\u003e💬 Slack · 🪝 Webhook (HMAC)\"]\n\n    SPA -- \"REST + WebSocket\u003cbr/\u003eJWT cookie\" --\u003e API\n    API \u003c--\u003e DB\n    API --\u003e SSH\n    WORKERS \u003c--\u003e DB\n    WORKERS --\u003e SSH\n    WORKERS --\u003e NOTIF\n    API --\u003e NOTIF\n    SSH -- \"SSH :22\" --\u003e SERVERS\n\n    classDef frontend fill:#3b82f6,stroke:#1d4ed8,color:#fff,stroke-width:2px\n    classDef api fill:#10b981,stroke:#047857,color:#fff,stroke-width:2px\n    classDef sched fill:#f59e0b,stroke:#b45309,color:#fff,stroke-width:2px\n    classDef data fill:#8b5cf6,stroke:#6d28d9,color:#fff,stroke-width:2px\n    classDef transport fill:#6366f1,stroke:#4338ca,color:#fff,stroke-width:2px\n    classDef external fill:#475569,stroke:#1e293b,color:#fff,stroke-width:2px\n    classDef notif fill:#ec4899,stroke:#be185d,color:#fff,stroke-width:2px\n\n    class SPA frontend\n    class API api\n    class WORKERS sched\n    class DB data\n    class SSH transport\n    class SERVERS external\n    class NOTIF notif\n```\n\n\u003e See [ARCHITECTURE.md](ARCHITECTURE.md) for full diagrams, request-flow details, data model, and CI/CD pipeline documentation.\n\n### Tech stack\n\n| Layer | Library / Tool |\n|---|---|\n| **Backend** | Python 3.12 · FastAPI · Uvicorn |\n| **Auth** | passlib[bcrypt] · PyJWT (HS256, 24 h httpOnly cookie) · pyotp (TOTP) · scrypt (API tokens) |\n| **SSH** | asyncssh — fresh connection per command, `known_hosts=None` (trusted LAN) |\n| **Encryption** | Fernet (AES-128-CBC + HMAC-SHA256) — per-server SSH keys + TOTP secrets at rest |\n| **Database** | SQLite · SQLAlchemy 2.x async · aiosqlite |\n| **Scheduler** | APScheduler 3.x AsyncIOScheduler — live reconfiguration, no restart needed |\n| **Notifications** | aiosmtplib (email) · httpx (Telegram / Slack / webhook with HMAC-SHA256) |\n| **Frontend** | React 18 · TypeScript · Vite · Tailwind CSS |\n| **State** | Zustand (auth + job store + servers store) |\n| **Charts** | Recharts |\n| **Terminal** | ansi-to-html (apt output) · @xterm/xterm (interactive shell) |\n| **Container** | Multi-stage Dockerfile — `node:20-alpine` build → `python:3.12-slim` runtime |\n| **Registry** | GitHub Container Registry — `linux/amd64` + `linux/arm64` |\n| **CI/CD** | GitHub Actions · CodeQL · Dependabot · multi-arch release pipeline |\n\n---\n\n## Development\n\n### Backend\n\n```bash\npython -m venv venv \u0026\u0026 source venv/bin/activate\npip install -r backend/requirements.txt\n\nexport SSH_PRIVATE_KEY=\"$(cat ~/.ssh/id_rsa)\"\nexport DATABASE_PATH=\"./data/dev.db\"\nexport PYTHONPATH=$(pwd)\nuvicorn backend.main:app --reload --port 8000\n```\n\n### Frontend\n\n```bash\ncd frontend\nnpm ci\nnpm run dev   # Vite dev server on :5173, proxies /api/* to :8000\n```\n\n### Local CI\n\n```bash\nmake ci      # mirrors GitHub Actions: Python syntax + import check + frontend build\nmake venv    # bootstrap a Python venv\nmake help    # list all targets\n```\n\n---\n\n## Project status\n\napt-ui ships on a **calendar versioning** cadence (`YYYY.MM.DD-NN`) — releases happen when a wave of features is ready, not on a fixed schedule. Every release publishes multi-arch (`linux/amd64` + `linux/arm64`) images to GHCR.\n\n| Area | Status |\n|---|---|\n| Core fleet management (Check / Upgrade / Reboot / Autoremove All, dashboard, groups, tags) | ✅ Stable |\n| Auth + RBAC + 2FA + API tokens | ✅ Stable |\n| Notifications (email / Telegram / Slack / webhook · daily summary · weekly digest) | ✅ Stable |\n| Maintenance windows · pre/post hooks · staged rollouts · rolling reboot | ✅ Stable |\n| CVE matcher + fleet CVE inventory + Prometheus `/metrics` + status page | ✅ Stable |\n| dpkg log · upgrade history · SSH audit log · reports (Patch Coverage / Success Rate / Security SLA) | ✅ Stable |\n| Proxmox VE / PBS / PMG awareness · Raspberry Pi EEPROM · OS EOL countdown | ✅ Stable |\n| Deployment: Docker Compose · Kubernetes · Tailscale sidecar | ✅ Stable |\n| WebAuthn / passkeys | ⛔ Out of scope (TOTP covers the 2FA need; WebAuthn requires HTTPS + stable origin which homelab apt-ui rarely has) |\n| Full automated snapshot/rollback | ⛔ Out of scope (snapshot capability + banner shipped; full automation deferred — pre-upgrade hooks let users wire whatever fits their layout) |\n\nSee [CHANGELOG.md](CHANGELOG.md) for the per-release feature list.\n\n---\n\n## Documentation\n\n| Document | Description |\n|---|---|\n| [ARCHITECTURE.md](ARCHITECTURE.md) | Full architecture diagram, router → file map, data model, CI/CD pipeline |\n| [CHANGELOG.md](CHANGELOG.md) | Per-release feature list and bug-fix history |\n| [SECURITY.md](SECURITY.md) | Security policy, vulnerability disclosure, threat model notes |\n| [CLAUDE.md](CLAUDE.md) | Authoritative spec for future Claude Code sessions |\n\n---\n\n## License\n\nReleased under the [MIT License](LICENSE).\n\n---\n\n\u003cp align=\"center\"\u003e\n  Built with ❤️ by Claude — entirely AI-written via \u003ca href=\"https://claude.ai/code\"\u003eClaude Code\u003c/a\u003e.\n\u003c/p\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmzac%2Fapt-ui","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmzac%2Fapt-ui","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmzac%2Fapt-ui/lists"}