@@ -85,6 +85,8 @@ let currentJsonMetadata = { variant: '', rig: '', week: '', events: null };
8585const VISUALS_LEGACY_FLAG = 'wyrd.visuals.legacy' ;
8686const visualsState = {
8787 container : null ,
88+ layout : null ,
89+ mainPanel : null ,
8890 mount : null ,
8991 fallback : null ,
9092 fallbackImg : null ,
@@ -94,6 +96,14 @@ const visualsState = {
9496} ;
9597let lastVisualPayload = null ;
9698
99+ const CALENDAR_HISTORY_LIMIT = 20 ;
100+ const calendarHistoryState = {
101+ runHistory : [ ] ,
102+ panel : null ,
103+ list : null ,
104+ activeId : null ,
105+ } ;
106+
97107function readVisualsLegacyFlag ( ) {
98108 try {
99109 return localStorage . getItem ( VISUALS_LEGACY_FLAG ) === '1' ;
@@ -210,9 +220,19 @@ function initVisualsMount() {
210220 visualsState . container = container ;
211221 visualsState . useLegacy = readVisualsLegacyFlag ( ) ;
212222
223+ const layout = document . createElement ( 'div' ) ;
224+ layout . className = 'visuals-layout' ;
225+ container . append ( layout ) ;
226+ visualsState . layout = layout ;
227+
228+ const mainPanel = document . createElement ( 'div' ) ;
229+ mainPanel . className = 'visuals-main' ;
230+ layout . append ( mainPanel ) ;
231+ visualsState . mainPanel = mainPanel ;
232+
213233 const mount = document . createElement ( 'div' ) ;
214234 mount . className = 'visuals-mount' ;
215- container . append ( mount ) ;
235+ mainPanel . append ( mount ) ;
216236 visualsState . mount = mount ;
217237
218238 const fallback = document . createElement ( 'div' ) ;
@@ -226,11 +246,16 @@ function initVisualsMount() {
226246 fallbackMessage . textContent = 'Legacy visuals preview unavailable.' ;
227247 fallbackMessage . hidden = true ;
228248 fallback . append ( fallbackImg , fallbackMessage ) ;
229- container . append ( fallback ) ;
249+ mainPanel . append ( fallback ) ;
230250 visualsState . fallback = fallback ;
231251 visualsState . fallbackImg = fallbackImg ;
232252 visualsState . fallbackMessage = fallbackMessage ;
233253
254+ const historyPanel = createCalendarHistoryPanel ( ) ;
255+ if ( historyPanel ) {
256+ layout . append ( historyPanel ) ;
257+ }
258+
234259 if ( typeof window !== 'undefined' ) {
235260 window . WYRD_SET_VISUALS_LEGACY = ( enabled ) => {
236261 const flag = Boolean ( enabled ) ;
@@ -260,6 +285,279 @@ function safeInitVisuals(initialData) {
260285 }
261286}
262287
288+ function generateCalendarHistoryId ( ) {
289+ try {
290+ if ( typeof crypto !== 'undefined' && typeof crypto . randomUUID === 'function' ) {
291+ return crypto . randomUUID ( ) ;
292+ }
293+ } catch ( error ) {
294+ // ignore
295+ }
296+ const random = Math . random ( ) . toString ( 16 ) . slice ( 2 ) ;
297+ return `calendar-${ Date . now ( ) } -${ random } ` ;
298+ }
299+
300+ function parseHistoryTime ( value ) {
301+ if ( typeof value !== 'string' ) {
302+ return null ;
303+ }
304+ const [ hoursPart , minutesPart ] = value . split ( ':' ) ;
305+ const hours = Number . parseInt ( hoursPart , 10 ) ;
306+ const minutes = Number . parseInt ( minutesPart , 10 ) ;
307+ if ( ! Number . isFinite ( hours ) || ! Number . isFinite ( minutes ) ) {
308+ return null ;
309+ }
310+ const total = hours * 60 + minutes ;
311+ return Number . isFinite ( total ) ? ( ( total % ( 24 * 60 ) ) + 24 * 60 ) % ( 24 * 60 ) : null ;
312+ }
313+
314+ function computeEventDurationMinutes ( event ) {
315+ if ( ! event || typeof event !== 'object' ) {
316+ return 0 ;
317+ }
318+ const start = parseHistoryTime ( event . start ) ;
319+ const end = parseHistoryTime ( event . end ) ;
320+ if ( start === null || end === null ) {
321+ return 0 ;
322+ }
323+ if ( end >= start ) {
324+ return end - start ;
325+ }
326+ return 24 * 60 - start + end ;
327+ }
328+
329+ function computeCalendarHistorySummary ( events ) {
330+ if ( ! Array . isArray ( events ) || events . length === 0 ) {
331+ return { totalEvents : Array . isArray ( events ) ? events . length : 0 } ;
332+ }
333+
334+ let sleepMinutes = 0 ;
335+ let workMinutes = 0 ;
336+
337+ events . forEach ( ( event ) => {
338+ const duration = computeEventDurationMinutes ( event ) ;
339+ if ( duration <= 0 ) {
340+ return ;
341+ }
342+ const activity = ( event ?. activity || event ?. label || '' ) . toString ( ) . toLowerCase ( ) ;
343+ if ( activity . includes ( 'sleep' ) ) {
344+ sleepMinutes += duration ;
345+ }
346+ if ( activity . includes ( 'work' ) ) {
347+ workMinutes += duration ;
348+ }
349+ } ) ;
350+
351+ const summary = { totalEvents : events . length } ;
352+ if ( sleepMinutes > 0 ) {
353+ summary . totalSleepHours = Math . round ( ( sleepMinutes / 60 ) * 10 ) / 10 ;
354+ }
355+ if ( workMinutes > 0 ) {
356+ summary . totalWorkHours = Math . round ( ( workMinutes / 60 ) * 10 ) / 10 ;
357+ }
358+ return summary ;
359+ }
360+
361+ function cloneCalendarHistoryPayload ( payload ) {
362+ try {
363+ return JSON . parse ( JSON . stringify ( payload ?? { } ) ) ;
364+ } catch ( error ) {
365+ console . warn ( 'Unable to clone calendar history payload:' , error ) ;
366+ return null ;
367+ }
368+ }
369+
370+ function formatHistoryTimestamp ( value ) {
371+ try {
372+ const date = new Date ( value ) ;
373+ if ( Number . isNaN ( date . getTime ( ) ) ) {
374+ throw new Error ( 'Invalid date' ) ;
375+ }
376+ const pad = ( num ) => String ( num ) . padStart ( 2 , '0' ) ;
377+ const year = date . getFullYear ( ) ;
378+ const month = pad ( date . getMonth ( ) + 1 ) ;
379+ const day = pad ( date . getDate ( ) ) ;
380+ const hours = pad ( date . getHours ( ) ) ;
381+ const minutes = pad ( date . getMinutes ( ) ) ;
382+ return `${ year } -${ month } -${ day } ${ hours } :${ minutes } ` ;
383+ } catch ( error ) {
384+ return 'Unknown time' ;
385+ }
386+ }
387+
388+ function formatHistoryHours ( value ) {
389+ if ( ! Number . isFinite ( value ) ) {
390+ return null ;
391+ }
392+ const rounded = Math . round ( value * 10 ) / 10 ;
393+ return rounded . toFixed ( 1 ) ;
394+ }
395+
396+ function renderCalendarRunHistory ( ) {
397+ const list = calendarHistoryState . list ;
398+ if ( ! list ) {
399+ return ;
400+ }
401+
402+ list . innerHTML = '' ;
403+ if ( calendarHistoryState . runHistory . length === 0 ) {
404+ const emptyItem = document . createElement ( 'li' ) ;
405+ emptyItem . className = 'visuals-history-empty' ;
406+ emptyItem . textContent = 'No runs yet. Generate a schedule to build history.' ;
407+ list . append ( emptyItem ) ;
408+ return ;
409+ }
410+
411+ const fragment = document . createDocumentFragment ( ) ;
412+ calendarHistoryState . runHistory . forEach ( ( entry ) => {
413+ const item = document . createElement ( 'li' ) ;
414+ item . className = 'visuals-history-item' ;
415+
416+ const button = document . createElement ( 'button' ) ;
417+ button . type = 'button' ;
418+ button . className = 'visuals-history-entry' ;
419+ if ( entry . id === calendarHistoryState . activeId ) {
420+ button . classList . add ( 'is-active' ) ;
421+ }
422+
423+ const headline = document . createElement ( 'span' ) ;
424+ headline . className = 'visuals-history-entry__headline' ;
425+ const parts = [ `[${ formatHistoryTimestamp ( entry . timestamp ) } ]` ] ;
426+ if ( entry . archetype ) {
427+ parts . push ( `archetype=${ entry . archetype } ` ) ;
428+ }
429+ if ( entry . seed !== undefined && entry . seed !== null && entry . seed !== '' ) {
430+ parts . push ( `seed=${ entry . seed } ` ) ;
431+ }
432+ const variantLabel = [ entry . variant , entry . rig ] . filter ( Boolean ) . join ( '/' ) ;
433+ if ( variantLabel ) {
434+ parts . push ( variantLabel ) ;
435+ }
436+ headline . textContent = parts . join ( ' ' ) ;
437+
438+ const meta = document . createElement ( 'span' ) ;
439+ meta . className = 'visuals-history-entry__meta' ;
440+ const metaParts = [ ] ;
441+ if ( entry . weekStart ) {
442+ metaParts . push ( `week=${ entry . weekStart } ` ) ;
443+ }
444+ if ( entry . summary && Number . isFinite ( entry . summary . totalEvents ) ) {
445+ metaParts . push ( `events=${ entry . summary . totalEvents } ` ) ;
446+ }
447+ if ( entry . summary && Number . isFinite ( entry . summary . totalSleepHours ) ) {
448+ const value = formatHistoryHours ( entry . summary . totalSleepHours ) ;
449+ if ( value ) {
450+ metaParts . push ( `sleep≈${ value } h` ) ;
451+ }
452+ }
453+ if ( entry . summary && Number . isFinite ( entry . summary . totalWorkHours ) ) {
454+ const value = formatHistoryHours ( entry . summary . totalWorkHours ) ;
455+ if ( value ) {
456+ metaParts . push ( `work≈${ value } h` ) ;
457+ }
458+ }
459+ meta . textContent = metaParts . join ( ' • ' ) || 'No summary available' ;
460+
461+ button . append ( headline , meta ) ;
462+ button . addEventListener ( 'click' , ( ) => {
463+ restoreCalendarHistoryEntry ( entry . id ) ;
464+ } ) ;
465+
466+ item . append ( button ) ;
467+ fragment . append ( item ) ;
468+ } ) ;
469+
470+ list . append ( fragment ) ;
471+ }
472+
473+ function createCalendarHistoryPanel ( ) {
474+ if ( calendarHistoryState . panel ) {
475+ return calendarHistoryState . panel ;
476+ }
477+ const panel = document . createElement ( 'aside' ) ;
478+ panel . className = 'visuals-history-panel' ;
479+
480+ const header = document . createElement ( 'div' ) ;
481+ header . className = 'visuals-history-header' ;
482+
483+ const title = document . createElement ( 'h3' ) ;
484+ title . className = 'visuals-history-title' ;
485+ title . textContent = 'History' ;
486+
487+ header . append ( title ) ;
488+ panel . append ( header ) ;
489+
490+ const list = document . createElement ( 'ul' ) ;
491+ list . className = 'visuals-history-list' ;
492+ panel . append ( list ) ;
493+
494+ calendarHistoryState . panel = panel ;
495+ calendarHistoryState . list = list ;
496+
497+ renderCalendarRunHistory ( ) ;
498+
499+ return panel ;
500+ }
501+
502+ function recordCalendarHistoryEntry ( entry ) {
503+ if ( ! entry || ! entry . rawResult ) {
504+ return ;
505+ }
506+ const normalized = {
507+ id : entry . id || generateCalendarHistoryId ( ) ,
508+ timestamp : entry . timestamp || new Date ( ) . toISOString ( ) ,
509+ archetype : entry . archetype || '' ,
510+ seed :
511+ Number . isFinite ( entry . seed )
512+ ? entry . seed
513+ : Number . isFinite ( Number . parseInt ( entry . seed , 10 ) )
514+ ? Number . parseInt ( entry . seed , 10 )
515+ : undefined ,
516+ variant : entry . variant || '' ,
517+ rig : entry . rig || '' ,
518+ weekStart : entry . weekStart || '' ,
519+ summary : entry . summary ? { ...entry . summary } : null ,
520+ rawResult : cloneCalendarHistoryPayload ( entry . rawResult ) || entry . rawResult ,
521+ } ;
522+
523+ calendarHistoryState . runHistory = [ normalized , ...calendarHistoryState . runHistory ] . slice (
524+ 0 ,
525+ CALENDAR_HISTORY_LIMIT ,
526+ ) ;
527+ calendarHistoryState . activeId = normalized . id ;
528+ renderCalendarRunHistory ( ) ;
529+ }
530+
531+ function restoreCalendarHistoryEntry ( entryId ) {
532+ if ( ! entryId ) {
533+ return ;
534+ }
535+ const entry = calendarHistoryState . runHistory . find ( ( item ) => item . id === entryId ) ;
536+ if ( ! entry ) {
537+ return ;
538+ }
539+ const payload = cloneCalendarHistoryPayload ( entry . rawResult ) || entry . rawResult ;
540+ if ( ! payload || typeof payload !== 'object' ) {
541+ return ;
542+ }
543+
544+ calendarHistoryState . activeId = entry . id ;
545+
546+ setJsonPayload ( payload , {
547+ variant : entry . variant ,
548+ rig : entry . rig ,
549+ weekStart : entry . weekStart ,
550+ } ) ;
551+ updateJsonActionsState ( ) ;
552+ const validation = validateWebV1Calendar ( payload ) ;
553+ setJsonValidationBadge ( validation . ok ? 'ok' : 'err' ) ;
554+ renderCalendarRunHistory ( ) ;
555+ dispatchIntent ( {
556+ type : INTENT_TYPES . NAVIGATE_TAB ,
557+ payload : { tab : 'visuals' } ,
558+ } ) ;
559+ }
560+
263561let getConfigSnapshot = ( ) => ( {
264562 classId : 'calendar' ,
265563 variant : '' ,
@@ -3343,6 +3641,20 @@ function hydrateConfigPanel() {
33433641 inputsSnapshot . budget = true ;
33443642 }
33453643
3644+ recordCalendarHistoryEntry ( {
3645+ archetype,
3646+ seed : normalizedSeed ,
3647+ variant : variantId ,
3648+ rig : rigId ,
3649+ weekStart :
3650+ typeof result . week_start === 'string' && result . week_start
3651+ ? result . week_start
3652+ : weekStartValue ,
3653+ rawResult : result ,
3654+ summary : computeCalendarHistorySummary ( result . events ) ,
3655+ timestamp : new Date ( ) . toISOString ( ) ,
3656+ } ) ;
3657+
33463658 addRunHistoryEntry ( {
33473659 kind : 'generate' ,
33483660 ts : Date . now ( ) ,
0 commit comments