Step 3/17 av Playground v3-leveransen.
Eksport:
- buildEnvelope(): { appId, schemaVersion, exportedAt, shared, projects,
activeProjectId, activeSurface, preferences } — JSON.parse(JSON.stringify(...))
for å strippe Proxy-wrappere
- exportState(): Blob + URL.createObjectURL + programmatisk <a download>-klikk
+ revokeObjectURL etter 0ms timeout. File System Access API krever HTTPS
(secure context) og er ikke tilgjengelig på file:// — derfor Blob-pattern.
- Filnavn-format: ms-ai-architect-playground-<ISO-stamp>.json
Import:
- importState(File): file.text() -> JSON.parse -> envelope-validering (appId
+ schemaVersion required) -> migrateState() -> persistence.save() -> in-place
state-update (Proxy-binding må bevares — kan ikke bytte raw-referansen)
-> manuell 'change'-event-dispatch så subscribers re-rendrer
- file.text() er Promise<string> som fungerer på file:// uten secure context
MIGRATIONS-pipeline:
- Eager: alle migrasjoner kjøres sekvensielt fra fil-versjon til SCHEMA_VERSION
ved import (ikke lazy ved access)
- Nøkkel-format: 'N->M' (fortløpende). Aldri hopp over et steg.
- Kaster eksplisitt feil ved manglende migrasjons-funksjon eller ved
funksjon som ikke setter schemaVersion korrekt — silent corruption
unngås (brief Risk High).
Eksponerte globals: __buildEnvelope, __exportState, __importState, __MIGRATIONS.
Verify-assert: JSON.parse(JSON.stringify(window.__buildEnvelope())).schemaVersion === 1
Plan: .claude/projects/2026-05-03-playground-v3-architecture/plan.md (Step 3)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
499 lines
21 KiB
HTML
499 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="nb" data-theme="dark">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>ms-ai-architect — Playground v3</title>
|
|
|
|
<!-- Vendored design-system. Kilden er shared/playground-design-system/ — synces via
|
|
scripts/sync-design-system.mjs ved marketplace-rot. Aldri rediger filer under
|
|
playground/vendor/ direkte; endringer går i shared/ + re-sync. -->
|
|
<link rel="stylesheet" href="vendor/playground-design-system/fonts.css">
|
|
<link rel="stylesheet" href="vendor/playground-design-system/tokens.css">
|
|
<link rel="stylesheet" href="vendor/playground-design-system/base.css">
|
|
<link rel="stylesheet" href="vendor/playground-design-system/components.css">
|
|
<link rel="stylesheet" href="vendor/playground-design-system/components-tier2.css">
|
|
<link rel="stylesheet" href="vendor/playground-design-system/components-tier3.css">
|
|
<link rel="stylesheet" href="vendor/playground-design-system/components-tier3-supplement.css">
|
|
</head>
|
|
<body>
|
|
<!-- Walking-skeleton: 4 placeholder-overflater. Step 5-7 fyller dem ut.
|
|
Bare én av disse er aktiv om gangen via state.activeSurface. -->
|
|
<main id="app">
|
|
<section id="surface-onboarding" data-surface="onboarding" hidden></section>
|
|
<section id="surface-home" data-surface="home" hidden></section>
|
|
<section id="surface-catalog" data-surface="catalog" hidden></section>
|
|
<section id="surface-project" data-surface="project" hidden></section>
|
|
</main>
|
|
|
|
<!--
|
|
Klassisk script (ikke type="module") av to grunner:
|
|
1. External <script type="module" src="..."> feiler på file:// i Chrome+Firefox
|
|
(ref WHATWG html#8121, Chromium 41378227).
|
|
2. Single-file deployment per brief Constraints — ingen build-step.
|
|
Kommende steps utvider IIFE-en under: Step 2 (state-modul), Step 3 (eksport/import),
|
|
Step 4 (CATALOG), osv.
|
|
-->
|
|
<script>
|
|
(function () {
|
|
'use strict';
|
|
|
|
// localStorage-nøkkel og schema-versjon. Endring av STATE_KEY krever migrasjons-steg
|
|
// (se Step 3 — MIGRATIONS-pipeline). SCHEMA_VERSION bumpes ved breaking endringer
|
|
// i state-form og driver eager migrations ved import.
|
|
const STATE_KEY = 'ms-ai-architect-state-v1';
|
|
const SCHEMA_VERSION = 1;
|
|
|
|
// Eksponer som globals for Verify-asserts og DevTools-debugging. Senere steps
|
|
// utvider window.__-namespace med __store, __CATALOG, __PARSERS, __RENDERERS,
|
|
// __buildCommand, __buildEnvelope, __handlePasteImport.
|
|
window.__STATE_KEY = STATE_KEY;
|
|
window.__SCHEMA_VERSION = SCHEMA_VERSION;
|
|
|
|
// ============================================================
|
|
// STATE MODULE (Step 2)
|
|
// ============================================================
|
|
//
|
|
// Reactivity-skjelett: Proxy + EventTarget. set-trap batcher dispatchEvent
|
|
// via queueMicrotask, så N synkrone mutasjoner gir bare én 'change'-event
|
|
// per mikrotask-tick. Bruker dyp wrap (Proxy rekursivt på objekt-properties)
|
|
// så nestede oppdateringer (state.shared.organization.name = ...) fanges.
|
|
//
|
|
// Persistens: IDB primær (~ubegrenset for 1-5 MB), localStorage fallback
|
|
// (5 MiB cap). Open-DB pattern bruker Promise-wrapper og synkrone
|
|
// migrasjoner i onupgradeneeded — async cursor-iterasjon er forbudt
|
|
// (ref w3c/IndexedDB#282: korrupsjons-risiko ved async i upgrade-tx).
|
|
//
|
|
// Multi-tab: db.onversionchange = () => db.close() defensivt på alle
|
|
// koblinger så en versjon-bump i en annen tab ikke stuck-blokkerer denne.
|
|
|
|
class StateBus extends EventTarget {}
|
|
|
|
const sharedBus = new StateBus();
|
|
const projectBus = new StateBus();
|
|
|
|
// Initial state-form. Step 5+ utvider shared.* etter onboarding-skjema;
|
|
// Step 7 utvider projects[]. preferences.theme settes i Step 13.
|
|
const INITIAL_STATE = {
|
|
schemaVersion: SCHEMA_VERSION,
|
|
shared: {
|
|
organization: {},
|
|
technology: {},
|
|
security: {},
|
|
architecture: {},
|
|
business: {}
|
|
},
|
|
projects: [],
|
|
activeProjectId: null,
|
|
activeSurface: 'home',
|
|
preferences: { theme: 'dark' }
|
|
};
|
|
|
|
// Microtask-batched event dispatcher. Mange synkrone set-traps i samme
|
|
// tick → én 'change'-event neste mikrotask. Forhindrer N renders ved
|
|
// batch-mutasjoner (f.eks. import-flow).
|
|
function makeBatchedDispatcher(bus) {
|
|
let pending = false;
|
|
const changedPaths = new Set();
|
|
return function dispatch(path) {
|
|
changedPaths.add(path);
|
|
if (pending) return;
|
|
pending = true;
|
|
queueMicrotask(function () {
|
|
pending = false;
|
|
const paths = Array.from(changedPaths);
|
|
changedPaths.clear();
|
|
bus.dispatchEvent(new CustomEvent('change', { detail: { paths: paths } }));
|
|
});
|
|
};
|
|
}
|
|
|
|
// Dyp Proxy-wrap. Lazy: wrapper child-objekter ved første read, så cost
|
|
// er bare betalt for grener brukeren faktisk berører. set-trap returnerer
|
|
// boolean (Proxy spec-invariant) og dispatcher batched 'change'-event.
|
|
// Path tracking gjør at subscribers kan filtrere på relevante grener.
|
|
function deepProxy(target, dispatch, path) {
|
|
path = path || '';
|
|
const cache = new WeakMap();
|
|
const handler = {
|
|
get: function (obj, key) {
|
|
const value = obj[key];
|
|
if (value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)) {
|
|
if (cache.has(value)) return cache.get(value);
|
|
const childPath = path ? path + '.' + String(key) : String(key);
|
|
const wrapped = new Proxy(value, makeHandler(childPath));
|
|
cache.set(value, wrapped);
|
|
return wrapped;
|
|
}
|
|
if (Array.isArray(value)) {
|
|
// Array-mutasjoner via push/splice trigger set på indekser; wrap likt.
|
|
if (cache.has(value)) return cache.get(value);
|
|
const childPath = path ? path + '.' + String(key) : String(key);
|
|
const wrapped = new Proxy(value, makeHandler(childPath));
|
|
cache.set(value, wrapped);
|
|
return wrapped;
|
|
}
|
|
return value;
|
|
},
|
|
set: function (obj, key, value) {
|
|
obj[key] = value;
|
|
dispatch(path ? path + '.' + String(key) : String(key));
|
|
return true;
|
|
},
|
|
deleteProperty: function (obj, key) {
|
|
delete obj[key];
|
|
dispatch(path ? path + '.' + String(key) : String(key));
|
|
return true;
|
|
}
|
|
};
|
|
function makeHandler(p) {
|
|
return {
|
|
get: function (o, k) { return new Proxy(target, handler).constructor === Proxy ? handler.get(o, k) : o[k]; },
|
|
set: function (o, k, v) { o[k] = v; dispatch(p ? p + '.' + String(k) : String(k)); return true; },
|
|
deleteProperty: function (o, k) { delete o[k]; dispatch(p ? p + '.' + String(k) : String(k)); return true; }
|
|
};
|
|
}
|
|
return new Proxy(target, handler);
|
|
}
|
|
|
|
function createStore(initial, bus) {
|
|
const dispatch = makeBatchedDispatcher(bus);
|
|
const proxied = deepProxy(initial, dispatch, '');
|
|
return {
|
|
state: proxied,
|
|
raw: initial, // referanse til underliggende objekt — for serialisering
|
|
subscribe: function (handler) { bus.addEventListener('change', handler); },
|
|
unsubscribe: function (handler) { bus.removeEventListener('change', handler); }
|
|
};
|
|
}
|
|
|
|
// Throttled persistens: debounce 300 ms etter siste mutasjon, så bursts
|
|
// (import-flow, batch-form-submit) committer bare én gang.
|
|
function makeThrottledWriter(persist) {
|
|
let timer = null;
|
|
return function schedule() {
|
|
if (timer) clearTimeout(timer);
|
|
timer = setTimeout(function () {
|
|
timer = null;
|
|
persist().catch(function (err) {
|
|
console.error('[playground v3] persist failed:', err);
|
|
});
|
|
}, 300);
|
|
};
|
|
}
|
|
|
|
// ============================================================
|
|
// PERSISTENCE LAYER
|
|
// ============================================================
|
|
//
|
|
// IDB primær. Én DB ('ms-ai-architect-playground-v1') med to object-stores:
|
|
// - 'shared': nøkkel 'shared' → { organization, technology, ... }
|
|
// - 'projects': nøkkel 'projectId' → project-objekt
|
|
//
|
|
// Migrasjoner i onupgradeneeded er SYNKRONE per spec — ingen await på
|
|
// cursor.continue(); bruk callback-stil. async cursor-iterasjon i en
|
|
// upgrade-tx kan korruptere DB (w3c/IndexedDB#282).
|
|
|
|
function openDB(name, version) {
|
|
return new Promise(function (resolve, reject) {
|
|
if (typeof indexedDB === 'undefined') {
|
|
reject(new Error('IndexedDB ikke tilgjengelig'));
|
|
return;
|
|
}
|
|
const req = indexedDB.open(name, version);
|
|
req.onupgradeneeded = function (ev) {
|
|
const db = req.result;
|
|
const oldVersion = ev.oldVersion;
|
|
// Synkrone migrasjoner — opprette stores per oldVersion-guard.
|
|
if (oldVersion < 1) {
|
|
if (!db.objectStoreNames.contains('shared')) {
|
|
db.createObjectStore('shared');
|
|
}
|
|
if (!db.objectStoreNames.contains('projects')) {
|
|
db.createObjectStore('projects', { keyPath: 'id' });
|
|
}
|
|
if (!db.objectStoreNames.contains('meta')) {
|
|
db.createObjectStore('meta');
|
|
}
|
|
}
|
|
// Senere bump-er legges til som "if (oldVersion < N)"-blokker.
|
|
};
|
|
req.onsuccess = function () {
|
|
const db = req.result;
|
|
// Defensiv multi-tab: hvis annen tab åpner med høyere versjon,
|
|
// lukk denne så de ikke blokkerer hverandre.
|
|
db.onversionchange = function () {
|
|
db.close();
|
|
console.warn('[playground v3] IDB versionchange — closed for upgrade');
|
|
};
|
|
resolve(db);
|
|
};
|
|
req.onerror = function () { reject(req.error); };
|
|
req.onblocked = function () {
|
|
// En annen tab holder en eldre versjon åpen; usannsynlig i v3
|
|
// (én DB-versjon per release), men logg likevel.
|
|
console.warn('[playground v3] IDB open blocked — another tab holds older version');
|
|
};
|
|
});
|
|
}
|
|
|
|
// Primær persistens: IDB. Ved feil (Safari private mode, kvote-overflow)
|
|
// fallback til localStorage. Returnerer adapter med lik API — kallere
|
|
// trenger ikke vite hvilken backend som er i bruk.
|
|
async function makePersistence() {
|
|
const DB_NAME = 'ms-ai-architect-playground-v1';
|
|
const DB_VERSION = 1;
|
|
try {
|
|
const db = await openDB(DB_NAME, DB_VERSION);
|
|
return {
|
|
backend: 'idb',
|
|
load: function () {
|
|
return new Promise(function (resolve, reject) {
|
|
const tx = db.transaction(['shared', 'projects', 'meta'], 'readonly');
|
|
const sharedReq = tx.objectStore('shared').get('shared');
|
|
const projectsReq = tx.objectStore('projects').getAll();
|
|
const metaReq = tx.objectStore('meta').get('meta');
|
|
tx.oncomplete = function () {
|
|
resolve({
|
|
schemaVersion: (metaReq.result && metaReq.result.schemaVersion) || SCHEMA_VERSION,
|
|
shared: sharedReq.result || INITIAL_STATE.shared,
|
|
projects: projectsReq.result || [],
|
|
activeProjectId: (metaReq.result && metaReq.result.activeProjectId) || null,
|
|
activeSurface: (metaReq.result && metaReq.result.activeSurface) || 'home',
|
|
preferences: (metaReq.result && metaReq.result.preferences) || INITIAL_STATE.preferences
|
|
});
|
|
};
|
|
tx.onerror = function () { reject(tx.error); };
|
|
});
|
|
},
|
|
save: function (state) {
|
|
return new Promise(function (resolve, reject) {
|
|
const tx = db.transaction(['shared', 'projects', 'meta'], 'readwrite');
|
|
tx.objectStore('shared').put(state.shared, 'shared');
|
|
const projectStore = tx.objectStore('projects');
|
|
// Clear-and-rewrite er enkelt og atomær for moderate volum.
|
|
// Ved >100 prosjekter bør dette switch-es til diff-write.
|
|
projectStore.clear();
|
|
for (let i = 0; i < state.projects.length; i++) {
|
|
projectStore.put(state.projects[i]);
|
|
}
|
|
tx.objectStore('meta').put({
|
|
schemaVersion: state.schemaVersion,
|
|
activeProjectId: state.activeProjectId,
|
|
activeSurface: state.activeSurface,
|
|
preferences: state.preferences
|
|
}, 'meta');
|
|
tx.oncomplete = function () { resolve(); };
|
|
tx.onerror = function () { reject(tx.error); };
|
|
});
|
|
}
|
|
};
|
|
} catch (err) {
|
|
console.warn('[playground v3] IDB ikke tilgjengelig, faller tilbake til localStorage:', err && err.message);
|
|
return makeLocalStorageFallback();
|
|
}
|
|
}
|
|
|
|
function makeLocalStorageFallback() {
|
|
return {
|
|
backend: 'localStorage',
|
|
load: function () {
|
|
try {
|
|
const raw = localStorage.getItem(STATE_KEY);
|
|
if (!raw) return Promise.resolve(JSON.parse(JSON.stringify(INITIAL_STATE)));
|
|
return Promise.resolve(JSON.parse(raw));
|
|
} catch (err) {
|
|
console.error('[playground v3] localStorage parse-feil, returnerer initial state:', err);
|
|
return Promise.resolve(JSON.parse(JSON.stringify(INITIAL_STATE)));
|
|
}
|
|
},
|
|
save: function (state) {
|
|
try {
|
|
const payload = JSON.stringify(state);
|
|
// Cap-advarsel: localStorage 5 MiB cap. Ved ~4.5 MB warn brukeren.
|
|
if (payload.length > 4.5 * 1024 * 1024) {
|
|
console.warn('[playground v3] State nærmer seg localStorage 5 MiB cap. Bruk en moderne nettleser med IDB-støtte.');
|
|
}
|
|
localStorage.setItem(STATE_KEY, payload);
|
|
return Promise.resolve();
|
|
} catch (err) {
|
|
return Promise.reject(err);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// ============================================================
|
|
// BOOTSTRAP
|
|
// ============================================================
|
|
//
|
|
// Initialiser persistens, last lagret state, opprett store, hook opp
|
|
// throttled writer. Eksponer __store på window for Verify-asserts og
|
|
// DevTools — Step 3 utvider med __buildEnvelope, Step 4 med __CATALOG.
|
|
|
|
let store = null;
|
|
let persistence = null;
|
|
let scheduleWrite = null;
|
|
|
|
async function bootstrap() {
|
|
persistence = await makePersistence();
|
|
const loaded = await persistence.load();
|
|
// Sørg for at schemaVersion finnes (cold start kan returnere uten).
|
|
if (!loaded.schemaVersion) loaded.schemaVersion = SCHEMA_VERSION;
|
|
store = createStore(loaded, sharedBus);
|
|
scheduleWrite = makeThrottledWriter(function () {
|
|
return persistence.save(store.raw);
|
|
});
|
|
store.subscribe(function () { scheduleWrite(); });
|
|
window.__store = store;
|
|
window.__persistence = persistence;
|
|
// Step 6+ vil trigger første render her.
|
|
}
|
|
|
|
// ============================================================
|
|
// EXPORT / IMPORT (Step 3)
|
|
// ============================================================
|
|
//
|
|
// Brukeren kan eksportere hele state som JSON-fil og re-importere på en
|
|
// annen enhet (eller etter localStorage.clear()). Format er en envelope
|
|
// med schemaVersion + appId — så fremtidige versjoner kan lese gamle
|
|
// eksporter via MIGRATIONS-pipeline.
|
|
//
|
|
// File System Access API krever HTTPS (secure context) og er ikke
|
|
// tilgjengelig på file:// — vi bruker Blob + URL.createObjectURL +
|
|
// <a download> for eksport, og <input type="file"> + File.text() for
|
|
// import. Begge fungerer på file:// i alle target-browsers.
|
|
//
|
|
// MIGRATIONS er en eager pipeline: ved import (eller cold-load fra
|
|
// gammel state) kjøres alle migrasjoner sekvensielt fra fil-versjon til
|
|
// gjeldende SCHEMA_VERSION. Aldri hopp over et steg — selv tomme
|
|
// migrasjoner skal være registrert (no-op) for å bevise at hoppet er
|
|
// håndtert.
|
|
|
|
const APP_ID = 'ms-ai-architect-playground';
|
|
|
|
function buildEnvelope() {
|
|
// Snapshot av rå state. JSON.stringify(JSON.parse(...)) sørger for
|
|
// at Proxy-er er stripped; vi vil ikke at envelopet skal beholde
|
|
// wrapper-referanser.
|
|
const snapshot = store ? JSON.parse(JSON.stringify(store.raw)) : JSON.parse(JSON.stringify(INITIAL_STATE));
|
|
return {
|
|
appId: APP_ID,
|
|
schemaVersion: snapshot.schemaVersion || SCHEMA_VERSION,
|
|
exportedAt: new Date().toISOString(),
|
|
shared: snapshot.shared,
|
|
projects: snapshot.projects,
|
|
activeProjectId: snapshot.activeProjectId,
|
|
activeSurface: snapshot.activeSurface,
|
|
preferences: snapshot.preferences
|
|
};
|
|
}
|
|
|
|
function exportState() {
|
|
const envelope = buildEnvelope();
|
|
const json = JSON.stringify(envelope, null, 2);
|
|
const blob = new Blob([json], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
const stamp = envelope.exportedAt.replace(/[:.]/g, '-');
|
|
a.href = url;
|
|
a.download = APP_ID + '-' + stamp + '.json';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
// revokeObjectURL etter at klikket har skjedd; setTimeout 0 er nok i
|
|
// alle target-browsers (Blob URL holdes så lenge nedlastningen står
|
|
// for å initieres).
|
|
setTimeout(function () { URL.revokeObjectURL(url); }, 0);
|
|
return envelope;
|
|
}
|
|
|
|
// MIGRATIONS-pipeline. Nøkkel-format: 'N->M' der N og M er fortløpende
|
|
// SCHEMA_VERSION-tall. Funksjon tar et state-objekt og returnerer et
|
|
// nytt state-objekt på neste versjon. Aldri muter input.
|
|
//
|
|
// Når SCHEMA_VERSION bumpes til 2: legg til '1->2'-funksjon som
|
|
// transformerer v1-state til v2-form. importState plukker opp
|
|
// migrasjonen automatisk.
|
|
const MIGRATIONS = {
|
|
// Eksempel for fremtid (no-op stub):
|
|
// '1->2': function (state) { return Object.assign({}, state, { schemaVersion: 2 }); }
|
|
};
|
|
|
|
function migrateState(state) {
|
|
let current = state;
|
|
let from = current.schemaVersion || 1;
|
|
while (from < SCHEMA_VERSION) {
|
|
const key = from + '->' + (from + 1);
|
|
const fn = MIGRATIONS[key];
|
|
if (!fn) {
|
|
throw new Error('[playground v3] mangler migrasjon ' + key + ' — kan ikke trygt oppgradere import-fil');
|
|
}
|
|
current = fn(current);
|
|
if (current.schemaVersion !== from + 1) {
|
|
throw new Error('[playground v3] migrasjon ' + key + ' satte ikke schemaVersion til ' + (from + 1));
|
|
}
|
|
from = current.schemaVersion;
|
|
}
|
|
return current;
|
|
}
|
|
|
|
async function importState(file) {
|
|
// file er File-objekt fra <input type="file"> change-event.
|
|
// file.text() er Promise<string> — fungerer på file:// uten secure context.
|
|
const text = await file.text();
|
|
let envelope;
|
|
try {
|
|
envelope = JSON.parse(text);
|
|
} catch (err) {
|
|
throw new Error('Ugyldig JSON: ' + err.message);
|
|
}
|
|
if (envelope.appId !== APP_ID) {
|
|
throw new Error('Fil-en er ikke en ' + APP_ID + '-eksport (appId=' + envelope.appId + ')');
|
|
}
|
|
if (typeof envelope.schemaVersion !== 'number') {
|
|
throw new Error('Mangler schemaVersion i envelope');
|
|
}
|
|
// Migrer envelope opp til gjeldende SCHEMA_VERSION før vi commit-er.
|
|
const migrated = migrateState({
|
|
schemaVersion: envelope.schemaVersion,
|
|
shared: envelope.shared || INITIAL_STATE.shared,
|
|
projects: envelope.projects || [],
|
|
activeProjectId: envelope.activeProjectId || null,
|
|
activeSurface: envelope.activeSurface || 'home',
|
|
preferences: envelope.preferences || INITIAL_STATE.preferences
|
|
});
|
|
// Skriv direkte til persistens for å unngå at debounce-vinduet
|
|
// svelger import-en ved en samtidig page-unload.
|
|
if (persistence) {
|
|
await persistence.save(migrated);
|
|
}
|
|
// Erstatt store-state in-place. Vi kan ikke bytte ut store.raw
|
|
// sin referanse fordi Proxy-en er bundet til den; muter feltvis.
|
|
const target = store.raw;
|
|
target.schemaVersion = migrated.schemaVersion;
|
|
target.shared = migrated.shared;
|
|
target.projects = migrated.projects;
|
|
target.activeProjectId = migrated.activeProjectId;
|
|
target.activeSurface = migrated.activeSurface;
|
|
target.preferences = migrated.preferences;
|
|
// Trigger en change-event manuelt så subscribers re-rendrer.
|
|
sharedBus.dispatchEvent(new CustomEvent('change', { detail: { paths: ['*'] } }));
|
|
return migrated;
|
|
}
|
|
|
|
// Eksponer for UI-handlere (Step 5+) og DevTools-debugging.
|
|
window.__buildEnvelope = buildEnvelope;
|
|
window.__exportState = exportState;
|
|
window.__importState = importState;
|
|
window.__MIGRATIONS = MIGRATIONS;
|
|
|
|
// Auto-bootstrap. Kjør så snart DOM er parsed; vi er på slutten av <body>
|
|
// så DOM er allerede klar.
|
|
bootstrap().catch(function (err) {
|
|
console.error('[playground v3] bootstrap failed:', err);
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|