let threads=[],allGeneratedEvents=[],mergedEvents=[],histogramBuckets=[],zoomRange=null,timezonesCache=[],histogramIsDragging=!1,threadColorMap=new Map,threadKeyByPath=new Map,_bucketIsolateKeys=null,_histHoverIndex=-1,_histSourceEvents=[],lastRunSummary=null,_exportModalEl=null;function ensureExportModalCss(){if(document.getElementById("tw-export-modal-css"))return;const e=`
/* ---- TimeWeave Export Modal ---- */
.tw-modal-backdrop{
  position:fixed; inset:0;
  background:rgba(0,0,0,.45);
  display:flex; align-items:center; justify-content:center;
  z-index:9999;
  padding:18px;
}
.tw-modal{
  width:min(760px, 100%);
  background:#12141a;
  border:1px solid rgba(255,255,255,.08);
  border-radius:14px;
  box-shadow:0 18px 60px rgba(0,0,0,.55);
  overflow:hidden;
}
.tw-modal-header{
  display:flex; align-items:center; justify-content:space-between;
  gap:12px;
  padding:14px 16px;
  border-bottom:1px solid rgba(255,255,255,.08);
  background:linear-gradient(180deg, rgba(255,255,255,.04), rgba(255,255,255,0));
}
.tw-modal-title{
  display:flex; align-items:center; gap:10px;
  font-weight:650;
  color:#e9edf7;
  letter-spacing:.2px;
}
.tw-badge{
  font-size:12px;
  padding:3px 8px;
  border-radius:999px;
  border:1px solid rgba(255,255,255,.12);
  background:rgba(255,255,255,.05);
  color:#d6deff;
}
.tw-modal-close{
  border:1px solid rgba(255,255,255,.12);
  background:rgba(255,255,255,.05);
  color:#e9edf7;
  border-radius:10px;
  padding:6px 10px;
  cursor:pointer;
}
.tw-modal-close:hover{ background:rgba(255,255,255,.08); }

.tw-modal-body{
  padding:14px 16px 12px 16px;
  color:#cfd6ea;
}
.tw-row{
  display:flex; gap:12px; flex-wrap:wrap;
  margin-bottom:10px;
}
.tw-kv{
  flex:1 1 280px;
  border:1px solid rgba(255,255,255,.08);
  background:rgba(255,255,255,.03);
  border-radius:12px;
  padding:10px 12px;
}
.tw-kv .k{ font-size:12px; color:rgba(233,237,247,.7); margin-bottom:4px; }
.tw-kv .v{
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  font-size:12.5px;
  color:#e9edf7;
  word-break:break-all;
}

.tw-list{
  margin-top:10px;
  border:1px solid rgba(255,255,255,.08);
  background:rgba(255,255,255,.02);
  border-radius:12px;
  overflow:hidden;
}
.tw-list-head{
  padding:10px 12px;
  border-bottom:1px solid rgba(255,255,255,.08);
  color:#e9edf7;
  font-weight:600;
  display:flex; justify-content:space-between; align-items:center;
}
.tw-list-body{ max-height:260px; overflow:auto; }
.tw-item{
  display:flex; gap:10px; align-items:flex-start;
  padding:10px 12px;
  border-bottom:1px solid rgba(255,255,255,.06);
}
.tw-item:last-child{ border-bottom:none; }
.tw-dot{
  width:10px; height:10px; border-radius:999px;
  margin-top:4px;
  background:rgba(130,170,255,.9);
  box-shadow:0 0 0 3px rgba(130,170,255,.15);
  flex:0 0 auto;
}
.tw-item .name{ color:#e9edf7; font-weight:600; font-size:13px; }
.tw-item .meta{ color:rgba(233,237,247,.65); font-size:12px; margin-top:2px; }
.tw-item .path{
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  font-size:12px;
  color:#cfd6ea;
  word-break:break-all;
  margin-top:4px;
}

.tw-modal-footer{
  display:flex; gap:10px; justify-content:flex-end;
  padding:12px 16px;
  border-top:1px solid rgba(255,255,255,.08);
  background:rgba(255,255,255,.02);
}
.tw-btn{
  border:1px solid rgba(255,255,255,.12);
  background:rgba(255,255,255,.05);
  color:#e9edf7;
  border-radius:12px;
  padding:8px 12px;
  cursor:pointer;
  font-weight:600;
}
.tw-btn:hover{ background:rgba(255,255,255,.08); }
.tw-btn.primary{
  background:rgba(130,170,255,.18);
  border-color:rgba(130,170,255,.35);
}
.tw-btn.primary:hover{ background:rgba(130,170,255,.24); }

.tw-ok{ color:#8bdc9b; }
.tw-warn{ color:#ffd27a; }
.tw-err{ color:#ff8a8a; }
    `,t=document.createElement("style");t.id="tw-export-modal-css",t.textContent=e,document.head.appendChild(t)}function closeExportResultModal(){_exportModalEl&&(_exportModalEl.remove(),_exportModalEl=null,document.removeEventListener("keydown",_exportModalEscHandler,!0))}function _exportModalEscHandler(e){e.key==="Escape"&&closeExportResultModal()}function _safeText(e){return e==null?"":String(e)}function showExportResultModal(e){ensureExportModalCss(),closeExportResultModal();const t=!!e?.ok,n=_safeText(e?.exportDir||e?.folder||e?.outputDir||""),o=Array.isArray(e?.files)?e.files:[],a=Array.isArray(e?.warnings)?e.warnings:[],s=_safeText(e?.error||""),i=t?"Export complete":"Export failed",r=t?"tw-ok":"tw-err",d=t?`${o.length} file${o.length===1?"":"s"}`:"Error",p=document.createElement("div");p.className="tw-modal-backdrop",p.addEventListener("click",f=>{f.target===p&&closeExportResultModal()});const u=document.createElement("div");u.className="tw-modal",u.setAttribute("role","dialog"),u.setAttribute("aria-modal","true"),u.innerHTML=`
<div class="tw-modal-header">
  <div class="tw-modal-title">
    <span class="${r}">●</span>
    <span>${i}</span>
    <span class="tw-badge">${d}</span>
  </div>
  <button class="tw-modal-close" type="button" aria-label="Close">Close</button>
</div>

<div class="tw-modal-body">
  ${n?`
  <div class="tw-row">
    <div class="tw-kv">
      <div class="k">Export folder</div>
      <div class="v" id="twExportDirText">${n}</div>
    </div>
  </div>`:""}

  ${a.length?`
    <div class="tw-row">
      <div class="tw-kv">
        <div class="k tw-warn">Warnings</div>
        <div class="v tw-warn">${a.map(f=>_safeText(f)).join(`
`)}</div>
      </div>
    </div>`:""}

  ${!t&&s?`
    <div class="tw-row">
      <div class="tw-kv">
        <div class="k tw-err">Error</div>
        <div class="v tw-err">${s.replace(/</g,"&lt;").replace(/>/g,"&gt;")}</div>
      </div>
    </div>`:""}

  ${t?`
  <div class="tw-list">
    <div class="tw-list-head">
      <span>Exported files</span>
      <span class="tw-badge">${o.length}</span>
    </div>
    <div class="tw-list-body">
      ${o.map(f=>{const T=_safeText(f?.name||f?.fileName||"(file)"),b=_safeText(f?.path||f?.fullPath||""),E=typeof f?.bytes=="number"?f.bytes:null,l=_safeText(f?.sha256||f?.hash||""),I=[E!==null?`${E.toLocaleString()} bytes`:"",l?`sha256: ${l.slice(0,12)}…`:""].filter(Boolean).join(" · ");return`
        <div class="tw-item">
          <div class="tw-dot"></div>
          <div style="min-width:0;">
            <div class="name">${T.replace(/</g,"&lt;").replace(/>/g,"&gt;")}</div>
            ${I?`<div class="meta">${I}</div>`:""}
            ${b?`<div class="path">${b.replace(/</g,"&lt;").replace(/>/g,"&gt;")}</div>`:""}
          </div>
        </div>`}).join("")}
    </div>
  </div>`:""}
</div>

<div class="tw-modal-footer">
  ${n?'<button class="tw-btn" type="button" id="twCopyExportDirBtn">Copy folder path</button>':""}
  <button class="tw-btn primary" type="button" id="twExportOkBtn">OK</button>
</div>
    `,p.appendChild(u),document.body.appendChild(p),_exportModalEl=p,u.querySelector(".tw-modal-close")?.addEventListener("click",closeExportResultModal),u.querySelector("#twExportOkBtn")?.addEventListener("click",closeExportResultModal);const c=u.querySelector("#twCopyExportDirBtn");c&&n&&c.addEventListener("click",async()=>{try{await navigator.clipboard.writeText(n),c.textContent="Copied!",setTimeout(()=>{c&&(c.textContent="Copy folder path")},900)}catch{const f=u.querySelector("#twExportDirText");if(f){const T=document.createRange();T.selectNodeContents(f);const b=window.getSelection();b.removeAllRanges(),b.addRange(T)}}}),document.addEventListener("keydown",_exportModalEscHandler,!0),setTimeout(()=>u.querySelector("#twExportOkBtn")?.focus(),0)}const TW_PROJECT_STORAGE_KEY="timeweave_project_state_v1";let _persistTimer=null,_persistDirty=!1,_persistRestoring=!1,_histStackMode="thread",_histContribThreadKeys=[],_histEventTypeOrder=[],_eventTypeLabelByKey=new Map,_autoRunAfterRestoreDone=!1,_restoredUiSnapshot=null;function loadProjectStateRaw(){return lsLoadProjectState()}function setRunStatus(e){const t=document.getElementById("runStatus");t&&(t.textContent=e||"")}function safeJsonParse(e){try{return JSON.parse(e)}catch{return null}}function nowIso(){return new Date().toISOString()}function snapshotUiState(){const e=(document.getElementById("fromUtc")?.value||"").trim(),t=(document.getElementById("toUtc")?.value||"").trim(),n=(document.getElementById("bucketSize")?.value||"hour").trim(),o=!!document.getElementById("collapseSameTs")?.checked,a=!!document.getElementById("filterByEventType")?.checked,s=[];return document.querySelectorAll("#eventTypeFiltersBody .evtTypeChk").forEach(i=>{i.checked&&s.push(i.value)}),{fromUtc:e||"",toUtc:t||"",bucketSize:n,collapseSameTs:o,filterByEventType:a,eventTypeChecked:s}}function snapshotThreadsState(){return(threads||[]).map(e=>({key:e?.key||"",path:e?.path||"",fileHashSha256:e?.fileHashSha256||(e?.thread?.fileHashSha256??null),selectedTsKeys:Array.isArray(e?.selectedTsKeys)?[...e.selectedTsKeys]:[],timeOptions:e?.timeOptions?{...e.timeOptions}:null}))}function snapshotFiltersState(){return{filterLocked:!!_filterLocked,bucketIsolateKeys:Array.isArray(_bucketIsolateKeys)?[..._bucketIsolateKeys]:null,legendPinnedKeys:_legendPinnedKeys&&_legendPinnedKeys.size?Array.from(_legendPinnedKeys):[]}}function buildProjectState(){const e=(document.getElementById("threadPaths")?.value||"").toString();return{v:1,savedAtUtc:nowIso(),threadPathsText:e,threads:snapshotThreadsState(),ui:snapshotUiState(),filters:snapshotFiltersState(),lastRunSummary}}function applyUiStateToDom(e){if(!e)return;const t=document.getElementById("bucketSize");t&&e.bucketSize&&(t.value=e.bucketSize);const n=document.getElementById("collapseSameTs");n&&(n.checked=!!e.collapseSameTs);const o=document.getElementById("fromUtc"),a=document.getElementById("toUtc");o&&(o.value=e.fromUtc||""),a&&(a.value=e.toUtc||"");const s=document.getElementById("filterByEventType");s&&(s.checked=!!e.filterByEventType),typeof updateEventTypeFilterUiVisibility=="function"&&updateEventTypeFilterUiVisibility()}function applyEventTypeSelections(e){if(!e||!Array.isArray(e.eventTypeChecked))return;const t=new Set(e.eventTypeChecked);document.querySelectorAll("#eventTypeFiltersBody .evtTypeChk").forEach(n=>{n.checked=t.has(n.value)})}function applyFiltersState(e){if(_filterLocked=!!e?.filterLocked,_bucketIsolateKeys=Array.isArray(e?.bucketIsolateKeys)?e.bucketIsolateKeys:null,_legendPinnedKeys&&typeof _legendPinnedKeys.clear=="function"){_legendPinnedKeys.clear();for(const t of e?.legendPinnedKeys||[])_legendPinnedKeys.add(t)}_legendPinnedKeys&&_legendPinnedKeys.size>0?setThreadAndLegendHighlights([..._legendPinnedKeys]):Array.isArray(_bucketIsolateKeys)&&_bucketIsolateKeys.length?setThreadAndLegendHighlights(_bucketIsolateKeys):clearThreadAndLegendHighlights(),typeof updateActiveFilterSummary=="function"&&updateActiveFilterSummary()}function mergeSavedThreadPrefsIntoLoadedThreads(e){if(!Array.isArray(e)||!e.length)return;const t=new Map,n=new Map;for(const o of e)o?.key&&t.set(o.key,o),o?.path&&n.set(normalizePathKey(o.path),o);for(const o of threads||[]){if(!o?.ok)continue;const a=t.get(o.key)||n.get(normalizePathKey(o.path))||null;a&&(Array.isArray(a.selectedTsKeys)&&(o.selectedTsKeys=[...new Set(a.selectedTsKeys.filter(Boolean))]),a.timeOptions&&(o.timeOptions={...o.timeOptions,...a.timeOptions}))}}async function apiLoadProjectState(){try{const e=await fetch("/api/project/state",{method:"GET"}),t=await e.text();if(!e.ok)return null;const n=safeJsonParse(t);return!n||!n.ok||!n.hasState?null:n.state||null}catch{return null}}async function apiSaveProjectState(e){try{return(await fetch("/api/project/state",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(e)})).ok}catch{return!1}}async function apiClearProjectState(){try{return(await fetch("/api/project/clear",{method:"POST",headers:{"content-type":"application/json"},body:"{}"})).ok}catch{return!1}}function lsLoadProjectState(){const e=localStorage.getItem(TW_PROJECT_STORAGE_KEY);return e?safeJsonParse(e):null}function lsSaveProjectState(e){try{localStorage.setItem(TW_PROJECT_STORAGE_KEY,JSON.stringify(e))}catch{}}function lsClearProjectState(){try{localStorage.removeItem(TW_PROJECT_STORAGE_KEY)}catch{}}async function saveProjectStateNow(e=""){if(_persistRestoring)return;const t=buildProjectState(),n=await apiSaveProjectState(t);lsSaveProjectState(t),n||console.warn("[TimeWeave] Failed to save project to disk (API). Saved to localStorage fallback.",e)}function queueSaveProjectState(e=""){_persistRestoring||(_persistDirty=!0,!_persistTimer&&(_persistTimer=setTimeout(async()=>{_persistTimer=null,_persistDirty&&(_persistDirty=!1,await saveProjectStateNow(e))},350)))}async function restoreProjectStateOnBoot(){let e=await apiLoadProjectState();if(e||(e=lsLoadProjectState()),!e||!e.v)return!1;_persistRestoring=!0;try{const t=document.getElementById("threadPaths");return t&&typeof e.threadPathsText=="string"&&(t.value=e.threadPathsText),applyUiStateToDom(e.ui),lastRunSummary=e.lastRunSummary||null,_restoredUiSnapshot=e.ui||null,((e.threadPathsText||"").trim().length>0||Array.isArray(e.threads)&&e.threads.some(o=>(o?.path||"").trim()))&&(await addThreads(),mergeSavedThreadPrefsIntoLoadedThreads(e.threads),renderThreads(),renderHistogramLegend(),applyFiltersState(e.filters),refreshTopbarStats("restoreProjectStateOnBoot"),typeof updateActiveFilterSummary=="function"&&updateActiveFilterSummary()),!0}catch(t){return console.warn("[TimeWeave] Restore failed:",t),!1}finally{_persistRestoring=!1,queueSaveProjectState("post-restore-normalize")}}window.twSaveProjectNow=async function(){await saveProjectStateNow("manual")},window.twClearSavedProject=async function(){await apiClearProjectState(),lsClearProjectState(),alert("Saved project cleared.")},window.addEventListener("beforeunload",()=>{try{const e=buildProjectState();if(lsSaveProjectState(e),navigator.sendBeacon){const t=new Blob([JSON.stringify(e)],{type:"application/json"});navigator.sendBeacon("/api/project/state",t)}}catch{}});function getSeriesKeyForEvent(e){return getThreadKeyForEvent(e)||"__unknown__"}function getTsUtc(e){return(e?.tsUtc??e?.TsUtc??"").toString()}function getSrcPath(e){return(e?.sourcePath??e?.SourcePath??"").toString()}function getRowIndex(e){return(e?.rowIndex??e?.RowIndex??"").toString()}function getTitle(e){return(e?.title??e?.Title??"").toString()}function joinKeyStrict(e){return`${getTsUtc(e)}|${getSrcPath(e)}|${getRowIndex(e)}`}function joinKeyLoose(e){return`${getTsUtc(e)}|${getSrcPath(e)}|${getTitle(e)}`}function buildAllEventsLookup(){const e=new Map;for(const t of allGeneratedEvents||[])e.set(joinKeyStrict(t),t),e.set(joinKeyLoose(t),t);return e}function normalizeMergedEvent(e){return{tsUtc:getTsUtc(e),title:getTitle(e)||"(event)",rowIndex:e?.rowIndex??e?.RowIndex??0,sourcePath:getSrcPath(e),eventType:e?.eventType??e?.EventType,eventTypeKey:e?.eventTypeKey??e?.EventTypeKey}}function rehydrateMergedEvents(e){const t=buildAllEventsLookup();return(e||[]).map(n=>{const o=normalizeMergedEvent(n);if(!o.eventType&&!o.eventTypeKey){const a=t.get(joinKeyStrict(o))||t.get(joinKeyLoose(o));a&&(o.eventType=a.eventType??a.EventType??o.eventType,o.eventTypeKey=a.eventTypeKey??a.EventTypeKey??o.eventTypeKey)}return o.eventType||(o.eventType="(timestamp)"),o.eventTypeKey||(o.eventTypeKey=eventTypeKey(o.eventType)),_eventTypeLabelByKey.set(o.eventTypeKey,o.eventType),o})}function resetTimelineState(e=""){console.log("[TimeWeave] Reset timeline state",e),mergedEvents=[],histogramBuckets=[],zoomRange=null,histogramIsDragging=!1;const t=document.getElementById("histogram");t&&t.getContext("2d").clearRect(0,0,t.width,t.height);const n=document.getElementById("histogramStats");n&&(n.textContent=""),refreshTopbarStats("resetTimelineState");const o=document.getElementById("timeline");o&&(o.innerHTML="")}function extractParsingStats(e){if(!e)return null;const t=[e.stats?.total,e.stats?.Total,e.stats?.parsing,e.stats?.Parsing,e.stats,e.Stats?.total,e.Stats,e.parsingStats,e.ParsingStats,e.parseStats,e.ParseStats];for(const n of t){if(!n||typeof n!="object")continue;if(n.parsedOk!=null||n.ParsedOk!=null||n.nonEmptyValues!=null||n.NonEmptyValues!=null||n.failed!=null||n.Failed!=null||n.ambiguousDate!=null||n.AmbiguousDate!=null||n.hasTimezone!=null||n.HasTimezone!=null)return n}return null}function updateTopbarStats({threadsCount:e,eventsCount:t,mode:n}){const o=document.getElementById("twPillThreads"),a=document.getElementById("twPillEvents"),s=document.getElementById("twPillMode");o&&(o.textContent=`${e??0} Threads`),a&&(a.textContent=`${(t??0).toLocaleString()} Events`),s&&(s.textContent=n||"Normal")}function getCurrentModeLabel(){const e=document.getElementById("twMode");return e&&(e.value||"Normal").trim()||"Normal"}function refreshTopbarStats(e=""){try{updateTopbarStats({threadsCount:(threads||[]).filter(t=>t&&t.ok).length,eventsCount:(allGeneratedEvents||[]).length,mode:getCurrentModeLabel()})}catch(t){console.warn("[TimeWeave] refreshTopbarStats failed",e,t)}}let _filterLocked=!1;function eventTypeKey(e){return encodeURIComponent(normalizeEventTypeLabel(e))}function normalizeEventTypeLabel(e){return(e??"").toString().trim()||"(unknown)"}function getAllEventTypesFromAllEvents(){const e=new Set;for(const t of allGeneratedEvents||[])e.add(normalizeEventTypeLabel(t.eventType??t.EventType));return Array.from(e).sort((t,n)=>t.localeCompare(n))}function getSelectedEventTypesFromUi(){if(!document.getElementById("filterByEventType")?.checked)return null;const t=document.querySelectorAll("#eventTypeFiltersBody .evtTypeChk"),n=new Set;return t.forEach(o=>{o.checked&&n.add(o.value)}),n}function applyEventTypeFilterToEvents(e){const t=getSelectedEventTypesFromUi();return t?(e||[]).filter(n=>{const o=n.eventType??n.EventType;return t.has(eventTypeKey(o))}):e}function setAllEventTypeCheckboxes(e){document.querySelectorAll("#eventTypeFiltersBody .evtTypeChk").forEach(t=>{t.checked=!!e})}function updateEventTypeFilterUiVisibility(){const e=!!document.getElementById("filterByEventType")?.checked,t=document.getElementById("eventTypeFilters"),n=document.getElementById("btn-eventType-all"),o=document.getElementById("btn-eventType-none");t&&(t.style.display=e?"block":"none"),n&&(n.style.display=e?"":"none"),o&&(o.style.display=e?"":"none")}function renderEventTypeFiltersFromAllEvents(){const e=document.getElementById("eventTypeFiltersBody");if(!e)return;const t=getAllEventTypesFromAllEvents();if(!t.length){e.innerHTML='<div class="hint">No event types yet. Generate events first.</div>';return}const n=new Set;document.querySelectorAll("#eventTypeFiltersBody .evtTypeChk").forEach(o=>{o.checked&&n.add(o.value)}),e.innerHTML=t.map(o=>{const a=n.size?n.has(eventTypeKey(o)):!0;return`
            <label style="display:block;margin-top:4px">
                <input type="checkbox" class="evtTypeChk" value="${eventTypeKey(o)}" ${a?"checked":""}>
                ${escapeHtml(o)}
            </label>
        `}).join(""),e.querySelectorAll(".evtTypeChk").forEach(o=>{o.addEventListener("change",()=>{typeof mergeTimeline=="function"&&mergeTimeline(),typeof updateActiveFilterSummary=="function"&&updateActiveFilterSummary(),queueSaveProjectState("eventType selection changed")})})}function getActiveThreadFilterKeys(){return _filterLocked&&Array.isArray(_bucketIsolateKeys)&&_bucketIsolateKeys.length?_bucketIsolateKeys:_legendPinnedKeys&&_legendPinnedKeys.size>0?Array.from(_legendPinnedKeys):Array.isArray(_bucketIsolateKeys)&&_bucketIsolateKeys.length?_bucketIsolateKeys:null}function updateActiveFilterSummary(){const e=document.getElementById("af-time"),t=document.getElementById("af-bucket"),n=document.getElementById("af-threads"),o=document.getElementById("af-events"),a=document.getElementById("af-lock");if(!e||!t||!n||!o||!a)return;const s=document.getElementById("fromUtc")?.value||"",i=document.getElementById("toUtc")?.value||"";e.textContent=s||i?`${s||"…"} → ${i||"…"}`:"All time",t.textContent=document.getElementById("bucketSize")?.value||"—";const r=getActiveThreadFilterKeys();if(!r||!r.length)n.textContent="All";else{const u=r.map(c=>{const f=threads.find(T=>T.key===c);return f?.thread?.displayName||f?.path?.split(/[\\/]/).pop()||c});n.textContent=u.join(", ")}const d=allGeneratedEvents?.length||0,p=mergedEvents?.length||0;o.textContent=d?`${d} → ${p}`:"—",a.textContent=_filterLocked?"🔒 Locked":"🔓 Unlock"}function readGlobalTimeOptions(){const e=(document.getElementById("dateOrder")?.value||"DMY").trim().toUpperCase(),t=(document.getElementById("naiveMode")?.value||"utc").trim().toLowerCase(),n=(document.getElementById("sourceTimeZoneId")?.value||"").trim();return{dateOrder:e,naiveMode:t,sourceTimeZoneId:n||null}}function buildTimezoneOptionsHtml(e){if(!timezonesCache.length)return'<option value="">(loading timezones…)</option>';const t=e||"";let n='<option value="">(select timezone)</option>';for(const o of timezonesCache){const a=o.id,s=`${o.displayName} — ${o.id}`;n+=`<option value="${escapeHtml(a)}" ${a===t?"selected":""}>${escapeHtml(s)}</option>`}return n}async function applyGlobalTimeSettingsToAllThreadsAndGenerate(){const e=document.getElementById("btn-apply-global-time"),t=document.getElementById("applyGlobalTimeStatus");e&&(e.disabled=!0),t&&(t.textContent="Applying settings…");try{const n=readGlobalTimeOptions();let o=0;for(const a of threads)!a||!a.ok||(a.timeOptions||(a.timeOptions={}),a.timeOptions.dateOrder=n.dateOrder,a.timeOptions.naiveMode=n.naiveMode,a.timeOptions.sourceTimeZoneId=n.sourceTimeZoneId,a.lastStats=null,a.lastWarnings=[],a.lastAppliedTime=null,o++);renderThreads(),t&&(t.textContent=`Applied to ${o} thread(s). Generating…`),await generateEvents(),t&&(t.textContent=`Done. Applied + generated for ${o} thread(s).`)}catch(n){t&&(t.textContent="Failed: "+(n?.message||n))}finally{e&&(e.disabled=!1)}}function groupByExactTimestamp(e){const t=new Map;for(const n of e){const o=n.tsUtc;t.has(o)||t.set(o,[]),t.get(o).push(n)}return Array.from(t.entries()).sort((n,o)=>n[0].localeCompare(o[0])).map(([n,o])=>({tsUtc:n,items:o}))}function normalizeSha256(e){return(e??"").toString().trim().toLowerCase()}function makeKeyFromHashOrPath(e,t){const n=normalizeSha256(e);return n&&n.length>=16?"h_"+n:makeKeyFromPath(t||"")}function getThreadHashFromApiResult(e){return getThreadStableIdFromApiResult(e)}function getThreadStableIdFromApiResult(e){return e?.thread?.threadId??e?.thread?.ThreadId??e?.thread?.fileHashSha256??e?.thread?.FileHashSha256??e?.fileHashSha256??e?.FileHashSha256??""}function makeKeyFromPath(e){let t=0;for(let n=0;n<e.length;n++)t=(t<<5)-t+e.charCodeAt(n)|0;return"t"+(t>>>0).toString(16)}function normalizeUserPathLine(e){if(!e)return"";let t=String(e).trim();return(t.startsWith('"')&&t.endsWith('"')||t.startsWith("'")&&t.endsWith("'"))&&(t=t.slice(1,-1).trim()),t}function normalizePathKey(e){return(e||"").trim().toLowerCase()}function rebuildThreadPathIndex(){threadKeyByPath=new Map;for(const e of threads||[]){if(!e?.ok)continue;const t=normalizePathKey(e.path);t&&threadKeyByPath.set(t,e.key)}}function getThreadKeyForEvent(e){const t=(e?.threadKey??e?.ThreadKey??"").toString();if(t)return t;const n=e?.sourcePath??e?.SourcePath??"",o=normalizePathKey(n);return threadKeyByPath.get(o)||null}function hslToCss(e,t,n){return`hsl(${Math.round(e)} ${Math.round(t)}% ${Math.round(n)}%)`}function parseHueFromHsl(e){const t=/hsl\(\s*([0-9.]+)/i.exec(e||"");return t?parseFloat(t[1]):null}function circularHueDist(e,t){const n=Math.abs(e-t)%360;return Math.min(n,360-n)}function makeDistinctThreadColor(e){const t=(e||[]).map(r=>parseHueFromHsl(r?.fill)).filter(r=>Number.isFinite(r)),n=137.50776405,o=36;let a=null;for(let r=0;r<o;r++){const d=r*n%360,p=70,u=r%2===0?52:46;let c=999;for(const T of t)c=Math.min(c,circularHueDist(d,T));t.length||(c=999);const f=c;(!a||f>a.score)&&(a={hue:d,sat:p,light:u,score:f})}const s=hslToCss(a.hue,a.sat,a.light),i=hslToCss(a.hue,a.sat,Math.max(18,a.light-18));return{fill:s,stroke:i}}function ensureThreadColor(e){if(e||(e="__unknown__"),threadColorMap.has(e))return threadColorMap.get(e);const t=Array.from(threadColorMap.values()),n=makeDistinctThreadColor(t);return threadColorMap.set(e,n),n}function ensureAllThreadColors(){for(const e of threads||[])e?.ok&&ensureThreadColor(e.key);ensureThreadColor("__unknown__")}function _hash32(e){e=String(e??"");let t=2166136261;for(let n=0;n<e.length;n++)t^=e.charCodeAt(n),t=Math.imul(t,16777619)>>>0;return t>>>0}function ensureEventTypeColor(e){const t=String(e||"(unknown)"),o=_hash32(t)%360,a=`hsl(${o} 72% 52%)`,s=`hsl(${o} 72% 30%)`;return{fill:a,stroke:s}}function getEventTypeLabel(e){const t=String(e||"");return _eventTypeLabelByKey.get(t)||decodeURIComponent(t||"")||"(unknown)"}function sizeCanvasToDisplay(e,t,n){const o=window.devicePixelRatio||1,a=Math.max(1,Math.floor(t)),s=Math.max(1,Math.floor(n)),i=Math.max(1,Math.floor(a*o)),r=Math.max(1,Math.floor(s*o));return e.style.width!==a+"px"&&(e.style.width=a+"px"),e.style.height!==s+"px"&&(e.style.height=s+"px"),e.width!==i&&(e.width=i),e.height!==r&&(e.height=r),{dpr:o,cssW:a,cssH:s}}function buildTooltipBreakdownForBucket(e,t){const n=mergedEvents||[];if(!n.length)return null;const o=new Date(t==="day"?e+"T00:00:00.000Z":t==="hour"?e+":00:00.000Z":e+":00.000Z"),a=new Date(t==="day"?o.getTime()+24*3600*1e3:t==="hour"?o.getTime()+3600*1e3:o.getTime()+60*1e3),s=new Map;for(const i of n){const r=new Date(i.tsUtc);if(r<o||r>=a)continue;const d=getSeriesKeyForEvent(i),p=normalizeEventTypeLabel(i.eventType??i.EventType);s.has(d)||s.set(d,new Map);const u=s.get(d);u.set(p,(u.get(p)||0)+1)}return s}let _histHoverWired=!1;function wireHistogramHoverOnce(){if(_histHoverWired)return;const e=document.getElementById("histogram"),t=document.getElementById("histTip");if(!e||!t)return;_histHoverWired=!0,t.style.position||(t.style.position="fixed"),t.style.pointerEvents="none";const n=e.closest(".histogram-body")||e.parentElement;function o(){t.style.display="none",_bucketIsolateKeys=null,clearThreadAndLegendHighlights(),_histHoverIndex!==-1&&(_histHoverIndex=-1),drawHistogram(histogramBuckets,-1),updateActiveFilterSummary()}function a(i,r,d){t.innerHTML=i,t.style.display="block";const p=12;let u=r+p,c=d+p;const f=t.getBoundingClientRect(),T=window.innerWidth,b=window.innerHeight;u+f.width>T-6&&(u=r-f.width-p),c+f.height>b-6&&(c=d-f.height-p),t.style.left=u+"px",t.style.top=c+"px"}function s(i,r){const d=e.getBoundingClientRect(),p=i.clientX-d.left;if(p<0||p>d.width)return-1;const u=d.width||1;let c=Math.floor(p/u*r);return c=Math.max(0,Math.min(r-1,c)),c}e.addEventListener("mousemove",i=>{try{let E=function(g){return String(g??"").replace(/\s+/g," ").trim()},l=function(g){const w=normalizeEventTypeLabel(g);for(const[A,C]of(_eventTypeLabelByKey||new Map).entries())if(normalizeEventTypeLabel(C)===w)return A;return eventTypeKey(w)},I=function(g,w){const A=_histSourceEvents||[];if(!A.length)return[];const C=[];for(const $ of A){const _=$?.tsUtc??$?.TsUtc;_&&bucketKey(_,w)===g&&C.push($)}return C};var r=E,d=l,p=I;const u=window.histogramBuckets||histogramBuckets;if(!u||!u.length){o();return}const c=s(i,u.length);if(c<0){o();return}const f=u[c];if(!f){o();return}c!==_histHoverIndex&&(_histHoverIndex=c,drawHistogram(histogramBuckets,_histHoverIndex));const T=document.getElementById("bucketSize")?.value||"hour",b=typeof formatBucketLabel=="function"?formatBucketLabel(f.key,T):f.key,P=I(f.key,T),F=P.length,y=new Map;for(const g of P){const w=getThreadKeyForEvent(g)||"__unknown__";let A=y.get(w);if(!A){const _=threads.find(K=>K.key===w),L=_?.thread?.displayName||_?.path?.split(/[\\/]/).pop()||(w==="__unknown__"?"Unknown":w);A={tk:w,threadName:L,total:0,byType:new Map},y.set(w,A)}A.total++;const C=g?.eventType??g?.EventType??"(unknown)",$=normalizeEventTypeLabel(C);A.byType.set($,(A.byType.get($)||0)+1)}const x=Array.from(y.values()).sort((g,w)=>w.total-g.total),h=x.map(g=>g.tk);i.altKey&&h.length?(!_bucketIsolateKeys||_bucketIsolateKeys.join("|")!==h.join("|"))&&(_bucketIsolateKeys=h,setThreadAndLegendHighlights(h),drawHistogram(histogramBuckets,_histHoverIndex),updateActiveFilterSummary()):(_bucketIsolateKeys&&(_bucketIsolateKeys=null,clearThreadAndLegendHighlights(),drawHistogram(histogramBuckets,_histHoverIndex),updateActiveFilterSummary()),setThreadAndLegendHighlights(h));let m=`
  <div class="ht-title">
    ${escapeHtml(b)} • ${F} events
  </div>
`;if(!x.length)m+='<div class="hint" style="margin-top:6px">No events in this bucket.</div>';else{for(const A of x.slice(0,6)){const C=E(A.threadName),$=ensureThreadColor(A.tk);m+=`
  <div class="ht-group" style="margin-top:10px">
    <div class="ht-group-title"
         style="display:flex;justify-content:space-between;gap:10px;align-items:center">
      <div class="ht-name"
           style="display:flex;align-items:center;gap:8px">
        <span
          style="
            width:10px;
            height:10px;
            border-radius:50%;
            background:${$.fill};
            border:1px solid ${$.stroke};
            flex:0 0 auto;
          ">
        </span>
        <strong>${escapeHtml(C)}</strong>
      </div>
      <div class="ht-count">${A.total}</div>
    </div>
`;const _=Array.from(A.byType.entries()).map(([L,K])=>({k:L,v:K})).sort((L,K)=>K.v-L.v);for(const L of _.slice(0,6)){const K=E(L.k);if(_histStackMode==="type"){const J=l(L.k),j=ensureEventTypeColor(J);m+=`
          <div class="ht-row" style="margin-top:4px;opacity:0.95;display:flex;justify-content:space-between;gap:10px;align-items:center">
            <div class="ht-name" style="padding-left:10px;display:flex;align-items:center;gap:8px;min-width:0">
              <span style="
                    width:10px;height:10px;border-radius:4px;
                    background:${j.fill};
                    border:1px solid ${j.stroke};
                    flex:0 0 auto;
                "></span>
              <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
                ${escapeHtml(K)}
              </span>
            </div>
            <div class="ht-count">${L.v}</div>
          </div>
        `}else m+=`
          <div class="ht-row" style="margin-top:4px;opacity:0.95">
            <div class="ht-name" style="padding-left:10px">${escapeHtml(K)}</div>
            <div class="ht-count">${L.v}</div>
          </div>
        `}_.length>6&&(m+=`
              <div class="hint" style="margin-top:4px;padding-left:10px">
                +${_.length-6} more types…
              </div>
            `),m+="</div>"}x.length>6&&(m+=`
          <div class="hint" style="margin-top:8px">
            +${x.length-6} more threads…
          </div>
        `)}a(m,i.clientX,i.clientY)}catch(u){console.error("[TimeWeave] histogram hover error:",u),o()}}),e.addEventListener("mouseleave",o),e.addEventListener("mousedown",o)}window.addEventListener("DOMContentLoaded",()=>{wireHistogramHoverOnce();let e=0;const t=setInterval(()=>{wireHistogramHoverOnce(),e++,(_histHoverWired||e>40)&&clearInterval(t)},250)}),window.addEventListener("DOMContentLoaded",()=>{wireActiveFilterUiOnce(),updateActiveFilterSummary()}),window.addEventListener("DOMContentLoaded",()=>{updateEventTypeFilterUiVisibility()});function getActiveIsolateKeys(){return Array.isArray(_bucketIsolateKeys)&&_bucketIsolateKeys.length>0?_bucketIsolateKeys:_legendPinnedKeys&&_legendPinnedKeys.size>0?Array.from(_legendPinnedKeys):null}async function addThreads(){resetTimelineState("loading new threads"),_bucketIsolateKeys=null,_legendPinnedKeys&&_legendPinnedKeys.size&&_legendPinnedKeys.clear(),_filterLocked=!1,clearThreadAndLegendHighlights();const e=document.getElementById("threadsUi"),t=document.getElementById("eventsOut");e&&(e.textContent="Analyzing…"),t&&(t.textContent=""),threads=[],refreshTopbarStats("addThreads start");try{const o=(document.getElementById("threadPaths")?.value||"").split(/\r?\n/).map(normalizeUserPathLine).filter(Boolean),a=await fetch("/api/threads/add",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({paths:o})}),s=await a.text();if(!a.ok){e&&(e.textContent=`HTTP ${a.status}
`+s);return}let i;try{i=JSON.parse(s)}catch{e&&(e.textContent=`Expected JSON but got:
`+s);return}if(!i||!i.ok||!Array.isArray(i.results)){e&&(e.textContent=`Unexpected payload:
`+JSON.stringify(i,null,2));return}const r=readGlobalTimeOptions();threads=i.results.map(d=>{const p=d.path||"",u=getThreadStableIdFromApiResult(d);return{key:makeKeyFromHashOrPath(u,p),ok:!!d.ok,path:p,fileHashSha256:u||null,thread:d.thread||null,error:d.error||null,selectedTsKeys:(()=>{const c=d.thread?.timestampCandidates?.[0]?.columnKey??d.thread?.timestampCandidates?.[0]?.ColumnKey??null;return c?[c]:[]})(),timeOptions:{...r},lastStats:null,lastWarnings:[],lastAppliedTime:null}}),rebuildThreadPathIndex(),ensureAllThreadColors(),renderThreads(),renderHistogramLegend(),refreshTopbarStats("addThreads success"),updateActiveFilterSummary(),typeof updateRunDiagnostics=="function"&&updateRunDiagnostics("addThreads success"),queueSaveProjectState("addThreads success")}catch(n){e&&(e.textContent="Add threads failed: "+(n?.message||n))}}function renderThreads(){const e=document.getElementById("threadsUi");if(e){if(e.innerHTML="",!threads.length){e.innerHTML='<div class="hint">No threads yet. Paste paths and click Add Threads.</div>';return}for(const n of threads){let x=function(){const h=(n.timeOptions?.naiveMode||"utc")==="timezone";y&&(y.disabled=!h,y.style.opacity=h?"1":"0.6")};var t=x;Array.isArray(n.selectedTsKeys)||(n.selectedTs?n.selectedTsKeys=[n.selectedTs]:n.selectedTsKeys=[]);const o=document.createElement("div");if(o.className="card thread-card",o.dataset.threadKey=n.key,o.style.marginTop="10px",!n.ok){o.innerHTML=`
                <strong>❌ Failed</strong>
                <div class="hint">${escapeHtml(n.path)}</div>
                <pre class="mono pre">${escapeHtml(n.error||"Unknown error")}</pre>
            `,e.appendChild(o);continue}const a=ensureThreadColor(n.key),s=n._genStatus==="working"?"⏳":n._genStatus==="ok"?"✅":n._genStatus==="fail"?"⚠️":"",i=n.thread?.timestampCandidates||[],r=i.map(h=>{const m=h.columnKey??h.ColumnKey??"",g=h.columnName??h.ColumnName??m??"",w=h.confidence??h.Confidence??0;return`
                <label style="display:block;margin-top:4px">
                    <input type="checkbox"
                           name="ts_${n.key}"
                           value="${escapeHtml(m)}"
                           ${n.selectedTsKeys.includes(m)?"checked":""}>
                    ${escapeHtml(g)}
                    <span class="hint">(${Math.round(w*100)}%)</span>
                </label>
            `}).join(""),d=!i||i.length===0?`
            <div class="hint" style="margin-top:6px;color:#f6c177">
                ⚠️ No timestamp column detected. TimeWeave currently expects
                <strong>one timestamp per cell</strong>.
            </div>
        `:"",p=n.lastStats,u=p?.parsedOk??p?.ParsedOk,c=p?.nonEmptyValues??p?.NonEmptyValues,f=p?.failed??p?.Failed,T=p?.ambiguousDate??p?.AmbiguousDate,b=p?.hasTimezone??p?.HasTimezone,E=p?`
            <div class="hint" style="margin-top:8px;">
                Parsed: ${u}/${c} •
                Failed: ${f} •
                Ambiguous: ${T} •
                TZ present: ${b}/${c}
            </div>
            ${n.lastWarnings?.length?`<div class="hint" style="margin-top:6px;">⚠️ ${escapeHtml(n.lastWarnings.join(" | "))}</div>`:""}
        `:"";n.timeOptions||(n.timeOptions=readGlobalTimeOptions());const l=n.timeOptions,I=`
            <div style="margin-top:10px;padding-top:10px;border-top:1px solid rgba(255,255,255,0.08);">
                <div class="hint">Time parsing (this thread)</div>

                <div style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;margin-top:6px">
                    <div>
                        <label class="hint">Date order</label><br/>
                        <select class="mono" id="t_dateOrder_${n.key}">
                            <option value="DMY" ${l.dateOrder==="DMY"?"selected":""}>DMY</option>
                            <option value="MDY" ${l.dateOrder==="MDY"?"selected":""}>MDY</option>
                            <option value="YMD" ${l.dateOrder==="YMD"?"selected":""}>YMD</option>
                            <option value="AUTO" ${l.dateOrder==="AUTO"?"selected":""}>AUTO</option>
                        </select>
                    </div>

                    <div>
                        <label class="hint">Naive mode</label><br/>
                        <select class="mono" id="t_naiveMode_${n.key}">
                            <option value="utc" ${l.naiveMode==="utc"?"selected":""}>Assume UTC</option>
                            <option value="local" ${l.naiveMode==="local"?"selected":""}>Assume Local</option>
                            <option value="timezone" ${l.naiveMode==="timezone"?"selected":""}>Assume Timezone</option>
                        </select>
                    </div>

                    <div style="min-width:260px;flex:1">
                        <label class="hint">Timezone</label><br/>
                        <select class="mono" id="t_tz_${n.key}" style="width:100%">
                            ${buildTimezoneOptionsHtml(l.sourceTimeZoneId)}
                        </select>
                    </div>
                </div>
            </div>
        `;o.innerHTML=`
            <strong style="display:flex;align-items:center;gap:8px">
                <span class="thread-swatch" style="background:${a.fill};border-color:${a.stroke}"></span>
                <span>${s} ${escapeHtml(n.thread?.displayName||"(csv)")}</span>
            </strong>

            <div class="hint">${escapeHtml(n.path)}</div>

            <div style="margin-top:6px">
                <div class="hint">
                    Timestamp columns:
                    <strong>${n.selectedTsKeys.length} selected</strong>
                </div>
                ${r||'<div class="hint">No timestamp columns detected</div>'}
                ${d}
            </div>

            ${I}
            ${E}
        `,o.querySelectorAll(`input[name="ts_${n.key}"]`).forEach(h=>{h.addEventListener("change",()=>{const m=h.value;h.checked?n.selectedTsKeys.includes(m)||n.selectedTsKeys.push(m):n.selectedTsKeys=n.selectedTsKeys.filter(g=>g!==m),n.selectedTsKeys=[...new Set(n.selectedTsKeys)],queueSaveProjectState("thread ts selection changed")})});const P=o.querySelector(`#t_dateOrder_${n.key}`),F=o.querySelector(`#t_naiveMode_${n.key}`),y=o.querySelector(`#t_tz_${n.key}`);P?.addEventListener("change",()=>{n.timeOptions.dateOrder=P.value.toUpperCase(),queueSaveProjectState("thread timeOptions changed")}),F?.addEventListener("change",()=>{n.timeOptions.naiveMode=F.value.toLowerCase(),x(),queueSaveProjectState("thread timeOptions changed")}),y?.addEventListener("change",()=>{n.timeOptions.sourceTimeZoneId=y.value||null,queueSaveProjectState("thread timeOptions changed")}),x(),e.appendChild(o)}}}function escapeHtml(e){return String(e).replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;")}let _timelineClickEvents=[],_selectedTimelineKey=null;function findThreadDisplayNameByKey(e){const t=(threads||[]).find(n=>n?.key===e);return t?.thread?.displayName||t?.path?.split(/[\\/]/).pop()||(e==="__unknown__"?"Unknown":e)}function setSelectedTimelineDom(e){_selectedTimelineKey=e||null,document.querySelectorAll("#timeline .tl-item[data-ev-key]").forEach(t=>{t.dataset.evKey===_selectedTimelineKey?t.classList.add("is-selected"):t.classList.remove("is-selected")})}function showEventDetailsPanel(){const e=document.getElementById("eventDetails");e&&(e.style.display="block")}function hideEventDetailsPanel(){const e=document.getElementById("eventDetails");e&&(e.style.display="none"),_selectedTimelineKey=null,setSelectedTimelineDom(null)}document.getElementById("btn-ed-close")?.addEventListener("click",hideEventDetailsPanel);function renderEventDetails(e){const t=document.getElementById("eventDetailsBody");if(!t)return;const n=e?.tsUtc||"",o=e?.title||"",a=e?.sourcePath||"",s=e?.rowIndex??"",i=e?.eventType||"(timestamp)",r=e?.threadKey||getThreadKeyForEvent(e)||"__unknown__",d=findThreadDisplayNameByKey(r),p=ensureThreadColor(r);t.innerHTML=`
        <div class="ed-row">
            <span class="ed-k">Thread</span>
            <span class="ed-v" style="display:flex;gap:8px;align-items:center;justify-content:flex-end">
                <span class="thread-swatch" style="background:${p.fill};border-color:${p.stroke}"></span>
                ${escapeHtml(d)}
            </span>
        </div>

        <div class="ed-row"><span class="ed-k">Timestamp (UTC)</span><span class="ed-v">${escapeHtml(n)}</span></div>
        <div class="ed-row"><span class="ed-k">Event type</span><span class="ed-v">${escapeHtml(i)}</span></div>
        <div class="ed-row"><span class="ed-k">Row</span><span class="ed-v">${escapeHtml(String(s))}</span></div>

        <div>
            <div class="hint" style="margin-bottom:6px">Title</div>
            <div class="ed-pre">${escapeHtml(o)}</div>
        </div>

        <div>
            <div class="hint" style="margin-bottom:6px">Source</div>
            <div class="ed-pre mono">${escapeHtml(a)}</div>
        </div>
    `}function wireTimelineItemClick(e,t){if(!e||!t)return;const n=joinKeyStrict(t);e.dataset.evKey=n,e.addEventListener("click",o=>{o.preventDefault(),o.stopPropagation(),showEventDetailsPanel(),setSelectedTimelineDom(n),renderEventDetails(t)})}function _pick(e,t){for(const n of t)if(e&&e[n]!==void 0&&e[n]!==null)return e[n];return null}function _threadDisplayName(e){return e?.thread?.displayName||e?.path?.split(/[\\/]/).pop()||e?.path||"(thread)"}function _escapeLines(e){return(e||[]).map(t=>escapeHtml(String(t))).join("<br/>")}function computeRunDiagnosticsSnapshot(){const e=(threads||[]).filter(m=>m?.ok),t=(threads||[]).filter(m=>m&&!m.ok),n=e.reduce((m,g)=>m+(Array.isArray(g.selectedTsKeys)?g.selectedTsKeys.length:0),0),o=(allGeneratedEvents||[]).length,a=Array.isArray(_histSourceEvents)?_histSourceEvents.length:0,s=[],i=[],r=[];let d=0,p=0,u=0,c=0,f=0,T=0;function b(m){const g=m?.lastStats;if(!g)return null;const w=_pick(g,["parsedOk","ParsedOk","ok","Ok","parsed","Parsed"]),A=_pick(g,["nonEmptyValues","NonEmptyValues","nonEmpty","NonEmpty","total","Total"]),C=_pick(g,["failed","Failed","fail","Fail"]),$=_pick(g,["ambiguousDate","AmbiguousDate","ambiguous","Ambiguous"]),_=_pick(g,["hasTimezone","HasTimezone","tzPresent","TzPresent"]),L={parsedOk:w!=null?Number(w):null,nonEmpty:A!=null?Number(A):null,failed:C!=null?Number(C):null,ambiguous:$!=null?Number($):null,hasTz:_!=null?Number(_):null};return Number.isFinite(L.parsedOk)||Number.isFinite(L.nonEmpty)||Number.isFinite(L.failed)||Number.isFinite(L.ambiguous)||Number.isFinite(L.hasTz)?L:null}for(const m of e){m?.lastWarnings?.length&&s.push({t:m,warnings:m.lastWarnings}),m?._genStatus==="fail"&&r.push(m);const g=m?.key||"";g&&o&&Array.isArray(m.selectedTsKeys)&&m.selectedTsKeys.length&&(allGeneratedEvents||[]).reduce((C,$)=>C+($?.threadKey===g?1:0),0)===0&&i.push(m);const w=b(m);w&&(w.parsedOk!=null&&(d+=w.parsedOk),w.nonEmpty!=null&&(p+=w.nonEmpty),w.failed!=null&&(u+=w.failed),w.ambiguous!=null&&(c+=w.ambiguous),w.hasTz!=null&&(f+=w.hasTz),T++)}const E=(document.getElementById("fromUtc")?.value||"").trim(),l=(document.getElementById("toUtc")?.value||"").trim(),I=(document.getElementById("bucketSize")?.value||"hour").trim(),P=!!document.getElementById("collapseSameTs")?.checked,F=!!document.getElementById("filterByEventType")?.checked;let y=null,x=null;const h=document.querySelectorAll("#eventTypeFiltersBody .evtTypeChk");return h&&h.length&&(x=h.length,y=Array.from(h).filter(m=>m.checked).length),{threads:{ok:e.length,failed:t.length,total:(threads||[]).length},timestampsSelected:n,generated:o,visible:a,stats:T?{parsedOk:d,nonEmpty:p,failed:u,ambiguous:c,hasTz:f,threadsWithStats:T}:null,time:{from:E,to:l,bucketSize:I,collapseSameTs:P},eventTypeFilter:{enabled:F,selected:y,total:x},warnings:s,genFailures:r,zeroEventThreads:i,threadFailures:t}}function renderRunDiagnostics(){const e=document.getElementById("runDiag");if(!e)return;const t=computeRunDiagnosticsSnapshot(),n=t.time.from||t.time.to?`${escapeHtml(t.time.from||"…")} → ${escapeHtml(t.time.to||"…")}`:"All time",o=t.eventTypeFilter.enabled?t.eventTypeFilter.total!=null?`${t.eventTypeFilter.selected}/${t.eventTypeFilter.total} types`:"Enabled":"Off",a=t.stats?`Parsed: ${t.stats.parsedOk}/${t.stats.nonEmpty} • Failed: ${t.stats.failed} • Ambiguous: ${t.stats.ambiguous} • TZ present: ${t.stats.hasTz}/${t.stats.nonEmpty}`:"Parsing stats: —";function s(c,f,T){if(!c||!c.length)return"";const b=c.slice(0,8).map(l=>`<div style="margin-top:6px">${T(l)}</div>`).join(""),E=c.length>8?`<div class="hint" style="margin-top:6px">+${c.length-8} more…</div>`:"";return`
          <div style="margin-top:10px">
            <div class="hint"><strong>${escapeHtml(f)}</strong></div>
            ${b}
            ${E}
          </div>
        `}const i=s(t.warnings,"Warnings",c=>{const f=c.t;return`
              <div style="padding:8px;border-radius:10px;background:rgba(0,0,0,0.12);border:1px solid rgba(255,255,255,0.06)">
                <div><strong>${escapeHtml(_threadDisplayName(f))}</strong></div>
                <div class="hint" style="margin-top:4px">${_escapeLines(c.warnings)}</div>
              </div>
            `}),r=s(t.genFailures,"Generate failures",c=>`
          <div style="padding:8px;border-radius:10px;background:rgba(0,0,0,0.12);border:1px solid rgba(255,255,255,0.06)">
            <div><strong>${escapeHtml(_threadDisplayName(c))}</strong></div>
            <div class="hint" style="margin-top:4px">${_escapeLines(c.lastWarnings||["Generate failed"])}</div>
          </div>
        `),d=s(t.zeroEventThreads,"Zero events (selected timestamps produced none)",c=>`
          <div style="padding:8px;border-radius:10px;background:rgba(0,0,0,0.12);border:1px solid rgba(255,255,255,0.06)">
            <div><strong>${escapeHtml(_threadDisplayName(c))}</strong></div>
            <div class="hint" style="margin-top:4px">
              Selected: ${escapeHtml((c.selectedTsKeys||[]).join(", ")||"—")}
            </div>
          </div>
        `),p=s(t.threadFailures,"Thread load failures",c=>`
          <div style="padding:8px;border-radius:10px;background:rgba(0,0,0,0.12);border:1px solid rgba(255,255,255,0.06)">
            <div><strong>${escapeHtml(_threadDisplayName(c))}</strong></div>
            <div class="hint" style="margin-top:4px">${escapeHtml(c.error||"Failed to load")}</div>
          </div>
        `),u=t.warnings.length||t.genFailures.length||t.zeroEventThreads.length||t.threadFailures.length;e.innerHTML=`
      <div style="display:flex;gap:14px;flex-wrap:wrap;align-items:center">
        <div><strong>Threads:</strong> ${t.threads.ok}/${t.threads.total}${t.threads.failed?` <span class="hint">(${t.threads.failed} failed)</span>`:""}</div>
        <div><strong>Timestamps:</strong> ${t.timestampsSelected}</div>
        <div><strong>Generated:</strong> ${t.generated}</div>
        <div><strong>Visible:</strong> ${t.visible}${t.generated?' <span class="hint">(after filters)</span>':""}</div>
      </div>

      <div class="hint" style="margin-top:8px">
        <strong>Time:</strong> ${n} •
        <strong>Bucket:</strong> ${escapeHtml(t.time.bucketSize)} •
        <strong>Collapse:</strong> ${t.time.collapseSameTs?"On":"Off"} •
        <strong>Event types:</strong> ${escapeHtml(o)}
      </div>

      <div class="hint" style="margin-top:8px">${escapeHtml(a)}</div>

      ${u?`
        <details style="margin-top:10px">
          <summary class="hint" style="cursor:pointer">Details</summary>
          ${i}
          ${r}
          ${d}
          ${p}
        </details>
      `:`
        <div class="hint" style="margin-top:10px">No warnings or failures detected.</div>
      `}
    `}function updateRunDiagnostics(e=""){try{renderRunDiagnostics()}catch(t){console.warn("[TimeWeave] Diagnostics render failed:",e,t)}}function buildLastRunSummary(e="manual"){const t=(document.getElementById("fromUtc")?.value||"").trim(),n=(document.getElementById("toUtc")?.value||"").trim(),o=(document.getElementById("bucketSize")?.value||"hour").trim(),a=(threads||[]).filter(i=>i?.ok),s=a.reduce((i,r)=>i+(Array.isArray(r.selectedTsKeys)?r.selectedTsKeys.length:0),0);return{v:1,runAtUtc:new Date().toISOString(),source:e,threads:{total:(threads||[]).length,ok:a.length,failed:(threads||[]).filter(i=>i&&!i.ok).length},timestampsSelected:s,counts:{generated:(allGeneratedEvents||[]).length,merged:(mergedEvents||[]).length,visible:Array.isArray(_histSourceEvents)?_histSourceEvents.length:0},ui:{timeRange:{fromUtc:t||null,toUtc:n||null},bucketSize:o,collapseSameTs:!!document.getElementById("collapseSameTs")?.checked,eventTypeFilterEnabled:!!document.getElementById("filterByEventType")?.checked},filters:snapshotFiltersState(),perThread:a.map(i=>({key:i.key,path:i.path,displayName:i?.thread?.displayName||null,selectedTsKeys:Array.isArray(i.selectedTsKeys)?[...i.selectedTsKeys]:[],timeOptions:i.timeOptions?{...i.timeOptions}:null,stats:i.lastStats||null,warnings:i.lastWarnings||[]}))}}let _activeHoverThreadKeys=null;function getThreadCardMap(){const e=document.querySelectorAll(".thread-card[data-thread-key]"),t=new Map;for(const n of e){const o=n.dataset.threadKey;o&&t.set(o,n)}return t}function getLegendItemMap(){const e=document.querySelectorAll(".hist-legend-item[data-thread-key]"),t=new Map;for(const n of e){const o=n.dataset.threadKey;o&&t.set(o,n)}return t}function clearThreadAndLegendHighlights(){_activeHoverThreadKeys=null,document.querySelectorAll(".thread-card").forEach(e=>e.classList.remove("is-hot","is-dim")),document.querySelectorAll(".hist-legend-item").forEach(e=>e.classList.remove("is-hot","is-dim"))}function setThreadAndLegendHighlights(e){const t=Array.from(new Set((e||[]).filter(Boolean)));if(_activeHoverThreadKeys=t,!t.length){clearThreadAndLegendHighlights();return}const n=getThreadCardMap(),o=getLegendItemMap();document.querySelectorAll(".thread-card").forEach(a=>{a.classList.remove("is-hot"),a.classList.add("is-dim")}),document.querySelectorAll(".hist-legend-item").forEach(a=>{a.classList.remove("is-hot"),a.classList.add("is-dim")});for(const a of t){const s=n.get(a);s&&(s.classList.remove("is-dim"),s.classList.add("is-hot"));const i=o.get(a);i&&(i.classList.remove("is-dim"),i.classList.add("is-hot"))}}function clearThreadCardHighlights(){clearThreadAndLegendHighlights()}function setThreadCardHighlights(e){setThreadAndLegendHighlights(e)}let _legendPinnedKeys=new Set;function setLegendIsolate(e){e&&(_legendPinnedKeys.size>0||(setThreadAndLegendHighlights([e]),Array.isArray(histogramBuckets)&&drawHistogram(histogramBuckets),updateActiveFilterSummary()))}function clearLegendIsolate(){_legendPinnedKeys.size>0||(clearThreadAndLegendHighlights(),Array.isArray(histogramBuckets)&&drawHistogram(histogramBuckets),updateActiveFilterSummary())}function toggleLegendPin(e,t){t?_legendPinnedKeys.has(e)?_legendPinnedKeys.delete(e):_legendPinnedKeys.add(e):_legendPinnedKeys.has(e)?_legendPinnedKeys.clear():(_legendPinnedKeys.clear(),_legendPinnedKeys.add(e)),_legendPinnedKeys.size>0?setThreadAndLegendHighlights([..._legendPinnedKeys]):clearThreadAndLegendHighlights(),Array.isArray(histogramBuckets)&&drawHistogram(histogramBuckets),updateActiveFilterSummary(),queueSaveProjectState("legend pin changed")}function buildTimestampLabelsForThread(e){const t={},n=e?.thread?.timestampCandidates||[];for(const o of n){const a=o.columnKey??o.ColumnKey;if(!a)continue;const s=o.columnName??o.ColumnName??a;t[a]=s}return t}function normalizeApiEvent(e,t){const n=t?._tsLabelMap||buildTimestampLabelsForThread(t);t&&!t._tsLabelMap&&(t._tsLabelMap=n);const o=(e?.TsUtc??e?.tsUtc??e?.TSUtc??e?.ts_utc??"").toString().trim(),a=(e?.EventTypeKey??e?.eventTypeKey??e?.TimestampColumnKey??e?.timestampColumnKey??e?.ColumnKey??e?.columnKey??"").toString().trim(),s=(e?.EventType??e?.eventType??"").toString().trim(),i=a&&n&&n[a]?String(n[a]):"",r=(s||i||a||"(timestamp)").trim(),d=a&&a.trim()?a.trim():eventTypeKey(r);return{threadKey:t?.key||"",tsUtc:o,title:e?.Title??e?.title??"(event)",rowIndex:e?.RowIndex??e?.rowIndex??0,sourcePath:e?.SourcePath??e?.sourcePath??(t?.path||""),eventType:r,eventTypeKey:d}}async function generateEvents(){const e=document.getElementById("eventsOut"),t=document.getElementById("applyGlobalTimeStatus");e&&(e.textContent="Generating…"),t&&(t.textContent="Generating…"),allGeneratedEvents=[],refreshTopbarStats("generateEvents start");const n=threads.filter(r=>r&&r.ok&&Array.isArray(r.selectedTsKeys)&&r.selectedTsKeys.length);if(!n.length){const r="Please select at least one timestamp column before generating events.";e&&(e.textContent=r),t&&(t.textContent=r),alert(r);return}const o=n.length;let a=0;for(const r of n)r._genStatus=null;renderThreads();for(const r of n){const d=(r.thread?.displayName||r.path||"(csv)").split(/[\\/]/).pop(),p=`Generating ${a+1}/${o}… ${d}`;e&&(e.textContent=p),t&&(t.textContent=p),r._genStatus="working",renderThreads();const u=r.timeOptions||readGlobalTimeOptions();r._tsLabelMap=buildTimestampLabelsForThread(r);const c={path:r.path,timestampColumnKeys:r.selectedTsKeys,timestampLabels:buildTimestampLabelsForThread(r),limit:5e3,dateOrder:(u.dateOrder||"DMY").trim().toUpperCase(),naiveMode:(u.naiveMode||"utc").trim().toLowerCase(),sourceTimeZoneId:u.sourceTimeZoneId||null};try{const T=await(await fetch("/api/events/from-csv",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(c)})).text();let b;try{b=JSON.parse(T)}catch{b={ok:!1,error:T}}if(r.lastStats=extractParsingStats(b),r.lastWarnings=b.warnings||[],r.lastAppliedTime=b.appliedTime||null,b.ok&&!r.lastStats&&console.warn("[TimeWeave] No parsing stats returned for thread:",r.path,b),b.ok&&Array.isArray(b.events)){const E=b.events.map(l=>normalizeApiEvent(l,r));allGeneratedEvents.push(...E),r._genStatus="ok",refreshTopbarStats("generateEvents progress")}else r._genStatus="fail",r.lastWarnings=r.lastWarnings.length?r.lastWarnings:[b.error||"Generate failed"]}catch(f){r._genStatus="fail",r.lastWarnings=[String(f?.message||f)]}a++,renderThreads()}const s=`Generated ${allGeneratedEvents.length} events from ${o} thread(s)`;e&&(e.textContent=s),t&&(t.textContent=s),rebuildThreadPathIndex(),ensureAllThreadColors(),renderHistogramLegend(),renderEventTypeFiltersFromAllEvents();const i=_restoredUiSnapshot||loadProjectStateRaw()?.ui||null;i?.eventTypeChecked?.length&&applyEventTypeSelections(i),updateEventTypeFilterUiVisibility(),typeof updateRunDiagnostics=="function"&&updateRunDiagnostics("generateEvents complete"),queueSaveProjectState("generateEvents complete"),refreshTopbarStats("generateEvents complete")}async function mergeTimeline(){const e=document.getElementById("timeline"),t=document.getElementById("timelineStats");if(!e||!t){alert("Timeline UI elements not found (timelineStats/timeline). Check index.html.");return}if(e.innerHTML="",t.textContent="Merging…",!allGeneratedEvents||allGeneratedEvents.length===0){t.textContent="No generated events yet. Click Generate Events first.";return}const n={events:allGeneratedEvents,fromUtc:(document.getElementById("fromUtc")?.value||"").trim()||null,toUtc:(document.getElementById("toUtc")?.value||"").trim()||null};try{const o=await fetch("/api/timeline/merge",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(n)}),a=await o.text();if(!o.ok){t.textContent=`Merge failed (HTTP ${o.status})`,e.innerHTML=`<pre class="mono pre">${escapeHtml(a)}</pre>`;return}let s;try{s=JSON.parse(a)}catch{t.textContent="Merge failed: expected JSON but got:",e.innerHTML=`<pre class="mono pre">${escapeHtml(a)}</pre>`;return}if(!s.ok){t.textContent="Merge failed:",e.innerHTML=`<pre class="mono pre">${escapeHtml(JSON.stringify(s,null,2))}</pre>`;return}mergedEvents=rehydrateMergedEvents(Array.isArray(s.events)?s.events:[]);const i=applyEventTypeFilterToEvents(mergedEvents);_histSourceEvents=i;const r=new Set;for(const u of i||[]){const c=getThreadKeyForEvent(u)||u.threadKey||"__unknown__";r.add(c)}_histContribThreadKeys=Array.from(r),_timelineClickEvents=i,t.textContent=`Events: ${i.length}`,rebuildThreadPathIndex(),ensureAllThreadColors();const d=document.getElementById("bucketSize")?.value||"hour";if(histogramBuckets=buildHistogram(i,d),window.histogramBuckets=histogramBuckets,drawHistogram(histogramBuckets,_histHoverIndex),renderHistogramLegend(),!!document.getElementById("collapseSameTs")?.checked){const u=groupByExactTimestamp(i);for(const c of u){const f=document.createElement("div");f.className="tl-item";const T=c.items.length,b=T===1?escapeHtml(c.items[0].title||""):`${T} events`;f.innerHTML=`
                    <div class="tl-ts">${c.tsUtc}</div>

                    <details ${T<=1?"open":""} style="margin-top:4px">
                        <summary class="tl-title" style="cursor:pointer">
                            ${b}
                            ${T>1?'<span class="hint"> • click to expand</span>':""}
                        </summary>

                        ${T>1?`
                            <div style="margin-top:8px;display:flex;flex-direction:column;gap:6px">
                                ${c.items.map(E=>`
                                <div class="tl-item tl-subitem" data-subkey="${escapeHtml(joinKeyStrict(E))}"
                                style="padding:8px;border-radius:10px;background:rgba(0,0,0,0.15);border:1px solid rgba(255,255,255,0.06);margin-top:6px">
                                <div class="tl-title">${escapeHtml(E.title||"")}</div>
                                <div class="tl-src">${escapeHtml(E.sourcePath||"")} • row ${escapeHtml(String(E.rowIndex))}</div>
                                </div>
                        `).join("")}
                            </div>
                        `:`
                            <div class="tl-src" style="margin-top:6px">
                                ${escapeHtml(c.items[0].sourcePath||"")} • row ${escapeHtml(c.items[0].rowIndex)}
                            </div>
                        `}
                    </details>
                `,e.appendChild(f),T===1?wireTimelineItemClick(f,c.items[0]):f.querySelectorAll(".tl-subitem[data-subkey]").forEach(E=>{const l=E.getAttribute("data-subkey"),I=c.items.find(P=>joinKeyStrict(P)===l);I&&wireTimelineItemClick(E,I)})}}else for(const u of i){const c=document.createElement("div");c.className="tl-item",c.innerHTML=`
            <div class="tl-ts">${escapeHtml(u.tsUtc)}</div>
            <div class="tl-title">${escapeHtml(u.title||"")}</div>
            <div class="tl-src">${escapeHtml(u.sourcePath||"")} • row ${escapeHtml(String(u.rowIndex))}</div>
        `,wireTimelineItemClick(c,u),e.appendChild(c)}updateActiveFilterSummary(),queueSaveProjectState("mergeTimeline complete"),typeof updateRunDiagnostics=="function"&&updateRunDiagnostics("mergeTimeline complete")}catch(o){t.textContent="Merge error: "+(o?.message||o)}}function bucketKey(e,t){const n=new Date(e);return t==="day"?n.toISOString().slice(0,10):t==="hour"?n.toISOString().slice(0,13):n.toISOString().slice(0,16)}function formatBucketLabel(e,t){return t==="day"?e+" UTC":t==="hour"?e.replace("T"," ")+":00 UTC":e.replace("T"," ")+" UTC"}function buildHistogram(e,t){const n=new Map,o=new Map;for(const s of e){const i=bucketKey(s.tsUtc,t);let r=n.get(i);r||(r={key:i,total:0,byThread:{},byType:{}},n.set(i,r));const d=getSeriesKeyForEvent(s),p=normalizeEventTypeLabel(s.eventType??s.EventType),u=(s.eventTypeKey??s.EventTypeKey??"").toString().trim()||eventTypeKey(p);_eventTypeLabelByKey.set(u,p),r.total++,r.byThread[d]=(r.byThread[d]||0)+1,r.byType[u]=(r.byType[u]||0)+1,o.set(u,(o.get(u)||0)+1)}const a=Array.from(n.entries()).sort((s,i)=>s[0].localeCompare(i[0])).map(([s,i])=>({key:s,total:i.total,byThread:i.byThread,byType:i.byType}));for(const s of a)s.count=s.total;return _histEventTypeOrder=Array.from(o.entries()).sort((s,i)=>i[1]-s[1]).map(([s])=>s),a}let _histMode=null;function drawHistogram(e,t=-1){const n=document.getElementById("histogram"),o=document.getElementById("histogramStats");if(!n)return;const a=document.getElementById("bucketSize")?.value||"hour",s=n.closest(".histogram-body")||n.parentElement,i=s&&s.clientWidth?s.clientWidth:window.innerWidth,r=n.getBoundingClientRect(),d=r.height>0?r.height:320,p=e&&e.length>0,c=p?e.length*4:0,T=c<=i+1?"fit":"scroll";if(_histMode!==T)T==="fit"?(s&&(s.style.overflowX="hidden",s.scrollLeft=0),n.style.width="100%",n.style.maxWidth="100%",n.style.minWidth="0"):(s&&(s.style.overflowX="auto"),n.style.width=Math.max(i,c)+"px",n.style.maxWidth="none",n.style.minWidth="0"),n.style.height=d+"px",_histMode=T;else if(n.style.height=d+"px",_histMode==="scroll"){const v=Math.max(i,c),k=parseFloat(n.style.width)||i;Math.abs(k-v)>1&&(n.style.width=v+"px")}const b=_histMode==="fit"?i:Math.max(i,c),E=window.devicePixelRatio||1;n.width=Math.max(1,Math.floor(b*E)),n.height=Math.max(1,Math.floor(d*E));const l=n.getContext("2d");if(l.setTransform(E,0,0,E,0,0),l.clearRect(0,0,b,d),!p){o&&(o.textContent="No events");return}function I(v,k,S,M,B,O,R,X,W){const z=Math.max(0,Math.min(O||0,M/2,B/2)),U=Math.max(0,Math.min(R||0,M/2,B/2)),q=Math.max(0,Math.min(X||0,M/2,B/2)),G=Math.max(0,Math.min(W||0,M/2,B/2));v.beginPath(),v.moveTo(k+z,S),v.lineTo(k+M-U,S),U?v.quadraticCurveTo(k+M,S,k+M,S+U):v.lineTo(k+M,S),v.lineTo(k+M,S+B-q),q?v.quadraticCurveTo(k+M,S+B,k+M-q,S+B):v.lineTo(k+M,S+B),v.lineTo(k+G,S+B),G?v.quadraticCurveTo(k,S+B,k,S+B-G):v.lineTo(k,S+B),v.lineTo(k,S+z),z?v.quadraticCurveTo(k,S,k+z,S):v.lineTo(k,S),v.closePath()}function P(v,k,S,M){l.save(),l.globalAlpha=.18,l.lineWidth=1,l.strokeStyle="rgba(255,255,255,0.12)";const B=4;for(let O=1;O<=B;O++){const R=k+M*O/(B+1);l.beginPath(),l.moveTo(v,R),l.lineTo(v+S,R),l.stroke()}l.restore()}function F(v,k,S,M,B,O){if(M<=.5||S<=.5)return;const R=!!O?.isTop,X=!!O?.isBottom,W=!!O?.dim,z=S<=4,U=M<=6,q=z||U,G=M<=3,D=Math.min(7,S*.4,M*.5),H=R?D:0,V=R?D:0,Y=X?D:0,te=X?D:0,ae=W||q||G?0:8,re=W?.18:z?.92:.96,ie=W?.1:z?.12:.26,Z=l.createLinearGradient(0,k,0,k+M);if(q?(Z.addColorStop(0,B),Z.addColorStop(1,B)):(Z.addColorStop(0,B),Z.addColorStop(1,"rgba(0,0,0,0.22)")),l.save(),l.shadowBlur=ae,l.shadowColor=B,l.shadowOffsetY=0,l.globalAlpha=re,l.fillStyle=Z,G){l.fillRect(v,k,S,M),l.restore();return}if(I(l,v,k,S,M,H,V,te,Y),l.fill(),l.shadowBlur=0,l.globalAlpha=ie,l.lineWidth=1,I(l,v+.5,k+.5,Math.max(0,S-1),Math.max(0,M-1),H,V,te,Y),l.strokeStyle="rgba(255,255,255,0.35)",l.stroke(),!R&&!z&&M>=8&&(l.globalAlpha=W?.1:.18,l.beginPath(),l.moveTo(v+1,k+.5),l.lineTo(v+S-1,k+.5),l.strokeStyle="rgba(0,0,0,0.45)",l.stroke()),!W&&!z&&M>=14){const pe=Math.min(10,M*.25),le=l.createLinearGradient(0,k,0,k+pe);le.addColorStop(0,"rgba(255,255,255,0.30)"),le.addColorStop(1,"rgba(255,255,255,0)"),l.globalAlpha=.45,I(l,v+1,k+1,Math.max(0,S-2),pe,Math.max(0,H-2),Math.max(0,V-2),0,0),l.fillStyle=le,l.fill()}l.restore()}const y=10,x=28,h=Math.max(...e.map(v=>v.total??v.count??0)),m=Math.max(1,d-y-x);P(0,y,b,m),l.save(),l.strokeStyle="rgba(255,255,255,0.12)",l.lineWidth=1,l.beginPath(),l.moveTo(0,d-x+.5),l.lineTo(b,d-x+.5),l.stroke(),l.restore();const g=b/e.length,w=Math.max(1,Math.floor(g*.18)),A=Math.max(3,Math.floor(g-w)),C=Math.log10(h+1)||1,$=_histContribThreadKeys?.length||0,_=_histEventTypeOrder?.length||0,J=A>=18&&$<=1&&_>=2&&_<=12?"type":"thread";_histStackMode!==J&&(_histStackMode=J,typeof renderHistogramLegend=="function"&&renderHistogramLegend());let j=[];if(_histStackMode==="thread"){for(const v of threads||[])v?.ok&&j.push(v.key);j.push("__unknown__")}else j=(_histEventTypeOrder||[]).slice();const Q=getActiveIsolateKeys?.()||null,ne=Array.isArray(Q)&&Q.length>0,ee=Number.isFinite(t)&&t>=0&&t<e.length,oe=.82;let N=null;for(let v=0;v<e.length;v++){const k=e[v],S=k.total??k.count??0,M=Math.floor(v*g+w/2),B=Math.max(1,A),O=Math.log10(S+1)/C*m;if(S<=0||O<=.5)continue;const R=ee&&v===t,X=!ee||R?1:oe;let W=d-x,z=!1;const U=[],q=_histStackMode==="type"?k.byType||{}:k.byThread||{};for(const D of j)q?.[D]&&U.push(D);const G=U.length===1;for(let D=0;D<U.length;D++){const H=U[D],V=q?.[H]||0;if(!V)continue;const Y=O*(V/S);if(Y<=.25)continue;const te=_histStackMode==="type"?ensureEventTypeColor(H):ensureThreadColor(H),ae=_histStackMode==="thread"&&ne&&!Q.includes(H),re=D===0,ie=D===U.length-1,Z=!G&&re;l.save(),l.globalAlpha=X,F(M,W-Y,B,Y,te.fill,{isTop:ie,isBottom:Z,dim:ae}),l.restore(),z=!0,W-=Y}if(z){l.save(),l.globalAlpha=.22*X,l.lineWidth=1;const D=d-x-O,H=Math.min(7,B*.4,O*.5);I(l,M+.5,D+.5,Math.max(0,B-1),Math.max(0,O-1),H,H,0,0),l.strokeStyle="rgba(255,255,255,0.25)",l.stroke(),l.restore()}R&&(N={x:M,w:B,yTop:d-x-O,h:O})}if(N&&N.h>2){l.save(),l.shadowBlur=10,l.shadowColor="rgba(255,255,255,0.25)",l.shadowOffsetY=0,l.globalAlpha=.95,l.lineWidth=2;const v=Math.min(9,N.w*.45,N.h*.4);I(l,N.x+1,N.yTop+1,Math.max(0,N.w-2),Math.max(0,N.h-2),v,v,0,0),l.strokeStyle="rgba(255,255,255,0.28)",l.stroke(),l.shadowBlur=0,l.globalAlpha=.7,l.lineWidth=1,I(l,N.x+.5,N.yTop+.5,Math.max(0,N.w-1),Math.max(0,N.h-1),v,v,0,0),l.strokeStyle="rgba(255,255,255,0.35)",l.stroke(),l.restore()}const ce=s?s.scrollLeft:0,de=s?s.scrollLeft+s.clientWidth:b,ue=(ce+de)/2;l.fillStyle="rgba(255,255,255,0.78)",l.font="12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",l.textBaseline="bottom";const me=formatBucketLabel(e[0].key,a),fe=formatBucketLabel(e[e.length-1].key,a);let se=Math.floor(ue/(b/e.length));se=Math.max(0,Math.min(e.length-1,se));const ge=formatBucketLabel(e[se].key,a);l.textAlign="left",l.fillText(me,Math.max(6,ce+6),d-6),l.textAlign="center",l.fillText(ge,ue,d-6),l.textAlign="right",l.fillText(fe,Math.min(b-6,de-6),d-6),o&&(o.textContent=`${e.length} buckets • max ${h} events • click-drag to zoom`)}(function(){const t=document.getElementById("histogram");if(!t)return;const n=document.getElementById("histogramStats"),o=t.closest(".histogram-body")||t.parentElement;if(!o)return;getComputedStyle(o).position==="static"&&(o.style.position="relative");let s=document.getElementById("histogramOverlay");s||(s=document.createElement("canvas"),s.id="histogramOverlay",s.style.position="absolute",s.style.pointerEvents="none",s.style.borderRadius="8px",o.appendChild(s));function i(){const y=window.devicePixelRatio||1;s.style.left=(t.offsetLeft||0)+"px",s.style.top=(t.offsetTop||0)+"px",s.style.width=(t.offsetWidth||1)+"px",s.style.height=(t.offsetHeight||1)+"px",s.width=Math.max(1,Math.floor((t.offsetWidth||1)*y)),s.height=Math.max(1,Math.floor((t.offsetHeight||1)*y))}function r(){i();const y=s.getContext("2d");y.setTransform(1,0,0,1,0,0),y.clearRect(0,0,s.width,s.height)}function d(){return Math.max(1,t.offsetWidth||t.getBoundingClientRect().width||1)}function p(y){const x=t.getBoundingClientRect();return y-x.left}function u(y,x){let h;return x==="day"?(h=new Date(y+"T00:00:00.000Z"),{start:h,end:new Date(h.getTime()+24*3600*1e3-1)}):x==="hour"?(h=new Date(y+":00:00.000Z"),{start:h,end:new Date(h.getTime()+3600*1e3-1)}):(h=new Date(y+":00.000Z"),{start:h,end:new Date(h.getTime()+60*1e3-1)})}function c(y,x){const h=histogramBuckets?.length||0;if(h<=0)return null;const m=d(),g=m/h,w=Math.max(0,Math.min(m,Math.min(y,x))),A=Math.max(0,Math.min(m,Math.max(y,x)));let C=Math.floor(w/g),$=Math.ceil(A/g)-1;return C=Math.max(0,Math.min(h-1,C)),$=Math.max(0,Math.min(h-1,$)),{snappedAbsL:C*g,snappedAbsR:($+1)*g,i1:C,i2:$}}function f(y,x){let h=0;for(let m=y;m<=x;m++)h+=histogramBuckets[m]?.count||0;return h}let T=!1,b=0,E=0,l=!1;function I(){if(l=!1,!T||!histogramBuckets?.length)return;i();const y=window.devicePixelRatio||1,x=s.getContext("2d");x.setTransform(y,0,0,y,0,0);const h=t.offsetWidth||s.getBoundingClientRect().width,m=t.offsetHeight||s.getBoundingClientRect().height;x.clearRect(0,0,h,m);const g=c(b,E);if(!g)return;const w=o.scrollLeft||0,A=g.snappedAbsL-w,$=g.snappedAbsR-w-A;x.fillStyle="rgba(255,255,255,0.15)",x.fillRect(A,0,$,m),x.strokeStyle="rgba(255,255,255,0.35)",x.strokeRect(A+.5,.5,Math.max(0,$-1),m-1);const _=document.getElementById("bucketSize")?.value||"hour",L=histogramBuckets[g.i1],K=histogramBuckets[g.i2];if(L&&K&&n){const J=u(L.key,_),j=u(K.key,_),Q=J.start.toISOString(),ne=j.end.toISOString(),ee=g.i2-g.i1+1,oe=f(g.i1,g.i2);n.textContent=`dragging: ${Q} → ${ne} • ${ee} buckets • ${oe} events`}}function P(){l||(l=!0,requestAnimationFrame(I))}t.addEventListener("pointerdown",y=>{if(histogramBuckets?.length){T=!0,b=p(y.clientX),E=b;try{t.setPointerCapture(y.pointerId)}catch{}P(),y.preventDefault()}}),t.addEventListener("pointermove",y=>{T&&(E=p(y.clientX),P(),y.preventDefault())});function F(){if(!T)return;if(T=!1,Math.abs(E-b)<6){r();return}const x=c(b,E);if(!x){r();return}const h=document.getElementById("bucketSize")?.value||"hour",m=histogramBuckets[x.i1],g=histogramBuckets[x.i2];if(!m||!g){r();return}const w=u(m.key,h),A=u(g.key,h),C=document.getElementById("fromUtc"),$=document.getElementById("toUtc");C&&(C.value=w.start.toISOString()),$&&($.value=A.end.toISOString()),r(),mergeTimeline()}t.addEventListener("pointerup",y=>{F(),y.preventDefault()}),t.addEventListener("pointercancel",y=>{F(),y.preventDefault()}),o.addEventListener("scroll",()=>{T?P():i()}),window.addEventListener("resize",()=>{i(),T&&P()}),i()})();function renderHistogramLegend(){const e=document.getElementById("histLegend");if(!e)return;ensureAllThreadColors();const t=[];for(const o of threads||[]){if(!o?.ok)continue;const a=ensureThreadColor(o.key),s=o.thread?.displayName||o.path?.split(/[\\/]/).pop()||o.path||"(thread)";t.push({key:o.key,name:s,c:a})}t.push({key:"__unknown__",name:"Unknown",c:ensureThreadColor("__unknown__")});let n="";_histStackMode==="type"&&Array.isArray(_histEventTypeOrder)&&_histEventTypeOrder.length&&(n=`
          <div style="margin-bottom:10px;padding-bottom:10px;border-bottom:1px solid rgba(255,255,255,0.08)">
            <div class="hint" style="margin-bottom:6px"><strong>Event types</strong> (auto mode)</div>
            <div style="display:flex;flex-wrap:wrap;gap:8px">
              ${_histEventTypeOrder.slice(0,10).map(s=>{const i=ensureEventTypeColor(s),r=getEventTypeLabel(s);return`
                    <div title="${escapeHtml(r)}"
                         style="display:flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;
                                background:rgba(0,0,0,0.12);border:1px solid rgba(255,255,255,0.06)">
                      <span class="thread-swatch" style="background:${i.fill};border-color:${i.stroke}"></span>
                      <span class="hist-legend-label">${escapeHtml(r)}</span>
                    </div>
                  `}).join("")}
              ${_histEventTypeOrder.length>10?`<div class="hint" style="align-self:center">+${_histEventTypeOrder.length-10} more…</div>`:""}
            </div>
          </div>
        `),e.innerHTML=n+t.map(o=>`
            <div class="hist-legend-item"
                 data-thread-key="${o.key}"
                 title="${escapeHtml(o.name)}">
                <span class="thread-swatch"
                      style="background:${o.c.fill};border-color:${o.c.stroke}"></span>
                <span class="hist-legend-label">${escapeHtml(o.name)}</span>
            </div>
        `).join(""),e.querySelectorAll(".hist-legend-item").forEach(o=>{const a=o.dataset.threadKey;o.addEventListener("mouseenter",()=>setLegendIsolate(a)),o.addEventListener("mouseleave",()=>clearLegendIsolate()),o.addEventListener("click",s=>{toggleLegendPin(a,s.shiftKey),s.stopPropagation()})})}document.addEventListener("click",()=>{_legendPinnedKeys.size>0&&(_legendPinnedKeys.clear(),clearThreadAndLegendHighlights(),Array.isArray(histogramBuckets)&&drawHistogram(histogramBuckets),updateActiveFilterSummary(),queueSaveProjectState("legend pins cleared"))});async function loadTimezonesDropdown(){const e=document.getElementById("sourceTimeZoneId");try{const n=await(await fetch("/api/timezones")).json();if(!n?.ok||!Array.isArray(n.timezones)){e&&(e.innerHTML='<option value="">(failed to load)</option>');return}if(timezonesCache=n.timezones.map(o=>({id:o.id??o.Id,displayName:o.displayName??o.DisplayName})).filter(o=>o.id&&o.displayName),e){e.innerHTML=buildTimezoneOptionsHtml(e.value||"");const o="SE Asia Standard Time";!e.value&&timezonesCache.some(a=>a.id===o)&&(e.value=o)}renderThreads()}catch{e&&(e.innerHTML='<option value="">(failed to load)</option>')}}function wireTimezoneUi(){const e=document.getElementById("naiveMode"),t=document.getElementById("sourceTimeZoneId");if(!e||!t)return;function n(){const o=(e.value||"").toLowerCase()==="timezone";t.disabled=!o,t.style.opacity=o?"1":"0.6"}e.addEventListener("change",n),n()}async function runGenerateAndMerge(e={}){const t={source:e.source||"manual",suppressAlerts:!!e.suppressAlerts},n=document.getElementById("btn-run"),o=document.getElementById("btn-generate"),a=document.getElementById("btn-merge"),s=document.getElementById("btn-add-threads");if(runGenerateAndMerge._running)return;runGenerateAndMerge._running=!0;const i=r=>{n&&(n.disabled=!!r),o&&(o.disabled=!!r),a&&(a.disabled=!!r),s&&(s.disabled=!!r)};try{const r=(threads||[]).filter(d=>d&&d.ok&&Array.isArray(d.selectedTsKeys)&&d.selectedTsKeys.length);if(!r.length){const d="Run: no timestamp columns selected. Select at least one timestamp checkbox for a thread.";setRunStatus(d),t.suppressAlerts||alert(d);return}i(!0),setRunStatus("Running… generating events"),await generateEvents();for(const d of r)d?._genStatus==="ok"&&!d.lastStats&&console.warn("[TimeWeave] No parsing stats returned for",d.path);setRunStatus("Running… merging timeline"),await mergeTimeline(),lastRunSummary=buildLastRunSummary(t.source||"manual"),queueSaveProjectState("lastRunSummary updated"),setRunStatus(t.source==="auto"?"Auto-run complete ✅":"Run complete ✅"),setTimeout(()=>setRunStatus(""),2500)}catch(r){const d="Run failed: "+(r?.message||r);setRunStatus(d),t.suppressAlerts||alert(d)}finally{i(!1),runGenerateAndMerge._running=!1,queueSaveProjectState("runGenerateAndMerge complete")}}document.getElementById("btn-add-threads")?.addEventListener("click",addThreads),document.getElementById("btn-generate")?.addEventListener("click",generateEvents),document.getElementById("btn-merge")?.addEventListener("click",mergeTimeline),document.getElementById("bucketSize")?.addEventListener("change",()=>{mergedEvents?.length&&(mergeTimeline(),queueSaveProjectState("ui changed: bucket/collapse/filter"))}),document.getElementById("btn-apply-global-time")?.addEventListener("click",applyGlobalTimeSettingsToAllThreadsAndGenerate),document.getElementById("collapseSameTs")?.addEventListener("change",()=>{mergedEvents?.length&&(mergeTimeline(),queueSaveProjectState("ui changed: bucket/collapse/filter"))}),document.getElementById("filterByEventType")?.addEventListener("change",()=>{updateEventTypeFilterUiVisibility(),document.getElementById("filterByEventType")?.checked&&renderEventTypeFiltersFromAllEvents(),typeof mergeTimeline=="function"&&mergeTimeline(),typeof updateActiveFilterSummary=="function"&&updateActiveFilterSummary(),queueSaveProjectState("ui changed: bucket/collapse/filter")}),document.getElementById("btn-eventType-all")?.addEventListener("click",()=>{setAllEventTypeCheckboxes(!0),typeof mergeTimeline=="function"&&mergeTimeline(),typeof updateActiveFilterSummary=="function"&&updateActiveFilterSummary(),queueSaveProjectState("eventType all")}),document.getElementById("btn-eventType-none")?.addEventListener("click",()=>{setAllEventTypeCheckboxes(!1),typeof mergeTimeline=="function"&&mergeTimeline(),typeof updateActiveFilterSummary=="function"&&updateActiveFilterSummary(),queueSaveProjectState("eventType none")}),window.addEventListener("resize",()=>{typeof histogramBuckets<"u"&&Array.isArray(histogramBuckets)&&drawHistogram(histogramBuckets,_histHoverIndex)});function clearAllFiltersAndRefresh(){_bucketIsolateKeys=null,_legendPinnedKeys?.clear?.(),_filterLocked=!1;const e=document.getElementById("fromUtc"),t=document.getElementById("toUtc");e&&(e.value=""),t&&(t.value=""),clearThreadAndLegendHighlights(),Array.isArray(histogramBuckets)&&drawHistogram(histogramBuckets,_histHoverIndex),updateActiveFilterSummary(),typeof mergeTimeline=="function"&&mergeTimeline()}function wireActiveFilterUiOnce(){const e=document.getElementById("af-lock"),t=document.getElementById("af-clear");!e||!t||e._wired||(e._wired=!0,e.addEventListener("click",()=>{_filterLocked=!_filterLocked,updateActiveFilterSummary(),queueSaveProjectState(_filterLocked?"lock active filter":"unlock active filter")}),t.addEventListener("click",()=>{clearAllFiltersAndRefresh(),queueSaveProjectState("clear active filters")}))}(function(){const t=document.getElementById("histogram");t&&t.addEventListener("dblclick",n=>{const o=document.getElementById("fromUtc"),a=document.getElementById("toUtc");o&&(o.value=""),a&&(a.value="");const s=document.getElementById("histogramOverlay");if(s){const i=s.getContext("2d");i&&(i.setTransform(1,0,0,1,0,0),i.clearRect(0,0,s.width,s.height))}typeof mergeTimeline=="function"&&mergeTimeline(),queueSaveProjectState("zoom cleared (dblclick)"),n.preventDefault()})})();function _safeJsonParse(e){try{return JSON.parse(e)}catch{return null}}async function _postForm(e,t){const n=await fetch(e,{method:"POST",body:t}),o=await n.text(),a=_safeJsonParse(o);if(!n.ok||!a?.ok){const s=a?.error||o||`HTTP ${n.status}`;throw new Error(s)}return a}async function _canvasToPngBlob(e){const t=await new Promise(n=>e.toBlob(n,"image/png"));if(!t)throw new Error("Failed to encode PNG from canvas.");return t}async function _buildHistogramViewportBlob(e){const t=e.closest(".histogram-body")||e.parentElement;if(!t)return null;const n=window.devicePixelRatio||1,o=Math.floor((t.scrollLeft||0)*n),a=0,s=Math.max(1,Math.floor(t.clientWidth*n)),i=e.height,r=document.createElement("canvas");return r.width=s,r.height=i,r.getContext("2d").drawImage(e,o,a,s,i,0,0,s,i),await new Promise(u=>r.toBlob(u,"image/png"))||null}function _buildBucketsPayload(){return{app:"TimeWeave",exportType:"histogramBuckets",utcExported:new Date().toISOString(),bucketSize:document.getElementById("bucketSize")?.value||"hour",bucketCount:(histogramBuckets||[]).length,stackMode:_histStackMode,buckets:histogramBuckets}}async function exportHistogramPngsAndGetExportDir(e){const t=document.getElementById("histogram");if(!t)throw new Error("Histogram canvas not found.");if(!Array.isArray(histogramBuckets)||histogramBuckets.length===0)throw new Error("No histogram data to export.");const n=await _canvasToPngBlob(t),o=await _buildHistogramViewportBlob(t),a=new FormData;a.append("targetDir",e),o&&a.append("histogramViewport",o,"histogram_viewport.png"),a.append("histogramFull",n,"histogram_full.png");const s=await _postForm("/api/export/histogram",a);if(!s.exportDir)throw new Error("Server did not return exportDir.");return s.exportDir}async function exportHistogramBucketsJsonToExportDir(e){if(!Array.isArray(histogramBuckets)||histogramBuckets.length===0)throw new Error("No histogram buckets to export.");const t=_buildBucketsPayload(),n=new FormData;return n.append("exportDir",e),n.append("json",JSON.stringify(t,null,2)),await _postForm("/api/export/histogram-buckets-json",n)}async function exportHistogramBucketsCsvToExportDir(e){if(!Array.isArray(histogramBuckets)||histogramBuckets.length===0)throw new Error("No histogram buckets to export.");const t=_buildBucketsPayload(),n=new FormData;return n.append("exportDir",e),n.append("json",JSON.stringify(t)),await _postForm("/api/export/histogram-buckets-csv",n)}async function exportManifestToExportDir(e){const t=new FormData;return t.append("exportDir",e),await _postForm("/api/export/manifest",t)}async function exportAllArtifacts(){const e=normalizeUserPathLine(document.getElementById("exportTargetDir")?.value||"");if(!e){alert("Please enter an export folder path.");return}try{const t=await exportHistogramPngsAndGetExportDir(e);await exportHistogramBucketsJsonToExportDir(t),await exportHistogramBucketsCsvToExportDir(t);const n=await exportManifestToExportDir(t);showExportResultModal({ok:!0,exportDir:t,files:[{name:"histogram_full.png"},{name:"histogram_viewport.png"},{name:"histogramBuckets.json"},{name:"histogramBuckets.csv"},{name:"manifest.json"}],fileCount:n?.fileCount})}catch(t){alert(`Export failed:
`+(t?.message||t))}}document.getElementById("btn-export-histogram")?.addEventListener("click",exportAllArtifacts),loadTimezonesDropdown(),wireTimezoneUi(),document.getElementById("btn-run")?.addEventListener("click",()=>runGenerateAndMerge({source:"manual"})),window.addEventListener("DOMContentLoaded",async()=>{const e=await restoreProjectStateOnBoot();typeof updateRunDiagnostics=="function"&&updateRunDiagnostics("restore complete"),e&&!_autoRunAfterRestoreDone&&(_autoRunAfterRestoreDone=!0,(threads||[]).filter(n=>n&&n.ok&&Array.isArray(n.selectedTsKeys)&&n.selectedTsKeys.length).length?(setRunStatus("Restored project — auto-running…"),setTimeout(()=>{runGenerateAndMerge({source:"auto",suppressAlerts:!0})},50)):(setRunStatus("Restored project (select timestamps to run)."),setTimeout(()=>setRunStatus(""),2500)))});
