Skip to content

Releases: tyxak/remotepower

v4.0.0 — scale out, encrypt everything, see more

07 Jun 20:47
v4.0.0

Choose a tag to compare

The 4.0 release — scale out without changing how it feels.

Highlights

  • Scale out — PostgreSQL, HA & satellites. Optional PostgreSQL backend with automatic failover + read replicas, a PgBouncer pooler, load-balanced app nodes behind a trusted proxy, and relay satellites for segmented networks. Flat-JSON stays the zero-dependency default; SQLite is the single-node step up.
  • Encrypt every hop, including the agent→satellite relay; session tokens are now hashed at rest.
  • macOS agent joins Linux + Windows, with install scripts for satellites, PostgreSQL (incl. HA primary/standby) and PgBouncer, plus a deployment map.
  • Deeper telemetry — fleet thermal roll-up, UPS/GPU power + energy chargeback, predictive disk-failure ETAs, an SSH-key audit, and CVE prioritization by CISA KEV + EPSS.
  • Real themes (13 full palettes) and a 5-language UI (English, 中文, हिन्दी, Español, العربية — RTL).
  • Security hardening pass — anchored webhook-host matching, SSRF-safe cloud import, scrubbed OIDC error logs, token hashing at rest. Independently scanned with Bandit, ZAP, Nikto, Nuclei and Wapiti with no exploitable findings.

No breaking changes for a single-node flat-JSON install. Full notes: CHANGELOG.md and docs/v4.0.0.md.

Verify the download

sha256sum -c remotepower-4.0.0.tar.gz.sha256
# expected: 4a580aadd657a72622ee8740af51d68fc75afff04b988e7a9d4327d01b84089a

The v4.0.0 git tag is GPG-signed.

v3.13.0 — bind it together (round four)

05 Jun 17:43
v3.13.0
9f86889

Choose a tag to compare

A bind-it-together sweep: surface the host signals the agent already collects but the UI never showed, cap every panel so it scrolls instead of growing unbounded, and a round of performance and security hardening. No breaking changes, no new dependencies.

Highlights

  • Newly surfaced in the device drawer: recent logins & source IPs, a failed-first systemd-timer inventory, this host's own ZFS/mdadm/btrfs storage & RAID health, listening-port bind address + world/LAN/local scope, the firewall ruleset fingerprint, a brute-force lockout badge, Disk/Swap pressure pills, and a per-container Restarts column.
  • Every box fits: drawer cards and large tables cap at ~15 rows and scroll internally.
  • Performance: version-busted static assets cached immutably, deferred scripts, file-cached fleet-risk.
  • Security: sandboxed SCAP reports, OIDC id_token expiry/issuer/audience checks, syslog-forward DNS-rebinding fix, and an SSRF classifier hardened against IPv6-embedded-IPv4 (NAT64/6to4/v4-mapped) metadata smuggling. Externally scanned (Bandit, OWASP ZAP, Nikto, Nuclei, Wapiti) with no exploitable findings — see docs/security-review-3.13.0.md.
  • Storage: optional embedded SQLite backend with a live, in-place, reversible migration (Settings → Advanced).

Full notes: CHANGELOG.md.

Verify

sha256sum -c remotepower-3.13.0.tar.gz.sha256
# expected: ea9c57abe458f9a5c9cddabcaf1ab00f7566977297b433423148541cbace05cc

v3.10.0 — bind round three, registry-SSRF & config-leak fixes

03 Jun 09:34
v3.10.0

Choose a tag to compare

A third bind-it-together and security sweep on top of v3.9.0 — agent data that was collected but stuck at zero now flows through, two real SSRF / secret-disclosure gaps are closed, and a couple of alert-label bugs are fixed. No new headline features. Hard-reload the web UI once after upgrading (cache remotepower-shell-v3.10.0).

Security

  • Container image-registry SSRF closed. The image-update scanner was the one outbound path not behind the connect-time SSRF guard: it followed redirects, re-resolved DNS between the pre-flight and the fetch, and fetched the registry-controlled bearer-token realm URL with no check at all — which could exfiltrate configured registry credentials. Every fetch (manifest and token realm) now routes through the SSRF-safe, no-redirect opener, and the realm is pre-flighted and forced to HTTPS.
  • GET /api/config secret-scrub backstop. The endpoint redacted secrets by name, so the AI-provider api_key and the per-registry credentials map leaked to viewer / read-only MCP keys. A recursive pass now strips any secret-named field at any depth while keeping every *_set / *_from_env indicator.
  • TCP uptime monitor + Healthchecks.io ping picked up the same IP-class SSRF checks (and connect-time peer recheck) the HTTP paths already had.

See docs/security-review-3.10.0.md.

Bind it together

  • Container restart tracking now works fleet-wide. Docker/Podman containers reported restart count and start time as zero, so the container_restarting alert only fired for Kubernetes pods and the drawer's container age was blank. The agent now fills them from a single batched docker inspect per heartbeat.
  • ClamAV last-scan time and per-interface MAC addresses now show in the device drawer.

Fixes

  • Config-drift alert titles named no file (every one read "? file(s)") — they now name the changed file or the number of drifting sections.
  • The Devices table-view Hostname column showed a sort arrow but never reordered — fixed.

Verify the download

sha256  0728d84f656d2d923edc9b0e0d01759ed9b20c0d5707b11b3ef713c6b40a9f44

Full notes: docs/v3.10.0.md · Changelog: CHANGELOG.md

v3.4.0 — fleet health & insights + RAG

30 May 23:10
v3.4.0

Choose a tag to compare

RemotePower v3.4.0 — an additive release (no breaking changes, no schema changes). Hard-reload once after upgrading to pick up the new service-worker cache.

Highlights

  • Hardware & health — per-disk SMART (failing / pre-fail alerts), kernel-vs-newest + livepatch status, and a passive hardware inventory, in the device drawer's Health & Hardware card.
  • Resource forecasting — projects per-mount disk-fill ("/ fills in ~18 days") from a daily metrics snapshot, with a dedicated Monitoring → Forecast page (fleet table + a scatter/regression-line chart per mount), plus "what changed" diffs over the last day/week.
  • Compliance reports — PCI DSS / HIPAA / SOC 2 controls scored pass/fail/N-A from data RemotePower already collects (an audit-prep aid, never a formal attestation).
  • RAG over your infrastructure — the AI assistant retrieves and cites facts from your own fleet, docs, CVEs and runbooks; lexical BM25 works with every provider, semantic search optional. The credentials vault is never indexed.
  • On-demand AI insights — fleet anomaly scan, plain-English cron builder, RAG-aware runbook suggestions, and CMDB doc drafts. All opt-in.
  • On-demand diagnostics & quarantine — one-click network speed test and LAN discovery; a per-device quarantine switch that blocks all actions server-side.
  • Helm release status where Helm + a kubeconfig are present.
  • Proxmox — create and delete LXC containers from a guided, validated, audited wizard (in addition to start/stop + snapshots).
  • Security hardening — the user-management CLI and installer use salted PBKDF2-HMAC-SHA256 when bcrypt is absent (never bare SHA-256); the legacy unsalted-SHA-256 verify path has been removed; outbound-webhook burst protection.

Full detail: docs/v3.4.0.md, docs/features.md, and CHANGELOG.md.

Verify

sha256sum -c remotepower-3.4.0.tar.gz.sha256
# expected: f976d31e6fed6351a0a3ab20efc5b1b9e4840393ab343eb8bea999ef5893001e

The v3.4.0 tag is GPG-signed (key 5D2CCDBF, jmo@tvipper.com).

v3.3.2

28 May 07:48
v3.3.2
a68f33e

Choose a tag to compare

RemotePower v3.3.2 — Release Notes

Released: 2026-05-28

Overview

Small follow-through release for the v3.3.1 UI fixes. Two things:

  1. The device-table column-shedding now applies everywhere. v3.3.1
    scoped the "drop low-priority columns so the Status pill can't
    collapse to a " fix to display-mode: standalone. That missed the
    common case: installed PWAs usually report minimal-ui (now the
    default), and a narrow browser window has the same docked sidebar
    and the same problem. The breakpoints are now general — whenever the
    sidebar is docked (>720px, not collapsed) the table sheds columns
    ~200px earlier so the essential columns never get squeezed. A
    width-independent backstop (.dev-status-cell { overflow: visible })
    guarantees the Status pill can't render an ellipsis regardless.

  2. The version bump itself is the fix delivery mechanism. The
    service worker caches CSS/JS cache-first, keyed to ?v=<version>.
    v3.3.1's later UI commits kept the same ?v=3.3.1, so installed PWAs
    kept serving the stale cached stylesheet and never saw the fixes.
    Bumping to v3.3.2 changes ?v= and CACHE_NAME, forcing every client
    to refetch — no manual cache clear needed.

No backend or schema changes.

Also in this release

  • Devices-table dots, finally fixed. The earlier overflow: visible
    backstop still left a stray on whichever cell was most squeezed;
    the whole table now uses text-overflow: clip, so no cell can render
    the ellipsis glyph regardless of width.
  • Emoji purge completed. Residual emoji the v3.3.0 sweep missed
    (🩺, , , plus stale doc mentions) replaced with Lucide SVG or
    plain text.
  • Per-device "Prioritise CVEs" AI button on the CVE page — ranked
    remediation plan per device (new prioritise_cves prompt). The CVE
    page now offers both Prioritise (per device) and Triage (per finding).

Note on shipping UI fixes to PWAs

Because of the cache-first service worker, any CSS/JS change only
reaches installed PWAs once the cache token changes — which is tied to a
version bump. Ship UI fixes with a version bump (or expect to clear the
PWA cache by hand) rather than under an already-released version string.

v3.2.2 — hotfix batch

27 May 02:53
v3.2.2
8063d68

Choose a tag to compare

v3.2.2 — Hotfix

Hotfix release. No new features. All changes are bug fixes surfaced by real-fleet debugging.

Deploy: pull the new api.py, index.html, app.js — reload nginx, hard-reload the browser once.
If devices were flapping on a low online_ttl: go to Settings → General and set online_ttl to 300.

Bug fixes

Scheduler cron jobs fired dozens of times per minute
process_schedule() runs on every CGI request. _cron_matches() returns True for the full 60-second matching window — so every heartbeat and browser poll within that window fired the job again (20+ duplicate upgrade_packages dispatches per hour). Fixed by stamping last_fired_minute on each recurring job when it fires and skipping cron evaluation if the minute hasn't advanced.

Race condition in offline/online detection (check_offline_webhooks)
Two concurrent CGI processes could both read offline_notified = {}, each fire device_offline, and write duplicate alerts and fleet events. The read-check-write sequence is now serialised under _LockedUpdate(CONFIG_FILE). Same fix covers patch_alerted.

Debug log timestamps were local time instead of UTC
Heartbeat entries used datetime.now() (server local, CEST); browser entries use toISOString() (UTC). Both server-side write sites now emit UTC.

online_ttl example comment showed 180 s
MIN_ONLINE_TTL dropped from 300 → 150 in v3.2.1, so a configured 180 s value now takes effect and flips devices offline after just 3 missed heartbeats. Comment updated to 300 (the default).

MAX_FLEET_EVENTS raised 200 → 1000
Active fleets (or fleets hit by the race condition above) rolled over the Home dashboard activity log in under an hour.

nginx client_max_body_size 64 KB → 2 MB
Server-side limit was raised to 50 MB in v1.7.0 but nginx configs were not updated. Systems with ≥ ~850 packages were silently receiving HTTP 413 on every /api/packages POST, leaving the CVE scanner with a stale package list.

nginx rate-limit comment corrected
Example zone used rate=30r/m — exceeded by normal traffic on a LAN where all devices share one NAT IP. Updated to rate=10r/s with explanatory note.

Alerts "Mark all" checkbox — TypeError: btn is undefined
The select-all checkbox used data-action dispatch which calls fn() with no arguments; toggleAllAlerts then read btn.checked on undefined. Fixed by switching to data-change dispatch, which passes the checked boolean directly.

Monitoring → Custom Scripts sort buttons missing
The results table had data-col attributes but no wireSortOnly wiring — clicking column headers did nothing. Fixed.

UI improvements

Sortable click-to-sort column headers (shift-click for multi-column; state persists in prefs):
Alerts inbox, CMDB, Configuration Drift, ACME Certificates, Webhook log, MCP Confirmations, Inbound webhooks, Scripts, Listening Ports, Monitoring Custom Scripts results.

Drift table header corrected — Device, Group, and Files watched columns were missing from the thead.

v3.2.1 — SNMP integration, threshold alerts, ops polish

26 May 17:10
907f200

Choose a tag to compare

RemotePower v3.2.1 — Release Notes

Released: 2026-05-26

Overview

v3.2.1 is a substantial follow-up release on top of v3.2.0. The big
features (alerts inbox, inbound webhooks, MCP write tools, OIDC SSO,
SNMPv2c polling, syslog ingestion) all shipped in v3.2.0; v3.2.1 is
the operability pass: integration into every dashboard surface,
threshold-driven alerting on SNMP-derived metrics, bug fixes from
real-world testing, and polish on every new feature.

No schema migrations. Drop in the new tarball, reload nginx, hard-reload
the browser once to refresh the service-worker cache
(remotepower-shell-v3.2.0remotepower-shell-v3.2.1).

SNMP — became a first-class data source

In v3.2.0 SNMP shipped as a basic sys-group poll, buried in the CMDB
asset modal. v3.2.1 surfaces SNMP data across the whole dashboard:

Surface What you see
Devices page card Green/red 📡 pill next to the hostname + "SNMP up Nd" meta cell
Monitoring → Device Metrics Separate SNMP table with CPU %, Memory %, Storage list, Temperature, Uptime
Device drawer → Actions & Settings SNMP config form (enable / community / port / Poll now) for agentless devices
Device drawer → Audit "SNMP" expandable card with full sys-group, per-core CPU pills, UCD-SNMP load avg + memory, storage table, interface table (admin/oper/speed/octets/errors), Mikrotik vendor block, UniFi vendor block, partial-error footer
Devices page filter New dropdown: Any SNMP / Configured / OK / Failing
Alerts inbox snmp_unreachable / snmp_dead / snmp_recover events
Outbound webhooks Same events available in Settings → Notifications
MCP read tool get_snmp_data(name) exposes the full data set to AI hosts

What gets polled every 5 minutes

MIB Why
sys-group (RFC 3418) sysDescr, sysObjectID, sysUpTime, sysContact, sysName, sysLocation
hrProcessorTable (RFC 2790) Per-core load %. Works on Mikrotik + Linux + BSD
hrStorageTable Memory + filesystems with used %
UCD-SNMP-MIB Load averages + raw CPU ticks + UCD memory totals (net-snmp boxes)
Mikrotik vendor (1.3.6.1.4.1.14988) RouterOS version, board temp, CPU MHz, voltage

Deep-poll on demand

The drawer Audit tab also walks the IF-MIB::ifTable (interface
table). That walk is heavier than the 5-min sweep — bounded at 64
interfaces, runs only when the operator opens the SNMP card. Returns
admin/oper status, link speed, in/out octets, error counters per port.

SNMP threshold alerts

SNMP-derived values flow through the same threshold pipeline as agent
metrics. The metric-threshold modal grows two rows:

  • SNMP CPU % — defaults warn 75, crit 90
  • Temperature °C — defaults warn 70, crit 85

Disk and memory % share the existing agent thresholds.

Fires metric_warning / metric_critical / metric_recovered
through the regular event pipeline — alerts inbox + outbound webhooks

  • Server Status site-health rollup, all wired transparently.

Unmonitored devices still collect SNMP data (visible in the metrics
table with a "silent" badge) but skip the alert fire. Same posture as
the agent metric pipeline.

Edge-triggered transitions

  • snmp_unreachable — fires once on the 2nd consecutive poll
    failure. Severity: high. Single-packet UDP loss never alerts.
  • snmp_dead — fires at the 72nd consecutive failure (~6 hours
    at the 5-min cadence). Severity: critical. Different event so
    operators can configure their webhook destinations separately.
  • snmp_recover — auto-resolves BOTH snmp_unreachable and
    snmp_dead rows in the Alerts inbox via _ALERT_RECOVER_EXTRA.

Alerts inbox — operability

  • Unmonitored devices no longer pollute the inbox. The
    _record_alert helper now applies the same monitored-gate that
    fire_webhook already had on outbound fan-out. Fleet-wide events
    (no device_id) and orphan events (device deleted) still record.
  • Clear-alerts buttons. "Clear resolved" purges every resolved
    row; "Clear all" wipes everything (with double-confirm). Admin-only,
    audit-logged.
  • Always-visible green-at-zero badges. Alerts and MCP Confirmations
    sidebar entries show their count badge even at 0 (green) — the
    empty state was misread as "no inbox exists".
  • metric_critical events now enter the inbox. Previously
    missing from _ALERT_RULES; 90%+ threshold breaches fired the
    webhook but never created an inbox row.
  • _fire_metric_webhook payload aliases. Added metric and
    level keys so the alert serializer can render the title and
    classify severity correctly from the metric-event shape.

MCP write tools — pre-validation

run_saved_script previously validated the script_id inside
_mcp_execute — which runs AFTER the operator approves a
confirmation. A typo would park a doomed confirmation that would fail
silently on approval. v3.2.1 adds _mcp_validate_params that runs
BEFORE the confirmation queue, so bogus IDs return 400 immediately.

Server Status

  • Site health card — load average (1/5/15 min from /proc/loadavg),
    system memory % (from /proc/meminfo), active session count,
    devices-online %, derived ok | warn rollup with reason flags
    (high load, memory > 90%, < 80% devices online, < 90% webhook rate).
  • Webhook delivery rate fix. No longer counts
    disabled/suppressed/filtered log entries as failed
    attempts. The 1/10 = 10% display on quiet fleets is gone. Now
    reads correctly as 1/1 = 100% with 9 skipped reported
    separately. Site-health flag ignores all-skipped case.
  • Inbound webhook + syslog rate card. New row alongside outbound
    delivery — 24h/7d rates with by-kind breakdown
    (alert:N · syslog:M).
  • Webhook log capacity bumped 100 → 500 entries so the 24h rate
    window survives a noisy day.

OIDC

  • Test discovery endpoint. POST /api/auth/oidc/test (admin)
    probes the configured issuer at /.well-known/openid-configuration
    and returns the discovered endpoints + warnings for common
    misconfigs (missing client_secret, no openid scope, etc.).
  • "Test discovery" button on the OIDC settings pane. Shows the
    redirect URI you need to register with your IdP, plus the parsed
    authorize / token / userinfo / jwks endpoints.

UI polish

  • Sidebar widened 200 → 220 px to fit "MCP Confirmations" + count
    badge cleanly without clipping. Page content margin moved in
    lockstep.
  • Duplicate "TLS / DNS" title above ACME Certificates removed.
    The title now lives inside the expiry panel so it doesn't render
    twice when viewing ACME.
  • README gallery — click-through screenshot gallery via GitHub
    <details> accordion. Index.png remains the hero.

Security review notes

  • Bearer auth (Authorization: Bearer <token>) accepted on every
    endpoint, not just /api/metrics. Token verification path is
    uniform — same TTL, role, admin gate. X-Token wins when both
    headers are present so proxy-injected Authorization headers can't
    override dashboard sessions. See docs/security.md.
  • MCP role separation enforced — admin tokens cannot call MCP write
    endpoints; only mcp role keys can. Audit log records the
    originating AI host (X-MCP-Client) and natural-language prompt
    (X-MCP-Prompt) on every call.

Test coverage

1727 tests pass (was 1672 at the v3.2.0 base ship). Adds coverage
for SNMP threshold alerts, clear-alerts endpoints, MCP write-tool
pre-validation, unmonitored alert suppression, webhook-rate fix,
SNMP walk subtree-boundary semantics, OIDC validation, and Bearer
auth parity (X-Token vs. Authorization).

Upgrade

make dist          # builds dist/remotepower-3.2.1.tar.gz

Deploy the tarball, reload nginx, hard-reload the browser once. No
schema migrations required.

v3.0.6

25 May 21:38
v3.0.6
22e08c8

Choose a tag to compare

Changelog

All notable changes to RemotePower. Newest first.

v3.0.6 — 2026-05-25

Production-readiness polish on top of v3.0.5. Recommended for any
operator running the v3.0.5 release
— every change here strengthens
the existing posture without changing visible behaviour for end users.
No schema changes, no migrations.

Added

  • /api/health liveness endpoint. Unauthenticated, returns
    {"status":"ok","version":"<x.y.z>"} and nothing else. Replaces
    / in the Docker HEALTHCHECK (probing the full SPA on every
    poll was wasteful) and gives external orchestrators / reverse
    proxies / uptime monitors a cheap, stable endpoint. Path is in
    _PWCHG_ALLOWED_PATHS so it works even from a session pending a
    forced password change.

  • CSP violation reporting at /api/csp-report. The strict CSP
    shipped in v3.0.5 has been silently blocking any inline script or
    style the browser tries to execute. With report-uri /api/csp-report
    added to the policy directive, every block now POSTs a JSON
    report to the new handler, which appends one audit-log line per
    violation (directive, blocked URI, source file, line, sample). A
    future regression that reintroduces inline code surfaces as a
    logged event instead of a silent visual bug. Endpoint is request-
    size-capped at 16 KB and is the one POST exempted from the same-
    origin CSRF check (browsers sometimes send Origin: null on CSP
    reports).

  • Subresource Integrity (SHA-384) on every bundled vendor library.
    swagger.html adds integrity= to the static <link> and
    <script> tags for swagger-ui; app.js adds it to the dynamic
    _loadXtermOnce() (xterm.js, xterm.min.css, addon-fit) and
    generateQRCode() (qrcode-generator) loads. If the file on disk is
    ever overwritten — corrupt deploy, swapped tarball, supply-chain
    compromise — the browser refuses to execute it. Updating a vendor
    version means recomputing one SHA-384; the procedure is documented
    inline next to each integrity= attribute.

  • GitHub Actions CI workflow (.github/workflows/ci.yml). Runs
    the full unittest suite on every push and PR to main. The
    TestCSPMigrationFidelity checks introduced in v3.0.5 are now
    CI-enforced — any commit that reintroduces an inline event
    handler, an unresolved CSS ${…} template, a javascript: URI, a
    duplicate ID, etc. fails the workflow before the PR can merge.
    On main-branch pushes the workflow also runs make dist to catch
    "works on dev, breaks on fresh checkout" issues.

Changed

  • Dockerfile HEALTHCHECK now probes /api/health instead of /.
  • Both nginx CSP directives carry report-uri /api/csp-report;
    configure-time addition for fresh installs, one-line append for
    existing deploys (see docs/v3.0.6.md §Upgrade).
  • Service worker cache name bumped to remotepower-shell-v3.0.6 so
    the activate handler evicts the previous shell on first reload.

Internals

  • Test suite at 1,560+ tests, all passing.
  • test_v306.py holds the strict version pins now; test_v305.py's
    pins loosened to ^3\.\d+\.\d+$ regexes (same convention every
    prior release-bump test followed).

v3.0.4

24 May 22:00
v3.0.4
f915f51

Choose a tag to compare

Changelog

All notable changes to RemotePower. Newest first.

v3.0.4 — 2026-05-24

A bug-fix release hot on the heels of v3.0.3. Eight real production bugs, all
landed the same evening they were spotted. Recommended for every operator
who runs the AI features, the metric thresholds, the per-device settings
drawer, or the mobile / PWA UI — i.e. nearly everyone.
No schema changes,
no migrations needed.

Fixed

  • AI chat returned 500 on every request. _http_post_json in
    ai_provider.py referenced cfg.get('insecure_ssl') from inside a function
    that never received cfg as a parameter. The reference resolved against an
    unbound name and raised NameError before the request ever left the box.
    The bug was latent in v3.0.2 — the change that "honoured" the insecure_ssl
    flag never wired it through — and triggered the first time a v3.0.2+ install
    actually exercised the chat path. Fix: explicit insecure_ssl: bool = False
    parameter; callers forward cfg.get('insecure_ssl'). Anthropic and
    OpenAI-compatible paths both updated. The matching _http_get_json got the
    same parameter for symmetry.

  • Monitor page showed "OK" badge while Needs Attention was screaming about a
    swap/memory/CPU warning on the same host.
    handle_devices_list() returned
    a curated subset of device fields and silently dropped metric_state. The
    client's row aggregator iterated d.metric_state || {}, got empty, and
    defaulted to "OK" — even though /api/attention read the same state on the
    server side directly and was correctly surfacing the alert. Fix: include
    metric_state in the device list response. The dict is small (one entry per
    active alert) and cheap to serialise.

  • No 🩺 Investigate button on memory/swap/CPU alerts. The AI prompt keys
    (mitigate_memory, mitigate_cpu) have existed in
    ai_provider.SYSTEM_PROMPTS since v3.0.1, but _MITIGATE_PLAYBOOKS only
    carried patches / disk / drift / service_down / reboot / brute_force. The
    alert kind landed in Needs Attention as 'swap' / 'memory' / 'cpu', no
    playbook lookup match, no button rendered. Fix: three new playbooks with
    concrete read-only diagnostics:

    • memory: free -h, /proc/meminfo top fields, top 20 by %mem,
      recent OOM events from journalctl + dmesg, vm.swappiness /
      overcommit sysctls, systemd-cgtop snapshot.
    • swap: free -h, swapon --show, per-process VmSwap ranking from
      /proc/*/status, vm.swappiness, PSI memory pressure, recent swap-related
      journal entries.
    • cpu: uptime, loadavg, top 20 by %cpu, processes in
      uninterruptible D-state, mpstat / iostat / vmstat for iowait, PSI
      CPU pressure.
      Each is explicitly marked non-destructive (test enforced). The client-side
      MITIGATE_KINDS set and _MITIGATE_KIND_LABELS dict were also updated
      in lockstep (they were a duplicate source of truth that previously had to
      be maintained manually) and a regression test asserts JS/Python parity.
  • "Save settings" button in the device drawer 404'd with "Not found". The
    drawer's _drawerSaveSettings() posts the full bundle (group, tags,
    icon, monitored, poll_interval, watched_services, log_watch,
    watched_files, cmd_allowlist) to POST /api/devices/<id> — but no
    bulk handler ever existed. The route fell through to the dispatcher's
    catch-all 404. New handle_device_save_bulk() accepts the bundle,
    validates every field with the same rules as the per-field PATCH endpoints,
    writes once atomically, and audits the save with the field list. The
    per-field endpoints still work and are unchanged. Two storage-name
    divergences are handled inside the bulk handler: client's
    watched_services is written as services_watched (server-side historical
    naming), and client's cmd_allowlist is written as allowed_commands
    (the canonical field _check_exec_allowlist() reads at command-execution
    time). The dispatcher route uses a slash-count guard so it cannot collide
    with any future /api/devices/<id>/<suffix> POST route.

  • "Re-run AI" on a mitigation playbook returned 200 OK with every field
    blank.
    _call_ai_with_prompts() passed arguments to
    chat_openai_compatible() in the wrong order — the messages parameter
    received the system-prompt STRING, then payload_messages.extend(messages)
    iterated it character by character and sent the LLM a messages array like
    [{role:'system', content:'Alert:...'}, 'Y', 'o', 'u', ...]. Ollama
    rejected the malformed payload, the provider returned {ok: False, ...},
    and the caller's ai_result.get('text', '') happily returned ''. Fix:
    build a proper messages=[{role, content}] list, pass the system prompt
    as system, unpack the per-prompt overrides into matching kwargs. And —
    related — the handler now returns 502 with the actual provider error
    message rather than 200 with an empty body, and logs the traceback to
    stderr so future failures are diagnosable.

  • Mobile / PWA sidebar drawer wouldn't collapse. Tap-outside-to-close
    silently failed because the handler required e.target === document.body,
    but real browsers report the click target as the underlying <div id="app">
    or .app-content rather than body itself. Burger-to-close also broken
    because the burger button (header z-index 100) sits behind the scrim
    (z-index 800) once the drawer is open, so the burger's own onclick
    never fires. Only the nav-button-click close path worked, which made the
    drawer feel half-broken. New handler uses explicit closest('.sidebar') /
    closest('.mobile-burger') / closest('.nav-btn') guards instead — any
    tap outside those zones closes the drawer on mobile. Regression test
    forbids the strict e.target === document.body pattern from sneaking
    back in.

  • "✨ AI Prioritise Updates" button felt unresponsive. Click, no
    in-place feedback, eventually a small toast that was easy to miss.
    Operators reported "I clicked it, nothing happened." Two changes: the
    button visibly disables and switches to ⏳ during the API call, and the
    negative-case toasts ("no patch history" / "no upgrade listing in
    history") got rewritten. Iter 2: the earlier "use Force re-scan
    packages" suggestion was misleading — force_package_scan only
    refreshes the upgradable COUNT, not the listing (the agent's
    get_patch_info() discards out and only keeps len()). The only
    path that populates patch_history with a real listing is an
    operator-triggered exec command. Rather than make the operator dig
    for that, ✨ now auto-queues the right per-package-manager listing
    command via POST /api/exec after a confirmation prompt (apt list --upgradable, dnf check-update, pacman -Qu). One click → wait
    ~60 s for the heartbeat → click ✨ again, AI engages.

  • Mobile burger button didn't close the open drawer. Tap-outside
    worked (v3.0.4 iter 1 fix), but tapping where the burger visually
    was had no effect on mobile Chrome / installed PWA. Root cause: the
    scrim (z-index 800, inset: 0) covers the burger (header z-index
    100) once the drawer is open, so the burger's onclick never fires.
    The body-level close handler should catch it via the scrim — and
    does on desktop browsers' touch emulation — but real mobile Chrome
    and PWA installs were unreliable here. Standard mobile-drawer fix:
    a dedicated ✕ button inside the sidebar header, visible only at
    max-width: 720px. Always discoverable, always works, no z-order
    trickery.

  • Silent except → logged exceptions on the heartbeat metric path.
    handle_heartbeat() wrapped process_metric_thresholds() in a bare
    except Exception: pass. Any logic bug there silently broke metric
    state recompute and the operator got no clue. Now logs class: message

    • traceback to stderr (journalctl -u fcgiwrap) while still keeping the
      heartbeat path resilient.

Internals

  • Test suite at 1,532 teststest_v304.py holds the strict version
    pins now; test_v303.py's pins loosened to ^3\.\d+\.\d+$ regexes.
    Same convention test_v302.py followed for v3.0.3.
  • This release ships preliminary scaffolding for v3.1.0 (the mcp role
    enum, an empty MCP_ACTION_ALLOWLIST, the require_mcp_action() gate, a
    get_mcp_attribution() helper that reads X-MCP-Client / X-MCP-Prompt
    headers, optional ai_host / ai_prompt kwargs on audit_log, and a
    per-device require_confirmation field with its own PATCH endpoint). All
    of it is silent — no MCP write tools are yet registered, so even a valid
    mcp-role API key still gets 403 on every action attempt. Tests for the
    scaffolding live in test_v310.py. Stage 4 of v3.1.0 will populate the
    allowlist.

v3.0.3

24 May 16:31
v3.0.3
ae93328

Choose a tag to compare

Changelog

All notable changes to RemotePower. Newest first.

v3.0.3 — 2026-05-24

A small, focused security + UX patch on top of v3.0.2. Recommended for all
operators
, especially anyone whose Install-as-app button stopped working in
Chrome or Brave.

Fixed

  • PWA install button silently broken. Chrome and Brave were never showing
    the "Install RemotePower" button. Three layered causes:

    1. A stylesheet rule with ID-selector specificity
      (#pwa-install-btn { display: none; }) was overriding the inline reveal —
      when the browser fired beforeinstallprompt and the JS cleared the inline
      display:none, the stylesheet rule took over and the button stayed hidden.
    2. A timing race: if beforeinstallprompt fired before DOMContentLoaded
      (common on warm reload when manifest + service worker are already cached),
      the button reference was still null and the reveal was a no-op. The
      event only fires once per session, so the button never came back.
    3. The two icons declared purpose: "any maskable" as a combined value.
      Some Chrome / Brave builds treat that as maskable-only, which doesn't
      satisfy the installability gate that requires at least one pure-any
      icon. Now split into separate any + maskable entries.

    The service worker cache name was bumped to remotepower-shell-v3.0.3 so
    existing installs evict the stale shell on first reload. If your install
    button still doesn't appear after upgrading, do one hard reload to pick up
    the new service worker.

  • Mobile drawer scrim rendered as a half-sized floating rectangle. Opening
    the mobile nav drawer dropped a partial translucent black box near the top
    of the screen instead of dimming the whole page behind the drawer, and
    tap-outside-to-close did nothing. Root cause: two body::after rules
    collided. The ambient blue-glow effect (a fixed 800×400 box with
    translateX(-50%) and pointer-events:none) sets properties that the
    mobile-nav scrim rule didn't override — inset: 0 only resets
    top/right/bottom/left, not width/height/transform/pointer-events. The
    scrim now explicitly resets those properties so it fills the viewport and
    catches taps. Mobile-only — desktop never used the scrim path.

  • Mobile hamburger had a visible square box around it. The
    .mobile-burger style carried border: 1px solid var(--border),
    which on the dark theme rendered as a discrete framed button next
    to the logo — easy to mistake for a separate clickable element.
    The hamburger glyph reads fine on its own; the border is now none.

  • Quick-SSH icon next to hostnames was unreadable blue on dark
    mode.
    The <a> carried no explicit color, so the browser's
    default link colour bled through and clashed with the dark
    sidebar/table. The icon now uses color:var(--text) (near-white
    in dark, near-black in light), so it stays visible in both themes.

Added

  • RP_SMTP_PASSWORD and RP_LDAP_BIND_PASSWORD environment variables.
    The two remaining plaintext secrets in config.json can now be supplied
    via the environment — same pattern as RP_PROXMOX_TOKEN_SECRET (v2.3.1).
    When the env var is set it takes precedence over the config file, the
    secret stays out of /var/lib/remotepower/, and it is not included in
    the backup export
    (so backups can be shared with support safely).

    Set them in your systemd unit or container env:

    # /etc/systemd/system/remotepower.service (or your override)
    Environment=RP_SMTP_PASSWORD=…
    Environment=RP_LDAP_BIND_PASSWORD=…
    # docker-compose.yml
    environment:
      RP_SMTP_PASSWORD: "${RP_SMTP_PASSWORD}"
      RP_LDAP_BIND_PASSWORD: "${RP_LDAP_BIND_PASSWORD}"

    The Settings page detects the env vars and shows a green "✓ Password is
    currently being read from RP_SMTP_PASSWORD" hint above the (now disabled)
    config field. Existing setups that keep the password in config.json are
    unchanged.

  • Forced password change for default-credential accounts. A fresh
    install seeds admin / remotepower. Since v2.3.2 the UI has shown a red
    banner nagging the operator to change it, but the app remained fully
    usable on the default password — the banner could be ignored. As of
    v3.0.3, every API call returns 403 until the password is changed,
    with only POST /api/users/passwd and GET /api/public-info reachable.
    The dashboard catches the 403, surfaces a clear toast, and routes you
    straight to the change-password form. As soon as the password is changed,
    the flag clears and everything unlocks.

    This applies only to accounts that still carry the must_change_password
    flag — once changed, never blocked again. API keys are unaffected (they
    can only be created from an already-cleared account in the first place).

Internals

  • Test suite at 1,453 tests (test_v303.py covers the new behaviour;
    one brittle fixed-offset slice in test_v227.py widened to use the
    whole @media block).
  • test_v302.py strict version pins loosened to 3.x.x regexes since
    test_v303.py now holds the strict pin role. Same convention going forward.