feat: Tailwind v4 + FlyonUI admin UI theme#615
Merged
Conversation
… [skip ci] Phase 0 of the admin UI reskin (Bootstrap 4/SB-Admin-2 -> Tailwind v4 + FlyonUI, SSR + jQuery retained): - devDependencies (exact pins, all MIT): tailwindcss, @tailwindcss/cli, flyonui, @iconify/tailwind4, @iconify-json/tabler. No charting library (apexcharts >= 5.16 has a restrictive dual license and breaks the license gate). - static/css/src/app.tailwind.css: build source with EmailEngine brand themes - light (primary #4C5CC5 / secondary+accent #F8485E from the logo sheet) and dark (lightened variants) - plus the custom component styles ported from app.css/callout.css/emailengine.css, converted to FlyonUI theme tokens so both themes work. - static/css/src/bootstrap-compat.css: un-layered compatibility bridge that keeps unconverted Bootstrap markup rendering after the Bootstrap bundles are removed (grid, BS-scale spacing, display/flex utils, forms, legacy component gaps, Preflight restorations). Shrinks as views convert; deleted at the end of the migration. - PT Sans self-hosted (OFL-1.1): static/fonts/pt-sans/*.woff2 + pt-sans-font.css, following the nunito pattern. - npm run build:css compiles static/css/flyonui.css (committed, bundled by pkg); flyonui.js vendored to static/js/ via copy-static-files.sh like ace/ee-client. - licenses regenerated; gate passes with the new dev-only deps. No template references the new assets yet - no runtime change. Claude-Session: https://claude.ai/code/session_01N4gy7CNzkic59TJZU1X9Yu
Phase 1 of the admin UI reskin. The global chrome is now FlyonUI-native
while every unconverted page keeps working through a two-part
compatibility bridge:
- views/layout/app.hbs rewritten: fixed drawer sidebar (FlyonUI
accordion menu), sticky topbar with search, system-alerts dropdown,
light/dark theme toggle (localStorage + prefers-color-scheme
fallback, pre-paint init via views/partials/theme_init.hbs), user
dropdown, FlyonUI-native tooltips, license/auth warning stat-cards,
overlay trial modal, footer.
- Central component library started: views/partials/ui/{btn, badge,
stat-card, modal, page-header, menu-item, menu-section}.hbs -
parameterized, theme-token-based partials that converted views
compose from; side_menu.hbs is built from menu-item/menu-section.
- static/js/ui.js (new): showToast (FlyonUI markup), uiDismissFade,
flash-alert wiring (alerts.hbs is markup-only now), uiModal
(HSOverlay), uiAutoInit, theme toggle. Loaded by all four layouts.
- static/js/bootstrap-compat.js (new): jQuery Bootstrap shim covering
the plugin calls and data-toggle/data-dismiss markup that unconverted
inline scripts rely on (modal incl. shown/hidden.bs.modal events,
alert incl. closed.bs.alert, collapse with data-parent accordion,
tabs, dropdowns, tooltip/popover bubbles). Stamps legacy dropdown
menus so bootstrap-compat.css can scope its styling; single
delegated ancestor walk per click; tracked open-dropdown/tooltip
state instead of document-wide scans.
- app.js: showToast moved to ui.js; the /admin/changes EventSource is
now gated on body[data-sse-changes] so the login/public layouts stop
opening an authenticated SSE stream that errored on the console.
- workers/api.js: eeColor handlebars helper - single translation point
from Bootstrap-era server color names (danger) to FlyonUI (error);
fixes the previously unstyled bg-danger system-alert icon circles.
- bootstrap-compat.css slimmed after a census sweep: dead SB-Admin
rules dropped, Tailwind-identical color utilities removed, collision
list documented (spacing 3/4/5, .shadow, .container), legacy
dropdown styling scoped to shim-stamped menus.
- e2e: test/e2e/pages-admin.spec.js - per-page smoke suite for the new
theme (shell render, accordion, dropdowns, theme toggle persistence,
toast, login redirect; all assert zero console errors). Shared
ADMIN_PASSWORD now exported from helpers/bootstrap.js.
Verified: lint, format, licenses gate, 1394 unit tests, 6 shell e2e
tests, Playwright screenshots light+dark with zero console errors, and
compat smoke checks (tooltips/modals/collapse) on six unconverted
pages. Security review: no findings.
Claude-Session: https://claude.ai/code/session_01N4gy7CNzkic59TJZU1X9Yu
…with datalists Phase 2 of the admin UI reskin: - All 8 section header partials (accounts/gateways/tokens/templates/ webhooks/security/oauth-config/document-store) now compose from ui/page-header (new icon slot, additive class contract, caller-owned margins). oauth_tabs and the document-store tab strip converted to FlyonUI tabs; the deprecation notice uses ui/stat-card. - add_account_modal rewritten as a ui/modal overlay with ui/form-field and ui/btn; focus/reset behavior moved to FlyonUI open.overlay/ close.overlay events; the two trigger sites (dashboard, accounts index) switched from data-toggle to data-overlay. - bootstrap-autocomplete removed: all four sites now use native datalist suggestions (smtp + imap-proxy listen addresses, accounts/edit sent-mail folder paths, gateway well-known-service picker incl. host/port/secure autofill on input). Script tag dropped from the layout; the $.fn.button version stub removed from the shim. - ui/form-field partial added (label + control + hint + error, is-invalid contract, optional built-in datalist via options param). - Bootstrap neutral-gray "secondary" bridged in compat CSS (btn-secondary/badge-secondary -> neutral) so legacy Cancel buttons and Disabled/Paused state badges do not turn brand-coral; converted views use variant="neutral" (documented in ui/btn and the collision list, which also now spells out the *-[1rem] spacing rule). Deviation from the phase plan: the four large single-consumer form partials (oauth_form, webhook_form, template_form, gateway_form) move to their page batches where per-page e2e verification happens. Verified: datalist suggestions + gateway autofill, modal overlay events (focus, clear-on-close, Escape), converted headers/tabs on live pages with zero console errors; lint, format, 1394 unit tests, 6 shell e2e tests. Security review: no findings. Claude-Session: https://claude.ai/code/session_01N4gy7CNzkic59TJZU1X9Yu
Dashboard batch (per-page pipeline): - views/dashboard.hbs rewritten: warning/welcome cards and all stat tiles compose from ui/stat-card; the three BullMQ queue tiles share a view-local queueCard inline partial (the old third copy had already drifted into broken markup); counters iterate with a |counter| block param so the title="" anti-fallthrough hash cannot mask their fields; responsive grid replaces the Bootstrap row/col layout; the software versions panel is a native details/summary with a rotating chevron (webkit marker hidden for older Safari). - New ui/tooltip partial (FlyonUI hover tooltip, [--placement:*] variants safelisted) replaces ~15 legacy data-toggle tooltips. - lib/ui-routes/dashboard-routes.js now emits full iconify classes for counter icons (safelisted in app.tailwind.css since lib/ is not scanned; coupling documented on both sides). - e2e: dashboard test added to pages-admin.spec.js (stat cards, FlyonUI tooltip on hover, versions panel toggle, zero console errors). Verified: light+dark screenshots with all icons/titles/accents, FlyonUI tooltips working, details toggle, 7 e2e tests green, lint/format/unit (1394) green. Combined cleanup+security review applied (queue-card extraction, producer-side safelist comment, summary affordance); security: no findings. Claude-Session: https://claude.ai/code/session_01N4gy7CNzkic59TJZU1X9Yu
Accounts batch (per-page pipeline; register/* pages move to Phase 4 with their public layout): - accounts/index.hbs: FlyonUI table with row-overlay links (interactive badge/tooltip cells lifted above the overlay so hover popovers and clicks coexist; duplicate overlay anchors removed from the tab order), ui/page-header with action slot, ui/pagination + ui/breadcrumb partials (new). - accounts/account.hbs: toolbar of tooltipped soft buttons (Tabler icons), FlyonUI dropdowns for logs and the gateway delivery test, all five modals on ui/modal, scripts rewired from jQuery Bootstrap events to FlyonUI open.overlay/close.overlay, .test-send-trigger click delegation replaces the Bootstrap relatedTarget contract, export wizard polling modernized (hidden class, FlyonUI badge colors), ee-dl description-list component replaces dl.row grids. - accounts/edit.hbs: composed from ui/form-field and the new ui/checkbox partial; datalist via the shared list param; password placeholders via the new `when` helper. - accounts/browse.hbs: ee-client confirm/alert dialogs on ui/modal + uiModal with once-only close.overlay resolution (fixes the inherited listener accumulation). - test_send.hbs: client-side rendered SPF/DKIM/DMARC results as native details panels; repetitive badge chains collapsed via browser-side statusColor/statusIcon helpers (icons safelisted). - ui/tooltip: inline-partial body fallback removed - it resolved unreliably through nested partial chains and crashed fresh-instance renders with "The partial tooltipBody could not be found"; composed bodies now use the new `concat` handlebars helper. HTML entities in partial hash strings replaced with plain characters (hash params are escaped on output, so entities rendered literally). - Security hardening from review: IMAP ID support-url is only rendered as a link when it passes an https?:// check in account-routes.js (the value is attacker-controllable by a hostile IMAP server, and the old template quoting had silently broken the link). - route-helpers.js: composed type.label for type-icon tooltips. - e2e: four accounts-page tests added (list + add-account overlay focus/reset contract, detail toolbar/modals, edit form, ee-client browse init); account-dependent tests skip on standalone runs. Verified: all pages light+dark with zero console errors against a live account (tooltips, modals, dropdowns, export modal open.overlay flow, delivery-test trigger, datalist, breadcrumbs), 8 e2e tests green, lint/format/licenses, 1394 unit tests. Review applied (S1 + C1-C7); security: one finding, fixed (support-url). Claude-Session: https://claude.ai/code/session_01N4gy7CNzkic59TJZU1X9Yu
Gateways batch (per-page pipeline):
- gateways/index.hbs: FlyonUI table with row-overlay links, ui/page-header
action slot, ui/pagination, info stat-card.
- gateways/gateway.hbs: tooltipped toolbar, ee-dl description list,
delete confirmation on ui/modal; the state badge data-account now uses
the gateway id (was the whole object, so the SSE selector could never
match).
- gateways/new.hbs + edit.hbs: composed from ui/form-field via the
reworked gateway_form partial (well-known-service picker keeps its
basicAutoComplete hook for the datalist autofill), card-header actions
dropdown (Test Connection) converted to a FlyonUI dropdown; verified
live against Ethereal ("Connection successful" toast).
- e2e: full gateway create/view/delete flow test; the suite now logs in
once per run via a storageState captured in beforeAll - per-test
logins were tripping the server's login rate limiter as the suite
grew (manual contexts must pass storageState: undefined because
Playwright fixture options are inherited). Create-redirect wait
anchored to the detail URL to avoid racing the form POST.
Verified: all four pages with a live gateway (row navigation, tooltips,
delete modal, actions dropdown, real SMTP test connection), zero console
errors; 9 e2e tests green in 14s; lint/format; 1394 unit tests. Review
applied (e2e redirect race, badge selector); security: clean.
Claude-Session: https://claude.ai/code/session_01N4gy7CNzkic59TJZU1X9Yu
Tokens + templates batches (per-page pipeline):
- tokens/index.hbs: list with tooltipped token-id + per-row delete
buttons wiring the confirmation modal via dataset, usage panel on the
new ui/details partial.
- tokens/new.hbs: scope checkbox list (or-else-all hooks preserved),
one-time token reveal on ui/modal with the NEW static=true mode -
review caught that the conversion had dropped the old static
backdrop, letting a stray Escape destroy the one-time secret;
ui/modal now supports [--overlay-backdrop:static] +
data-overlay-keyboard="false". Loader swapped to the spinner idiom.
- templates/{index,new,edit,template}.hbs + template_form.hbs: FlyonUI
data-tabs drive the ACE editor panes (replacing Bootstrap tabs),
send-test modal with open.overlay reset + gated send button,
Handlebars-payload panel on ui/details, delete modals, ui/form-field
composition, breadcrumbs.
- SYSTEMIC FIX uncovered by the e2e run: block-form ui/* partials
leaked their own hash params into nested partial calls inside their
blocks (every button inside a ui/modal inherited the modal id -
duplicate DOM ids on all converted modals; the dashboard welcome
button silently inherited the stat-card's variant). All eight
block-form partials now neutralize their own param names when
invoking @partial-block. Page sweep confirms zero duplicate ids
(except the pre-existing layout/page #crumb overlap) and zero console
errors across all converted pages.
- New ui/details partial replaces the four hand-rolled details/summary
collapsible cards (dashboard, templates, tokens, template payload).
- e2e: tokens list/delete-modal test (reusing the createApiToken
bootstrap helper; deterministic opacity-based overlay settle wait)
and a templates create/tabs/send-test/delete flow test.
Verified: tokens + templates pages against live data (FlyonUI tabs with
ACE, reveal flow incl. redirect, delete modals, spinner states), page
sweep for duplicate ids/console errors, 11 e2e tests green, lint/
format, 1394 unit tests. Reviews applied (S1 static backdrop, C1-C5).
Claude-Session: https://claude.ai/code/session_01N4gy7CNzkic59TJZU1X9Yu
Webhooks batch of the admin UI reskin (4 pages + partials), each page
investigated, converted, and verified per the page pipeline:
- views/webhooks/index.hbs: page header, info stat-card, stretched-link
table rows with ui/badge status (Failing/Enabled/Disabled), pagination.
- views/webhooks/webhook.hbs: toolbar with ui/tooltip buttons, ee-dl
definition list, FlyonUI data-tabs for the fn/map/error-log panes
(ACE previews untouched), delete confirmation via ui/modal; the error
log pane now only renders when an error log exists; legacy
clear-alert-btn alert kept for the Phase 5 app.js cluster.
- views/webhooks/{new,edit}.hbs: breadcrumbs + ui/btn submits.
- views/partials/webhook_form.hbs: ui/form-field, ui/checkbox,
ui/details for custom headers, editor chrome via .code-editor +
.editor-embed-* component classes.
- views/partials/webhooks_editor_functions.hbs: setPayloadModal on
ui/modal + uiModal.open; the filter evaluation result now toggles
semantic text-error instead of compat-bridged text-danger and drops
the initial text-info placeholder (layered text-error sorts before
text-info, so the leftover class would win the cascade).
- NEW views/partials/ui/tooltip-rich.hbs: central FlyonUI tooltip with
a rich HTML body (block = body, trigger icon param); replaces the old
data-html editor-scope popover and covers the upcoming popover-heavy
config batches. editor_scope_info.hbs is now a thin call into it.
- ui/badge: render numeric 0 labels (if includeZero=true) instead of
failing on the absent @partial-block; custom-header badges use the
badge block form.
- Duplicate #crumb ids swept from all converted pages (the app layout
already renders the #crumb element that scripts read; form inputs
keep name="crumb", standalone JS-only inputs removed.
Verified with Playwright against a live instance in light + dark:
create/edit/delete round-trip, live filter/map evaluation via the
worker, payload modal with predefined payloads, scope tooltips, editor
fullscreen toggle, webhook preview send, zero console errors and no
duplicate DOM ids. New e2e test in pages-admin.spec.js covers the
create -> evaluate -> tabs -> delete flow; full spec green. Cleanup +
security review agents ran on the batch (all findings applied; no new
vulnerabilities found).
EOF
)
Config-core batch of the admin UI reskin (network, logging, license, webhooks, smtp, imap-proxy, ai, service, service-preview), each page investigated, converted, and verified per the page pipeline: - NEW ui/setting-ref partial: the floated [settingKey] swagger reference shown on nearly every config field (link, plain env-var, suffix, and class modes); used across all converted config pages. - ui/page-header: badgeLabel/badgeVariant slot for status badges next to the title (license, config/webhooks; smtp/imap-proxy join in Phase 5 when their SSE badges retarget). - ui/checkbox: disabled, labelTitle, dataReverse params; ui/details: summarySmall suffix; ui/btn: external param (target/rel/referrerpolicy for external links; also backported to the templates index doc link); ui/tooltip-rich: label (text trigger) alternative to icon. - network: browser-compiled address_list.hbs converted (FlyonUI markup only - it cannot use server partials), delete dropdown replaced with a delegated trash button that survives client-side re-renders, uiAutoInit() after list redraws, scan button de-bootstrapped (and given type="button" - the original submitted the settings form). - smtp/imap-proxy: join-based password reveal, FlyonUI tabs for the integration examples, TLS cert modal on ui/modal with hidden-class state toggling; fixed the original's shown.bs.modal handler accumulation (duplicate cert fetches on reopen); dropped dead NODEMAILER/PHPMAILER replaces from the imap-proxy example renderer. - ai: privacy popover on ui/tooltip-rich, test-filter/test-prompt modals on ui/modal, flask/spinner icon swap in iconify classes, evaluation script drops the text-info placeholder before adding text-error (the layered text-error sorts before text-info in the compiled CSS). - service: language-override collapse as a native details element, header-template preview image on ui/tooltip-rich, queue-clean and model-reload spinner buttons standardized on ui/btn + icon class hooks, duplicate section ids deduped (imap_settings/script_settings). - layout/app.hbs: the global #crumb input now renders unconditionally - it sat inside the trialPossible conditional, so on licensed instances every fetch-based admin action would have lost its CSRF crumb source once the page-local duplicates were removed (security-review catch). - Legacy contracts kept for Phase 5: SSE state badges (smtp/imap-proxy headers), clear-alert-btn alerts (config/webhooks, ai), the shim-backed tls-label tooltip repaint. Verified with Playwright against a live instance in light + dark: IP rescan round-trip with client-side Handlebars re-render + delegated delete modal, or-else-all gating in both directions, integration-example tabs + live re-render, cert-label repaint path, AI live filter evaluation + payload/test-prompt modals, service preview window, queue cleanup toasts; zero console errors, no duplicate DOM ids. Seven new e2e tests in pages-admin.spec.js; full spec green. Cleanup + security review agents ran on the batch (findings applied; the crumb regression above was the one security-relevant catch).
Config/oauth batch of the admin UI reskin (apps index, subscriptions, new, edit, app detail + oauth_form/scope_info partials), each page investigated, converted, and verified per the page pipeline: - index/subscriptions: FlyonUI dropdown for the create-app provider menu, stretched-link table rows, ui/badge statuses, ui/pagination; status badges with dynamic titles keep native title tooltips lifted above the row overlay. - oauth_form (1445 lines): provider info cards on ui/stat-card, fields on ui/form-field block form, base-scope radio tables with FlyonUI radios, account-type helper as a joined button group, Additional and Disabled scopes as ui/details (the scope-preset JS now toggles the details open property instead of the Bootstrap collapse show class); the disabled-scopes textarea renamed to skipScopesList, fixing a pre-existing duplicate id with the collapse container the JS actually targeted. The auth-method switcher stays custom JS (deliberately not data-tabs) with FlyonUI tab styling and hidden-class sections. - new/edit: breadcrumbs via ui/breadcrumb; the ~110-line credential file-loader + auth-method script that was duplicated verbatim across both pages now lives once in oauth_form.hbs next to the markup it targets. - app detail: toolbar on the established tooltip+soft-button idiom, ee-dl with a joined copy-to-clipboard control, verify-setup modal ported to ui/modal with open.overlay auto-run, iconify status icons in the step renderer (esc() wrapping preserved on all server-supplied fields), pub/sub badges composed from ui/tooltip + ui/badge. - views/config/oauth.hbs converted too but found to be ORPHANED (no route renders it; the /admin/config/oauth-legacy paths are gone) - flagged for deletion in the Phase 5 cleanup. Verified with Playwright against a live instance in light + dark for all five provider variants: base-scope gating, account-type presets filling and expanding/collapsing the scope sections, authority/tenant gating, auth-method tab switching, create -> detail -> verify-setup (auto-run renders steps + verdict) -> edit prefill -> delete round trip; zero console errors, no duplicate DOM ids. New e2e test in pages-admin.spec.js; full spec green (20 passed). Cleanup + security review agents ran on the batch (findings applied; no new vulnerabilities found).
Document-store batch of the admin UI reskin (settings, chat, mappings index/new, pre-processing). Deprecated feature, so this is a straight conversion on the established idioms - no redesign: - settings/chat/pre-processing: enable-strips with setting-refs and status pills, ui/form-field blocks, test-connection and try-chat buttons on ui/btn with iconify spinner toggles, tryChatModal and setPayloadModal on ui/modal (open.overlay reset listener replaces show.bs.modal), chat response rendering keeps its textContent-only sinks, hidden-class state toggling replaces d-none throughout. - chat enable label composes ui/tooltip (ElasticSearch version note) and ui/tooltip-rich (document-store-disabled warning) triggers. - mappings: ui/stat-card intro, searchable badges on ui/badge, add-mapping confirmation on ui/modal, index-status popover on ui/tooltip-rich, breadcrumb via ui/breadcrumb. - pre-processing: same evaluation-script conversion as ai.hbs and the webhooks editor (text-error rename with the text-info placeholder removed - compiled-CSS order trap), editor grid on sm:col-span-*. - playwright.config.js: the e2e server now boots with EENGINE_DOCUMENT_STORE_ENABLED=true so the gated pages render and can be smoke-tested; the gate-on/setting-off state reuses the tested setting-off runtime paths. Verified with Playwright against a live gate-enabled instance in light + dark: test-connection toast, try-chat round trip (error path + spinner restore), mapping confirmation modal + rich tooltip, live filter evaluation and payload modal in pre-processing; zero console errors, no duplicate DOM ids. New e2e test covers all four page types; full spec green (21 passed). Note: after a failed chat request the overlay no longer closes on Escape because the disabled/re-enabled submit button drops focus to body and FlyonUI needs focus inside the overlay (Bootstrap's focus trap masked this) - Close/backdrop work, documented in the e2e test. Combined quality + security review agent ran on the batch: clean, no findings to apply.
Internals batch of the admin UI reskin (system threads + thread accounts), both pages investigated, converted, and verified per the page pipeline: - index: thread table with unresponsive rows on bg-error/10, configuration and error popovers on ui/tooltip-rich, health badges on ui/badge with the route now emitting FlyonUI variants (error/neutral instead of compat-bridged danger/secondary), snapshot/restart circle buttons wrapped in ui/tooltip (the main-thread placeholder keeps its invisible slot for column alignment), kill/snapshot confirmation modals on ui/modal with the row buttons filling the hidden thread inputs before uiModal.open. - thread: ui/breadcrumb, stretched-link account rows, the provider type icon on the accounts-index ui/tooltip idiom (the legacy title/data-toggle attributes were unreachable under the row overlay), SSE state badge kept byte-identical to accounts/index.hbs including the z-lifted wrapping link, ui/pagination. - swagger and arena wrappers audited: pure embeds with no Bootstrap markup, nothing to convert. views/index.hbs and oauth-scope-error.hbs moved to the Phase 4 batch - they render under the still-unconverted main/public layouts. Verified with Playwright against a live instance in light + dark: modals fill and open per row, config/error tooltips, thread detail with breadcrumb + state badge + type tooltip; zero console errors, no duplicate DOM ids. New e2e test covers both pages with a three-branch thread-detail fallback (accounts link, empty IMAP worker, or the non-IMAP redirect guard) because late in a full-suite run the e2e instance stops listing its IMAP/webhooks/submit threads (pre-existing environment behaviour, unrelated to the theme). Full spec green (22 passed). Combined quality + security review agent ran on the batch; all findings applied (type-tooltip idiom, route badge variants, w-32, thread-page e2e coverage), security clean.
Phase 4 of the admin UI reskin: the three remaining layouts (login,
public, main) and every page that renders under them, plus the two
app-layout account pages:
- layouts: theme_init + PT Sans + flyonui.css/flyonui.js +
bootstrap-compat.js + ui.js replace the Bootstrap/SB-Admin stacks;
the login/public shells keep their split-card and centered-card
structure on a brand primary-to-secondary gradient; the
.bg-login-page-image rule is ported into app.tailwind.css (it had
been left behind in the old app.css). The main layout footer comment
no longer embeds the license key - that layout serves
unauthenticated pages (/ and /license.html), so the key was visible
in public page source (carried over from the old theme;
security-review catch).
- account/login + account/totp: pill inputs, FlyonUI divider/buttons;
the pages keep their own #crumb (the login layout renders no global
one; login-passkey.js reads it by id).
- account/security: status grid rows, five modals on ui/modal, passkey
register flow (passkey-register.js now opens through uiModal), Okta
and OIDC instruction tables restyled.
- account/password, legal, upgrade, index, oauth-scope-error, error,
unsubscribe (#resubscribeModal id and resubscribe-link contract
preserved; uiModal.open replaces the jQuery call), license (eulaText
wrapper).
- accounts/register/*: hosted-form pages converted with all e2e ids
intact (#imap_*/#smtp_*, #submit-settings-btn-down/#submit-wo-testing
split dropdown now on the FlyonUI join+dropdown pattern); the
verify-connection JS swaps iconify classes and btn-primary <->
btn-neutral (btn-secondary is off-limits in converted views); the
connection-error list renders into an ee-dl; the appPassword XSS fix,
DOM-API-only error rendering, and the single-use-nonce double-submit
guard are preserved verbatim.
- i18n: every {{_ ...}} msgid byte-identical to master (verified by
extraction diff) so existing translations keep working.
Verified with Playwright against a live instance: fresh-context login
page render (light + dark), full hosted-form walk with a live Ethereal
IMAP/SMTP verify round trip (spinner -> check icon -> submit enable ->
split dropdown -> skip-verification -> redirect page), all main/public
pages, security modals; zero console errors. Two new e2e tests
(main-layout public pages; security modals + password form) and the
full four-spec e2e suite is green (30 passed) - including login,
hosted-form, and unsubscribe flows that now run through the converted
layouts. Cleanup + security review agents ran on the phase (findings
applied; no security regressions).
Phase 5 of the admin UI reskin - the last Bootstrap remnants are retargeted or deleted: - State badges: every emitter (route-helpers formatAccountData and formatServerState, the app.js SSE maps, the gateway label builders in admin-entities-routes, the internals fallback, the cert labels in workers/api.js) now emits FlyonUI variants (danger->error, secondary/light->neutral); badge-pill markup became rounded-full and the SSE class-swap exception for it was dropped. Flash alert types map through the eeColor helper at the template boundary, so the 140+ request.flash call sites keep their semantic names. - Icons: account-type/provider icons (route-helpers, oauth2-apps) and the systemAlerts list (workers/api.js) emit full iconify classes, rendered as spans; showToast maps its legacy icon names to iconify classes; the SSE spinner and pending-form submit spinner inject iconify classes; every server-emitted class is safelisted in app.tailwind.css (lib/ and workers/ are not scanned). - clear-alert-btn error alerts: app.js binds the close button directly (removes the alert + clears the stored error server-side); the closed.bs.alert contract is gone. - bootstrap-compat.js rewritten as a residual tooltip/popover-only shim for the SSE state badges and TLS cert labels; it also gains hover delegation for data-trigger="hover" popovers - the migration-era shim only opened popovers on click (and preventDefault()ed the badge's wrapping link), so badge hover popovers never actually worked; the unsanitized data-html popover path is deleted (textContent only now). - bootstrap-compat.css rewritten to just the shim bubbles + the .close button; the entire Bootstrap bridge (grid, spacing, forms, components) is gone. A systematic sweep found one live orphan (font-weight-bold on the Subconnections heading -> font-bold). - Deleted: bootstrap-4.6.2-dist, sb-admin-2 css/js, fontawesome-free (~16MB), nunito fonts, jquery-easing, bootstrap-autocomplete, moment-with-locales, popper, the stray jquery 3.4.1 slim copy, static/icons (~330 svg files, showToast-only), app.css, callout.css, emailengine.css, and the orphaned views/config/oauth.hbs (no route renders it). Font Awesome links removed from all four layouts. - startbootstrap-sb-admin-2 removed from dependencies and .ncurc.js; static/licenses.html regenerated without it. Dead card-subtitle class swept; main layout footer keeps the version-only comment. Verified with Playwright against a live instance: retargeted state badges render and update live through an SSE reconnect round trip, toast/systemAlert/type/provider icons all render as iconify spans with Font Awesome fully unloaded (zero fa elements, zero console errors), gateway/tls badges, hover popover delegation. Full four-spec e2e suite green (30 passed) and the unit tier passes. Combined quality + security review agent ran on the phase: three missed retargets found and fixed (gateway error labels, internals Unknown fallback, the font-weight-bold orphan); security clean - the shim rewrite removes an unsanitized innerHTML path.
Final-gate pass over the complete migration branch. Two independent whole-branch review agents (quality + security) audited the full master...HEAD diff; the security review found no regressions (and the branch nets out safer: identical raw-HTML surface to master, all POST forms CSRF-covered, CVE-carrying vendored JS removed, remote-controlled support-url now validated). Quality findings applied: - ui/page-header: the badgeLabel slot leaked the partial's own icon and class hash params into the nested ui/badge call - the license and config/webhooks title badges rendered a spurious icon and margin; the nested call now neutralizes them. - badge-neutral: FlyonUI ships no such variant, so the neutral state badges (disabled/paused/unknown) only looked right because the base .badge shares the tokens; app.tailwind.css now defines the variant explicitly so the SSE class swaps stay meaningful. - static/index.html deleted: an unlinked old-theme page that referenced six stylesheets this branch removed - it was publicly served and rendered broken. - Enable-strip alignment: config/imap-proxy and config/webhooks now use the same bordered feature-toggle strip as their smtp/ai/document-store siblings. - ui/setting-ref adopted at the two remaining byte-identical hand-rolled reference blocks (accounts/edit, document-store pre-processing); stale bridge note in ui/btn docs reworded. - CLAUDE.md gains the promised Admin UI Theme section: build pipeline (build:css + committed artifacts), brand tokens, the ui/ partial library conventions (hash-param neutralization, safelist trap, entity-in-hash-string trap), iconify-only icons, the residual bootstrap-compat shim contract, and the no-prettier-on-hbs rule. Deliberately not changed (reviewed and accepted): documented-but-unused partial params kept as API surface, referrerpolicy spelling variance on _blank links (rel=noreferrer already governs), address_list.hbs hand-rolled tooltip (browser-compiled template cannot use partials), the 4-level breadcrumb in templates/edit.hbs, and the pre-existing orphan candidates in static/ (vendor/jquery, front/, spare logos). Final gates all green: full npm test (1394 unit + 140 integration, 0 failures), complete four-spec e2e suite (30 passed), licenses gate exits 0, compiled CSS/JS artifacts byte-identical to fresh builds.
FlyonUI's Tabs script toggles a plain `active` class, but its stylesheet only styles `.tab-active`, so nothing matched `.tab.active` and every tab strip rendered the selected tab identically to the unselected ones. The panes switched correctly - only the highlight was missing. Use FlyonUI's `active-tab:` variant (it compiles to `[data-tab].active`) to apply `tab-active` on the data-tabs strips. The OAuth auth-method strip is driven by its own page script rather than data-tabs, so the variant cannot reach it; that script and its markup now toggle `tab-active` directly. The e2e tests clicked these tabs and asserted pane visibility, which is why they stayed green through the bug. Add expectSelectedTab(), which compares the computed color against the theme's resolved --color-primary (theme-agnostic) and parks the pointer first, since hover also paints a tab primary and would mask a regression. Each assertion was verified to fail on the unfixed markup. The OAuth strip had no tab coverage at all, so it gets some.
…dler The vendored clipboard.min.js (v2.0.0, 2017) served three admin pages through the .copy-btn init in app.js. Replace it with a delegated click handler in ui.js: data-copy-target names the element to copy, the async Clipboard API does the write, and installs served over plain HTTP (not a secure context) fall back to select() + execCommand. Successful copies flash the button icon to a check mark - ClipboardJS gave no feedback at all. Delegation also means buttons inside dynamically injected markup work without re-binding, which the load-time ClipboardJS init did not. The three near-identical input+button blocks collapse into a new ui/copy-field partial (small/join/class/inputClass variants cover the existing sites), so the id/data-copy-target pairing is correct by construction. The vendored library and its script tags are removed from all three layouts; login/public never had a copy button to begin with. E2e covers both write paths: the templates detail page exercises the real Clipboard API (grantPermissions + readText + icon feedback), a synthetic fixture with navigator.clipboard stubbed out exercises the execCommand fallback, and the oauth app page covers the join-variant partial wiring.
Three config pages (SMTP, IMAP proxy, service) each carried a 10-line inline script that swapped the password input's type attribute. FlyonUI's HSTogglePassword plugin does this declaratively via data-toggle-password, so the scripts go away, and the button now swaps between eye/eye-off icons through the vendor password-active: variant (shipped by the strong-password plugin's variants.css, which flyonui/variants.css already imports - toggling adds `active` on the button and the variant compiles to a descendant selector). The input+button join group was near-identical at the three sites, so it becomes a ui/secret-field partial: the id/name/reveal-target triple and the data-lpignore/autocomplete boilerplate are now correct by construction. The conditional-on-value placeholder wrappers were redundant (placeholders only display while the input is empty) and are dropped. E2e: a shared expectPasswordToggle helper asserts the full round-trip on all three pages - type flips to text with the eye-off icon shown, then back. The old imap-proxy assertion only checked the one-way type flip.
The TLS status badges on the SMTP and IMAP proxy config pages were the
last data-toggle="tooltip" consumers of the bootstrap-compat shim, and
their paintCertData() repaint carried the only inline jQuery calls left
in views/ (the $().tooltip('dispose'/'enable') dance). Wrap the badge in
the standard ui/tooltip partial instead; the repaint now just writes the
status text into the tooltip body, since HSTooltip reads content live and
needs no re-init.
paintCertData() itself was byte-identical on both pages, takes no page
context, and this change would have grown both copies - it moves to
ui.js and the page scripts call the shared version.
E2e (expectTlsLabelTooltip, both pages): the tooltip body carries the
certificate status text in the exact structure paintCertData() targets,
and hovering the badge opens the FlyonUI tooltip.
The last legacy contract from the Bootstrap migration was the jQuery tooltip/popover shim serving the SSE state badges. Those badges (five account/gateway/internals views plus the SMTP and IMAP proxy page headers) now render through a new ui/state-badge partial: a FlyonUI tooltip carries the connection error, hidden while there is no error. The SSE handlers repaint through plain DOM writes - a shared repaintStateBadge() swaps the badge class/spinner/text and toggles the tooltip body, replacing the $().popover() enable/disable dance (the two updaters had carried byte-identical loop bodies; now there is one). The page-header badges gain a working error tooltip: they were SSE targets but never carried the popover markup the shim triggered on, so their error bubbles could never actually show. With no consumers left: bootstrap-compat.js, bootstrap-compat.css (the .close rules move into app.tailwind.css) and the hand-vendored jQuery 3.6.0 are deleted, the script tags are gone from all four layouts, and the unreferenced jquery 4.0.0 npm dependency + static/vendor/jquery copies are removed (license listing regenerated). CLAUDE.md now documents the admin UI as jQuery-free. E2e: an account with deliberately failing IMAP auth drives its badge into the error state live on the accounts list (SSE repaint), asserting the badge class/text swap, the tooltip body text, unhiding, and the hover bubble. Runs last in the spec - a mid-deletion account transiently 404s the internals thread pages, so cleanup polls the account away.
…[skip ci] Evaluated migrating the oauth_form auth-method strip to HSTabs: each method shows two section blocks interleaved with fields shared by both methods, and HSTabs maps one toggle to exactly one content element, so the migration would need field duplication or a form reshuffle. The 25-line script stays; the comment now records the reasoning.
The same 24-line .toggle-fullscreen block (click toggles .full-screen-div on the editor container, Escape exits, editor resized and refocused on both transitions) was copy-pasted in six page scripts. It moves to ui.js as uiEditorFullscreen(editors); the pages pass the id->ace-instance Map they already maintain, which also filters out the styling-only .toggle-fullscreen links that have no editor attached. E2e: the templates form test runs the fullscreen round-trip through the shared helper at one representative call site.
The build-a-datalist-and-attach-inputs block (the replacement for the old bootstrap-autocomplete plugin) was duplicated on four pages with small variations. It moves to ui.js as uiDatalist(id, values, inputs); the pages keep only what genuinely differs: the smtp/imap-proxy pages pass their interface-address lists, accounts/edit references the list id from markup, and the gateway form maps its "key / description" labels and keeps the page-local listener that autofills host/port/secure when a well-known service is picked. E2e: the gateway test types "gmail" into the name field and asserts the host autofill (datalist + listener end to end); the smtp and imap-proxy tests assert the listen-address inputs are wired to a populated suggestion list.
Three defects reported on the editor blocks:
- The toolbar's title cell ("Filter function" etc.) was styled identically
to the clickable action cells. Title cells get a new editor-embed-title
class (tinted background, semibold), and the action links render in the
primary color with hover underline - Tailwind's preflight had stripped
the anchor styling Bootstrap used to provide, leaving everything
identical black text.
- Toggle fullscreen collapsed the editor to a 2px strip. ACE injects its
own un-layered .ace_editor{position:relative} stylesheet at runtime,
which beats the @layer components .full-screen-div rule, so the fixed
positioning never applied. The rule moves out of the layer with
!important on the geometry (comment documents the cascade reasoning
and the layered-!important footgun).
- The scope-info hover rendered as unreadable ghost text. ui/tooltip-rich
put block content (a ul) inside the inline tooltip-body span, and an
inline box fragments its background around block children - the bubble
background painted as two tiny blobs while the light bubble text sat
bare over the white editor. The body is now display:block.
E2e assertions are effect-based, since the class-presence fullscreen
check stayed green through the bug: the fullscreen rect must cover the
viewport, the tooltip body must paint a non-transparent background that
encloses the list, and the title cell background must differ from the
action cells.
Initiating TLS certificate generation on the SMTP or IMAP proxy config pages without a usable Service URL showed "Setting up TLS certificate for false". getServiceDomain() returns a sentinel when the URL is unset, an IP, localhost or a .local/.lan name - and the templates interpolated it raw, so data-domain rendered as the string "false", which is truthy and sailed through the page script's provisioning guard. The sentinel becomes an empty string (interpolates to nothing; every consumer only ever truthiness-checks it, verified across all callers), the data-domain attribute and the modal's domain text render only when a domain exists, and the TLS checkbox is disabled without one - except when TLS is already enabled, so it can still be switched off after the Service URL was changed to something unusable. The helper text under the checkbox already explains how to set the domain. Server-side this was never exploitable: the provisioning endpoint takes no domain from the client and the ACME call was already guarded on the settings-derived hostname - the UI just pretended otherwise. E2e: clears serviceUrl through the settings API, asserts the checkbox is disabled with no data-domain on both config pages, restores the setting in a finally block, and asserts both pages are operable again.
The stat/notice cards still carried SB-Admin-2's signature look - a
colored start-edge stripe (border-s-4) with a bold uppercase colored
label - which read as Bootstrap inside the FlyonUI theme. The variant
color now lives in a tinted rounded-field icon container (the reference
dashboard's avatar-placeholder pattern) on the plain card shell every
other card uses.
The partial keeps its API and both roles: with a title the icon+title
form a notice-card header row; without one the icon anchors to the top
of the content row (items-start, so it does not float mid-height next
to tall notice bodies). The dashboard's inline KPI labels drop the
uppercase colored treatment for a quiet muted caption with a semibold
value, which also unifies the two label styles that page had drifted
into. The orphaned border-s-{variant} safelist is removed and
bg-{variant}/10 is safelisted for the assembled tint classes.
The sidebar brand block padding shrinks from py-12 to py-5, in line
with the FlyonUI reference sidebar header rhythm.
E2e: the dashboard test asserts the new idiom (tinted icon container
present, no edge stripes anywhere) instead of the old stripe class.
Four visual defects plus a verification, from manual testing feedback: - Tooltips clipped under the sidebar. FlyonUI emits .tooltip-content (z-20) in @layer utilities, so the components-layer z-60 override was dead CSS - utilities wins the layer order regardless of specificity. The override moves un-layered (like the ACE fullscreen rule) and now computes to 60, above the drawer (z-40) and topbar (z-30), below modals and toasts. Verified with a browser probe: the bubble paints over the sidebar and elementFromPoint inside the overlap hits the bubble. - Hovering an errorless state badge showed an empty dark bubble. HSTooltip's show() removes the hidden class from the content - the exact class the empty state was gated with, so the first hover destroyed the gate. The gate is now ee-tooltip-empty on the wrapper (display:none on the content), which HSTooltip does not manage; the SSE repaint toggles it in setStateTooltip(). - All badges are square-cornered now: the pill param is removed from ui/badge and ui/state-badge along with every caller, and the hardcoded badge rounded-full copies in accounts/account.hbs (markup and both JS classNames) and test_send.hbs are swept. - rounded-full -> rounded-field on the icon containers (system-alert level icon, topbar avatar). The auth pages' pill-shaped inputs and buttons keep their deliberate style - they are neither badges nor icons. - The "System" section label is removed from the sidebar menu, and ui/tooltip renders its block bare when the text is empty, so a missing tooltip source can no longer produce an empty bubble. SSE verification: the state-badge e2e now installs a MutationObserver (class/children only - the tooltip teardown mutates style attributes asynchronously and must not count) and forces a reconnect through the API; the badge must repaint with no navigation. Live account state streaming works in the new theme. A second e2e covers the case found in manual testing: hovering an errorless badge shows nothing.
The service and AI config pages set enableBasicAutocompletion, enableSnippets and enableLiveAutocompletion on their editors but never loaded ext-language_tools.js, so ACE logged 'misspelled option' console warnings for every editor and silently ignored the options - editor autocompletion never actually worked on those two pages. The webhook and document-store editor pages already load the extension; these two now match. Verified in a browser that the warnings are gone on all four ACE pages.
@postalsys/ee-client 1.5.0 brings the host-controlled dark mode API (darkMode/showDarkModeToggle options, setDarkMode method) used by the admin message browser theme sync.
The ee-client webmail embed managed its own light/dark mode with a builtin toggle, independent of the admin topbar switch. With ee-client 1.5.0 the host controls the mode: the embed initializes from the resolved admin theme, hides its own toggle (showDarkModeToggle: false), and follows changes live - a MutationObserver on the root element's data-theme attribute (stamped by the topbar toggle) plus a prefers-color-scheme listener for installs that follow the system scheme drive client.setDarkMode(). ui.js exposes the previously-private theme resolution as uiEffectiveTheme() so embeds can read the effective choice (the data-theme attribute alone is absent when following the system). E2e (browse page test): the embed renders no dark mode toggle of its own, and clicking the topbar theme switch flips the client's dark mode live, then back.
Adopts the license generation approach from KEKA: list-generate.js now walks npm ls --omit=dev itself (the license-checker dependency and its tree are removed), collects each package's declared license, homepage and full license text, tags build-embed packages (tailwind/flyonui/ iconify) and hand-vendored assets (PT Sans, highlight.js) as assets, and renders a self-contained static/licenses.html - card per package with expandable license text, light and dark, no more Bootstrap CSS from a CDN. The SPDX allow-list gate stays and got stricter: licenses are read from declared metadata only (no license-file guessing), all violations are reported before the non-zero exit, and the two packages the old tool silently guessed for are pinned in a documented LICENSE_OVERRIDES map. javascript: cannot ride in on a hostile homepage field - link schemes are allow-listed. Byte-identical license texts render once and other packages reference them, keeping the page at two thirds of its inline size.
swagger-ui ships no dark styles at all - its stylesheet contains zero prefers-color-scheme rules and assumes a light page. What looked like automatic dark mode support was the dark admin theme's page background and color-scheme bleeding through swagger-ui's transparent panels, which rendered the reference unreadable. There is no dark theme to tie the admin toggle to, so per the fallback: the swagger container is pinned to a light data-theme subtree (white card, color-scheme light) while the admin chrome around it stays themed. E2e: with the dark theme forced, the reference island computes a white background and light color-scheme while the page itself is dark.
The gateways page notice faked a bullet list with literal asterisks and <br> tags; it renders as a proper list-disc ul now, matching the other pages' list styling.
FlyonUI's tooltip-content uses width:max-content, so long tooltip texts (e.g. the gateway form's direct-TLS explanation) rendered as one viewport-wide line. The bubble is capped at 20rem in the un-layered tooltip overrides, and the body becomes display:block with balanced wrapping - the body span is inline in FlyonUI's markup, and an inline box fragments its background into a separate dark box per wrapped line. E2e asserts the gateway form's long tooltip stays within the cap.
The Security (TLS) card, the certificate provisioning modal and the provisioning script were byte-for-byte duplicated (modulo the field name) between the SMTP and IMAP proxy config pages - verified by normalization diff before extraction. They live in three partials now (tls_security_card / tls_security_modal / tls_security_js), parameterized by the setting field name; the modal stays outside the settings form so its Close button cannot submit it. Both pages keep provisioning through the same endpoint deliberately - the certificate belongs to the service domain, not to either server. 162 duplicated lines gone from each page; all element ids are preserved, so the page scripts, paintCertData() and the existing e2e coverage of both pages hold unchanged.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Complete reskin of the admin web UI from Bootstrap 4 + SB-Admin-2 + jQuery to Tailwind v4 + FlyonUI, converted page by page with per-page Playwright coverage.
Highlights
views/partials/ui/(buttons, badges, modals, tooltips, form fields, stat cards, copy/secret fields, state badges, pagination, menus) backed bystatic/js/ui.jsTesting
🤖 Generated with Claude Code
https://claude.ai/code/session_01RwJF5u6yxWQHvE8LapWPB3