velxio/frontend/src/hooks/useAutoSaveProject.ts

67 lines
2.4 KiB
TypeScript
Raw Normal View History

feat: persist multi-board projects + add auto-save The project save/load pipeline only persisted a single `board_type`, so multi-board workspaces silently lost every board except the active one on save, and wires referencing the dropped boards' IDs orphaned to the canvas corner on reload. An audit of the production backup found 74/306 projects (24%) with at least one orphaned wire and 174/301 non-trivial projects whose code was still the default Blink template — strong signal that users save once and never re-save. Backend - Add `boards_json` column on `projects` with idempotent ALTER TABLE in the lifespan migration list. - New `FileGroup` schema + `file_groups` array on ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat. - `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via `read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on read; legacy single-list `files` only updates the active group, leaving other boards' files intact. - `_persist_files_from_body` honors file_groups → files → code priority. Frontend - `useSimulatorStore.addBoard` accepts an optional `explicitId` so saved board IDs can be restored verbatim (wires reference IDs literally). - New `loadProjectState({boards, fileGroups, components, wires, activeBoardId})` action: tears down current boards, recreates from the payload, restores file groups atomically, recalculates wire positions on the next frame, and refreshes the Interconnect. - `useEditorStore.replaceFileGroups` for atomic multi-group restore. - `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through `buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects by synthesising a default board from `board_type`). Auto-save (#useAutoSaveProject hook) - 2.5s debounced silent PUT triggered ONLY when an authenticated user has a `currentProject` with a UUID. State hash detects real changes vs. UI-only churn; baseline is reset on project load so the just-loaded state isn't immediately re-saved. - `beforeunload` flush via `fetch keepalive: true` (supports PUT + credentials, survives unload). - Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error). Backfill script (one-off, idempotent) - `backend/scripts/backfill_boards_2026_05.py` populates `boards_json` for legacy projects. Heuristic per project, based on which board IDs the wires reference: Case A — wires only ref 'arduino-uno' but board_type ≠ uno: rename id→board_type and rewrite wire endpoints. Case B — single-board normal: keep verbatim. Case C — multi-board: recreate one board per distinct ref, infer kind by stripping trailing -N suffix. Also moves any flat files into the active board's group subdir. Stdlib-only, runs from host or `docker exec`. Docker - `Dockerfile.standalone` now copies `backend/scripts/` into the image so the backfill is callable via `docker exec velxio-app python /app/scripts/backfill_boards_2026_05.py --apply`. Verified locally on the restored production backup (363 projects): 33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans. Re-running the script after apply skips all 363 (idempotent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:43:33 +07:00
/**
refactor(oss-split): introduce extension hooks for auth, DB, metrics, auto-save First phase of the OSS / pro split. Goal: open the seams so the auth/DB/admin stack can move into the private overlay (Phase 2-3) without the routes that stay in OSS (compile, libraries, simulation, iot_gateway) having to know. Backend ------- * New app/core/hooks.py — registry for record_compile, get_current_user_id, and lifespan startup tasks. Each hook is a no-op by default; overlays call register_* in register_pro(app) to plug in a real implementation. * compile.py now imports only from app.core.hooks. Drops the direct deps on app.core.dependencies, app.database.session, app.models.user, and app.services.metrics. Route signatures use `Depends(get_current_user_id)` instead of `Depends(get_current_user)`; the metric helper passes user_id through rather than a User instance. * compile_chip.py drops the unused _current_user Depends entirely. * main.py wraps the auth/DB stack import in try/except. When it succeeds (today's behavior on velxio.dev), an adapter bridges record_compile and get_current_user_id to the existing app.services.metrics + dependencies, and the create_all + ALTER TABLE migration block runs via a registered lifespan_startup hook. When it fails (the post-Phase-2 OSS image), main logs "running stateless" and skips registering anything — the routes still load and behave as no-ops for metrics + always-anonymous for auth. Frontend -------- * useAutoSaveProject becomes a skeleton: one useState + one useEffect that delegates to an installed AutoSaveImpl. installAutoSaveImpl() replaces the impl without changing hook count, so React's rules-of-hooks stay satisfied even after the impl moves out of OSS. * New hooks/autoSaveImpl.ts holds the original logic (debouncing, dirty detection, owner eligibility, fetch keepalive on unload), refactored to emit() instead of useState. It self-registers at module load; main.tsx imports it for the side effect. * AppHeader wraps the entire user-vs-login UI in a data-velxio-slot ="header-auth" boundary. Today the OSS UI still renders inside the slot — the overlay can portal-inject additional items now, and in Phase 3 the slot becomes the sole owner of header auth UX. Behavior is identical on velxio.dev (pro overlay imports everything successfully, every adapter wires up). The change is purely structural: deleting the auth/DB modules tomorrow no longer crashes OSS at import. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:24:51 +07:00
* Auto-save hook skeleton.
feat: persist multi-board projects + add auto-save The project save/load pipeline only persisted a single `board_type`, so multi-board workspaces silently lost every board except the active one on save, and wires referencing the dropped boards' IDs orphaned to the canvas corner on reload. An audit of the production backup found 74/306 projects (24%) with at least one orphaned wire and 174/301 non-trivial projects whose code was still the default Blink template — strong signal that users save once and never re-save. Backend - Add `boards_json` column on `projects` with idempotent ALTER TABLE in the lifespan migration list. - New `FileGroup` schema + `file_groups` array on ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat. - `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via `read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on read; legacy single-list `files` only updates the active group, leaving other boards' files intact. - `_persist_files_from_body` honors file_groups → files → code priority. Frontend - `useSimulatorStore.addBoard` accepts an optional `explicitId` so saved board IDs can be restored verbatim (wires reference IDs literally). - New `loadProjectState({boards, fileGroups, components, wires, activeBoardId})` action: tears down current boards, recreates from the payload, restores file groups atomically, recalculates wire positions on the next frame, and refreshes the Interconnect. - `useEditorStore.replaceFileGroups` for atomic multi-group restore. - `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through `buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects by synthesising a default board from `board_type`). Auto-save (#useAutoSaveProject hook) - 2.5s debounced silent PUT triggered ONLY when an authenticated user has a `currentProject` with a UUID. State hash detects real changes vs. UI-only churn; baseline is reset on project load so the just-loaded state isn't immediately re-saved. - `beforeunload` flush via `fetch keepalive: true` (supports PUT + credentials, survives unload). - Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error). Backfill script (one-off, idempotent) - `backend/scripts/backfill_boards_2026_05.py` populates `boards_json` for legacy projects. Heuristic per project, based on which board IDs the wires reference: Case A — wires only ref 'arduino-uno' but board_type ≠ uno: rename id→board_type and rewrite wire endpoints. Case B — single-board normal: keep verbatim. Case C — multi-board: recreate one board per distinct ref, infer kind by stripping trailing -N suffix. Also moves any flat files into the active board's group subdir. Stdlib-only, runs from host or `docker exec`. Docker - `Dockerfile.standalone` now copies `backend/scripts/` into the image so the backfill is callable via `docker exec velxio-app python /app/scripts/backfill_boards_2026_05.py --apply`. Verified locally on the restored production backup (363 projects): 33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans. Re-running the script after apply skips all 363 (idempotent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:43:33 +07:00
*
refactor(oss-split): introduce extension hooks for auth, DB, metrics, auto-save First phase of the OSS / pro split. Goal: open the seams so the auth/DB/admin stack can move into the private overlay (Phase 2-3) without the routes that stay in OSS (compile, libraries, simulation, iot_gateway) having to know. Backend ------- * New app/core/hooks.py — registry for record_compile, get_current_user_id, and lifespan startup tasks. Each hook is a no-op by default; overlays call register_* in register_pro(app) to plug in a real implementation. * compile.py now imports only from app.core.hooks. Drops the direct deps on app.core.dependencies, app.database.session, app.models.user, and app.services.metrics. Route signatures use `Depends(get_current_user_id)` instead of `Depends(get_current_user)`; the metric helper passes user_id through rather than a User instance. * compile_chip.py drops the unused _current_user Depends entirely. * main.py wraps the auth/DB stack import in try/except. When it succeeds (today's behavior on velxio.dev), an adapter bridges record_compile and get_current_user_id to the existing app.services.metrics + dependencies, and the create_all + ALTER TABLE migration block runs via a registered lifespan_startup hook. When it fails (the post-Phase-2 OSS image), main logs "running stateless" and skips registering anything — the routes still load and behave as no-ops for metrics + always-anonymous for auth. Frontend -------- * useAutoSaveProject becomes a skeleton: one useState + one useEffect that delegates to an installed AutoSaveImpl. installAutoSaveImpl() replaces the impl without changing hook count, so React's rules-of-hooks stay satisfied even after the impl moves out of OSS. * New hooks/autoSaveImpl.ts holds the original logic (debouncing, dirty detection, owner eligibility, fetch keepalive on unload), refactored to emit() instead of useState. It self-registers at module load; main.tsx imports it for the side effect. * AppHeader wraps the entire user-vs-login UI in a data-velxio-slot ="header-auth" boundary. Today the OSS UI still renders inside the slot — the overlay can portal-inject additional items now, and in Phase 3 the slot becomes the sole owner of header auth UX. Behavior is identical on velxio.dev (pro overlay imports everything successfully, every adapter wires up). The change is purely structural: deleting the auth/DB modules tomorrow no longer crashes OSS at import. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:24:51 +07:00
* The actual save logic (debouncing, dirty detection, owner eligibility,
* PUT to /api/projects/{id}) is supplied by an installed implementation.
* OSS without an overlay registers no implementation, and the hook stays
* idle forever exactly the behavior we want once project persistence
* moves to the private overlay (Phase 3 of the OSS split).
feat: persist multi-board projects + add auto-save The project save/load pipeline only persisted a single `board_type`, so multi-board workspaces silently lost every board except the active one on save, and wires referencing the dropped boards' IDs orphaned to the canvas corner on reload. An audit of the production backup found 74/306 projects (24%) with at least one orphaned wire and 174/301 non-trivial projects whose code was still the default Blink template — strong signal that users save once and never re-save. Backend - Add `boards_json` column on `projects` with idempotent ALTER TABLE in the lifespan migration list. - New `FileGroup` schema + `file_groups` array on ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat. - `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via `read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on read; legacy single-list `files` only updates the active group, leaving other boards' files intact. - `_persist_files_from_body` honors file_groups → files → code priority. Frontend - `useSimulatorStore.addBoard` accepts an optional `explicitId` so saved board IDs can be restored verbatim (wires reference IDs literally). - New `loadProjectState({boards, fileGroups, components, wires, activeBoardId})` action: tears down current boards, recreates from the payload, restores file groups atomically, recalculates wire positions on the next frame, and refreshes the Interconnect. - `useEditorStore.replaceFileGroups` for atomic multi-group restore. - `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through `buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects by synthesising a default board from `board_type`). Auto-save (#useAutoSaveProject hook) - 2.5s debounced silent PUT triggered ONLY when an authenticated user has a `currentProject` with a UUID. State hash detects real changes vs. UI-only churn; baseline is reset on project load so the just-loaded state isn't immediately re-saved. - `beforeunload` flush via `fetch keepalive: true` (supports PUT + credentials, survives unload). - Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error). Backfill script (one-off, idempotent) - `backend/scripts/backfill_boards_2026_05.py` populates `boards_json` for legacy projects. Heuristic per project, based on which board IDs the wires reference: Case A — wires only ref 'arduino-uno' but board_type ≠ uno: rename id→board_type and rewrite wire endpoints. Case B — single-board normal: keep verbatim. Case C — multi-board: recreate one board per distinct ref, infer kind by stripping trailing -N suffix. Also moves any flat files into the active board's group subdir. Stdlib-only, runs from host or `docker exec`. Docker - `Dockerfile.standalone` now copies `backend/scripts/` into the image so the backfill is callable via `docker exec velxio-app python /app/scripts/backfill_boards_2026_05.py --apply`. Verified locally on the restored production backup (363 projects): 33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans. Re-running the script after apply skips all 363 (idempotent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:43:33 +07:00
*
refactor(oss-split): introduce extension hooks for auth, DB, metrics, auto-save First phase of the OSS / pro split. Goal: open the seams so the auth/DB/admin stack can move into the private overlay (Phase 2-3) without the routes that stay in OSS (compile, libraries, simulation, iot_gateway) having to know. Backend ------- * New app/core/hooks.py — registry for record_compile, get_current_user_id, and lifespan startup tasks. Each hook is a no-op by default; overlays call register_* in register_pro(app) to plug in a real implementation. * compile.py now imports only from app.core.hooks. Drops the direct deps on app.core.dependencies, app.database.session, app.models.user, and app.services.metrics. Route signatures use `Depends(get_current_user_id)` instead of `Depends(get_current_user)`; the metric helper passes user_id through rather than a User instance. * compile_chip.py drops the unused _current_user Depends entirely. * main.py wraps the auth/DB stack import in try/except. When it succeeds (today's behavior on velxio.dev), an adapter bridges record_compile and get_current_user_id to the existing app.services.metrics + dependencies, and the create_all + ALTER TABLE migration block runs via a registered lifespan_startup hook. When it fails (the post-Phase-2 OSS image), main logs "running stateless" and skips registering anything — the routes still load and behave as no-ops for metrics + always-anonymous for auth. Frontend -------- * useAutoSaveProject becomes a skeleton: one useState + one useEffect that delegates to an installed AutoSaveImpl. installAutoSaveImpl() replaces the impl without changing hook count, so React's rules-of-hooks stay satisfied even after the impl moves out of OSS. * New hooks/autoSaveImpl.ts holds the original logic (debouncing, dirty detection, owner eligibility, fetch keepalive on unload), refactored to emit() instead of useState. It self-registers at module load; main.tsx imports it for the side effect. * AppHeader wraps the entire user-vs-login UI in a data-velxio-slot ="header-auth" boundary. Today the OSS UI still renders inside the slot — the overlay can portal-inject additional items now, and in Phase 3 the slot becomes the sole owner of header auth UX. Behavior is identical on velxio.dev (pro overlay imports everything successfully, every adapter wires up). The change is purely structural: deleting the auth/DB modules tomorrow no longer crashes OSS at import. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:24:51 +07:00
* The skeleton always runs the same useState + useEffect, so registering
* an implementation later cannot change the hook count and break React.
* Implementations are expected to be installed once at module load via
* installAutoSaveImpl() see ./autoSaveImpl.ts for the default wiring.
feat: persist multi-board projects + add auto-save The project save/load pipeline only persisted a single `board_type`, so multi-board workspaces silently lost every board except the active one on save, and wires referencing the dropped boards' IDs orphaned to the canvas corner on reload. An audit of the production backup found 74/306 projects (24%) with at least one orphaned wire and 174/301 non-trivial projects whose code was still the default Blink template — strong signal that users save once and never re-save. Backend - Add `boards_json` column on `projects` with idempotent ALTER TABLE in the lifespan migration list. - New `FileGroup` schema + `file_groups` array on ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat. - `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via `read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on read; legacy single-list `files` only updates the active group, leaving other boards' files intact. - `_persist_files_from_body` honors file_groups → files → code priority. Frontend - `useSimulatorStore.addBoard` accepts an optional `explicitId` so saved board IDs can be restored verbatim (wires reference IDs literally). - New `loadProjectState({boards, fileGroups, components, wires, activeBoardId})` action: tears down current boards, recreates from the payload, restores file groups atomically, recalculates wire positions on the next frame, and refreshes the Interconnect. - `useEditorStore.replaceFileGroups` for atomic multi-group restore. - `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through `buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects by synthesising a default board from `board_type`). Auto-save (#useAutoSaveProject hook) - 2.5s debounced silent PUT triggered ONLY when an authenticated user has a `currentProject` with a UUID. State hash detects real changes vs. UI-only churn; baseline is reset on project load so the just-loaded state isn't immediately re-saved. - `beforeunload` flush via `fetch keepalive: true` (supports PUT + credentials, survives unload). - Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error). Backfill script (one-off, idempotent) - `backend/scripts/backfill_boards_2026_05.py` populates `boards_json` for legacy projects. Heuristic per project, based on which board IDs the wires reference: Case A — wires only ref 'arduino-uno' but board_type ≠ uno: rename id→board_type and rewrite wire endpoints. Case B — single-board normal: keep verbatim. Case C — multi-board: recreate one board per distinct ref, infer kind by stripping trailing -N suffix. Also moves any flat files into the active board's group subdir. Stdlib-only, runs from host or `docker exec`. Docker - `Dockerfile.standalone` now copies `backend/scripts/` into the image so the backfill is callable via `docker exec velxio-app python /app/scripts/backfill_boards_2026_05.py --apply`. Verified locally on the restored production backup (363 projects): 33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans. Re-running the script after apply skips all 363 (idempotent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:43:33 +07:00
*/
refactor(oss-split): introduce extension hooks for auth, DB, metrics, auto-save First phase of the OSS / pro split. Goal: open the seams so the auth/DB/admin stack can move into the private overlay (Phase 2-3) without the routes that stay in OSS (compile, libraries, simulation, iot_gateway) having to know. Backend ------- * New app/core/hooks.py — registry for record_compile, get_current_user_id, and lifespan startup tasks. Each hook is a no-op by default; overlays call register_* in register_pro(app) to plug in a real implementation. * compile.py now imports only from app.core.hooks. Drops the direct deps on app.core.dependencies, app.database.session, app.models.user, and app.services.metrics. Route signatures use `Depends(get_current_user_id)` instead of `Depends(get_current_user)`; the metric helper passes user_id through rather than a User instance. * compile_chip.py drops the unused _current_user Depends entirely. * main.py wraps the auth/DB stack import in try/except. When it succeeds (today's behavior on velxio.dev), an adapter bridges record_compile and get_current_user_id to the existing app.services.metrics + dependencies, and the create_all + ALTER TABLE migration block runs via a registered lifespan_startup hook. When it fails (the post-Phase-2 OSS image), main logs "running stateless" and skips registering anything — the routes still load and behave as no-ops for metrics + always-anonymous for auth. Frontend -------- * useAutoSaveProject becomes a skeleton: one useState + one useEffect that delegates to an installed AutoSaveImpl. installAutoSaveImpl() replaces the impl without changing hook count, so React's rules-of-hooks stay satisfied even after the impl moves out of OSS. * New hooks/autoSaveImpl.ts holds the original logic (debouncing, dirty detection, owner eligibility, fetch keepalive on unload), refactored to emit() instead of useState. It self-registers at module load; main.tsx imports it for the side effect. * AppHeader wraps the entire user-vs-login UI in a data-velxio-slot ="header-auth" boundary. Today the OSS UI still renders inside the slot — the overlay can portal-inject additional items now, and in Phase 3 the slot becomes the sole owner of header auth UX. Behavior is identical on velxio.dev (pro overlay imports everything successfully, every adapter wires up). The change is purely structural: deleting the auth/DB modules tomorrow no longer crashes OSS at import. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:24:51 +07:00
import { useEffect, useState } from 'react';
feat: persist multi-board projects + add auto-save The project save/load pipeline only persisted a single `board_type`, so multi-board workspaces silently lost every board except the active one on save, and wires referencing the dropped boards' IDs orphaned to the canvas corner on reload. An audit of the production backup found 74/306 projects (24%) with at least one orphaned wire and 174/301 non-trivial projects whose code was still the default Blink template — strong signal that users save once and never re-save. Backend - Add `boards_json` column on `projects` with idempotent ALTER TABLE in the lifespan migration list. - New `FileGroup` schema + `file_groups` array on ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat. - `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via `read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on read; legacy single-list `files` only updates the active group, leaving other boards' files intact. - `_persist_files_from_body` honors file_groups → files → code priority. Frontend - `useSimulatorStore.addBoard` accepts an optional `explicitId` so saved board IDs can be restored verbatim (wires reference IDs literally). - New `loadProjectState({boards, fileGroups, components, wires, activeBoardId})` action: tears down current boards, recreates from the payload, restores file groups atomically, recalculates wire positions on the next frame, and refreshes the Interconnect. - `useEditorStore.replaceFileGroups` for atomic multi-group restore. - `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through `buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects by synthesising a default board from `board_type`). Auto-save (#useAutoSaveProject hook) - 2.5s debounced silent PUT triggered ONLY when an authenticated user has a `currentProject` with a UUID. State hash detects real changes vs. UI-only churn; baseline is reset on project load so the just-loaded state isn't immediately re-saved. - `beforeunload` flush via `fetch keepalive: true` (supports PUT + credentials, survives unload). - Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error). Backfill script (one-off, idempotent) - `backend/scripts/backfill_boards_2026_05.py` populates `boards_json` for legacy projects. Heuristic per project, based on which board IDs the wires reference: Case A — wires only ref 'arduino-uno' but board_type ≠ uno: rename id→board_type and rewrite wire endpoints. Case B — single-board normal: keep verbatim. Case C — multi-board: recreate one board per distinct ref, infer kind by stripping trailing -N suffix. Also moves any flat files into the active board's group subdir. Stdlib-only, runs from host or `docker exec`. Docker - `Dockerfile.standalone` now copies `backend/scripts/` into the image so the backfill is callable via `docker exec velxio-app python /app/scripts/backfill_boards_2026_05.py --apply`. Verified locally on the restored production backup (363 projects): 33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans. Re-running the script after apply skips all 363 (idempotent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:43:33 +07:00
export type AutoSaveStatus = 'idle' | 'dirty' | 'saving' | 'saved' | 'error';
export interface AutoSaveState {
status: AutoSaveStatus;
lastSavedAt: number | null;
errorMessage: string | null;
}
refactor(oss-split): introduce extension hooks for auth, DB, metrics, auto-save First phase of the OSS / pro split. Goal: open the seams so the auth/DB/admin stack can move into the private overlay (Phase 2-3) without the routes that stay in OSS (compile, libraries, simulation, iot_gateway) having to know. Backend ------- * New app/core/hooks.py — registry for record_compile, get_current_user_id, and lifespan startup tasks. Each hook is a no-op by default; overlays call register_* in register_pro(app) to plug in a real implementation. * compile.py now imports only from app.core.hooks. Drops the direct deps on app.core.dependencies, app.database.session, app.models.user, and app.services.metrics. Route signatures use `Depends(get_current_user_id)` instead of `Depends(get_current_user)`; the metric helper passes user_id through rather than a User instance. * compile_chip.py drops the unused _current_user Depends entirely. * main.py wraps the auth/DB stack import in try/except. When it succeeds (today's behavior on velxio.dev), an adapter bridges record_compile and get_current_user_id to the existing app.services.metrics + dependencies, and the create_all + ALTER TABLE migration block runs via a registered lifespan_startup hook. When it fails (the post-Phase-2 OSS image), main logs "running stateless" and skips registering anything — the routes still load and behave as no-ops for metrics + always-anonymous for auth. Frontend -------- * useAutoSaveProject becomes a skeleton: one useState + one useEffect that delegates to an installed AutoSaveImpl. installAutoSaveImpl() replaces the impl without changing hook count, so React's rules-of-hooks stay satisfied even after the impl moves out of OSS. * New hooks/autoSaveImpl.ts holds the original logic (debouncing, dirty detection, owner eligibility, fetch keepalive on unload), refactored to emit() instead of useState. It self-registers at module load; main.tsx imports it for the side effect. * AppHeader wraps the entire user-vs-login UI in a data-velxio-slot ="header-auth" boundary. Today the OSS UI still renders inside the slot — the overlay can portal-inject additional items now, and in Phase 3 the slot becomes the sole owner of header auth UX. Behavior is identical on velxio.dev (pro overlay imports everything successfully, every adapter wires up). The change is purely structural: deleting the auth/DB modules tomorrow no longer crashes OSS at import. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:24:51 +07:00
/** Implementation contract: receive a setter, return an unsubscribe. */
export type AutoSaveImpl = (emit: (state: AutoSaveState) => void) => () => void;
feat: persist multi-board projects + add auto-save The project save/load pipeline only persisted a single `board_type`, so multi-board workspaces silently lost every board except the active one on save, and wires referencing the dropped boards' IDs orphaned to the canvas corner on reload. An audit of the production backup found 74/306 projects (24%) with at least one orphaned wire and 174/301 non-trivial projects whose code was still the default Blink template — strong signal that users save once and never re-save. Backend - Add `boards_json` column on `projects` with idempotent ALTER TABLE in the lifespan migration list. - New `FileGroup` schema + `file_groups` array on ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat. - `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via `read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on read; legacy single-list `files` only updates the active group, leaving other boards' files intact. - `_persist_files_from_body` honors file_groups → files → code priority. Frontend - `useSimulatorStore.addBoard` accepts an optional `explicitId` so saved board IDs can be restored verbatim (wires reference IDs literally). - New `loadProjectState({boards, fileGroups, components, wires, activeBoardId})` action: tears down current boards, recreates from the payload, restores file groups atomically, recalculates wire positions on the next frame, and refreshes the Interconnect. - `useEditorStore.replaceFileGroups` for atomic multi-group restore. - `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through `buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects by synthesising a default board from `board_type`). Auto-save (#useAutoSaveProject hook) - 2.5s debounced silent PUT triggered ONLY when an authenticated user has a `currentProject` with a UUID. State hash detects real changes vs. UI-only churn; baseline is reset on project load so the just-loaded state isn't immediately re-saved. - `beforeunload` flush via `fetch keepalive: true` (supports PUT + credentials, survives unload). - Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error). Backfill script (one-off, idempotent) - `backend/scripts/backfill_boards_2026_05.py` populates `boards_json` for legacy projects. Heuristic per project, based on which board IDs the wires reference: Case A — wires only ref 'arduino-uno' but board_type ≠ uno: rename id→board_type and rewrite wire endpoints. Case B — single-board normal: keep verbatim. Case C — multi-board: recreate one board per distinct ref, infer kind by stripping trailing -N suffix. Also moves any flat files into the active board's group subdir. Stdlib-only, runs from host or `docker exec`. Docker - `Dockerfile.standalone` now copies `backend/scripts/` into the image so the backfill is callable via `docker exec velxio-app python /app/scripts/backfill_boards_2026_05.py --apply`. Verified locally on the restored production backup (363 projects): 33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans. Re-running the script after apply skips all 363 (idempotent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:43:33 +07:00
refactor(oss-split): introduce extension hooks for auth, DB, metrics, auto-save First phase of the OSS / pro split. Goal: open the seams so the auth/DB/admin stack can move into the private overlay (Phase 2-3) without the routes that stay in OSS (compile, libraries, simulation, iot_gateway) having to know. Backend ------- * New app/core/hooks.py — registry for record_compile, get_current_user_id, and lifespan startup tasks. Each hook is a no-op by default; overlays call register_* in register_pro(app) to plug in a real implementation. * compile.py now imports only from app.core.hooks. Drops the direct deps on app.core.dependencies, app.database.session, app.models.user, and app.services.metrics. Route signatures use `Depends(get_current_user_id)` instead of `Depends(get_current_user)`; the metric helper passes user_id through rather than a User instance. * compile_chip.py drops the unused _current_user Depends entirely. * main.py wraps the auth/DB stack import in try/except. When it succeeds (today's behavior on velxio.dev), an adapter bridges record_compile and get_current_user_id to the existing app.services.metrics + dependencies, and the create_all + ALTER TABLE migration block runs via a registered lifespan_startup hook. When it fails (the post-Phase-2 OSS image), main logs "running stateless" and skips registering anything — the routes still load and behave as no-ops for metrics + always-anonymous for auth. Frontend -------- * useAutoSaveProject becomes a skeleton: one useState + one useEffect that delegates to an installed AutoSaveImpl. installAutoSaveImpl() replaces the impl without changing hook count, so React's rules-of-hooks stay satisfied even after the impl moves out of OSS. * New hooks/autoSaveImpl.ts holds the original logic (debouncing, dirty detection, owner eligibility, fetch keepalive on unload), refactored to emit() instead of useState. It self-registers at module load; main.tsx imports it for the side effect. * AppHeader wraps the entire user-vs-login UI in a data-velxio-slot ="header-auth" boundary. Today the OSS UI still renders inside the slot — the overlay can portal-inject additional items now, and in Phase 3 the slot becomes the sole owner of header auth UX. Behavior is identical on velxio.dev (pro overlay imports everything successfully, every adapter wires up). The change is purely structural: deleting the auth/DB modules tomorrow no longer crashes OSS at import. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:24:51 +07:00
const IDLE: AutoSaveState = { status: 'idle', lastSavedAt: null, errorMessage: null };
feat: persist multi-board projects + add auto-save The project save/load pipeline only persisted a single `board_type`, so multi-board workspaces silently lost every board except the active one on save, and wires referencing the dropped boards' IDs orphaned to the canvas corner on reload. An audit of the production backup found 74/306 projects (24%) with at least one orphaned wire and 174/301 non-trivial projects whose code was still the default Blink template — strong signal that users save once and never re-save. Backend - Add `boards_json` column on `projects` with idempotent ALTER TABLE in the lifespan migration list. - New `FileGroup` schema + `file_groups` array on ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat. - `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via `read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on read; legacy single-list `files` only updates the active group, leaving other boards' files intact. - `_persist_files_from_body` honors file_groups → files → code priority. Frontend - `useSimulatorStore.addBoard` accepts an optional `explicitId` so saved board IDs can be restored verbatim (wires reference IDs literally). - New `loadProjectState({boards, fileGroups, components, wires, activeBoardId})` action: tears down current boards, recreates from the payload, restores file groups atomically, recalculates wire positions on the next frame, and refreshes the Interconnect. - `useEditorStore.replaceFileGroups` for atomic multi-group restore. - `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through `buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects by synthesising a default board from `board_type`). Auto-save (#useAutoSaveProject hook) - 2.5s debounced silent PUT triggered ONLY when an authenticated user has a `currentProject` with a UUID. State hash detects real changes vs. UI-only churn; baseline is reset on project load so the just-loaded state isn't immediately re-saved. - `beforeunload` flush via `fetch keepalive: true` (supports PUT + credentials, survives unload). - Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error). Backfill script (one-off, idempotent) - `backend/scripts/backfill_boards_2026_05.py` populates `boards_json` for legacy projects. Heuristic per project, based on which board IDs the wires reference: Case A — wires only ref 'arduino-uno' but board_type ≠ uno: rename id→board_type and rewrite wire endpoints. Case B — single-board normal: keep verbatim. Case C — multi-board: recreate one board per distinct ref, infer kind by stripping trailing -N suffix. Also moves any flat files into the active board's group subdir. Stdlib-only, runs from host or `docker exec`. Docker - `Dockerfile.standalone` now copies `backend/scripts/` into the image so the backfill is callable via `docker exec velxio-app python /app/scripts/backfill_boards_2026_05.py --apply`. Verified locally on the restored production backup (363 projects): 33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans. Re-running the script after apply skips all 363 (idempotent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:43:33 +07:00
refactor(oss-split): introduce extension hooks for auth, DB, metrics, auto-save First phase of the OSS / pro split. Goal: open the seams so the auth/DB/admin stack can move into the private overlay (Phase 2-3) without the routes that stay in OSS (compile, libraries, simulation, iot_gateway) having to know. Backend ------- * New app/core/hooks.py — registry for record_compile, get_current_user_id, and lifespan startup tasks. Each hook is a no-op by default; overlays call register_* in register_pro(app) to plug in a real implementation. * compile.py now imports only from app.core.hooks. Drops the direct deps on app.core.dependencies, app.database.session, app.models.user, and app.services.metrics. Route signatures use `Depends(get_current_user_id)` instead of `Depends(get_current_user)`; the metric helper passes user_id through rather than a User instance. * compile_chip.py drops the unused _current_user Depends entirely. * main.py wraps the auth/DB stack import in try/except. When it succeeds (today's behavior on velxio.dev), an adapter bridges record_compile and get_current_user_id to the existing app.services.metrics + dependencies, and the create_all + ALTER TABLE migration block runs via a registered lifespan_startup hook. When it fails (the post-Phase-2 OSS image), main logs "running stateless" and skips registering anything — the routes still load and behave as no-ops for metrics + always-anonymous for auth. Frontend -------- * useAutoSaveProject becomes a skeleton: one useState + one useEffect that delegates to an installed AutoSaveImpl. installAutoSaveImpl() replaces the impl without changing hook count, so React's rules-of-hooks stay satisfied even after the impl moves out of OSS. * New hooks/autoSaveImpl.ts holds the original logic (debouncing, dirty detection, owner eligibility, fetch keepalive on unload), refactored to emit() instead of useState. It self-registers at module load; main.tsx imports it for the side effect. * AppHeader wraps the entire user-vs-login UI in a data-velxio-slot ="header-auth" boundary. Today the OSS UI still renders inside the slot — the overlay can portal-inject additional items now, and in Phase 3 the slot becomes the sole owner of header auth UX. Behavior is identical on velxio.dev (pro overlay imports everything successfully, every adapter wires up). The change is purely structural: deleting the auth/DB modules tomorrow no longer crashes OSS at import. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:24:51 +07:00
let installedImpl: AutoSaveImpl | null = null;
feat: persist multi-board projects + add auto-save The project save/load pipeline only persisted a single `board_type`, so multi-board workspaces silently lost every board except the active one on save, and wires referencing the dropped boards' IDs orphaned to the canvas corner on reload. An audit of the production backup found 74/306 projects (24%) with at least one orphaned wire and 174/301 non-trivial projects whose code was still the default Blink template — strong signal that users save once and never re-save. Backend - Add `boards_json` column on `projects` with idempotent ALTER TABLE in the lifespan migration list. - New `FileGroup` schema + `file_groups` array on ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat. - `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via `read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on read; legacy single-list `files` only updates the active group, leaving other boards' files intact. - `_persist_files_from_body` honors file_groups → files → code priority. Frontend - `useSimulatorStore.addBoard` accepts an optional `explicitId` so saved board IDs can be restored verbatim (wires reference IDs literally). - New `loadProjectState({boards, fileGroups, components, wires, activeBoardId})` action: tears down current boards, recreates from the payload, restores file groups atomically, recalculates wire positions on the next frame, and refreshes the Interconnect. - `useEditorStore.replaceFileGroups` for atomic multi-group restore. - `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through `buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects by synthesising a default board from `board_type`). Auto-save (#useAutoSaveProject hook) - 2.5s debounced silent PUT triggered ONLY when an authenticated user has a `currentProject` with a UUID. State hash detects real changes vs. UI-only churn; baseline is reset on project load so the just-loaded state isn't immediately re-saved. - `beforeunload` flush via `fetch keepalive: true` (supports PUT + credentials, survives unload). - Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error). Backfill script (one-off, idempotent) - `backend/scripts/backfill_boards_2026_05.py` populates `boards_json` for legacy projects. Heuristic per project, based on which board IDs the wires reference: Case A — wires only ref 'arduino-uno' but board_type ≠ uno: rename id→board_type and rewrite wire endpoints. Case B — single-board normal: keep verbatim. Case C — multi-board: recreate one board per distinct ref, infer kind by stripping trailing -N suffix. Also moves any flat files into the active board's group subdir. Stdlib-only, runs from host or `docker exec`. Docker - `Dockerfile.standalone` now copies `backend/scripts/` into the image so the backfill is callable via `docker exec velxio-app python /app/scripts/backfill_boards_2026_05.py --apply`. Verified locally on the restored production backup (363 projects): 33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans. Re-running the script after apply skips all 363 (idempotent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:43:33 +07:00
/** Hooks that mounted before an impl was installed, waiting to start it. */
const installWaiters = new Set<() => void>();
refactor(oss-split): introduce extension hooks for auth, DB, metrics, auto-save First phase of the OSS / pro split. Goal: open the seams so the auth/DB/admin stack can move into the private overlay (Phase 2-3) without the routes that stay in OSS (compile, libraries, simulation, iot_gateway) having to know. Backend ------- * New app/core/hooks.py — registry for record_compile, get_current_user_id, and lifespan startup tasks. Each hook is a no-op by default; overlays call register_* in register_pro(app) to plug in a real implementation. * compile.py now imports only from app.core.hooks. Drops the direct deps on app.core.dependencies, app.database.session, app.models.user, and app.services.metrics. Route signatures use `Depends(get_current_user_id)` instead of `Depends(get_current_user)`; the metric helper passes user_id through rather than a User instance. * compile_chip.py drops the unused _current_user Depends entirely. * main.py wraps the auth/DB stack import in try/except. When it succeeds (today's behavior on velxio.dev), an adapter bridges record_compile and get_current_user_id to the existing app.services.metrics + dependencies, and the create_all + ALTER TABLE migration block runs via a registered lifespan_startup hook. When it fails (the post-Phase-2 OSS image), main logs "running stateless" and skips registering anything — the routes still load and behave as no-ops for metrics + always-anonymous for auth. Frontend -------- * useAutoSaveProject becomes a skeleton: one useState + one useEffect that delegates to an installed AutoSaveImpl. installAutoSaveImpl() replaces the impl without changing hook count, so React's rules-of-hooks stay satisfied even after the impl moves out of OSS. * New hooks/autoSaveImpl.ts holds the original logic (debouncing, dirty detection, owner eligibility, fetch keepalive on unload), refactored to emit() instead of useState. It self-registers at module load; main.tsx imports it for the side effect. * AppHeader wraps the entire user-vs-login UI in a data-velxio-slot ="header-auth" boundary. Today the OSS UI still renders inside the slot — the overlay can portal-inject additional items now, and in Phase 3 the slot becomes the sole owner of header auth UX. Behavior is identical on velxio.dev (pro overlay imports everything successfully, every adapter wires up). The change is purely structural: deleting the auth/DB modules tomorrow no longer crashes OSS at import. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:24:51 +07:00
export function installAutoSaveImpl(impl: AutoSaveImpl | null): void {
installedImpl = impl;
// Overlays load through a dynamic import that races the first React
// commit: a hook whose mount effect ran before the overlay chunk
// evaluated used to see `installedImpl === null` and stay idle for the
// whole life of the tab — no auto-save, no unload flush. Start those
// already-mounted hooks now that the impl exists.
if (impl) installWaiters.forEach((start) => start());
refactor(oss-split): introduce extension hooks for auth, DB, metrics, auto-save First phase of the OSS / pro split. Goal: open the seams so the auth/DB/admin stack can move into the private overlay (Phase 2-3) without the routes that stay in OSS (compile, libraries, simulation, iot_gateway) having to know. Backend ------- * New app/core/hooks.py — registry for record_compile, get_current_user_id, and lifespan startup tasks. Each hook is a no-op by default; overlays call register_* in register_pro(app) to plug in a real implementation. * compile.py now imports only from app.core.hooks. Drops the direct deps on app.core.dependencies, app.database.session, app.models.user, and app.services.metrics. Route signatures use `Depends(get_current_user_id)` instead of `Depends(get_current_user)`; the metric helper passes user_id through rather than a User instance. * compile_chip.py drops the unused _current_user Depends entirely. * main.py wraps the auth/DB stack import in try/except. When it succeeds (today's behavior on velxio.dev), an adapter bridges record_compile and get_current_user_id to the existing app.services.metrics + dependencies, and the create_all + ALTER TABLE migration block runs via a registered lifespan_startup hook. When it fails (the post-Phase-2 OSS image), main logs "running stateless" and skips registering anything — the routes still load and behave as no-ops for metrics + always-anonymous for auth. Frontend -------- * useAutoSaveProject becomes a skeleton: one useState + one useEffect that delegates to an installed AutoSaveImpl. installAutoSaveImpl() replaces the impl without changing hook count, so React's rules-of-hooks stay satisfied even after the impl moves out of OSS. * New hooks/autoSaveImpl.ts holds the original logic (debouncing, dirty detection, owner eligibility, fetch keepalive on unload), refactored to emit() instead of useState. It self-registers at module load; main.tsx imports it for the side effect. * AppHeader wraps the entire user-vs-login UI in a data-velxio-slot ="header-auth" boundary. Today the OSS UI still renders inside the slot — the overlay can portal-inject additional items now, and in Phase 3 the slot becomes the sole owner of header auth UX. Behavior is identical on velxio.dev (pro overlay imports everything successfully, every adapter wires up). The change is purely structural: deleting the auth/DB modules tomorrow no longer crashes OSS at import. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:24:51 +07:00
}
feat: persist multi-board projects + add auto-save The project save/load pipeline only persisted a single `board_type`, so multi-board workspaces silently lost every board except the active one on save, and wires referencing the dropped boards' IDs orphaned to the canvas corner on reload. An audit of the production backup found 74/306 projects (24%) with at least one orphaned wire and 174/301 non-trivial projects whose code was still the default Blink template — strong signal that users save once and never re-save. Backend - Add `boards_json` column on `projects` with idempotent ALTER TABLE in the lifespan migration list. - New `FileGroup` schema + `file_groups` array on ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat. - `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via `read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on read; legacy single-list `files` only updates the active group, leaving other boards' files intact. - `_persist_files_from_body` honors file_groups → files → code priority. Frontend - `useSimulatorStore.addBoard` accepts an optional `explicitId` so saved board IDs can be restored verbatim (wires reference IDs literally). - New `loadProjectState({boards, fileGroups, components, wires, activeBoardId})` action: tears down current boards, recreates from the payload, restores file groups atomically, recalculates wire positions on the next frame, and refreshes the Interconnect. - `useEditorStore.replaceFileGroups` for atomic multi-group restore. - `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through `buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects by synthesising a default board from `board_type`). Auto-save (#useAutoSaveProject hook) - 2.5s debounced silent PUT triggered ONLY when an authenticated user has a `currentProject` with a UUID. State hash detects real changes vs. UI-only churn; baseline is reset on project load so the just-loaded state isn't immediately re-saved. - `beforeunload` flush via `fetch keepalive: true` (supports PUT + credentials, survives unload). - Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error). Backfill script (one-off, idempotent) - `backend/scripts/backfill_boards_2026_05.py` populates `boards_json` for legacy projects. Heuristic per project, based on which board IDs the wires reference: Case A — wires only ref 'arduino-uno' but board_type ≠ uno: rename id→board_type and rewrite wire endpoints. Case B — single-board normal: keep verbatim. Case C — multi-board: recreate one board per distinct ref, infer kind by stripping trailing -N suffix. Also moves any flat files into the active board's group subdir. Stdlib-only, runs from host or `docker exec`. Docker - `Dockerfile.standalone` now copies `backend/scripts/` into the image so the backfill is callable via `docker exec velxio-app python /app/scripts/backfill_boards_2026_05.py --apply`. Verified locally on the restored production backup (363 projects): 33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans. Re-running the script after apply skips all 363 (idempotent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:43:33 +07:00
refactor(oss-split): introduce extension hooks for auth, DB, metrics, auto-save First phase of the OSS / pro split. Goal: open the seams so the auth/DB/admin stack can move into the private overlay (Phase 2-3) without the routes that stay in OSS (compile, libraries, simulation, iot_gateway) having to know. Backend ------- * New app/core/hooks.py — registry for record_compile, get_current_user_id, and lifespan startup tasks. Each hook is a no-op by default; overlays call register_* in register_pro(app) to plug in a real implementation. * compile.py now imports only from app.core.hooks. Drops the direct deps on app.core.dependencies, app.database.session, app.models.user, and app.services.metrics. Route signatures use `Depends(get_current_user_id)` instead of `Depends(get_current_user)`; the metric helper passes user_id through rather than a User instance. * compile_chip.py drops the unused _current_user Depends entirely. * main.py wraps the auth/DB stack import in try/except. When it succeeds (today's behavior on velxio.dev), an adapter bridges record_compile and get_current_user_id to the existing app.services.metrics + dependencies, and the create_all + ALTER TABLE migration block runs via a registered lifespan_startup hook. When it fails (the post-Phase-2 OSS image), main logs "running stateless" and skips registering anything — the routes still load and behave as no-ops for metrics + always-anonymous for auth. Frontend -------- * useAutoSaveProject becomes a skeleton: one useState + one useEffect that delegates to an installed AutoSaveImpl. installAutoSaveImpl() replaces the impl without changing hook count, so React's rules-of-hooks stay satisfied even after the impl moves out of OSS. * New hooks/autoSaveImpl.ts holds the original logic (debouncing, dirty detection, owner eligibility, fetch keepalive on unload), refactored to emit() instead of useState. It self-registers at module load; main.tsx imports it for the side effect. * AppHeader wraps the entire user-vs-login UI in a data-velxio-slot ="header-auth" boundary. Today the OSS UI still renders inside the slot — the overlay can portal-inject additional items now, and in Phase 3 the slot becomes the sole owner of header auth UX. Behavior is identical on velxio.dev (pro overlay imports everything successfully, every adapter wires up). The change is purely structural: deleting the auth/DB modules tomorrow no longer crashes OSS at import. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:24:51 +07:00
export function useAutoSaveProject(): AutoSaveState {
const [state, setState] = useState<AutoSaveState>(IDLE);
feat: persist multi-board projects + add auto-save The project save/load pipeline only persisted a single `board_type`, so multi-board workspaces silently lost every board except the active one on save, and wires referencing the dropped boards' IDs orphaned to the canvas corner on reload. An audit of the production backup found 74/306 projects (24%) with at least one orphaned wire and 174/301 non-trivial projects whose code was still the default Blink template — strong signal that users save once and never re-save. Backend - Add `boards_json` column on `projects` with idempotent ALTER TABLE in the lifespan migration list. - New `FileGroup` schema + `file_groups` array on ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat. - `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via `read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on read; legacy single-list `files` only updates the active group, leaving other boards' files intact. - `_persist_files_from_body` honors file_groups → files → code priority. Frontend - `useSimulatorStore.addBoard` accepts an optional `explicitId` so saved board IDs can be restored verbatim (wires reference IDs literally). - New `loadProjectState({boards, fileGroups, components, wires, activeBoardId})` action: tears down current boards, recreates from the payload, restores file groups atomically, recalculates wire positions on the next frame, and refreshes the Interconnect. - `useEditorStore.replaceFileGroups` for atomic multi-group restore. - `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through `buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects by synthesising a default board from `board_type`). Auto-save (#useAutoSaveProject hook) - 2.5s debounced silent PUT triggered ONLY when an authenticated user has a `currentProject` with a UUID. State hash detects real changes vs. UI-only churn; baseline is reset on project load so the just-loaded state isn't immediately re-saved. - `beforeunload` flush via `fetch keepalive: true` (supports PUT + credentials, survives unload). - Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error). Backfill script (one-off, idempotent) - `backend/scripts/backfill_boards_2026_05.py` populates `boards_json` for legacy projects. Heuristic per project, based on which board IDs the wires reference: Case A — wires only ref 'arduino-uno' but board_type ≠ uno: rename id→board_type and rewrite wire endpoints. Case B — single-board normal: keep verbatim. Case C — multi-board: recreate one board per distinct ref, infer kind by stripping trailing -N suffix. Also moves any flat files into the active board's group subdir. Stdlib-only, runs from host or `docker exec`. Docker - `Dockerfile.standalone` now copies `backend/scripts/` into the image so the backfill is callable via `docker exec velxio-app python /app/scripts/backfill_boards_2026_05.py --apply`. Verified locally on the restored production backup (363 projects): 33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans. Re-running the script after apply skips all 363 (idempotent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:43:33 +07:00
refactor(oss-split): introduce extension hooks for auth, DB, metrics, auto-save First phase of the OSS / pro split. Goal: open the seams so the auth/DB/admin stack can move into the private overlay (Phase 2-3) without the routes that stay in OSS (compile, libraries, simulation, iot_gateway) having to know. Backend ------- * New app/core/hooks.py — registry for record_compile, get_current_user_id, and lifespan startup tasks. Each hook is a no-op by default; overlays call register_* in register_pro(app) to plug in a real implementation. * compile.py now imports only from app.core.hooks. Drops the direct deps on app.core.dependencies, app.database.session, app.models.user, and app.services.metrics. Route signatures use `Depends(get_current_user_id)` instead of `Depends(get_current_user)`; the metric helper passes user_id through rather than a User instance. * compile_chip.py drops the unused _current_user Depends entirely. * main.py wraps the auth/DB stack import in try/except. When it succeeds (today's behavior on velxio.dev), an adapter bridges record_compile and get_current_user_id to the existing app.services.metrics + dependencies, and the create_all + ALTER TABLE migration block runs via a registered lifespan_startup hook. When it fails (the post-Phase-2 OSS image), main logs "running stateless" and skips registering anything — the routes still load and behave as no-ops for metrics + always-anonymous for auth. Frontend -------- * useAutoSaveProject becomes a skeleton: one useState + one useEffect that delegates to an installed AutoSaveImpl. installAutoSaveImpl() replaces the impl without changing hook count, so React's rules-of-hooks stay satisfied even after the impl moves out of OSS. * New hooks/autoSaveImpl.ts holds the original logic (debouncing, dirty detection, owner eligibility, fetch keepalive on unload), refactored to emit() instead of useState. It self-registers at module load; main.tsx imports it for the side effect. * AppHeader wraps the entire user-vs-login UI in a data-velxio-slot ="header-auth" boundary. Today the OSS UI still renders inside the slot — the overlay can portal-inject additional items now, and in Phase 3 the slot becomes the sole owner of header auth UX. Behavior is identical on velxio.dev (pro overlay imports everything successfully, every adapter wires up). The change is purely structural: deleting the auth/DB modules tomorrow no longer crashes OSS at import. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:24:51 +07:00
useEffect(() => {
let cleanup: (() => void) | null = null;
const start = () => {
if (installedImpl && !cleanup) cleanup = installedImpl(setState);
};
start();
// Late-install support only — swapping a live impl at runtime is still
// unsupported (the first installed impl keeps running until unmount).
installWaiters.add(start);
return () => {
installWaiters.delete(start);
cleanup?.();
cleanup = null;
};
feat: persist multi-board projects + add auto-save The project save/load pipeline only persisted a single `board_type`, so multi-board workspaces silently lost every board except the active one on save, and wires referencing the dropped boards' IDs orphaned to the canvas corner on reload. An audit of the production backup found 74/306 projects (24%) with at least one orphaned wire and 174/301 non-trivial projects whose code was still the default Blink template — strong signal that users save once and never re-save. Backend - Add `boards_json` column on `projects` with idempotent ALTER TABLE in the lifespan migration list. - New `FileGroup` schema + `file_groups` array on ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat. - `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via `read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on read; legacy single-list `files` only updates the active group, leaving other boards' files intact. - `_persist_files_from_body` honors file_groups → files → code priority. Frontend - `useSimulatorStore.addBoard` accepts an optional `explicitId` so saved board IDs can be restored verbatim (wires reference IDs literally). - New `loadProjectState({boards, fileGroups, components, wires, activeBoardId})` action: tears down current boards, recreates from the payload, restores file groups atomically, recalculates wire positions on the next frame, and refreshes the Interconnect. - `useEditorStore.replaceFileGroups` for atomic multi-group restore. - `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through `buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects by synthesising a default board from `board_type`). Auto-save (#useAutoSaveProject hook) - 2.5s debounced silent PUT triggered ONLY when an authenticated user has a `currentProject` with a UUID. State hash detects real changes vs. UI-only churn; baseline is reset on project load so the just-loaded state isn't immediately re-saved. - `beforeunload` flush via `fetch keepalive: true` (supports PUT + credentials, survives unload). - Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error). Backfill script (one-off, idempotent) - `backend/scripts/backfill_boards_2026_05.py` populates `boards_json` for legacy projects. Heuristic per project, based on which board IDs the wires reference: Case A — wires only ref 'arduino-uno' but board_type ≠ uno: rename id→board_type and rewrite wire endpoints. Case B — single-board normal: keep verbatim. Case C — multi-board: recreate one board per distinct ref, infer kind by stripping trailing -N suffix. Also moves any flat files into the active board's group subdir. Stdlib-only, runs from host or `docker exec`. Docker - `Dockerfile.standalone` now copies `backend/scripts/` into the image so the backfill is callable via `docker exec velxio-app python /app/scripts/backfill_boards_2026_05.py --apply`. Verified locally on the restored production backup (363 projects): 33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans. Re-running the script after apply skips all 363 (idempotent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:43:33 +07:00
}, []);
refactor(oss-split): introduce extension hooks for auth, DB, metrics, auto-save First phase of the OSS / pro split. Goal: open the seams so the auth/DB/admin stack can move into the private overlay (Phase 2-3) without the routes that stay in OSS (compile, libraries, simulation, iot_gateway) having to know. Backend ------- * New app/core/hooks.py — registry for record_compile, get_current_user_id, and lifespan startup tasks. Each hook is a no-op by default; overlays call register_* in register_pro(app) to plug in a real implementation. * compile.py now imports only from app.core.hooks. Drops the direct deps on app.core.dependencies, app.database.session, app.models.user, and app.services.metrics. Route signatures use `Depends(get_current_user_id)` instead of `Depends(get_current_user)`; the metric helper passes user_id through rather than a User instance. * compile_chip.py drops the unused _current_user Depends entirely. * main.py wraps the auth/DB stack import in try/except. When it succeeds (today's behavior on velxio.dev), an adapter bridges record_compile and get_current_user_id to the existing app.services.metrics + dependencies, and the create_all + ALTER TABLE migration block runs via a registered lifespan_startup hook. When it fails (the post-Phase-2 OSS image), main logs "running stateless" and skips registering anything — the routes still load and behave as no-ops for metrics + always-anonymous for auth. Frontend -------- * useAutoSaveProject becomes a skeleton: one useState + one useEffect that delegates to an installed AutoSaveImpl. installAutoSaveImpl() replaces the impl without changing hook count, so React's rules-of-hooks stay satisfied even after the impl moves out of OSS. * New hooks/autoSaveImpl.ts holds the original logic (debouncing, dirty detection, owner eligibility, fetch keepalive on unload), refactored to emit() instead of useState. It self-registers at module load; main.tsx imports it for the side effect. * AppHeader wraps the entire user-vs-login UI in a data-velxio-slot ="header-auth" boundary. Today the OSS UI still renders inside the slot — the overlay can portal-inject additional items now, and in Phase 3 the slot becomes the sole owner of header auth UX. Behavior is identical on velxio.dev (pro overlay imports everything successfully, every adapter wires up). The change is purely structural: deleting the auth/DB modules tomorrow no longer crashes OSS at import. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:24:51 +07:00
return state;
feat: persist multi-board projects + add auto-save The project save/load pipeline only persisted a single `board_type`, so multi-board workspaces silently lost every board except the active one on save, and wires referencing the dropped boards' IDs orphaned to the canvas corner on reload. An audit of the production backup found 74/306 projects (24%) with at least one orphaned wire and 174/301 non-trivial projects whose code was still the default Blink template — strong signal that users save once and never re-save. Backend - Add `boards_json` column on `projects` with idempotent ALTER TABLE in the lifespan migration list. - New `FileGroup` schema + `file_groups` array on ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat. - `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via `read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on read; legacy single-list `files` only updates the active group, leaving other boards' files intact. - `_persist_files_from_body` honors file_groups → files → code priority. Frontend - `useSimulatorStore.addBoard` accepts an optional `explicitId` so saved board IDs can be restored verbatim (wires reference IDs literally). - New `loadProjectState({boards, fileGroups, components, wires, activeBoardId})` action: tears down current boards, recreates from the payload, restores file groups atomically, recalculates wire positions on the next frame, and refreshes the Interconnect. - `useEditorStore.replaceFileGroups` for atomic multi-group restore. - `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through `buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects by synthesising a default board from `board_type`). Auto-save (#useAutoSaveProject hook) - 2.5s debounced silent PUT triggered ONLY when an authenticated user has a `currentProject` with a UUID. State hash detects real changes vs. UI-only churn; baseline is reset on project load so the just-loaded state isn't immediately re-saved. - `beforeunload` flush via `fetch keepalive: true` (supports PUT + credentials, survives unload). - Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error). Backfill script (one-off, idempotent) - `backend/scripts/backfill_boards_2026_05.py` populates `boards_json` for legacy projects. Heuristic per project, based on which board IDs the wires reference: Case A — wires only ref 'arduino-uno' but board_type ≠ uno: rename id→board_type and rewrite wire endpoints. Case B — single-board normal: keep verbatim. Case C — multi-board: recreate one board per distinct ref, infer kind by stripping trailing -N suffix. Also moves any flat files into the active board's group subdir. Stdlib-only, runs from host or `docker exec`. Docker - `Dockerfile.standalone` now copies `backend/scripts/` into the image so the backfill is callable via `docker exec velxio-app python /app/scripts/backfill_boards_2026_05.py --apply`. Verified locally on the restored production backup (363 projects): 33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans. Re-running the script after apply skips all 363 (idempotent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:43:33 +07:00
}