You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Panel: manage Phrases from the panel's web page #98
The panel serves a web page on the home network for adding, editing, removing and ordering its Buttons and Phrases.
Decisions (Eric):
Access: HTTPS plus an admin password. The panel serves HTTPS with its own self-signed cert, so the browser warns once. The admin password is set on the touchscreen at first setup and stored hashed (salted PBKDF2 or similar) in NVS. Phrases are typed into computers, so editing them is as sensitive as running commands. There's no unauthenticated write path, and sessions time out.
Phrases: the panel's own list. It's stored on the panel (NVS/LittleFS) and isn't synced with the iPhone Deck.
Acceptance:
The first-setup screen on the touchscreen sets the admin password.
The HTTPS server runs in its own task (not the LVGL loop), and the screen stays responsive during requests.
The page lists Buttons and can add, edit, delete and reorder them. Changes show on the panel without a reboot.
Phrases persist across reboots and survive a firmware update.
Bad passwords are rate-limited. Every write needs the panel's own Origin (web::originAllowed, checked in web_server.cpp) and a __Host- session cookie with SameSite=Strict (Eric chose this over a CSRF token on 2026-10-27). Phrase length and count are capped.
Host tests cover the phrase-store logic and the password check.
Factory reset (holding a corner of the touchscreen at boot) clears the password and phrases.
Blocked by: Wi-Fi working on the panel (slice 1b-2).
Design review for the web phrase manager (Application Security Engineer subagent, read-only)
The proposed design is PBKDF2, a lockout, one session with an HttpOnly/Secure/SameSite=Strict cookie and idle and absolute timeouts, a CSRF token plus an Origin check, an on-device ECDSA P-256 cert, esp_https_server on its own task, and LittleFS writes as temp-then-rename. The review adds the points below; I'm building them into #98.
Building in:
First-use trust: the panel shows its cert's SHA-256 so the first browser accept can be compared. The cert names both the IP and the .local name. A factory reset makes a new key.
XSS: Phrases are served as JSON and rendered only with textContent.
CSP default-src 'self'; frame-ancestors 'none', with no inline script.
DNS rebinding: reject any Host header that isn't the panel's IP or .local name.
Lockout:
The failure count persists in NVS and clears on success, so a crash can't reset it.
The touchscreen can clear a lockout, so a LAN attacker can't lock the owner out for good.
Failed logins show on screen.
Password and reset: changing the password needs the current one. Factory reset wipes the password, Phrases, cert key and sessions together.
Writes and login:
Every POST, /login included, needs a matching Origin; a POST with no Origin is refused.
Writes also need Content-Type: application/json.
SameSite and the CSRF token stay too.
PBKDF2: at least 10,000 iterations, even if a login takes about 1 s; measured on the S3 with esp_timer. The expert's estimate is 2k–10k iterations per 300 ms; not measured yet.
esp_https_server:
CONFIG_HTTPD_MAX_REQ_HDR_LEN 2048 (Chrome headers plus the cookie go over 512).
A truncated cookie counts as no session.
A body over the cap gets 413 before it's read.
3–4 sockets with lru_purge_enable, and short recv/send timeouts.
CONFIG_MBEDTLS_DYNAMIC_BUFFER, and a 10 KB+ httpd stack.
httpd runs at a lower priority than LVGL, never calls lv_*, and posts to a queue.
Flash cache: LittleFS writes turn off the flash cache, which can glitch the RGB panel. The panel already uses bounce buffers; watch for it on the board.
Needs Eric (product calls):
A Phrase with Enter or control keys acts as a command in a terminal. Should the panel send an edit counter, so the Helper shows a notice the first time it types a changed Phrase? Separately, should newlines in panel Phrases be capped or flagged?
Firmware updates stay USB only for now. Any over-the-air path would need signed images.
Notice on first send. Every panel Phrase carries an edit counter, which goes up on each edit. The panel sends it with /type. The Helper shows a one-time notice the first time it types a Phrase whose counter it hasn't seen yet. The Helper side is its own small PR; agent1 reviews it. On the web editor, a Phrase containing Enter shows a visible ↵.
USB-only updates for now; no over-the-air updates. The partition table keeps its two app slots (app0/app1), so OTA can be added later with signed images.
Eric's decisions (2026-10-25, via main); they replace the 80-character preview:
Confirm on the panel first. The first tap on a Phrase that was changed on the panel shows its text and asks "Send?". Nothing is sent until the user confirms. Later taps on that Phrase send right away.
The Helper notice shows no Phrase text. It reads only "Typed a Phrase that changed on a panel".
This covers a panel on current main (#140 through #154). Every step happens at the panel, except where it says otherwise.
Before you start
Have main confirm the panel has the current main build and is in Wi-Fi Mode.
Have main confirm your Helper runs with -lan (for example -lan 192.168.1.0/24). The panel reaches it only over the Home network route.
First start: admin password (Step 1 of 3)
Turn the panel on. A new or erased panel opens "Step 1 of 3: set an admin password".
Type a password of at least 8 characters, twice, and tap OK. It guards Settings and the web editor.
If it fails: "The two don't match" means retype both. "Couldn't save it" means try again; if it repeats, tell main.
Lockout: after 5 wrong passwords, each new try waits 30 s, then longer, up to 15 min. A restart doesn't reset the count.
Pair with your Helper (Step 2 of 3, Wi-Fi Mode)
main runs phrased pair on your Computer. It prints a Pairing code and a line starting Pin for a panel:.
On the panel's Pair screen, pick your Computer from the Nearby list. Enter the Pairing code, then the Pin (16 hex characters), and tap Pair.
Expected: "Paired with ".
Your Computer isn't in the Nearby list: check that the Helper is running with -lan and that the panel is on the same Wi-Fi, then reopen the Pair screen.
"Pin doesn't match": don't retry with the same Pin. The panel refused a certificate that isn't your Helper's. Have main run phrased pair again and use the new code and Pin. If it still doesn't match, stop and tell main.
Any other error: have main run phrased pair again; each code works once and only for a short time.
Step 3 of 3 shows the web editor's address. Open it later; there's nothing to do here now.
Tap a Button. Expected: the Phrase is typed on your Computer and the panel says "Typed".
Bluetooth Mode with your Mac (optional)
Gear > Mode > Switch to Bluetooth keyboard. The panel restarts.
Gear > Computer > Pair a Computer. The panel opens pairing for 2 minutes and shows where the passkey will appear.
On the Mac: System Settings > Bluetooth, find Phrasedeck panel, and click Connect.
When the Mac asks, type the 6-digit passkey shown on the panel. It's new for every pairing.
If macOS opens the Keyboard Setup Assistant, close it, or choose ANSI (US). The panel types on a US layout.
Expected: Settings > Computer says "Bluetooth keyboard: Connected". Tap a Button with a text editor focused on the Mac. The panel says "Sent as keystrokes".
No passkey appears / the Mac says pairing failed: pairing may have closed (2 minutes). Tap Pair a Computer again. On the Mac, choose Forget This Device for the panel, then connect again.
"Send? No Computer is connected": the Mac isn't connected. Check Bluetooth on the Mac, or pair again.
Restart check: turn the panel off and on, wait for it to start, then tap a Button.
To go back: Gear > Mode > Switch to Wi-Fi. The Helper pairing from step 7 is kept.
Factory reset (erases everything)
With the panel off, put a finger on the top-left corner of the screen and keep it there. Turn the panel on and keep holding. The screen counts down "Keep holding to erase this panel: 10 s".
After 10 s it shows "Erased. Lift your finger." Everything is gone: the admin password, the Helper pairing, the Mode, the Buttons and the Bluetooth bonds. The panel starts again at step 3.
To cancel, lift your finger before the countdown ends. Nothing is erased.
"Erase FAILED": tell main. Don't keep using the panel.
On a Mac that was paired over Bluetooth, also choose Forget This Device.
If anything else fails
Note the step number and the exact words on the screen, and send them to main. Don't keep power-cycling it in the middle of a step.
#110 (lib/admin_password), #126 (the wizard: "Step 1 of 3: set an admin password")
test_admin_password: test_short_passwords_and_low_iteration_counts_are_refused, test_only_the_right_password_checks_out. Eric went through this screen on 2026-10-26.
Done
2
HTTPS in its own task, and the screen stays responsive
#133: web_server.cpp:367xTaskCreate(run, "web", ...), separate from the LVGL loop and from the net task
No host test (task wiring). No PR records a hardware check that the screen stays responsive during a request.
Code done; responsiveness not recorded
3
List, add, edit, delete and reorder; the panel updates without a reboot
test_web_editor: test_buttons_can_be_added_edited_moved_and_deleted, test_edits_apply_to_the_button_they_name, test_a_stale_edit_changes_nothing. #137 hardware run: the grid rebuilds with no reboot.
Done
4
Phrases persist across reboots and survive a firmware update
#113: /phrases.json in LittleFS on the spiffs partition, written to .tmp then renamed (main.cpp:363-368)
test_phrases: test_the_list_round_trips_through_json, test_a_bad_file_is_refused_and_the_list_kept. #113 hardware: the list survives a reboot. A firmware update rewrites only the app (the partition table hasn't changed since #102), but no PR records a reflash check.
Reboot done; firmware update by design, not recorded
5a
Bad passwords are rate-limited
#110lib/lockout, used by the web login in #133 (admin::check, 429)
test_lockout (3 tests). #133 hardware: the 6th wrong try returns 429. #137: the lockout is still on after a reboot.
Done
5b
A CSRF token protects writes
#133 + #156 use a different defense: every POST needs Origin: https://<panel ip> (else 403), and the session cookie is __Host-s; HttpOnly; Secure; SameSite=Strict
There's no CSRF token. The Origin check plus a SameSite=Strict cookie covers the same attack. Decision needed: accept this and amend the item, or add a token
5c
Phrase length and count are capped
phrases.h: 48 Buttons, a 40-byte label, a 2,000-byte Phrase; kMaxEditBody on the request
#138: holding the top-left corner for 10 s erases the panel NVS and the whole Buttons partition (main.cpp:977)
test_factory_reset (the hold logic). #138 hardware run.
Done
Gaps
5b, CSRF token. The code uses Origin plus SameSite=Strict instead. I recommend accepting that and rewording the item, since a token adds code and no protection against an attacker who can't send our Origin. If a token is wanted anyway, it's a small PR.
2 and 4, hardware checks not recorded. "The screen stays responsive during requests" and "Phrases survive a firmware update" have never been written down as hardware checks. Each is about 2 minutes when someone has the panel: open the web page while tapping, and reflash then look at the list.
Activity
Skeyelab commentedon Oct 26, 2026
Design review for the web phrase manager (Application Security Engineer subagent, read-only)
The proposed design is PBKDF2, a lockout, one session with an HttpOnly/Secure/SameSite=Strict cookie and idle and absolute timeouts, a CSRF token plus an Origin check, an on-device ECDSA P-256 cert, esp_https_server on its own task, and LittleFS writes as temp-then-rename. The review adds the points below; I'm building them into #98.
Building in:
.localname. A factory reset makes a new key.textContent.default-src 'self'; frame-ancestors 'none', with no inline script.X-Frame-Options: DENY,Cache-Control: no-store,nosniff,Referrer-Policy: no-referrer.Hostheader that isn't the panel's IP or.localname./loginincluded, needs a matchingOrigin; a POST with no Origin is refused.Content-Type: application/json.CONFIG_HTTPD_MAX_REQ_HDR_LEN2048 (Chrome headers plus the cookie go over 512).lru_purge_enable, and short recv/send timeouts.CONFIG_MBEDTLS_DYNAMIC_BUFFER, and a 10 KB+ httpd stack.lv_*, and posts to a queue.Needs Eric (product calls):
Skeyelab commentedon Oct 26, 2026
Eric's decisions (2026-10-25, via main):
/type. The Helper shows a one-time notice the first time it types a Phrase whose counter it hasn't seen yet. The Helper side is its own small PR; agent1 reviews it. On the web editor, a Phrase containing Enter shows a visible ↵.app0/app1), so OTA can be added later with signed images.Skeyelab commentedon Oct 26, 2026
Eric's decisions (2026-10-25, via main); they replace the 80-character preview:
helper: the edit notice shows no Phrase text (Eric, #98)
helper: notify the first time a panel Phrase is typed after an edit (#98
firmware: Button ids and edit counts stop below a limit, and the pass…
firmware: panel Button list, admin password and lockout, edit confirm (…
firmware: "Send?" confirms the edit it showed, and a list that won't …
firmware: Buttons from the panel's saved list, "Send?" for web-edited…
firmware: dark theme on every screen, and a header whose status and n…
firmware: Pair screen stays open for the result, with a code/Pin coun…
firmware: Buttons flash their Send result; "Send?" shows Enter and Ta…
firmware: a gear opens Settings behind the admin password (#98, UI 4a) (
firmware: a new panel walks through password, pairing and the web edi…
firmware: past 8 Buttons, Pages swipe sideways with a dot for each (#98…
firmware: the web login gets its own lockout, runs below the UI and c…
firmware: the panel serves its admin login over HTTPS with a key and …
firmware: fixes from the #137 reviews: no control bytes in a Phrase, …
firmware: the panel's web page edits, adds, moves and deletes Buttons…
Panel factory reset: hold the top-left corner from power-on for 10 s (#…
Skeyelab commentedon Oct 26, 2026
Panel setup checklist (Eric)
This covers a panel on current main (#140 through #154). Every step happens at the panel, except where it says otherwise.
Before you start
-lan(for example-lan 192.168.1.0/24). The panel reaches it only over the Home network route.First start: admin password (Step 1 of 3)
Pair with your Helper (Step 2 of 3, Wi-Fi Mode)
phrased pairon your Computer. It prints a Pairing code and a line startingPin for a panel:.-lanand that the panel is on the same Wi-Fi, then reopen the Pair screen.phrased pairagain and use the new code and Pin. If it still doesn't match, stop and tell main.phrased pairagain; each code works once and only for a short time.Bluetooth Mode with your Mac (optional)
Factory reset (erases everything)
If anything else fails
Skeyelab commentedon Oct 27, 2026
Acceptance check against main (no panel)
lib/admin_password), #126 (the wizard: "Step 1 of 3: set an admin password")test_admin_password:test_short_passwords_and_low_iteration_counts_are_refused,test_only_the_right_password_checks_out. Eric went through this screen on 2026-10-26.web_server.cpp:367xTaskCreate(run, "web", ...), separate from the LVGL loop and from thenettasktest_web_editor:test_buttons_can_be_added_edited_moved_and_deleted,test_edits_apply_to_the_button_they_name,test_a_stale_edit_changes_nothing. #137 hardware run: the grid rebuilds with no reboot./phrases.jsonin LittleFS on thespiffspartition, written to.tmpthen renamed (main.cpp:363-368)test_phrases:test_the_list_round_trips_through_json,test_a_bad_file_is_refused_and_the_list_kept. #113 hardware: the list survives a reboot. A firmware update rewrites only the app (the partition table hasn't changed since #102), but no PR records a reflash check.lib/lockout, used by the web login in #133 (admin::check, 429)test_lockout(3 tests). #133 hardware: the 6th wrong try returns 429. #137: the lockout is still on after a reboot.Origin: https://<panel ip>(else 403), and the session cookie is__Host-s; HttpOnly; Secure; SameSite=Stricttest_web:test_a_post_needs_the_panels_own_origin,test_only_the_panels_own_address_is_an_allowed_hostphrases.h: 48 Buttons, a 40-byte label, a 2,000-byte Phrase;kMaxEditBodyon the requesttest_count_and_lengths_are_capped,test_control_characters_are_refusedtest_phrases,test_web_editor,test_admin_password,test_lockoutfirmwarecheck (pio test -e native)main.cpp:977)test_factory_reset(the hold logic). #138 hardware run.Gaps