Releases: tyxak/remotepower
v4.0.0 — scale out, encrypt everything, see more
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)
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
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/configsecret-scrub backstop. The endpoint redacted secrets by name, so the AI-providerapi_keyand 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_envindicator.- 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_restartingalert only fired for Kubernetes pods and the drawer's container age was blank. The agent now fills them from a single batcheddocker inspectper 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
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
RemotePower v3.3.2 — Release Notes
Released: 2026-05-28
Overview
Small follow-through release for the v3.3.1 UI fixes. Two things:
-
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 todisplay-mode: standalone. That missed the
common case: installed PWAs usually reportminimal-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. -
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=andCACHE_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 usestext-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 (newprioritise_cvesprompt). 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
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
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.0 → remotepower-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 BOTHsnmp_unreachableand
snmp_deadrows in the Alerts inbox via_ALERT_RECOVER_EXTRA.
Alerts inbox — operability
- Unmonitored devices no longer pollute the inbox. The
_record_alerthelper now applies the same monitored-gate that
fire_webhookalready 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_criticalevents now enter the inbox. Previously
missing from_ALERT_RULES; 90%+ threshold breaches fired the
webhook but never created an inbox row._fire_metric_webhookpayload aliases. Addedmetricand
levelkeys 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 %, derivedok | warnrollup with reason flags
(high load, memory > 90%, < 80% devices online, < 90% webhook rate). - Webhook delivery rate fix. No longer counts
disabled/suppressed/filteredlog entries as failed
attempts. The 1/10 = 10% display on quiet fleets is gone. Now
reads correctly as1/1 = 100%with 9skippedreported
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-Tokenwins when both
headers are present so proxy-injected Authorization headers can't
override dashboard sessions. Seedocs/security.md. - MCP role separation enforced — admin tokens cannot call MCP write
endpoints; onlymcprole 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
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/healthliveness endpoint. Unauthenticated, returns
{"status":"ok","version":"<x.y.z>"}and nothing else. Replaces
/in the DockerHEALTHCHECK(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_PATHSso 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. Withreport-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 sendOrigin: nullon CSP
reports). -
Subresource Integrity (SHA-384) on every bundled vendor library.
swagger.htmladdsintegrity=to the static<link>and
<script>tags for swagger-ui;app.jsadds 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 eachintegrity=attribute. -
GitHub Actions CI workflow (
.github/workflows/ci.yml). Runs
the full unittest suite on every push and PR tomain. The
TestCSPMigrationFidelitychecks introduced in v3.0.5 are now
CI-enforced — any commit that reintroduces an inline event
handler, an unresolved CSS${…}template, ajavascript:URI, a
duplicate ID, etc. fails the workflow before the PR can merge.
On main-branch pushes the workflow also runsmake distto catch
"works on dev, breaks on fresh checkout" issues.
Changed
- Dockerfile
HEALTHCHECKnow probes/api/healthinstead 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.6so
the activate handler evicts the previous shell on first reload.
Internals
- Test suite at 1,560+ tests, all passing.
test_v306.pyholds 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
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_jsonin
ai_provider.pyreferencedcfg.get('insecure_ssl')from inside a function
that never receivedcfgas a parameter. The reference resolved against an
unbound name and raisedNameErrorbefore 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: explicitinsecure_ssl: bool = False
parameter; callers forwardcfg.get('insecure_ssl'). Anthropic and
OpenAI-compatible paths both updated. The matching_http_get_jsongot 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 droppedmetric_state. The
client's row aggregator iteratedd.metric_state || {}, got empty, and
defaulted to "OK" — even though/api/attentionread the same state on the
server side directly and was correctly surfacing the alert. Fix: include
metric_statein 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_PROMPTSsince v3.0.1, but_MITIGATE_PLAYBOOKSonly
carriedpatches / 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/meminfotop fields, top 20 by%mem,
recent OOM events fromjournalctl+dmesg,vm.swappiness/
overcommit sysctls,systemd-cgtopsnapshot. - swap:
free -h,swapon --show, per-processVmSwapranking 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/vmstatfor iowait, PSI
CPU pressure.
Each is explicitly marked non-destructive (test enforced). The client-side
MITIGATE_KINDSset and_MITIGATE_KIND_LABELSdict 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.
- memory:
-
"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) toPOST /api/devices/<id>— but no
bulk handler ever existed. The route fell through to the dispatcher's
catch-all 404. Newhandle_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_servicesis written asservices_watched(server-side historical
naming), and client'scmd_allowlistis written asallowed_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 — themessagesparameter
received the system-prompt STRING, thenpayload_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'sai_result.get('text', '')happily returned''. Fix:
build a propermessages=[{role, content}]list, pass the system prompt
assystem, 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 requirede.target === document.body,
but real browsers report the click target as the underlying<div id="app">
or.app-contentrather 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 ownonclick
never fires. Only the nav-button-click close path worked, which made the
drawer feel half-broken. New handler uses explicitclosest('.sidebar')/
closest('.mobile-burger')/closest('.nav-btn')guards instead — any
tap outside those zones closes the drawer on mobile. Regression test
forbids the stricte.target === document.bodypattern 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_scanonly
refreshes the upgradable COUNT, not the listing (the agent's
get_patch_info()discardsoutand only keepslen()). The only
path that populatespatch_historywith 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 viaPOST /api/execafter 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'sonclicknever 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()wrappedprocess_metric_thresholds()in a bare
except Exception: pass. Any logic bug there silently broke metric
state recompute and the operator got no clue. Now logsclass: message- traceback to stderr (
journalctl -u fcgiwrap) while still keeping the
heartbeat path resilient.
- traceback to stderr (
Internals
- Test suite at 1,532 tests —
test_v304.pyholds 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
mcprole
enum, an emptyMCP_ACTION_ALLOWLIST, therequire_mcp_action()gate, a
get_mcp_attribution()helper that readsX-MCP-Client/X-MCP-Prompt
headers, optionalai_host/ai_promptkwargs onaudit_log, and a
per-devicerequire_confirmationfield 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 intest_v310.py. Stage 4 of v3.1.0 will populate the
allowlist.
v3.0.3
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:- A stylesheet rule with ID-selector specificity
(#pwa-install-btn { display: none; }) was overriding the inline reveal —
when the browser firedbeforeinstallpromptand the JS cleared the inline
display:none, the stylesheet rule took over and the button stayed hidden. - A timing race: if
beforeinstallpromptfired beforeDOMContentLoaded
(common on warm reload when manifest + service worker are already cached),
the button reference was stillnulland the reveal was a no-op. The
event only fires once per session, so the button never came back. - 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 separateany+maskableentries.
The service worker cache name was bumped to
remotepower-shell-v3.0.3so
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. - A stylesheet rule with ID-selector specificity
-
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: twobody::afterrules
collided. The ambient blue-glow effect (a fixed 800×400 box with
translateX(-50%)andpointer-events:none) sets properties that the
mobile-nav scrim rule didn't override —inset: 0only resets
top/right/bottom/left, notwidth/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-burgerstyle carriedborder: 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 nownone. -
Quick-SSH icon next to hostnames was unreadable blue on dark
mode. The<a>carried no explicitcolor, so the browser's
default link colour bled through and clashed with the dark
sidebar/table. The icon now usescolor:var(--text)(near-white
in dark, near-black in light), so it stays visible in both themes.
Added
-
RP_SMTP_PASSWORDandRP_LDAP_BIND_PASSWORDenvironment variables.
The two remaining plaintext secrets inconfig.jsoncan now be supplied
via the environment — same pattern asRP_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 fromRP_SMTP_PASSWORD" hint above the (now disabled)
config field. Existing setups that keep the password inconfig.jsonare
unchanged. -
Forced password change for default-credential accounts. A fresh
install seedsadmin / 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 onlyPOST /api/users/passwdandGET /api/public-inforeachable.
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.pycovers the new behaviour;
one brittle fixed-offset slice intest_v227.pywidened to use the
whole@mediablock). test_v302.pystrict version pins loosened to3.x.xregexes since
test_v303.pynow holds the strict pin role. Same convention going forward.