nixos/panel.nix
agent 76a1e09913 panel: redesign web UI and make syncing responsive
Web UI (panel/frontend/index.html) rewritten:
- Cards update in place from a single /status poll instead of being rebuilt
  on every action, so open tabs, unsaved compose/route edits, logs and the
  file browser position survive refreshes. Polling speeds up while an
  operation runs and pauses in background tabs; a header indicator shows
  when the panel last synced and detects an expired Authelia session.
- New-app dialog (starter / compose / git), suggested port and domain,
  proper confirm dialogs (the old "OK = keep volumes" remove prompt is gone),
  toasts, an activity drawer with operation output, overflow menu, search,
  status filters, keyboard shortcuts, deep links, dark mode and mobile layout.
- Tabs: overview (containers + routes), compose editor (dirty tracking,
  Ctrl+S), logs with follow, validated routes editor, file browser with
  drag-and-drop upload, backups, and a git source tab (deployed commit,
  check for updates, sync & deploy).

API (panel/panel-api.py):
- ThreadingHTTPServer so a long deploy no longer blocks every other request.
- Per-app operation lock; concurrent writes to a busy app return 409.
- GET /status: all apps, routes and container status in one request
  (statuses gathered in parallel); status reports running/partial/stopped.
- Git sync is fetch + hard reset instead of pull-or-reclone, keeps the stored
  token, reports before/after commits; GET /apps/<name>/repo[?fetch=1].
- Any http(s) git host (e.g. Forgejo), default branch detection, git
  timeouts, no credential prompts, tokens redacted from errors, and manifest
  values validated before being written into the bash-sourced manifest.

panelctl:
- flock around routes.caddy rewrites (util-linux added to the service path).
- deploy returns compose output so failures are visible in the UI.
- inspect-volumes no longer fails for apps without named podman volumes,
  which broke the file browser.

Docs: README/API.md updated; fixed outdated panelctl init examples.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UbWSNkXxZhYf7eqHTyx3Bf
2026-09-26 22:17:07 +00:00

79 lines
No EOL
2.3 KiB
Nix

{ config, pkgs, ... }:
{
environment.systemPackages = [
(pkgs.writeShellScriptBin "panelctl" (builtins.readFile ./panel/panelctl.sh))
];
users.groups.panelroutes = { };
users.users.reudy.extraGroups = [ "panelroutes" ];
users.users.caddy.extraGroups = [ "panelroutes" ];
systemd.tmpfiles.rules = [
"d /var/lib/containers 0750 reudy panelroutes -"
"d /var/lib/containers/stacks 0750 reudy panelroutes -"
"d /var/lib/containers/volumes 0750 reudy panelroutes -"
"d /var/lib/containers/routes 0750 reudy panelroutes -"
"d /var/lib/containers/state 0750 reudy panelroutes -"
"d /var/lib/containers/state/apps 0750 reudy panelroutes -"
"d /var/lib/containers/backups 0750 reudy panelroutes -"
"f /var/lib/containers/routes/routes.caddy 0640 reudy panelroutes -"
];
systemd.services.panel-api = {
description = "Minimal panel API service";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = [
pkgs.podman
pkgs.podman-compose
pkgs.curl
pkgs.coreutils
pkgs.zip
pkgs.unzip
pkgs.git
pkgs.util-linux # flock, used by panelctl to serialise routes file writes
];
serviceConfig = {
Type = "simple";
User = "reudy";
Group = "panelroutes";
Restart = "always";
RestartSec = 3;
WorkingDirectory = "/var/lib/containers";
ExecStart = "${pkgs.python3}/bin/python3 ${./panel/panel-api.py}";
};
environment = {
PANEL_API_BIND = "127.0.0.1";
PANEL_API_PORT = "9911";
PANEL_BASE_DIR = "/var/lib/containers";
PANELCTL_PATH = "/run/current-system/sw/bin/panelctl";
PANEL_FRONTEND_DIR = "${./panel/frontend}";
};
};
services.caddy.virtualHosts."panel.srazka.com".extraConfig = ''
forward_auth 127.0.0.1:9091 {
uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
}
reverse_proxy 127.0.0.1:9911
'';
systemd.paths."caddy-routes-reload" = {
wantedBy = [ "multi-user.target" ];
pathConfig = {
PathChanged = "/var/lib/containers/routes/routes.caddy";
};
};
systemd.services."caddy-routes-reload" = {
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.systemd}/bin/systemctl reload caddy.service";
};
};
}