Skip to content

FrameOS Cloud 4/4: cloud store and "My cloud drive"#257

Open
mariusandra wants to merge 56 commits into
cloud-config-backupsfrom
backend-cloud-connect
Open

FrameOS Cloud 4/4: cloud store and "My cloud drive"#257
mariusandra wants to merge 56 commits into
cloud-config-backupsfrom
backend-cloud-connect

Conversation

@mariusandra

@mariusandra mariusandra commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Final part of the FrameOS Cloud stack — on top of #263 (linking), #264 (cloud login), and #265 (backups). This PR now carries only the store + cloud drive work; the earlier phases were split into the stack below (and ESP32/JS-runtime/editor work went to #258/#259/#260 before that).

  1. FrameOS Cloud 1/4: device-flow linking of backends and frames #263 — device-flow linking (phase 0)
  2. FrameOS Cloud 2/4: cloud login on backends and frames #264 — cloud login (phase 1)
  3. FrameOS Cloud 3/4: scene and frame config backups #265 — scene/frame config backups (phase 3)
  4. This PR — cloud store + "My cloud drive" (phase 2)
  • Store publishing (store:publish scope): publish scenes as immutable versions to the cloud store; the connected provider's store repository is auto-seeded per project (with provider-change migration of the old marker), and store templates show author/version/risk flags plus a "View store page" link
  • Install by URL: pasting a store page URL into the Templates search installs the scene — the backend follows the page's <meta name="frameos:zip"> tag; URLs on the linked provider get the link token attached so private drive scenes install too
  • "My cloud drive": the account's own scenes (private ones included) listed in the Templates panel, proxied through the backend because <img> tags can't carry the link token; "Save to cloud drive" from the scene dropdowns and template modal, with visibility control on republish
  • Template save modal polish: selected scenes float to the top, and the preview image is the first selected scene's snapshot when the frame currently shows an unrelated scene (image_scene_id)

Test plan

  • pytest test_cloud_store.py test_repositories.py test_templates.py — green (full backend suite passes)
  • E2E happy path against a local frameos-cloud dev server via test_cloud_e2e.py (FRAMEOS_CLOUD_E2E_URL)
  • pnpm --dir frontend run build passes

🤖 Generated with Claude Code

mariusandra and others added 30 commits July 9, 2026 04:15
Adds the "FrameOS Cloud" settings section (below Account) on both the
backend and the on-device frame admin. Linking uses the OAuth 2.0 device
authorization grant against a user-editable provider URL: outbound-only,
scoped bearer token, encrypted at rest. Reuses the cloud_backend_link
table from the accidentally-shipped 2c4a6f8d9b10 migration (load-bearing,
kept) and documents the whole protocol in docs/cloud-link.md so anyone
can run a compatible provider. Roadmap and scope table in CLOUD-TODO.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gement

Phase 1 — cloud login (auth:login):
- Login handoff: "Continue with FrameOS Cloud" on /login and the first-run
  signup screen (open /api/cloud/setup/* endpoints link a fresh install and
  create the first user from the approving cloud account).
- cloud_identity mapping keyed on issuer+subject with a stable account_id
  fallback; auto-linked at connect time from the poll's approved_by claims.
  Email match alone never logs anyone in.
- Local-fallback toggle: disabling passwords needs a linked owner identity
  and a live grants check; losing the link always re-enables local login.
- Logout returns a cloud_logout_url so the provider session ends too.
- Grants sync singleton (app/cloud/sync.py) in the arq worker: revocation,
  inventory heartbeat, membership sync.
- Frame on-device admin login via the same handoff (cloud_api_routes.nim).

Phase 3 — config backups (backup:templates / backup:frames):
- Push/restore templates (interchange zip) and frames (secret-stripped JSON,
  fresh credentials on restore) via /api/cloud/backups*; automatic frame
  backup after each successful deploy.
- Local tar.gz export (GET /api/backup/export) as the no-cloud alternative.

Enabled features UX:
- Connecting requests only the base link scopes; features are enabled later
  in place via POST {provider}/api/backends/scopes (removals immediate,
  additions approved by the owner on the provider's device screen; the link
  token never changes). UI says "enabled features", never "permissions".
- Dev fix: the login callback returns the browser to the origin it started
  from (Vite on :8616 proxying to :8989), restricted to loopback or the
  link's own origin.

Plus the Phase 0 leftovers: client_kind sent on device/start, and a real
cross-repo E2E (test_cloud_e2e.py, driven by frameos-cloud's
scripts/e2e-frameos.sh) covering link, login handoff, feature changes and
backups over live HTTP.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- POST /api/cloud/store/publish sends template zip to the cloud store
- Seed "FrameOS Cloud store" repository once per project when linked
- "Publish to FrameOS Cloud" action in template dropdown (store:publish)
- docs/cloud-link.md store protocol section

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- "My cloud drive" section above "My local scenes" in the Templates panel:
  collapsible, lists the account's store scenes (private included) via
  GET /api/cloud/store/drive with a preview-image proxy; settings promo
  while FrameOS Cloud is not connected
- "Save to cloud drive" (renamed from "Publish to FrameOS Cloud") in the
  template dropdown, scene dropdowns, and frames-home scene menus; warns
  when the cloud link or store scope is missing; publishes inline scenes
  straight off a frame (no local template needed)
- Save-template modal: selected scenes highlighted and sorted to the top;
  the preview image is labeled and switches to the first selected scene's
  snapshot when the active scene is not part of the template
  (image_scene_id, honored by POST /api/templates too)
- Link token attached when fetching template zips/images from the linked
  provider host (private drive installs); never sent to other hosts
- Repository templates show "by {author}" and a "shell" risk badge with an
  install confirmation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- /scenes now renders a cross-frame overview: every frame's scenes as
  tiles (with the full scene menu), then My cloud drive, My local
  scenes, and repositories. /scenes/{frameId} keeps the editor.
- template.json gains frameosVersion at export; the store keeps and
  displays it, and Templates rows show 'newer than this install' as an
  upgrade nudge.
- New frameos-wasm npm package (frontend/wasm): typed wrapper around
  the wasm preview worker + a showIf-aware management interface
  (fields, event buttons, logs), shipping the committed runtime assets.
  Version is synced to the frameos release version by
  tools/update_versions.py; the release workflow publishes to npm
  (requires the NPM_TOKEN secret; skips when the version exists).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/scenes falls back to the first frame's scene editor again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
POST /api/templates {url} now resolves HTML pages through their
frameos:zip meta tag (link token attached for provider hosts), and the
Templates panel offers 'Add scene from URL' when the search box
contains a pasted URL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- frameos_library.js tries a direct browser XHR before the same-origin
  proxy, so the proxy only carries CORS-blocked hosts (rebuilt wasm
  runtime; behavior also benefits the backend live preview).
- The frameos-wasm manager caps the canvas at min(60vh, 480px).
- The Templates-panel pasted-URL quick action now installs the scenes
  onto the current frame (POST /api/templates format=scenes +
  applyTemplate) instead of saving a local template.
- The npm publish job builds the wasm runtime in CI (nim + emsdk) —
  the artifacts are gitignored build outputs, not committed files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
npm publish refused to run nested under frontend/ (a higher
package.json); frameos/wasm sits next to the Nim wasm runtime it wraps.
Updated the workspace, version-sync tooling, release workflow, docs,
and the Dockerfile (which also needed the package.json copied for
pnpm --frozen-lockfile now that the package is a workspace member).
frameos/wasm/package.json is excluded from the frameos project hash so
the release version-sync write does not itself force a bump on the
next release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Listener nodes filter on payload values (eventNodeMatchesPayload), so
a 'button' listener with label 'A' never fired for the manager's empty
payload.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The visual scene editor now also builds as a standalone static bundle
(frontend/dist-editor): the same Diagram/EditApp code with frameLogic
and logsLogic swapped for in-memory shims (esbuild alias), the app
catalog and app sources embedded at build time, and scenes exchanged
over a documented postMessage protocol. This is how the private
frameos-cloud (and any site) embeds the editor in an iframe without
mixing AGPL code into its own bundle -- same arms-length model as the
cloud-link protocol.

- scripts/generateBuiltinApps.mjs embeds frameos/src/apps configs and
  sources (also improves the main app: catalog/source fallbacks work
  offline).
- frameos/editor npm package: the bundle + a parent-side embed helper
  (createFrameOSEditor) + demo.html showing scene list, editor, and
  frameos-wasm live preview together. AGPL-3.0-only; version synced to
  the frameos release like frameos-wasm; published by the release
  workflow.
- Runtime-verified by frontend/scripts/smokeEditorEmbed.mjs (Playwright:
  nodes render, postMessage round-trip).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
editor.html defaults data-frameos-theme from prefers-color-scheme and
the init message's theme overrides it (applyFrameosTheme + themed
wrapper), so embedding pages render the diagram in their own light or
dark palette.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
apiFetch resolves a project id and redirects to auth screens on
failure — routeToAuthStatus returns a never-resolving promise, so in
the backendless embedded editor loadApps hung forever and every
built-in app node rendered as 'Node'. editor.html now sets
FRAMEOS_EMBEDDED_NO_BACKEND: apiFetch answers all API calls with a
synthetic 404 (loaders' fallbacks — embedded app catalog, fonts —
engage immediately) and socketLogic skips the websocket.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stable names cached forever in browsers meant redeployed bundles never
loaded; the html now points at the hashed editor-*.js/css (monaco
workers keep hashless names for configureMonaco).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The light-theme node/edge styles key off .scene-editor-canvas inside a
non-dark .frameos-app-shell; the embed's diagram wrapper was missing
that class, so light mode rendered with the dark base styles. Verified
with a two-theme browser probe (node text slate-900 on light, gray-200
on dark).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The standalone embedded editor (frameos-editor) now carries the same
side panels as the main app's scene workspace — scene settings (with
rename, stats, and the embedding page's scene description via the new
init `description` field), state variables, the app catalog, events and
raw scene JSON — plus fit-to-view/realign diagram buttons, and app
source editing moved into a modal.

A new Preview panel runs the edited scenes through the frameos-wasm
runtime in the browser: event buttons, editable scene state, the
runtime log (usage notes tucked behind an "i" tooltip), and an API-keys
form for the services the scene's apps need (OpenAI, Unsplash, ...) —
keys stay in the tab and restart the preview when applied. Serve the
frameos-wasm assets next to the bundle (./frameos-wasm/) to enable it,
and pass previewProxyUrl in the init message to route CORS-blocked
fetches. Re-render now restarts the runtime on the current scenes when
they were edited since the preview started (this also fixes the main
app's "Preview in browser" rendering a stale snapshot after edits).

To support this without a backend, embedFrameLogic is now a real
kea-forms frameForm (sharing the per-scene validation via the new
frameFormSceneErrors helper) and grew the lastDeploy/isFrameAdminMode/
sendEvent surface that scenesLogic and friends connect to; controlLogic
sync and projectApi short-circuit under FRAMEOS_EMBEDDED_NO_BACKEND.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Repository templates that carry a page url (the cloud store's
repository.json now provides one per scene) get a "View store page"
entry in their "..." menu, opening the scene's page in a new tab.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Redirecting to login/signup on a 401 must not add a history entry:
Back would loop through the redirect forever, and when the app runs
inside an iframe (FrameOS Cloud's backend viewer) the loop pollutes
the embedding page's history too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Captures the current wasm frame and offers it to the embedding page
over a new frameos-editor:save-screenshot message (FrameOS Cloud adds
it to the scene's image gallery); without an acknowledgement it falls
back to downloading the PNG locally. Protocol documented in the
package README and embed.js.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"Apply & render" joins the event buttons and "Render" in a single row.
State reports from the runtime (which follow every render) used to
reset all form edits; now only values the runtime has confirmed are
dropped, so typing into a field and clicking Render no longer silently
reverts the field. The form also skips rebuilding while an input is
focused, so mid-typing values and focus survive incoming state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Home Assistant authenticates the user itself in ingress mode — the
backend has no login of its own, so there is no cloud-login permission
to ask for or receive. The FrameOS Cloud settings hide the "Cloud
login" feature checkbox there (availableCloudFeatures), along with the
account-link and local-password sections, even when an existing link
still carries the auth:login scope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Flash-then-hang, three causes fixed:
- The DTR/RTS reset pulse strapped XIAO ESP32-S3 (USB-Serial/JTAG) back
  into ROM download mode. Reset via the RTC watchdog over the stub
  protocol instead (esptool --after watchdog-reset), clearing the
  force-download-boot latch first; device-loss after the arming write IS
  the reset succeeding. DTR/RTS pulse stays as non-S3 fallback and can no
  longer abort the flow on a vanished port.
- The watchdog reset re-enumerates USB, leaving the browser's SerialPort
  object permanently dead. resolveLiveSerialPort() re-acquires the granted
  replacement from getPorts(); USB API commands retry on it and the wait
  loop re-resolves each poll.
- First boot after an erase-all flash formats the 24MB SPIFFS state
  partition (~180s measured) before the console starts; the USB-API wait
  now allows 6 minutes, says so, and holds the port open across polls
  (open/close churn toggles DTR/RTS, which can spuriously reset the chip).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gallery scenes painted "Error rendering image: No image provided" on
ESP32: the 2.6MB source PNG was rejected at the HTTP layer (download cap
= half the largest free PSRAM block ≈ 1.4MB at render time next to the
7.7MB canvas), and decode would have needed a second contiguous copy of
the compressed body anyway (multi-IDAT concat in pixie).

- pixie bumped to 4913417: the streaming inflate consumes IDAT chunks as
  segments, so a multi-IDAT PNG decodes with body + ~64KB window + two
  rows — never a full-size RGBA buffer or a concatenated body copy.
- HTTP glue cap is now largest_block - 64KB: with streaming decoders the
  body is the only large allocation a decode needs.
- image.nim skips the full-RGBA guard on the streamed decode path;
  pixie's decode budget covers it accurately.

Verified on frame 59 hardware: the 1024x1024 gallery PNG (326 IDATs)
downloads, streams into the 1200x1600 canvas and refreshes the panel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Frames fetch and render images directly from their sources — never
through the backend as a fetch/resize middleman, and host-side resize
params are not the fix either: oversized sources are handled by better
on-device streaming decode. Proxies are for in-browser previews only.
Written into AGENTS.md, CLOUD-TODO principles, the ESP32 README, and
code comments at the spots where a proxy would be reintroduced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion

The HTTP glue buffered every response in one contiguous PSRAM buffer and
capped downloads at the largest free block, so image size limits depended
on heap fragmentation next to the 7.7MB canvas. Bodies now accumulate in
fixed-size 512KB PSRAM chunks (fos_nim_http_request_chunked): the only
bounds left are the Nim-side max_bytes and a live free-PSRAM reserve.

PNGs decode straight from the chunks — pixie 8b6b6e7 parses the PNG
structure and inflates IDATs from segmented sources, streaming scanlines
into the canvas without ever assembling a contiguous copy of the file.
Other formats coalesce; the legacy contiguous C hook remains for them and
for the WASM shim.

Verified on frame 59: 2.6MB gallery PNG downloads as six chunks and
renders to the panel with ~4MB PSRAM free.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When a scene's image producer failed (e.g. a too-large source before
chunked loading, or any HTTP error), the interpreter swallowed the
exception and render/image only said "No image provided" — the reason
never reached the panel. Now:
- data/frameOSGallery catches download errors and returns a rendered
  error image with the actual message, matching data/downloadImage.
- The interpreter substitutes a rendered error image for any image input
  whose producer raised, so every raising producer gets the same
  treatment; non-image fields keep their defaults (asString now raises
  ValueError on kind mismatch instead of a release-stripped assert).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The firmware build cache keyed only on the per-frame config header and a
manually bumped project version, so deploys kept flashing an old cached
image after code changes — and the device's periodic OTA check then
"downgraded" locally flashed boards back to that stale build (the
version flip-flop between deploys). Builds now record a source
fingerprint (git describe of this repo and the local pixie checkout,
with dirty trees hashing their diff) and go stale when it changes.
EMBEDDED_FIRMWARE_VERSION bumped for the chunked-HTTP/streamed-PNG work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
eraseAll wiped the 24MB SPIFFS state partition on every browser flash,
costing a ~3 minute format on each first boot. The merged image's FF
padding already overwrites NVS, otadata and RF calibration (they sit
inside the written range), so freshly baked defaults still win without
the full-chip erase; the formatted state partition survives and the
board answers within seconds of rebooting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The USB scene upload (console task) and the backend sync (fos_client
task) write /state/scenes.json through the same tmp path; concurrent
write+rename dances deleted each other's tmp file — rename failed ENOENT
right after a successful write, and the raced file could vanish on the
next mount while scenes.etag survived, leaving the frame sceneless with
a matching etag (so syncs 304'd forever). A mutex now serializes the
write+rename sequence. Verified on frame 59: scenes survive both clean
restarts and watchdog resets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mariusandra and others added 14 commits July 14, 2026 03:20
Image producers decode straight into the render canvas on embedded, but
their error paths built a brand-new full-frame image — renderError() at
1200x1600 is a second 7.7MB allocation next to the live canvas, which is
exactly what fatally OOM'd frame 59 tonight when a gallery download
failed mid-render. Data apps now share renderErrorForContext(), which
paints the error into the context canvas on embedded (the consumer
already short-circuits when the producer returns the canvas itself), and
render/svg mirrors the render/image renderErrorInto idiom. The
interpreter's image-input fallback sets the canvas as the field value
first and paints only after the field accepted it, so a non-image field
mismatch leaves the canvas untouched. Host behavior is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fatal-OOM longjmp abandons the Nim stack without running destructors,
so everything the aborted call held — the 7.7MB render canvas, decode
buffers, HTTP chunks — leaks with its refcounts stuck; no GC pass can
ever reclaim it. One abort tonight left 6MB free on a 16MB module and
every subsequent render failed at the canvas allocation: the containment
kept the device alive but permanently unable to render, which is worse
than the reboot it was built to avoid.

All five guarded entry points now feed an abort streak. When two
consecutive aborts leave the largest free PSRAM block smaller than a
render canvas (provably wedged), or on the fourth abort regardless, the
device restarts; a successful render resets the streak, so a one-off
transient abort still just fails that frame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With CONFIG_FREERTOS_HZ=100, pdMS_TO_TICKS(1) rounds to 0 ticks, so the
console task's fgetc poll loop never actually blocked — a priority-2
task busy-spinning on non-blocking stdin starves IDLE0 on CPU 0 and
trips the task watchdog (decoded backtrace: fos_console_usb_task →
fgetc → esp_vfs_read). Sleep 10ms (one full tick) in both the command
loop and the usb_api payload reader so the idle task gets scheduled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ng object literals

Two token-level fixes:
- The } closing a ${...} interpolation was never matched to its ${ opener
  (ttDollarBraceL), so every brace/scope counter desynced past a template
  literal: object keys after a template-literal call argument were marked
  as variable bindings and their string values erased as type annotations
  ({ method: "POST" } became { method, }).
- stripMethodAndMemberTypes took the = of ==/=== for a field-initializer
  boundary, so { ok: value === true } lost its value expression.

Regression tests in test_js_transpiler.nim plus a runtime parity fixture
against npm Sucrase.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… access

New frameos.* bindings for repo JavaScript apps:
- httpRequest(url, {method, headers, body, bodyBase64, base64, timeoutMs}):
  bounded requests with any method and headers; base64: true returns the
  body base64-encoded, since raw bytes cannot cross the JS string boundary.
- Asset management scoped to the frame's assets folder: listAssets,
  assetExists, assetSize, readAsset (with {offset, length} ranged reads),
  writeAsset, appendAsset, deleteAsset. Full-buffer reads are capped at
  2MB on embedded targets; bigger files read in chunks or via streams.
- loadAssetImage: display-bounded, memory-aware decode straight from disk
  into an image reference.
- Streams for chunked processing: openAssetStream (r/w/a), createStream,
  streamRead/Write/AtEnd/Rewind/Close. Refs are plain JSON
  ({__frameosType: "streamRef", id}) held in a capped global registry, so
  a data app can return one and a downstream app can consume it.
- getSetting(...path): reads frameConfig.settings, but only namespaces the
  app declares in its config.json "settings" list, so access travels
  visibly with the scene JSON (same convention as the Nim apps).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… plates

New sample scene + birdJournal JS app. Each render it polls iNaturalist v2
for birds spotted within a configurable radius and time window (licensed
photos only, attribution recorded), logs every species to disk-persisted
scene state, and draws one missing plate per render: reference photos and
a field-journal style prompt go to the OpenAI Responses API
image_generation tool, the model double-checks the plate against a
reference photo (strict-JSON verdict, up to 3 attempts), and verified
plates are saved as PNG assets. The frame then cycles through the
collection with a caption overlay; least-recently-seen species are pruned
beyond a cap.

The template preview is a real render of the scene showing an actual
OpenAI-generated, verification-approved plate. End-to-end test drives the
scene through the interpreter against mock iNaturalist/OpenAI servers;
the scene also renders offline (placeholder + hint) in the repo-scenes
harness when no coordinates are set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Backups and store publishing are included with every cloud account:
requested at link time, silently kept on every feature change, and shown
as always-on rows in settings. Only security-sensitive features (cloud
login) keep a real opt-in checkbox.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
embeddedUsbApiCanUse now checks the remembered port is still connected,
and render / scene activation / scene upload retry over HTTP when the
USB command fails, so an unplugged board no longer strands a
network-reachable frame. Firmware downloads keep the deploy drawer open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rename the backup:templates scope to backup:scenes — nothing in the UI
says "template" anymore (the stored backup kind string stays
"templates" for protocol stability).

The backup scopes still come with every cloud account, but they are a
permission only: new backup_scenes_enabled / backup_frames_enabled
switches on the link (default off, toggled instantly via
POST /api/cloud/backup-features) gate manual pushes and the
after-deploy auto backup, so connecting the cloud alone never uploads
anything.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This PR now carries only the FrameOS Cloud link work. The rest moved
to sibling PRs branched off main:

- ESP32 and embedded runtime hardening -> esp32-embedded-hardening
- Bird field journal scene + JS app runtime -> bird-journal-js-apps
- Embeddable editor + frameos-wasm -> embedded-editor-wasm

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mariusandra mariusandra changed the title FrameOS Cloud link, embedded scene editor, and ESP32 runtime hardening FrameOS Cloud: link backends and frames to cloud.frameos.net Jul 14, 2026
mariusandra added a commit that referenced this pull request Jul 15, 2026
Split out of #257. Device runtime, firmware pipeline, and embedded
frontend workflows:

- Streaming decode of multi-MB PNGs and chunked HTTP bodies, so
  downloads are no longer size-limited by fragmentation; images too
  large for the device paint a download-failure frame instead of
  'No image provided'
- Memory-aware scene node cache (frame-sized images are not pinned),
  OOM-aborted renders that leak the heap beyond recovery trigger a
  restart; render errors paint into the live canvas
- Serialized scenes.json writers, reliable OTA acknowledgement,
  post-flash reboot/reconnect, and the state partition survives
  re-flashing
- Content-addressed firmware download URLs with no-store responses;
  stale firmware cache invalidates on source changes
- USB serial render lifecycle events with Pi event parity; frontend
  USB commands fall back to HTTP when the port is stale; post-deploy
  USB preview retries until the first render exists; identical-board
  reconnects require explicit reselection
- Document the no-image-proxies-for-frames rule

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
mariusandra added a commit that referenced this pull request Jul 15, 2026
Split out of #257. The embedded scene editor and in-browser scene
runtime:

- Embeddable AGPL scene editor bundle published as the frameos-editor
  npm package: full scene workspace panels, theme support via init
  message, light-mode diagram styling, hashed entry files, and a demo
  page; standalone embeds answer API calls with synthetic 404s
- frameos-wasm npm package (moved to frameos/wasm): typed wrapper
  around the emscripten wasm preview worker with a showIf-aware
  management interface; preview HTTP goes client-side first so the
  same-origin proxy only carries CORS-blocked hosts
- Live preview: renders keep unapplied form edits, one action row,
  "Save screenshot" in the Preview panel, event buttons send their
  label in the payload
- Release plumbing: publish-npm workflow job, per-package version
  sync in tools/update_versions.py (editor gets its own component
  hash in versions.json), pnpm workspace + Dockerfile entries
- Auth-guard redirects use replace instead of push so Back does not
  loop through the redirect

The regenerated frontend/src/generated/builtinApps.ts snapshot
includes app data from sibling PRs split out of #257 (bird journal
app, gallery error-image change); `pnpm --dir frontend run build`
regenerates it from the checked-out sources.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
mariusandra and others added 4 commits July 15, 2026 09:22
The cloud work was split into a reviewable stack: cloud-link (device-flow
linking), cloud-login (cloud sign-in), cloud-config-backups (scene/frame
backups). This branch now carries only the store + cloud drive work on top.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mariusandra mariusandra changed the title FrameOS Cloud: link backends and frames to cloud.frameos.net FrameOS Cloud 4/4: cloud store and "My cloud drive" Jul 18, 2026
@mariusandra
mariusandra changed the base branch from main to cloud-config-backups July 18, 2026 21:36
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