@@ -2046,7 +2046,9 @@ function updateModels(preserve = true) {
20462046 const provider = $("provider").value;
20472047 const previous = $("model").value;
20482048 const models = providerModels[provider] || [];
2049- $("model").innerHTML = models.map((model) => `<option value="${model}">${model}</option>`).join("");
2049+ $("model").replaceChildren(
2050+ ...models.map((model) => new Option(String(model), String(model))),
2051+ );
20502052 if (preserve && models.includes(previous)) $("model").value = previous;
20512053 $("activeProvider").textContent = provider;
20522054 $("activeModel").textContent = $("model").value || "-";
@@ -3576,7 +3578,10 @@ function normalizeTimeState(raw = {}) {
35763578 const bpm = Math.min(300, Math.max(20, Number(raw?.bpm) || defaults.bpm));
35773579 const bars = Math.min(128, Math.max(1, Math.round(Number(raw?.bars) || defaults.bars)));
35783580 const ppq = Math.min(3840, Math.max(24, Math.round(Number(raw?.ppq) || defaults.ppq)));
3579- const sampleRate = Math.round(Number(raw?.sampleRate ?? raw?.sample_rate) || defaults.sampleRate);
3581+ // Backend time renders are fixed to SAME's 44.1 kHz clock. Session data
3582+ // created by older builds may contain another rate, so normalize it here
3583+ // before the clock reaches the API.
3584+ const sampleRate = 44100;
35803585 const snapDivision = ["1/4", "1/8", "1/16", "1/32", "triplet"].includes(raw?.snapDivision ?? raw?.snap_division)
35813586 ? (raw.snapDivision ?? raw.snap_division)
35823587 : defaults.snapDivision;
@@ -3599,34 +3604,41 @@ function timeClockDerived(clock = timeState) {
35993604 const normalized = normalizeTimeState(clock);
36003605 const secondsPerBeat = 60 / normalized.bpm;
36013606 const totalBeats = normalized.bars * normalized.timeSignature.beatsPerBar;
3602- const loopSeconds = totalBeats * secondsPerBeat;
36033607 const ticksPerBar = normalized.timeSignature.beatsPerBar * normalized.ppq;
36043608 const totalTicks = normalized.bars * ticksPerBar;
3609+ const loopStartTick = Math.min(totalTicks - 1, normalized.loopStartTick);
3610+ const requestedLoopEnd = Number(normalized.loopEndTick);
3611+ const loopEndTick = Number.isFinite(requestedLoopEnd) && requestedLoopEnd > loopStartTick
3612+ ? Math.min(totalTicks, Math.round(requestedLoopEnd))
3613+ : totalTicks;
3614+ const loopSeconds = ((loopEndTick - loopStartTick) / normalized.ppq) * secondsPerBeat;
36053615 return {
36063616 secondsPerBeat,
36073617 totalBeats,
36083618 loopSeconds,
36093619 loopSamples: Math.round(loopSeconds * normalized.sampleRate),
36103620 ticksPerBar,
36113621 totalTicks,
3612- loopStartTick: normalized.loopStartTick ,
3613- loopEndTick: normalized.loopEndTick || totalTicks ,
3622+ loopStartTick,
3623+ loopEndTick,
36143624 };
36153625}
36163626
36173627function timeStateApiClock() {
3628+ const normalized = normalizeTimeState(timeState);
3629+ const derived = timeClockDerived(normalized);
36183630 return {
3619- enabled: Boolean(timeState .enabled),
3620- bpm: Number(timeState .bpm) ,
3621- beats_per_bar: Number(timeState .timeSignature.beatsPerBar) ,
3622- beat_unit: Number(timeState .timeSignature.beatUnit) ,
3623- bars: Number(timeState .bars) ,
3624- ppq: Number(timeState .ppq) ,
3625- sample_rate: Number(timeState .sampleRate) ,
3626- snap_division: timeState .snapDivision,
3627- swing: Number(timeState .swing) || 0 ,
3628- loop_start_tick: Number(timeState .loopStartTick) || 0 ,
3629- loop_end_tick: timeState .loopEndTick || null ,
3631+ enabled: Boolean(normalized .enabled),
3632+ bpm: normalized .bpm,
3633+ beats_per_bar: normalized .timeSignature.beatsPerBar,
3634+ beat_unit: normalized .timeSignature.beatUnit,
3635+ bars: normalized .bars,
3636+ ppq: normalized .ppq,
3637+ sample_rate: normalized .sampleRate,
3638+ snap_division: normalized .snapDivision,
3639+ swing: normalized .swing,
3640+ loop_start_tick: derived .loopStartTick,
3641+ loop_end_tick: derived .loopEndTick,
36303642 };
36313643}
36323644
@@ -7963,19 +7975,20 @@ function renderSnapshotLibrary() {
79637975 const nodeCount = record.nodes?.length || 0;
79647976 const edgeCount = record.edges?.length || 0;
79657977 const date = record.createdAt ? new Date(record.createdAt).toLocaleString() : "—";
7966- return `<div class="snapshot-card" data-snapshot-id="${record.id}">
7967- <button class="snapshot-card-fav${isFav ? " is-fav" : ""}" data-action="snapshot-toggle-fav" data-snapshot-id="${record.id}" title="${isFav ? "Unfavorite" : "Favorite"}" type="button">
7978+ const safeRecordId = escapeHtml(record.id);
7979+ return `<div class="snapshot-card" data-snapshot-id="${safeRecordId}">
7980+ <button class="snapshot-card-fav${isFav ? " is-fav" : ""}" data-action="snapshot-toggle-fav" data-snapshot-id="${safeRecordId}" title="${isFav ? "Unfavorite" : "Favorite"}" type="button">
79687981 <svg viewBox="0 0 24 24" width="14" height="14" fill="${isFav ? "currentColor" : "none"}" stroke="currentColor" stroke-width="2"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01z"/></svg>
79697982 </button>
7970- <div class="snapshot-card-info" data-action="snapshot-load" data-snapshot-id="${record.id }">
7983+ <div class="snapshot-card-info" data-action="snapshot-load" data-snapshot-id="${safeRecordId }">
79717984 <div class="snapshot-card-name">${escapeHtml(record.name || record.id)}</div>
7972- <div class="snapshot-card-meta">${nodeCount} nodes · ${edgeCount} edges · ${date}</div>
7985+ <div class="snapshot-card-meta">${escapeHtml( nodeCount) } nodes · ${escapeHtml( edgeCount) } edges · ${escapeHtml( date) }</div>
79737986 </div>
79747987 <div class="snapshot-card-actions">
7975- <button data-action="snapshot-rename" data-snapshot-id="${record.id }" title="Rename" type="button">
7988+ <button data-action="snapshot-rename" data-snapshot-id="${safeRecordId }" title="Rename" type="button">
79767989 <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
79777990 </button>
7978- <button class="snapshot-delete" data-action="snapshot-delete" data-snapshot-id="${record.id }" title="Delete" type="button">
7991+ <button class="snapshot-delete" data-action="snapshot-delete" data-snapshot-id="${safeRecordId }" title="Delete" type="button">
79797992 <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
79807993 </button>
79817994 </div>
@@ -8076,7 +8089,7 @@ function renderSessionLibrary() {
80768089 return `<div class="snapshot-card" data-session-id="${escapeHtml(session.id)}">
80778090 <div class="snapshot-card-info" data-action="session-load" data-session-id="${escapeHtml(session.id)}">
80788091 <div class="snapshot-card-name">${escapeHtml(session.name || session.id)}</div>
8079- <div class="snapshot-card-meta">${session.node_count} nodes · ${session.edge_count} edges · ${date}</div>
8092+ <div class="snapshot-card-meta">${escapeHtml( session.node_count) } nodes · ${escapeHtml( session.edge_count) } edges · ${escapeHtml( date) }</div>
80808093 </div>
80818094 <div class="snapshot-card-actions">
80828095 <button class="snapshot-delete" data-action="session-delete" data-session-id="${escapeHtml(session.id)}" title="Delete session" aria-label="Delete session" type="button">
@@ -16160,7 +16173,9 @@ async function canvasAcceptCandidate(candidateId, acceptAs) {
1616016173 if (!candidate || !candidateAsset) return;
1616116174 const sourceNode = canvasNodes.find((node) => node.id === candidate.sourceNodeId) || canvasSelectedNode();
1616216175 if (acceptAs === "branch" || !sourceNode || sourceNode.type !== "sound") {
16163- const sourceEl = sourceNode ? document.querySelector(`.canvas-node[data-node-id="${sourceNode.id}"]`) : null;
16176+ const sourceEl = sourceNode
16177+ ? document.querySelector(`.canvas-node[data-node-id="${CSS.escape(sourceNode.id)}"]`)
16178+ : null;
1616416179 const sourceH = sourceEl ? sourceEl.offsetHeight : (sourceNode?.height || 262);
1616516180 let newX = (sourceNode?.x || 210) + 48;
1616616181 let newY = (sourceNode?.y || 110) + 260;
0 commit comments