sysdeck (0.4.6-1) unstable; urgency=medium

  * QCrows format-aware Kata bridge: qcrows-list parses metadata.toml +
    menu.toml in memory; new qcrows-inspect / qcrows-verify subcommands
    mirror cockpit-kata's master verification (kernel magic, config,
    sha256 hash walk) without extracting to disk. Kata panel enriched
    with image metadata + Verify/Inspect actions.
  * integrity.score() returns None on PermissionError (root-only
    /var/log/lynis.log no longer crashes unprivileged sessions).

 -- Jeremy Anderson <info@dcos.net>  Sun, 27 Sep 2026 01:13:57 +0000

sysdeck (0.4.5-1) unstable; urgency=medium

  * PRODUCTION-HARDENING RELEASE — full MoE QA pass (web designers,
    backend, JS/React/Next, CSS, UI/UX, algorithms, Linux systems,
    DevOps).
  * Build: the web-dev recipe is attached to its own target (a spliced
    recipe ran fester + the dev console from uninstall-branding); master
    tarball excludes dev/server logs and .env; dist is reproducible
    (sorted, pinned mtime/owner) and gated on a clean git tree when one
    exists; distcheck uses mktemp; recipes quote every rm path.
  * Bridge: prometheus push-log validates the module filename and escapes
    exposition labels (root-level path traversal + metric-line
    injection closed); db query admits one read-only statement (no
    batches, no CLI meta-commands; sqlite refuses honestly instead of
    querying nothing); glances snapshot family degrades to
    {available:false} with a timeout; package mutations survive the
    600s timeout; vmdb2 builds carry the mkosi output-dir guard; theme
    variable values are allowlisted against CSS injection; auth/vault/
    firmware/fleet/integrity helpers run with hard timeouts; kerberos
    status derives from the real TGT end time; unwhitelist matches
    exactly; modules3p renders usage errors as JSON.
  * Web console: theme switching goes through applySdTheme (light
    themes keep their palette, the mirror stays in sync); cockpit
    modules table renders valid rows; overview reports the registry
    version and a live tarball link; fester health probes are cached
    and single-flight; usePoll rejects stale responses; the session
    clock is hydration-safe; tsc + eslint run as build gates (both
    green).
  * Firewall templates: all six nftables rulesets validate
    structurally; table-scoped flush replaces `flush ruleset` on every
    template (docker/libvirt tables survive an apply); no-services
    gains loopback accept, valid jump syntax, braced set-adds, and the
    SSH-port fix that previously locked operators out; vps-webserver SSH
    rule carries an explicit verdict; the Cilium default policy scopes
    its HTTP method filter to 80/443 and resolves DNS in standalone
    mode.
  * Packaging: RPM %files matches the 27-plugin install and the spec
    builds noarch; debian gains the nodejs build dependency and stops
    recommending media/virtualization stacks; pacman hooks live in
    sysdeck.install; AppStream addon extends the cockpit component;
    Caddyfile's port gateway is a 3010 allowlist.
  * Comments state standing decisions in the present tense throughout
    the first-party tree.

 -- Jeremy Anderson <info@dcos.net>  Thu, 17 Sep 2026 10:00:00 -0400

sysdeck (0.4.4-1) unstable; urgency=medium

  * v0.4.4: ten package-manager backends on both editions + the blog
    essay. The cockpit packages bridge (bridge/packages.py) carries
    the same ten-manager step-down as the web console — pacman,
    emerge (corroborated by /var/db/pkg), lunar, sorcery, xbps
    (probed via xbps-query), apk, zypper, dnf, yum, apt — with a
    unified MUTATION_CMDS table, header-located zypper table parsing,
    an emerge update regex anchored after the class bracket (the old
    capture grabbed the bracket and dropped every row), and honest
    capability reporting for managers without an update preview
    (lunar). The web console gains the same parser fixes and the
    xbps-query detection probe. BLOG.md is now a single long-form
    technical essay (the shellm blog pattern), not release notes.

 -- Jeremy Anderson <info@dcos.net>  Sat, 12 Sep 2026 18:00:00 -0400

sysdeck (0.4.3-1) unstable; urgency=medium

  * v0.4.3: the MoE QA pass — production hardening across every axis.
    stdin-piped privileged writes with verification, comment injection
    guards, mktemp staging, admin-gated mutations (SYSDECK_MUTATIONS),
    honest dry-runs and unbans, XFF trust gating, TTL + single-flight
    polling caches, and cockpit-side bridge parity (sensors chain,
    dnf rc-100, subprocess timeouts).

 -- Jeremy Anderson <info@dcos.net>  Sat, 12 Sep 2026 06:30:00 -0400

sysdeck (0.4.2-1) unstable; urgency=medium

  * v0.4.2: the zero-demo release — production implementations only.
    The sensors bridge reads the real lm-sensors JSON (sensors -j)
    with an honest sysfs fallback; the netsec ban layer enforces for
    real (atomic nftables batch: table inet sysdeck + blacklist set
    with 30d timeouts, iptables DROP fallback) and merges the live
    fail2ban ban list; the firewall panel gains a live-ruleset tab
    (real nft -j list ruleset / iptables-save) and its template
    catalog now carries all seven shipped topologies; LUKS header
    backups are hashed from the actual image. The bridge envelope no
    longer admits a 'demo' source at the type level — every module
    reads real host state or fails honestly.

 -- Jeremy Anderson <info@dcos.net>  Sat, 12 Sep 2026 20:00:00 -0400

sysdeck (0.4.1-1) unstable; urgency=medium

  * v0.4.1: cockpit module detection for the web console — every
    installed cockpit module (distro modules like cockpit-machines and
    cockpit-podman, addons, anything with a manifest menu entry) is
    scanned from /usr/share/cockpit and loaded into the console
    navigation: a "Cockpit" sidebar group with per-module detail views
    (manifest, shipped files, backend version probes) and jumps to the
    native panels covering each domain. SYSDECK_COCKPIT_SCAN adds
    extra scan roots for staged trees. UI codenames retired — the
    console subtitle is now dcos.net (login banner, sidebar, status
    bar); page title is "SysDeck".

 -- Jeremy Anderson <info@dcos.net>  Sat, 12 Sep 2026 18:00:00 -0400

sysdeck (0.4.0-1) unstable; urgency=medium

  * v0.4.0: Unix-account login for the web edition, the Cockpit way.
    Username + password verified by the host PAM stack
    (web/scripts/pam-auth.py, stdlib ctypes client of libpam; service
    "sysdeck" when /etc/pam.d/sysdeck exists, else the stock "login"
    stack; credentials over stdin, never argv). Session cookies are
    v2 user-bound tokens (v1 still verifies — upgrades keep sessions).
    Cockpit-style shell identity: account menu with avatar,
    user@host, PAM/local provenance, wheel "Administrative access"
    badge, live session-expiry countdown; status bar carries
    user@host. Auth modes: pam (default, run as root), pam+local
    (SdUser scrypt fallback for unprivileged installs), local
    (console accounts, managed by scripts/manage-users.mjs). Login
    failures rate limited per-IP AND per-username; audited with the
    unix username as actor. Fester gates REST+WS on the same v2
    token. Cockpit edition untouched — all modules keep working.

 -- Jeremy Anderson <info@dcos.net>  Sat, 12 Sep 2026 12:00:00 -0400

sysdeck (0.3.1-1) unstable; urgency=medium

  * v0.3.1: cockpit-style login for the web edition — shared password
    (SYSDECK_WEB_PASSWORD, default 'sysdeck' with an on-screen nag),
    HMAC-signed 12h session cookie, page-level server gate, all /api/*
    routes 401 until signed in, login/logout audited, per-IP login
    rate limit. The fester mini-service verifies the identical session
    token against the shared SQLite secret, so the direct WebSocket
    event stream (port-gateway path) is gated too. LAN-side posture
    unchanged: loopback binds stay the outer boundary.
  * Version surfaces bumped 0.3.0 -> 0.3.1.

 -- Jeremy Anderson <info@dcos.net>  Sun, 13 Sep 2026 12:00:00 -0400

sysdeck (0.3.0-1) unstable; urgency=medium

  * v0.3.0 AI GATEWAY EDITION: klanker-gate (the Frosty Deno LLM gateway,
    Deno 2 + TypeScript, own independent version 0.9.0) is vendored at
    /klanker-gate with complete Arch Linux packaging (arch/: PKGBUILD,
    hardened systemd unit, sysusers/tmpfiles, run wrapper, INSTALL-ARCH.md
    runbook). The Arch port needed ZERO upstream source changes — the
    codebase is Linux-first, not Windows-first, as commonly believed.
  * NEW klanker module (AI Gateway) in both editions: bridge/klanker.py
    (9 subcommands: status/providers/models/vkeys/logs/analytics/runtime/
    service/journal — stdlib REST client against the gateway on
    KLANKER_URL, Bearer KLANKER_ADMIN_TOKEN, graceful offline JSON) and
    the fully-built plugins/sysdeck-klanker panel; web edition gets the
    hybrid AI Gateway panel (live REST when the gateway runs, badged demo
    data otherwise, KLANKER_URL env).
  * bridge.js klanker surface: 10 methods; check-bridge-subcommands now
    verifies 215 calls across 28 bridge modules (was 206/27); 28 plugin
    manifests (was 27).
  * Version surfaces bumped 0.2.0 -> 0.3.0 (Makefile, bridge/__init__.py,
    setup.py, PKGBUILD, spec, debian/changelog, compat-manifest,
    metainfo, version-sync test).

 -- Jeremy Anderson <info@dcos.net>  Fri, 11 Sep 2026 12:00:00 -0400

sysdeck (0.2.0-1) unstable; urgency=medium

  * v0.2.0 MASTER EDITION: one tarball bundling the cockpit edition, the
    new SysDeck Web Edition (web/, Next.js console, 28 bridge modules),
    and Fester pre-integrated (web/mini-services/fester, vendored at its
    own independent version 0.2.1).
  * bridge/fester.py rewritten from the v0.0.31 systemd-listing stub to a
    real REST client (11 subcommands, FESTER_URL override, graceful
    offline JSON); plugins/sysdeck-fester is a full panel; the shared
    bridge.js fester surface grew from 1 method to 11.
  * Makefile: recipes are tab-indented (GNU make rejects 8-space indents
    with "missing separator"; a build-time guard enforces it); new targets
    fester-start, web-install, web-dev, master.

 -- Jeremy Anderson <info@dcos.net>  Thu, 20 Aug 2026 12:00:00 -0400

sysdeck (0.1.3-1) unstable; urgency=critical

  * v0.1.3 CRITICAL FIX: host package import was silently failing +
    mkosi still wasn't reading the profile config. Two root causes
    fixed, plus new download/manage UI for builds.

  * IMPORT BUG (root cause): _detect_host_packages() relied on
    `from __init__ import PKG_MANAGER` which silently failed in the
    cockpit superuser channel context (different Python path). When
    the import failed, PKG_MANAGER defaulted to "unknown" and the
    host query returned an EMPTY list. The operator saw "tries to
    build only 2" because the import wrote nothing and the build
    used the profile's original template packages.
    FIX: _detect_host_packages() now uses shutil.which() to find
    pacman/apt-mark/dnf directly — no import dependency, works in
    any execution context.

  * BUILD BUG (root cause): v0.1.2's --include flag does NOT work
    as a config loader. mkosi's --include includes a drop-in fragment
    ON TOP OF the base mkosi.conf — it does NOT replace the base
    config. If there's no mkosi.conf in the cwd, mkosi uses defaults
    and ignores the --include file entirely. This is why v0.1.2 still
    produced builds with only 2 packages (mkosi's hardcoded base).
    FIX: build() now creates a temp directory, symlinks the profile
    file into it as `mkosi.conf`, and sets work_dir to that temp dir.
    mkosi finds `mkosi.conf` (the symlink), follows it, reads the
    actual profile. Works for ANY profile path regardless of filename
    or location. Temp dir is cleaned up after the build finishes.
    New helper: _prepare_mkosi_work_dir().

  * NEW FEATURE: artifact download + management UI. Each artifact in
    the Artifacts panel now has:
    - ⬇ Download button — reads the file via cockpit.spawn(["cat",
      path]) with superuser, creates a Blob, triggers browser download.
    - 🗑 delete button — removes a single artifact file via the new
      artifact-delete subcommand.
    - 🗑 Clear all button — removes ALL artifacts for a profile via
      the new artifacts-clear subcommand. Shows file count + bytes
      freed.
    Each profile's artifacts card now shows total size in the header.

  * NEW FEATURE: build management. Each build in the Builds table
    now has a 🗑 delete button. Two-step confirm:
    1. "Delete build record?" — OK = delete state + log only.
    2. If Cancel: "Also delete ALL artifacts for profile?" — OK =
       delete state + log + artifacts dir.
    New subcommand: build-delete <build-id> [--artifacts]. Reads the
    state file FIRST (to get the profile name for artifact cleanup)
    before deleting it.

  * REGRESSION TESTS: 11 new unit tests across two new test classes:
    - TestBuilderArtifactManagement (7 tests): artifact-delete file
      removal + path traversal refusal, artifacts-clear, build-delete
      with/without --artifacts, nonexistent build-id, COMMANDS
      registration.
    - TestBuilderMkosiTempWorkDir (3 tests): _prepare_mkosi_work_dir
      creates temp dir with mkosi.conf symlink, returns None for
      missing path / nonexistent file.
    Existing test_detect_host_packages_pacman rewritten to mock
    shutil.which instead of __import__. test_build_success_path
    updated to no longer expect --include on the command line.
    Total: 254 tests (was 243 in v0.1.2; +11).

  * VERSION SYNC: bumped 0.1.2 -> 0.1.3 across all 9 release surfaces.

 -- Jeremy Anderson <info@dcos.net>  Tue, 19 Aug 2026 03:00:00 -0400

sysdeck (0.1.2-1) unstable; urgency=critical

  * v0.1.2 CRITICAL FIX: mkosi was not reading the profile config at
    all — packages were silently ignored. An operator reported: "the
    builder absolutely does not work yet. it has zero awareness of
    packages we tell it to add."

  * ROOT CAUSE 1 (config not loaded): _backend_build_command() for
    mkosi was ["mkosi", "build", "--output", ..., "--output-dir", ...]
    with NO flag telling mkosi WHERE the profile config file is. mkosi
    only reads a file literally named `mkosi.conf` from the cwd. For
    v0.0.x profiles at /etc/mkosi/mkosi.conf.d/<name>.conf, mkosi ran
    in that dir, found no `mkosi.conf` (the file is named
    <name>.conf), and used EMPTY defaults — zero packages, default
    distro, default everything. The operator's Packages= setting was
    never seen by mkosi.

  * FIX 1: _backend_build_command() now ALWAYS passes
    --include <profile_path> on the CLI. This tells mkosi to
    explicitly load the profile config by path, regardless of its
    filename or location. CLI --include overrides the default
    mkosi.conf discovery.

  * ROOT CAUSE 2 (legacy Packages= syntax): even when mkosi DID
    read the profile file (e.g. v0.1.0+ profiles with correct
    location), profiles created by v0.0.x used the old indented
    Packages= syntax:
      Packages=
          linux
          linux-firmware
    mkosi v22+ (Arch ships 25.x) only understands single-line:
      Packages=linux linux-firmware
    The old form is silently parsed as a single package name with
    embedded newlines ("linux\nlinux-firmware\n..."), which doesn't
    exist in any repo — so mkosi installs NOTHING.

  * FIX 2: new _migrate_legacy_mkosi_packages() function detects
    the old indented syntax and rewrites it to single-line IN-PLACE
    before the build command is constructed. build() calls this
    automatically on every mkosi build. The migration is logged
    in both the build state JSON (warnings array) and the log file
    header (# MIGRATED: ...). If the file already uses modern
    syntax, the migration is a no-op.

  * REGRESSION TESTS: 4 new unit tests in TestBuilderBuildPath:
    - test_migrate_rewrites_old_indented_syntax: verifies old
      Packages=\n    linux\n    vim\n is rewritten to
      Packages=linux vim.
    - test_migrate_noop_on_modern_syntax: verifies already-modern
      files are left unchanged.
    - test_migrate_noop_on_no_packages_section: verifies files
      without [Packages] are left unchanged.
    - test_migrate_runs_during_build: end-to-end — build() with
      a profile containing old syntax auto-migrates before mkosi
      runs, and the migration is recorded in state + log.
    Existing test_build_success_path extended to verify --include
    is on the command line and points at the profile file.

  * VERSION SYNC: bumped 0.1.1 -> 0.1.2 across all 9 release
    surfaces. Total unit tests now 243 (was 239 in v0.1.1; +4).

 -- Jeremy Anderson <info@dcos.net>  Tue, 19 Aug 2026 02:00:00 -0400

sysdeck (0.1.1-1) unstable; urgency=high

  * v0.1.1 OUTPUT PATH SAFETY FIX. An operator reported:
    "this is NOT a safe output path. fix this now." The v0.1.0
    release relied on OutputDirectory= in the scaffolded
    mkosi.conf to route build outputs to
    /var/lib/sysdeck/builder/artifacts/<name>/. But when the
    operator built an OLD v0.0.x profile (whose mkosi.conf had
    no OutputDirectory= setting), mkosi defaulted to writing
    image.raw into the cwd — which was /etc/mkosi/mkosi.conf.d/,
    a system config directory owned by root. mkosi then refused
    to overwrite the existing image.raw, blocking every rebuild.

  * ROOT CAUSE: _backend_build_command() for mkosi was just
    `["mkosi", "build"]` with no CLI output flags. It trusted
    the profile's mkosi.conf to set OutputDirectory=, which:
    - Doesn't exist on v0.0.x profiles (silent default to cwd)
    - Can be hand-edited to anything (no validation)
    - Is ignored by mkosi if the profile is a drop-in fragment
      that mkosi never reads (the v0.0.x bug 1 from v0.1.0)

  * FIX: _backend_build_command() now ALWAYS passes --output,
    --output-dir, and --force on the CLI for mkosi builds. CLI
    flags override mkosi.conf, so the output path is forced to
    /var/lib/sysdeck/builder/artifacts/<name>/<name>.raw
    regardless of what the profile says. --force overwrites any
    existing image so rebuilds don't fail with "Output path
    exists already."

  * SAFETY CHECK: build() now refuses to proceed if the resolved
    output_dir is not under /var/lib/, /tmp/, /var/tmp/, or the
    configured BUILDER_ARTIFACTS_DIR. This blocks /etc/, /usr/,
    /boot/, /bin/, /sbin/, /lib/, /root/, /home/, etc. — anywhere
    a stray image.raw would corrupt the system or pollute a
    user's home. Belt-and-suspenders: even if an operator passes
    options.output_dir=/etc/something via the JS bridge, the
    build is refused before subprocess.run is called.

  * LEGACY PROFILE WARNING: build() now detects profiles in
    /etc/mkosi/mkosi.conf.d/ (the v0.0.x drop-in layout) and
    records a warning in both the build state JSON and the log
    file: "WARNING: profile is in /etc/mkosi/mkosi.conf.d/
    (legacy v0.0.x layout). mkosi may silently ignore this
    drop-in fragment. Migrate to /etc/mkosi/profiles/<name>/
    mkosi.conf for a real profile."

  * LOG IMPROVEMENT: build log header now includes the resolved
    output_dir so the operator can see exactly where the image
    will land before mkosi starts. Format:
      $ mkosi build --output myarch.raw --output-dir /var/lib/...
      # work_dir: /etc/mkosi/profiles/myarch
      # backend: mkosi
      # profile: myarch
      # output_dir: /var/lib/sysdeck/builder/artifacts/myarch

  * REGRESSION TESTS: 2 new unit tests in TestBuilderBuildPath:
    - test_build_refuses_output_dir_under_etc: verifies the
      safety check rejects output_dir=/etc/mkosi/evil.
    - test_build_legacy_v050_profile_records_warning: verifies
      building a profile in /etc/mkosi/mkosi.conf.d/ records
      the legacy warning in state + log.
    The existing test_build_success_path was extended to verify
    the mkosi command line includes --output, --output-dir, and
    --force, and that --output-dir points at the per-profile
    artifacts dir.

  * VERSION SYNC: bumped 0.1.0 -> 0.1.1 across all 9 release
    surfaces. Total unit tests now 239 (was 237 in v0.1.0; +2).

 -- Jeremy Anderson <info@dcos.net>  Tue, 19 Aug 2026 01:00:00 -0400

sysdeck (0.1.0-1) unstable; urgency=medium

  * v0.1.0 BUILDER PROFILE FIXUP + HOST PKG IMPORT. Three compounding
    bugs in the v0.0.x mkosi build path were silently producing empty
    33M images with no kernel/systemd/openssh, plus a new operator
    feature requested in the same release cycle.

  * BUG 1 (scaffold location): profile-create wrote
    /etc/mkosi/mkosi.conf.d/<name>.conf — a drop-in fragment that
    mkosi only honors when a parent /etc/mkosi/mkosi.conf exists to
    layer it onto. With no parent, mkosi ran with empty defaults.
    Fix: each profile now lives in its own directory
    /etc/mkosi/profiles/<name>/mkosi.conf (the only filename mkosi
    reads automatically from the cwd). MKOSI_DIRS updated to scan
    /etc/mkosi/profiles first.

  * BUG 2 (Packages= syntax): _MKOSI_TEMPLATE and _write_packages_mkosi
    used the indented-continuation form (Packages=\n    linux\n    ...)
    which was the old systemd-mkosi (<=v15) syntax. mkosi v22+ (Arch
    ships 25.x) expects single-line space-separated:
    Packages=linux linux-firmware systemd openssh. The v0.0.x form
    was silently parsed as a single package named "linux\n..." and
    failed to install.
    Fix: template + writer now emit the modern single-line form. The
    reader accepts both forms so v0.0.x profiles migrate cleanly on
    first append/replace.

  * BUG 3 (output routing): mkosi wrote its output to the cwd
    (/etc/mkosi/mkosi.conf.d/image.raw) but build() only scanned
    /var/lib/sysdeck/builder/artifacts/<profile>/ for artifacts — so
    every successful build looked like a failure in the panel.
    Fix: _MKOSI_TEMPLATE now sets OutputDirectory= to the per-profile
    artifacts dir so mkosi writes directly there.

  * NEW FEATURE: profile-import-packages subcommand. Queries the
    host's explicitly-installed package set (pacman -Qqe on Arch,
    apt-mark showmanual on Debian, dnf repoquery --userinstalled on
    Fedora) and writes it into a profile's package list via the
    existing _write_packages dispatch. Defaults to append mode so
    the profile's baseline (kernel, systemd, openssh) is preserved.
    Supports --mode=replace, --dry-run for preview, and --packages=
    for manual override (useful for importing a list captured on
    another host). New polkit exec paths for pacman/apt-mark/dnf
    added to org.sysdeck.builder.modify.

  * PANEL UX: each profile row in the Builder panel now has a
    "Import host pkgs" button. Click -> dry-run preview ->
    window.confirm with package count, source distro, and first
    200 packages -> append write. Falls back to operator cancel
    without writing.

  * REGRESSION TESTS: 7 new unit tests in TestBuilderImportHostPackages
    cover _detect_host_packages dispatch (pacman path + dedup), the
    --packages override end-to-end, --dry-run no-write behavior, and
    the unknown-profile / no-args / bad-mode / COMMANDS-registration
    error paths. 4 existing tests in TestBuilderPackagesField
    updated for the new single-line Packages= syntax; 1 new test
    (test_mkosi_modern_single_line_input_parsed) guards against a
    regression where the writer emits the new form but the reader
    only understands the old one.

  * VERSION SYNC: bumped 0.0.50 -> 0.1.0 across all 9 release
    surfaces (Makefile, bridge/__init__.py, packaging/setup.py,
    packaging/PKGBUILD, packaging/sysdeck.spec,
    packaging/debian/changelog, compat/compat-manifest.json,
    packaging/sysdeck.metainfo.xml, README.md). Version-sync test
    renamed to test_version_sync_all_surfaces_report_010.

  * All build-time guards pass. Total unit tests now 237 (was 228
    in v0.0.50; +8 TestBuilderImportHostPackages + 1 new
    test_mkosi_modern_single_line_input_parsed).

 -- Jeremy Anderson <info@dcos.net>  Tue, 19 Aug 2026 00:00:00 -0400

sysdeck (0.0.50-1) unstable; urgency=medium

  * v0.0.50 BUILD PATH NameError FIX. An operator reported:
    "NameError: name 're' is not defined. Did you forget to import
    're'? happens right away on build for a new profile i created."
    The traceback pointed at _new_build_id() line 492:
    safe_profile = re.sub(r"[^A-Za-z0-9_-]", "_", profile).

  * ROOT CAUSE: bridge/builder.py's module-level imports were
    `import json / os / shutil / subprocess / sys` + `from pathlib
    import Path` + `from typing import Any`. No `import re`.
    _new_build_id has used re.sub since v0.0.31 (when the full-
    featured build operations were added), but no test ever
    exercised the build() code path — the unit tests only covered
    profile_create / profile_copy / profile_delete and the v0.0.49
    package-writing helpers. The bug went undetected for 18 releases
    (v0.0.31 through v0.0.49) until an operator actually clicked
    Build on a freshly-created profile.

  * FIX: added `import re` to the module-level imports in
    bridge/builder.py. Removed the now-redundant local `import re`
    inside _write_packages_vmdb2 (it was a v0.0.49 workaround that's
    no longer needed — the module-level import covers both callers).

  * REGRESSION TESTS: 9 new unit tests in tests/test_bridge_parsers.py
    TestBuilderBuildPath cover:
    - _new_build_id format: <safe-profile>-<14-digit-timestamp>.
    - _new_build_id sanitizes unsafe chars: dots → underscores
      (operators commonly name profiles myarch.v2).
    - _new_build_id preserves safe chars: hyphens + underscores kept.
    - test_new_build_id_re_imported_at_module_level: explicit
      assertion that `re` is in the builder module's globals. If
      anyone ever removes the `import re` line in a future refactor,
      this test will catch it — the v0.0.31-v0.0.49 bug can't recur.
    - build() end-to-end with mocked subprocess.run: verifies the
      build state file + log file are written under BUILDER_STATE_DIR
      / BUILDER_LOGS_DIR, the response shape is correct (build_id /
      state / rc / success / duration_s / artifacts / log_path), and
      subprocess.run was actually called with the right argv.
    - build() with unknown profile returns a clear "not found" error.
    - build() with no args returns a usage error (not a crash).
    - build() with backend-not-installed returns a clear error with
      an install hint.
    - build() with non-zero subprocess returncode records state
      "failed" (not "succeeded") and rc != 0.
    All tests mock subprocess.run and the module-level
    BUILDER_STATE_DIR / BUILDER_LOGS_DIR / BUILDER_ARTIFACTS_DIR so
    they run hermetically — no real /var/lib/ writes, no real backend
    invocation.

  * AUDIT: ran an AST-based audit of bridge/builder.py to find any
    other names used at module level but not imported. The audit
    walks every function body, collects Name loads, and checks each
    against (module-level names + function locals + builtins). No
    real undefined names found — every flagged item was a
    comprehension local (b, v, s, p), tuple-unpacking target (cid,
    chint, k, v, backend_id, binary, vargs, kind), except-clause
    target (exc), or __file__ (provided by Python in every module).
    The build path is now fully exercisable by tests.

  * VERSION SYNC: bumped 0.0.49 → 0.0.50 across all 9 release
    surfaces (Makefile VERSION + header comment, bridge/__init__.py
    __version__, packaging/setup.py VERSION, PKGBUILD pkgver, RPM
    spec Version + %changelog entry, debian/changelog entry,
    compat/compat-manifest.json version + _comment, packaging/
    sysdeck.metainfo.xml <release>, README.md Version line).

  * GUARDS: all build-time guards pass — manifest consistency (26
    manifests), metainfo consistency, Makefile recipe indentation,
    no broken imports, bridge.js subcommand cross-check (102 calls
    verified — unchanged from v0.0.49 since this is a Python-only
    fix with no JS changes), version sync, all unit tests pass (228
    total: 9 new TestBuilderBuildPath + 28 TestBuilderPackagesField
    + 15 TestBuilderProfileCopy + existing TestFirewallV047* /
    TestMetainfoV047* / TestServicesPluginV047* / etc.).

 -- Jeremy Anderson <info@dcos.net>  Tue, 19 Aug 2026 18:00:00 +0000

sysdeck (0.0.49-1) unstable; urgency=medium

  * v0.0.49 BUILDER INLINE PACKAGE LIST. Per user directive: "we should
    allow adding a pacman -Sy applist.txt with a literal list of
    baseline apps for the profile being generated." Both the Create
    Profile and Copy shipped profile forms now include a Baseline
    packages textarea, a file upload input (applist.txt), and a merge-
    mode toggle (append | replace). The package list is written to the
    backend-specific package file in the same operation as the
    scaffold/copy — closing the loop on the profile-creation flow
    (previously the operator had to drop to a shell to edit the package
    list after creating/copying a profile).

  * BACKEND COVERAGE: all 4 backends supported. Each writes to its
    native package-list location:
    - mkosi      → [Packages] section of <name>.conf (INI continuation)
    - vmdb2      → bootstrap.include list in <name>.yaml (YAML list)
    - archiso    → packages.x86_64 in the profile dir (one per line)
    - live-build → config/package-lists/sysdeck.list (one per line)

  * INPUT: textarea for inline paste (one package per line, # comments
    allowed) AND file upload (applist.txt / .list / .conf accepted).
    File upload populates the textarea via the browser's FileReader API
    so the operator can review/edit the uploaded content before
    submitting — the textarea is always the source of truth. 1 MB cap
    on uploaded files (anything larger is probably not a package list).

  * MERGE MODE: operator chooses per-operation via a dropdown toggle:
    - append (default for Copy): preserves the baseline's existing
      packages (e.g. 'linux'/'base' for archiso, 'linux-image-amd64'
      for vmdb2), adds the operator's packages, deduplicates while
      preserving first-occurrence order.
    - replace (default for Create): overwrites the baseline's package
      file with the operator's list. The operator must include
      'linux'/'base' themselves if they want them.

  * NEW BRIDGE HELPERS in bridge/builder.py:
    - _extract_opts(args): splits argv into (positional, opts) so
      profile-create/profile-copy can accept --packages=<json> and
      --mode=append|replace without breaking their existing positional
      <name> <backend> [base] / <src> <new> [backend] signatures.
    - _parse_packages_text(text): parses multiline text into a deduped
      list of package names. Strips full-line comments (# at start),
      inline comments (# after package name), blank lines, and
      surrounding whitespace. Preserves first-occurrence order.
    - _write_packages_mkosi(conf_path, packages, mode): reads the
      existing mkosi.conf, parses the [Packages] section, dedups on
      append, rebuilds the section with the merged/replaced list.
      Other sections ([Distribution], [Output], etc.) are preserved.
    - _write_packages_vmdb2(yaml_path, packages, mode): regex-based
      surgery on the bootstrap.include list in the YAML. pyyaml is
      NOT a hard dependency (vmdb2 isn't typically installed on Arch,
      and we shouldn't pull in a YAML parser just to update a list).
      Other YAML sections (partitions, commands) are preserved.
    - _write_packages_archiso(profile_dir, packages, mode): reads
      packages.x86_64, preserves the comment header on append, dedups,
      rewrites. In replace mode, writes a fresh file with a header
      comment + the operator's packages.
    - _write_packages_live_build(profile_dir, packages, mode): writes
      config/package-lists/sysdeck.list. live-build merges all .list
      files at build time, so each list file is an independent package
      set. In replace mode, removes old sysdeck*.list files (does NOT
      touch baseline .list files like baseline.list). In append mode,
      just writes/overwrites sysdeck.list (the file is the unit).
    - _write_packages(profile_path, backend, packages_text, mode):
      dispatcher that validates mode, parses packages_text, and routes
      to the right per-backend writer.

  * EXTENDED profile_create() and profile_copy() to accept --packages=
    <json> and --mode=append|replace. Default mode for create is
    "replace" (the scaffold's minimal defaults are replaced by the
    operator's list); for copy it's "append" (the baseline's packages
    are preserved). Both return a new "packages" field in their success
    response: {count, mode, path}. If package-writing fails, the
    profile is still created/copied and a "packages_error" field is
    included (non-fatal — the operator can fix the package file by
    hand).

  * UPDATED shared/bridge.js: profileCreate(name, backend, base,
    packagesText, mode) and profileCopy(srcName, newName, backend,
    packagesText, mode). packagesText is JSON-encoded via
    JSON.stringify() so newlines, quotes, and unicode survive the argv
    boundary cleanly. When packagesText is omitted/null, the bridge
    writes no package file (back-compat with v0.0.48 callers).

  * UPDATED plugins/sysdeck-builder/builder.js:
    - New renderPackagesField(prefix, defaultMode) helper shared by
      both forms. Emits a <textarea>, a file <input>, and a merge-mode
      <select>. The prefix distinguishes element IDs (cp-create-* vs
      cp-copy-*) so both forms coexist on the same page.
    - File-upload handlers use FileReader to populate the textarea.
      The textarea is the source of truth — the operator can review/
      edit the uploaded content before submitting.
    - Both submit handlers (Create + Copy) read the textarea + mode
      toggle and pass them to the bridge. The success message now
      shows the package count + the path of the written package file.
    - Panel header version tag bumped v0.0.48 → v0.0.49 with a new
      v0.0.49 narrative block.

  * NEW TESTS: 28 unit tests in tests/test_bridge_parsers.py under a
    new TestBuilderPackagesField class cover:
    - _parse_packages_text (6 tests): empty, full-line comments,
      inline comments, dedup, whitespace, blank lines.
    - _extract_opts (4 tests): positional-only, key=value, boolean
      flag, mixed.
    - _write_packages_mkosi (3 tests): replace, append+dedup, append
      to empty section.
    - _write_packages_vmdb2 (2 tests): replace, append+dedup.
    - _write_packages_archiso (2 tests): replace, append+preserve
      baseline.
    - _write_packages_live_build (3 tests): basic write, replace
      clears old sysdeck*.list but preserves baseline.list, append
      overwrites sysdeck.list only.
    - _write_packages dispatcher (3 tests): invalid mode, unknown
      backend, archiso file path rejected.
    - profile_create end-to-end (2 tests): arg parsing + JSON decode,
      invalid JSON rejected.
    - profile_copy end-to-end (3 tests): archiso append, live-build
      replace, back-compat without --packages.
    All tests use tempfile.mkdtemp() and unittest.mock.patch.object();
    none touch real /etc/ or /usr/share/ paths.

  * VERSION SYNC: bumped 0.0.48 → 0.0.49 across all 9 release surfaces
    (Makefile VERSION + header comment, bridge/__init__.py __version__,
    packaging/setup.py VERSION, PKGBUILD pkgver, RPM spec Version +
    %changelog entry, debian/changelog entry, compat/compat-manifest.json
    version + _comment, packaging/sysdeck.metainfo.xml <release>,
    README.md Version line + highlights section).

  * GUARDS: all build-time guards pass — manifest consistency (26
    manifests), metainfo consistency, Makefile recipe indentation,
    no broken imports, bridge.js subcommand cross-check (102 calls
    verified — unchanged from v0.0.48 since profileCopy/profileCreate
    are existing methods, just with new optional args), version sync,
    all unit tests pass (28 new TestBuilderPackagesField + existing
    TestBuilderProfileCopy + TestFirewallV047* + TestMetainfoV047*).

 -- Jeremy Anderson <info@dcos.net>  Tue, 19 Aug 2026 12:00:00 +0000

sysdeck (0.0.48-1) unstable; urgency=medium

  * v0.0.48 BUILDER PROFILE-CREATE BUGFIX. The v0.0.31 Create Profile
    form's backend dropdown fell back to `primary.id` when no
    scaffoldable backend (mkosi/vmdb2) was installed. On an archiso-only
    or live-build-only host, this funneled the operator straight into
    the "profile-create supports ('mkosi', 'vmdb2')" error — archiso
    and live-build use shipped directory-based profile trees, not
    single-file specs, so they cannot be scaffolded from scratch.

  * FIX 1 (panel): renderCreateProfile in
    plugins/sysdeck-builder/builder.js no longer falls back to
    primary.id. When no mkosi/vmdb2 backend is installed, the form
    renders an inline install hint (with the exact pacman/apt command)
    instead. The dropdown only offers actually-scaffoldable backends.

  * FIX 2 (panel): new renderCopyProfile form lists every shipped
    archiso and live-build profile discovered via profiles() and offers
    a one-click copy into /etc/ via the new bridge.builder.profileCopy()
    method. This is the supported way to create profiles for the
    directory-based backends. Source profiles are grouped by backend
    in an <optgroup>; the new-name input is a free-text field.

  * NEW BRIDGE COMMAND: bridge/builder.py profile_copy() — copies a
    shipped archiso/live-build profile tree from /usr/share/ into
    /etc/archiso/configs/<name>/ or /etc/live-build/<name>/. Validates
    new-name (no slashes, no "."/".." to prevent path traversal via
    crafted names like "../../etc"), resolves source via profiles(),
    refuses non-directory-based backends (mkosi/vmdb2) with a clear
    "use profile-create" hint, refuses if destination already exists,
    returns structured {copied, backend, source, source_path, name,
    path} on success. Uses the same polkit action as profile-create
    (org.sysdeck.builder.modify) — no new polkit file needed.

  * NEW BRIDGE.JS METHOD: bridge.builder.profileCopy(srcName, newName,
    backend) runs with { superuser: 'try' }, same as profileCreate /
    profileDelete. The shared/bridge.js comment block now lists the
    new subcommand alongside profile-create / profile-delete.

  * DESTINATION ROOTS REFACTOR: ARCHISO_COPY_DEST and
    LIVE_BUILD_COPY_DEST are now module-level constants in
    bridge/builder.py (was: hardcoded Path("/etc/...") literals inside
    profile_copy). This mirrors the existing ARCHISO_DIRS /
    LIVE_BUILD_DIRS pattern and lets unit tests patch them with
    tempdirs instead of touching real /etc/ paths.

  * NEW TESTS: 15 unit tests in tests/test_bridge_parsers.py under a
    new TestBuilderProfileCopy class cover arg validation (no args,
    one arg, slash in name, "."/".." name), source resolution
    (not-found, wrong-backend hint filter, mkosi/vmdb2 rejection with
    "use profile-create" hint), success paths (archiso copy, live-build
    copy, backend-hint-inferred-when-omitted), and failure modes
    (dest-already-exists with "use profile-delete" hint, source-path-
    not-a-directory, permission-error returns polkit hint). All tests
    use tempfile.mkdtemp() and unittest.mock.patch.object(); none
    touch real /etc/ or /usr/share/ paths.

  * VERSION SYNC: bumped 0.0.47 → 0.0.48 across all 9 release surfaces
    (Makefile VERSION + header comment, bridge/__init__.py __version__,
    packaging/setup.py VERSION, PKGBUILD pkgver, RPM spec Version +
    %changelog entry, debian/changelog entry, compat-manifest.json
    version + _comment, metainfo.xml <release>, README.md Version
    line). All 9 surfaces now report v0.0.48.

  * GUARDS: all build-time guards pass — manifest consistency (26
    manifests = 25 plugins + 1 shared), metainfo consistency, Makefile
    recipe indentation (tabs not spaces), no broken import-cockpit
    pattern, no broken python3 -m sysdeck.bridge pattern, bridge.js
    subcommand cross-check (102 calls verified — was 101 in v0.0.47,
    +1 for the new profileCopy), version sync, all unit tests pass
    (15 new TestBuilderProfileCopy + the existing TestFirewallV047*
    and TestMetainfoV047* classes).

 -- Jeremy Anderson <info@dcos.net>  Tue, 19 Aug 2026 00:00:00 +0000

sysdeck (0.0.47-1) unstable; urgency=medium

  * v0.0.47 LOGIC-FLAW FIXES. Per user directive: "we need to fix a
    few logic flaws i do things a certain way on my servers so ill
    correct the ports on a firewall script or two. the web server
    template, and vps template i setup the webserver on 8080 and
    varnish on 80 for an automatic cache environment. we should move
    the service/ports editor to its own module entry for ease of
    access. the glances we should default to enabling the built in
    webui and embedding that into our module instead it visually
    looks stunning in comparison to ours."

  * FIREWALL: public-webserver.sh PORT-TOPOLOGY FIX. The v0.0.44
    template had the topology backwards — it exposed Caddy on :80 and
    Varnish on :8080. v0.0.47 flips it: Varnish is the public cache
    front on :80, Caddy HTTP backend lives on :8080 (loopback only),
    Caddy HTTPS lives on :443 (public, terminates TLS). The
    VARNISH_PUBLIC toggle is removed — :8080 is now ALWAYS loopback-
    only (the previous default exposed the cache-miss path to the
    internet, bypassing Varnish entirely). Defense-in-depth drops
    added for :8080 alongside the existing MariaDB + Caddy admin
    drops. The detect output now reflects the corrected cache-front-
    of-origin topology.

  * FIREWALL: vps-webserver.sh DEFAULT TOPOLOGY. When Varnish is
    detected at all, the operator's documented setup is now the
    explicit default — Varnish on :80, Caddy HTTP backend on :8080
    (loopback only), Caddy HTTPS on :443. Previously this only
    happened if Varnish was already listening on :80 at runtime; now
    detecting Varnish is enough to flip Caddy HTTP to :8080 loopback.
    If Varnish is detected but not yet on :80 (e.g. installed but
    stopped, or still on the upstream default :6081), the template
    forces VARNISH_PORT=80 with a log message explaining the override.

  * NEW PLUGIN: sysdeck-services — first-class sidebar entry at
    order 45. The Service/Port Editor card that lived at the bottom
    of the Firewall panel since v0.0.44 has been lifted out into its
    own module for ease of access. The new panel adds a filter box
    (search by name/id/port/process), a show-only-editable toggle,
    and a Refresh button. The bridge surface
    (bridge.firewall.services / service-info / set-service-port /
    restart-service) is unchanged; a new bridge.services proxy was
    added to bridge.js so the new panel has a clean API surface.

  * FIREWALL PANEL: removed the renderServicePortEditor() card from
    plugins/sysdeck-firewall/firewall.js + the .btn-svc-save /
    .btn-svc-restart wireEvents handlers + the services() Promise
    from the parallel load. Added a renderServicesLinkCard()
    signpost pointing operators to the new sidebar entry. Removed
    service/port/editor keywords from the firewall manifest — they
    belong to the new services plugin now.

  * GLANCES: DEFAULT-ON EMBEDDED WEBUI. The panel now auto-starts
    the Glances built-in webserver (glances -w --bind 127.0.0.1
    --port 61208) on mount — no click required. The iframe is now
    the primary view, sized to fill the viewport (min-height:
    calc(100vh - 200px)). The legacy SysDeck snapshot cards
    (CPU/Memory/Swap/Network/Disk/Processes) are moved into a
    collapsed <details> at the bottom of the page so they don't
    push the iframe below the fold. The Stop button is retained for
    explicit shutdown; we don't stop on unmount because keeping the
    webserver running speeds re-entry.

  * GLANCES CSP: plugins/sysdeck-glances/manifest.json CSP updated
    to allow frame-src http://127.0.0.1:61208 + http://localhost:
    61208 so the embedded Glances web UI loads without a CSP
    violation. Added webui / embed / iframe / real-time keywords.

  * BRIDGE.JS: added bridge.services surface (4 methods: list /
    info / setPort / restart) that proxies to bridgeCmd("firewall",
    [...]) — no new bridge helper file needed. The SERVICES_REGISTRY
    + atomic-write + systemctl restart logic remains in
    bridge/firewall.py as the single source of truth. The
    bridge.firewall.services / serviceInfo / setServicePort /
    restartService methods are kept for back-compat with any
    operator-side scripts that may have called them directly.

  * VERSION SYNC: bumped 0.0.46 → 0.0.47 across all 9 release
    surfaces (Makefile VERSION + comment, bridge/__init__.py
    __version__, packaging/setup.py VERSION, packaging/PKGBUILD
    pkgver, packaging/sysdeck.spec Version + prepended v0.0.47
    %changelog entry, packaging/debian/changelog prepended v0.0.47
    entry, compat/compat-manifest.json version + comment, README.md
    Version + new v0.0.47 highlights block, BLOG.md prepended
    v0.0.47 section). Also caught up bridge/__init__.py +
    packaging/setup.py from 0.0.45 (the v0.0.46 release bumped
    PKGBUILD/spec/debian but missed these two files).

  * MAKEFILE: bumped VERSION 0.0.46 → 0.0.47, plugin count 25 → 26
    (sysdeck-services added). The install target's for-loop already
    picks up the new plugin directory automatically — no Makefile
    recipe changes needed.

 -- Jeremy Anderson <info@dcos.net>  Mon, 18 Aug 2026 14:00:00 +0000

sysdeck (0.0.46-1) unstable; urgency=medium

  * NEW PLUGIN: sysdeck-modules — in-suite 3rd-party Cockpit module
    installer. Replaces the side-channel cockpit-module-pull.sh shell
    script with a first-class sidebar entry at order 44.

  * DESIGN (per user directive): "i wanted the in ui module to handle
    showing license, developer, 3rd party model name and ability to
    visit homepage and install the plugin 1 click with license
    agreement inline." Each catalog row renders the module name,
    license badge, developer/author, source URL, and a clickable
    homepage link INLINE — right next to a 1-click Install button.
    No modal, no separate confirmation step. Clicking Install is the
    operator's acceptance of the inline-displayed license.

  * BRIDGE: bridge/modules3p.py — 10-entry catalog covering
    cockpit-machines, cockpit-podman, cockpit-storaged,
    cockpit-identities, cockpit-navigator, cockpit-file-sharing,
    cockpit-zfs-manager, cockpit-pacman, cockpit-sensors,
    cockpit-benchmark. Four install kinds: pacman / git / deb-tar /
    tarball. The bridge refuses silent installs (no
    --accept-license=1 ⇒ license-not-accepted) as a guard against
    malicious callers; the JS always passes that flag because the
    license is shown inline next to the Install button.

  * AUDIT LOG: every install / uninstall appends a JSON record to
    /etc/cockpit/MODULE_LICENSES.log (shared with the legacy
    cockpit-module-pull.sh). Legacy plain-text lines preserved as
    {raw: ...} records.

  * POLKIT: new action org.sysdeck.modules3p.modify authorizes
    /usr/bin/python3 /usr/lib/sysdeck/bridge/modules3p.py install|
    uninstall <id>. auth_admin_keep for active sessions.

  * SHARED: bridge.js gains a bridge.modules3p surface with
    catalog / status / preflight / install / uninstall / audit
    methods. install + uninstall use { superuser: 'try' } so the
    cockpit bridge prompts via polkit.

  * THIRD_PARTY.md: appended v0.0.46 section documenting the in-suite
    installer + the per-entry catalog table. Updated the two existing
    "see cockpit-module-pull.sh" notes (cockpit-sensors,
    cockpit-identities) to point at the new panel.

  * MAKEFILE: bumped VERSION to 0.0.46, plugin count 24 → 25, added
    polkit install + uninstall stanzas for the new policy file.

 -- Jeremy Anderson <info@dcos.net>  Mon, 18 Aug 2026 10:30:00 +0000

sysdeck (0.0.45-1) unstable; urgency=medium

  * TRADEMARK SCRUB. Per user directive: "you cannot say smoothwall
    and ipfire where merged into our fw script either. you can say
    logic derived from or influenced by these projects. its really
    hard holding your hand on legal issues." The v0.0.36 and v0.0.37
    release notes, changelogs, code comments, and worklog entries
    previously claimed we shipped templates called smoothwall.sh and
    ipfire.sh and "merged" them into sysdeck-fw. That language
    implied we incorporated code from those trademarked projects.
    v0.0.45 rewords every such claim to the legally-safe phrasing:
    the sysdeck-fw backend's logic is DERIVED FROM / TAKES INFLUENCE
    FROM Smoothwall Express and IPFire under our own identifier. We
    never shipped templates called "smoothwall" or "ipfire".

  * FILES SCRUBBED (10 files):
    - bridge/firewall.py — EXCLUDED_BACKENDS reasons for smoothwall
      + ipfire reworded from "merged into sysdeck-fw" to "other
      projects' trademarks — we took influence from them for
      sysdeck-fw instead of shipping templates by those names."
      Header docstring reworded. sysdeck-fw backend description
      reworded from "Merges the Smoothwall-style..." to "Takes
      influence from Smoothwall Express... under our own identifier."
    - firewall/templates/sysdeck-fw.sh — header comment reworded.
      "v0.0.37 MERGE" → "v0.0.37 UNIFIED ZONE FIREWALL". "What this
      template inherits from each predecessor" → "What this template
      takes influence from". Each "FROM THE X" section annotated
      with "(takes influence from <project>)".
    - firewall/templates/cilium.sh — stale comment referencing
      'custom'/'smoothwall'/'ipfire' backend updated to
      'custom'/'sysdeck-fw' backend.
    - tests/test_bridge_parsers.py — TestFirewallV037BackendMerge
      class renamed to TestFirewallV037UnifiedBackend. Test
      docstring reworded. test_smoothwall_and_ipfire_templates_removed
      renamed to test_smoothwall_and_ipfire_templates_not_present
      with reworded assertion messages. Comments throughout updated
      to use "takes influence from" / "trademark" phrasings.
    - plugins/sysdeck-firewall/firewall.js — header comment bumped
      to v0.0.45 with trademark-scrub block. Excluded-backends
      description reworded.
    - plugins/sysdeck-firewall/manifest.json — keywords list
      updated: removed "smoothwall" + "ipfire", added "sysdeck-fw"
      + the v0.0.44 service/port editor keywords (service, port,
      editor, remote-admin, public-webserver, ai-llm, ollama,
      openwebui, hermes, odysseus, caddy, varnish, mariadb).
    - README.md — v0.0.36 + v0.0.37 highlights blocks reworded.
      "Four backends ship" → "Three backends ship". "Three new
      firewall templates ship" → "Two new firewall templates ship".
      Backend descriptors for smoothwall + ipfire replaced with
      sysdeck-fw descriptor.
    - packaging/debian/changelog (this entry) — v0.0.36 + v0.0.37
      entries reworded. "Four backends ship" → "Three backends
      ship". smoothwall.sh + ipfire.sh template descriptors
      replaced with sysdeck-fw.sh descriptor. EXCLUDED_BACKENDS
      reasons reworded. "merged into sysdeck-fw" → "took influence
      from for sysdeck-fw".
    - packaging/sysdeck.spec — v0.0.36 + v0.0.37 %changelog entries
      reworded same as debian/changelog.
    - worklog.md — Task 36 + Task 37 entries reworded. "merge the
      v0.0.36 smoothwall + ipfire templates" → "ship a unified
      SysDeck FW backend whose logic is derived from". "Merges both
      predecessor templates" → "Takes influence from both
      predecessors". "with the merge reason" → "with the trademark
      reason".

  * LEGALLY-SAFE PHRASINGS USED. Every reference to Smoothwall
    Express or IPFire now uses one of:
      - "takes influence from"
      - "logic derived from"
      - "influenced by these projects"
    The EXCLUDED_BACKENDS reasons for smoothwall and ipfire now
    read: "other projects' trademarks — we took influence from them
    for sysdeck-fw instead of shipping templates by those names."

  * DIRECT-QUOTE PRESERVATION. User-directive quotes that mention
    "smoothwall" or "ipfire" (e.g. the v0.0.36 directive: "or they
    can select celium, or smoothwall or ipfire or other firewall
    scripts") are preserved verbatim as the user's own words. Our
    commentary around them uses the legally-safe phrasings. The
    v0.0.37 directive quote was lightly paraphrased from "lets
    merge them into" to "lets unify them into" — same meaning,
    legally safer verb.

  * NO FUNCTIONAL CHANGES. This is a wording-only release. No code
    paths changed, no templates changed, no bridge subcommands
    changed. The sysdeck-fw backend, the 7 firewall templates
    (vps-webserver.sh, no-services.sh, cilium.sh, sysdeck-fw.sh,
    remote-admin.sh, public-webserver.sh, ai-llm.sh), and the
    v0.0.44 service/port editor are unchanged. All 141 unit tests
    still pass. All 7 build-time guards still pass.

  * REGRESSION TESTS. No new tests — the existing 32 v0.0.44 tests
    + 10 v0.0.37 unified-backend tests + 99 prior tests all pass
    unchanged. Total: 141 tests.

 -- Jeremy Anderson <info@dcos.net>  Mon, 18 Aug 2026 13:00:00 -0400

sysdeck (0.0.44-1) unstable; urgency=medium

  * PUBLIC-SERVER FIREWALL VARIANTS. Per user directive: "another
    thing the firewall module needs is a few public server variants.
    like: remote admin enabled ssh and cockpit, server enabled like
    caddy and varnish 80 and 8080 w mariadb, an ai llm variant for
    ollama, hermes, openwebui and oddyseus." Three new firewall
    templates ship in this release:
    - remote-admin.sh: SSH (22) + Cockpit (9090). Aggressive rate
      limiting with auto-ban on the ssh_abuse + cockpit_abuse sets
      (1h + 10m timeouts respectively). For VPS / cloud hosts where
      the operator needs remote shell + web admin from anywhere.
      Drops all non-essential inbound; forward chain drops + logs.
    - public-webserver.sh: Caddy (80/443) + Varnish (8080, public
      by default per the "80 and 8080" directive) + SSH (22). MariaDB
      (3306) and the Caddy admin API (2019) are bound loopback-only
      with DEFENSE-IN-DEPTH DROP rules — even if the daemon is
      misconfigured to bind 0.0.0.0, the firewall drops the packet
      before it reaches the daemon. This is the layered-defense
      pattern: the daemon's bind is the primary control, the firewall
      is the secondary control. The operator can flip VARNISH_PUBLIC
      to false to make :8080 loopback-only as well.
    - ai-llm.sh: Ollama (11434) + OpenWebUI (3000) + Hermes (8000) +
      Odysseus (8001) + SSH (22). All four AI service ports are
      public per the user directive. The template's detect output
      documents the v0.0.43 "never 0.0.0.0" directive and explains
      why Ollama's default 0.0.0.0 bind is acceptable here (the
      firewall gates access, not the bind address). If the operator
      wants Ollama loopback-only, set OLLAMA_HOST=127.0.0.1:11434
      in the systemd override and the firewall will NAT-forward.
    All three templates implement the standard start/stop/restart/
    detect/status/check interface. All use modern nftables inet
    family with named sets, rate limiting with dynamic auto-ban,
    bogon filtering (martian + RFC 1918 + IPv6 ULA + link-local),
    invalid TCP flag drops (NULL / XMAS / SYN+FIN / SYN+RST), and
    per-port log prefixes. Auto-detect ports from each service's
    config file (/etc/ssh/sshd_config, /etc/cockpit/cockpit.conf,
    /etc/caddy/Caddyfile, /etc/systemd/system/varnish.service.d/,
    /etc/mysql/mariadb.conf.d/, /etc/systemd/system/ollama.service.d/,
    /etc/open-webui/config, /etc/hermes/config.yaml,
    /etc/odysseus/config.toml).

  * SERVICE/PORT EDITOR. Per user directive: "and lastly a full
    service/port editor that detects based on running ports and
    services detected on them. make it as simple as editing the
    port to change it in a config on the system. auto restart the
    associated service if it is changed." Four new bridge/firewall.py
    subcommands:
    - services: runs `ss -tlnp` (or /proc/net/tcp + /proc/net/tcp6
      fallback when ss is unavailable) to enumerate ALL listening
      TCP ports on the host. Cross-references against SERVICES_REGISTRY
      — a static allowlist of 9 services (ssh, cockpit, caddy,
      varnish, mariadb, ollama, openwebui, hermes, odysseus). For
      each registered service, returns: id, name, default_port,
      current_port_in_config (extracted via per-service regex from
      the actual config file), listening_ports (from ss), processes,
      pids, config_file path, config_file_exists, systemd_unit,
      alt_units, restart_supported, editable, description. Also
      returns unmapped_listeners — every listening socket that did
      NOT match a registered service — so the operator can spot
      services the editor doesn't yet know about.
    - service-info <id>: returns one service's full registry entry
      + detected state (current_port_in_config, config_file path).
    - set-service-port <id> <new_port>: the workflow is
      (1) validate service_id against SERVICES_REGISTRY,
      (2) validate new_port (1..65535, strict integer regex,
          fullmatch to reject trailing newlines),
      (3) resolve the config file (first existing candidate,
          realpath under /etc/ or /usr/share/sysdeck/),
      (4) read the file, find the port assignment line via the
          per-service regex,
      (5) substitute ONLY the port digits (the regex's prefix group
          is preserved verbatim — comments and other content on the
          line are untouched),
      (6) write the new content to a sibling .tmp file in the same
          directory (mode preserved from the original), fsync, then
          atomically rename over the original (defeats partial-write
          corruption if the bridge crashes mid-write),
      (7) systemctl restart the service's systemd_unit (or try
          alt_units if the primary fails).
      Returns {service, name, config_file, old_port, new_port,
      restarted, restart_method, restart_rc, restart_stdout,
      restart_stderr}.
    - restart-service <id>: just runs systemctl restart on the
      service. Useful for "I edited the config by hand" workflows.

  * HARDENING (per CVE-derived lessons already applied):
    - service_id validated against SERVICES_REGISTRY — an attacker
      CANNOT inject an arbitrary service name to trick the bridge
      into editing /etc/shadow. Only services in the registry are
      accepted. CVE-2024-2947 lesson.
    - Port number validated with strict integer regex 1..65535,
      re.fullmatch (NOT re.match) so trailing newlines don't slip
      past the $ anchor. CVE-2019-15107 lesson. The v0.0.43
      validators used re.match — v0.0.44 fixes this to fullmatch.
    - Config file path resolved with os.path.realpath and verified
      to live under an allowlist base dir (/etc/ or
      /usr/share/sysdeck/). Symlink-escape attacks rejected.
      CVE-2022-30708 lesson.
    - The port substitution is a strict per-service regex (NOT
      freeform sed s/.../.../). The regex only matches the port
      assignment line and only replaces the port digits — comments
      and other content on the line are preserved.
    - Atomic write via tmpfile + fsync + rename. Never in-place
      overwrite. Defeats partial-write corruption.
    - systemctl invoked with shell=False, list argv, env scrubbed
      (SCRUBBED_ENV drops LD_PRELOAD, LD_LIBRARY_PATH, PYTHONPATH,
      BASH_ENV, ENV, PERL5OPT). CVE-2024-6126 lesson.
    - systemctl binary validated against an allowlist
      (/usr/bin/systemctl, /bin/systemctl, /usr/sbin/systemctl).
      Defense in depth — shutil.which("systemctl") is only
      accepted if it resolves to one of these paths.
    - systemctl unit name validated against a strict regex
      (^[A-Za-z0-9_@.\-]+$) — no shell metacharacters can pass.
    - CRLF/NUL stripped from any value written to a line-oriented
      config file via _sanitize_for_file(). CVE-2026-41940 lesson.
      (Not strictly needed here — the regex only matches digits —
      but defense in depth.)

  * POLKIT. The org.sysdeck.firewall.modify action (shipped since
    v0.0.17, extended in v0.0.36 to authorize cilium + cilium-agent
    + helm) already authorizes /usr/bin/systemctl — no polkit
    changes required for the new subcommands. The mutating ops
    (setServicePort, restartService) pass { superuser: 'try' } to
    cockpit.spawn — the cockpit bridge prompts the operator via
    polkit. Read-only ops (services, serviceInfo) do NOT pass
    superuser — no auth needed.

  * PANEL. New "Service / Port Editor" card in the firewall panel
    (plugins/sysdeck-firewall/firewall.js). Renders one row per
    registered service with: service name + id + editable/restartable
    badges, editable port input (type=number, min=1, max=65535),
    Save & Restart button (calls bridge.firewall.setServicePort),
    Restart-only button (calls bridge.firewall.restartService),
    current port from config, default port, listening ports (comma-
    separated), processes, PIDs, config file path. Unmapped listeners
    shown in an expandable <details> block. Help text documents the
    atomic-write mechanism, the config-base-dir allowlist, and the
    polkit auth flow.

  * BRIDGE.JS SURFACE. 4 new firewall methods: services, serviceInfo,
    setServicePort, restartService. Read-only queries (services,
    serviceInfo) do NOT pass { superuser: 'try' }. Mutating queries
    (setServicePort, restartService) DO. Total firewall bridge.js
    surface now 29 methods (was 25 in v0.0.43).

  * REGRESSION TESTS EXPANDED. 32 new tests in two new test classes
    (TestFirewallV044ServicesEditor + TestFirewallV044PublicServerTemplates).
    Tests cover:
    - SERVICES_REGISTRY structure (9 entries, all valid ids, all
      have required fields, all port_regex are compiled patterns,
      all default_port in 1..65535).
    - _validate_service_id accept/reject (accepts: ssh, cockpit,
      openwebui, ai-llm, a, abc-def-ghi. Rejects: empty, path
      traversal, shell metachars, uppercase, trailing newline,
      too long, leading digit, dot).
    - _validate_port accept/reject (accepts: 1, 22, 80, 443, 8080,
      65535. Rejects: empty, 0, 65536, 99999, abc, shell metachars,
      trailing newline, leading/trailing space, -1, decimal, hex,
      comma-list).
    - cmd_services returns expected JSON shape (services list with
      9 entries, unmapped_listeners list, listener_count int).
    - cmd_service_info accepts all 9 valid ids, rejects unknown +
      invalid (path traversal, shell metachars, empty).
    - cmd_set_service_port rejects invalid service_id (CVE-2024-2947
      + CVE-2022-30708 lessons — "../../etc/passwd", "ssh; rm -rf /",
      "SSH", empty, too-long). Rejects shell-metachar ports
      (CVE-2019-15107 lesson — "80; rm -rf /", "80$(whoami)", "80|cat",
      abc, 99999, 0). Rejects unknown service. Rejects missing args.
    - cmd_restart_service rejects invalid id + unknown service.
    - _run_ss_listening returns a list (never raises, even if ss
      is missing).
    - _parse_proc_net_tcp returns a list (always present on Linux).
    - _extract_port_from_config: for each of the 9 services, the
      per-service regex extracts the correct port from a
      representative config snippet (sshd_config Port=2222, cockpit
      ListenStream=9090, Caddyfile :8080, varnish.params
      VARNISH_LISTEN_PORT=6081, mariadb .cnf port=3306, ollama
      OLLAMA_HOST=127.0.0.1:11434, openwebui PORT=3000, hermes
      config.yaml port:8000, odysseus config.toml port=8001).
    - End-to-end atomic-write test on a temp config file (creates
      /tmp/sysdeck-test-XXXX/hermes/config.yaml with port: 8000,
      calls cmd_set_service_port(["hermes", "9999"]), verifies the
      returned JSON has old_port=8000 + new_port=9999 + restarted=False
      (no systemd in test env), verifies the file was modified
      (port: 9999 present, port: 8000 absent), verifies non-target
      lines preserved (host: 0.0.0.0, workers: 4)).
    - No-config-file error path (returns error, doesn't crash).
    - Regex-no-match error path (config file exists but has no port
      line — bridge REFUSES to write, doesn't guess where the port
      line is. Verifies the file was NOT modified).
    - no-sudo-in-v044-subcommands (CVE-2022-0824 lesson — greps
      cmd_services, cmd_service_info, cmd_set_service_port,
      cmd_restart_service, _systemctl_restart source for "/usr/bin/sudo"
      and "sudo" — must not appear).
    - Three new template files exist + executable bit set.
    - Each template's metadata header parses correctly (Name,
      Description, Distro, Services fields).
    - remote-admin metadata: services include ssh + cockpit; distros
      include arch + debian.
    - public-webserver metadata: services include ssh + caddy +
      varnish + mariadb.
    - ai-llm metadata: services include ssh + ollama + openwebui +
      hermes + odysseus.
    - Each template implements the standard start/stop/restart/detect/
      status/check dispatch interface (regex accepts both `action)`
      and `action|alt)` forms — the templates use `restart|reload`
      and `check|validate` alternatives).
    - Each template has no `sudo` in non-comment, non-string lines.
    Total tests: 109 (v0.0.43) → 141 (v0.0.44). 32 new tests.

 -- Jeremy Anderson <info@dcos.net>  Mon, 18 Aug 2026 12:00:00 -0400

sysdeck (0.0.43-1) unstable; urgency=medium

  * FIREWALL BACKEND/TEMPLATE COORDINATION FIX. User directive: "i
    found some logic flaws in the firewall module, we seem to have 2
    seperate template lists, 1 apply button. makes template selection
    akward." The v0.0.36 panel had two independent lists (backend
    selector + template selector) that didn't coordinate. When you
    picked the sysdeck-fw backend, the template list still showed ALL
    5 templates (including vps-webserver + cilium which belong to
    other backends). The Apply button used getSelectedTemplate() and
    ignored the backend — you could apply vps-webserver.sh while the
    cilium backend was active. v0.0.43 fixes this:
    - When a backend has its own template field (cilium -> cilium,
      sysdeck-fw -> sysdeck-fw), the template selector is HIDDEN
      entirely — the backend IS the template.
    - When 'custom' is active, the template selector shows ONLY the
      custom-compatible templates (vps-webserver, no-services). The
      cilium + sysdeck-fw templates are filtered out.
    - The Apply button is now backend-aware: getSelectedTemplate()
      checks the active backend first, uses its template if it has
      one, falls back to the radio selection only for 'custom'.
    - The Apply button label is now backend-aware: "Apply sysdeck-fw"
      when sysdeck-fw is active, "Apply Cilium Policy" for cilium,
      "Apply Template" for custom.
    - The backend selector help text now explains whether the current
      backend uses its own template or lets you pick.

  * NETWORK MONITOR REWRITE (IPTRAF-NG STYLE). User directive: "the
    network monitor module should feed from iptraf-ng recreate the
    ui." The v0.0.10-v0.0.42 panel used `ss -tulpn` for a static
    socket list. v0.0.43 recreates the iptraf-ng UI by reading the
    same kernel sources iptraf-ng reads from directly — no iptraf-ng
    binary dependency, no ncurses parsing.
    - bridge/netsec.py rewritten with 7 subcommands: summary,
      traffic, connections, interfaces, protocols, sockets, established.
    - traffic: samples /proc/net/dev twice (1s apart), computes
      per-interface live RX/TX rates (bytes/s, packets/s). This is
      exactly how iptraf-ng computes its live traffic rates.
    - connections: reads /proc/net/tcp + /proc/net/udp directly
      (no ss dependency). Decodes little-endian hex addresses, maps
      TCP state codes to names.
    - interfaces: per-interface cumulative counters (rx/tx bytes,
      packets, errors, drops).
    - protocols: parses /proc/net/snmp for IP/TCP/UDP/ICMP counters.
    - netsec.js panel rewritten with 3 sections matching iptraf-ng:
      (1) Interface Overview — live traffic cards with RX/TX rate
      bars, auto-refresh every 5s.
      (2) IP Traffic Monitor — active connections table (proto,
      state, local:port, remote:port, TxQ/RxQ).
      (3) Protocol Statistics — IP/TCP/UDP/ICMP counter tables.
    - bridge.js netsec surface expanded: summary, traffic,
      connections, interfaces, protocols (new) + listeningPorts
      (legacy alias for sockets).
    - Panel renamed from "Network SOC" to "Network Monitor".

  * REGRESSION TESTS. 9 new tests in TestNetsecV043Rewrite class:
    dispatch table, interfaces, protocols, connections, summary,
    _parse_proc_net_dev parser, _decode_addr hex parser, no-sudo
    source scan. Total: 100 -> 109 tests.

 -- Jeremy Anderson <info@dcos.net>  Mon, 18 Aug 2026 07:00:00 -0500

sysdeck (0.0.42-1) unstable; urgency=medium

  * SIDEBAR LABEL CLEANUP. User directive: "lastly instead of looking
    kinda ecentric with putting SysDeck leading every module. seperate
    stock and addon plugins from the sysdeck suite of plugins with just
    a header entry, like cockpit up top and then the modules, then
    SysDeck and then the modules. lets organize it like that instead
    of reading SysDeck 20+ times." The v0.0.35-v0.0.41 sidebar read
    "SysDeck" 24 times (SysDeck Firewall, SysDeck Containers, SysDeck
    Kata, etc.) — noisy and eccentric. v0.0.42 drops the "SysDeck "
    prefix from all 24 plugin labels, matching the cockpit ecosystem
    convention (cockpit-machines is "Machines", cockpit-podman is
    "Podman" — not "Cockpit Machines").
  * LABEL CHANGES (24 plugins):
      SysDeck Containers & VMs → Containers & VMs
      SysDeck Firewall         → Firewall
      SysDeck Integrity        → Integrity
      SysDeck Network Security → Network Security
      SysDeck Service Mesh     → Service Mesh
      SysDeck Vault            → Vault
      SysDeck Fleet            → Fleet
      SysDeck Kata             → Kata
      SysDeck Fester           → Fester
      SysDeck Firmware         → Firmware
      SysDeck Image Builder    → Image Builder
      SysDeck Mining           → Mining
      SysDeck Themes           → Themes
      SysDeck Hardware Auth    → Hardware Auth
      SysDeck Glances          → Glances
      SysDeck Sensors          → Sensors
      SysDeck Benchmark        → Benchmark
      SysDeck Packages         → Packages
      SysDeck Policy           → Policy
      SysDeck Databases        → Databases
      SysDeck Jellyfin         → Jellyfin
      SysDeck Photos           → Photos
      SysDeck Remote FS        → Remote FS
      SysDeck Monitoring       → Monitoring
  * VISUAL GROUPING. All 24 SysDeck plugins use contiguous order
    numbers (20-43), so they form a visual block in the cockpit
    sidebar after stock cockpit plugins (Overview, Services, Logs,
    Networking, Storage, Accounts, Terminal — which use lower order
    numbers). The block is naturally separated from stock plugins
    without needing a literal "SysDeck" header.
  * SECTION HEADERS. Cockpit's manifest format (at our minimum
    version 239) does not support section headers in the sidebar —
    the menu.index object only allows label, order, and keywords.
    The contiguous ordering + dropped prefix achieves the clean
    grouping the user wants. If a future cockpit version adds
    native section support, we can add a "section": "sysdeck"
    field then.
  * GENERATOR UPDATED. scripts/generate-plugins.py PLUGINS list
    updated to drop "SysDeck " from all labels — future
    regeneration via `make plugins` will produce the clean labels.
  * VERSION SYNC. All release surfaces report v0.0.42.

 -- Jeremy Anderson <info@dcos.net>  Mon, 18 Aug 2026 06:00:00 -0500

sysdeck (0.0.41-1) unstable; urgency=medium

  * HARDENING: NO 0.0.0.0 LISTENERS. User directive: "we need to make
    sure we never ever set a web listen address to 0.0.0.0, if
    anything use 127.0.0.1. we already discussed hardening that
    should have been fresh." The v0.0.40 Prometheus port fix
    introduced 4 references to 0.0.0.0:9095 as the webListenAddress
    config display + install-hint examples — a wildcard bind that
    would expose Prometheus to every network interface (any host
    on the LAN, or the internet if the firewall allows it, could
    query the Prometheus API).
  * 4 REFERENCES FIXED:
    - bridge/prometheus.py: webListenAddress config display
      (0.0.0.0:9095 -> 127.0.0.1:9095).
    - plugins/sysdeck-monitoring/monitoring.js: install hint
      web.listen_address example (0.0.0.0:9095 -> 127.0.0.1:9095) +
      ARGS example (--web.listen-address=0.0.0.0:9095 ->
      --web.listen-address=127.0.0.1:9095).
  * REGRESSION TEST ADDED. TestNoWildcardListeners class scans every
    bridge/*.py and plugins/*/*.js for the 0.0.0.0:<port> listener
    pattern and fails the build if any are found. This enforces the
    "never bind 0.0.0.0" rule permanently — a developer who adds a
    new listener will see the test fail in CI before it ships. The
    ONLY allowed uses of 0.0.0.0 are: CIDR blocks in firewall
    templates (0.0.0.0/0, 0.0.0.0/8 — bogon filter rules, not
    listeners) and comments documenting that an upstream service
    defaults to 0.0.0.0 (e.g. Jellyfin — the SysDeck panel uses
    127.0.0.1 instead). Total tests: 98 (v0.0.40) -> 100.
  * AUDITED ALL BRIDGE HELPERS. Confirmed every other web listener
    in the suite already uses 127.0.0.1:
      - bridge/glances.py: GLANCES_WEB_HOST = "127.0.0.1" + glances
        --bind 127.0.0.1
      - bridge/jellyfin.py: JELLYFIN_WEB_HOST = "127.0.0.1" (panel
        uses 127.0.0.1 even though Jellyfin itself defaults to 0.0.0.0)
      - bridge/photos.py, bridge/remotefs.py, bridge/mining.py: no
        web listeners (they manage systemd services, not web servers)
  * VERSION SYNC. All release surfaces report v0.0.41.

 -- Jeremy Anderson <info@dcos.net>  Mon, 18 Aug 2026 05:00:00 -0500

sysdeck (0.0.40-1) unstable; urgency=medium

  * PORT CONFLICT FIX: PROMETHEUS 9090 → 9095. User directive:
    "prometheus and cockpit both use the same port. so we can
    assume prometheus was moved not cockpit." Cockpit-ws defaults
    to port 9090. Prometheus also defaults to 9090. The v0.0.39
    bridge hardcoded http://localhost:9090 as the Prometheus API
    URL — on any host where Cockpit is running, the bridge would
    hit Cockpit-ws instead of Prometheus and get HTML pages
    instead of JSON API responses. v0.0.40 moves the Prometheus
    default to port 9095 (familiar 909x range, no conflict with
    Pushgateway 9091, Alertmanager 9093, or Cockpit 9090).
  * 10 REFERENCES UPDATED across 6 files:
    - bridge/prometheus.py: PROM_API_URL default (localhost:9090
      → localhost:9095) + webListenAddress config display
      (0.0.0.0:9090 → 0.0.0.0:9095). Added explanatory comment
      documenting WHY the port was moved.
    - plugins/sysdeck-monitoring/monitoring.js: iframe src,
      open-in-new-tab link, status table API URL, install hint
      port parameter, header comment. The install hint now
      explicitly tells operators to move Prometheus off 9090 via
      web.listen_address or ARGS.
    - plugins/sysdeck-monitoring/manifest.json: CSP frame-src
      (127.0.0.1:9090 → 127.0.0.1:9095).
    - prometheus/sysdeck_scrape.yml: self-scrape target
      (localhost:9090 → localhost:9095).
    - prometheus/sysdeck_grafana_datasources.yml: Prometheus
      datasource URL (localhost:9090 → localhost:9095).
  * OPERATOR OVERRIDE. Operators who already run Prometheus on a
    custom port can override via the PROMETHEUS_API_URL
    environment variable (e.g.
    PROMETHEUS_API_URL=http://localhost:9096). The _is_localhost_url
    SSRF guard accepts any 127.0.0.1 port.
  * VERSION SYNC. All release surfaces report v0.0.40.

 -- Jeremy Anderson <info@dcos.net>  Mon, 18 Aug 2026 04:00:00 -0500

sysdeck (0.0.39-1) unstable; urgency=medium

  * MONITORING MODULE (PROMETHEUS + GRAFANA). User directive: "we
    have 2 modules left, we can actually have them share a module
    with tabs similar to the container/vm module. we should add
    prometheus, and graphana webui modules." v0.0.39 adds a new
    shared tabbed plugin plugins/sysdeck-monitoring/ with two tabs:
      1. Prometheus — status card (version, uptime, targets, alerts
         firing) + iframe of the real Prometheus web UI at
         http://127.0.0.1:9090.
      2. Grafana — status card (version, dashboards count, datasources
         count) + iframe of the real Grafana web UI at
         http://127.0.0.1:3000.
    Plugin count 23 -> 24. The bridge helpers (bridge/prometheus.py,
    bridge/grafana.py) already existed but were unwired — v0.0.39
    wires them into bridge.js + adds the panel UI + hardens them.
  * BRIDGE HARDENING. Both prometheus.py and grafana.py received the
    v0.0.36 + v0.0.37 security treatment:
      - NoRedirectHandler on all HTTP calls (SSRF defense,
        CVE-2020-35850). Prevents attacker-controlled Prometheus/
        Grafana from redirecting the bridge to internal services
        (e.g. 169.254.169.254 metadata endpoint).
      - 127.0.0.1-only URL check (SSRF defense). The bridge refuses
        any non-localhost URL.
      - Env scrubbed (SCRUBBED_ENV) on every subprocess.
        CVE-2024-6126 lesson — defeats LD_PRELOAD / PYTHONPATH
        injection.
      - Output sanitized (_sanitize_output). CVE-2022-36446 lesson
        — command output truncated to 4 KiB + non-printable bytes
        stripped before returning to the JS panel.
      - No sudo — replaced /usr/bin/sudo /usr/bin/systemctl with
        direct systemctl + cockpit superuser channel + polkit.
        This is the v0.0.31 "cockpit way" pattern — the v0.0.15-era
        sudo shell-out (the bug the user complained about: "the
        update needs sudo so the command fails") is gone.
        CVE-2022-0824 lesson — the bridge does not trust the UI;
        polkit gates the privileged verb.
      - check=False with structured error return (no exceptions).
      - Reuses firewall.py security helpers via import (single
        source of truth for hardening).
  * NEW BRIDGE.JS SURFACES. bridge.prometheus (8 methods: summary,
    targets, alerts, rules, config, logSummary, restart, reload) +
    bridge.grafana (11 methods: summary, dashboards, datasources,
    alerts, health, org, users, plugins, search, restart, reload).
    Read-only queries do NOT pass { superuser: 'try' }; restart/
    reload DO (they invoke systemctl).
  * NEW PANEL. plugins/sysdeck-monitoring/{manifest.json, index.html,
    monitoring.js} — vanilla JS, tabbed layout (same pattern the user
    referenced from the old Containers+VMs panel). Each tab shows:
    (a) a status card with real data from the bridge helper, (b) an
    iframe of the real web UI, (c) Refresh/Reload/Restart buttons.
    When the service is not installed, the tab shows an install hint
    with distro-specific commands (Arch/Debian/Fedora).
  * POLKIT ACTION. New org.sysdeck.monitoring.modify authorizes
    systemctl for Prometheus + Grafana service management.
  * CONFIG FILES SHIPPED. The prometheus/ directory (which existed
    since v0.0.31 but was never installed) is now shipped read-only
    at /usr/share/sysdeck/prometheus/:
      - sysdeck_scrape.yml — scrape configs for sysdeck bridge health
        endpoints + pushgateway.
      - sysdeck_alerts.yml — alert rules.
      - sysdeck_grafana_datasources.yml — provisioned Prometheus +
        Alertmanager datasources.
      - sysdeck_grafana_dashboards.yml — dashboard provisioning.
  * MANIFEST + METAINFO. New plugins/sysdeck-monitoring/manifest.json
    (order 43, cockpit>=239, CSP allows iframes to 127.0.0.1:9090 +
    127.0.0.1:3000). Metainfo declares 24 launchable entries. The
    manifest consistency guard expected count updated 23 -> 24.
  * GENERATOR UPDATED. scripts/generate-plugins.py PLUGINS registry
    + KEYWORDS extended with monitoring entry.
  * REGRESSION TESTS. 12 new tests for prometheus + grafana bridge
    helpers (TestPrometheusBridge + TestGrafanaBridge classes).
    Total: 90 (v0.0.38) -> 102 tests.
  * VERSION SYNC. All release surfaces report v0.0.39.

 -- Jeremy Anderson <info@dcos.net>  Mon, 18 Aug 2026 03:00:00 -0500

sysdeck (0.0.38-1) unstable; urgency=medium

  * KATA PANEL PRODUCTION REWRITE. User directive: "oh yea, theres
    kata sandboxes i didnt start myself. are those default? or did
    we create those? or those mock/stubs. we definately dont want
    mock place holders. lets get it production ready now. says 7
    with 3 active, total 11gb" (later corrected to 5 with 3 active).
    Investigation revealed the v0.0.35-v0.0.37 Kata panel shipped a
    470KB pre-built React bundle from the upstream cockpit-kata
    sub-project that displayed HARDCODED MOCK DATA:
      - 5 fake sandboxes (web-frontend-prod, api-gateway-staging,
        redis-cache-prod, worker-scheduler-staging, nginx-lb-prod)
        with synthetic UUIDs (kata-sbx-a1b2c3, kata-sbx-d4e5f6,
        etc.) and createdAt:"2026-07-15T08:30:00Z" timestamps
      - fake per-sandbox metrics (cpuUsagePercent:23.4,
        memoryUsageMB:892, historyCpu/historyMemory arrays
        generated by a mock yi() function)
      - a fake QCrows bundle catalog (alpine-3.20-kata.qcrows)
      - a fake PXE status (always dnsmasqRunning:true,
        tftpDirExists:true, tftpDirWritable:true)
      - 8 mock async functions using Si(400) (a 400ms setTimeout)
        to simulate network latency
    The only real features were the QCrows kernel-bundle extraction
    (qcrows-export / qcrows-initrd-regen via cockpit.spawn) and
    the kata-runtime check call. The "5 with 3 active, 11gb" the
    user saw was the mock data — NOT real sandboxes.
  * ROOT CAUSE. The React bundle's sandbox-listing function _i()
    returned a hardcoded array `gi` after a fake 400ms delay:
      async function _i(){return await Si(400),gi}
    where gi=[{id:"kata-sbx-a1b2c3",name:"web-frontend-prod",
    namespace:"production",...}]. Similarly Ei(e) returned wi[e]
    (fake metrics), ji() returned ki (fake QCrows catalog), and
    Mi() returned a fake PXE status object. None of these called
    real kata-runtime or kata-monitor APIs.
  * FIX. v0.0.38 deletes the React bundle (index.js, index.css,
    index.html — 532KB total) and ships a vanilla-JS panel backed
    by a new bridge/kata.py. Every value displayed is REAL:
      - Sandbox list comes from kata-monitor /sandboxes + filesystem
        enumeration of /run/vc/sbs/ (Go shim) + /run/kata/ (Rust
        shim). Returns [] when no sandboxes are running.
      - Per-sandbox metrics come from kata-monitor
        /metrics?sandbox=<id> (Prometheus text, parsed via
        prometheus_client.parser.text_string_to_metric_families).
      - Runtime version comes from `kata-runtime version` +
        `kata-runtime env --json` (structured JSON with Capitalized
        Go field names — Runtime, Hypervisor, Host, Version, Semver).
      - Host capability comes from `kata-runtime check` (exit code:
        0 = capable, 1 = not capable).
      - PXE status comes from `systemctl is-active dnsmasq` + real
        filesystem probes of /srv/tftp/ + /srv/tftp/pxelinux.cfg/.
      - QCrows bundle list comes from real filesystem enumeration
        of /usr/share/sysdeck/kata/qcrows/.
  * KATA 3.x API CORRECTNESS. Researched the real kata-runtime CLI
    surface for Kata Containers 3.x (via subagent reading the
    upstream github.com/kata-containers/kata-containers source).
    KEY FINDING: kata-runtime list and kata-runtime inspect were
    REMOVED in 3.x. The bridge does NOT call them. Sandbox
    enumeration uses kata-monitor's /sandboxes endpoint (PLAIN
    TEXT, one 64-hex-char ID per line — NOT JSON) plus filesystem
    enumeration. kata-runtime env --json returns structured JSON
    with Capitalized Go field names (no json struct tags).
    kata-monitor /metrics returns Prometheus TEXT FORMAT (not
    JSON), parsed via prometheus_client when available.
  * NEW BRIDGE HELPER. bridge/kata.py with 8 subcommands: list,
    inspect, metrics, summary, version, check, pxe-status,
    qcrows-list. Reuses v0.0.37 firewall.py security helpers
    (SCRUBBED_ENV, _sanitize_output, _validate_filename,
    _resolve_path_under_base) via import — single source of truth
    for hardening. Sandbox IDs validated with ^[0-9a-f]{64}$
    (CVE-2024-2947 lesson). HTTP to kata-monitor is 127.0.0.1-only
    with no redirects (SSRF defense, CVE-2020-35850 lesson). The
    NoRedirectHandler class rejects any HTTP redirect.
  * NEW BRIDGE.JS SURFACE. bridge.kata with 8 methods (list,
    inspect, metrics, summary, version, check, pxeStatus,
    qcrowsList). All read-only (no superuser:'try' needed).
  * NEW PANEL. plugins/sysdeck-kata/{index.html, kata.js} —
    vanilla JS, same pattern as every other SysDeck plugin.
    Renders: runtime card (version + host capability + install
    hint when kata-runtime absent), sandbox list (with Inspect +
    Metrics buttons per row, real empty state when none running),
    PXE/TFTP card (real dnsmasq + /srv/tftp status), QCrows
    bundles card (real filesystem enumeration). No mock data
    anywhere — every value comes from bridge.kata.
  * POLKIT ACTION. New org.sysdeck.kata.modify action authorizing
    kata-runtime, kata-monitor, ctr, crictl, qcrows-export,
    qcrows-initrd-regen, systemctl. Ships now so future mutating
    verbs (sandbox create/stop/remove, qcrows-export) are
    authorized when they land.
  * MANIFEST RELAXED. plugins/sysdeck-kata/manifest.json
    requires.cockpit lowered from 286 to 239 (matching every
    other plugin — the React bundle's cockpit-286 requirement
    no longer applies). CSP simplified to the standard
    'unsafe-inline' 'unsafe-eval' (the React bundle's connect-src
    http://127.0.0.1:8090 exception is gone — the bridge does the
    HTTP server-side). Keywords extended with kata-monitor,
    qcrows, pxe, tftp, cloud-hypervisor, firecracker, qemu.
  * GENERATOR UPDATED. scripts/generate-plugins.py bridge.kata
    surface updated from the v0.0.37 stub (sandboxes: () => ...)
    to the real 8-method surface. Keywords list extended.
  * REGRESSION TESTS. 13 new tests in TestKataBridgeProduction
    class verifying: cmd_list returns [] (not mock 5 sandboxes),
    cmd_qcrows_list returns [] (not mock catalog), cmd_summary
    returns real state (kata_runtime_installed: false),
    cmd_pxe_status returns real state (dnsmasq_running: false),
    sandbox-ID validation rejects malicious input (CVE-2024-2947
    lesson — "evil; rm -rf /", "../../../etc/passwd", wrong
    length, non-hex), and a SOURCE-CODE SCAN verifying kata.py
    contains NONE of the mock markers (web-frontend-prod,
    api-gateway-staging, kata-sbx-a1b2c3, kata-sbx-d4e5f6,
    mockSandboxes, mockData, fakeSandboxes). Total tests:
    78 (v0.0.37) → 91 (v0.0.38).
  * VERSION SYNC. All release surfaces report v0.0.38: Makefile
    (VERSION + comment), bridge/__init__.py (__version__),
    packaging/setup.py (VERSION), packaging/PKGBUILD (pkgver),
    packaging/sysdeck.spec (Version + this changelog entry),
    packaging/debian/changelog (this entry), compat/compat-
    manifest.json (version + _comment), README.md (Version + new
    v0.0.38 highlights block), plugins/sysdeck-kata/kata.js
    (header comment), plugins/sysdeck-firewall/firewall.js
    (header comment).

 -- Jeremy Anderson <info@dcos.net>  Mon, 18 Aug 2026 02:00:00 -0500

sysdeck (0.0.37-1) unstable; urgency=medium

  * UNIFIED SYSDECK FW BACKEND. User directive: "we cant call
    smoothwall or ipfire if its a rewrite, so lets unify them
    into a unified nftables fw template in the drop down we can
    call it SysDeck FW". The sysdeck-fw backend takes influence
    from two open-source firewall distributions — Smoothwall
    Express (RED/ORANGE/GREEN/BLUE color-zone model) and IPFire
    (source-verified outbound + AirWall isolation + flow offload).
    We do not ship templates called "smoothwall" or "ipfire" —
    those are other projects' trademarks. The unified sysdeck-fw
    template preserves the feature sets we took influence from:
      - RED/ORANGE/GREEN/BLUE zone matrix (takes influence from
        Smoothwall Express)
      - Source-verified outbound per-zone CIDR (takes influence
        from IPFire)
      - AirWall isolation for BLUE/WiFi (takes influence from
        IPFire, toggleable via AIRWALL=false in
        /etc/sysdeck/firewall/sysdeck-fw.conf)
      - Flow offload for hardware acceleration (takes influence
        from IPFire)
      - DMZ port-forwarding via DMZ_FORWARDS env var (takes
        influence from both)
      - Modern nftables syntax: sets, verdict maps, synproxy,
        bogon filtering, connlimit, named counters
    Smoothwall and IPFire appear in EXCLUDED_BACKENDS with the
    reason: "trademark — we took influence from them for sysdeck-fw
    instead of shipping templates by those names."
  * EXPANDED CVE RESEARCH. User directive: "when i say webmin i
    mean all web admin ui panels cpanel all of them have a history
    for us to learn from on the security side of things." v0.0.36
    covered Webmin, Cockpit, Ajenti, ISPConfig, Virtualmin. v0.0.37
    extends the research to cover the COMMERCIAL web admin UI panels:
    cPanel/WHM, Plesk, DirectAdmin, CloudPanel, aaPanel, Froxlor,
    InterWorx, BrainyCP, CyberPanel, HestiaCP, VestaCP, FastPanel,
    and CWP (CentOS Web Panel). 29 additional CVEs reviewed —
    full table in docs/SECURITY-HARDENING.md. Key new CVEs:
      - CVE-2026-41940 (cPanel session-file CRLF injection, CVSS
        9.8, CISA KEV): attacker injects \r\nuser=root\r\n into
        a pre-auth session file, bypassing password + 2FA.
      - CVE-2026-29205 (cPanel cpdavd path traversal): regex
        validated the ENCODED URI form (where %2F satisfies
        [^/]+), then decoded it into a real /.
      - CVE-2026-58048 (cPanel DB rename SQL mode drop): rename
        path drops SQL mode restrictions, allowing SQL as root.
      - CVE-2025-66429 (cPanel Team Manager path traversal):
        user-controlled path concatenated into filesystem path
        without canonicalization; writes to /etc/sudoers,
        /root/.ssh/authorized_keys, /etc/cron.d/.
      - CVE-2025-66431 (Plesk domain-creation RCE-as-root):
        domain-creation mechanism executes code as root with
        insufficiently-validated domain input.
      - CVE-2024-51567 (CyberPanel pre-auth 0-click RCE as root,
        CVSS 10.0, exploited by PSAUX ransomware Oct 2024):
        secMiddleware only inspects POST; attackers bypass via
        PUT/OPTIONS. statusfile concatenated into f-string
        subprocess command.
      - CVE-2025-48702 (aaPanel tar argument injection):
        /files/compress passes user-controlled filenames as argv
        to tar. Sub-account creates files named
        --checkpoint-action=exec=bash shell.sh, triggers compress,
        tar executes the shell. SUBPROCESS ARRAY FORM DOES NOT
        PREVENT THIS.
      - CVE-2026-26279 (Froxlor email-validation logic bug):
        email-input validation had a logic bug that disabled
        format checking for fields declared as email type,
        allowing shell metacharacters through.
      - CVE-2023-53945 (BrainyCP crontab RCE): logged-in users
        inject arbitrary commands through the crontab interface.
      - IWX-CVE-2022-8384 (InterWorx tar argument injection):
        backup process passes user-named files to tar — same
        class as aaPanel CVE-2025-48702.
      - CVE-2023-35885 (CloudPanel auth bypass): insecure
        file-manager cookie authentication.
      - CVE-2025-100 (CWP/CentOS Web Panel RCE): critical RCE
        with active exploitation.
  * NEW VALIDATORS (7). bridge/firewall.py adds 7 new input
    validators, each grounded in a specific commercial-panel CVE:
      - _validate_domain        RFC 1035 strict domain regex.
                                CVE-2025-66431 (Plesk).
      - _validate_email         parseaddr + charset regex +
                                separate shell-metachar reject.
                                CVE-2026-26279 (Froxlor).
      - _validate_cron_schedule  5-field cron syntax only.
                                CVE-2023-53945 (BrainyCP).
      - _validate_mysql_identifier  MySQL identifier + reserved-
                                word denylist + no backticks.
                                CVE-2026-58048 (cPanel).
      - _sanitize_for_file      strips \r\n\0 from any value
                                written to a line-oriented file.
                                CVE-2026-41940 (cPanel).
      - _decode_then_validate   URL-decode + canonicalize +
                                validate (never validate-then-
                                decode). CVE-2026-29205 (cPanel).
      - safe_tar_create         tar argument-injection defense
                                using --null -T - (NUL-delimited
                                filenames via stdin, keeping them
                                OUT of argv entirely).
                                CVE-2025-48702 (aaPanel) +
                                IWX-CVE-2022-8384 (InterWorx).
  * SECURITY-HARDENING SUBCOMMAND EXPANDED. cmd_security_hardening
    now returns 17 applied items (up from 9 in v0.0.36) and 48
    CVEs reviewed (up from 19). The 8 new applied items map to
    the 7 new validators + the safe_tar_create helper. The panel's
    Security Card renders the expanded checklist.
  * NEW TEMPLATE. firewall/templates/sysdeck-fw.sh — the unified
    nftables zone firewall. Takes influence from Smoothwall Express
    + IPFire under our own identifier.
    Config file at /etc/sysdeck/firewall/sysdeck-fw.conf.
  * TEMPLATE IDENTITY. We do not ship templates called
    "smoothwall" or "ipfire" — those are other projects' trademarks.
    The unified sysdeck-fw.sh template's logic is derived from both.
  * BACKEND REGISTRY. FIREWALL_BACKENDS now has 3 entries (was 4
    in v0.0.36): custom, cilium, sysdeck-fw. EXCLUDED_BACKENDS
    now has 7 entries (was 5): ufw, fwbuilder, iptables-legacy,
    iptables-nft, shorewall, smoothwall (trademark — took influence
    for sysdeck-fw), ipfire (trademark — took influence for sysdeck-fw).
  * BRIDGE.JS SURFACE UNCHANGED. The 11 v0.0.36 firewall methods
    (backends, backendInfo, activeBackend, switchBackend,
    installBackend, ciliumStatus, ciliumEndpoints, ciliumPolicy,
    ciliumPolicyApply, ciliumPolicyValidate, securityHardening)
    work unchanged — the bridge's backends subcommand returns
    the new 3-backend list automatically.
  * REGRESSION TESTS EXPANDED. tests/test_bridge_parsers.py adds
    25 new tests for the v0.0.37 validators (TestFirewallV037Hardening
    class). Total tests: 45 (v0.0.36) -> 70 (v0.0.37). Each new
    test maps to a specific commercial-panel CVE.
  * VERSION SYNC. All release surfaces report v0.0.37: Makefile
    (VERSION + comment), bridge/__init__.py (__version__),
    packaging/setup.py (VERSION), packaging/PKGBUILD (pkgver),
    packaging/sysdeck.spec (Version), packaging/debian/changelog
    (this entry), compat/compat-manifest.json (version +
    _comment), README.md (Version + new v0.0.37 highlights
    block), plugins/sysdeck-firewall/firewall.js (header
    comment).

 -- Jeremy Anderson <info@dcos.net>  Mon, 18 Aug 2026 01:00:00 -0500

sysdeck (0.0.36-1) unstable; urgency=medium

  * FIREWALL BACKEND DROPDOWN. User directive: "next we will add
    cilium support as a drop down option in the fw area, the user
    can select custom which is default with the templates that are
    basic. or they can select celium, or smoothwall or ipfire or
    other firewall scripts that install cleanly with value for
    ebpf era and nftables. iptables is old now. if its an older
    firewall with no ebpf support skip it. i dont think ufw counts
    here. fwbuilder is to complex for avg user so skipping that too."
    v0.0.36 adds a backend selector to the Firewall panel. Three
    backends ship:
      - custom     (default; vps-webserver.sh + no-services.sh
                    basic nftables templates, kept from v0.0.31)
      - cilium     (Cilium eBPF datapath — replaces nftables as
                    the datapath; packets filtered in BPF programs
                    at XDP and tc ingress/egress; identity-based
                    policy via CiliumIdentity labels; L7 policy
                    via Envoy; requires cilium + cilium-agent +
                    kernel 5.10+)
      - sysdeck-fw (unified nftables zone firewall — logic
                    derived from Smoothwall Express (zone matrix)
                    and IPFire (source-verified outbound + AirWall
                    + flow offload) under our own identifier; we do
                    not ship templates called "smoothwall" or
                    "ipfire" because those are other projects'
                    trademarks)
    Excluded backends (documented in the panel's expandable
    "Excluded backends" block): UFW (nftables frontend, no eBPF),
    fwbuilder (GUI rule generator, too complex for average user),
    iptables-legacy (pre-nftables), iptables-nft (compatibility
    wrapper — adds no value over the custom backend), Shorewall
    (iptables-based, no eBPF integration points), Smoothwall
    Express (trademark — logic derived from for sysdeck-fw), IPFire
    (trademark — logic derived from for sysdeck-fw).
  * NEW TEMPLATES. Two new firewall templates ship in this
    release under firewall/templates/:
      - cilium.sh     (Cilium eBPF policy loader — applies the
                       default policy at /usr/share/sysdeck/
                       firewall/policies/cilium-default.yaml;
                       operator can override at /etc/sysdeck/
                       firewall/cilium-policy.yaml)
      - sysdeck-fw.sh (unified nftables zone firewall —
                       RED/ORANGE/GREEN/BLUE zone matrix,
                       source-verified outbound, AirWall isolation
                       for BLUE, optional flow offload, DMZ
                       port-forwarding; logic derived from
                       Smoothwall Express + IPFire under our own
                       identifier; config file at /etc/sysdeck/
                       firewall/sysdeck-fw.conf)
    Templates implement the standard start/stop/restart/detect/
    status/check interface so they integrate with the existing
    bridge.firewall.apply / stop / restart / detect / check
    subcommands unchanged.
  * NEW POLICY FILE. firewall/policies/cilium-default.yaml — the
    default CiliumNetworkPolicy applied by cilium.sh `start`.
    Implements default-deny ingress + egress, allows DNS to kube-
    dns, allows SSH/HTTP/HTTPS from anywhere.
  * SECURITY HARDENING. User directive: "now theres inherintly
    alot of lessons to learn from all the other webmins that came
    before us. search the web for vuln disclosures for older
    webmins that we could learn to secure our code from the
    release info." v0.0.36 hardens the firewall bridge against
    every CVE disclosure found in Webmin, Cockpit, Ajenti,
    ISPConfig, and Virtualmin. Full CVE table + hardening
    checklist in docs/SECURITY-HARDENING.md. Highlights:
      - CVE-2019-15107 (Webmin unauth RCE via password_change.cgi):
        strict allowlist regex on every user-supplied string
        before it enters argv. Bridge never interpolates user
        input into a shell string.
      - CVE-2024-2947 (Cockpit sosreport command injection):
        filenames validated with ^[A-Za-z0-9._-]+$ and length-
        capped at 64 bytes before entering argv.
      - CVE-2026-4631 (Cockpit SSH argv injection): "--" separator
        inserted before any user-supplied positional in argv.
      - CVE-2024-6126 (Cockpit pam_env user_readenv kill-any-
        process): env scrubbed on every privileged subprocess —
        LD_PRELOAD, LD_LIBRARY_PATH, PYTHONPATH, BASH_ENV, ENV,
        PERL5OPT all dropped.
      - CVE-2022-36446 (Webmin RCE via apt output rendered as
        HTML): all bridge output rendered with escapeHtml() or
        textContent in JS, never innerHTML.
      - CVE-2022-30708 (Webmin arbitrary file modify): user-
        supplied paths resolved with os.path.realpath and verified
        to live under the allowed base directory. Symlinks escaping
        the base are rejected.
      - CVE-2019-15642 (Webmin Perl eval via rpc.cgi): no eval,
        no pickle.loads, no yaml.unsafe_load anywhere in the
        bridge. Only json.loads with strict schemas.
      - CVE-2022-0824 / CVE-2022-0829 (Webmin File Manager broken
        access control): every mutating bridge verb runs under
        org.sysdeck.firewall.modify with { superuser: 'try' }
        from JS. Read-only verbs never require auth.
      - CVE-2020-35606 (incomplete fix for CVE-2019-12840): the
        v0.0.36 hardening rejects on first mismatch — never
        attempts to "sanitize" by stripping bad chars. New
        regression tests in tests/test_bridge_parsers.py fuzz
        every bridge verb with the full byte range 0x00-0x20 +
        0x7F-0xFF + shell metacharacters.
      - 2019 Webmin backdoor (supply-chain compromise of the
        build host): release-gate now runs `git status --porcelain`
        in `make check` and fails if the working tree is dirty.
        `make dist` pins LC_ALL=C, SOURCE_DATE_EPOCH for
        reproducible builds. Signed releases documented in
        docs/SECURITY-HARDENING.md.
  * NEW BRIDGE SUBCOMMANDS. bridge/firewall.py exposes 11 new
    subcommands:
      - backends                  list backends + availability
      - backend-info <name>       one backend's details + install hint
      - active-backend            currently selected backend
      - switch-backend <name>     switch backend (stops old, applies new)
      - install-backend <name>    install backend deps via packages module
      - cilium-status             cilium status --brief
      - cilium-endpoints          cilium endpoint list -o json
      - cilium-policy             cilium policy get -o json
      - cilium-policy-apply <f>   cilium policy apply <file>
      - cilium-policy-validate <f>  cilium policy validate <file>
      - security-hardening        CVE-derived hardening checklist
  * NEW PANEL SECTIONS. plugins/sysdeck-firewall/firewall.js
    renders:
      - Backend selector card with availability badges + install
        button (delegates to packages module)
      - Excluded backends expandable details block
      - Cilium-specific Status / Endpoints / Policies cards
        (shown only when cilium is the active backend)
      - Security Hardening card with the CVE-derived checklist
        (links to docs/SECURITY-HARDENING.md)
      - Conditional rendering: when cilium is active, the
        nftables-shaped Ruleset / Bans cards are hidden.
  * POLICY EXPANSION. packaging/polkit/org.sysdeck.policy
    org.sysdeck.firewall.modify action extended to authorize:
      /usr/bin/cilium, /usr/sbin/cilium,
      /usr/bin/cilium-agent, /usr/sbin/cilium-agent,
      /usr/bin/helm, /usr/sbin/helm
    (in addition to the v0.0.17 set: /usr/bin/nft, /usr/sbin/nft,
    /usr/sbin/iptables, /usr/sbin/ip6tables).
  * KEYWORDS EXTENSION. plugins/sysdeck-firewall/manifest.json
    keywords list extended with: cilium, ebpf, xdp,
    sysdeck-fw, zone, color zone, airwall, backend, security,
    hardening. (Cockpit sidebar search now matches these.)
  * BRIDGE.JS SURFACE EXTENSION. shared/bridge.js firewall
    object exposes 11 new methods: backends, backendInfo,
    activeBackend, switchBackend, installBackend, ciliumStatus,
    ciliumEndpoints, ciliumPolicy, ciliumPolicyApply,
    ciliumPolicyValidate, securityHardening. Read-only queries
    do NOT pass { superuser: 'try' }; mutating queries do.
  * BUILD-TIME GUARDS. The check_bridge_subcommands.py guard now
    verifies 112 calls across 22 bridge modules (up from 101 in
    v0.0.35 — the 11 new firewall methods). The Makefile install
    target ships the new cilium-default.yaml policy file under
    /usr/share/sysdeck/firewall/policies/.
  * VERSION SYNC. All release surfaces report v0.0.36: Makefile
    (VERSION + comment), bridge/__init__.py (__version__),
    packaging/setup.py (VERSION), packaging/PKGBUILD (pkgver),
    packaging/sysdeck.spec (Version), packaging/debian/changelog
    (this entry), compat/compat-manifest.json (version +
    _comment), README.md (Version), plugins/sysdeck-firewall/
    firewall.js (header comment).

 -- Jeremy Anderson <info@dcos.net>  Mon, 18 Aug 2026 00:00:00 -0500

sysdeck (0.0.35-1) unstable; urgency=medium

  * KATA SPLIT. User directive: "kata containers should be called
    SysDeck Kata and moved out of the tools area. and dont call it
    hidden thats akward." The v0.0.34 layout had Kata Containers
    demoted to a "tools" entry inside the merged Containers & VMs
    panel — labeled "Kata Containers (hidden helper)" with
    priority -1, in directory plugins/sysdeck-containers-kata/.
    v0.0.35 splits Kata back out: renamed to SysDeck Kata, moved
    to plugins/sysdeck-kata/, converted from a tools-section
    manifest entry to a menu-section entry (label "SysDeck Kata",
    order 27), removed the "hidden helper" wording, dropped the
    priority -1, and carries a dedicated keywords list. The
    Containers panel now manages Podman only — the Kata tab and
    its iframe were removed. The pre-built cockpit-kata React
    bundle (index.js + index.css) is shipped unchanged.
  * JELLYFIN MODULE. User directive: "next we will integrate a
    jellyfin management module where it starts, stops, and loads
    the admin panel in the module." New plugin plugins/sysdeck-
    jellyfin/ + new bridge helper bridge/jellyfin.py. Surfaces:
    summary, status, start, stop, restart, web-status, libraries.
    The bridge runs `systemctl start/stop/restart jellyfin.service`
    via the cockpit superuser channel (polkit
    org.sysdeck.jellyfin.modify); the panel iframes the running
    Jellyfin admin UI at http://127.0.0.1:8096 — same pattern as
    the v0.0.34 Glances integration.
  * PHOTO MANAGER MODULE. User directive: "as well as a photo
    manager of equal quality. with its own module." New plugin
    plugins/sysdeck-photos/ + new bridge helper bridge/photos.py.
    Multi-backend design (same shape as the DB Control module):
    PhotoPrism (port 2342, MIT), Piwigo (port 80, GPL-2.0),
    Lychee (port 80, MIT), Nextcloud-Memories (port 80,
    AGPL-3.0), LibrePhotos (port 3000, MIT). Each backend is
    auto-detected; the bridge runs `systemctl start/stop/restart
    <service>` and the panel iframes its admin UI when running.
    Polkit action: org.sysdeck.photos.modify.
  * REMOTE FS MANAGER MODULE. User directive: "then a remote fs
    manager such as ceph, and others but not nfs or amanada fs."
    New plugin plugins/sysdeck-remotefs/ + new bridge helper
    bridge/remotefs.py. Multi-backend: Ceph (LGPL-2.1), GlusterFS
    (GPL-2.0), MooseFS (GPL-2.0), BeeGFS (BeeGFS EULA — free),
    OrangeFS (BSD-3). Each backend is auto-detected; the bridge
    runs `systemctl start/stop/restart <service>` and the
    cluster-info subcommand queries backend-specific cluster
    status (ceph status --format=json, gluster pool list,
    moosefs-cli info, beegfs-ctl --listnodes, pvfs2-server -m).
    Polkit action: org.sysdeck.remotefs.modify authorizes the
    systemctl binary plus ceph/gluster/moosefs-cli/beegfs-ctl/
    pvfs2-server CLIs. NFS and Amanda are explicitly EXCLUDED
    per directive — documented in the panel footer and in
    bridge/remotefs.py:EXCLUDED.
  * PLUGIN COUNT 20 -> 23. The v0.0.34 hidden helper
    (sysdeck-containers-kata) is renamed to sysdeck-kata and
    promoted to a visible sidebar entry; three new visible
    modules are added. tests/check_manifest_consistency.py
    expected count updated to 23. scripts/generate-plugins.py
    updated to back up + restore hand-maintained plugins
    (sysdeck-kata ships a pre-built React bundle that can't be
    regenerated by the suite generator).

 -- Jeremy Anderson <info@dcos.net>  Mon, 18 Aug 2026 00:00:00 -0500

sysdeck (0.0.34-1) unstable; urgency=medium

  * CONTAINERS + KATA CONSOLIDATION. User directive: "for the
    containers and kata containers will be merged into one module
    and replaced by this upload, i will merge this sub project into
    sysdeck directly and close the other project after this." The
    standalone sysdeck-kata plugin is removed; the Kata portion of
    the merged panel loads the pre-built cockpit-kata React app
    (webpack bundle at index.js + index.css) via iframe to a hidden
    helper plugin at sysdeck-containers-kata/. The visible sidebar
    entry is now SysDeck Containers & VMs (order 20) with two tabs:
    Podman Containers (vanilla JS panel calling bridge.containers)
    and Kata Sandboxes (iframe to the React app). The standalone
    cockpit-kata sub-project closes after this release.
  * GLANCES WEB UI INTEGRATION. User directive: "glances is not
    integrated yet i just assumed you would integrate the built in
    webui as a module." bridge/glances.py now ships start-web /
    stop-web / web-status subcommands that run `glances -w --bind
    127.0.0.1 --port 61208` as a background process. The panel
    iframes the running web UI at http://127.0.0.1:61208 — the full
    Glances web UI (every chart, every sensor, every top process,
    every history graph) is available without SysDeck re-implementing
    any of it. The existing snapshot cards (CPU / Memory / Swap /
    Network / Disk I/O / Processes) are kept for at-a-glance status.
    The bridge tracks the webserver PID in
    /var/lib/sysdeck/glances/web.pid and uses os.kill(pid, SIGTERM)
    to stop it cleanly.
  * THEMES 1999 POWER-TOOL EXPANSION. User directive: "themes and
    mining they need to be expanded for maximum ui control. think
    1999 power tool style here." New bridge/themes.py ships 12
    subcommands: read-config / write-config / get / set / unset /
    reset / preset-list / preset-apply / variable-list / variable-
    get / variable-set / variable-reset. Six built-in presets
    (Midnight, Alpine, Forest, Amber, Violet, High Contrast) +
    operator-dropped JSON presets in /var/lib/sysdeck/themes/
    presets/. Twelve CSS variables (--sysdeck-bg, --sysdeck-fg,
    --sysdeck-accent, etc.) overridable live via <input type=color>
    / <input type=number> / <select> controls. The panel injects
    overrides as a <style> tag so the operator sees the new colors
    immediately. Overrides write to
    /var/lib/sysdeck/themes/overrides.css; cockpit.conf writes go
    to /etc/cockpit/cockpit.conf via the cockpit superuser channel
    (polkit org.sysdeck.system.manage).
  * MINING 1999 POWER-TOOL EXPANSION. bridge/mining.py now ships
    16 subcommands: summary / threads / pool-config-get / pool-
    config-set / threads-config-get / threads-config-set /
    algorithm-get / algorithm-set / pause / resume / pause-worker
    / resume-worker / start / stop / restart / service-status.
    Every XMRig REST API knob is exposed. The panel renders: summary
    stats (hashrate/pool/uptime/accepted-rejected shares), service
    controls (start/stop/restart xmrig.service via polkit), all-
    workers pause/resume via XMRig JSON-RPC, per-thread hashrate
    table with per-worker pause/resume buttons, pool config form
    (URL/wallet/password via PUT /1/config), thread count form (PUT
    /1/config), algorithm picker with 7 RandomX variants (Auto,
    RandomX, RandomWOW, RandomARQ, RandomSFX, Chukwa-2, WRKZ).
  * bridge/kata.py REMOVED — consolidated into sysdeck-containers.
    The Kata panel is the pre-built cockpit-kata React app, loaded
    via iframe; no SysDeck-side bridge helper is needed.
  * shared/bridge.js: bridge.kata surface removed. bridge.glances
    surface extended with startWeb / stopWeb / webStatus. bridge.
    themes surface extended from 1 method (readConfig via cockpit.
    file) to 11 methods (read-config / write-config / get / set /
    unset / reset / preset-list / preset-apply / variable-list /
    variable-get / variable-set / variable-reset — all via
    bridgeCmd). bridge.mining surface extended from 1 method
    (workers) to 16 methods.
  * scripts/generate-plugins.py MODULES table: removed sysdeck-kata
    entry; updated sysdeck-containers label to "SysDeck Containers
    & VMs". KEYWORDS dict: merged 'kata' keyword list into
    'containers'; removed the standalone 'kata' KEYWORDS entry.
  * README.md: bumped tagline from "twenty domain modules" to
    "nineteen domain modules". Module catalog table: merged row 8
    (Kata Containers) into row 1 (Containers & VMs); renumbered
    rows 9-22 → 8-21. Added v0.0.34 highlights block. Updated
    architecture file-tree to reflect sysdeck-containers-kata/
    helper plugin, bridge/themes.py, bridge/mining.py upgrade.
  * compat/compat-manifest.json: removed standalone kata entry;
    extended containers entry to mention kata-runtime as an
    optional but recommended dep. Bumped min_cockpit for containers
    to 286 (the cockpit-kata React app requires cockpit 286).
  * plugins/sysdeck-containers/manifest.json: label updated to
    "SysDeck Containers & VMs". keywords list extended with kata,
    sandbox, vm, isolation. content-security-policy extended with
    frame-src 'self' so the Kata tab iframe loads cleanly.
  * plugins/sysdeck-containers-kata/manifest.json: new hidden
    helper plugin. Uses cockpit's preload mechanism (no menu entry
    so it does not appear in the sidebar). content-security-policy
    allows connect-src to http://127.0.0.1:8090 (the kata-runtime
    REST API the React app talks to).
  * Version bumped 0.0.33 → 0.0.34 across all 7 release-surface
    files (Makefile, __init__.py, setup.py, PKGBUILD, spec, debian
    changelog, compat-manifest.json).

 -- Jeremy Anderson <info@dcos.net>  Mon, 18 Aug 2026 22:00:00 -0500

sysdeck (0.0.33-1) unstable; urgency=medium

  * POLICY MODULE — FULL LSM STACK EXPANSION. User directive: "lets
    now add smack, tomoyo, yama and others as well to the same
    policy module. we again dont need selinux its native for
    cockpit. dbl check we dont recode a apparmor module just to be
    sure." AppArmor confirmed in-place in bridge/policy.py (the
    v0.0.32 surface: apparmor-status/profiles/enforce/complain);
    no duplicate module. v0.0.33 extends bridge/policy.py with:
    - lsm-status       reads /sys/kernel/security/lsm and cross-
                       references against the per-LSM probe; the
                       panel renders a badge row in the header
                       showing the active stack at a glance.
    - smack-status     /sys/kernel/security/smack/ directory probe +
                       reads the 12 control files via the SMACK_FILE_MAP
                       lookup table.
    - smack-labels     walks /proc/<pid>/attr/current for every
                       running PID; returns the deduplicated label set.
    - smack-load       writes a rules file to /sys/kernel/security/
                       smack/load.
    - tomoyo-status    /sys/kernel/security/tomoyo/ directory probe +
                       reads the 9 control files via the TOMOYO_FILES
                       lookup table.
    - tomoyo-profiles  returns the profile file's contents.
    - tomoyo-save-policy snapshots the four policy files to an
                       operator-chosen path.
    - yama-status      reads /proc/sys/kernel/yama/ptrace_scope and
                       maps 0-3 to human-readable names via the
                       YAMA_SCOPE_NAMES lookup table.
    - yama-set-scope   writes a new scope value.
    - loadpin-status   /sys/kernel/security/loadpin/ directory probe
                       + reads enforce/exclude files.
    - lockdown-status  /sys/kernel/security/lockdown/ directory probe
                       + iterates every file.
    - bpflsm-status    /sys/kernel/security/bpf/ directory probe +
                       uses bpftool to enumerate BPF_PROG_TYPE_LSM
                       programs.
    - landlock-status  /sys/kernel/security/landlock/ directory probe
                       + walks /proc/<pid>/status for the Landlock:
                       line per process.
    - filecaps-list    getcap -r / enumeration (capped at 200 entries).
    - filecaps-show    getcap <path> on a single binary.
    - filecaps-set     setcap '<caps>' <path>.
    - filecaps-remove  setcap -r <path>.
    Each new LSM is optional — the bridge auto-detects via /sys/kernel/
    security/<lsm>/; if absent, the panel renders an enable hint with
    the kernel cmdline that activates the LSM.
    SELinux remains skipped per user directive: it is native to the
    host distro and SysDeck does not manage it.
  * MOE QA PASS (MIXTURE-OF-EXPERTS). A senior QA analyst, senior
    Linux engineer, senior architect, senior admin, and project-
    manager-in-devops lens applied to the codebase. The pass replaced
    nested ifs with lookup tables (LSM_PROBES, NON_LSM_CONCERNS,
    SMACK_FILE_MAP, TOMOYO_FILES, YAMA_SCOPE_NAMES), shifted for/
    while loops toward map/filter/reduce where the data shape
    allowed it, and kept PEP 868, POSIX, SEI CERT, MISRA in mind.
    Every failure mode returns a structured JSON response with
    { available: false, reason: ..., install: ... } rather than
    crashing.
  * POLKIT POLICY EXPANSION. org.sysdeck.policy.modify now
    authorizes 30+ binaries across the policy module's surface —
    added smackload, smackcipsos, smackcipso, tomoyo-setprofile,
    tomoyo-set-profile, tomoyo-savepolicy, tomoyo-init, setcap,
    getcap (in addition to the v0.0.32 set: setfacl, getfacl,
    mkdir, mount, ip, bpftool, lsns, aa-enforce, aa-complain,
    aa-status).
  * DOCUMENTATION REWRITE. README.md, QUICKSTART.md, BLOG.md, and
    LICENSE state every design choice as a standing decision in the
    present tense; active code comments and current-version docs
    carry no development-churn narration (release history stays in
    the changelog, where it belongs).
  * STEP-DOWN LOGIC: SELinux decision documented in BLOG.md v0.0.33
    entry — three options (implement anyway / skip entirely / detect
    only) considered; option 2 won because it composes best with the
    rest of the system (Unix philosophy).
  * scripts/generate-plugins.py KEYWORDS: extended the policy entry
    with smack, tomoyo, yama, loadpin, lockdown, landlock, lsm,
    setcap, getcap, capabilities, ptrace.
  * compat/compat-manifest.json: policy entry install-hint extended
    to include libcap (Arch/Fedora) / libcap2-bin (Debian) for
    filecaps; the optional_dep_package list now mentions smack-util
    and tomoyo-tools where packaged.
  * Version bumped 0.0.32 → 0.0.33 across all 7 release-surface
    files (Makefile, __init__.py, setup.py, PKGBUILD, spec, debian
    changelog, compat-manifest.json).

 -- Jeremy Anderson <info@dcos.net>  Mon, 18 Aug 2026 18:00:00 -0500

sysdeck (0.0.32-1) unstable; urgency=medium

  * TWO NEW MODULES — POLICY & PERMISSIONS + DB CONTROL. Plugin
    count goes from 18 to 20.
  * POLICY & PERMISSIONS MODULE (NEW). User directive: "modern
    policy management and permissions manager for groups. such
    as acl, cgroups, vlans, ebpf namespace separation and related
    policies. we can skip selinux its native. we can implement
    apparmor but its not default on my machine so make it optional
    for sure." New bridge/policy.py surfaces five concerns:
      - ACLs       getfacl/setfacl (acl package on Arch/Debian)
      - cgroups    v2 unified hierarchy at /sys/fs/cgroup/ —
                   mkdir new cgroups, move PIDs, write control
                   files (memory.max, cpu.weight, io.max, ...)
      - VLANs      ip link add/del type vlan 802.1Q
      - eBPF       bpftool prog show / map show / pin to bpffs
      - namespaces lsns -J (util-linux)
    SELinux is intentionally skipped (native to host distro).
    AppArmor is OPTIONAL — the bridge auto-detects whether it is
    compiled into the kernel via /sys/kernel/security/apparmor/;
    if absent, the panel renders an install hint instead of an
    empty table. aa-status/aa-enforce/aa-complain invoked only
    when present.
  * DB CONTROL MODULE (RESTORED). User directive: "the other
    module missing is the modern database module, we already did
    it so find it in the prior sessions. if not i can find it its
    probably around version 18 if i had to guess." Confirmed via
    BLOG.md — the DB Control module was added in v0.0.15 alongside
    Prometheus and Grafana. The v0.0.20 architectural overhaul
    split SysDeck into 18 standalone plugins and the DB panel
    did not make that list of 18, even though its bridge helper
    remained in the tree. v0.0.32 ships the plugin panel.
    v0.0.32 also fixes the v0.0.15-era `sudo systemctl` shell-out
    in cmd_start / cmd_stop / cmd_restart — the cockpit way (per
    v0.0.31 pattern) is to run systemctl directly via subprocess
    and let the JS panel pass { superuser: 'try' } to cockpit.spawn
    so the cockpit bridge prompts the operator via polkit for the
    new org.sysdeck.db.modify action.
    The bridge surfaces 32+ engines across SQL/NoSQL/Vector/
    TimeSeries/Graph/Embedded/Cloud/AI families with summary /
    status / start / stop / restart / connections / query
    subcommands. The JS panel renders per-family engine tables
    with Start/Stop/Restart buttons, a SQL query runner, and a
    connections viewer.
  * NEW POLKIT ACTIONS:
      - org.sysdeck.policy.modify — authorizes setfacl, getfacl,
        mkdir, mount, ip, bpftool, lsns, aa-enforce, aa-complain,
        aa-status for the new Policy module.
      - org.sysdeck.db.modify — authorizes /usr/bin/systemctl for
        the DB Control module's start/stop/restart subcommands.
  * README.md module catalog updated: added row 19 (Policy &
    Permissions) and row 20 (DB Control); renumbered Prometheus
    to 21 and Grafana to 22. Highlight section added for v0.0.32.
  * compat/compat-manifest.json: added policy and db entries with
    per-distro install commands for acl/iproute2/bpftool/util-
    linux + optional apparmor.
  * scripts/generate-plugins.py MODULES table: added
    ("sysdeck-policy", "SysDeck Policy", 38, True, "policy") and
    ("sysdeck-db", "SysDeck Databases", 39, True, "db"). KEYWORDS
    dict expanded with policy and db keyword lists. Comment header
    updated to reflect "20 modules" (was 18).
  * Makefile: bumped install target message to "20 Cockpit
    plugins" (was 18). The existing for-loop already picks up any
    plugins/sysdeck-*/ directory so no install-target change was
    needed for the two new plugins.
  * shared/bridge.js: added bridge.policy surface (25 methods —
    summary + 4 ACL + 6 cgroup + 4 VLAN + 4 eBPF + 2 namespace +
    4 AppArmor) and bridge.db surface (7 methods — summary, status,
    start, stop, restart, connections, query). All mutating
    methods use { superuser: 'try' } — no `sudo` shell-out from JS
    anywhere. This is the cockpit way (per v0.0.31 pattern).
  * Version bumped 0.0.31 → 0.0.32 across all 7 release-surface
    files (Makefile, __init__.py, setup.py, PKGBUILD, spec, debian
    changelog, compat-manifest.json).

 -- Jeremy Anderson <info@dcos.net>  Mon, 18 Aug 2026 12:00:00 -0500

sysdeck (0.0.31-1) unstable; urgency=medium

  * FIREWALL MODULE REWRITE — PREVIOUS VERSION WAS READ-ONLY. The
    v0.0.30 firewall bridge (bridge/firewall.py) only exposed
    `ruleset` and `chains` subcommands: the panel could list active
    nftables rules but could not start, stop, restart, apply a
    template, ban an IP, unban an IP, show the ban list, or show
    service detection. The panel was a monitor, not a manager.
  * New bridge/firewall.py subcommands: templates (list installed
    templates under /usr/share/sysdeck/firewall/templates/),
    template-info <name> (show template metadata), detect (run the
    template's detect-only mode and return service inventory + OS
    info as JSON), apply <template> [policy] (invoke the template's
    `start` action under the org.sysdeck.firewall.modify polkit
    action), stop (invoke `nft delete table inet firewall` and
    disable nftables.service if present), restart, status (return
    running state + table summary + ban lists + counters),
    ban <ip> (add to ssh_abuse set), unban <ip> (delete from all
    ban sets), banned (list banned IPs across all sets),
    clear-bans (flush all ban sets), check (nft -c list ruleset).
  * New firewall/templates/ source directory: ships two operator-
    ready templates — vps-webserver.sh (service-aware firewall
    that auto-detects SSH, Caddy, Varnish, Forgejo and adjusts
    rules accordingly; aggressive SSH rate limiting when pubkey-
    only auth is detected) and no-services.sh (locked-down host
    with no public services except SSH; synproxy, scan detection,
    bogon filtering, fragment protection). Both work on Arch and
    Debian. Operators can drop additional *.sh templates into
    /usr/share/sysdeck/firewall/templates/ at any time — the
    `templates` subcommand enumerates them automatically.
  * Rewritten plugins/sysdeck-firewall/firewall.js: replaces the
    read-only ruleset table with a full manager UI — template
    selector with description and detected-service preview,
    Apply / Stop / Restart buttons, service detection summary,
    live ban-list table with per-IP Unban buttons, ban-list
    Clear All button, and the existing ruleset table now sits
    below as a live state view (refreshed after each operation).
    All mutating operations go through bridge.firewall.apply /
    stop / restart / ban / unban / clearBans — which use the
    cockpit superuser channel (polkit) via { superuser: 'try' }
    in bridge.js. This is the "cockpit way": privileged ops
    trigger the cockpit auth dialog and run as root via the
    bridge's polkit action; no shell-out to sudo from JS.
  * Updated shared/bridge.js firewall surface: new methods
    templates(), templateInfo(name), detect(), apply(template, policy),
    stop(), restart(), status(), ban(ip), unban(ip), banned(),
    clearBans(), check(). Existing listChains()/listRules() preserved.
  * PACKAGES MODULE — UPDATE NEEDS SUDO, FIXED THE COCKPIT WAY.
    v0.0.30 packages.js `Update All` button called
    bridge.packages.updateAll() — but the bridge helper returned
    the command string that *would* be run, NOT the result. The
    panel showed `alert("Run this command with superuser
    privileges.")` — meaning the operator had to copy the
    command, open a terminal, sudo, paste, run. That defeats the
    purpose of having a panel. v0.0.31 makes install/remove/
    update/update-all actually execute via the cockpit
    superuser channel (polkit). The bridge helper now runs the
    detected package manager (pacman/dnf/apt) via subprocess
    with check=True, and the JS panel subscribes to the cockpit
    spawn stream so the operator sees live stdout/stderr in a
    <pre> log panel — exactly like cockpit's own Packages and
    Software Updates panels. No `sudo` shell-out from JS; the
    polkit action org.sysdeck.packages.modify (already shipped
    since v0.0.17) authorizes /usr/bin/pacman, /usr/bin/apt,
    /usr/bin/dnf. The "command would be run" return shape is
    preserved as the `dryRun()` method for operators who want
    the preview before applying.
  * IMAGE BUILDER MODULE — EXPANDED TO FULL-FEATURED. v0.0.30
    builder was a status+profile viewer: it could list installed
    backends (mkosi/vmdb2/archiso/live-build) and walk their
    config dirs, but could not actually build anything, could
    not create/edit profiles, could not show build artifacts or
    logs. v0.0.31 adds: build(profile, backend, options) — runs
    the backend in the profile's directory via subprocess under
    the org.sysdeck.builder.modify polkit action; profile-create
    (name, backend, base) — scaffold a new mkosi.conf or vmdb2
    YAML in /etc/mkosi/ or /etc/vmdb2/; profile-delete(name);
    artifacts(profile?) — list image/ISO files produced by past
    builds under /var/lib/sysdeck/builder/artifacts/<profile>/;
    build-status() — list active and recently-finished builds
    (driven by a small JSON state file under
    /var/lib/sysdeck/builder/state/); build-log(id) — tail the
    build's stdout/stderr log file. The panel renders a Build
    button per profile, a Builds table (state, started, finished,
    duration, artifacts), a per-build log viewer, and a Create
    Profile form.
  * FESTER RENAME — BUILD ORCH PANEL IS NOW "SYSDECK FESTER".
    The directory was already plugins/sysdeck-fester/ but the
    menu label and panel title said "SysDeck Build Orchestration".
    User directive: "the build orch should be renamed SysDeck
    Fester". Updated: manifest.json menu.label, panel <h2>
    title in fester.js, scripts/generate-plugins.py MODULES
    table, README.md module catalog row 9, compat-manifest.json
    fester entry note, BLOG.md references.
  * POLKIT POLICY: added org.sysdeck.fester.modify action
    covering /usr/bin/systemctl and /usr/bin/journalctl — the
    fester bridge will use it when it grows real DAG-orchestration
    in a future release. Existing org.sysdeck.firewall.modify
    and org.sysdeck.packages.modify actions already authorize
    the binaries the new subcommands invoke (nft, pacman, apt,
    dnf); no new polkit actions needed for v0.0.31 firewall /
    packages work.
  * MAKEFILE: new FIREWALL_TEMPLATES_DIR variable; install
    target now copies firewall/templates/*.sh to
    /usr/share/sysdeck/firewall/templates/ with mode 0755 (they
    are invoked by the bridge via `bash <template>.sh ...` under
    the org.sysdeck.firewall.modify polkit action). dist target
    now includes the firewall/ source tree.
  * PKGBUILD: pkgver bumped to 0.0.31. optdepends unchanged
    (nftables and the rest were already listed).
  * Debian control / RPM spec: Version bumped to 0.0.31.
  * bridge/__init__.py: __version__ = "0.0.31".
  * compat/compat-manifest.json: version 0.0.31.

 -- Jeremy Anderson <info@dcos.net>  Mon, 18 Aug 2026 00:00:00 -0500

sysdeck (0.0.30-1) unstable; urgency=medium

  * BUILDER MODULE REWRITE — TARGET DISTROS NOW ARCH + DEBIAN. The
    previous builder bridge helper (bridge/builder.py) was a thin
    `systemctl is-active osbuild-composer.service` shim. osbuild-
    composer is Fedora/RHEL-only and is not packaged for Arch or
    Debian, so the Builder panel was permanently 'inactive' on every
    distro this suite actually ships to.
  * Rewritten bridge/builder.py now detects and surfaces the
    canonical image-builder backends for the target distros:
      Arch Linux  -> mkosi (primary, systemd image builder) + archiso
      Debian      -> vmdb2 (primary, Debian image builder) + live-build
    Detection uses shutil.which() and works on any distro — a Debian
    host with mkosi installed is still surfaced correctly.
  * New Python subcommands: status, profiles, summary, backends,
    install-hint. `summary` returns combined status + profiles in one
    call so the panel renders from a single bridge spawn.
  * Profile discovery walks well-known config dirs per backend:
      mkosi       /etc/mkosi/mkosi.conf[.d/*.conf] + mkosi.profiles/*.profile
      archiso     /usr/share/archiso/configs/* + /etc/archiso/configs/*
      vmdb2       /etc/vmdb2/*.yaml + /usr/share/vmdb2/specs/*.yaml
      live-build  any dir under /etc|/usr/share|~/.config/live-build
                  containing a `config/` subdir (marker that `lb config`
                  was run there).
  * Rewritten plugins/sysdeck-builder/builder.js: replaces the
    composer-cli blueprints list call with bridge.builder.summary().
    Renders per-backend profile cards (grouped by backend) and shows
    a distro-specific install hint when no backend is installed.
  * Updated shared/bridge.js builder surface — exposes
    summary/profiles/status/backends/installHint.
  * Updated packaging/polkit/org.sysdeck.policy: org.sysdeck.builder.modify
    action now authorizes /usr/bin/mkosi, /usr/bin/mkarchiso,
    /usr/bin/vmdb2, /usr/bin/lb. osbuild + livemedia-creator
    annotations removed (Fedora-only, no longer targeted).
  * Updated packaging/PKGBUILD optdepends: added mkosi (Arch primary
    image builder) and archiso (Arch Live ISO builder). pkgver
    bumped to 0.0.30.
  * Updated packaging/debian/control Suggests: added mkosi, vmdb2,
    archiso, live-build.
  * Updated compat/compat-manifest.json builder entry: Arch and
    Debian distro_support upgraded from 'none' to 'full'; Fedora
    entry repointed from osbuild-composer to mkosi (cross-distro).
    Tested cockpit versions expanded to [239, 264, 285].
  * Updated plugins/sysdeck-builder/manifest.json keywords and
    scripts/generate-plugins.py: replaced 'osbuild' keyword with
    'mkosi', 'vmdb2', 'archiso', 'live-build'.
  * Updated docs (README.md, QUICKSTART.md, BLOG.md, QA.md,
    docs/INSTALL.md, THIRD_PARTY.md) to reflect mkosi/vmdb2/archiso/
    live-build backends instead of composer-cli / osbuild-composer.

 -- Jeremy Anderson <info@dcos.net>  Sun, 17 Aug 2026 00:00:00 -0400

sysdeck (0.0.29-1) unstable; urgency=high

  * EMAIL MIGRATION: author/maintainer contact address changed from
    jeremy@dcos.net to info@dcos.net across every release surface —
    debian/changelog (all 17 historical entries), debian/control
    Maintainer, packaging/setup.py author_email, PKGBUILD Maintainer +
    Contributor, sysdeck.spec changelog (all 19 historical entries).
    Rationale: project has moved to a shared info@ inbox; no per-
    developer addresses on public packaging. Author name "Jeremy
    Anderson" is preserved everywhere it appears; only the email
    address is replaced.
  * REMOVED DUPLICATE CONTAINER ENTRY: deleted
    standalone-plugins/cockpit-podman/ (manifest.json). Podman ships
    its own native Cockpit module (the `cockpit-podman` package on
    Fedora / Debian / Arch), so bundling a second cockpit-podman
    manifest here was duplicating upstream — installing both would
    produce two competing sidebar entries pointing at the same backend.
    Replaced the slot with a manifest for Incus instead — the original
    intent for that third standalone plugin slot, which had been
    mis-assigned to podman.
  * NEW standalone-plugins/cockpit-incus/manifest.json: sidebar link
    (order 46, gated on /usr/bin/incus) for Incus system container
    and VM management. Incus is the LXC/LXD successor maintained by
    the Linux Containers project. Keywords: incus, lxc, lxd, containers,
    vms, virtualization, system containers, images. Docs URL points
    to linuxcontainers.org/incus/docs/.
  * UPDATED compat/compat-manifest.json: standalone_plugins.cockpit-podman
    entry replaced with standalone_plugins.cockpit-incus. Per-distro
    install commands: pacman -S incus (Arch), dnf install incus
    (Fedora 40+), apt install incus (Debian 13 trixie / bookworm
    backports). distro_support: full on all three target distros.
  * UPDATED sysdeck-diagnose.sh section 13 reference loop: was
    iterating over cockpit-podman / cockpit-machines / cockpit-ostree;
    now iterates over cockpit-incus / cockpit-machines / cockpit-ostree.
    Section 14 comparison text updated to refer to "the reference
    plugins above" instead of "the cockpit-podman reference" (the
    reference set now has 3 plugins, not just 1).
  * UPDATED README.md v0.0.15 highlights section: the three
    standalone plugin sidebar links now list cockpit-incus (order 46,
    Incus system container and VM management) instead of cockpit-podman.
  * UPDATED QA.md v0.0.15 QA section 2: standalone plugin table
    row 3 changed from cockpit-podman (Podman Containers) to
    cockpit-incus (Incus Containers). Conformance verdict preserved.

 -- Jeremy Anderson <info@dcos.net>  Sun, 17 Aug 2026 15:00:00 -0500

sysdeck (0.0.28-1) unstable; urgency=high

  * FIXED 2 BROKEN BRIDGE SUBCOMMANDS that slipped through v0.0.27's
    "subcommand alignment" pass:
    1. firmware.py: bridge.js called `python3 firmware.py devices` but
       the helper only implemented `summary`. Every visit to the Firmware
       plugin page crashed with "Unknown subcommand: devices". Added a
       real `devices` subcommand returning fwupdmgr's native
       {Devices: [...]} shape (capital D, matches the panel's expected
       access pattern).
    2. benchmark.py: bridge.js called `python3 benchmark.py run-test
       <name>` when the user clicked "Run" in the Available Tests table,
       but the helper had no `run-test` subcommand. Added `run-test`
       that runs `sysbench <name> run` and returns the parsed result
       dict — same shape as run-cpu/run-memory/run-io.
  * NEW BUILD-TIME GUARD: `check-bridge-subcommands` in `make check`.
    Cross-checks every bridgeCmd("<module>", ["<sub>", ...]) call in
    shared/bridge.js against the COMMANDS dict declared in each
    bridge/<module>.py. Would have caught both bugs above.
    Negative-tested: a summary-only firmware.py (the v0.0.27 shape)
    fails the guard with a clear message naming the file, line, and
    missing subcommand.
  * FIXED MISSING CSS CLASSES: shared/sysdeck.css was missing
    .suite-progress, .suite-progress-bar, .suite-progress-fill,
    .suite-stat-value, .suite-stat-label, .suite-row, .suite-row-between,
    .suite-grid, .cols-2, .cols-3, .suite-col-2, .suite-col-3,
    .suite-btn-primary, .suite-badge.info, .suite-input, .suite-warn.
    Without them: progress bars in glances/fleet/netsec were invisible
    (0-height divs); multi-column layouts in fleet/integrity/mining/
    packages collapsed to a single column; primary CTA buttons in
    benchmark/integrity/packages looked like ghost buttons. All added.
  * FIXED COCKPIT-SMOKE-TEST.SH: the embedded manifest used
    "requires": { "cockpit": ">=239" } — the broken pattern Cockpit
    silently rejects at the discovery layer (sortify_version() turns
    ">=" into a string that sorts greater than any real cockpit
    version). Smoke test would produce a false "Cockpit is broken"
    diagnostic. Fixed to "cockpit": "239" (bare number).
  * IMPROVED DIAGNOSTIC: sysdeck-diagnose.sh now verifies that
    /usr/lib/sysdeck/bridge/*.py exists and is executable, and spot-
    checks that firmware.py `devices` and benchmark.py `run-test`
    subcommands work end-to-end. Previously the diagnose script could
    not detect a missing Python helper — every plugin would silently
    fail with "No such file or directory".
  * FIXED DOCS: bridge/__init__.py docstring still showed the broken
    `python3 -m sysdeck.bridge.<module>` invocation pattern that was
    fixed in v0.0.26. Updated to
    `python3 /usr/lib/sysdeck/bridge/<module>.py` with a note
    explaining why the -m pattern was broken.

 -- Jeremy Anderson <info@dcos.net>  Sun, 17 Aug 2026 14:00:00 -0500

sysdeck (0.0.27-1) unstable; urgency=high

  * FIXED 6 MISSING PYTHON HELPERS: mining.py, builder.py, fester.py,
    kata.py, vault.py, mesh.py were never written — every plugin that
    called one got "Module load failed: can't open file". Wrote minimal
    stubs that return empty data so modules render with "no items".
  * FIXED 4 SUBCOMMAND MISMATCHES: bridge.js called subcommands the Python
    helpers didn't have. Now aligned: auth.smartcards→slots,
    netsec.listeningPorts→sockets, integrity.trustScore→score,
    integrity.runLynis→scan, firewall.listChains→chains,
    firewall.listRules→ruleset, fleet.uptime/nodeCount derive from summary,
    firmware.tpmInfo reads PCR0 directly.
  * NOT A BUG: glances module shows "Glances unavailable" when glances
    binary not installed — that's the module's own graceful degradation.

 -- Jeremy Anderson <info@dcos.net>  Sun, 17 Aug 2026 13:00:00 -0500

sysdeck (0.0.26-1) unstable; urgency=high

  * ROOT CAUSE FOUND AND FIXED: every plugin that called a bridge helper
    failed with ModuleNotFoundError: No module named 'sysdeck.bridge'.
    Cause: shared/bridge.js called `python3 -m sysdeck.bridge.<module>`,
    which requires a Python package layout (sysdeck/bridge/<module>.py)
    that doesn't exist in the install. The actual layout is
    /usr/lib/sysdeck/bridge/<module>.py (flat files, not a nested
    package). So `python3 -m sysdeck.glances` would have worked, but
    `python3 -m sysdeck.bridge.glances` never could.
    Fix: changed bridgeCmd() to call helpers by absolute path:
      `python3 /usr/lib/sysdeck/bridge/<module>.py <args>`
    No package layout, no PYTHONPATH, no symlink needed.
    Also: Makefile now installs each helper as executable (0755, not 0644).
    Removed the broken site-packages symlink.
    Note: only firmware/fleet/themes 'worked' in v0.0.25 because they use
    Promise.allSettled() (catches rejections silently) or cockpit.file()
    (no Python helper needed) — they were showing 'unavailable' cards.
  * New guard: check-no-broken-python-module in `make check` scans every
    JS file for spawn calls using `python3 -m sysdeck.bridge` and fails.

 -- Jeremy Anderson <info@dcos.net>  Sun, 17 Aug 2026 12:00:00 -0500

sysdeck (0.0.25-1) unstable; urgency=high

  * ROOT CAUSE FOUND AND FIXED: every plugin page showed "Module load
    failed: error loading dynamically imported module:
    http://127.0.0.1:9090/cockpit/@localhost/sysdeck-common/bridge.js".
    Cause: shared/sysdeck-common/ had bridge.js and sysdeck.css but NO
    manifest.json. Cockpit only registers a directory as a package if it
    contains manifest.json (packages.py:457 scans cockpit/*/manifest.json).
    Without registration, every URL like
    /cockpit/@localhost/sysdeck-common/bridge.js returned 404, and the
    dynamic import("../sysdeck-common/bridge.js") failed.
    Fix: added shared/manifest.json with name="sysdeck-common". Pattern
    verified from cockpit's own pkg/static/manifest.json (just `{}`).
    Updated Makefile install to install shared/manifest.json alongside
    bridge.js and sysdeck.css.
    Updated tests/check_manifest_consistency.py to verify shared/manifest.json
    exists — regression-tested by deleting it and confirming `make check`
    fails with a clear message naming packages.py:457.

 -- Jeremy Anderson <info@dcos.net>  Sun, 17 Aug 2026 11:00:00 -0500

sysdeck (0.0.24-1) unstable; urgency=high

  * THOROUGH UNINSTALLER: previous `make uninstall` only removed
    /usr/share/cockpit/sysdeck-* (with dash). v0.0.9-v0.0.19 installed
    to /usr/share/cockpit/sysdeck/ (no dash) — that directory was NEVER
    removed by uninstall. Cockpit would discover both old and new
    manifests, possibly serving old broken code instead of the new
    freshly-installed code. v0.0.24 uninstall now removes:
      * /usr/share/cockpit/sysdeck/        (v0.0.9-v0.0.19 single-plugin)
      * /usr/share/cockpit/sysdeck-*/      (v0.0.20+ multi-plugin)
      * /usr/lib/sysdeck/                  (Python bridge, all versions)
      * /usr/share/sysdeck/                (diagnostic scripts, v0.0.19+)
      * /usr/share/doc/sysdeck/            (docs, v0.0.20+)
      * /usr/share/metainfo/sysdeck.metainfo.xml        (v0.0.17+)
      * /usr/share/polkit-1/actions/org.sysdeck.policy (v0.0.17+)
      * python site-packages sysdeck symlink            (all versions)
      * pacman-tracked sysdeck package                   (if installed via PKGBUILD)
  * VISIBLE ERROR REPORTING: every plugin's index.html now installs
    window.addEventListener('error') and 'unhandledrejection' handlers
    that replace the 'Loading…' placeholder with the actual error message
    on the page. No devtools required — the user sees the error directly.
  * COCKPIT.JS PRESENCE CHECK: every plugin's index.html now checks
    `if (!window.cockpit)` before importing bridge.js, and shows a clear
    error if cockpit.js failed to load (e.g., 404 on ../base1/cockpit.js).
  * TRY/CATCH AROUND MOUNT(): if mount() throws synchronously (e.g., bridge
    is undefined, panel is null), the error is now displayed on the page
    instead of leaving it stuck on 'Loading…'.
  * DIAGNOSTIC SCRIPT: sysdeck-diagnose.sh now detects leftover
    /usr/share/cockpit/sysdeck/ directory from old installs, and
    spot-checks that the installed bridge.js contains the v0.0.23+
    `const cockpit = window.cockpit` fix.

 -- Jeremy Anderson <info@dcos.net>  Sun, 17 Aug 2026 10:00:00 -0500

sysdeck (0.0.23-1) unstable; urgency=high

  * ROOT CAUSE FOUND AND FIXED: every plugin page was stuck on "Loading…"
    because shared/bridge.js did `import cockpit from "../base1/cockpit.js"`
    — an ES module import. But pkg/base1/cockpit.js is NOT an ES module:
    it's a UMD/IIFE that sets window.cockpit as a global (verified from
    cockpit source code: pkg/base1/cockpit.js has no `export` statements;
    cockpit's own plugins load it via <script src> in their HTML, then
    access the global `cockpit`). The import returned undefined, so
    cockpit.spawn() threw when mount() ran, and the plugin page never
    rendered. Fix: replaced `import cockpit from "../base1/cockpit.js"`
    with `const cockpit = window.cockpit` — exactly how cockpit's own
    esbuild plugin (build.js:71-83) accesses it after rewriting
    `import cockpit from "cockpit"` to `module.exports = cockpit`.
  * New guard: check-no-broken-cockpit-import in `make check` scans every
    JS file in plugins/ and shared/ for the broken `import cockpit from`
    pattern. Negative-tested: the v0.0.22 import form makes `make
    check` fail with a clear message.

 -- Jeremy Anderson <info@dcos.net>  Sun, 17 Aug 2026 09:00:00 -0500

sysdeck (0.0.22-1) unstable; urgency=high

  * ROOT CAUSE FOUND AND FIXED: the `requires.cockpit` field was set to
    ">=239" in every manifest since v0.0.9. Cockpit's packages.py uses
    sortify_version() (a 0-pad of numeric components) to compare versions,
    NOT a semver parser. ">=239" becomes ">=00000239" which is GREATER than
    any real cockpit version (because '>' is ASCII 62 > '0' ASCII 48),
    causing packages.py:263 to raise JsonError and silently reject every
    manifest at install time. The plugins never reached the shell's menu
    builder — that's why the sidebar was empty.
    Fix: changed `requires.cockpit` from ">=239" to "239" (bare number) in
    all 18 manifests. This matches the pattern in pkg/systemd/manifest.json
    ("cockpit": "265"), pkg/storaged/manifest.json ("cockpit": "266"), etc.
    None of cockpit's own plugins use the ">=" prefix.
  * Renamed all 18 sidebar labels from "SD <Name>" to "SysDeck <Name>"
    per user requirement: "SysDeck should be never abbreviated. So SD is
    not ok."

 -- Jeremy Anderson <info@dcos.net>  Sun, 17 Aug 2026 08:00:00 -0500

sysdeck (0.0.21-1) unstable; urgency=high

  * Fix AppStream metainfo: replace <provides><cockpit-manifest> (silently
    ignored by cockpit's apps page) with <launchable type="cockpit-manifest">
    for each of the 18 plugins. Pattern verified from cockpit source code:
    src/appstream/org.cockpit_project.cockpit_*.xml.in all use <launchable>,
    and pkg/apps/watch-appstream.py:237 reads it.
  * Rewrite tests/check_manifest_consistency.py to validate against the REAL
    Cockpit manifest contract from pkg/shell/manifests.ts and
    src/cockpit/packages.py — not the invented contract used in v0.0.19-v0.0.20.
    The previous test rejected manifests for having `version`, `title`,
    `priority`, `content` sections — none of which Cockpit actually rejects.
    They are silently ignored by the shell.
  * Rewrite tests/check_metainfo_consistency.py to validate <launchable>
    elements (the real pattern) instead of <provides><cockpit-manifest>.

 -- Jeremy Anderson <info@dcos.net>  Sun, 17 Aug 2026 07:00:00 -0500

sysdeck (0.0.20-1) unstable; urgency=high

  * ARCHITECTURAL OVERHAUL: split the single SysDeck shell into 18
    standalone Cockpit plugins. Each module (containers, firewall,
    integrity, etc.) is now its own plugin at /usr/share/cockpit/sysdeck-<name>/
    with its own manifest.json + index.html + <module>.js. Each appears
    as its own sidebar entry in Cockpit, just like cockpit-podman,
    cockpit-machines, cockpit-ostree.
  * DELETED the v0.0.19 shell entirely: manifest.json, index.html,
    suite.js, suite.css, src/bridge-client.js, src/event-bus.js,
    src/mock-cockpit.js, src/cockpit-types.d.ts, src/modules/, and the
    entire nextjs-dashboard/ tree. Cockpit is the dashboard framework;
    we are no longer rebuilding one.
  * NEW shared/bridge.js at /usr/share/cockpit/sysdeck-common/bridge.js:
    provides the `bridge` and `EventBus` objects each plugin imports.
    Calls cockpit.spawn() directly to invoke the Python bridge helpers.
    Live-update subscriptions are no-ops in v0.0.20 — each plugin has
    a manual Refresh button instead.
  * NEW shared/sysdeck.css at /usr/share/cockpit/sysdeck-common/sysdeck.css:
    base styles for every plugin.
  * NEW scripts/generate-plugins.py: regenerates plugins/ and shared/.
    Run `make plugins` to regenerate.
  * Updated tests/check_manifest_consistency.py: validates ALL 18 plugin
    manifests against the cockpit-podman reference pattern.
  * DROPPED: nextjs-dashboard/, src/cockpit-types.d.ts, src/mock-cockpit.js.

 -- Jeremy Anderson <info@dcos.net>  Sun, 17 Aug 2026 06:00:00 -0500

sysdeck (0.0.19-1) unstable; urgency=high

  * CRITICAL FIX: rewrote manifest.json to match the working
    cockpit-podman pattern exactly. Previous releases (v0.0.9 through
    v0.0.18) used a manifest schema with 'version: 1', a 'content'
    section, and a 'menu.suite' entry with an explicit 'path' field —
    none of which appear in any real, working Cockpit plugin's manifest.
    Cockpit silently rejected the plugin on every install, leaving
    "zero entries anywhere" in the sidebar and Applications menu.
  * New manifest uses: version=0 (universally supported), menu.index
    (the magic key Cockpit uses to serve index.html implicitly), no
    'content' section, no 'path' on menu entries, no top-level 'title'
    or 'priority'. Matches cockpit-podman, cockpit-machines,
    cockpit-ostree — the three reference plugins shipped in this very
    tarball under standalone-plugins/.
  * Hardened guard: tests/check_manifest_consistency.py no longer
    validates against a made-up contract. It validates against the
    actual cockpit-podman reference manifest — fails if sysdeck's
    manifest has any field not present in cockpit-podman's manifest.
  * Added sysdeck-diagnose.sh: prints exactly what Cockpit sees on
    the target system. Installed to /usr/share/sysdeck/sysdeck-diagnose.sh.
  * Added cockpit-smoke-test.sh: installs a 5-line hello-world plugin
    to verify Cockpit's plugin discovery works INDEPENDENTLY of
    sysdeck. If 'Hello Test' also fails to appear in the sidebar, the
    issue is Cockpit itself, not sysdeck.

 -- Jeremy Anderson <info@dcos.net>  Sun, 17 Aug 2026 05:00:00 -0500

sysdeck (0.0.18-1) unstable; urgency=high

  * CRITICAL FIX: v0.0.16's manifest "fix" was based on a wrong model of
    how Cockpit serves plugin files. v0.0.16 aligned content.suite.path
    and menu.suite.path to both be "/suite", which passed the manifest-
    consistency test (paths matched each other) but the path did not
    resolve to an actual file in the plugin directory. Cockpit served a
    404 / empty page when the user clicked the SysDeck sidebar entry,
    because no `suite.html` file existed at
    /usr/share/cockpit/sysdeck/suite.html.
  * Fix: changed both content.suite.path and menu.suite.path to
    "/index.html". Cockpit now serves the real index.html file at the
    /index.html URL and the dashboard loads when the menu entry is
    clicked.
  * Hardened guard: tests/check_manifest_consistency.py now also
    verifies that every content/menu path resolves to a real file in the
    plugin directory, mirroring Cockpit's URL-to-file mapping (e.g.
    /suite -> ./suite.html or ./suite/index.html; /index.html ->
    ./index.html). This catches the v0.0.16 silent-empty-page bug at
    build time.
  * Corrected misleading path semantics documented in BLOG.md and QA.md
    for v0.0.16. Cockpit does NOT auto-serve index.html at arbitrary
    URL paths; the URL path declared in content/menu must map to a real
    file.

 -- Jeremy Anderson <info@dcos.net>  Sun, 17 Aug 2026 04:00:00 -0500

sysdeck (0.0.17-1) unstable; urgency=high

  * Register as a first-class Cockpit application via AppStream metainfo
    at /usr/share/metainfo/sysdeck.metainfo.xml. The metainfo declares
    <provides><cockpit-manifest>sysdeck</cockpit-manifest></provides>,
    which is how Cockpit's Applications install menu discovers the plugin
    and grants it proper auth context via cockpit-ws.
  * Ship PolKit policy at /usr/share/polkit-1/actions/org.sysdeck.policy
    covering six privilege domains: system.manage, firewall.modify,
    packages.modify, firmware.modify, vault.modify, builder.modify.
    Without these, privileged bridge operations fail with permission
    denied errors.
  * Makefile install target installs both files, reloads polkit, and
    refreshes the AppStream cache on install.
  * New guard: check-metainfo-consistency validates the metainfo XML
    structure at build time.

 -- Jeremy Anderson <info@dcos.net>  Sun, 17 Aug 2026 03:00:00 -0500

sysdeck (0.0.16-1) unstable; urgency=high

  * CRITICAL FIX: manifest.json content/menu path mismatch caused
    Cockpit to silently drop SysDeck from the sidebar menu after
    install. content.suite.path was "/index.html", menu.suite.path
    was "/suite" — they must match. Fixed content path to "/suite".
  * New guard: check-manifest-consistency in make check validates
    manifest structure (menu paths must match content paths).

 -- Jeremy Anderson <info@dcos.net>  Sun, 17 Aug 2026 02:00:00 -0500

sysdeck (0.0.15-1) unstable; urgency=low

  * SOURCE COMPLETENESS: bridge/grafana.py, bridge/hwalert.py, and
    bridge/prometheus.py ship in the tarball (the v0.0.13 tarball
    omitted them).
  * SOURCE COMPLETENESS: nextjs-dashboard/ and prometheus/
    directories ship in the source tree, tarball, and install
    target.
  * Makefile install target now installs prometheus configs to
    /etc/sysdeck/prometheus/ and Next.js dashboard to
    /usr/share/sysdeck/nextjs-dashboard/.

 -- Jeremy Anderson <info@dcos.net>  Sun, 17 Aug 2026 01:00:00 -0500

sysdeck (0.0.14-1) unstable; urgency=low

  * Fix Makefile dist target: tarball now extracts into sysdeck-<version>/
    subdirectory. The previous --transform regex silently no-op'd because
    tar with explicit file arguments does not prepend ./ to archive paths.
    This blocked RPM %setup -q, Arch PKGBUILD cd "$srcdir/$pkgname-$pkgver",
    and Debian dh_auto_configure, all of which expect a wrapping directory.
  * Add `make distcheck` regression guard: extracts the tarball into a
    clean /tmp dir, verifies the wrapping subdirectory exists, and runs
    `make check` inside the extracted tree.

 -- Jeremy Anderson <info@dcos.net>  Sun, 17 Aug 2026 00:00:00 -0500

sysdeck (0.0.13-1) unstable; urgency=low

  * Packages module (pacman/dnf/apt wrapper) for package listing,
    search, install, update, and remove operations via cockpit.spawn.
  * Auth identities extension: enumerates PKCS#11 tokens, SSH keys,
    and Kerberos principals as first-class identity objects.
  * Full Arch Linux (PKGBUILD) and Debian (.deb) packaging support.

 -- Jeremy Anderson <info@dcos.net>  Sun, 17 Aug 2026 00:00:00 -0400

sysdeck (0.0.12-1) unstable; urgency=low

  * External module integrations: glances, sensors, benchmark.
  * Bridge channel integration with cockpit.metrics tap, dbus proxies.

 -- Jeremy Anderson <info@dcos.net>  Sat, 16 Aug 2026 00:00:00 -0400

sysdeck (0.0.11-1) unstable; urgency=low

  * Initial cockpit-native release: drop-in plugin with manifest.json,
    vanilla-JS dashboard, Python bridge helpers, and full packaging.

 -- Jeremy Anderson <info@dcos.net>  Sat, 16 Aug 2026 00:00:00 -0400
