Skip to content

Adopted-card bench: the empty deploy drawer, and Reboot device on the frame's own page - #490

Merged
mariusandra merged 7 commits into
mainfrom
adopted-card-bench-fixes
Sep 13, 2026
Merged

Adopted-card bench: the empty deploy drawer, and Reboot device on the frame's own page#490
mariusandra merged 7 commits into
mainfrom
adopted-card-bench-fixes

Conversation

@mariusandra

@mariusandra mariusandra commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Two findings from the 2026-09-13 bench on frame 14 — a generic Buildroot card adopted into the 2026.9.15 Home Assistant add-on — plus the todo bookkeeping that closes the whole non-ESP32 side of the manual testing list.

The deploy button was dirty with an empty drawer

Right after adoption the button said there was something to save and the drawer listed nothing.

sanitizeFrame seeds the form with buildroot.compilationMode: '' whether or not the row has that key. It does the same for rpios — but rpios has normalizeRpiosForComparison to undo it and buildroot had no normalizer at all, so the key read as an unsaved edit forever.

It took an adopted card to make it visible, because two things had to line up: adoption writes buildroot as {adopted: true} with no compilationMode, and buildroot is in SHELL_LESS_BACKEND_ONLY_KEYS — which computeChangeDetails deliberately hides on a shell-less frame while unsavedChanges still counted it. Dirty button, nothing to show.

  • normalizeBuildrootForComparison in frameLogic.ts, mirroring the rpios one
  • a shared-spa regression test, verified red without the fix rather than just asserting the diagnosis

"Reboot device" on the frame's own admin page

It works from the backend, so it should work from the frame. The device did not serve POST /api/frames/@id/reboot — the backend reaches a shell-less frame's reboot through POST /event/reboot (_shell_less_control_event in tasks/restart_frame.py). Rather than special-case the SPA, the device now answers that path with the same reboot control event, mirroring /restart, so framesModel.rebootFrame works unchanged on both control planes.

reboot was already a ControlEvent gated on an admin session or the serverApiKey bearer, so this adds no authorization surface. The menu now reads Save / Re-render / Reload runtime / Restart FrameOS / Reboot device, with a confirm; Restart takes ArrowUturnUpIcon so the power icon means the bigger hammer.

The "crash" after the first save was not a crash

Pulled the card and read its logs. The runtime went down and came back because the save asked it to:

02:11:00  http POST /api/frames/1
02:11:00  event:restart
02:11:00  restart "Restarting FrameOS runtime"
02:11:07  startup

runtime/frameos-last-exit reads serviceResult=success exitCode=exited exitStatus=0, state/boot_guard.json reads crashesWithoutRender: 0, and the whole session's log holds exactly one restart and zero errors. A save restarts by design when a frameConfigRestartKeys value differs (classifyFrameConfigChange).

Worth writing down: boot:guard crashesWithoutRender: N means "this boot has not rendered yet", not "it crashed N times"registerBootCrash() runs on every startup and logs previous+1 before the first render. That misreading is what made the 2026-09-14 activate-then-restart sighting look like a crash too.

docs/manual-testing-todo.md

Every non-ESP32 box is now closed:

  • Standalone on-device admin, round three — PASSED on 2026.9.15, with the restart explained above.
  • Adopted card, round two/three — PASSED: pending changes opened clean (the 2026-09-09 network-secrets regression is gone), restart and reboot worked with no shell, and a second backend adopting the card logged server:changed to the first, naming the new server.
  • "Generic image still adopts with no Remote on it" — CLOSED as obsolete. Written 2026-09-04 for a world where the backend SSHed in and installed Remote; since remote lite (Admin-API-only frames ("remote lite"): assets, fonts, service keys, scene activation and snapshots over the frame's own API #485) a generic card has no shell at all and frame_has_shell_access() says outright that the SSH/Remote deploy "cannot even connect". Nothing to install, and frameos.service stays User=frameos (verified on the card).

Only the ESP32 bench is left, and every box there needs hardware that was not to hand.

The status screen is a frame action, not a scene

system/index was the first tile in every frame's scene list: a scene you
cannot edit, rename or delete, taking a slot from the ones you can. It moves to
the frame's actions menu as Render status screen, which sends the activation
outright — no drawer, one click.

framesModel.renderStatusScreen sends it rather than controlLogic: the frames
home draws a menu per card, and mounting a keyed controlLogic for each would
fetch every frame's state (the same reason FrameSceneTile uses
sceneUpdatesLogic). It is the same setCurrentScene event a scene tile sends,
so both control planes answer it unchanged — the cloud shim keeps the plain
set_current_scene select for an id nothing is assigned to. With the tile gone,
"Show status screen" in the list's display menu has nothing to switch, so it and
its stored flag go too. The drawer itself stays: a restored
?drawer=scene&sceneId=system/index still lands on it.

The scene list keeps its current layout and A-Z order here — the one-row rewrite
and "sort by recently used" are #491's, and this branch no longer carries them.
The two still both touch FrameDashboardSurface.tsx, but only in the block that
renders the tile, so whichever lands second should resolve cleanly.

Verification

  • cloud pnpm verify — 28/28
  • frontend npx tsc --noEmit — clean
  • nim c -r src/frameos/server/tests/test_frame_api_routes_behavior.nim — 13/13, including the new /reboot assertions and its 404 on a mismatched frame id
  • frontend npx kea-typegen check --inline — clean

🤖 Generated with Claude Code

https://claude.ai/code/session_01JkGsPJHBxQApJoLE9d9hr2

… frame's own page

Two findings from the 2026-09-13 bench on frame 14 (a generic Buildroot card
adopted into the 2026.9.15 Home Assistant add-on), plus the todo bookkeeping
that closes the whole non-ESP32 side of the list.

**The deploy button was dirty with an empty drawer.** Right after adoption the
button said there was something to save and the drawer listed nothing.
`sanitizeFrame` seeds the form with `buildroot.compilationMode: ''` whether or
not the row has that key — it does the same for `rpios`, which HAS
`normalizeRpiosForComparison` to undo it, while `buildroot` had no normalizer
at all — so the key read as an unsaved edit forever. It took an adopted card
to make it visible: adoption writes `buildroot` as `{adopted: true}` with no
compilationMode, and `buildroot` is in `SHELL_LESS_BACKEND_ONLY_KEYS`, which
`computeChangeDetails` hides on a shell-less frame while `unsavedChanges`
still counted it. Dirty button, nothing to show.

- frameLogic.ts: `normalizeBuildrootForComparison`, mirroring the rpios one
- a shared-spa regression test, verified red without the fix

**"Reboot device" on the frame's own admin page.** It works from the backend,
so it should work from the frame. The device did not serve
`POST /api/frames/@id/reboot` — the backend reaches a shell-less frame's
reboot through `POST /event/reboot` (`_shell_less_control_event`) — so rather
than special-case the SPA, the device now answers that path with the same
`reboot` control event, mirroring `/restart`. `framesModel.rebootFrame` then
works unchanged on both control planes. `reboot` was already a ControlEvent
gated on an admin session or the serverApiKey bearer, so no new authorization
surface. Restart FrameOS takes ArrowUturnUp so the power icon means the bigger
hammer.

**docs/manual-testing-todo.md** — the non-ESP32 boxes are closed:

- Standalone on-device admin round three PASSED on 2026.9.15. The runtime
  going down after the first save was NOT a crash: the card's logs show
  `POST /api/frames/1` -> `event:restart` -> back in 7 s, with
  `frameos-last-exit` reading `exitCode=exited exitStatus=0` and
  `boot_guard.json` at `crashesWithoutRender: 0`. A save restarts by design
  when a `frameConfigRestartKeys` value differs.
- Recorded that `boot:guard crashesWithoutRender: N` means "this boot has not
  rendered yet", not "it crashed N times" — `registerBootCrash()` runs on
  every startup and logs previous+1 before the first render. That misreading
  is what made the 2026-09-14 sighting look like a crash too.
- Adopted card round two/three PASSED: pending changes opened clean (the
  2026-09-09 network-secrets regression is gone), restart and reboot worked
  with no shell, and a second backend adopting the card logged `server:changed`
  to the first.
- "Generic image still adopts with no Remote on it" CLOSED as obsolete. It was
  written 2026-09-04 for a world where the backend SSHed in and installed
  Remote; since remote lite (#485) a generic card has no shell at all and
  `frame_has_shell_access()` routes it down the admin-API path.

Only the ESP32 bench is left, and every box there needs hardware.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkGsPJHBxQApJoLE9d9hr2
@github-actions

github-actions Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

ESP32 firmware size

Built from c22e9db by this run; compared with the latest release v2026.9.15.

Image This PR v2026.9.15 Δ
esp32-s3 app (OTA image) 3,160,224
87.7% of 3520K slot, 434 KB free
3,160,224 ±0
esp32-s3 merged flash image 3,225,760 3,225,760 ±0
esp32-s3-32mb app (OTA image) 3,160,208
76.5% of 4032K slot, 946 KB free
3,160,208 ±0
esp32-s3-32mb merged flash image 3,291,280 3,291,280 ±0

Breakdown by subsystem — esp32-s3

Subsystem This PR v2026.9.15 Δ
FrameOS core (Nim) 404,620 404,620 ±0
QuickJS 356,328 356,328 ±0
FrameOS apps (Nim) 316,786 316,786 ±0
pixie 300,949 300,949 ±0
Wi-Fi stack 295,300 295,300 ±0
Nim stdlib 249,026 249,026 ±0
ESP-IDF misc 178,065 178,065 ±0
Embedded font 146,358 146,358 ±0
String pool (attributed to efuse) 132,857 132,857 ±0
mbedTLS + certificates 132,539 132,539 ±0
lwIP / HTTP / WebSocket 126,644 126,644 ±0
fos_* firmware shell (C) 116,897 116,897 ±0
libc / libm / newlib 116,259 116,259 ±0
Storage (SPIFFS / FatFS / SD / NVS) 94,132 94,132 ±0
Nim packages (chrono, zippy, chroma, qrgen, ...) 72,043 72,043 ±0
Display drivers (C) 64,934 64,934 ±0
Crypto (monocypher) 35,149 35,149 ±0
Total mapped flash 3,138,886 3,138,886 ±0
Inside each subsystem

FrameOS core (Nim) — 404,620

Part This PR v2026.9.15 Δ
frameos/js_runtime 102,097 102,097 ±0
frameos/interpreter.nim 97,276 97,276 ±0
frameos/utils 89,928 89,928 ±0
embedded_main.nim 18,319 18,319 ±0
lib/tz.nim 16,751 16,751 ±0
embedded_runtime.nim 15,056 15,056 ±0
frameos_nim_glue.c.obj 9,814 9,814 ±0
frameos/planner.nim 9,605 9,605 ±0
frameos/types.nim 7,913 7,913 ±0
frameos/values.nim 6,873 6,873 ±0
frameos/apps.nim 6,054 6,054 ±0
frameos/spool.nim 5,678 5,678 ±0
embedded_scene.nim 4,945 4,945 ±0
frameos/app_config.nim 2,871 2,871 ±0
frameos/reboot_reason.nim 2,628 2,628 ±0
frameos/node_config.nim 2,590 2,590 ±0
frameos/app_capabilities.nim 2,489 2,489 ±0
fos_netguard.c.obj 2,092 2,092 ±0
frameos/runtime_diagnostics.nim 763 763 ±0
fos_version.c.obj 324 324 ±0
frameos/cloud 255 255 ±0
frameos/ids.nim 137 137 ±0
frameos/channels.nim 122 122 ±0
frameos/hal 40 40 ±0

QuickJS — 356,328

Part This PR v2026.9.15 Δ
quickjs.c.obj 283,813 283,813 ±0
libunicode.c.obj 50,174 50,174 ±0
libregexp.c.obj 13,995 13,995 ±0
dtoa.c.obj 5,681 5,681 ±0
cutils.c.obj 2,141 2,141 ±0
fos_qjs_glue.c.obj 449 449 ±0
fos_quickjs_tz.c.obj 75 75 ±0

FrameOS apps (Nim) — 316,786

Part This PR v2026.9.15 Δ
apps/data/icalJson 50,265 50,265 ±0
apps/render/calendar 30,707 30,707 ±0
apps/render/chart 21,541 21,541 ±0
apps/data/immich 17,695 17,695 ±0
apps/data/wikicommons 17,110 17,110 ±0
apps/data/openaiImage 13,970 13,970 ±0
apps/data/unsplash 11,084 11,084 ±0
apps/data/weather 10,193 10,193 ±0
apps/data/beRecycle 9,918 9,918 ±0
apps/render/split 9,648 9,648 ±0
apps/data/localImage 9,202 9,202 ±0
apps/data/eventsToAgenda 9,060 9,060 ±0
apps/data/googlePhotos 9,044 9,044 ±0
apps/apps.nim 8,636 8,636 ±0
apps/render/zoomPan 8,544 8,544 ±0
apps/data/haSensor 7,287 7,287 ±0
apps/data/openaiText 6,762 6,762 ±0
apps/data/xmlToJson 5,420 5,420 ±0
apps/render/text 5,419 5,419 ±0
apps/render/svg 5,176 5,176 ±0
apps/render/image 4,510 4,510 ±0
apps/data/rotateImage 3,457 3,457 ±0
apps/logic/setAsState 3,441 3,441 ±0
apps/data/downloadImage 3,345 3,345 ±0
apps/render/gradient 3,173 3,173 ±0
apps/data/qr 3,118 3,118 ±0
apps/data/frameOSGallery 3,046 3,046 ±0
apps/data/downloadUrl 3,045 3,045 ±0
apps/render/color 2,804 2,804 ±0
apps/data/newImage 2,676 2,676 ±0
apps/data/resizeImage 2,550 2,550 ±0
apps/data/log 2,283 2,283 ±0
apps/render/opacity 2,272 2,272 ±0
apps/data/prettyJson 2,213 2,213 ±0
apps/data/parseJson 1,844 1,844 ±0
apps/data/clock 1,835 1,835 ±0
apps/logic/nextSleepDuration 1,639 1,639 ±0
apps/logic/ifElse 1,476 1,476 ±0
apps/logic/breakIfRendering 1,378 1,378 ±0

pixie — 300,949

Part This PR v2026.9.15 Δ
pixie/fontformats/opentype.nim 55,507 55,507 ±0
pixie/fileformats/webp.nim 37,525 37,525 ±0
pixie/fileformats/svg.nim 37,143 37,143 ±0
pixie/paths.nim 32,570 32,570 ±0
pixie/fileformats/jpeg.nim 25,338 25,338 ±0
pixie/fileformats/png.nim 22,849 22,849 ±0
pixie/images.nim 15,783 15,783 ±0
pixie/fonts.nim 11,019 11,019 ±0
pixie/paints.nim 9,889 9,889 ±0
pixie/fileformats/bmp.nim 8,234 8,234 ±0
pixie/common.nim 7,416 7,416 ±0
pixie/blends.nim 6,520 6,520 ±0
pixie/fileformats/gif.nim 5,925 5,925 ±0
pixie/inflatestream.nim 5,325 5,325 ±0
pixie.nim 5,224 5,224 ±0
pixie/fileformats/webp_vp8_tables.nim 3,881 3,881 ±0
pixie/fileformats/ppm.nim 3,681 3,681 ±0
pixie/contexts.nim 3,038 3,038 ±0
pixie/fileformats/qoi.nim 2,682 2,682 ±0
pixie/internal.nim 754 754 ±0
pixie/rgb565.nim 381 381 ±0
pixie/fontformats/svgfont.nim 215 215 ±0
pixie/decodebudget.nim 50 50 ±0

Wi-Fi stack — 295,300

Part This PR v2026.9.15 Δ
libnet80211.a 136,168 136,168 ±0
libpp.a 62,154 62,154 ±0
libwpa_supplicant.a 54,900 54,900 ±0
libphy.a 34,176 34,176 ±0
libesp_wifi.a 4,780 4,780 ±0
libesp_phy.a 2,541 2,541 ±0
libesp_coex.a 291 291 ±0
libcore.a 287 287 ±0
libespnow.a 3 3 ±0

Nim stdlib — 249,026

Part This PR v2026.9.15 Δ
pure/collections/tables.nim 70,556 70,556 ±0
system.nim 46,659 46,659 ±0
pure/unicode.nim 17,405 17,405 ±0
pure/times.nim 16,984 16,984 ±0
pure/json.nim 13,414 13,414 ±0
std/private/dragonbox.nim 10,351 10,351 ±0
pure/strutils.nim 8,520 8,520 ±0
pure/parsexml.nim 5,999 5,999 ±0
pure/algorithm.nim 5,368 5,368 ±0
pure/base64.nim 5,302 5,302 ±0
pure/hashes.nim 4,114 4,114 ±0
pure/xmlparser.nim 3,567 3,567 ±0
pure/parsejson.nim 3,553 3,553 ±0
std/formatfloat.nim 3,508 3,508 ±0
pure/uri.nim 3,225 3,225 ±0
pure/collections/sets.nim 3,211 3,211 ±0
pure/streams.nim 2,820 2,820 ±0
pure/strformat.nim 2,641 2,641 ±0
std/syncio.nim 2,355 2,355 ±0
pure/xmltree.nim 2,051 2,051 ±0
pure/lexbase.nim 1,805 1,805 ±0
pure/parseutils.nim 1,757 1,757 ±0
system/exceptions.nim 1,598 1,598 ±0
pure/strtabs.nim 1,568 1,568 ±0
pure/pathnorm.nim 1,356 1,356 ±0
pure/options.nim 1,118 1,118 ±0
std/private/ospaths2.nim 1,032 1,032 ±0
pure/collections/lists.nim 983 983 ±0
system/dollars.nim 891 891 ±0
pure/random.nim 835 835 ±0
std/private/digitsutils.nim 651 651 ±0
pure/math.nim 650 650 ±0
std/oserrors.nim 637 637 ±0
std/private/osdirs.nim 580 580 ±0
std/monotimes.nim 401 401 ±0
std/private/oscommon.nim 329 329 ±0
std/envvars.nim 250 250 ±0
std/private/decode_helpers.nim 240 240 ±0
std/private/ossymlinks.nim 187 187 ±0
pure/bitops.nim 162 162 ±0
pure/os.nim 99 99 ±0
std/private/osfiles.nim 96 96 ±0
std/assertions.nim 87 87 ±0
core/macros.nim 69 69 ±0
system/iterators.nim 33 33 ±0
system/ctypes.nim 9 9 ±0

ESP-IDF misc — 178,065

Part This PR v2026.9.15 Δ
libesp_hw_support.a 37,650 37,650 ±0
libhal.a 21,464 21,464 ±0
libfreertos.a 17,194 17,194 ±0
libesp_system.a 13,780 13,780 ±0
libesp_driver_uart.a 11,627 11,627 ±0
libheap.a 8,660 8,660 ±0
libesp_driver_i2c.a 8,062 8,062 ±0
libesp_driver_spi.a 7,756 7,756 ±0
libconsole.a 5,733 5,733 ±0
libjson.a 4,940 4,940 ±0
libesp_driver_gpio.a 3,890 3,890 ±0
libbootloader_support.a 3,813 3,813 ±0
libxtensa.a 3,691 3,691 ±0
libesp_driver_usb_serial_jtag.a 3,408 3,408 ±0
libesp_ringbuf.a 3,322 3,322 ±0
libesp_event.a 3,257 3,257 ±0
libesp_mm.a 2,451 2,451 ±0
libapp_update.a 2,422 2,422 ±0
libesp_psram.a 2,329 2,329 ±0
libesp_timer.a 2,326 2,326 ±0
libsoc.a 2,315 2,315 ±0
libesp_adc.a 2,289 2,289 ±0
liblog.a 1,375 1,375 ±0
libpthread.a 1,365 1,365 ±0
libesp_security.a 1,050 1,050 ±0
libesp_rom.a 902 902 ±0
libesp_app_format.a 507 507 ±0
libxt_hal.a 437 437 ±0
libesp_common.a 26 26 ±0
libesp_pm.a 24 24 ±0

Embedded font — 146,358

Part This PR v2026.9.15 Δ
assets/fonts.nim 146,358 146,358 ±0

String pool (attributed to efuse) — 132,857

Part This PR v2026.9.15 Δ
esp_efuse_utility.c.obj 131,811 131,811 ±0
esp_efuse_rtc_calib.c.obj 566 566 ±0
esp_efuse_table.c.obj 268 268 ±0
esp_efuse_api.c.obj 151 151 ±0
esp_efuse_startup.c.obj 61 61 ±0

mbedTLS + certificates — 132,539

Part This PR v2026.9.15 Δ
libmbedcrypto.a 71,391 71,391 ±0
libmbedtls.a 45,803 45,803 ±0
libmbedx509.a 7,084 7,084 ±0
libesp-tls.a 6,709 6,709 ±0
libesp_https_server.a 1,552 1,552 ±0

lwIP / HTTP / WebSocket — 126,644

Part This PR v2026.9.15 Δ
liblwip.a 78,305 78,305 ±0
libhttp_parser.a 10,350 10,350 ±0
libesp_http_server.a 10,016 10,016 ±0
libtcp_transport.a 8,237 8,237 ±0
libespressif__esp_websocket_client.a 6,826 6,826 ±0
libesp_http_client.a 6,666 6,666 ±0
libesp_netif.a 6,244 6,244 ±0

fos_ firmware shell (C)* — 116,897

Part This PR v2026.9.15 Δ
fos_http.c.obj 22,202 22,202 ±0
fos_cloud.c.obj 16,975 16,975 ±0
fos_console.c.obj 13,601 13,601 ±0
fos_client.c.obj 10,499 10,499 ±0
fos_scenes.c.obj 7,551 7,551 ±0
fos_settings.c.obj 6,511 6,511 ±0
fos_ota.c.obj 5,039 5,039 ±0
fos_config.c.obj 4,298 4,298 ±0
fos_wifi.c.obj 3,201 3,201 ±0
fos_assets.c.obj 3,124 3,124 ±0
fos_cloud_contract.c.obj 2,896 2,896 ±0
fos_sd_probe.c.obj 2,753 2,753 ±0
fos_assets_sd.c.obj 2,480 2,480 ±0
fos_schedule.c.obj 2,351 2,351 ±0
fos_buttons.c.obj 2,139 2,139 ±0
fos_status_screen.c.obj 2,049 2,049 ±0
main.c.obj 1,667 1,667 ±0
fos_config_parse.c.obj 1,467 1,467 ±0
fos_url_guard.c.obj 1,397 1,397 ±0
fos_tz.c.obj 1,381 1,381 ±0
fos_battery.c.obj 1,140 1,140 ±0
fos_minisig.c.obj 497 497 ±0
fos_framebuffer.c.obj 483 483 ±0
fos_assets_path.c.obj 259 259 ±0
fos_power.c.obj 241 241 ±0
fos_battery_filter.c.obj 214 214 ±0
fos_wake.c.obj 184 184 ±0
fos_board.c.obj 179 179 ±0
fos_json_guard.c.obj 119 119 ±0

libc / libm / newlib — 116,259

Part This PR v2026.9.15 Δ
libc.a 76,467 76,467 ±0
libm.a 31,563 31,563 ±0
libnewlib.a 6,609 6,609 ±0
libstdc++.a 1,476 1,476 ±0
libgcc.a 98 98 ±0
libcxx.a 46 46 ±0

Storage (SPIFFS / FatFS / SD / NVS) — 94,132

Part This PR v2026.9.15 Δ
libspiffs.a 20,380 20,380 ±0
libfatfs.a 20,133 20,133 ±0
libspi_flash.a 14,168 14,168 ±0
libnvs_flash.a 13,741 13,741 ±0
libsdmmc.a 13,013 13,013 ±0
libvfs.a 6,196 6,196 ±0
libesp_driver_sdspi.a 3,858 3,858 ±0
libesp_partition.a 1,960 1,960 ±0
libesp_vfs_console.a 678 678 ±0
libnvs_sec_provider.a 5 5 ±0

Nim packages (chrono, zippy, chroma, qrgen, ...) — 72,043

Part This PR v2026.9.15 Δ
QRgen 3.1.0 22,036 22,036 ±0
chrono 0.3.1 15,510 15,510 ±0
zippy 0.10.19 15,280 15,280 ±0
chroma 1.0.0 14,924 14,924 ±0
checksums 0.2.1 3,371 3,371 ±0
flatty 0.3.4 533 533 ±0
jsony 1.1.5 272 272 ±0
bumpy 1.1.3 117 117 ±0

Display drivers (C) — 64,934

Part This PR v2026.9.15 Δ
(everything else) 21,290 21,290 ±0
frameos_panel_table.c.obj 6,685 6,685 ±0
EPD_13in3e.c.obj 1,792 1,792 ±0
EPD_3in7.c.obj 1,564 1,564 ±0
EPD_5in79.c.obj 1,481 1,481 ±0
DEV_Debug.c.obj 1,309 1,309 ±0
EPD_2in7.c.obj 1,292 1,292 ±0
EPD_4in2.c.obj 1,262 1,262 ±0
EPD_7in3e.c.obj 1,208 1,208 ±0
EPD_4in2_V2.c.obj 1,184 1,184 ±0
DEV_Config_esp.c.obj 1,180 1,180 ±0
EPD_4in26.c.obj 1,123 1,123 ±0
29 smaller rows 23,564 23,564 ±0

Crypto (monocypher) — 35,149

Part This PR v2026.9.15 Δ
monocypher.c.obj 31,916 31,916 ±0
monocypher-ed25519.c.obj 3,233 3,233 ±0
Largest 30 objects
Object Subsystem This PR v2026.9.15 Δ
quickjs.c.obj QuickJS 283,813 283,813 ±0
assets/fonts.nim Embedded font 146,358 146,358 ±0
esp_efuse_utility.c.obj String pool (attributed to efuse) 131,811 131,811 ±0
frameos/interpreter.nim FrameOS core (Nim) 97,276 97,276 ±0
nim/lib/pure/collections/tables.nim Nim stdlib 70,556 70,556 ±0
frameos/js_runtime/app_runtime.nim FrameOS core (Nim) 61,493 61,493 ±0
pkgs/pixie-6.1.0/pixie/fontformats/opentype.nim pixie 55,507 55,507 ±0
libunicode.c.obj QuickJS 50,174 50,174 ±0
nim/lib/system.nim Nim stdlib 46,659 46,659 ±0
apps/data/icalJson/ical.nim FrameOS apps (Nim) 44,568 44,568 ±0
pkgs/pixie-6.1.0/pixie/fileformats/webp.nim pixie 37,525 37,525 ±0
pkgs/pixie-6.1.0/pixie/fileformats/svg.nim pixie 37,143 37,143 ±0
frameos/utils/image.nim FrameOS core (Nim) 34,490 34,490 ±0
pkgs/pixie-6.1.0/pixie/paths.nim pixie 32,570 32,570 ±0
monocypher.c.obj Crypto (monocypher) 31,916 31,916 ±0
frameos/js_runtime/runtime.nim FrameOS core (Nim) 30,086 30,086 ±0
pkgs/pixie-6.1.0/pixie/fileformats/jpeg.nim pixie 25,338 25,338 ±0
apps/render/calendar/app.nim FrameOS apps (Nim) 23,659 23,659 ±0
pkgs/pixie-6.1.0/pixie/fileformats/png.nim pixie 22,849 22,849 ±0
fos_http.c.obj fos_* firmware shell (C) 22,202 22,202 ±0
apps/render/chart/app.nim FrameOS apps (Nim) 18,940 18,940 ±0
embedded_main.nim FrameOS core (Nim) 18,319 18,319 ±0
frameos/utils/status_screen.nim FrameOS core (Nim) 18,111 18,111 ±0
x509_crt_bundle.S.obj mbedTLS + certificates 17,928 17,928 ±0
nim/lib/pure/unicode.nim Nim stdlib 17,405 17,405 ±0
nim/lib/pure/times.nim Nim stdlib 16,984 16,984 ±0
fos_cloud.c.obj fos_* firmware shell (C) 16,975 16,975 ±0
lib/tz.nim FrameOS core (Nim) 16,751 16,751 ±0
apps/data/immich/app.nim FrameOS apps (Nim) 16,148 16,148 ±0
pkgs/pixie-6.1.0/pixie/images.nim pixie 15,783 15,783 ±0

Flash = .text + .rodata from the linker map via esp_idf_size; "Inside each subsystem" splits every bucket one level down (the Nim core by directory, apps by app, packages by package, ESP-IDF by archive) over all objects, not just the largest. "String pool (attributed to efuse)" is the linker's merged string-literal pool for the whole image, not efuse code — see docs/esp32-image-size.md.

mariusandra and others added 5 commits September 13, 2026 03:06
…urface, and a clean backend disconnect

Bench follow-ups from the 2026-09-13 session, mostly small, three of them not.

**Settings page (standalone admin).** The cloud box is a tag and two switches
now: each switch's paragraph moved behind the (i) it always should have been,
and only a blocker still takes a line of its own. "Clear serverHost in
frame.json first" was advice with no destination — it is a link to Backend
host under "Backend access" (scrollToFrameHttpApiSection generalised to
scrollToFrameSettingsSection). The WiFi country hint became a tooltip. The
"Service secrets" boxes collapse (a dozen services of key fields pushed every
other setting off the page; the summary row still says Saved/Missing). HTTPS
moved directly under the HTTP API block it secures, rather than two sections
below it. "Frame admin panel (BETA)" lost the tag and the paragraph; how to
reach it, and its URL, are in an (i) on the heading. The cert textareas are
three rows. And the form no longer butts against the cloud box above it — its
first heading's margin was collapsing through the form element.

**The status screen is listed on every surface.** `system/index` is a real
scene every Linux frame draws on request, but only the on-device page listed
it, so from a backend or the cloud there was no way to put it on the panel at
all. It is in the scene list everywhere now, behind a new "Show status screen"
switch in the list's display menu (stored per frame per surface, default on,
next to the two that were already there). Not offered for ESP32 frames, whose
firmware draws its own boot screen in C.

**Upgrade progress reaches the frame's own log.** `frameos upgrade` runs
detached and reports only through its status file, and the only thing watching
that file lived in the cloud hub client — so an upgrade started from the
frame's admin page showed a spinner and then silence. The watch is a
UpgradeLogWatcher in frameos/upgrade now, driven from the runner's message
loop, and the hub client's copy is gone: one implementation, and the lines
ride the ordinary log shipper to every surface, the cloud included.

**A frame leaving a backend says so.** Clearing the server URL on the device
was the one move that produced no notice at all: the backend kept the frame,
stopped hearing from it, and then offered a frame.json merge for its own
address ("Server Host — Backend: 10.4.0.47 / Frame: Not set", two equally
wrong answers). `backendChangeNotice` now returns a `server:detached` notice
for it, delivered to the backend being left exactly as `server:changed` is,
and the four `server_*` keys became FRAME_SYNC_BACKEND_OWNED_KEYS so they
never reach the sync diff again (the admin-API deploy still carries them, via
FRAME_PUSH_EXTRA_KEYS — that push is how an adopted card learns where to send
its logs).

**The first save on a fresh card no longer restarts the runtime.** Predicted
in the last commit's todo entry and duly hit on the bench. `apiDeviceConfig`
serves the typed device config when frame.json has none, renaming
httpUploadUrl to uploadUrl and dropping the -1 "driver default" pins; the SPA
posts that back and `deviceConfig` read as changed exactly once.
`canonicalDeviceConfig` compares both sides in one spelling.

Also: the status screen's blurb is plain English and one em dash shorter; the
index scene says nothing about GPIO buttons when none are wired up, and drops
the Inputs row entirely when nothing can drive the frame; a cloud-managed
frame's "Remote control" row says "enabled (no shell access)"; "Reboot device"
sits in the frame admin menu (the device answers POST /api/frames/@id/reboot
now, mirroring /restart); a loading frame image gets a corner spinner instead
of pulsing the whole picture in and out; and the AI prompt box on
scenes.frameos.net is two lines tall below 640px, where its placeholder never
fit on one.

Not fixed, written down instead (docs/todo.md): a frame that joins the cloud
arrives with no scenes. The hub protocol has no device → cloud scene path at
all — importing a device's scenes means minting store-scene records for
scenes that were never in anyone's library. A feature, not a fix.

Verified: cloud `pnpm verify` 28/28, frontend `tsc --noEmit` clean, backend
`pytest app/api/tests app/tasks/tests` 1089 passed, and the Nim suites for
the API, the frame API routes, the server, the portal, the index scene and
the upgrade helpers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkGsPJHBxQApJoLE9d9hr2
…in sync

The drift guard added two commits ago failed on its first real PR, correctly
in spirit and wrongly in fact: builtinApps.ts embeds every app's
app_loader.nim, those files are generated AND gitignored, and a clean CI
checkout has none of them — so the regenerated catalog was always missing
them and could never match what a dev machine committed.

Generate them first, exactly as the Nim job does
(`FRAMEOS_ROOT_DIR=. python3 tools/makeapploaders.py`, no third-party deps),
and say so in the error message for whoever hits this next. Verified locally:
regenerating the loaders and then the catalog leaves the tree clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkGsPJHBxQApJoLE9d9hr2
… moved behind an (i)

Both from listing the status screen outside the on-device panel and slimming
the cloud box, caught by the frontend-visual suite.

**`system/index` 404'd on every surface but the device.** It is a scene id
with a slash in it, and both control planes routed scene images through a
single path segment — FastAPI's `{scene_id}` and Next's `[sceneId]` — so the
tile's image request never reached a handler. The device got this right
already (a literal route registered before the `@sceneId` one, because mummy
matches `@` as one segment); the other two now match the rest of the path.
Nothing longer lives under either GET prefix, so swallowing it is exactly
right. The backend's POST stays single-segment: nothing posts this id, the
backend pulls the picture off the device.

This is what the e2e suite was actually reporting — every "Unexpected frontend
errors" failure was one `Failed to load resource: 404`, on each workspace
route that drew the tile.

**The cloud box's two sentences are behind the (i) now**, so the tests that
asserted them on the page open the tooltip first. The status tooltip has an
explicit label ("What this cloud status means") so the assertion targets it
rather than one of the page's many bare (i) buttons.

Verified locally against the real suite (`FRONTEND_VISUAL_MAX_DIFF=1`, and
`pnpm --dir frameos/frontend run build` first — the frame-admin specs serve
the ON-FRAME bundle, not frontend/dist): frame-admin 7/7 and frontend-e2e
13/13, both previously failing. Backend `pytest app/api/tests app/tasks/tests`
1090 passed, cloud `pnpm verify` 28/28.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkGsPJHBxQApJoLE9d9hr2
The built-in status screen (`system/index`) was a tile in the scene list —
a scene you cannot edit, rename or delete, sitting first among the ones you
can. It is a frame action, not a scene, so it moves to the frame's actions
menu as "Render status screen", which sends the activation outright instead
of opening a drawer first.

`framesModel.renderStatusScreen` sends it, not controlLogic: the frames home
draws a menu per card, and mounting a keyed controlLogic for each would fetch
every frame's state (the same reason FrameSceneTile uses sceneUpdatesLogic).
It is the same `setCurrentScene` event a scene tile sends, so both control
planes answer it unchanged — the cloud's shim keeps the plain select for an
id nothing is assigned to.

With the tile gone, "Show status screen" in the list's display menu has
nothing to switch, so it and its stored flag go too. The status-screen drawer
stays: a restored `?drawer=scene&sceneId=system/index` still lands on it.

The scene list keeps its current layout and A-Z order here — the one-row
rewrite and "sort by recently used" are #491's.

Verified: `frontend npx tsc --noEmit` and `kea-typegen check --inline` clean,
`cloud pnpm verify` 28/28.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192oKE2E4j6dVcQmEmFSP4i
@mariusandra
mariusandra force-pushed the adopted-card-bench-fixes branch from efb4884 to ba2997f Compare September 13, 2026 19:03
@mariusandra
mariusandra merged commit e3e71ae into main Sep 13, 2026
@mariusandra
mariusandra deleted the adopted-card-bench-fixes branch September 13, 2026 19:13
mariusandra added a commit that referenced this pull request Sep 13, 2026
Conflicts:
- docs/manual-testing-todo.md — main (#490) closed the whole non-ESP32 side
  and left only the ESP32 bench; this branch had added a Preview drawer box
  main never saw. Took main's closures and carried the Preview drawer box
  over under its Backend (self-hosted) bench heading, header line reworded
  so it no longer claims the non-ESP32 side is empty.
- four frame-scenes--expanded-scene snapshots — took main's baseline (#490's
  real UI change). CI re-renders these with --update-snapshots and the bot
  job commits the result, so this branch's own diffs come back from CI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019tywARkZTUQNozKsXMHEq9
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant