Skip to content

feat(Internal_Boot): add initial boot options#2538

Draft
SimonFair wants to merge 19 commits intounraid:masterfrom
SimonFair:Feat(Internal_Boot)-add-boot-pages-and-options
Draft

feat(Internal_Boot): add initial boot options#2538
SimonFair wants to merge 19 commits intounraid:masterfrom
SimonFair:Feat(Internal_Boot)-add-boot-pages-and-options

Conversation

@SimonFair
Copy link
Contributor

@SimonFair SimonFair commented Jan 30, 2026

Summary by CodeRabbit

  • New Features

    • "Boot Device Settings" page with flash backup workflow and registration/blacklist notices.
    • Full GRUB support for boot parameters (parsing, previews, validation, backup/restore) and GRUB-aware server handling.
    • New Boot Pool creation UI with Activate and Activate+Reboot actions.
    • Disk discovery and duplicate-detection APIs.
  • Enhancements

    • Boot menu labeling clarified; device/pool status view enriched with pool-aware metrics, links, and SMB warnings.
  • Bug Fixes

    • Guard added to prevent errors when a device form is missing.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 30, 2026

Walkthrough

Adds a Boot Device Settings page with backup/cleanup flow; implements GRUB support across BootParameters UI, backend handler, and management script; enriches device↔pool mapping and rendering and block-device helpers; extends CacheDevices boot‑pool UI; and guards DeviceInfo form access.

Changes

Cohort / File(s) Summary
Boot UI pages
emhttp/plugins/dynamix/BootInfo.page, emhttp/plugins/dynamix/BootMode.page
Adds "Boot Device Settings" page (flash vendor/product/GUID, backup → Download.php, cleanup retry logic) and updates BootMode menu label to include Boot:4.
Boot Parameters UI (GRUB support)
emhttp/plugins/dynamix/BootParameters.page
Adds bootloaderType, parseGrubConfig/parseBootloaderConfig, GRUB-aware parse/compare/preview/apply flows, GRUB-specific validation and diff/rendering while preserving Syslinux paths.
Backend handler (PHP)
emhttp/plugins/dynamix/include/boot_params_handler.php
Read/write/restore now detect/handle grub vs syslinux configs, validate GRUB content (menuentry/linux/initrd) and accept GRUB-style backup names; read_config returns bootloader_type.
Management script (bash)
emhttp/plugins/dynamix/scripts/manage_boot_params.sh
Adds detect_bootloader()/BOOTLOADER_TYPE, GRUB parsing/updating helpers, timeout extraction, GRUB read/write/backup/list/restore/delete paths, kernel-arg builders, GRUB-aware validation and public-like helpers.
Device ↔ Pool mapping & rendering
emhttp/plugins/dynamix/nchan/device_list
Large additions: device→pool mapping, pool-first-member/boot-disk selection, boot/data metrics, HTML helpers (pool_status_html, pool_function_row), flash_smb_warning_html, extended device_info/array_online/array_offline signatures with options, and state tracking.
Block-device helpers
emhttp/plugins/dynamix/include/Helpers.php
New helpers: get_block_devices(), sysfs_read(), udev_property(), get_disk_identity(), find_duplicate_disks_json() for device enumeration, sysfs/udev reads, identity extraction, and duplicate detection.
Cache Devices UI: Boot Pool flow
emhttp/plugins/dynamix/CacheDevices.page
New boot-pool creation flow and modal (addBootPoolPopupNew, bootPoolInit, bootPoolRenderSlots, bootPoolSyncDeviceOptions), templates and slot/device selection sync; updated option-removal logic.
DeviceInfo guard
emhttp/plugins/dynamix/DeviceInfo.page
Adds a null-check around form usage to avoid errors when form is absent.

Sequence Diagram(s)

sequenceDiagram
  participant Browser as Browser
  participant UI as Web UI (JS)
  participant PHP as emhttp PHP
  participant Script as manage_boot_params.sh
  participant FS as Filesystem

  Browser->>UI: user action (apply params / start backup / create boot pool)
  UI->>PHP: POST apply / request backup / create boot pool
  PHP->>Script: invoke bootloader-aware operation (read/write/list/restore)
  Script->>FS: read/update grub.cfg or syslinux.cfg, create/remove backups, update defaults
  FS-->>Script: operation result
  Script-->>PHP: result/status (includes bootloader_type, backup path)
  PHP-->>UI: response (success/error, download URL)
  UI->>Browser: show preview/result or redirect to backup ZIP
  Browser->>UI: on backup completion trigger cleanup
  UI->>PHP: request unlink backup file
  PHP->>FS: remove zip
  FS-->>PHP: confirmation
  PHP-->>UI: cleanup confirmation
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

7.3

Poem

🐇 I hopped through grub and syslinux tides,

zipped the flash while guarding tiny guides,
mapped pools to disks with careful paws,
slots and backups stitched without a pause,
the little rabbit cheers — smooth boots and bright rides!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 85.00% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title 'feat(Internal_Boot): add initial boot options' accurately describes the main change—introducing boot-related UI pages and configuration management features for an Internal_Boot system component.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions
Copy link

github-actions bot commented Jan 30, 2026

🔧 PR Test Plugin Available

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

Version: 2026.02.06.1632
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-2538/webgui-pr-2538.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

📝 Modified Files:

Click to expand file list
emhttp/plugins/dynamix/BootInfo.page
emhttp/plugins/dynamix/BootMode.page
emhttp/plugins/dynamix/BootParameters.page
emhttp/plugins/dynamix/CacheDevices.page
emhttp/plugins/dynamix/DeviceInfo.page
emhttp/plugins/dynamix/include/Helpers.php
emhttp/plugins/dynamix/include/boot_params_handler.php
emhttp/plugins/dynamix/nchan/device_list
emhttp/plugins/dynamix/scripts/manage_boot_params.sh

🔄 To Remove:

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

plugin remove webgui-pr-2538

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

@SimonFair SimonFair marked this pull request as draft January 30, 2026 14:31
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@emhttp/plugins/dynamix/BootInfo.page`:
- Around line 58-60: The external anchor element linking to
"https://unraid.net/contact" with text '_(Contact Support)_' uses
target="_blank" and should include rel="noopener noreferrer" to prevent
tab-nabbing; update that <a> tag in BootInfo.page (the anchor with href
"https://unraid.net/contact" and text '_(Contact Support)_') to add
rel="noopener noreferrer" while keeping target="_blank".
- Around line 47-54: The template currently prints raw device metadata via
$var['flashVendor'], $var['flashProduct'], and $var['flashGUID']; escape these
values before rendering to prevent HTML injection by applying an HTML-escaping
function (e.g., htmlspecialchars with ENT_QUOTES and UTF-8) wherever these
variables are output in BootInfo.page so the rendered text is safe even if
descriptors contain markup or quotes.

In `@emhttp/plugins/dynamix/BootParameters.page`:
- Around line 1917-1919: Normalize the bootloader type string by lowercasing the
value assigned to bootloaderType (from config.bootloader_type) before any
comparisons; update the assignment for bootloaderType and ensure all downstream
checks that use bootloaderType (e.g., any conditionals or switch statements
referencing bootloaderType) operate on the normalized lowercase value so inputs
like "GRUB" or "Syslinux" match correctly against "grub" and "syslinux".
- Around line 1244-1285: The dropdown currently uses deciseconds (timeoutDeci /
timeoutOptions) for option values causing a 10x mismatch on submit; update the
value logic so the <select id="timeout-dropdown"> stores seconds (not
deciseconds) that the backend expects: convert parsedTimeout (from set timeout=)
back to seconds for the selected value, build option .val() using
Math.round(deci/10) (or keep a data attribute like data-deci if you must
preserve precision) and ensure the customOption uses the original parsed
seconds; keep UI labels in seconds via formatSeconds but submit seconds, and
ensure markFormModified() remains on change.

In `@emhttp/plugins/dynamix/scripts/manage_boot_params.sh`:
- Around line 288-290: Replace regex matching of GRUB menuentry labels with
AWK's literal substring matching: wherever the script sets in_section using the
regex operator against "$0" and the quoted label (e.g., the block matching
"menuentry" that assigns in_section = ($0 ~ "\"" label "\"")), change the logic
to use AWK's index() to test for the literal substring of "\"" label "\""
instead of "~" so parentheses in labels are treated literally; apply the same
replacement for the analogous assignments around the other occurrences
referenced (the blocks around the symbols handling menuentry, label, and
in_section at the other two locations).
- Around line 291-295: The AWK uses the GNU-only \s in gsub and should be made
POSIX-compliant: in the block matching linux/linuxefi (the match(...) line and
the subsequent gsub(...)), replace instances of \s with the POSIX whitespace
class [[:space:]] (e.g. change gsub(/^\s+|\s+$/, "", args) to
gsub(/^[[:space:]]+|[[:space:]]+$/, "", args)); keep the existing [ \t] regex
for explicit space/tab matching elsewhere.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
emhttp/plugins/dynamix/nchan/device_list (1)

551-622: ⚠️ Potential issue | 🟠 Major

Remove duplicated device cells in the DISK_NP branch.
There are four identical <td> outputs, which breaks table column alignment.

🧩 Suggested fix
     if (in_array(_var($disk,'name'),$pools) || $fstype=='zfs') {
       $echo[] = "<td>".device_info($disk,true,$poolName,$poolstatusData,$options)."</td>";
-        $echo[] = "<td>".device_info($disk,true,$poolName,$poolstatusData,$options)."</td>";
-        $echo[] = "<td>".device_info($disk,true,$poolName,$poolstatusData,$options)."</td>";
-        $echo[] = "<td>".device_info($disk,true,$poolName,$poolstatusData,$options)."</td>";
       $echo[] = "<td><a class='static'><i class='icon-disk icon'></i><span></span></a><em>".($fstype=='zfs' ? _('Not present') : _('Not installed'))."</em></td>";
       $echo[] = "<td colspan='4'></td>";
       $echo[] = $showFsInfo ? fs_info($disk,true) : "<td colspan='4'></td>";
     }

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@emhttp/plugins/dynamix/nchan/device_list`:
- Around line 628-637: In the DISK_NP case remove the duplicated device_info()
calls so only a single
device_info($disk,true,$poolName,$poolstatusData,$options) is appended to the
$echo array for the device info column; ensure the remaining line produces
exactly one <td> worth of output and keep the subsequent lines that append the
"Not present/Not installed" <td>, colspan placeholders and fs_info() logic
untouched so the table HTML remains valid.
🧹 Nitpick comments (3)
emhttp/plugins/dynamix/include/Helpers.php (1)

1686-1700: Function only returns SATA/SAS disks, excluding NVMe and other device types.

The regex /^sd[a-z]+$/ filters out NVMe devices (nvme*), virtio devices (vd*), and other block device types. Since this is used for duplicate disk detection, systems with NVMe drives won't have duplicates detected for those devices.

Consider expanding the pattern to include other device types:

♻️ Proposed fix to include NVMe and other device types
 function get_block_devices(): array
 {
     $out = [];
     exec('lsblk -ndo NAME 2>/dev/null', $out);

     $devs = [];
     foreach ($out as $l) {
-        if (preg_match('/^sd[a-z]+$/', trim($l))) {
+        if (preg_match('/^(sd[a-z]+|nvme\d+n\d+|vd[a-z]+|hd[a-z]+)$/', trim($l))) {
             $devs[] = trim($l);
         }
     }

     sort($devs);
     return $devs;
 }
emhttp/plugins/dynamix/nchan/device_list (2)

385-393: Remove commented-out debug code.

Line 390 contains commented-out debug code that should be removed before merging.

🧹 Proposed cleanup
   if (!$online && isset($duplicate_devices[$fullDevName])) {
     $duplicate  = isset($duplicate_devices[$fullDevName]['other_devices']) ? implode("<br>", $duplicate_devices[$fullDevName]['other_devices'])."<br>" : "";
-    #$duplicate = json_encode($duplicate_devices[$fullDevName]);
     $isduplicate = isset($duplicate_devices[$fullDevName]) ? "true" : "false";
     $warndup .= "<a class='info'><i class='fa fa-clone fa-fw orange-text'></i><span>".$duplicate._('Duplicate device identifier detected (WWID or Serial)')."&nbsp;</span></a>";
   }

391-391: Unused variable $isduplicate.

The variable $isduplicate is assigned but never used. Consider removing it.

🧹 Proposed cleanup
   if (!$online && isset($duplicate_devices[$fullDevName])) {
     $duplicate  = isset($duplicate_devices[$fullDevName]['other_devices']) ? implode("<br>", $duplicate_devices[$fullDevName]['other_devices'])."<br>" : "";
-    $isduplicate = isset($duplicate_devices[$fullDevName]) ? "true" : "false";
     $warndup .= "<a class='info'><i class='fa fa-clone fa-fw orange-text'></i><span>".$duplicate._('Duplicate device identifier detected (WWID or Serial)')."&nbsp;</span></a>";
   }

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@emhttp/plugins/dynamix/CacheDevices.page`:
- Around line 119-130: The "_(Activate and Reboot)_" button handler currently
duplicates the "_(Activate)_" logic (calls validate(), submits the form, closes
the dialog) but never triggers a reboot; update the "_(Activate and Reboot)_"
handler in CacheDevices.page so that after validate() it sets a hidden form
field (e.g., add or set input name="reboot" value="true" on the same form) or
otherwise marks the form payload to indicate a reboot before calling
form.submit() and then closing the dialog; locate the handler by the literal
"_(Activate and Reboot)_" string and the validate() /
$(this).find('form').submit() calls to implement the change.
- Around line 355-369: The device label/value logic can produce a leading-space
label and an empty option value when $devId is empty; update the construction in
the loop (variables $devId, $devName, $devLabel inside the foreach) so that if
$devId is non-empty you use $devId as the primary label and set value to $devId
(append " ($devName)" if $devName present), otherwise set both the option value
and label to $devName (no leading space); ensure you still run htmlspecialchars
on the chosen value and label before echoing.
- Around line 316-321: The page currently renders an "Add Bootable Pool" button
twice and a new variant unconditionally; remove the unconditional duplicate and
gate the "Add Bootable Pool New" button behind the same bootEligible check so
both addBootPoolPopup and addBootPoolPopupNew are only output when
_var($var,'bootEligible') == "yes" (remove the lone input with
onclick="addBootPoolPopup()" and wrap the input with
onclick="addBootPoolPopupNew()" inside the existing <?if
(_var($var,'bootEligible')=="yes"):?> ... <?endif;?> block).
🧹 Nitpick comments (1)
emhttp/plugins/dynamix/CacheDevices.page (1)

146-181: Slot rendering logic is well-structured; minor note on event handler rebinding.

The implementation correctly preserves existing selections, clones options from the hidden source, and syncs device availability. One thing to be aware of: bootPoolSyncDeviceOptions rebinding .on('change', …) on line 178 will accumulate handlers if bootPoolRenderSlots is called multiple times without the DOM elements being removed first. However, since line 155 removes .bootpool-device-row elements (destroying old selects) before creating new ones, this is safe in practice.

Consider using event delegation on $container instead, which would avoid re-binding entirely:

Optional: event delegation approach
-  $container.find('select[name="poolDevice[]"]').on('change', function(){
-    bootPoolSyncDeviceOptions(popup);
-  });
+  // Bind once in bootPoolInit instead:
+  // popup.find('#bootPoolDeviceSlots').on('change', 'select[name="poolDevice[]"]', function(){
+  //   bootPoolSyncDeviceOptions(popup);
+  // });

Comment on lines +119 to +130
"_(Activate)_": function() {
if (validate($(this).find('input[name="poolName"]').val())) {
$(this).find('form').submit();
$(this).dialog('close');
}
},
"_(Activate and Reboot)_": function() {
if (validate($(this).find('input[name="poolName"]').val())) {
$(this).find('form').submit();
$(this).dialog('close');
}
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

"Activate and Reboot" performs the same action as "Activate" — reboot logic is missing.

Both button handlers execute identical code (validate → submit form → close dialog). The "Activate and Reboot" button never triggers a reboot, which will mislead users into thinking a reboot was initiated when it wasn't.

You likely need to either set a hidden input (e.g., reboot=true) or issue a separate reboot command after the form submission.

Proposed fix (example approach using a hidden field)
     "_(Activate and Reboot)_": function() {
         if (validate($(this).find('input[name="poolName"]').val())) {
+          $(this).find('form').append('<input type="hidden" name="reboot" value="yes">');
           $(this).find('form').submit();
           $(this).dialog('close');
         }
       },
📝 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
"_(Activate)_": function() {
if (validate($(this).find('input[name="poolName"]').val())) {
$(this).find('form').submit();
$(this).dialog('close');
}
},
"_(Activate and Reboot)_": function() {
if (validate($(this).find('input[name="poolName"]').val())) {
$(this).find('form').submit();
$(this).dialog('close');
}
},
"_(Activate)_": function() {
if (validate($(this).find('input[name="poolName"]').val())) {
$(this).find('form').submit();
$(this).dialog('close');
}
},
"_(Activate and Reboot)_": function() {
if (validate($(this).find('input[name="poolName"]').val())) {
$(this).find('form').append('<input type="hidden" name="reboot" value="yes">');
$(this).find('form').submit();
$(this).dialog('close');
}
},
🤖 Prompt for AI Agents
In `@emhttp/plugins/dynamix/CacheDevices.page` around lines 119 - 130, The
"_(Activate and Reboot)_" button handler currently duplicates the "_(Activate)_"
logic (calls validate(), submits the form, closes the dialog) but never triggers
a reboot; update the "_(Activate and Reboot)_" handler in CacheDevices.page so
that after validate() it sets a hidden form field (e.g., add or set input
name="reboot" value="true" on the same form) or otherwise marks the form payload
to indicate a reboot before calling form.submit() and then closing the dialog;
locate the handler by the literal "_(Activate and Reboot)_" string and the
validate() / $(this).find('form').submit() calls to implement the change.

Comment on lines 316 to +321
<?if (_var($var,'bootEligible')=="yes"):?>
<input type="button" value="_(Add Bootable Pool)_" style="margin:0" onclick="addBootPoolPopup()">
<?endif;?>
<input type="button" value="_(Add Bootable Pool)_" style="margin:0" onclick="addBootPoolPopup()">
<input type="button" value="_(Add Bootable Pool New)_" style="margin:0" onclick="addBootPoolPopupNew()">

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Duplicate / unconditional "Add Bootable Pool" buttons appear to be debug leftovers.

  • Line 317 renders "Add Bootable Pool" conditionally (gated by bootEligible).
  • Line 319 renders an identical "Add Bootable Pool" button unconditionally, bypassing the eligibility check entirely.
  • Line 320 renders "Add Bootable Pool New" also unconditionally.

This means users will always see bootable-pool buttons regardless of whether their system is eligible. If this is intentional for development, it should be removed or gated before merge.

Suggested fix — gate new buttons behind the eligibility check and remove the duplicate
 <?if (_var($var,'bootEligible')=="yes"):?>
 <input type="button" value="_(Add Bootable Pool)_" style="margin:0" onclick="addBootPoolPopup()">
+<input type="button" value="_(Add Bootable Pool New)_" style="margin:0" onclick="addBootPoolPopupNew()">
 <?endif;?>
-<input type="button" value="_(Add Bootable Pool)_" style="margin:0" onclick="addBootPoolPopup()">
-<input type="button" value="_(Add Bootable Pool New)_" style="margin:0" onclick="addBootPoolPopupNew()">
📝 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
<?if (_var($var,'bootEligible')=="yes"):?>
<input type="button" value="_(Add Bootable Pool)_" style="margin:0" onclick="addBootPoolPopup()">
<?endif;?>
<input type="button" value="_(Add Bootable Pool)_" style="margin:0" onclick="addBootPoolPopup()">
<input type="button" value="_(Add Bootable Pool New)_" style="margin:0" onclick="addBootPoolPopupNew()">
<?if (_var($var,'bootEligible')=="yes"):?>
<input type="button" value="_(Add Bootable Pool)_" style="margin:0" onclick="addBootPoolPopup()">
<input type="button" value="_(Add Bootable Pool New)_" style="margin:0" onclick="addBootPoolPopupNew()">
<?endif;?>
🤖 Prompt for AI Agents
In `@emhttp/plugins/dynamix/CacheDevices.page` around lines 316 - 321, The page
currently renders an "Add Bootable Pool" button twice and a new variant
unconditionally; remove the unconditional duplicate and gate the "Add Bootable
Pool New" button behind the same bootEligible check so both addBootPoolPopup and
addBootPoolPopupNew are only output when _var($var,'bootEligible') == "yes"
(remove the lone input with onclick="addBootPoolPopup()" and wrap the input with
onclick="addBootPoolPopupNew()" inside the existing <?if
(_var($var,'bootEligible')=="yes"):?> ... <?endif;?> block).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@emhttp/plugins/dynamix/nchan/device_list`:
- Line 395: The tooltip text concatenation currently assigned to $errorLines
(the expression _("Pool name").":".$poolLabel) omits a space after the colon;
update that assignment so the colon is followed by a space (e.g. change the
_("Pool name").":".$poolLabel concatenation to include ": " between the label
and $poolLabel) so tooltips render like "Pool name: Cache".
- Around line 38-44: normalize_device_name currently uses a single regex
'/p?\d+$/' which treats the 'p' as optional and therefore strips the final digit
from NVMe base names like "nvme0n1"; fix by detecting NVMe-style names first
(e.g. preg_match('/^nvme\d+n\d+/', $device) inside normalize_device_name) and
for those only strip a trailing partition token with a required 'p' (use
preg_replace('/p\d+$/', '', $device)), otherwise keep the existing behavior for
non-NVMe devices (use preg_replace('/p?\d+$/', '', $device)).
🧹 Nitpick comments (4)
emhttp/plugins/dynamix/nchan/device_list (4)

64-104: Duplicated pool-resolution logic between get_fs_errors and get_fs_error_tooltip.

The device-key normalization → $devicePools lookup → find_pools_for_device_in_status merge → dedup → member-matching loop is copy-pasted across both functions (Lines 64-101 vs Lines 350-399). Extracting a shared helper (e.g., resolve_pools_for_device) would keep the two call sites in sync and reduce ~40 lines of duplication.

Also, array_unique on Line 85 is redundant — $poolsToCheck was already deduplicated on Line 80.


300-348: Consider using an options array for pool_function_row to reduce its 12-parameter signature.

The function has 12 positional parameters, most of which are optional with defaults. This makes call sites hard to read and error-prone (see the 12-argument invocations at Lines 1047-1063, 1067-1080, 1163-1179, 1192-1205). An $options associative array — like you already use for device_info and array_online — would improve readability and reduce the risk of argument-order bugs.


492-500: Dead code: $isduplicate is assigned but never read; debug line should be removed.

Line 498 sets $isduplicate which is unused. Line 497 has a commented-out debug statement.

Proposed cleanup
   $warndup = '';
   $devName = _var($disk,'device','');
   $fullDevName = "/dev/" . _var($disk,'device', '');
   if (!$online && isset($duplicate_devices[$fullDevName])) {
     $duplicate  = isset($duplicate_devices[$fullDevName]['other_devices']) ? implode("<br>", $duplicate_devices[$fullDevName]['other_devices'])."<br>" : "";
-    #$duplicate = json_encode($duplicate_devices[$fullDevName]);
-    $isduplicate = isset($duplicate_devices[$fullDevName]) ? "true" : "false";
     $warndup .= "<a class='info'><i class='fa fa-clone fa-fw orange-text'></i><span>"._('Duplicate device identifier detected (WWID or Serial)')."<br>$duplicate</span></a>";
   }

1034-1042: Nit: $bootPoolName is computed on Line 1034 then identically reassigned on Line 1040.

Same pattern occurs in the started-state block at Lines 1140 vs 1144. The second assignment can be removed.

Proposed fix (stopped-state block)
       $bootDisk = find_boot_disk_for_pool($pool, $Boot, $Cache);
       $bootPoolName = prefix(_var($bootDisk,'name',''));
       $bootMetrics = get_boot_pool_metrics($Boot, $bootPoolName, $bootDisk);
       $dataMetrics = get_pool_data_metrics($poolDisk, $bootMetrics);
       $poolLabel = ucfirst($pool);
       $summaryName = $poolLabel;
       if (!empty($bootDisk)) {
-        $bootPoolName = prefix(_var($bootDisk,'name',''));
         $bootLabel = ucfirst($bootPoolName);

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@emhttp/plugins/dynamix/BootParameters.page`:
- Around line 1088-1157: The parseGrubConfig function only captures
double-quoted menuentry labels; update the menuentry parsing in parseGrubConfig
to accept both single and double quotes (e.g. match
menuentry\s+["']([^"']+)["']) and strip surrounding quotes from the captured
label so labels like menuentry 'My OS' are parsed correctly; keep the existing
fallback that uses trimmed.substring('menuentry '.length).trim() when no quotes
are present and ensure entry.label is stored unquoted so the defaultValue
comparison (which already strips double quotes) works for single-quoted defaults
as well.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@emhttp/plugins/dynamix/CacheDevices.page`:
- Around line 388-410: The file contains a duplicated HTML template with id
"templatePopupSubpool" which causes invalid duplicate IDs and ambiguous
selectors; fix by either removing this duplicate template block or giving it a
unique id (e.g., change id on the duplicate and update any JS/CSS selectors that
reference "templatePopupSubpool"); ensure any references to the template
(selectors used to query the DOM, event handlers, or code that clones the
template) are updated to the new id, and confirm the remaining template keeps
the form fields (select[name="subpool"], select[name="poolSlots"], and the
hidden inputs) so behavior is unchanged.

In `@emhttp/plugins/dynamix/scripts/manage_boot_params.sh`:
- Around line 287-302: The GRUB menuentry matching only handles double-quoted
labels; update parse_grub_linux_args, update_grub_entry, and update_grub_default
to accept either single or double quotes by changing the pattern that looks for
"\"" label "\"" to a regex that matches either "'"label"'" or "\""label"\"";
ensure the awk/grep/sed expressions and index/match checks account for both
quote characters (e.g., check for index/$0 ~ /['"]label['"]/ or use a regex
alternation) so menuentry 'Label' and menuentry "Label" are both recognized and
processed.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@emhttp/plugins/dynamix/scripts/manage_boot_params.sh`:
- Around line 1154-1157: The current conditional lets GRUB configs skip
validation; update the logic around BOOTLOADER_TYPE, validate_config, temp_file
and target_cfg so GRUB still undergoes a basic sanity check before mv: when
BOOTLOADER_TYPE == "grub" run a minimal validator (e.g., non-empty and contains
"menuentry") — similar to write_config_grub's checks — and only proceed to mv
"$temp_file" "$target_cfg" if that basic check passes (otherwise abort/log an
error).
- Around line 529-539: The reserved-directive check in the function that uses
lower_param with BOOTLOADER_TYPE incorrectly rejects parameters that merely
start with reserved words (e.g., "setpci") because the regexes (used in the GRUB
branch and the syslinux branch) match prefixes like "set" or "menu" without
requiring a separator; update those regexes to only match exact directive names
or the directive followed by a separator (e.g., end-of-string or '='), so modify
the patterns used in the if [[ "$lower_param" =~ ... ]] checks to include a word
boundary or explicit separator (or anchor with ($|=)) for the reserved tokens
before calling error_exit, ensuring legitimate kernel params like setpci are
allowed.
- Around line 1108-1116: The write_config_grub function currently
unconditionally moves the temp file into GRUB_CFG; change it to validate the
temp file (temp_file) before mv: ensure the file is non-empty and contains at
least one "menuentry" (or other expected grub stanza) and only then perform mv
"$temp_file" "$GRUB_CFG"; if validation fails, do not overwrite GRUB_CFG, log an
error (echo or process logger) and exit with non-zero status so callers detect
failure—leave the original GRUB_CFG intact and surface the failure to the
caller.
- Around line 287-302: The AWK snippet in manage_boot_params.sh incorrectly
embeds a literal single-quote sequence "'" label "'" which breaks the outer
single-quoted shell string and causes AWK to search for the literal text " label
"; fix each occurrence by replacing the problematic "'" label "'" embedding with
an escaped single-quote byte sequence such as "\x27" label "\x27" so the AWK
expression index($0, "\"" label "\"") || index($0, "\x27" label "\x27")
correctly matches both double- and single-quoted menuentry names (apply this
change to all three occurrences of the pattern in the AWK/menuentry matching
logic).
🧹 Nitpick comments (9)
emhttp/plugins/dynamix/CacheDevices.page (3)

79-83: bootPoolInit is a no-op for addBootPoolPopup.

templatePopupBootPool (lines 412-431) doesn't contain #bootPoolDeviceSlots or #bootPoolDevicesSource, so bootPoolInitbootPoolRenderSlotsbootPoolSyncDeviceOptions all operate on empty jQuery sets. It also needlessly binds a change listener on poolSlots that re-runs the no-op on every change.

Consider guarding or removing this call for the old flow, keeping bootPoolInit only for addBootPoolPopupNew.


169-171: Attribute selector escaping is incomplete — CSS meta-characters in device values could break the lookup.

val.replace(/"/g, '\\"') only escapes double quotes. If a device ID contains ], \, or other characters meaningful in CSS selectors, the option[value="…"] lookup silently fails and the previously selected value is lost.

Consider using $.escapeSelector() (jQuery 3+) or .filter() for a robust match:

Proposed fix
     if (existing[i]) {
       var val = existing[i];
-      if ($select.find('option[value="' + val.replace(/"/g, '\\"') + '"]').length) {
+      if ($select.find('option').filter(function(){ return $(this).attr('value') === val; }).length) {
         $select.val(val);
       }
     }

412-431: Consider consolidating templatePopupBootPool and templatePopupBootPoolNew.

These two templates share most of their structure (name input, slot select capped at 2, boot size field, same hidden inputs). The "new" template extends the original with device-slot scaffolding. If the intent is for addBootPoolPopupNew to eventually replace addBootPoolPopup, you could simplify maintenance by having a single template and conditionally showing the device section.

This is non-blocking — fine to defer until the flows stabilize.

emhttp/plugins/dynamix/scripts/manage_boot_params.sh (6)

483-506: Minor: Inconsistent JSON indentation.

Lines 485 and 504 use 4-space indentation while lines 486–503 use 2-space indentation. This doesn't break functionality but makes the JSON output inconsistent.


722-728: Simplify: remove unnecessary echo "$(...)".

echo "$(command)" is equivalent to just running the pipeline directly. This avoids an extra subshell and is more idiomatic.

♻️ Suggested simplification
 build_kernel_args() {
-    echo "$(build_append_line | sed 's/^initrd=\/bzroot[ ]*//')"
+    build_append_line | sed 's/^initrd=\/bzroot[ ]*//'
 }
 
 build_kernel_args_gui_safe() {
-    echo "$(build_append_line_gui_safe | sed 's/^initrd=\/bzroot[ ]*//')"
+    build_append_line_gui_safe | sed 's/^initrd=\/bzroot[ ]*//'
 }

1014-1039: BEGIN block placement is unconventional; silent return on missing label may hide config errors.

The BEGIN { idx=0 } on line 1026 is placed after the main rule. AWK executes BEGIN first regardless of position, so this works, but placing it before the main rule is more conventional and readable.

More importantly, if the requested DEFAULT_BOOT_ENTRY label doesn't exist in the GRUB config, the function silently returns (line 1030) without any warning. If a user mistypes a label name, the default won't change and there's no feedback. Consider logging a warning.


1189-1196: ls iteration and unescaped dots in sed are fragile but low-risk.

Line 1189: ShellCheck SC2045 flags for backup in $(ls ...) as fragile. Since backup filenames are tightly controlled (no spaces), this is safe in practice.

Line 1196: The sed pattern uses ${backup_prefix} without escaping dots, so syslinux.cfg matches syslinuxXcfg in theory. Again, no practical risk given the controlled naming.

Both are fine to defer.


1058-1070: ShellCheck SC2155: local masks return values from command substitutions.

Multiple lines in this function (and throughout the file) use local var=$(cmd). Under set -e, if the command fails, local swallows the non-zero exit code. The highest-risk instance is line 1058 where build_kernel_args failure would silently produce empty args that get written to the GRUB config.

The idiomatic fix is to separate declaration and assignment:

local new_args
new_args=$(build_kernel_args)

This pattern applies to lines 530, 636, 711, 1018, 1058, 1059, 1065, 1070, 1149, 1195, and 1196.


1130-1137: Repeated bootloader-type branching for paths could be consolidated.

The pattern of setting target_cfg, target_backup_dir, and backup_prefix based on BOOTLOADER_TYPE is duplicated in write_raw_config, list_backups, restore_backup, and delete_all_backups. Consider extracting this to a helper or setting these as global variables alongside BOOTLOADER_TYPE (near line 39).

💡 Example consolidation
# Near line 39, after BOOTLOADER_TYPE is set:
if [[ "$BOOTLOADER_TYPE" == "grub" ]]; then
    ACTIVE_CFG="$GRUB_CFG"
    ACTIVE_BACKUP_DIR="$GRUB_BACKUP_DIR"
    BACKUP_PREFIX="grub.cfg"
else
    ACTIVE_CFG="$SYSLINUX_CFG"
    ACTIVE_BACKUP_DIR="$BACKUP_DIR"
    BACKUP_PREFIX="syslinux.cfg"
fi

Also applies to: 1174-1179, 1214-1221, 1264-1269

SimonFair and others added 2 commits February 6, 2026 14:16
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@emhttp/plugins/dynamix/scripts/manage_boot_params.sh`:
- Around line 1101-1108: The current conversion of TIMEOUT deciseconds to GRUB
integer seconds in manage_boot_params.sh uses truncation
(timeout_seconds=$((TIMEOUT / 10))) which drops fractional seconds; change this
to round to the nearest second (e.g. compute timeout_seconds with integer
rounding such as timeout_seconds=$(((TIMEOUT + 5) / 10))) before updating
temp_file so a UI value like 15 deciseconds becomes 2 seconds rather than 1;
update the assignment of timeout_seconds and leave the existing sed logic that
writes/updates "set timeout=" unchanged.
🧹 Nitpick comments (5)
emhttp/plugins/dynamix/scripts/manage_boot_params.sh (5)

483-506: Minor: inconsistent JSON indentation.

Lines 485 and 504 use 4-space indentation while all other properties use 2-space. This won't affect parsing but is a cosmetic inconsistency.

-    "bootloader_type": "$BOOTLOADER_TYPE",
+  "bootloader_type": "$BOOTLOADER_TYPE",
-    "timeout": "$timeout"
+  "timeout": "$timeout"

653-717: build_append_line_gui_safe is nearly identical to build_append_line — consider extracting shared logic.

The two functions differ only in whether framebuffer parameters are included (lines 574–577). The rest (~60 lines of PCI merging, power management, custom params, and unraiduuid handling) is duplicated verbatim. A single function with a skip_framebuffer flag would eliminate this maintenance burden.


722-728: Useless echo "$(…)" wrapper — pipe directly.

echo "$(cmd | sed …)" spawns an extra subshell. Simplify:

 build_kernel_args() {
-    echo "$(build_append_line | sed 's/^initrd=\/bzroot[ ]*//')"
+    build_append_line | sed 's/^initrd=\/bzroot[ ]*//'
 }
 
 build_kernel_args_gui_safe() {
-    echo "$(build_append_line_gui_safe | sed 's/^initrd=\/bzroot[ ]*//')"
+    build_append_line_gui_safe | sed 's/^initrd=\/bzroot[ ]*//'
 }

1196-1204: Iterating over ls output is fragile (SC2045); also unescaped dots in sed pattern.

While backup filenames are tightly controlled, for backup in $(ls -t ...) will break on whitespace in paths. A safer alternative:

-    for backup in $(ls -t "$target_backup_dir"/${backup_prefix}.bak.* 2>/dev/null); do
+    while IFS= read -r -d '' backup; do

using find ... -print0 | sort -z -r instead of ls -t.

Additionally, on line 1203, ${backup_prefix} (e.g., grub.cfg) contains literal dots that are unescaped in the sed regex, so they match any character. Low risk here given the naming pattern, but worth noting.


1230-1241: Comments reference only syslinux format, but now GRUB is supported too.

The block comment (lines 1231–1239) documents only the syslinux.cfg.bak.* format/example. Now that grub.cfg.bak.* is also valid, the comments should mention both formats or be made generic. Minor documentation nit.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
emhttp/plugins/dynamix/scripts/manage_boot_params.sh (2)

1230-1241: ⚠️ Potential issue | 🟡 Minor

Stale comments — still reference syslinux-only format.

The comment block (lines 1231–1240) and example only describe syslinux.cfg.bak.* format, but the code now dynamically handles both syslinux and GRUB backup prefixes. Update the comments to reflect the dual-bootloader support.


483-506: ⚠️ Potential issue | 🟡 Minor

Inconsistent JSON indentation — bootloader_type and timeout use 4 spaces, rest uses 2.

Lines 485 and 504 are indented with 4 spaces while all other JSON fields use 2 spaces. This doesn't break parsing but is a visible inconsistency.

✏️ Fix indentation
-    "bootloader_type": "$BOOTLOADER_TYPE",
+  "bootloader_type": "$BOOTLOADER_TYPE",
-    "timeout": "$timeout"
+  "timeout": "$timeout"
🧹 Nitpick comments (4)
emhttp/plugins/dynamix/scripts/manage_boot_params.sh (4)

719-728: Remove unnecessary echo "$(…)" wrapper and consider reducing duplication.

echo "$(cmd)" is a no-op wrapper — the pipeline output is captured and re-echoed. Also, stripping initrd=/bzroot via sed is fragile coupling with build_append_line. A shared internal function that builds args without the initrd prefix would be cleaner; build_append_line would call it and prepend initrd=/bzroot, while build_kernel_args uses it directly.

♻️ Minimal fix (remove UUOC)
 build_kernel_args() {
-    echo "$(build_append_line | sed 's/^initrd=\/bzroot[ ]*//')"
+    build_append_line | sed 's/^initrd=\/bzroot[ ]*//'
 }
 
 build_kernel_args_gui_safe() {
-    echo "$(build_append_line_gui_safe | sed 's/^initrd=\/bzroot[ ]*//')"
+    build_append_line_gui_safe | sed 's/^initrd=\/bzroot[ ]*//'
 }

1196-1203: Minor: Iterating over ls output is fragile (SC2045).

Filenames are controlled by this script so spaces are unlikely, but a glob-based approach would be more robust and avoids the ShellCheck warning.

♻️ Proposed glob-based iteration
-    for backup in $(ls -t "$target_backup_dir"/${backup_prefix}.bak.* 2>/dev/null); do
+    local -a backups=()
+    while IFS= read -r -d '' f; do
+        backups+=("$f")
+    done < <(find "$target_backup_dir" -maxdepth 1 -name "${backup_prefix}.bak.*" -print0 | xargs -0 ls -t 2>/dev/null | tr '\n' '\0')

Alternatively, if time-sorted order is not critical, a simple glob would suffice:

for backup in "$target_backup_dir"/${backup_prefix}.bak.*; do

653-717: ~75 lines duplicated between build_append_line and build_append_line_gui_safe.

These two functions differ only in whether framebuffer parameters are included (lines 574–577). Every other section is identical. Any future parameter change must be applied in both places — a maintenance hazard.

Consider a single function with a flag:

♻️ Sketch
-build_append_line() {
+build_append_line() {
+    local include_framebuffer="${1:-1}"
     local params="initrd=/bzroot"
     ...
-    # 2. Framebuffers
-    [[ "${EFIFB_OFF}" == "1" ]] && params="$params video=efifb:off"
-    ...
+    # 2. Framebuffers (excluded for GUI-safe mode)
+    if [[ "$include_framebuffer" == "1" ]]; then
+        [[ "${EFIFB_OFF}" == "1" ]] && params="$params video=efifb:off"
+        [[ "${VESAFB_OFF}" == "1" ]] && params="$params video=vesafb:off"
+        [[ "${SIMPLEFB_OFF}" == "1" ]] && params="$params video=simplefb:off"
+        [[ "${SYSFB_BLACKLIST}" == "1" ]] && params="$params initcall_blacklist=sysfb_init"
+    fi
     ...
 }
 
-build_append_line_gui_safe() {
-    ... (entire duplicate function removed)
-}
+build_append_line_gui_safe() {
+    build_append_line 0
+}

1058-1070: Optional: local var=$(cmd) masks failures under set -e (SC2155).

With set -e active, local always returns 0 and swallows any command-substitution failure. This pattern appears throughout the new GRUB code (lines 1058, 1059, 1065, 1070, and others). Splitting declaration and assignment would let set -e catch failures:

local new_args
new_args=$(build_kernel_args)

Low risk here since these commands are unlikely to fail silently, but worth noting for correctness under set -e.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@emhttp/plugins/dynamix/CacheDevices.page`:
- Around line 147-188: In bootPoolSizeInit, remove the no-op conditional that
sets $input.val('') when already empty (the check inside syncBootSize) and
simply ensure the custom input is toggled readonly/visibility only (function:
bootPoolSizeInit / syncBootSize; element: $input), change the validation in the
$input 'input change' handler from allowing zero to require positive values
(replace gb >= 0 with gb > 0; references: $input handler, $hidden), and update
the templates that render the poolBootSizeCustomGb numeric input to use HTML min
and max attributes (and remove maxlength) so the browser enforces a valid
numeric range for that input (targets: the poolBootSizeCustomGb input elements
in both template instances).
- Around line 467-497: templatePopupBootPool is missing the device-source and
device-slot containers that bootPoolInit -> bootPoolRenderSlots expects
(`#bootPoolDevicesSource` and `#bootPoolDeviceSlots`), causing slot rendering to
no-op; fix by adding the same device-selection markup (elements with IDs
`#bootPoolDevicesSource` and `#bootPoolDeviceSlots` used by bootPoolRenderSlots)
into templatePopupBootPool so bootPoolRenderSlots can find them, or
alternatively add a guard at the start of bootPoolRenderSlots to early-return if
popup.find('#bootPoolDevicesSource').length===0 ||
popup.find('#bootPoolDeviceSlots').length===0.
🧹 Nitpick comments (1)
emhttp/plugins/dynamix/CacheDevices.page (1)

426-438: Inconsistent inline style between the two boot-size <select> elements.

Line 427 uses max-width: 180px while the equivalent select in templatePopupBootPool (line 483) uses min-width: 180px. This will cause different sizing behavior between the two templates. Unify to one or the other.

Pick one — e.g., min-width in both
-: <span><select name="poolBootSizePreset" class="wide bootpool-size-preset" style="max-width: 180px;">
+: <span><select name="poolBootSizePreset" class="wide bootpool-size-preset" style="min-width: 180px;">

Comment on lines +147 to +188
function bootPoolSizeInit(popup) {
var $select = popup.find('select[name="poolBootSizePreset"]');
var $input = popup.find('input[name="poolBootSizeCustomGb"]');
var $hidden = popup.find('input[name="poolBootSize"]');
var $inputWrap = popup.find('.bootpool-size-custom');
if (!$select.length || !$input.length || !$hidden.length) return;

function syncBootSize() {
var val = $select.val();
if (val === 'custom') {
$input.prop('readonly', false);
$inputWrap.removeClass('hidden');
if ($input.val() === '' || $input.val() === null) $input.val('');
} else {
$input.val('');
$input.prop('readonly', true);
$inputWrap.addClass('hidden');
}
}

var currentMiB = $.trim($hidden.val());
if (currentMiB && $select.find('option[value="' + currentMiB.replace(/"/g, '\\"') + '"]').length) {
$select.val(currentMiB);
} else {
$select.val('custom');
if (currentMiB) {
$input.val(Math.round(parseInt(currentMiB, 10) / 1024));
}
}

$input.on('input change', function(){
var gb = parseInt($input.val(), 10);
if (!isNaN(gb) && gb >= 0) {
$hidden.val(gb * 1024);
} else {
$hidden.val('');
}
});

$select.on('change', syncBootSize);
syncBootSize();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Several issues in bootPoolSizeInit.

  1. Line 159 is a no-op: if ($input.val() === '' || $input.val() === null) $input.val(''); — setting an empty/null input to '' does nothing.
  2. Line 179 allows zero: gb >= 0 means a user can submit a boot reserved size of 0 MiB, which is almost certainly invalid. Use gb > 0.
  3. Line 436 / 492: maxlength has no effect on <input type="number"> per the HTML spec. Use min and max attributes to constrain the value range.
Proposed fixes

For the no-op on line 159:

-      if ($input.val() === '' || $input.val() === null) $input.val('');
+      // no need to reset — input is already empty

For allowing zero on line 179:

-    if (!isNaN(gb) && gb >= 0) {
+    if (!isNaN(gb) && gb > 0) {

For the number input constraints (applies to both templates at lines 436 and 492):

-    <input type="number" name="poolBootSizeCustomGb" maxlength="10" class="narrow"> GB
+    <input type="number" name="poolBootSizeCustomGb" min="1" max="9999999999" class="narrow"> GB
🤖 Prompt for AI Agents
In `@emhttp/plugins/dynamix/CacheDevices.page` around lines 147 - 188, In
bootPoolSizeInit, remove the no-op conditional that sets $input.val('') when
already empty (the check inside syncBootSize) and simply ensure the custom input
is toggled readonly/visibility only (function: bootPoolSizeInit / syncBootSize;
element: $input), change the validation in the $input 'input change' handler
from allowing zero to require positive values (replace gb >= 0 with gb > 0;
references: $input handler, $hidden), and update the templates that render the
poolBootSizeCustomGb numeric input to use HTML min and max attributes (and
remove maxlength) so the browser enforces a valid numeric range for that input
(targets: the poolBootSizeCustomGb input elements in both template instances).

Comment on lines +467 to 497
<div id="templatePopupBootPool" class="template">
<form markdown="1" method="POST" action="/update.htm" target="progressFrame" onsubmit="return validate(this.poolName.value)">
<input type="hidden" name="poolBootable" value="yes">
<input type="hidden" name="changeSlots" value="apply">

_(Name)_:
: <input type="text" name="poolName" maxlength="40" value="<?=count($pools)==0?'cache':''?>">

_(Slots)_:
: <select name="poolSlots">
<?for ($n=1; $n<=2; $n++):?>
<?=mk_option(1,$n,$n)?>
<?endfor;?>
</select>

_(Boot reserved size)_ (_(MiB)_):
: <select name="poolBootSizePreset" class="wide bootpool-size-preset" style="min-width: 180px;">
<option value="8192">8 GB</option>
<option value="16384">16 GB</option>
<option value="32768">32 GB</option>
<option value="65536">64 GB</option>
<option value="131072">128 GB</option>
<option value="custom">_(User defined)_</option>
</select>
<span class="bootpool-size-custom">
<input type="number" name="poolBootSizeCustomGb" maxlength="10" class="narrow"> GB
</span>
<input type="hidden" name="poolBootSize" value="<?=_var($var,'bootSizeDefault')?>">

</form>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for all references to addBootPoolPopup
rg -n 'addBootPoolPopup' --type=php --type=html -g '!node_modules'

Repository: unraid/webgui

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Search for templatePopupBootPool references
echo "=== Searching for templatePopupBootPool ==="
rg -n 'templatePopupBootPool' 

# Search for bootPoolInit to find active boot pool initialization
echo "=== Searching for bootPoolInit ==="
rg -n 'bootPoolInit'

# Search for related boot pool functions
echo "=== Searching for bootPoolRenderSlots ==="
rg -n 'bootPoolRenderSlots'

Repository: unraid/webgui

Length of output: 1008


🏁 Script executed:

#!/bin/bash
# Get context for old flow (around line 82-83)
echo "=== Old flow context (lines 75-95) ==="
sed -n '75,95p' emhttp/plugins/dynamix/CacheDevices.page

# Get context for new flow (lines 100-120)
echo "=== New flow context (lines 100-120) ==="
sed -n '100,120p' emhttp/plugins/dynamix/CacheDevices.page

# Get the new template to see device-slot UI (lines 384-430)
echo "=== New template (lines 384-430) ==="
sed -n '384,430p' emhttp/plugins/dynamix/CacheDevices.page

# Verify old template (already provided, but check around it)
echo "=== Old template vicinity (lines 467-520) ==="
sed -n '467,520p' emhttp/plugins/dynamix/CacheDevices.page

Repository: unraid/webgui

Length of output: 3886


🏁 Script executed:

#!/bin/bash
# Get bootPoolRenderSlots implementation
echo "=== bootPoolRenderSlots function (lines 190-250) ==="
sed -n '190,250p' emhttp/plugins/dynamix/CacheDevices.page

Repository: unraid/webgui

Length of output: 2361


🏁 Script executed:

#!/bin/bash
# Find where addBootPoolPopup and addBootPoolPopupNew are called from
echo "=== Where addBootPoolPopup is called ==="
rg -n 'addBootPoolPopup\(' --type=php --type=html -B 2 -A 1

echo ""
echo "=== Where addBootPoolPopupNew is called ==="
rg -n 'addBootPoolPopupNew\(' --type=php --type=html -B 2 -A 1

Repository: unraid/webgui

Length of output: 142


🏁 Script executed:

#!/bin/bash
# Search for onclick attributes or references to boot pool functions
echo "=== Searching for onclick handlers with boot pool ==="
rg -n "onclick.*[Bb]oot[Pp]ool" --type=php --type=html

# Search for button text mentioning "Add Bootable Pool"
echo ""
echo "=== Searching for Add Bootable Pool mentions ==="
rg -n "Add Bootable Pool" --type=php --type=html -B 2 -A 2

# Search for any reference to these function names in quotes or attributes
echo ""
echo "=== Searching for addBootPool in attributes ==="
rg -n 'addBootPool' --type=php --type=html

Repository: unraid/webgui

Length of output: 209


🏁 Script executed:

#!/bin/bash
# Check file size and general structure
echo "=== File info ==="
wc -l emhttp/plugins/dynamix/CacheDevices.page

# Search for references to CacheDevices.page
echo ""
echo "=== References to CacheDevices.page ==="
rg -n 'CacheDevices.page' --type=php --type=html --type=xml

# Check if boot pool functions are referenced in any other context
echo ""
echo "=== Check for boot pool anywhere in codebase ==="
rg -n 'bootPool' --type=php --type=html | head -20

Repository: unraid/webgui

Length of output: 210


🏁 Script executed:

#!/bin/bash
# Check if both templatePopupBootPool and templatePopupBootPoolNew have duplicate IDs
echo "=== Checking for duplicate element IDs in both templates ==="
sed -n '384,450p' emhttp/plugins/dynamix/CacheDevices.page | grep -n 'id='
sed -n '467,497p' emhttp/plugins/dynamix/CacheDevices.page | grep -n 'id='

# Check the entire file for element IDs to see if there are other duplicates
echo ""
echo "=== All element IDs in CacheDevices.page ==="
rg -n 'id="' emhttp/plugins/dynamix/CacheDevices.page

Repository: unraid/webgui

Length of output: 795


templatePopupBootPool lacks the device-slot selection UI that bootPoolInit expects.

The old flow and new flow both call bootPoolInit(popup), which then invokes bootPoolRenderSlots. That function searches for #bootPoolDeviceSlots and #bootPoolDevicesSource inside the popup (lines 192–193). The new template contains these elements (lines 421 and 399 respectively), but templatePopupBootPool (lines 467–497) does not. When bootPoolRenderSlots runs against the old template, it silently produces zero device slots because jQuery's find() returns an empty object, and the appended DOM elements are lost.

Add the same device-source and device-slots markup to templatePopupBootPool to maintain functional parity with the new template, or guard bootPoolRenderSlots to skip rendering when the containers are absent.

🤖 Prompt for AI Agents
In `@emhttp/plugins/dynamix/CacheDevices.page` around lines 467 - 497,
templatePopupBootPool is missing the device-source and device-slot containers
that bootPoolInit -> bootPoolRenderSlots expects (`#bootPoolDevicesSource` and
`#bootPoolDeviceSlots`), causing slot rendering to no-op; fix by adding the same
device-selection markup (elements with IDs `#bootPoolDevicesSource` and
`#bootPoolDeviceSlots` used by bootPoolRenderSlots) into templatePopupBootPool so
bootPoolRenderSlots can find them, or alternatively add a guard at the start of
bootPoolRenderSlots to early-return if
popup.find('#bootPoolDevicesSource').length===0 ||
popup.find('#bootPoolDeviceSlots').length===0.

@SimonFair SimonFair changed the title feat(Internal_Boot) add initial boot options feat(Internal_Boot): add initial boot options Feb 6, 2026
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