| Name | Modified | Size | Downloads / Week |
|---|---|---|---|
| Parent folder | |||
| README.md | 2026-05-24 | 9.0 kB | |
| v3.0.4 source code.tar.gz | 2026-05-24 | 4.6 MB | |
| v3.0.4 source code.zip | 2026-05-24 | 4.8 MB | |
| Totals: 3 Items | 9.4 MB | 0 | |
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_jsoninai_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 = Falseparameter; 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: includemetric_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 inai_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-sideMITIGATE_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. -
"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'swatched_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 tochat_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 required
e.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 ownonclicknever 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'sget_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 atmax-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 bareexcept 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.
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, aget_mcp_attribution()helper that readsX-MCP-Client/X-MCP-Promptheaders, 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 validmcp-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.