Skip to content

feat(power): configurable power button behavior + rename Power Mode → Power Options (OS-463)#2671

Open
elibosley wants to merge 4 commits into
masterfrom
feature/os-463-power-options
Open

feat(power): configurable power button behavior + rename Power Mode → Power Options (OS-463)#2671
elibosley wants to merge 4 commits into
masterfrom
feature/os-463-power-options

Conversation

@elibosley

@elibosley elibosley commented Jun 19, 2026

Copy link
Copy Markdown
Member

Summary

Implements OS-463: a native WebUI setting to control what the physical power button does, so an accidental bump on small-form-factor hardware (NUCs, etc.) no longer forces a shutdown.

The existing Power Mode settings page is renamed to Power Options and split into two tabs:

  • Power Mode — the existing CPU governor controls (moved verbatim).
  • Power Button — new action selector.

Power Button actions

Core ships only the trivially-safe primitives:

Action Behavior
Shut down (default) Clean shutdown (/sbin/init 0) — preserves today's behavior
Reboot Clean reboot (/sbin/init 6)
Do nothing Logs the press and takes no action — the core ask in OS-463

The choice is saved to [powermode] powerbutton= in dynamix.cfg (on the flash, so it persists) and read live on each press, so changes take effect with no reload or reboot. Unset/unexpected values fall back to Shut down.

Two handlers, both neutralized

Hardware testing surfaced that a single power press fires two independent events:

  1. acpid/etc/acpi/acpi_handler.sh. We install our setting-aware handler over the stock Slackware one at boot via rc.acpid (acpid_configure), so it wins regardless of package install order — mirroring how rc.cpufreq applies the [powermode] governor at boot.
  2. elogind → grabs the Power Button input device (PNP0C0C); its default HandlePowerKey=poweroff shut the box down regardless of acpid. A shipped logind.conf.d drop-in sets HandlePowerKey=ignore, making acpid the sole authority.

Both were verified on real hardware (elogind D-Bus HandlePowerKey now reports ignore).

Plugin extension hook

Actions that need their own setup or are hardware-fragile (suspend, running a script) are not baked into core. Instead the power button is extensible so plugins add actions in their own repos:

  • UI: PowerButton.page includes plugins/*/include/powerbutton-option.php (and powerbutton-extra.php for action-specific fields).
  • Action: unraid_power_handler.sh dispatches any non-builtin action to /etc/acpi/powerbutton.d/<action>.
  • Contract documented in etc/acpi/powerbutton.d/README.

Follow-up work to wire the first two consumers (tracked as sub-tasks of OS-463):

  • S3 Sleep plugin → adds a Sleep action.
  • User Scripts plugin → adds a Run script action.

Files

File Change
emhttp/plugins/dynamix/PowerMode.page Now a tabbed container titled "Power Options" (basename kept, so /Settings/PowerMode URLs still work)
emhttp/plugins/dynamix/PowerModeCpu.page New — tab 1, the CPU governor form
emhttp/plugins/dynamix/PowerButton.page New — tab 2, action selector + plugin include hooks
etc/acpi/unraid_power_handler.sh New — setting-aware ACPI handler with plugin dispatch
etc/acpi/powerbutton.d/README New — plugin extension contract
etc/rc.d/rc.acpid Installs the handler + creates powerbutton.d/ at boot
etc/elogind/logind.conf.d/20-unraid-powerbutton.conf New — elogind ignores the power key
emhttp/languages/en_US/helptext.txt Help text for the new setting

Testing

  • bash -n clean on the handler and rc.acpid; php -l clean on both page bodies.
  • Handler routing verified for every action (shutdown / reboot / ignore / unset-default) plus plugin dispatch and missing-handler fallback.
  • Boot-time install simulated and verified live on devgen.local; acpid + elogind both confirmed neutralized for "Do nothing".

Notes for reviewers

  • The new etc/* files (acpi, elogind, rc.acpid) reach a system via the package build, not the deploy_to_unraid.sh dev script (which only syncs emhttp/).
  • Scope per OS-463 is Shut down + Do nothing; Reboot is a cheap safe addition. Suspend and Run-script are deliberately left to their plugins via the hook.

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@elibosley, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 53 minutes and 44 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d06a21a8-8901-4c3b-90d8-12cd176c2664

📥 Commits

Reviewing files that changed from the base of the PR and between 2e3f39f and 1e7413b.

📒 Files selected for processing (8)
  • emhttp/languages/en_US/helptext.txt
  • emhttp/plugins/dynamix/PowerButton.page
  • emhttp/plugins/dynamix/PowerMode.page
  • emhttp/plugins/dynamix/PowerModeCpu.page
  • etc/acpi/powerbutton.d/README
  • etc/acpi/unraid_power_handler.sh
  • etc/elogind/logind.conf.d/20-unraid-powerbutton.conf
  • etc/rc.d/rc.acpid

Walkthrough

Adds configurable physical power button behavior: a new ACPI handler script reads a setting from dynamix.cfg to either shut down or ignore button presses, with rc.acpid wiring to install it before the daemon starts. A new PowerButton.page exposes this setting in the web UI. Separately, PowerMode.page is renamed to "Power Options" and its CPU governor form is extracted into a new PowerModeCpu.page.

Changes

Physical Power Button Behavior

Layer / File(s) Summary
ACPI handler script and rc.acpid wiring
etc/acpi/unraid_power_handler.sh, etc/rc.d/rc.acpid
unraid_power_handler.sh handles button/power ACPI events by reading powerbutton from dynamix.cfg and either running /sbin/init 0 or logging an ignore. rc.acpid gains acpid_configure() which copies the handler to /etc/acpi/acpi_handler.sh, called from acpid_start() before the daemon launches.
PowerButton.page UI and helptext
emhttp/plugins/dynamix/PowerButton.page, emhttp/languages/en_US/helptext.txt
New page POSTs a powerbutton dropdown (shutdown / ignore) to /update.php, writing into dynamix/dynamix.cfg under the powermode section. Helptext describes both behaviors.

Power Options Menu Reorganization

Layer / File(s) Summary
PowerModeCpu.page extraction and PowerMode.page rename
emhttp/plugins/dynamix/PowerModeCpu.page, emhttp/plugins/dynamix/PowerMode.page
PowerModeCpu.page is a new page implementing CPU governor selection: PHP reads current/available governors, renders radio inputs with checked/disabled states, syncs the selection into a hidden #arg[1] field via JS, and shows a VM notice when hypervisor detection fires. PowerMode.page loses its previous form content and its menu title changes to "Power Options".

Sequence Diagram

sequenceDiagram
  participant Browser
  participant update.php
  participant dynamix.cfg
  participant acpid
  participant unraid_power_handler.sh
  participant init

  Browser->>update.php: POST powerbutton=ignore|shutdown
  update.php->>dynamix.cfg: write powerbutton setting
  Note over acpid: button/power ACPI event fires
  acpid->>unraid_power_handler.sh: dispatch event
  unraid_power_handler.sh->>dynamix.cfg: read powerbutton
  alt powerbutton == ignore
    unraid_power_handler.sh->>unraid_power_handler.sh: log press ignored
  else shutdown
    unraid_power_handler.sh->>init: /sbin/init 0
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 Hop, hop — the power key twitched,
A new handler rose, perfectly stitched!
"Ignore" logs the press with a grin,
"Shutdown" lets the cleanup begin.
The menu renamed, CPU form set free —
Power Options, just as it should be! ⚡

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the main changes: configurable power button behavior and the Power Mode to Power Options rename, with the OS ticket reference.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/os-463-power-options

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown

🔧 PR Test Plugin Available

A test plugin has been generated for this PR that includes the modified files.

Version: 2026.06.19.1437
Build: View Workflow Run

📥 Installation Instructions:

Install via Unraid Web UI:

  1. Go to Plugins → Install Plugin
  2. Copy and paste this URL:
https://preview.dl.unraid.net/pr-plugins/pr-2671/webgui-pr-2671.plg
  1. Click Install

Alternative: Direct Download

⚠️ Important Notes:

  • Testing only: This plugin is for testing PR changes
  • Backup included: Original files are automatically backed up
  • Easy removal: Files are restored when plugin is removed
  • Conflicts: Remove this plugin before installing production updates
  • Post-merge behavior: This preview stays available after merge until preview storage expires or it is manually cleaned up

📝 Modified Files:

Click to expand file list
emhttp/languages/en_US/helptext.txt
emhttp/plugins/dynamix/PowerButton.page
emhttp/plugins/dynamix/PowerMode.page
emhttp/plugins/dynamix/PowerModeCpu.page
etc/acpi/powerbutton.d/README
etc/acpi/unraid_power_handler.sh
etc/elogind/logind.conf.d/20-unraid-powerbutton.conf
etc/rc.d/rc.acpid

🔄 To Remove:

Navigate to Plugins → Installed Plugins and remove webgui-pr-2671, or run:

plugin remove webgui-pr-2671

🤖 This comment is automatically generated and will be updated with each new push to this PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@emhttp/plugins/dynamix/PowerModeCpu.page`:
- Around line 38-40: The jQuery selector `$(this).next('span')` in the disabled
radio button check loop is not finding the correct elements because radio inputs
are not directly followed by span siblings, but rather by text nodes or
differently structured elements. Inspect the actual HTML structure around the
radio inputs to determine what element actually follows them, then update the
traversal method in the loop (replacing `.next('span')`) to correctly locate the
element that should receive the unavailable label text. This may require using a
different jQuery method like .closest(), .parent(), or .find() depending on the
actual DOM hierarchy.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 14608339-fb32-4abb-a61e-ba54b9d0fe05

📥 Commits

Reviewing files that changed from the base of the PR and between cc6a800 and 2e3f39f.

📒 Files selected for processing (6)
  • emhttp/languages/en_US/helptext.txt
  • emhttp/plugins/dynamix/PowerButton.page
  • emhttp/plugins/dynamix/PowerMode.page
  • emhttp/plugins/dynamix/PowerModeCpu.page
  • etc/acpi/unraid_power_handler.sh
  • etc/rc.d/rc.acpid

Comment on lines +38 to +40
$('input[type=radio]').each(function(){
if ($(this).prop('disabled')) $(this).next('span').html(" <i>(_(unavailable)_)</i>");
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unavailable-label injection currently never matches the DOM.

At Line 39, $(this).next('span') does not find anything because the radio inputs are followed by text nodes, not <span> siblings. As written, disabled options won’t get the “unavailable” note.

Suggested fix
 $(function(){
   $('input[type=radio]').each(function(){
-    if ($(this).prop('disabled')) $(this).next('span').html(" <i>(_(unavailable)_)</i>");
+    if ($(this).prop('disabled')) {
+      $(this).after(" <i>(_(unavailable)_)</i>");
+    }
   });
 <?if (exec("dmesg | grep -Pom1 'Hypervisor detected'")):?>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$('input[type=radio]').each(function(){
if ($(this).prop('disabled')) $(this).next('span').html(" <i>(_(unavailable)_)</i>");
});
$(function(){
$('input[type=radio]').each(function(){
if ($(this).prop('disabled')) {
$(this).after(" <i>(_(unavailable)_)</i>");
}
});
<?if (exec("dmesg | grep -Pom1 'Hypervisor detected'")):?>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@emhttp/plugins/dynamix/PowerModeCpu.page` around lines 38 - 40, The jQuery
selector `$(this).next('span')` in the disabled radio button check loop is not
finding the correct elements because radio inputs are not directly followed by
span siblings, but rather by text nodes or differently structured elements.
Inspect the actual HTML structure around the radio inputs to determine what
element actually follows them, then update the traversal method in the loop
(replacing `.next('span')`) to correctly locate the element that should receive
the unavailable label text. This may require using a different jQuery method
like .closest(), .parent(), or .find() depending on the actual DOM hierarchy.

@elibosley

Copy link
Copy Markdown
Member Author

Update — hardware testing on devgen.local revealed a second handler.

The power button generates two events: an ACPI event (acpid → acpi_handler.sh) and an input-key event handled by elogind, whose default HandlePowerKey=poweroff shut the box down regardless of the acpid handler. There's a Power Button input device (PNP0C0C) that elogind grabs, so the acpid-only fix was not sufficient.

Added etc/elogind/logind.conf.d/20-unraid-powerbutton.conf (HandlePowerKey=ignore, HandlePowerKeyLongPress=ignore) so acpid is the sole authority over the power button. Verified live: elogind's D-Bus HandlePowerKey now reports "ignore" and the button no longer powers off in "Do nothing" mode.

Note for reviewers: the new etc/acpi/* and etc/elogind/* files land via the package build, not the deploy_to_unraid.sh dev script (which only syncs emhttp/).

@elibosley

Copy link
Copy Markdown
Member Author

Update — added more actions + a plugin extension point (commit 98fdab5ea)

Power Button options are now: Shut down (default), Reboot, Sleep (suspend) (shown only when the kernel reports suspend-to-RAM support), and Do nothing.

Rather than baking plugin-specific actions into core, the power button is now extensible so plugins add their own actions in their own repos:

  • UI hook: PowerButton.page includes plugins/*/include/powerbutton-option.php (and powerbutton-extra.php for action-specific fields).
  • Action hook: unraid_power_handler.sh dispatches non-builtin actions to /etc/acpi/powerbutton.d/<action>. The builtin Sleep also defers to powerbutton.d/sleep if present, so the S3 Sleep plugin can do array-aware prep before suspend.
  • Contract documented in etc/acpi/powerbutton.d/README.

This is the mechanism the User Scripts plugin would use to add a "Run script" action — intentionally left to that plugin rather than coupled into core.

Verified on devgen.local: all four handler branches (shutdown/reboot/sleep/ignore) dispatch correctly, the sleep plugin-override and generic echo mem fallback both work, and unknown/plugin actions route through powerbutton.d/.

@elibosley

Copy link
Copy Markdown
Member Author

Update — Sleep is now plugin-provided, not a core option (commit b23289324)

Dropped the built-in Sleep option and its generic echo mem suspend. Rationale: a core suspend bypasses the array/Docker/VM/wake prep the S3 Sleep plugin does and is hardware-fragile, and Unraid intentionally ships S3 sleep as a plugin rather than core.

Core now offers only the trivially-safe primitives: Shut down, Reboot, Do nothing. Sleep is added by the S3 Sleep plugin via the same extension hook as any other plugin action (/etc/acpi/powerbutton.d/sleep + powerbutton-option.php). The handler already routes powerbutton="sleep" through the plugin dir, so no special-casing remains in core.

elibosley and others added 4 commits June 19, 2026 10:36
Rename the "Power Mode" settings page to "Power Options" and turn it into
a tabbed page with two tabs:

- Power Mode  - the existing CPU governor controls (moved verbatim)
- Power Button - new setting to choose what the physical power button does

The power button behavior (Shut down / Do nothing) is stored in the
[powermode] section of dynamix.cfg and read live by the ACPI handler on
each press, so changes take effect without a reload or reboot.

A setting-aware ACPI handler (etc/acpi/unraid_power_handler.sh) is
installed over the stock Slackware acpi_handler.sh at boot by rc.acpid,
so it reliably overrides the default poweroff regardless of package
install order. This mirrors how rc.cpufreq applies the [powermode]
governor setting at boot.

Closes OS-463

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A physical power press fires both an ACPI event (handled by acpid) and an
input key event handled by elogind, whose default HandlePowerKey=poweroff
shuts the system down regardless of the acpid handler. Ship a
logind.conf.d drop-in setting HandlePowerKey/HandlePowerKeyLongPress to
ignore so acpid is the sole authority over the power button, making the
"Do nothing" Power Options setting actually take effect.

Verified on hardware: elogind D-Bus HandlePowerKey now reports "ignore".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Power Button now offers Shut down, Reboot, Sleep (suspend, shown only when
the kernel supports suspend-to-RAM), and Do nothing.

Plugins can add or override actions without touching core:
- WebUI: PowerButton.page includes plugins/*/include/powerbutton-option.php
  (and powerbutton-extra.php for action-specific fields).
- Action: unraid_power_handler.sh dispatches to /etc/acpi/powerbutton.d/<action>
  for non-builtin actions, and the builtin "sleep" defers to
  powerbutton.d/sleep when present (so the S3 Sleep plugin can do array-aware
  prep). Contract documented in /etc/acpi/powerbutton.d/README.

This lets the User Scripts plugin add a "Run script" action in its own repo
rather than coupling it into core.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the built-in Sleep option and its generic echo-mem suspend. A core
suspend bypasses the array/Docker/VM/wake prep the S3 Sleep plugin performs
and is hardware-fragile, and Unraid intentionally ships S3 sleep as a plugin.

Core now offers only the trivially-safe primitives (Shut down, Reboot, Do
nothing). Sleep is added by the S3 Sleep plugin via the same extension hook
used for other plugin actions: it ships /etc/acpi/powerbutton.d/sleep and a
powerbutton-option.php fragment. The handler already routes powerbutton="sleep"
through the plugin dir.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@elibosley elibosley force-pushed the feature/os-463-power-options branch from b232893 to 1e7413b Compare June 19, 2026 14:37
elibosley added a commit that referenced this pull request Jun 23, 2026
…rop shared-file edits

- Save mode is now much faster: pipe the single-pass streaming zip
  (scripts/flash_backup) into the destination file instead of appending each
  top-level item to a growing archive (one zip invocation vs many).
- On completion the save job raises an Unraid notification (the tray bell) with a
  click-to-download link, exposes the saved file for download via a docroot
  symlink, and the GUI shows a Download button next to the saved path.
- Revert the helptext.txt and .gitignore edits to base so this PR touches only
  its own files. helptext.txt was the single file shared with other in-flight PR
  test plugins (#2671, #2673), which made the stacked PR-plugin patch fail to
  apply; with it gone, #2677 stacks cleanly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant