Repository navigation
iOS app: a native SwiftUI client for FrameOS Cloud and self-hosted backends - #724
mariusandra wants to merge 18 commits into
Conversation
…ckends ios/ is a SwiftUI app (iOS 17+) that signs in to FrameOS Cloud by default (email, password, the second factor) or, below it, to a self-hosted backend, and does on a phone what the workspace does without the diagram editor: frames with their pictures, status and deploy bar; scenes on a frame with their public fields (Activate / Apply), install from the store or the backend's catalog, update, remove, the preview row; render, restart, reboot, deploy, firmware updates, the command queue; settings, schedule, button actions, voice permissions; assets; logs and metrics; the photo library with uploads, albums, share links and "play on a frame"; the AI chat; several accounts at once. It calls the routes the shared SPA calls, through one transport boundary (APIClient, the counterpart of apiFetch: project scoping on a backend, the Origin header on the cloud). What the SPA decides in shared code, the app decides from the same sources, so the two cannot drift silently: - generated tables: generate_deploy_progress_contract.py and generate_cloud_contract.py gain a Swift output (the backend suite's --check covers them); - new fixture corpora replayed by both the SPA (cloud shared-spa tests) and the app: frame-status (status word, activity line, deploy bar, firmware word), scene-form (implicit refresh interval, showIf), button-map (Button actions rows), api-path (project scoping); server-address and deploy-progress gain an iOS runner; - the pending-command labels are read from the SPA's source by a test; - a backend's scene and album installs call its own MCP tools, so the id remapping stays in one place. FrameOSKit (no UI) builds and tests without Xcode (ios/bin/test); FrameOSUI type-checks for macOS (ios/bin/check-ui); the new "iOS app" workflow runs the tests and builds the app for the simulator on GitHub's macOS runners. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…w SPA runners typed for eslint; workflow lint Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…etwork access, link switches), more tests, a launch check on a simulator Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s token; an end-to-end test against a real backend and read-only cloud probes; the socket pings at once Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…x tests outside tsc and eslint (bare JSX namespace) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tings_groups The SPA grants a store scene the service keys it declares when it installs it, on both planes; the cloud's MCP and its /scenes/add route take `settings_groups` for that (granted ∩ declared). The backend's MCP install had no such argument, so a store scene installed through it (the iOS app's backend installs) got none of its keys. It now takes `settings_groups` and answers `declared_settings_groups` / `granted_settings_groups`, the cloud tool's names; without the argument nothing changes. The declared-groups walk the deploy ships keys by is one helper now (scene_declared_setting_groups, scene_is_from_store in models/frame.py), used by shipped_frame_settings and the tool. The app passes every group on both planes, so the plane grants exactly what the scene declares. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ESP32 firmware sizeBuilt from a543266 by this run; compared with the latest release v2026.11.6.
Breakdown by subsystem —
|
| Subsystem | This PR | v2026.11.6 | Δ |
|---|---|---|---|
| FrameOS core (Nim) | 1,117,543 | 1,117,543 | ±0 |
| QuickJS | 364,558 | 364,558 | ±0 |
| Wi-Fi stack | 284,079 | 284,079 | ±0 |
| fos_* firmware shell (C) | 205,527 | 205,527 | ±0 |
| Nim stdlib | 205,007 | 205,007 | ±0 |
| ESP-IDF misc | 194,513 | 194,513 | ±0 |
| String pool (attributed to efuse) | 132,155 | 132,155 | ±0 |
| mbedTLS + certificates | 124,396 | 124,396 | ±0 |
| lwIP / HTTP / WebSocket | 123,433 | 123,433 | ±0 |
| libc / libm / newlib | 93,168 | 93,168 | ±0 |
| Storage (SPIFFS / FatFS / SD / NVS) | 92,234 | 92,234 | ±0 |
| Display drivers (C) | 87,429 | 87,429 | ±0 |
| FrameOS apps (Nim) | 77,014 | 77,014 | ±0 |
| Embedded font | 57,894 | 57,894 | ±0 |
| Nim packages (chrono, zippy, chroma, qrgen, ...) | 29,468 | 29,468 | ±0 |
| Crypto (monocypher) | 19,633 | 19,633 | ±0 |
| Total mapped flash | 3,208,051 | 3,208,051 | ±0 |
Inside each subsystem
FrameOS core (Nim) — 1,117,543
| Part | This PR | v2026.11.6 | Δ |
|---|---|---|---|
pixie/src |
375,923 | 375,923 | ±0 |
frameos/js_runtime |
155,580 | 155,580 | ±0 |
frameos/utils |
148,518 | 148,518 | ±0 |
frameos/interpreter.nim |
112,337 | 112,337 | ±0 |
frameos/ui |
73,943 | 73,943 | ±0 |
system/sceneSettings |
32,725 | 32,725 | ±0 |
embedded_main.nim |
25,956 | 25,956 | ±0 |
embedded_runtime.nim |
22,116 | 22,116 | ±0 |
frameos/event_loop.nim |
19,119 | 19,119 | ±0 |
frameos/input_state.nim |
14,179 | 14,179 | ±0 |
frameos_nim_glue.c.obj |
11,911 | 11,911 | ±0 |
system/sceneSwitcher |
11,717 | 11,717 | ±0 |
lib/tz.nim |
10,529 | 10,529 | ±0 |
frameos/single_scene_host.nim |
9,976 | 9,976 | ±0 |
frameos/planner.nim |
9,529 | 9,529 | ±0 |
frameos/types.nim |
8,900 | 8,900 | ±0 |
frameos/values.nim |
7,642 | 7,642 | ±0 |
frameos/scene_rhythm.nim |
7,229 | 7,229 | ±0 |
embedded_scene.nim |
6,855 | 6,855 | ±0 |
frameos/apps.nim |
6,459 | 6,459 | ±0 |
frameos/spool.nim |
5,786 | 5,786 | ±0 |
frameos/events_gen.nim |
5,088 | 5,088 | ±0 |
frameos/app_config.nim |
3,614 | 3,614 | ±0 |
fos_netguard.c.obj |
3,508 | 3,508 | ±0 |
frameos/event_map.nim |
3,076 | 3,076 | ±0 |
patched_chroma_names.nim |
2,909 | 2,909 | ±0 |
frameos/node_config.nim |
2,566 | 2,566 | ±0 |
frameos/app_capabilities.nim |
2,473 | 2,473 | ±0 |
frameos/render_probe.nim |
2,389 | 2,389 | ±0 |
frameos/events.nim |
2,343 | 2,343 | ±0 |
frameos/reboot_reason.nim |
2,312 | 2,312 | ±0 |
frameos/refresh_interval.nim |
2,150 | 2,150 | ±0 |
frameos/loading_mark.nim |
1,371 | 1,371 | ±0 |
fos_log_ring.c.obj |
1,269 | 1,269 | ±0 |
frameos/layout_choice.nim |
1,246 | 1,246 | ±0 |
frameos/runtime_diagnostics.nim |
767 | 767 | ±0 |
patched_dragonbox.nim |
743 | 743 | ±0 |
frameos/render_scale.nim |
678 | 678 | ±0 |
fos_rows.c.obj |
464 | 464 | ±0 |
frameos/event_log.nim |
427 | 427 | ±0 |
fos_version.c.obj |
405 | 405 | ±0 |
fos_table_probe.nim |
269 | 269 | ±0 |
frameos/channels.nim |
153 | 153 | ±0 |
frameos/ids.nim |
120 | 120 | ±0 |
fos_log_backoff.c.obj |
93 | 93 | ±0 |
fos_heap_walk.c.obj |
82 | 82 | ±0 |
frameos/hal |
44 | 44 | ±0 |
patched_zippy_crc.nim |
28 | 28 | ±0 |
frameos/retired_apps.nim |
14 | 14 | ±0 |
frameos/panel_shape.nim |
13 | 13 | ±0 |
QuickJS — 364,558
| Part | This PR | v2026.11.6 | Δ |
|---|---|---|---|
quickjs.c.obj |
291,975 | 291,975 | ±0 |
libunicode.c.obj |
50,178 | 50,178 | ±0 |
libregexp.c.obj |
14,007 | 14,007 | ±0 |
dtoa.c.obj |
5,689 | 5,689 | ±0 |
cutils.c.obj |
2,141 | 2,141 | ±0 |
fos_qjs_glue.c.obj |
568 | 568 | ±0 |
Wi-Fi stack — 284,079
| Part | This PR | v2026.11.6 | Δ |
|---|---|---|---|
libnet80211.a |
136,616 | 136,616 | ±0 |
libpp.a |
61,986 | 61,986 | ±0 |
libwpa_supplicant.a |
43,697 | 43,697 | ±0 |
libphy.a |
34,168 | 34,168 | ±0 |
libesp_wifi.a |
4,616 | 4,616 | ±0 |
libesp_phy.a |
2,419 | 2,419 | ±0 |
libesp_coex.a |
291 | 291 | ±0 |
libcore.a |
283 | 283 | ±0 |
libespnow.a |
3 | 3 | ±0 |
fos_ firmware shell (C)* — 205,527
| Part | This PR | v2026.11.6 | Δ |
|---|---|---|---|
fos_http.c.obj |
23,385 | 23,385 | ±0 |
fos_cloud.c.obj |
20,884 | 20,884 | ±0 |
fos_client.c.obj |
20,687 | 20,687 | ±0 |
fos_console.c.obj |
17,117 | 17,117 | ±0 |
fos_scenes.c.obj |
11,866 | 11,866 | ±0 |
fos_settings.c.obj |
9,366 | 9,366 | ±0 |
fos_config.c.obj |
6,114 | 6,114 | ±0 |
fos_ota.c.obj |
6,093 | 6,093 | ±0 |
fos_cloud_contract.c.obj |
5,578 | 5,578 | ±0 |
fos_wifi.c.obj |
4,706 | 4,706 | ±0 |
fos_scene_store.c.obj |
4,674 | 4,674 | ±0 |
fos_voice.c.obj |
4,076 | 4,076 | ±0 |
fos_touch.c.obj |
3,655 | 3,655 | ±0 |
setup_page.html.gz.S.obj |
3,540 | 3,540 | ±0 |
fos_buttons.c.obj |
3,242 | 3,242 | ±0 |
fos_assets.c.obj |
3,144 | 3,144 | ±0 |
fos_panel_draw.c.obj |
2,982 | 2,982 | ±0 |
fos_config_parse.c.obj |
2,891 | 2,891 | ±0 |
fos_codec.c.obj |
2,842 | 2,842 | ±0 |
fos_sd_probe.c.obj |
2,761 | 2,761 | ±0 |
fos_assets_sd.c.obj |
2,746 | 2,746 | ±0 |
fos_schedule.c.obj |
2,708 | 2,708 | ±0 |
fos_events.c.obj |
2,639 | 2,639 | ±0 |
main.c.obj |
2,615 | 2,615 | ±0 |
fos_frame_settings.c.obj |
2,613 | 2,613 | ±0 |
fos_rtc.c.obj |
2,446 | 2,446 | ±0 |
fos_mic.c.obj |
2,413 | 2,413 | ±0 |
fos_assets_flash.c.obj |
2,075 | 2,075 | ±0 |
fos_buzzer.c.obj |
1,959 | 1,959 | ±0 |
fos_animation.c.obj |
1,929 | 1,929 | ±0 |
fos_i2c.c.obj |
1,859 | 1,859 | ±0 |
fos_scene_state.c.obj |
1,817 | 1,817 | ±0 |
fos_board.c.obj |
1,473 | 1,473 | ±0 |
fos_url_guard.c.obj |
1,389 | 1,389 | ±0 |
fos_tz.c.obj |
1,228 | 1,228 | ±0 |
fos_minisig.c.obj |
1,140 | 1,140 | ±0 |
fos_battery.c.obj |
1,128 | 1,128 | ±0 |
fos_led.c.obj |
1,105 | 1,105 | ±0 |
fos_commands.c.obj |
1,076 | 1,076 | ±0 |
fos_backend_http.c.obj |
1,064 | 1,064 | ±0 |
fos_status_screen.c.obj |
873 | 873 | ±0 |
fos_gauge.c.obj |
841 | 841 | ±0 |
fos_lan_access.c.obj |
841 | 841 | ±0 |
fos_wake.c.obj |
805 | 805 | ±0 |
fos_assets_path.c.obj |
568 | 568 | ±0 |
fos_framebuffer.c.obj |
513 | 513 | ±0 |
fos_ota_download.c.obj |
472 | 472 | ±0 |
fos_voice_binding.c.obj |
441 | 441 | ±0 |
fos_button_timing.c.obj |
434 | 434 | ±0 |
fos_slot_cache.c.obj |
369 | 369 | ±0 |
fos_climate.c.obj |
303 | 303 | ±0 |
fos_json_guard.c.obj |
267 | 267 | ±0 |
fos_power.c.obj |
264 | 264 | ±0 |
fos_printf.c.obj |
261 | 261 | ±0 |
fos_battery_filter.c.obj |
214 | 214 | ±0 |
fos_charger.c.obj |
186 | 186 | ±0 |
fos_wifi_networks.c.obj |
183 | 183 | ±0 |
fos_spiffs_gc.c.obj |
170 | 170 | ±0 |
fos_sscanf.c.obj |
154 | 154 | ±0 |
fos_boot_scene.c.obj |
142 | 142 | ±0 |
fos_voice_hold.c.obj |
112 | 112 | ±0 |
fos_spiffs_footprint.c.obj |
89 | 89 | ±0 |
Nim stdlib — 205,007
| Part | This PR | v2026.11.6 | Δ |
|---|---|---|---|
system.nim |
53,916 | 53,916 | ±0 |
pure/collections/tables.nim |
44,133 | 44,133 | ±0 |
pure/unicode.nim |
17,431 | 17,431 | ±0 |
pure/times.nim |
16,413 | 16,413 | ±0 |
pure/json.nim |
13,678 | 13,678 | ±0 |
pure/strutils.nim |
7,714 | 7,714 | ±0 |
pure/parsexml.nim |
5,790 | 5,790 | ±0 |
pure/base64.nim |
5,103 | 5,103 | ±0 |
pure/hashes.nim |
4,347 | 4,347 | ±0 |
pure/xmlparser.nim |
3,407 | 3,407 | ±0 |
pure/parsejson.nim |
3,392 | 3,392 | ±0 |
pure/uri.nim |
3,017 | 3,017 | ±0 |
pure/streams.nim |
2,720 | 2,720 | ±0 |
std/syncio.nim |
2,334 | 2,334 | ±0 |
pure/algorithm.nim |
2,307 | 2,307 | ±0 |
pure/lexbase.nim |
1,775 | 1,775 | ±0 |
pure/parseutils.nim |
1,744 | 1,744 | ±0 |
system/exceptions.nim |
1,526 | 1,526 | ±0 |
pure/strtabs.nim |
1,422 | 1,422 | ±0 |
pure/pathnorm.nim |
1,204 | 1,204 | ±0 |
pure/xmltree.nim |
1,202 | 1,202 | ±0 |
pure/options.nim |
1,080 | 1,080 | ±0 |
pure/collections/sets.nim |
1,045 | 1,045 | ±0 |
std/private/ospaths2.nim |
1,024 | 1,024 | ±0 |
pure/math.nim |
835 | 835 | ±0 |
pure/collections/deques.nim |
782 | 782 | ±0 |
pure/strformat.nim |
774 | 774 | ±0 |
pure/random.nim |
688 | 688 | ±0 |
std/private/digitsutils.nim |
529 | 529 | ±0 |
std/private/osdirs.nim |
510 | 510 | ±0 |
std/monotimes.nim |
497 | 497 | ±0 |
std/oserrors.nim |
489 | 489 | ±0 |
system/dollars.nim |
431 | 431 | ±0 |
std/envvars.nim |
387 | 387 | ±0 |
std/private/oscommon.nim |
325 | 325 | ±0 |
std/private/ossymlinks.nim |
187 | 187 | ±0 |
std/private/decode_helpers.nim |
185 | 185 | ±0 |
pure/bitops.nim |
162 | 162 | ±0 |
std/formatfloat.nim |
111 | 111 | ±0 |
pure/os.nim |
99 | 99 | ±0 |
std/private/osfiles.nim |
96 | 96 | ±0 |
core/macros.nim |
89 | 89 | ±0 |
std/assertions.nim |
87 | 87 | ±0 |
system/iterators.nim |
20 | 20 | ±0 |
ESP-IDF misc — 194,513
| Part | This PR | v2026.11.6 | Δ |
|---|---|---|---|
libesp_hw_support.a |
36,962 | 36,962 | ±0 |
libhal.a |
22,252 | 22,252 | ±0 |
libfreertos.a |
17,447 | 17,447 | ±0 |
libesp_system.a |
13,503 | 13,503 | ±0 |
libesp_driver_uart.a |
11,565 | 11,565 | ±0 |
libheap.a |
8,893 | 8,893 | ±0 |
libesp_driver_i2c.a |
8,365 | 8,365 | ±0 |
libesp_lcd.a |
7,737 | 7,737 | ±0 |
libesp_driver_spi.a |
7,600 | 7,600 | ±0 |
libconsole.a |
5,745 | 5,745 | ±0 |
libjson.a |
5,421 | 5,421 | ±0 |
libesp_driver_gpio.a |
4,760 | 4,760 | ±0 |
liblog.a |
4,528 | 4,528 | ±0 |
libxtensa.a |
3,691 | 3,691 | ±0 |
libbootloader_support.a |
3,498 | 3,498 | ±0 |
libesp_driver_usb_serial_jtag.a |
3,344 | 3,344 | ±0 |
libesp_ringbuf.a |
3,322 | 3,322 | ±0 |
libesp_event.a |
3,095 | 3,095 | ±0 |
libesp_driver_ledc.a |
2,971 | 2,971 | ±0 |
libsoc.a |
2,648 | 2,648 | ±0 |
libesp_mm.a |
2,435 | 2,435 | ±0 |
libapp_update.a |
2,368 | 2,368 | ±0 |
libesp_rom.a |
2,364 | 2,364 | ±0 |
libesp_adc.a |
2,273 | 2,273 | ±0 |
libesp_timer.a |
2,237 | 2,237 | ±0 |
libesp_psram.a |
2,056 | 2,056 | ±0 |
libpthread.a |
1,397 | 1,397 | ±0 |
libesp_security.a |
1,042 | 1,042 | ±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 |
String pool (attributed to efuse) — 132,155
| Part | This PR | v2026.11.6 | Δ |
|---|---|---|---|
esp_efuse_utility.c.obj |
131,119 | 131,119 | ±0 |
esp_efuse_rtc_calib.c.obj |
556 | 556 | ±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 — 124,396
| Part | This PR | v2026.11.6 | Δ |
|---|---|---|---|
libmbedcrypto.a |
67,044 | 67,044 | ±0 |
libmbedtls.a |
43,452 | 43,452 | ±0 |
libmbedx509.a |
6,755 | 6,755 | ±0 |
libesp-tls.a |
5,691 | 5,691 | ±0 |
libesp_https_server.a |
1,454 | 1,454 | ±0 |
lwIP / HTTP / WebSocket — 123,433
| Part | This PR | v2026.11.6 | Δ |
|---|---|---|---|
liblwip.a |
78,465 | 78,465 | ±0 |
libhttp_parser.a |
10,347 | 10,347 | ±0 |
libesp_http_server.a |
9,219 | 9,219 | ±0 |
libtcp_transport.a |
7,196 | 7,196 | ±0 |
libesp_http_client.a |
6,166 | 6,166 | ±0 |
libespressif__esp_websocket_client.a |
6,056 | 6,056 | ±0 |
libesp_netif.a |
5,984 | 5,984 | ±0 |
libc / libm / newlib — 93,168
| Part | This PR | v2026.11.6 | Δ |
|---|---|---|---|
libc.a |
53,207 | 53,207 | ±0 |
libm.a |
31,559 | 31,559 | ±0 |
libnewlib.a |
6,661 | 6,661 | ±0 |
libstdc++.a |
1,476 | 1,476 | ±0 |
libgcc.a |
219 | 219 | ±0 |
libcxx.a |
46 | 46 | ±0 |
Storage (SPIFFS / FatFS / SD / NVS) — 92,234
| Part | This PR | v2026.11.6 | Δ |
|---|---|---|---|
libspiffs.a |
20,348 | 20,348 | ±0 |
libfatfs.a |
19,924 | 19,924 | ±0 |
libspi_flash.a |
13,730 | 13,730 | ±0 |
libnvs_flash.a |
13,665 | 13,665 | ±0 |
libsdmmc.a |
11,948 | 11,948 | ±0 |
libvfs.a |
6,224 | 6,224 | ±0 |
libesp_driver_sdspi.a |
3,822 | 3,822 | ±0 |
libesp_partition.a |
1,890 | 1,890 | ±0 |
libesp_vfs_console.a |
678 | 678 | ±0 |
libnvs_sec_provider.a |
5 | 5 | ±0 |
Display drivers (C) — 87,429
| Part | This PR | v2026.11.6 | Δ |
|---|---|---|---|
(everything else) |
24,491 | 24,491 | ±0 |
lcd_splash.c.obj |
7,844 | 7,844 | ±0 |
frameos_panel_table.c.obj |
6,899 | 6,899 | ±0 |
lcd_st7701s_rgb.c.obj |
5,687 | 5,687 | ±0 |
it8951.c.obj |
4,628 | 4,628 | ±0 |
frameos_display.c.obj |
3,213 | 3,213 | ±0 |
EPD_13in3e.c.obj |
1,792 | 1,792 | ±0 |
DEV_Config_esp.c.obj |
1,570 | 1,570 | ±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 |
| 29 smaller rows | 25,659 | 25,659 | ±0 |
FrameOS apps (Nim) — 77,014
| Part | This PR | v2026.11.6 | Δ |
|---|---|---|---|
apps/data/localImage |
9,875 | 9,875 | ±0 |
apps/render/split |
9,664 | 9,664 | ±0 |
apps/render/zoomPan |
7,784 | 7,784 | ±0 |
apps/apps.nim |
5,975 | 5,975 | ±0 |
apps/render/text |
4,976 | 4,976 | ±0 |
apps/data/xmlToJson |
4,465 | 4,465 | ±0 |
apps/render/svg |
4,381 | 4,381 | ±0 |
apps/render/image |
3,710 | 3,710 | ±0 |
apps/render/gradient |
3,129 | 3,129 | ±0 |
apps/logic/setAsState |
2,460 | 2,460 | ±0 |
apps/data/downloadImage |
2,402 | 2,402 | ±0 |
apps/data/rotateImage |
2,206 | 2,206 | ±0 |
apps/data/downloadUrl |
2,081 | 2,081 | ±0 |
apps/data/qr |
1,874 | 1,874 | ±0 |
apps/data/newImage |
1,742 | 1,742 | ±0 |
apps/data/resizeImage |
1,575 | 1,575 | ±0 |
apps/render/color |
1,361 | 1,361 | ±0 |
apps/render/opacity |
1,333 | 1,333 | ±0 |
apps/data/log |
1,324 | 1,324 | ±0 |
apps/data/prettyJson |
1,218 | 1,218 | ±0 |
apps/data/clock |
928 | 928 | ±0 |
apps/data/parseJson |
852 | 852 | ±0 |
apps/logic/nextSleepDuration |
661 | 661 | ±0 |
apps/logic/ifElse |
578 | 578 | ±0 |
apps/logic/breakIfRendering |
460 | 460 | ±0 |
Embedded font — 57,894
| Part | This PR | v2026.11.6 | Δ |
|---|---|---|---|
assets/fonts.nim |
57,894 | 57,894 | ±0 |
Nim packages (chrono, zippy, chroma, qrgen, ...) — 29,468
| Part | This PR | v2026.11.6 | Δ |
|---|---|---|---|
chrono 0.3.1 |
13,414 | 13,414 | ±0 |
zippy 0.10.19 |
6,716 | 6,716 | ±0 |
chroma 1.0.0 |
5,242 | 5,242 | ±0 |
checksums 0.2.1 |
3,290 | 3,290 | ±0 |
flatty 0.3.4 |
530 | 530 | ±0 |
jsony 1.1.5 |
148 | 148 | ±0 |
bumpy 1.1.3 |
128 | 128 | ±0 |
Crypto (monocypher) — 19,633
| Part | This PR | v2026.11.6 | Δ |
|---|---|---|---|
monocypher.c.obj |
16,408 | 16,408 | ±0 |
monocypher-ed25519.c.obj |
3,225 | 3,225 | ±0 |
Largest 30 objects
| Object | Subsystem | This PR | v2026.11.6 | Δ |
|---|---|---|---|---|
quickjs.c.obj |
QuickJS | 291,975 | 291,975 | ±0 |
esp_efuse_utility.c.obj |
String pool (attributed to efuse) | 131,119 | 131,119 | ±0 |
frameos/interpreter.nim |
FrameOS core (Nim) | 112,337 | 112,337 | ±0 |
frameos/js_runtime/app_runtime.nim |
FrameOS core (Nim) | 107,191 | 107,191 | ±0 |
assets/fonts.nim |
Embedded font | 57,894 | 57,894 | ±0 |
pixie/src/pixie/fileformats/svg.nim |
FrameOS core (Nim) | 57,751 | 57,751 | ±0 |
pixie/src/pixie/fontformats/opentype.nim |
FrameOS core (Nim) | 57,425 | 57,425 | ±0 |
pixie/src/pixie/paths.nim |
FrameOS core (Nim) | 54,167 | 54,167 | ±0 |
nim/lib/system.nim |
Nim stdlib | 53,916 | 53,916 | ±0 |
libunicode.c.obj |
QuickJS | 50,178 | 50,178 | ±0 |
nim/lib/pure/collections/tables.nim |
Nim stdlib | 44,133 | 44,133 | ±0 |
frameos/ui/layout.nim |
FrameOS core (Nim) | 43,573 | 43,573 | ±0 |
frameos/utils/image.nim |
FrameOS core (Nim) | 40,803 | 40,803 | ±0 |
pixie/src/pixie/fileformats/webp.nim |
FrameOS core (Nim) | 37,050 | 37,050 | ±0 |
pixie/src/pixie/images.nim |
FrameOS core (Nim) | 36,338 | 36,338 | ±0 |
system/sceneSettings/scene.nim |
FrameOS core (Nim) | 32,725 | 32,725 | ±0 |
pixie/src/pixie/fileformats/jpeg.nim |
FrameOS core (Nim) | 31,489 | 31,489 | ±0 |
frameos/js_runtime/runtime.nim |
FrameOS core (Nim) | 30,830 | 30,830 | ±0 |
embedded_main.nim |
FrameOS core (Nim) | 25,956 | 25,956 | ±0 |
fos_http.c.obj |
fos_* firmware shell (C) | 23,385 | 23,385 | ±0 |
embedded_runtime.nim |
FrameOS core (Nim) | 22,116 | 22,116 | ±0 |
pixie/src/pixie/fileformats/png.nim |
FrameOS core (Nim) | 21,483 | 21,483 | ±0 |
fos_cloud.c.obj |
fos_* firmware shell (C) | 20,884 | 20,884 | ±0 |
fos_client.c.obj |
fos_* firmware shell (C) | 20,687 | 20,687 | ±0 |
frameos/event_loop.nim |
FrameOS core (Nim) | 19,119 | 19,119 | ±0 |
frameos/utils/asset_colors.nim |
FrameOS core (Nim) | 17,930 | 17,930 | ±0 |
x509_crt_bundle.S.obj |
mbedTLS + certificates | 17,928 | 17,928 | ±0 |
nim/lib/pure/unicode.nim |
Nim stdlib | 17,431 | 17,431 | ±0 |
fos_console.c.obj |
fos_* firmware shell (C) | 17,117 | 17,117 | ±0 |
frameos/utils/status_screen.nim |
FrameOS core (Nim) | 16,590 | 16,590 | ±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.
A review of every cloud call against cloud/apps/auth-web found these, now fixed: - a preview installed on the cloud is saved and pushed the way the SPA does it: without origin, button map and layout in the store copy (they ride the row), with the frame's snapshot as its cover, and granted the keys it declares; - the re-confirm sheet sends the code whenever the account takes one (a recovery code included; a two-factor account refuses any password) and says so for a passkey-only account; - creating a store scene waits 120 s (moderation runs first); - "no time zone" is never sent to the cloud (the contract wants one); - path segments are percent-encoded once, not twice; - an error code without words of its own reads as words, not JSON; - an ESP32's button map stops at the contract's 8 rules (the generator now hands the app each profile's own rule); - a photo upload reports the name it was stored under; - a scene that joins a frame with an active layout wears it, on both planes (the SPA's activeFrameLayout / layoutsWornBy). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- FrameOSUITests: the sign-in screen (every CI build, replacing the bare launch check) and a tour of the whole app against a backend or a cloud, a screenshot of every screen. ios/bin/ui-test runs them and copies the screenshots out. - ios/bin/test-backend: a backend from this checkout with its own SQLite and Redis, a first user and two virtual frames; Ctrl-C removes it. - CloudIntegrationTests: the services signed in to a cloud you may write to: frames, a store scene installed, activated and removed, settings, schedule, photos, sign-out. Passed against a local cloud with a hub and virtual frames. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cloud compares Origin with its own origins as text. A cloud address typed with a capital letter (Cloud.FrameOS.net, Frames.local) went out as typed and every write was refused as invalid_origin; a default port or an IPv6 address did the same. Found by the signed-in cloud test, which now also opens the live socket when asked (FRAMEOS_IT_CLOUD_LIVE=1, a cloud with the hub on its own origin). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Xcode is on this Mac now, and the UI tour taps through the whole app on
both planes (a throwaway backend with virtual frames; a local cloud with
its hub and virtual frames). What it found, fixed:
- "Add photos" (Photos) and "Upload photos" (Assets) opened nothing: a
PhotosPicker inside a Menu does not present on iOS 27. A menu button now
opens .photosPicker.
- Every photo read as having a color profile: JSONValue takes `nil` as a
literal, so `{ $0.isNull ? nil : $0 }` made JSON null of it. Same trap
closed in a schedule event's "disabled"; a test pins both.
- The frame picture sized itself to its content, narrower than its row;
it is now the panel's shape at full width, and a frame that does not say
its size takes the picture's shape instead of 4:3.
- After a search the keyboard stayed up over the install sheet, hiding
"Install & activate".
- Store scenes are grouped by the store's shelves, in shelf order, with the
SPA's titles (StoreCategories, pinned to storeCategories.ts by a test).
- Photo tiles say their name and "has a location" to VoiceOver, not
"Map Pin"; album covers are decorative.
- Words: Behavior (US spelling), "Render every … seconds", "0 KB" (not
"Zero KB"), a frame's own time zone shown even when the phone does not
list it, number fields that no longer cut their labels.
The tour itself: sign in, frames, a frame, a store scene searched for and
installed, the scene's settings, every frame page, every tab, a new album,
a photo through the system picker, its info (location), the album played
on a frame. ios/bin/ui-test prefers the newest iPhone Pro and waits for it
to boot; CI's sign-in test reads the real error text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e tour CI's UI test step hung for 15 minutes before any test, with no output to say where. The script now names each stage, adds the tour's photo to the simulator only when a tour will run (with 60 s at most: addmedia can hang on a fresh simulator), and the CI step has its own 20-minute limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FRAMEOS_UITEST_DEVICE now picks any simulator by name, and the tour finds tabs by their symbol and name: an iPad shows them in a bar at the top, not a tab bar. The backend tour passes on an iPad Pro 11-inch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A native app cannot read the browser's cookies, so the browser hands it a
session (cloud/docs/auth.md "Signing in to the app"):
- GET /api/auth/app/authorize?code_challenge=<S256>&state=… (the app opens
it in the system's sign-in sheet) sends the person through /login, or
straight to Google with provider=google, and comes back to itself.
Signed in, it redirects to frameos://sign-in with a two-minute code: a
signed token naming account and identity by id, no email in the URL.
- POST /api/auth/app/token {code, code_verifier}: PKCE S256, spent once
(claimSingleUse), the profile read fresh, a new session cookie for the
app. Every failure is invalid_grant; audited as account.signed_in
(app_handoff).
Safety:
- A browser session from before the request is never handed on silently:
/login/app asks "Sign in to the app?" (Continue / Use another account /
Cancel). Another app could open the same URL in its own sheet; signing in
during the request is the consent. Continue is a fetch, then a
navigation: the CSP's form-action would refuse a frameos: redirect.
- The app's session inherits the browser session's authenticated_at
(createSession's new authenticatedAt), so a handoff never counts as a
fresh sign-in for sudo mode.
- reauth=1 sends a stale browser session through /login/reauth first: an
account without a password or code (Google, passkeys) confirms it is you
in the app this way.
Tested: unit tests (RFC 7636 vector, token types, expiry, keys) and an
integration suite on Postgres (signed out → sign in → code; Google; ask
first; origin check; sudo; cancel; bad challenge; deleted account; replay;
inherited authenticated_at).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- "Continue with Google" and "Use a passkey or another way" on the sign-in screen open the cloud's own sign-in page in the system's sign-in sheet (SwiftUI's webAuthenticationSession, Safari's cookies shared) and trade the code it sends back to frameos://sign-in, with a PKCE verifier only the app holds, for a session of the app's own (CloudWebSignIn, AuthService.cloudFinishWebSignIn). - Confirming it is you: an account without a password or code (Google, passkeys) goes through the same sheet with reauth=1; the fresh session replaces the app's and the old one is signed out. - Tests: the RFC 7636 vector, the URL the sheet opens, every callback case; CloudIntegrationTests runs the handoff against a real cloud (asked first, Continue, exchange, a working session, replay refused); the UI test testCloudWebSignIn signs in through the real sheet on a simulator, both signed out and with Safari already signed in. - NSAllowsLocalNetworking is gone from the ATS settings: beside it iOS ignores NSAllowsArbitraryLoads, so plain http reached only local addresses and a backend at an ordinary hostname was refused. The tour passes over http://127.0.0.1.nip.io:8997. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/frame-status-fixtures.json gains `expect.active` (frameIsActive) on every case and a `groups` section (groupFramesByStatus: Active, Inactive, Archived, each by name with numbers in order and case ignored, ties by id), generated by running the SPA's own functions. The SPA's runner checks both; the app's ports (FrameStatus.isActive, FrameGroups) replay them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
App Store guideline 3.1.3(f) lets a free companion to a paid web service skip in-app purchase only without calls to buy outside the app. The sign-in pages the app's sheet shows (/login and /login/reauth on their way back to /api/auth/app/authorize, and /login/app) leave "Pricing" out of the footer (AuthCard inApp → LegalFooter hidePricing). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e readiness
- Frames: grouped Active, Inactive, Archived like the website (FrameGroups),
as large cards or a compact list (the toggle at the top right, kept per
device). The frame pickers (install, play an album) use the same order.
- Scenes: Store and Saved tabs on both planes. The store sits on its
shelves in shelf order; a backend's repositories are shelved the same way
and its saved scenes are the Saved tab.
- App Store guidelines (docs/ios-app-store.md has the table):
- 5.1.1(v): Settings → Delete account (the cloud's own route; the
password, or a fresh check of who you are plus the typed email).
- 5.1.2(i): the AI chat asks once before its first message, saying the
words go to OpenAI.
- 1.2: "Report this scene" on store scenes (the cloud's report queue).
- 3.1.3(f): no plan or billing link (the plan is shown, not sold).
- Privacy policy and terms links; PrivacyInfo.xcprivacy (UserDefaults
CA92.1; email, photos, user content for app functionality; no
tracking).
- ios/bin/release: archive for devices and upload to App Store Connect
(TestFlight), signed with your team, build number the UTC time;
--unsigned checks the device build (it archives clean, no warnings).
- The tour covers the list layout and the Saved tab.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A native iOS app (SwiftUI, iOS 17+, iPhone and iPad) in
ios/. It signs in to FrameOS Cloud by default, or below that to a self-hosted backend, and keeps several accounts side by side. It does on a phone what the workspace does, minus the diagram editor.From the UI tour on an iOS 27.2 simulator, against a throwaway backend with virtual frames (the store from a local cloud). The last screen is the photo the tour uploaded through the system picker, played as an album: the backend's virtual frame renders it.
What it does
docs/cloud-frames-contract.json; on a backend, Save & deploy. Also the schedule, Button actions, voice-chat switches (frames with a microphone), and cloud-only items: confirm a claimed frame, import its own scenes, local network access with the panel's code, and the API-key and telemetry switches.Signing in with Google or a passkey (a small cloud addition)
The app signs in with email and password through
/api/auth/loginlike the web. For everything else the web can do, Continue with Google and Use a passkey or another way open the cloud's own sign-in page in the system's sign-in sheet (ASWebAuthenticationSession). The cloud then hands the app a session of its own. An app cannot read the browser's cookies, so this needed two new cloud routes (cloud/docs/auth.md"Signing in to the app"):GET /api/auth/app/authorize?code_challenge=<S256>&state=…sends the person through/login, or straight to Google withprovider=google, and comes back to itself.frameos://sign-inwith a two-minute code: a signed token naming the account and identity by id, with no email in the URL.POST /api/auth/app/token {code, code_verifier}checks PKCE S256, spends the code once (claimSingleUse), reads the profile fresh, and sets a new session cookie for the app.Security notes:
frameos://sign-in, and spending one needs the PKCE verifier only the requesting app holds. The app registers no URL scheme: the sign-in sheet catches the callback itself./login/appasks "Sign in to the app?" (Continue, Use another account, Cancel). Signing in during the request counts as the consent. Continue is afetchfollowed by a navigation, because the CSP'sform-actionwould refuse aframeos:redirect after a form post.authenticated_at(createSessiongains an optionalauthenticatedAt). So a handoff never counts as a fresh sign-in for sudo mode, and an old browser session can't become a recent one by passing through the app.reauth=1: a stale browser session goes through/login/reauthfirst, and the fresh app session replaces the old one, which is signed out.Tests:
app-sign-in.integration.test.ts): sign-in, Google, ask-first, origin check, sudo, cancel, bad challenge, deleted account, replay, inheritedauthenticated_at.CloudIntegrationTests.webSignInHandoffagainst a real cloud.App Store readiness (
docs/ios-app-store.md)The guidelines a first submission most often fails on are handled in the app:
ios/bin/release, which archives for devices and uploads to App Store Connect for TestFlight. The device build archives with no warnings.One decision is left, 4.8. With "Continue with Google" in the app, App Review expects Sign in with Apple as well. Either add it to the cloud (web and app; it needs Apple keys), or ship 1.0 without Google in the app. The doc has both.
How it avoids drift
The app calls the routes the SPA calls, through one transport boundary (
ios/FrameOSKit/Sources/FrameOSKit/Core/APIClient.swift, the counterpart ofapiFetch). It gets no routes of its own. What the SPA decides in shared code, the app reads from the same sources (docs/ios-app.mdhas the table):generate_deploy_progress_contract.pyandgenerate_cloud_contract.pygain a Swift output, checked by the backend suite's--check.cloud/apps/auth-web/src/test/shared-spa/*-fixtures.test.tsand the app's tests. The expected values were produced by the SPA's own functions:docs/frame-status-fixtures.json: status word, the line under a frame's name, deploy bar, firmware word.docs/scene-form-fixtures.json: the implicit refresh-interval field andshowIf.docs/button-map-fixtures.json: the Button actions rows and the rules they write.docs/api-path-fixtures.json: backend project scoping.server-address-fixtures.jsongets its fourth runner, and the reader sections ofdeploy-progress-fixtures.jsonreplay in Swift.frame_scene_install,photo_album_install), so the id remapping is not copied into Swift. The cloud's installs are single routes./scenes/addtakessettings_groupsfor that, and only grants what the scene declares. The backend's MCP install had no such argument, so a store scene installed through it got none of its keys.frame_scene_installnow takessettings_groupsand answersdeclared_settings_groups/granted_settings_groups, the cloud tool's names. Without the argument nothing changes.scene_declared_setting_groupsinmodels/frame.py), shared by the deploy and the tool. A backend test covers it, and the mypy baseline drops two errors.AGENTS.mdgets a section for the app, anddocs/api-triality.mdnotes the second client.Checking it
ios/bin/test: FrameOSKit's 36 tests (the corpora, the contracts, the client and parsing). They need only the Command Line Tools.ios/bin/check-ui: type-checks every screen for macOS.ios/bin/ui-test(new): XCUITest on an iPhone simulator, keeping a screenshot of every screen. CI runs the sign-in tests on every change. Locally,TourUITeststaps through the whole app on either plane: sign in, frames, a frame, a store scene searched for and installed, its settings, every frame page, every tab, a new album, a photo through the system picker, its info, and the album played on a frame. Both tours pass on Xcode 27.2, on an iPhone 18 Pro and an iPad Pro 11-inch.ios/bin/test-backend(new): a throwaway backend from the checkout, with its own SQLite and Redis, a user and two virtual frames.CloudIntegrationTests(new) passes against a separate local cloud with its own database, its hub and two virtual frames. It covers a store scene installed, activated and removed, settings, schedule, photos, the live socket (through a one-origin proxy, as nginx does in production) and sign-out. The cloud tour passes against the same instance.BackendIntegrationTestspasses.NSAllowsLocalNetworkingnext toNSAllowsArbitraryLoadsmade iOS ignore the latter, so a backend at an ordinary hostname over http was refused. Fixed; the tour passes overhttp://127.0.0.1.nip.io:8997.What the simulator runs found, all fixed:
PhotosPickerinside aMenudoes not present on iOS 27.Originheader kept a typed cloud address's capitals, so every write was refused asinvalid_origin. It is now written like a browser's.JSONValuetakesnilas a literal (JSON null).Not in the app, deliberately
The diagram editor, code and app editing, scene publishing, adding frames (flashing, SD images, SSH, adoption), hardware wiring, per-photo color profiles, a backend's terminal and build tools. Settings links to the web workspace for those.
Found along the way
FrameDashboardSurface.tsx), a worddocs/ui-vocabulary.mdbans. The app says "update queued". Not changed in the SPA here.127.0.0.1,192.168.x.x) unless the frame's Local network access is on. Its scene HTTP is kept off the LAN by design (embedded_wasm_render.mjs). With the switch on, the album plays (last screenshot above). This is the same from the web, so it is not changed here. Whether a backend's own share links should be exempt is a decision for you.🤖 Generated with Claude Code