Skip to content

Migrate to AdminLTE 4 / Bootstrap 5#3809

Draft
PromoFaux wants to merge 11 commits into
developmentfrom
new/adminLTE4
Draft

Migrate to AdminLTE 4 / Bootstrap 5#3809
PromoFaux wants to merge 11 commits into
developmentfrom
new/adminLTE4

Conversation

@PromoFaux

@PromoFaux PromoFaux commented Jul 6, 2026

Copy link
Copy Markdown
Member

Summary

Migrates the web interface from AdminLTE 2.4.18 / Bootstrap 3.4.1 to AdminLTE 4 / Bootstrap 5, and upgrades the other frontend dependencies that no longer had a Bootstrap-3-era version to pair with (bootstrap-select → Tom Select, bootstrap-togglebootstrap5-toggle, icheck-bootstrap → native .form-check, DataTables/select2 to their -bs5 builds, plus Font Awesome and remaining vendor bumps).

  • Layout shell (49f6bd0d): .wrapper/.main-header/.main-sidebar/.content-wrapper → AdminLTE4's .app-wrapper/.app-header/.app-sidebar/.app-main/.app-content.
  • Box → Card (b0d95fcd): mechanical but reviewed conversion across all 19 affected templates, including collapse widgets and color variants.
  • Checkboxes/radios (6657d28a): dropped icheck-bootstrap for Bootstrap 5's native .form-check/.form-switch.
  • JS plugin swaps (6deb2e79): DataTables/select2 to -bs5 builds, bootstrap-select rewritten against Tom Select, bootstrap-toggle swapped for the maintained bootstrap5-toggle fork, jquery-confirm/daterangepicker/bstreeview class names updated for BS5.
  • Font Awesome v7 + theme rename (42646493).
  • Tab-switching fix (b049ef9a): Bootstrap 5's Tab component tracks the active state on the link itself, not the surrounding <li>.
  • Regression fixes from manual QA (9f1d2155, afd69379, 075921f4): boxed layout, badge alignment, settings/tools sidebar submenus navigating away instead of expanding, white flash between page loads on the auto theme, day theme rendering dark, a bootstrap5-toggle initialization deadlock that silently broke the Basic/Expert and All/Modified settings toggles, the All Settings tab bar's layout, and several CSP img-src 'self' violations from Bootstrap 5 (and its plugins') inline data: URI icons — re-hosted as local SVGs so nothing is blocked by FTL's webserver CSP.

Why

AdminLTE's maintainers are explicit that v3→v4 has no incremental upgrade path, so this necessarily touches nearly every .lp template. Bootstrap 3 is long past EOL and several of the jQuery plugins we depended on (bootstrap-select, bootstrap-toggle) have no Bootstrap-5-compatible fork from their original maintainers, so those needed replacement rather than a version bump.

Test plan

  • npm run test (prettier + xo) passes with only pre-existing warnings, no new errors
  • Manually verified via Docker + browser walkthrough across dashboard, query log (filters, live update, on-disk toggle), groups (clients/domains/lists), network, settings (DNS/DHCP/API/system/teleporter/privacy/all settings), in both light and dark theme
  • Verified zero browser console errors across all pages after the CSP icon fixes
  • Additional community/maintainer testing on real hardware, given the size of this change

Notes for reviewers

This is a large, mechanical-but-reviewed diff by nature of the v3→v4 jump. Opening as a draft for early feedback on direction before final polish.

PromoFaux and others added 11 commits July 6, 2026 12:13
Swap vendored AdminLTE 2.4.18 + Bootstrap 3.4.1 for AdminLTE 4.1.0 +
Bootstrap 5.3.8 (AdminLTE's dist CSS now bundles Bootstrap, so the
separate Bootstrap stylesheet is dropped; only its JS bundle remains).

Rewrite the page shell (header, header_authenticated, sidebar, footer)
to AdminLTE 4's app-wrapper/app-header/app-sidebar/app-main/app-content
structure and nav-sidebar/nav-treeview markup, and convert the dashboard
(index.lp) box/small-box markup to Bootstrap 5 cards as the first fully
migrated page.

Verified against a live container: fixed a chart.js tooltip lookup that
still targeted the old .box selector, restored the header logo mini/full
swap and sidebar user-panel collapse behavior that AdminLTE 2 used to
provide for free, and resolved a duplicate id="navigation" collision
with AdminLTE 4's own accessibility skip-link script.

Every other page still uses the old box/Bootstrap-3-grid/BS3-plugin
markup and will render incorrectly until the remaining migration phases
land.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Adam Warner <me@adamwarner.co.uk>
Convert the remaining 20 .lp templates from AdminLTE 2's .box/.panel
component family to Bootstrap 5 .card markup, matching the pattern
established in index.lp/queries.lp during the layout migration:
box/box-header/box-title/box-body/box-footer -> card equivalents,
box-info/-danger/-warning etc. -> card-info/-danger/-warning +
card-outline for the non-solid variant, collapsed-box + data-widget
-> collapsed-card + data-lte-toggle="card-collapse" with the dual
expand/collapse icon AdminLTE 4 expects. Also renames the accompanying
Bootstrap-3-isms throughout (col-xs-*, pull-left/right, input-group-addon,
data-toggle/-dismiss/-backdrop/-keyboard, label -> badge, btn-default,
BS3 modal close-button markup).

Fix the JS-side selectors that referenced the old markup and would have
silently broken once the classes changed: charts.js's tooltip anchor
lookup, footer.js's "no visible cards" redirect check, login.js's
2FA/forgot-password card color swap (plus made both boxes consistently
non-solid so the swap doesn't need to touch card-outline), and
settings-advanced.js's dynamically generated settings cards and its
manual Bootstrap 3 "in"/"active" tab-pane class toggling (Bootstrap 5
renamed .in to .show). Rewrite settings-dhcp.js's jQuery-plugin tooltip
delegation (`$("body").tooltip(...)`, `.tooltip("hide")`) as a small
lazily-instantiating delegated listener plus direct bootstrap.Tooltip
calls, since Bootstrap 5 dropped jQuery integration entirely and the
DHCP lease action buttons are added to the DOM dynamically. Restore
Bootstrap 3's .has-error/.has-warning/.has-success form-validation
color feedback in pi-hole.css, since Bootstrap 5 dropped those classes
in favor of .is-invalid/.is-valid and a couple of pages still toggle
the old class names for simple color-coded feedback.

Refresh package-lock.json for the admin-lte/bootstrap version bump from
the previous commit.

Verified against a live container: dashboard, query log (incl. the
collapsible advanced-filter card and populated DataTable), groups/lists,
groups/domains, and settings/dns all render correctly with zero console
errors; card-collapse toggling confirmed working on the query log's
filter card. Plugin-styled elements (select2/bootstrap-select dropdowns,
DataTables chrome, icheck checkboxes, bootstrap-toggle switches, nav-tabs)
still look wrong until the JS-plugin-swap phase lands, as expected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Adam Warner <me@adamwarner.co.uk>
…rm-check

Convert all 35 static checkbox/radio inputs across 9 templates (queries,
search, settings-api, settings-dhcp, groups-domains, settings-dns,
settings-privacy, settings-teleporter) plus the dynamically-generated
ones (settings-advanced.js's per-setting boolean/enum controls,
settings-dns.js's DNS-upstream picker) from icheck-bootstrap's
JS-post-processed styling to Bootstrap 5's native .form-check/
.form-check-input/.form-check-label markup, which needs no JS at all.

Remove footer.js's applyCheckboxRadioStyle() (and its call sites in
settings-advanced.js/settings-dns.js) entirely - it used to strip and
re-add classes on every checkbox/radio's parent after page load, which
would have destroyed the new static .form-check classes at runtime.
Drop the icheck-bootstrap dependency, its vendor CSS and the <link> in
header.lp, and replace its now-dead pseudo-element checkbox styling in
pi-hole.css with a couple of plain .form-check-input/.form-check-label
rules for the one spot that needs a smaller checkbox size (the domain
list's filter-by-type row).

Two "no-icheck"-excluded checkboxes (a table-header select-all in both
settings-dns.lp and settings-dns.js) are left as bare native checkboxes,
matching their original deliberate exclusion.

Also confirmed pi-hole's own text-red/text-green/bg-*/etc. color utility
classes are unaffected by the AdminLTE core removal - each theme file
already defines them fully rather than only overriding an AdminLTE base,
so no fix was needed there.

Verified against a live container: settings/dns (checkbox/radio styling,
custom-servers card-collapse toggle) and settings/privacy (privacy-level
radios) render correctly with zero console errors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Adam Warner <me@adamwarner.co.uk>
- DataTables: replace the combined BS3 download-builder bundles with
  the official core + Bootstrap 5 styling-adapter package pairs
  (datatables.net(-buttons/-select) + datatables.net-bs5/-buttons-bs5/
  -select-bs5), loaded core-then-adapter.
- bootstrap-select -> Tom Select: bootstrap-select has no Bootstrap 5
  build. Add utils.createGroupSelect(), a small wrapper around Tom
  Select that recreates bootstrap-select's actionsBox (Select all/
  Select none) via a couple of buttons injected into the dropdown, and
  use it for all four "assign to group(s)" multi-selects (groups,
  groups/clients, groups/domains, groups/lists). Two behavioral fixes
  were needed to match the old widget: `hideSelected: false` (Tom
  Select refuses to open its dropdown once every option is already
  selected, which made the actions box unreachable) and
  `dropdownParent: "body"` (otherwise the dropdown is clipped by the
  surrounding .table-responsive/.card ancestors' overflow, same reason
  bootstrap-select was configured with container: "body").
- bootstrap-toggle -> bootstrap5-toggle: drop-in replacement, same
  $.fn.bootstrapToggle() API and onstyle/offstyle option names (all
  call sites already used Bootstrap 5-valid color names).
- select2: bump to 4.1.0 and add the select2-bootstrap-5-theme package
  for correct Bootstrap 5 styling; wire theme: "bootstrap-5" into both
  call sites.
- Remove now-dead bootstrap-select cleanup code (the
  "$('body > .bootstrap-select.dropdown').remove()" DataTables
  drawCallback lines across 8 files) and dead icheck/bootstrap-select
  CSS in pi-hole.css.

Also fixed a Phase 1 regression surfaced by this pass: utils.js's
loadingOverlay() still targeted the old ".wrapper" class instead of
".app-wrapper", throwing whenever a settings page triggered the
Save & Apply overlay.

Verified against a live container: groups/clients' per-row Tom Select
multiselect (open dropdown, Select all/none, Apply, restore-on-cancel),
bootstrap5-toggle switches on groups, select2 filters on the query log,
and DataTables' select/export button row on groups/lists all work with
zero console errors. Settings pages' Save & Apply overlay confirmed
fixed and no longer throwing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Adam Warner <me@adamwarner.co.uk>
Bump @fortawesome/fontawesome-free 6.7.2 -> 7.3.0 (the only remaining
outdated dependency; everything else was already at latest). Drop the
now-unused .ttf webfonts - FA7's dist CSS only references .woff2.

Rename the same AdminLTE2/Bootstrap3 selectors across all 6 theme files
under style/themes/ that were already renamed everywhere else in this
migration: .main-header/.main-sidebar/.main-footer -> .app-header/
.app-sidebar/.app-footer, .wrapper -> .app-wrapper, .content-wrapper ->
.app-main (this one predates AdminLTE 4 entirely and no longer exists
anywhere - confirmed dead in the current markup), .treeview-menu ->
.nav-treeview, sidebar .header list items -> .nav-header, and the full
box/panel -> card component family (including collapsed-box -> collapsed-
card and the color/state variants). Also removed now-dead bootstrap-select/
icheck-bootstrap selectors that can never match anything anymore.

All 7 theme options (default-auto/-light/-dark/-darker, high-contrast,
high-contrast-dark, lcars) are genuinely selectable in Settings -> Web
interface/API, confirmed via the live theme dropdown - none of these
files were dead code, despite "default-darker" not being referenced by
name anywhere else in this repo (themes are loaded via string
concatenation of a backend-supplied name).

Removed pi-hole.css's ~210-line hand-rolled polyfill of Bootstrap 4/5's
margin/padding/display utility classes (.m-0.../.py-3/.d-none) - these
exist natively in Bootstrap 5 now (confirmed present with identical
values in the vendored adminlte.min.css), so the polyfill was pure dead
weight.

Verified against a live container: switched the active theme through
default-darker, lcars, and high-contrast and reloaded the dashboard for
each - all three render correctly (LCARS's very custom skin in
particular came through intact) with zero console errors. FA7 icons
render correctly across the sidebar and dashboard.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Adam Warner <me@adamwarner.co.uk>
Found via manual verification of every page: two places set up tabs
using the Bootstrap 3 pattern of putting the "active" state on the <li>
wrapper (and, for the domains page, only in aria-expanded) rather than
on the <a>/tab-link element itself. Bootstrap 5's Tab component tracks
the active tab exclusively via the "active" class on the link, so with
neither page marking any link "active" at load, Tab.js could never find
a previous tab to deactivate - clicking a different tab just added a
second active pane on top of the first instead of replacing it.

- groups-domains.lp: the Domain/RegEx filter tabs. Added class="active"
  to the initially-selected tab's <a>, and swapped the stale Bootstrap
  3/4 "in" class for "show" on its pane (harmless leftover, but dead
  code once "active" is what actually matters). Also fixed
  groups-domains.js's addDomain(), which was reading
  aria-expanded="true" to figure out which tab is active - Bootstrap 5
  never touches that attribute for plain tabs (only aria-selected/the
  active class), so that lookup would have always resolved to the
  Domain tab even when the RegEx tab was showing, silently misfiling
  regex entries as exact-domain ones. Switched it to read the .active
  class instead.
- settings-advanced.js: the All Settings page's topic pills
  (DNS/DHCP/NTP/etc.). Same fix - add "active" to the first pill's <a>
  in both the initial-load and "only changed settings" code paths.

Verified against a live container: groups/domains now correctly shows
only one tab's content at a time and addDomain() resolves to the right
tab; settings/all now correctly switches between topic panes instead of
stacking them, confirmed via the pane class list before/after a real
click. Zero console errors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Adam Warner <me@adamwarner.co.uk>
…sting

Reviewed and built on top of a round of fixes already applied to the
working tree (boxed layout, badge alignment, sidebar treeview links,
bootstrap5-toggle's on/off->onlabel/offlabel rename, and forcing
data-bs-theme explicitly instead of leaving Bootstrap 5 to fall back to
the OS's prefers-color-scheme, plus the new scripts/js/theme.js that
locks the resolved theme so AdminLTE's own color-mode logic can't
silently override Pi-hole's own theme selection). On top of that:

- Fixed the "day theme still shows dark" gap: the anti-FOUC inline
  background style in header.lp only covered the case where the theme
  is unconditionally dark, never the "auto" theme (whose Lua-side flag
  is literally the string "auto", not "dark") - added a
  prefers-color-scheme media query fallback so the auto theme also
  avoids a white flash without needing to wait for theme.js/CSS to load.
- Fixed a strict Content-Security-Policy (img-src 'self', set by FTL's
  webserver) blocking every checkbox/radio checkmark, the modal close
  button, and the select2 dropdown arrow/clear icon: Bootstrap 5 and its
  plugins ship these as inline data: URI SVGs, which the CSP rejects
  outright. Re-hosted the exact same icons as local files under
  style/icons/ and pointed the same CSS custom properties at them.
  Learned the hard way that a url() stored in a custom property
  resolves relative to the stylesheet that *consumes* it via var(), not
  the one that declares it - the consumer here is always
  vendor/adminLTE/adminlte.min.css, so the override paths needed to
  account for that rather than being relative to pi-hole.css itself.
- Found and fixed three more leftover .box-* selectors in pi-hole.css
  that never got updated to .card-* during the earlier card-conversion
  phase: the query-types/forward-destinations pie chart flex layout,
  the domains-list filter row header, and the dynamically-generated
  settings cards' sizing/hidden-state selectors - all on pages that
  render in normal use, unlike the handful of genuinely-unreachable
  .box-solid/.box-comment selectors left alone in the theme files
  (those modifier classes are never applied by any current markup).

Verified against a live container: boxed layout now visibly constrains
and centers the page, day theme renders light content with checkmarks/
close icons/dropdown arrows all visible with zero console errors,
clicking Settings/Tools now expands the submenu instead of navigating
to the dashboard, and sidebar badges are aligned to a consistent right
edge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Adam Warner <me@adamwarner.co.uk>
bootstrap5-toggle defers building the switch until a ResizeObserver
sees the source checkbox's parent report non-zero dimensions. The
pre-hide rule that kept #expert-settings/#only-changed as raw
display:none checkboxes (to avoid a flash of unstyled input) collapsed
that parent to 0x0, so the observer's condition could never be met and
the toggle silently never rendered on any settings page. The vendor
CSS already hides the checkbox once it's wrapped (.toggle > input),
so the extra pre-hide rule is no longer needed.

Also restore the old tab bar's full-width, gapless layout on the All
Settings page: the tab links only had .btn (inline-block, sized to
content), so a lone item wrapped onto its own row sat as a small
centered pill instead of filling the row like Bootstrap 3's implicit
`.nav > li > a { display: block }` used to.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Adam Warner <me@adamwarner.co.uk>
AdminLTE4's bundled Bootstrap 5 CSS sets --bs-form-select-bg-img (the
chevron background image on .form-select) to an inline data: URI SVG,
which FTL's img-src 'self' CSP silently blocks. DataTables' Bootstrap 5
integration adds .form-select to its own "entries per page" <select>
on every table it builds, so this hit every DataTables-driven page
(query log, DHCP leases, DNS/CNAME record tables, group tables, etc.)
even though no template in this repo uses .form-select directly.

Re-host the light- and dark-theme arrow icons as local files and point
the custom property at them, same approach as the existing checkbox/
radio/close-button fixes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Adam Warner <me@adamwarner.co.uk>
editorconfig-checker requires comment continuation lines to be
indented in multiples of the file's indent size (4 for .lp, 2 for
.css) - several block comments added during the AdminLTE4 migration
used visual alignment under the opening delimiter instead, tripping
the check. Reflowed them to a flat, spec-compliant indent.

Also fix a spell-check false-negative turned real hit: codespell flags
"pre-selected" as a common misspelling of "preselected".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Adam Warner <me@adamwarner.co.uk>
Bootstrap 3 had two separate legacy container concepts - .box and
.panel - that both map onto Bootstrap 5's single .card. The box/panel
to card conversion renamed each occurrence independently, so wherever
a theme file already styled one of them under the other's name (or a
pre-existing .card rule for the advanced-settings cards collided with
a freshly renamed .box/.panel), the result was two separate rule
blocks for the same selector - flagged by CodeFactor's
no-duplicate-selectors check.

Merged each pair into a single rule, keeping whichever value wins the
cascade in the current (duplicated) state so the resolved styles are
unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Adam Warner <me@adamwarner.co.uk>
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