FrameOS Cloud 4/4: cloud store and "My cloud drive"#257
Open
mariusandra wants to merge 56 commits into
Open
Conversation
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>
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>
This was referenced 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>
…end-cloud-connect
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>
This was referenced Jul 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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).
store:publishscope): 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<meta name="frameos:zip">tag; URLs on the linked provider get the link token attached so private drive scenes install too<img>tags can't carry the link token; "Save to cloud drive" from the scene dropdowns and template modal, with visibility control on republishimage_scene_id)Test plan
pytest test_cloud_store.py test_repositories.py test_templates.py— green (full backend suite passes)test_cloud_e2e.py(FRAMEOS_CLOUD_E2E_URL)pnpm --dir frontend run buildpasses🤖 Generated with Claude Code