From f7ba32ef7b84f2b18350f7c3229c4dfbdaa4b8d1 Mon Sep 17 00:00:00 2001 From: Lennart van der Molen Date: Fri, 14 Nov 2025 20:09:22 +0100 Subject: [PATCH] Fix radial visuals payload handling --- web/app.js | 83 ++++++++++++++++++++++++++-------- web/ui/visuals/RadialUrchin.js | 19 ++++++++ 2 files changed, 82 insertions(+), 20 deletions(-) diff --git a/web/app.js b/web/app.js index 2431843..515d1bd 100644 --- a/web/app.js +++ b/web/app.js @@ -99,6 +99,7 @@ const visualsState = { runLabel: null, }; let lastVisualPayload = null; +let lastVisualSchedule = null; let isGeneratingCalendar = false; const GENERATE_BUTTON_DEFAULT_LABEL = 'Generate schedule'; const GENERATE_BUTTON_LOADING_LABEL = 'Generating…'; @@ -242,8 +243,36 @@ function resolveLegacyPng(payload) { return candidates.find((value) => typeof value === 'string' && value.trim().length > 0) || ''; } +function resolveVisualPayload(payload) { + if (!payload || typeof payload !== 'object') { + return null; + } + if (Array.isArray(payload.events)) { + return payload; + } + if (payload.calendar && typeof payload.calendar === 'object') { + return resolveVisualPayload(payload.calendar); + } + if (payload.calendarJson && typeof payload.calendarJson === 'object') { + return resolveVisualPayload(payload.calendarJson); + } + if (payload.rawResult && typeof payload.rawResult === 'object') { + return resolveVisualPayload(payload.rawResult); + } + if (payload.data && typeof payload.data === 'object') { + return resolveVisualPayload(payload.data); + } + return null; +} + function updateVisuals(payload) { lastVisualPayload = payload && typeof payload === 'object' ? payload : null; + lastVisualSchedule = resolveVisualPayload(payload); + + if (!lastVisualSchedule && payload && typeof payload === 'object' && !visualsState.useLegacy) { + console.warn('[visuals] received payload without events, skipping radial render'); + } + if (visualsState.useLegacy) { resetVisualsInstance(); const src = resolveLegacyPng(lastVisualPayload); @@ -267,12 +296,12 @@ function updateVisuals(payload) { } } } else { - maybeCreateUrchinInstance(lastVisualPayload); + maybeCreateUrchinInstance(lastVisualSchedule); } if (visualsState.urchin) { try { - visualsState.urchin.update({ data: visualsState.useLegacy ? null : lastVisualPayload }); + visualsState.urchin.update({ data: visualsState.useLegacy ? null : lastVisualSchedule }); } catch (error) { console.error('[visuals] failed to update radial urchin:', error); } @@ -298,21 +327,23 @@ function resetVisualsInstance() { } function hasVisualEvents(payload) { - return ( - payload && - typeof payload === 'object' && - Array.isArray(payload.events) && - payload.events.length > 0 - ); + const schedule = resolveVisualPayload(payload); + return Boolean(schedule && Array.isArray(schedule.events) && schedule.events.length > 0); } -function maybeCreateUrchinInstance(payload) { - if (visualsState.useLegacy || visualsState.urchin || !visualsState.mount || !hasVisualEvents(payload)) { +function maybeCreateUrchinInstance(schedule) { + if ( + visualsState.useLegacy || + visualsState.urchin || + !visualsState.mount || + !schedule || + !hasVisualEvents(schedule) + ) { return; } resetVisualsInstance(); const instance = createRadialUrchin(visualsState.mount, { - data: payload, + data: schedule, mode: 'day-rings', onSelect: handleUrchinSelect, }); @@ -500,7 +531,9 @@ function ensureCalendarHistorySummary(entry) { if (entry.summary && typeof entry.summary === 'object') { return entry.summary; } - const events = entry.rawResult && Array.isArray(entry.rawResult.events) + const events = entry.calendarJson && Array.isArray(entry.calendarJson.events) + ? entry.calendarJson.events + : entry.rawResult && Array.isArray(entry.rawResult.events) ? entry.rawResult.events : null; if (!events) { @@ -756,9 +789,15 @@ function renderCalendarHistorySummary() { function setCurrentCalendarHistoryEntry(entry, options = {}) { const { updateJson = true, focusVisuals = false, showEmptyState = true } = options; - const payload = entry && entry.rawResult ? cloneCalendarHistoryPayload(entry.rawResult) || entry.rawResult : null; + const rawPayload = + entry && entry.rawResult ? cloneCalendarHistoryPayload(entry.rawResult) || entry.rawResult : null; + const calendarPayloadSource = + entry && entry.calendarJson + ? cloneCalendarHistoryPayload(entry.calendarJson) || entry.calendarJson + : rawPayload; + const visualPayload = resolveVisualPayload(calendarPayloadSource); - if (!entry || !payload || typeof payload !== 'object') { + if (!entry || !visualPayload || typeof visualPayload !== 'object') { calendarHistoryState.activeId = null; calendarHistoryState.currentRun = null; updateVisuals(null); @@ -770,19 +809,20 @@ function setCurrentCalendarHistoryEntry(entry, options = {}) { calendarHistoryState.currentRun = { ...entry, summary: entry.summary && typeof entry.summary === 'object' ? { ...entry.summary } : null, - rawResult: cloneCalendarHistoryPayload(payload) || payload, + rawResult: rawPayload || visualPayload, + calendarJson: cloneCalendarHistoryPayload(visualPayload) || visualPayload, }; if (updateJson) { - setJsonPayload(payload, { + setJsonPayload(rawPayload || visualPayload, { variant: entry.variant, rig: entry.rig, weekStart: entry.weekStart, }); - const validation = validateWebV1Calendar(payload); + const validation = validateWebV1Calendar(rawPayload || visualPayload || {}); setJsonValidationBadge(validation.ok ? 'ok' : 'err'); } else { - updateVisuals(payload); + updateVisuals(rawPayload || visualPayload); } hideVisualsOverlay(); @@ -865,6 +905,7 @@ function recordCalendarHistoryEntry(entry) { if (!entry || !entry.rawResult) { return; } + const visualPayload = resolveVisualPayload(entry.calendarJson || entry.rawResult); const normalized = { id: entry.id || generateCalendarHistoryId(), timestamp: entry.timestamp || new Date().toISOString(), @@ -880,6 +921,7 @@ function recordCalendarHistoryEntry(entry) { weekStart: entry.weekStart || '', summary: entry.summary ? { ...entry.summary } : null, rawResult: cloneCalendarHistoryPayload(entry.rawResult) || entry.rawResult, + calendarJson: cloneCalendarHistoryPayload(visualPayload) || visualPayload, }; calendarHistoryState.runHistory = [normalized, ...calendarHistoryState.runHistory].slice( @@ -1247,8 +1289,9 @@ function setJsonPayload(payload, options = {}) { : snapshot.week_start || ''; metadata.week = weekFromOptions; - if (parsedPayload && Array.isArray(parsedPayload.events)) { - metadata.events = parsedPayload.events.length; + const scheduleForMeta = resolveVisualPayload(parsedPayload); + if (scheduleForMeta && Array.isArray(scheduleForMeta.events)) { + metadata.events = scheduleForMeta.events.length; } currentJsonMetadata = metadata; diff --git a/web/ui/visuals/RadialUrchin.js b/web/ui/visuals/RadialUrchin.js index ec99d3e..8006b29 100644 --- a/web/ui/visuals/RadialUrchin.js +++ b/web/ui/visuals/RadialUrchin.js @@ -138,6 +138,7 @@ export class RadialUrchin { this.contrastQuery = null; this.hasRenderableData = false; this.didWarnNoData = false; + this.didWarnInvalidCenter = false; this.handleResize = this.handleResize.bind(this); this.handlePointerMove = this.handlePointerMove.bind(this); @@ -646,6 +647,7 @@ export class RadialUrchin { this.overlay.setAttribute('width', width); this.overlay.setAttribute('height', height); this.center = { x: width / 2, y: height / 2 }; + this.didWarnInvalidCenter = false; this.canvasRect = getElementRect(this.canvas); this.rebuildDisplayArcs(); this.render(); @@ -661,6 +663,23 @@ export class RadialUrchin { return; } + if ( + !this.center || + typeof this.center.x !== 'number' || + Number.isNaN(this.center.x) || + typeof this.center.y !== 'number' || + Number.isNaN(this.center.y) + ) { + if (!this.didWarnInvalidCenter) { + console.warn('[RadialUrchin] invalid center point, skipping render', this.center); + this.didWarnInvalidCenter = true; + } + this.updateSelectionOverlay(); + this.updateScrubOverlay(); + return; + } + this.didWarnInvalidCenter = false; + if (!this.hasRenderableData || !this.layout || !Array.isArray(this.layout.arcs) || this.layout.arcs.length === 0) { ctx.clearRect(0, 0, this.offscreen.width, this.offscreen.height); mainCtx.clearRect(0, 0, this.canvas.width, this.canvas.height);