Dropped the fault framing. The notes describe the mechanism and the change,
not whose code it was.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
beta_86 put the emoji picker on the long-press menu, which covers the firmware's
own field and so covers wada.ui.input(). It does not cover an app that builds its
own buffer from key events, because there is no textarea to long-press -- and
that is exactly the case Jade asked about: their composer shows a pad-bytes
counter the firmware's field cannot, so it takes the custom path and the menu
never applies to it.
wada.ui.emoji(cb) opens the picker and hands the glyph back as UTF-8, nil if
dismissed. Same discipline as wada.ui.input's callback: one ref, one call,
released either way, refused if one is already open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reported by Jade as "screen memory errors, worse the longer the app has been
open", with an app that rebuilds its screens on a 120 ms debounce.
Button and list-row callbacks live in one registry table keyed by the LVGL
object POINTER. clear() calls lv_obj_clean(), which frees every one of those
objects, and bumps s_ui_gen so stale handles go inert, but it never touched the
table. So each rebuild added an entry and kept the Lua closure, with its
upvalues, alive forever. Unbounded, and worst for exactly the apps that are
built properly: a list of twenty contacts redrawn on a timer leaks twenty
closures a go.
It was also a correctness bug in waiting. LVGL reuses freed addresses, so a new
button created WITHOUT a callback could land on an address a dead one left
behind and fire that stale closure on the next tap.
clear() now replaces the table. Every entry in it describes an object
lv_obj_clean just destroyed, so there is nothing there worth keeping.
Found by reading the host after Jade asked what the memory budget was; the
budget was never the problem.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Curated from the two beta notes. Leads with the two things a user must know
before this reaches their device: do not switch a T-Display P4 to landscape on
this build (fixed in beta_86), and a T-Deck Max on beta_84 must be updated over
USB rather than from the device.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Jade asked for three things for Lua apps. Two already existed and the answer is
documentation, not code: wada.sys.epoch() and wada.sys.datetime() have returned
the clock since #245, and wada.ui.input() is the firmware's own field, so it
already carries the long-press Cut/Copy/Paste/All/Sym menu, which is exactly the
"copy/paste from chats into a Lua text field" that was asked for.
The third is here. The long-press menu gains Emoji, so the picker reaches every
field in the firmware rather than only the chat composer: a Lua app's text entry,
a node name, a Wi-Fi password. The row is now sized to the panel instead of a
fixed 48 px per cell. It was already 268 px at five cells, wider than a 240 px
screen, so a sixth would have hung off the M9 and the V4 entirely.
Status bar: the right-hand ladder (battery, signal bars, Wi-Fi, Bluetooth, sleep,
clock) is a chain of fixed offsets tuned for 12 px text. The boards with
font-only UI-size presets grow the font and deliberately leave geometry at 100%,
so the labels widened and the ladder did not, and the battery percentage ran into
the signal bars (jd1227, M9, beta_85 -- the release that gave that board the
presets). SBX() scales those offsets by how much the text actually grew and
returns exactly SC() at Normal, so nobody who kept the default size sees any
change. Verified on the M9, V4-R8, Pager, T-Deck and V4.
SDK docs: corrected the app path. They said to copy the file to /apps/ on the SD
card; a card-backed device roots everything at /meshcomod, so it is
/meshcomod/apps/ there and a bare /apps/ only on boards that store internally.
Following the old wording cost gottlieb8849 an evening.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The five apps from #585 and #586 all defaulted to being seeded, which bakes
their source into the boards that cannot reach the Store. That is 40 KB of Lua,
and it took the Heltec V4 from 95.4% of flash to 96.7%: the tightest board in
the matrix paying for apps two of which it cannot use.
gen-lua-builtin.py states the rule: an app that is large, or that needs hardware
those boards do not have, earns its place rather than getting it by being in the
catalog. ProTreck (17 KB, altimeter/chrono/sensors) and Trip Odometer (GPS-only)
are both that, so they are seed:false, exactly like gpscompass and sdscan. They
remain in the Store for every board that can download.
Tetris, Breakout and Ping stay baked: no hardware needs, small, and the boards
that get seeded apps are precisely the ones that cannot fetch them otherwise.
V4 lands at 96.2%.
lua_builtin.h is regenerated here because it is tracked and the build rewrites
it. Leaving it out is how Wardrive and Nearby ended up missing from the seeded
boards after beta_68.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stacked on #585 and merged after it, as the PR asks. Two conflicts, both from
the stack base moving:
apps.json: unioned by id, so all fourteen apps survive.
harness.lua: the additive blocks came from #586. Where both sides edited the
same code, #585's is newer and wins: its ping scenarios pass cfg.gps as a
FUNCTION, which is what the harness calls (cfg.gps()); #586 still passed the
table and would have failed. Taking #586 there would have broken ping.
Then restored the two dispatch branches my first resolution dropped. The
breakout and tripodometer scenarios existed but nothing ran them, so both
apps silently fell through to the gpscompass scenarios and "failed".
All five new apps pass their own scenarios, and so do gpscompass and sdscan.
Closes#562, #563, #564, #568, #569, #570, #571, #574, #576 and adds the
Elecrow CrowPanel Advance 3.5 (#561). Carries the fix for the beta_85 P4
landscape touch, which was rotated twice: once by the driver and once by LVGL.
- New crowpanel_35 variant: display, SD, board and target files, plus board JSON
- Portrait mode is drawn rotated 180°; landscape stays at rotation 3
- Share the GT911 touch driver between Wio Tracker L2 and CrowPanel
(WioTrackerL2Touch.cpp -> src/helpers/input/LgfxGt911Touch.cpp)
- platformio envs, flasher/site entries, release and merge-bin support
- Tests for the CrowPanel SD codec and merge-bin
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132MkJ7RUV32mV9F7o4PJMm
Sun/Moon rise+set from GPS lat/lon + date (Spencer 1971), moon phase 0-29.
Chronometer with lap times, countdown timer with alarm beep and preset
cycling, altimeter with GPS alt_m, min/max persistence and trend graph.
Swipe left/right to switch tabs. 4 tabs fit any screen size.
test(harness): add 7 ProTreck scenarios (tdeck, v4, no_gps, chrono,
timer, alti, cost). ALL OK 7/7.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Send a DM to any contact, measure round-trip time. Both devices
exchange their GPS coordinates; the result shows RTT, distance and
cardinal bearing when both have a fix. The app also auto-replies to
incoming pings so two devices running Ping can time each other.
Harness additions: send_dm mock, wada.geo (haversine), deliver_dm
helper, and 6 scenarios (tdeck, v4, no_contacts, round_trip,
auto_reply, cost) — all pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tracks total distance (Haversine accumulation), elapsed time, current
speed, max speed, and altitude from live GPS fixes. Start/Stop button
pauses accumulation; Reset clears the trip. The current trip persists
across app restarts via wada.store.
Also adds 6 harness scenarios (no-fix, T-Deck, moving, persist, reset,
cost) and registers the app in apps.json.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ball + paddle + 8×5 brick grid. Swipe left/right (or arrow keys) to
move the paddle; swipe up / Enter / Space to launch. Angle on bounce
varies by paddle-hit position. Speed increases each level; high score
persists via wada.store.
Adds 6 harness scenarios (tdeck, v4, keyboard, logic, hiscore, cost).
Worst tick ~8k instructions, 8% of the 100k budget.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Detects /tetris/ in the app path and routes to 6 dedicated scenarios:
- tetris_tdeck/v4: open + swipe input on touch screens
- tetris_keyboard: arrow + enter input on keyboard boards
- tetris_logic: hard-drop to game-over, verify restart resets timer to 700 ms
- tetris_hiscore: verify high score is stored as a number after game-over
- tetris_cost: 100 ticks, worst ~6000 instructions (budget 100000)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds Tetris to the app catalog. Full 7-tetromino implementation with:
- Correct piece rotations for all 7 tetrominos (I, O, T, S, Z, J, L)
- Wall kick on rotation (tries +/-1 col when center position collides)
- Hard-drop on swipe-down or Enter/Space
- Soft-drop on keyboard arrow-down
- Score scaling by level (100/300/500/800 x level per 1/2/3/4 lines)
- Speed increase every 10 lines cleared (700ms to 80ms floor)
- Next-piece preview and high score persistence via wada.store
- Game-over overlay with tap-to-restart
- Centered layout adapts to any screen width/height
Tested against the wada.* API contract (integer coords, canvas bounds,
store string/number restriction, instruction budget per tick).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The P4's larger UI scale grew the button over the swatch, which sat at a fixed
x. On the P4 the button is narrower and the swatch sits a gap past its end, in
every orientation. Other boards are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On touch boards without a built-in keyboard, opening a chat focused the
composer and that focus raised the on-screen keys, covering the conversation.
The focus now only binds the composer (cursor shown, a Bluetooth, clip-on or
CardKB keyboard types straight in); tapping the composer brings the keys up.
T-Deck, T-Deck Pro and M9 are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the keyboard expansion arrives while the UI is portrait, a dialog counts
down 15 s and then saves landscape and restarts (the Settings orientation
switch's path); "Reboot Now" does it at once. Cancel, or taking the keyboard
off again, keeps portrait. A keyboard already attached at boot is not an
arrival, so a Cancel is not asked again on every start, and nothing is offered
under the lock screen. showConfirm() gains confirmSetMessage() to update the
open dialog's text in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GRWzzRs7oRvFRNcHUZF4c
Lock screen (all boards, #579): the default view is now drawn from parts
instead of the old 320x240 bitmap with the padlock and wordmark baked in,
which cover-scaled into a jumble on any other panel shape. WADAMESH sits at
the top, the clock below it (clear of the status bar), a smaller padlock in
the centre with the unread count to its left and a new battery readout to its
right, each centred between the padlock and the screen edge. Short panels get
a smaller wordmark and padlock. The clock's burn-in drift now moves around its
new place instead of snapping it back to y=30 every minute. T-Deck Pro keeps
its black-on-white e-paper colours; custom wallpapers are unchanged.
T-Display P4: the BOOT button (GPIO35) now works like the Wio Tracker L2's
wake button -- a press wakes or lights the lock screen, a 2 s hold locks or
unlocks -- so the P4 gets the lock screen and the "Lock when screen off"
setting. It gets no Power-off row: GPIO35 cannot wake the P4 from deep sleep.
Also: the chat list's per-row gear clears the list scrollbar on large panels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The T-Display P4 keyboard expansion (TCA8418 + its own XL9555, ported from
camillia-mt) now works. It is looked for every 1.5 s, so it can be clipped on
or off while running, and is routed like the CardKB and Bluetooth keyboards:
it types into the focused field and keeps the on-screen keyboard down. The
external-keyboard routing was gated on Bluetooth, which the P4 lacks, so it
now follows a new CAP_EXT_KEYBOARD; the Bluetooth-only parts stay on
CAP_BLE_KEYBOARD.
- Keys: Shift/Sym/Alt one-shot, Caps Lock lights the expansion LEDs, arrows
and Backspace repeat, Alt+Backspace backs out, F1-F5 open the tabs left to
right, and F11 steps the keyboard light through off/low/medium/high (saved
across reboots).
- Wire1 is shared with the ES8311 codec on other pins; P4I2c1 switches it
under a lock, and the codec's lazy bring-up now goes through it.
- With the keyboard attached the tab bar labels F1-F5 beside each icon, and
Enter on a chat bubble opens its action menu.
Also, on any keyboard build, Enter runs the command in the terminal and the
repeater admin console (the admin console's Enter hook sat on its Send button,
where nothing raised it). And on the P4: the status-bar name shrinks to fit the
bar, the Discover buttons are spaced out, contact names clear their icon in
landscape, and leaving an app returns the focus to its tile in the app drawer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The keyboard/trackball cursor is a 3 px accent outline, and on an accent
fill it disappears. setSelectionGlow already swaps to the contrast
outline (white, black on Day) there, but only for a fully opaque fill;
the Map's side buttons are accent at 70% over the tiles, so they kept
the accent outline and the selected one barely stood out. Any fill that
is at least half accent now takes the contrast outline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The accent popup keeps a pointer to the text box it edits, and about 25
separate paths close the popup by hand. A path that deleted the text
box without closing it (e.g. a form closing after its send) left the
popup on screen pointing at freed memory; tapping an accent letter then
ran lv_textarea_del_char on it. That is the beta_85 T-Deck crash in
#574 (loopTask LoadProhibited at 0x3, accentBoxCellCb ->
lv_textarea_del_char), matched against the release image.
Tie the popup to its text box with an LV_EVENT_DELETE hook that clears
the pointer and hides the popup (hide only: in a layer clean the popup
may be deleted in the same pass). The cell handler also checks the text
box is still valid, and accentBoxHide no longer deletes a popup that
something else already removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The right-aligned peak readout shares the RBW/span line, and on the
narrow portrait panel it ran over that text. In portrait the RBW/span
line, chart and waterfall start one text line lower; the peak stays put
and the tall panel has the room. Landscape, the T-Deck Pro (peak on its
own line already) and the other boards are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page offset its content by a second STATUSBAR_H to clear the tall
status bar's lower row, which overlaps the page on square panels. The
round panel's bar is already its full two rows and never doubles, so on
the P4 that only left an empty band above the Home button, in both
orientations. Start 8 px below the bar there instead.
In portrait, Flood and Zero-hop stack as two full-width buttons instead
of sharing a cramped row. Landscape and the other boards are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sheet's two-column grid of half-width buttons cut most labels off
with "..." on the narrow portrait panel. In portrait every action gets
its own full-width row; the card grows to fit and its body scrolls past
the screen height like any tall sheet. Delete stays the red bottom row.
Landscape and the other boards keep the grid.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The P4 is a large-screen board, so its quick-reply/emoji chips were
56 px against a 34x30 send button, all pinned to the composer's bottom
edge, where the landscape row sat under the panel's bottom corner arcs.
All three buttons are now 45 px (the average) in both orientations and
vertically centred on the text box, re-centred as it grows. That lifts
them ~9 px off the screen edge, with a floor matching the status bar's
top-corner clearance. In landscape the quick-reply button also moves
16 px in from the bottom-left corner. Replaces the 32 px the portrait
two-row change assumed from the small boards' sizes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Node name and advert location save on blur, and every blur saved and
showed "Name saved" / "Position saved", edited or not. On the M9 (no
touch) moving focus past a field with the d-pad is a blur, so the
alert flashed for each field crossed.
On blur, skip the save and the alert when the field still holds what
it was filled with: the name with missing glyphs replaced, the
position as "%.6f". An edit still saves and says so. This also stops a
bare blur writing the glyph-replaced display form over the real name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chan add <name> [key], chan add #tag, chan del <slot|name> and
chan key <slot|name>, in the on-device console and remote terminal.
They reuse the Channels screen's save and delete paths, so the chats
list updates at once. No rename: chat threads are matched to channels
by name, and the Channels screen has no rename for the same reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The channel/sender line was pinned at y=30 and the message at y=60,
so a title taller than 30 px (larger UI-size presets, bigger panels)
was drawn over the message, and the rest of a tall screen stayed empty.
Centre both horizontally and centre the pair vertically with 16 px
between them; the message box takes the height its text needs, capped
to what fits and cut at a whole line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The name started a fixed 24 px after the icon, but the antenna and room
glyphs are wider than the person glyph and all three grow with the
UI-size preset, so some names touched their icon. Start the name 8 px
past the widest of the three, never closer than before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The send button sat at the composer's bottom-right, under the panel's
rounded corner. Pull it in by the same 16 px safe-area the tab bar
uses, and narrow the text box to match.
Portrait: the single row (QR | emoji | text | send) was crunched on the
284-px panel. Split it into two rows: quick-reply and emoji
right-aligned on top, text and send below. All three buttons are 32 px,
midway between the old 30-px chips and the 34-px send button.
Landscape keeps the single row, and chatComposerApplyRows re-lays it
out when the keyboard's rotate button flips orientation with a chat
open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a Lua app was open, every physical-keyboard key went to the app,
and luaAppKey always claims the key, including while the app's own
wada.ui.input prompt was open. The prompt's text box never received a
character on the T-Deck, Pager, M9 or Attaky, or from a Bluetooth
keyboard (reported with Wardrive's "Name this run").
Don't forward keys to the app while that prompt is open, in
handleHwKey and both bleKbdDispatch paths; they then type into the
bound text box like any other prompt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In landscape the P4's tab bar barely responded to taps, while swipes between
tabs still worked.
P4 landscape is LVGL software rotation, and LVGL 8 rotates pointer input for a
rotated display itself (indev_pointer_proc): it expects points in the
unrotated panel frame. The touch driver was also mapping points to landscape
(heltecV4CapTouchSetPointRotation), on the belief that LVGL does not, so every
point was rotated twice. In ROT_90, the rotation the Orientation button picks,
a tap at (X, Y) reached LVGL at (615 - Y, X): a tap on the bar at the bottom
landed in the middle of the screen or below it, and never on a tab button.
Swipes were unaffected because the driver detects them in its own mapped
coordinates.
The driver keeps its landscape mapping, which its swipe detection and the
direct readers (tab-bar guard, remote-mode hold) need. lvglTouchRead now
converts the point back to the panel frame before giving it to LVGL, keyed on
the driver's rotation, so the chat keyboard's rotate trick on a portrait UI
(LVGL rotated, driver not) passes through unchanged. Also corrects the comment
in UITask::begin that said LVGL does not rotate input.
Not built or tested on hardware.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbY4FsGuX2fNDpNttB6a7R
In landscape the P4 home screen mixed its portrait column-flow layout with the
landscape right-hand button column: the TX/RX chart ran full width underneath
the buttons, and the info card was placed below the portrait launcher grid,
off the bottom of the 284-px-tall screen.
Landscape now follows the Wio Tracker L2's arrangement: status lines and a
chart sized to the remaining height on the left, clear of a right-hand column
of Advert / Terminal / Apps / Control spread evenly down the content height
(6-px gaps, each 22..46 px). The info card is portrait-only; its eight rows do
not fit landscape. The height is taken from the measured tab, since the
round-corner status bar leaves less than tabContentH() reports. Portrait is
unchanged.
Not built or tested on hardware.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbY4FsGuX2fNDpNttB6a7R
After a setting that reboots (theme, rotation, language, remote mode...) the
P4 came back with the panel dark, and only a second reboot brought it back.
esp_restart() on the ESP32-P4 is a CPU reset: esp_system_reset_modules_on_exit()
resets DMA, UARTs, SDMMC and crypto, but not the MIPI-DSI host or its DPI path.
The display driver then initialises over a host still half-configured from the
previous session and the panel stays dark. A full chip reset always recovers it.
So every software restart now becomes a full system reset: a shutdown handler,
installed first thing in app_main so it runs last, arms the RTC watchdog with
RESET_SYSTEM. Being a shutdown handler it covers rebootDevice(), OTA, the
console and factory reset without touching any of them. Same fix as
camillia-mt's T-Display P4 port.
Those restarts now arrive as ESP_RST_WDT, so an RTC_NOINIT marker records that
the restart was deliberate, and the crash-report prompt and Settings -> About
read the reset reason through bootResetReason(), which reports ESP_RST_SW for
them. Otherwise a lingering coredump would re-prompt "restarted after a crash"
on every settings reboot.
Not built here (no ESP-IDF toolchain in this environment); needs a P4 build and
an on-device check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbY4FsGuX2fNDpNttB6a7R
camillia-mt's working T-Display P4 GPS wakes the L76K through the XL9535
before opening UART1 on RX 22 / TX 23, starts at 9600 and probes other
rates until valid NMEA arrives. Wake and pins already match here; the
difference was the rate. The P4 used the core MicroNMEALocationProvider
locked to one rate (115200), so a receiver at any other rate gave an
open but silent UART.
Build the P4's provider as WadaNmeaLocationProvider with UART probing
(already on main via PR #517, used by the T-Deck Pro), starting at 9600:
it walks 9600/19200/38400/57600/115200 until a checksummed sentence
arrives and saves the working rate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "Sync time from saved Wi-Fi after cold boot" option (#383) existed
only on the T-Deck and ThinkNode M9. The P4's PCF8563 has come back with
impossible dates too, so enable it there: the Settings > Clock switches
(CAP_BOOT_TIME_SYNC), the runtime gate in BootTimeSync.cpp, and a call
in the P4's own startup at the same point the S3 main.cpp uses, before
the TCP/WS/BLE transports start.
Esp-hosted builds only: the sync drives Arduino's real WiFi, which the
legacy ESP-AT build must never touch. As elsewhere it is opt-in, runs
only after a true power-on, and skips when the RTC already gave a
trustworthy time or Wi-Fi is on anyway.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The track started 4 px in at 96% width, but the knob overhangs each end
by its 6 px padding plus half the track height, so at the lowest setting
it was drawn past the left edge. Inset the track by that overhang on both
sides of the content width.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The mirror page only forwarded key presses: its key handler skips
anything with Ctrl/Cmd held, so Ctrl+V / Cmd+V did nothing, and a paste
into the hidden input phones use was discarded. Catch the document
'paste' event and type the clipboard text into the device's focused
field, and add a Paste button (a prompt box) for phones, where a
plain-HTTP page cannot read the clipboard from script.
Pasted text goes out in batches of 16 keys every 80 ms so the device's
256-entry key ring is never overrun; line breaks become spaces so a
multi-line paste cannot press Enter and send a half-typed message;
characters outside the BMP are dropped (keys are 16-bit). Same page on
every board.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WEB_UI_PORT_STR was 5000 on every P4 build, from the ESP-AT days when the
single AT listener shared the companion port. The esp-hosted build serves
the web UI on WS_PORT (8765) like the other boards, so the VNC, Remote,
Files, terminal and remote-placeholder addresses pointed at the wrong
port. Keep 5000 only for WADA_P4_LEGACY_AT builds; other boards were
already correct.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The switch label drops from the 14 px to the 12 px body font and the
browser address from 16 px to 14 px, so both fit comfortably.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The form hugged the header and left most of the tall page empty, so on
on-screen-keyboard boards start it where its Join button ends just above
the keyboard's top edge (keyboard height as kbApplyLayoutForRotation sets
it: CHAT_KB_H portrait, half the screen landscape). Physical-keyboard
boards, and screens without the spare room, keep the old layout.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>