BLOBBO/demos/lane-j.html
type-two 624caf08da fix(editor): stop the workshop destroying the user's work
Seven hardening fixes on the lane-J editor, several sharing one root cause.

- The demo ran against the LIVE database. Opening demos/lane-j.html deleted
  every saved model (pruneOrphans treats anything the demo's manifest omits as
  garbage) and an interrupted run left a fake machine.boot override the real
  game would load. The database name is now a variable, the demo uses a
  throwaway one, deletes it in a finally, and fingerprints the real store
  before/after so the run FAILS if it moved.
- A .glb with no mesh was accepted on every non-paintable slot: original hidden,
  nothing in its place, cheerful success message, savable. The mesh check now
  runs for every slot, before anything is written.
- Multi-object slots (nine puddles, every cannon barrel) hid all instances and
  added one replacement. One fit node per instance now, each at its own pose and
  its own footprint scale — the same contract the runtime's per-instance
  attachSlot already implements.
- Storage failures rejected into void-discarded promises, freezing the UI on
  "Reading…". Nothing leaves Workshop as a rejection any more: failures come
  back as results carrying a sentence a person can read, and every DOM handler
  goes through one guard.
- idbAvailable() was a typeof check, so private windows passed it and then blew
  up, replacing the whole built UI with a raw error string. It now probes open()
  once and caches the boolean; failure means a labelled read-only editor.
- An override whose GLB failed to reload was dropped from the manifest, after
  which the next save deleted its bytes for good. Failed entries are kept,
  flagged unloadable, surfaced in the slot list, and never pruned.
- The editor kept its own slot list, which disagreed with the runtime's in both
  directions. It now imports SLOT_IDS/SLOT_LABELS from src/assets/slots and
  keeps only presentation data, keyed by SlotId so describing a slot the runtime
  lacks is a compile error.

Also fixes a cross-lane mismatch found on the way: the editor stored GLBs as a
{name,bytes,savedAt} wrapper while the game's reader feeds the record straight
into new Blob([buf]). Writes are raw ArrayBuffer now (reads stay tolerant of the
old shape), so a saved swap can actually load in the game.

Build passes; 312 assertions across 9 headless test files, 49 of them new.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 12:13:07 +10:00

95 lines
5.5 KiB
HTML

<!doctype html>
<html lang="en">
<head><meta charset="utf-8" /><title>BLOBBO lane j demo — workshop editor</title>
<style>
:root{
--ink:#0a2540; --ink-soft:#4a627d; --paper:#f4f7fb; --card:#ffffff;
--line:#d7e2ee; --hot:#FF9500; --good:#34C759; --bad:#FF3B30; --pick:#0A84FF;
}
*{box-sizing:border-box}
html,body{margin:0;height:100%;background:var(--paper);color:var(--ink);
font:15px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif;overflow:hidden}
button,input{font:inherit;color:inherit}
.workshop{display:grid;grid-template-columns:280px 1fr 330px;height:100%}
.panel{overflow-y:auto;padding:16px}
.panel-slots{background:var(--card);border-right:1px solid var(--line)}
.panel-fit{background:var(--card);border-left:1px solid var(--line)}
.panel-stage{position:relative;padding:0;overflow:hidden;background:#bfe3ff}
.stage-host{position:absolute;inset:0}
.stage-host canvas{display:block;width:100%;height:100%}
.brand{margin:0 0 4px;font-size:19px}
.lede{margin:0 0 18px;color:var(--ink-soft);font-size:13px}
.group{margin:16px 0 6px;font-size:11px;letter-spacing:.09em;text-transform:uppercase;color:var(--ink-soft)}
.slot-list{display:flex;flex-direction:column;gap:4px}
.slot{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%;
padding:9px 11px;border:1px solid var(--line);border-radius:10px;background:#fff;
cursor:pointer;text-align:left}
.slot[data-selected="true"]{border-color:var(--pick);background:#e7f2ff;box-shadow:inset 3px 0 0 var(--pick)}
.slot-name{font-weight:600}
.slot-state{font-size:11px;padding:2px 7px;border-radius:20px;background:#eef3f8;color:var(--ink-soft)}
.slot[data-state="custom"] .slot-state{background:#e6f8ea;color:#1c7a35}
.slot[data-state="derived"] .slot-state{background:#f3ecff;color:#6b3fa0}
.slot[data-state="broken"] .slot-state{background:#fff1ef;color:#a3271d}
.readonly-banner{margin:18px 0 0;padding:11px 12px;border-radius:10px;
border:1px solid #f0d9a8;background:#fff8ea;color:#7a5200;font-size:12.5px}
.big-actions{display:flex;flex-direction:column;gap:8px;margin:22px 0 12px}
.action{padding:12px;border:1px solid var(--line);border-radius:11px;background:#fff;cursor:pointer;font-weight:600}
.action:disabled{opacity:.45;cursor:not-allowed}
.action.primary{background:var(--pick);border-color:var(--pick);color:#fff}
.action.danger{color:var(--bad);border-color:#f6d2cf}
.status{margin:0;padding:10px 12px;border-radius:10px;background:#eef3f8;font-size:13px;color:var(--ink-soft)}
.status[data-tone="warn"]{background:#fff3e2;color:#8a5100}
.stage-hint{position:absolute;left:14px;bottom:14px;padding:7px 11px;border-radius:9px;
background:rgba(10,37,64,.72);color:#fff;font-size:12px;pointer-events:none}
.focus-btn{position:absolute;right:14px;bottom:14px;padding:10px 14px;border:0;border-radius:10px;
background:rgba(10,37,64,.86);color:#fff;font-weight:600;cursor:pointer}
.fit-title{margin:0 0 4px;font-size:18px}
.fit-hint{margin:0 0 14px;color:var(--ink-soft);font-size:13px}
.dropzone{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;
min-height:132px;border:2.5px dashed var(--line);border-radius:14px;background:#fbfdff;
cursor:pointer;text-align:center;padding:14px}
.dropzone[data-hot="true"]{border-color:var(--hot);background:#fff6ea}
.dropzone-label{font-size:17px;font-weight:700}
.dropzone-sub{font-size:12px;color:var(--ink-soft)}
.paint-card{display:none;margin-top:14px;padding:12px;border-radius:12px;border:1px solid var(--line);background:#fbfdff}
.paint-card[data-tone="ok"]{border-color:#bde5c8;background:#f2fcf5}
.paint-card[data-tone="bad"]{border-color:#f3b9b4;background:#fff3f2}
.paint-head{font-weight:700;margin-bottom:6px}
.paint-facts{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:6px}
.paint-facts span{font-size:11px;padding:3px 8px;border-radius:20px;background:#fff;border:1px solid var(--line)}
.paint-note{font-size:12.5px;color:var(--ink-soft);margin-top:4px}
.fit-controls{display:none;margin-top:16px}
.fit-row{display:grid;gap:4px;margin-bottom:11px}
.fit-label{font-size:12px;font-weight:600;color:var(--ink-soft)}
.fit-row input[type=range]{width:100%}
.fit-row input[type=number]{width:96px;padding:5px 7px;border:1px solid var(--line);border-radius:7px;background:#fff}
.help-card{margin-top:20px;border:1px solid var(--line);border-radius:12px;background:#fbfdff;padding:10px 12px}
.help-card summary{cursor:pointer;font-weight:600}
.help-body{font-size:13px;color:var(--ink-soft)}
.help-body h3{margin:14px 0 4px;font-size:13px;color:var(--ink)}
.help-body code{background:#eef3f8;padding:1px 5px;border-radius:5px;font-size:12px}
/* demo report overlay */
#results{position:fixed;top:10px;left:50%;transform:translateX(-50%);z-index:50;
max-height:46vh;overflow:auto;min-width:520px;max-width:74vw;
background:rgba(12,20,34,.92);color:#eaf2ff;padding:12px 15px;border-radius:11px;
font:12.5px/1.55 ui-monospace,Menlo,monospace;box-shadow:0 10px 30px rgba(0,0,0,.28)}
#results .pass{color:#8ef0a8}
#results .fail{color:#ff9b93}
#results .note{color:#ffd98a}
#results .summary{margin-top:8px;padding-top:8px;border-top:1px solid rgba(255,255,255,.18);
font-weight:700;font-size:14px}
#results h1{margin:0 0 8px;font-size:13px;color:#ffd60a;letter-spacing:.05em}
</style></head>
<body>
<div id="workshop" data-manual></div>
<div id="results"><h1>LANE J — SCRIPTED DROP ONTO THE SPRING BOOT SLOT</h1></div>
<script type="module" src="/src/demo/lane-j.ts"></script>
</body>
</html>