Skip to content

Captive Portal 2.0 - #543

Open
ryanckulp wants to merge 19 commits into
mainfrom
redesign/wifi-captive-portal
Open

Captive Portal 2.0#543
ryanckulp wants to merge 19 commits into
mainfrom
redesign/wifi-captive-portal

Conversation

@ryanckulp

Copy link
Copy Markdown
Contributor

summary

this PR redesigns our WiFi and Advanced Settings interfaces, along with UX improvements and bug fixes.

before / after

TRMNL-captive-portal-before-after

other improvements

  • fun stuff: light/dark modes, empty + loading state animations, "saved" network section
  • bug fixes: validation errors, Advanced Settings could previously be ignored by Safari
  • DRY: single index.html with hash-based "Setup" and "Advanced" views that still feel like separate pages
  • performance: dropped localStorage and multiple service calls

tested on my OG + X, phone + computer. click tested most Advanced Settings (custom host name, server URI, sensor test) to confirm state between "pages" (Setup/Advanced), re-scan populates networks, validations, password show/hide, etc.

ryanckulp and others added 18 commits August 6, 2026 18:36
- Add a global [hidden] { display: none !important; } rule so every future
  display: declaration cannot silently un-hide an element carrying the
  hidden attribute (previously .p-head-mac/.mac-footer rendered as
  visible-but-empty boxes on load and whenever no 5GHz MAC was present).
- Remove the inert /device-settings MAC fallback from getInfo's catch
  branch per Ryan's ruling; the endpoint has no mac field, so the header
  row now simply stays hidden on scan failure instead of calling a
  fallback that could never populate it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add min-height:48px to .inp and switch .pw-toggle to top/bottom:0 so text
inputs and the password toggle meet the same 48px floor Task 3 set for
network rows. Delete dead .form-group/textarea/select CSS and the unused
showAdvanced() function left over from the old markup.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
confirmSoftReset/confirmCustomServer/showCustomServerInput/
closeCustomServerModal/closeModal had zero callers and two of the three
DOM ids they referenced didn't exist in this file. showCustomServerInput
still drove #custom_server_form_group via style.display, which the
global [hidden]{display:none!important} rule would silently defeat if it
ever ran, dropping a custom server URL from /connect with no error.
Delete the whole dead cluster rather than repair the one live element
reference. Also add min-height:48px to .btn so its touch-target size is
explicit rather than a coincidence of padding and font metrics, matching
.inp's fix in the prior commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merges advanced.html into index.html as a second sibling view
(view-setup / view-advanced) swapped by hash routing, so the page
still reads as two screens without a second network round trip. This
removes the unguarded localStorage.setItem bridge between the pages,
which could throw QuotaExceededError in Safari private mode or
restricted WebViews mid-write and leave a device configured for a
static IP with no address. Settings are now read straight from the
DOM at Connect time. WebServer.cpp's /advanced route now redirects to
/#advanced since ADVANCED_HTML is no longer emitted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The .switch label wrapping the static-IP checkbox had no min-height,
so its clickable region was only the 18px checkbox plus line-height
(roughly 20-24px) -- well under the 48px touch-target floor already
applied to .inp and .btn elsewhere in the page. Adding min-height:
48px to the label (not the checkbox) makes the whole row tappable,
including the label text, while keeping the visual checkbox at 18x18.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
index.html had no charset meta tag, and WebServer.cpp served it as
plain "text/html" with no charset parameter. Browsers defaulted to
Windows-1252 and rendered the UTF-8 typographic characters introduced
by the redesign as mojibake (garbled ellipsis, degree sign, dashes).

Add a UTF-8 charset meta tag as the first element in <head>, and set
the response Content-Type to "text/html; charset=utf-8". Also convert
every non-ASCII character in index.html to an HTML entity (markup) or
a JS \u escape (string literals), so the page cannot mojibake again
even if a header regresses later.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every /scan request carrying `force` restarted the scan server-side
(WebServer.cpp), but the 202-retry branch in getInfo() kept re-sending
force on each poll, so "Scan again" restarted the scan forever and the
network list never repopulated. force means "start a fresh scan," not
"this session was user-initiated" - retries should only poll:

    setTimeout(() => getInfo(false), 2000);

That loop was also the cause of a focus bug: disabling #btnRefresh
while it held focus (setRefreshButtonState) forced the browser to
relocate focus, landing it on the SSID input, and the 2s retry cadence
made it look like focus kept jumping back. Blur the button first so
focus loss is deterministic instead of incidental:

    if (scanning && document.activeElement === btn) btn.blur();

Separately, focus #ssid exactly once when a scan completes and finds
zero in-range networks (a real "type it manually" moment), guarded by
emptyScanFocused so it can't repeat on a loop - only a later
non-empty scan re-arms it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
showView() calls title.focus() on every hash change so screen readers
announce the new view, and mobile Safari treats that programmatic
focus as :focus-visible, painting a 2px orange ring around a heading
that isn't interactive - it reads as a button.

Keep the focus() call and tabindex="-1" (that's what announces the
view change to assistive tech); just suppress the visible outline on
.p-title. WCAG's visible-focus requirement is about interactive
controls, not a programmatic focus target. Every genuinely interactive
control still gets its --spark outline via the existing global
:focus-visible rule.

Also regenerate the PROGMEM page data (WifiCaptivePage.h) to match
the accumulated index.html changes from this round of device-testing
fixes (charset entities, scan-loop/focus behaviour, this outline fix).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ryanckulp
ryanckulp requested a review from schrockwell August 7, 2026 02:48
@schrockwell schrockwell added the enhancement New feature or request label Aug 10, 2026
@schrockwell schrockwell modified the milestones: v1.8.15, v1.8.16 Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants