let TL = null, TL_ENV = "", TL_AT = 0, TL_BUSY = false;
let TL_RANGE = "24h", TL_CUSTOM = null;
let TL_FROM = 0, TL_TO = 0;
let TL_GEOM = null, TL_MK = [];
let TL_SEL = null;
let TL_HEAD_AT = null, TL_SEEK_TIMER = null, TL_STATE_SEQ = 0, TL_STATE_LOADING = false;
let TIME_STATE = null;
let TL_AI_MD = "", TL_AI_AT = 0, TL_AI_BUSY = false;
let TL_PM_MD = "", TL_PM_RAW = "", TL_PM_AT = 0, TL_PM_BUSY = false;
let TL_RESIZE = null, TL_DRAG = null;

const TL_RANGES = [["1h", 1], ["6h", 6], ["24h", 24], ["3d", 72], ["7d", 168]];
const TL_LANES = [
	["deploys", "Deploys"],
	["alarms", "Alarms"],
	["changes", "Changes"],
	["security", "Security"],
	["lambdaErrors", "Lambda errors"],
];
const TL_MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const TL_SEV_COLOR = { critical: "var(--critical)", high: "var(--red)", medium: "var(--warning)", low: "var(--ink-3)" };
const TL_PAD_L = 96, TL_PAD_R = 18, TL_BAND_T = 14, TL_BAND_H = 130, TL_GAP = 22, TL_LANE_H = 26, TL_AXIS_H = 26;

const tl2 = (n) => String(n).padStart(2, "0");
function tlStamp(ts) {
	const d = new Date(ts);
	if (isNaN(+d)) return "";
	return `${d.getDate()} ${TL_MONTHS[d.getMonth()]} ${tl2(d.getHours())}:${tl2(d.getMinutes())}`;
}
function tlClock(ts) {
	const d = new Date(ts);
	if (isNaN(+d)) return "";
	return `${tl2(d.getHours())}:${tl2(d.getMinutes())}`;
}
function tlInputValue(ms) {
	const d = new Date(ms);
	return `${d.getFullYear()}-${tl2(d.getMonth() + 1)}-${tl2(d.getDate())}T${tl2(d.getHours())}:${tl2(d.getMinutes())}`;
}
const tlNum = (v) => (v == null || !isFinite(+v) ? null : +v);

function tlSaveRange() {
	try {
		localStorage.setItem(wsKey("me-tl-range"), JSON.stringify(TL_CUSTOM ? { custom: TL_CUSTOM } : { range: TL_RANGE }));
	} catch { }
}
function tlLoadRange() {
	let saved = null;
	try { saved = JSON.parse(wsGet(localStorage, "me-tl-range") || "null"); } catch { saved = null; }
	if (saved && saved.custom && saved.custom.from && saved.custom.to) {
		TL_CUSTOM = { from: +saved.custom.from, to: +saved.custom.to };
		TL_RANGE = "custom";
		return;
	}
	if (saved && saved.range && TL_RANGES.some(([k]) => k === saved.range)) TL_RANGE = saved.range;
	TL_CUSTOM = null;
}
function tlWindow() {
	if (TL_RANGE === "custom" && TL_CUSTOM) return { from: TL_CUSTOM.from, to: TL_CUSTOM.to };
	const hours = (TL_RANGES.find(([k]) => k === TL_RANGE) || ["24h", 24])[1];
	const to = Date.now();
	return { from: to - hours * 3600000, to };
}

function loadTimelineTab() {
	tlLoadRange();
	if (TL_BUSY) return;
	if (!TL || TL_ENV !== ENV || Date.now() - TL_AT > 120000) fetchTimeline(false);
	else { renderTimeline(); }
}

function timelineTabChanged(view) {
	if (view === "viewTimeline") { loadTimelineTab(); return; }
	if (TIME_STATE || TL_HEAD_AT != null) exitTimeTravel();
}

function tlEnvChanged() {
	TL = null; TL_ENV = ""; TL_AT = 0;
	TL_SEL = null;
	TL_AI_MD = ""; TL_AI_AT = 0;
	TL_PM_MD = ""; TL_PM_RAW = ""; TL_PM_AT = 0;
	exitTimeTravel();
	if ($("#viewTimeline")?.classList.contains("active")) loadTimelineTab();
}

async function fetchTimeline(fresh) {
	if (TL_BUSY) return;
	TL_BUSY = true;
	const w = tlWindow();
	const envKey = ENV;
	if (!TL || TL_ENV !== envKey) renderTimelineSkeleton();
	else {
		const b = $("#tlRefresh");
		if (b) { b.disabled = true; b.textContent = "Loading…"; }
	}
	const q = `/api/timeline?env=${encodeURIComponent(envKey)}&from=${encodeURIComponent(new Date(w.from).toISOString())}&to=${encodeURIComponent(new Date(w.to).toISOString())}${fresh ? "&fresh=1" : ""}`;
	try {
		const r = await fetch(q, { cache: "no-store" });
		TL = await r.json();
	} catch { TL = { ok: false, reason: "server" }; }
	TL_ENV = envKey;
	TL_AT = Date.now();
	TL_BUSY = false;
	if (envKey !== ENV) { if ($("#viewTimeline")?.classList.contains("active")) fetchTimeline(false); return; }
	TL_SEL = null;
	renderTimeline();
}

function renderTimelineSkeleton() {
	const el = $("#tlBody");
	if (!el) return;
	el.innerHTML = `<div class="skl" style="height:320px;border-radius:14px"></div>
		<div class="skl" style="height:72px;margin-top:14px"></div>
		<div class="skl" style="height:120px;margin-top:14px"></div>`;
}

function tlBarState() {
	const rng = $("#tlRanges");
	if (rng && !rng.dataset.built) {
		rng.innerHTML = TL_RANGES.map(([k]) => `<button class="pill" data-r="${k}">${k}</button>`).join("");
		rng.dataset.built = "1";
		$$(".pill", rng).forEach((b) => b.onclick = () => {
			TL_RANGE = b.dataset.r; TL_CUSTOM = null;
			tlSaveRange();
			exitTimeTravel();
			fetchTimeline(false);
		});
	}
	$$("#tlRanges .pill").forEach((b) => b.classList.toggle("active", b.dataset.r === TL_RANGE && !TL_CUSTOM));
	const w = TL?.ok && TL.from && TL.to ? { from: +new Date(TL.from), to: +new Date(TL.to) } : tlWindow();
	const from = $("#tlFrom"), to = $("#tlTo");
	if (from && !from.value) from.value = tlInputValue(w.from);
	if (to && !to.value) to.value = tlInputValue(w.to);
	const lbl = $("#tlWindow");
	if (lbl) lbl.textContent = `${tlStamp(w.from)} → ${tlStamp(w.to)}`;
	const clamp = $("#tlClamp");
	if (clamp) clamp.style.display = TL?.clamped ? "" : "none";
	const btn = $("#tlRefresh");
	if (btn) { btn.disabled = false; btn.textContent = "Refresh"; }
	const live = $("#tlLive");
	if (live) live.style.display = TIME_STATE || TL_HEAD_AT != null ? "" : "none";
}

function tlErrorHtml() {
	const msg = TL.reason === "auth" ? "session expired · " + escHtml(typeof ssoManualCmd === "function" ? ssoManualCmd() : "aws sso login")
		: TL.reason === "no-workspace" ? "no workspace selected"
		: TL.reason === "no-env" ? "no environments configured"
		: escHtml(TL.detail || "timeline unavailable — is the sync server running?");
	return `<div class="tiles"><div class="tile"><div class="num">–</div><div class="lbl">${msg}</div></div></div>`;
}

function renderTimeline() {
	const el = $("#tlBody");
	if (!el) return;
	tlBarState();
	if (!TL) return renderTimelineSkeleton();
	if (!TL.ok) { el.innerHTML = tlErrorHtml(); return; }
	el.innerHTML = `
		<div class="tl-chart" id="tlChart">
			<div id="tlSvgWrap"></div>
			<div class="tl-selbox" id="tlSelBox" style="display:none"></div>
			<div class="tl-playhead" id="tlHead" tabindex="0" role="slider" aria-label="playhead" style="display:none"><span class="tl-ph-at" id="tlHeadAt"></span></div>
			<div class="tl-tip" id="tlTip" style="display:none"></div>
		</div>
		<div class="tl-legend" id="tlLegend"></div>
		<div class="tl-state" id="tlState"></div>
		<div class="tl-ai">
			<h2 class="tl-h">Incident analysis <span class="hint" id="tlAiRange"></span></h2>
			<div class="tl-aibar">
				<button class="btn" id="tlExplain">Explain this window</button>
				<button class="btn" id="tlRegen" style="display:none">Regenerate</button>
				<button class="btn" id="tlPost">Postmortem draft</button>
				<button class="btn" id="tlClearSel" style="display:none">Clear selection</button>
				<span class="hint" id="tlAiMeta"></span>
			</div>
			<div class="ai-box" id="tlAiBox" style="display:none"></div>
			<div id="tlPmWrap" style="display:none">
				<div class="tl-aibar tl-pmbar"><button class="btn" id="tlPmCopy">Copy markdown</button><button class="btn" id="tlPmRegen">Regenerate</button><span class="hint" id="tlPmMeta"></span></div>
				<div class="ai-box" id="tlPmBox"></div>
			</div>
		</div>`;
	drawTimeline();
	renderTlLegend();
	renderTlState();
	renderTlAi();
	tlBindChart();
}

function tlLaneCounts() {
	const l = TL?.lanes || {};
	return TL_LANES.reduce((a, [k]) => a + (Array.isArray(l[k]) ? l[k].length : 0), 0);
}

function tlSeries(name) {
	const s = TL?.metrics?.[name];
	return Array.isArray(s) ? s : [];
}

function drawTimeline() {
	const wrap = $("#tlSvgWrap");
	const chart = $("#tlChart");
	if (!wrap || !chart || !TL?.ok) return;
	const W = Math.max(640, chart.clientWidth || 980);
	const lanesTop = TL_BAND_T + TL_BAND_H + TL_GAP;
	const H = lanesTop + TL_LANES.length * TL_LANE_H + TL_AXIS_H;
	TL_FROM = +new Date(TL.from);
	TL_TO = +new Date(TL.to);
	if (!isFinite(TL_FROM) || !isFinite(TL_TO) || TL_TO <= TL_FROM) { TL_TO = Date.now(); TL_FROM = TL_TO - 86400000; }
	const X0 = TL_PAD_L, X1 = W - TL_PAD_R;
	TL_GEOM = { W, H, X0, X1, from: TL_FROM, to: TL_TO, lanesTop };
	TL_MK = [];

	const x = (t) => X0 + ((+new Date(t) - TL_FROM) / (TL_TO - TL_FROM)) * (X1 - X0);
	const yPct = (v) => TL_BAND_T + TL_BAND_H * (1 - Math.min(100, Math.max(0, +v || 0)) / 100);
	const base = TL_BAND_T + TL_BAND_H;
	const memWarn = tlNum(SETTINGS?.memWarn) ?? 70;
	const memCrit = tlNum(SETTINGS?.memCrit) ?? 90;

	let h = `<svg id="tlSvg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" class="tl-svg">`;
	h += `<rect class="tl-zone tl-zone-crit" x="${X0}" y="${TL_BAND_T}" width="${X1 - X0}" height="${Math.max(0, yPct(memCrit) - TL_BAND_T)}"></rect>`;
	h += `<rect class="tl-zone tl-zone-warn" x="${X0}" y="${yPct(memCrit)}" width="${X1 - X0}" height="${Math.max(0, yPct(memWarn) - yPct(memCrit))}"></rect>`;
	h += `<line class="tl-thresh crit" x1="${X0}" y1="${yPct(memCrit)}" x2="${X1}" y2="${yPct(memCrit)}"></line>`;
	h += `<line class="tl-thresh warn" x1="${X0}" y1="${yPct(memWarn)}" x2="${X1}" y2="${yPct(memWarn)}"></line>`;
	h += `<text class="tl-axis" x="${X0 - 6}" y="${yPct(memCrit) + 3}" text-anchor="end">${memCrit}%</text>`;
	h += `<text class="tl-axis" x="${X0 - 6}" y="${yPct(memWarn) + 3}" text-anchor="end">${memWarn}%</text>`;
	h += `<line class="tl-base" x1="${X0}" y1="${base}" x2="${X1}" y2="${base}"></line>`;

	const ticks = 6;
	for (let i = 0; i <= ticks; i++) {
		const t = TL_FROM + ((TL_TO - TL_FROM) * i) / ticks;
		const tx = X0 + ((X1 - X0) * i) / ticks;
		h += `<line class="tl-grid" x1="${tx}" y1="${TL_BAND_T}" x2="${tx}" y2="${lanesTop + TL_LANES.length * TL_LANE_H}"></line>`;
		h += `<text class="tl-axis" x="${tx}" y="${H - 8}" text-anchor="${i === 0 ? "start" : i === ticks ? "end" : "middle"}">${escHtml(i === 0 || i === ticks ? tlStamp(t) : tlClock(t))}</text>`;
	}

	const fx = tlSeries("fivexx");
	const maxFx = Math.max(1, ...fx.map((p) => +p.sum || 0));
	fx.forEach((p) => {
		const v = +p.sum || 0;
		if (v <= 0) return;
		const bh = Math.max(2, (v / maxFx) * (TL_BAND_H * 0.5));
		h += `<rect class="tl-5xx" x="${(x(p.t) - 1.6).toFixed(1)}" y="${(base - bh).toFixed(1)}" width="3.2" height="${bh.toFixed(1)}"><title>${v} 5xx · ${escHtml(tlStamp(p.t))}</title></rect>`;
	});

	const line = (series, cls) => {
		const pts = series.map((p) => `${x(p.t).toFixed(1)},${yPct(p.max != null ? p.max : p.avg).toFixed(1)}`);
		if (pts.length < 2) return "";
		return `<polyline class="${cls}" points="${pts.join(" ")}" fill="none"></polyline>`;
	};
	const cpu = tlSeries("cpu"), mem = tlSeries("memory");
	h += line(cpu, "tl-cpu");
	if (mem.length > 1) {
		const pts = mem.map((p) => `${x(p.t).toFixed(1)},${yPct(p.max != null ? p.max : p.avg).toFixed(1)}`);
		h += `<polygon class="tl-mem-fill" points="${x(mem[0].t).toFixed(1)},${base} ${pts.join(" ")} ${x(mem[mem.length - 1].t).toFixed(1)},${base}"></polygon>`;
	}
	h += line(mem, "tl-mem");
	if (!mem.length && !cpu.length && !fx.length) {
		h += `<text class="tl-axis" x="${(X0 + X1) / 2}" y="${TL_BAND_T + TL_BAND_H / 2}" text-anchor="middle">no metrics in this window</text>`;
	}

	TL_LANES.forEach(([key, label], li) => {
		const ly = lanesTop + li * TL_LANE_H;
		const cy = ly + TL_LANE_H / 2;
		h += `<line class="tl-lane-line" x1="${X0}" y1="${cy}" x2="${X1}" y2="${cy}"></line>`;
		h += `<text class="tl-lane-lbl" x="${X0 - 10}" y="${cy + 3.5}" text-anchor="end">${escHtml(label)}</text>`;
		const rows = Array.isArray(TL.lanes?.[key]) ? TL.lanes[key] : [];
		rows.forEach((row) => {
			const px = x(row.at);
			if (!isFinite(px)) return;
			const mk = tlMarker(key, row, px, cy);
			if (mk) h += mk;
		});
	});

	const vline = (m, cls, label) => {
		if (!m || !m.at) return "";
		const vx = x(m.at);
		if (!isFinite(vx)) return "";
		const anchor = vx > X1 - 90 ? "end" : "start";
		const tx = anchor === "end" ? vx - 5 : vx + 5;
		return `<line class="tl-vline ${cls}" x1="${vx}" y1="${TL_BAND_T}" x2="${vx}" y2="${lanesTop + TL_LANES.length * TL_LANE_H}"></line>` +
			`<text class="tl-vlabel ${cls}" x="${tx}" y="${TL_BAND_T - 3}" text-anchor="${anchor}">${escHtml(label)}</text>`;
	};
	h += vline(TL.markers?.firstAnomaly, "amber", "first anomaly");
	h += vline(TL.markers?.worst, "red", "worst" + (TL.markers?.worst?.what ? " · " + String(TL.markers.worst.what).slice(0, 30) : ""));

	h += `</svg>`;
	wrap.innerHTML = h;

	if (!tlLaneCounts()) {
		const em = document.createElement("div");
		em.className = "tl-empty";
		em.id = "tlEmpty";
		em.textContent = "No events in this window.";
		wrap.appendChild(em);
	}
	tlPositionHead();
	tlPositionSel();
}

function tlMarker(lane, row, px, cy) {
	const i = TL_MK.length;
	let shape = "", color = "", url = "", lines = [], title = "";
	if (lane === "deploys") {
		const st = String(row.status || "");
		color = /succ|complete/i.test(st) ? "var(--good)" : /fail|error|cancel/i.test(st) ? "var(--critical)" : "var(--blue)";
		url = row.runUrl || row.pr?.url || row.commit?.url || "";
		title = row.name || "deploy";
		lines = [
			row.status ? `status: ${row.status}` : "",
			row.sha ? `sha: ${String(row.sha).slice(0, 10)}` : "",
			row.commit?.msg ? `commit: ${row.commit.msg}` : "",
			row.commit?.author ? `by ${row.commit.author}` : "",
			row.pr ? `PR #${row.pr.number} ${row.pr.title || ""}` : "",
		];
		shape = `<path class="tl-mk" data-i="${i}" d="M${px.toFixed(1)},${cy - 6.5} L${(px + 6).toFixed(1)},${cy + 5} L${(px - 6).toFixed(1)},${cy + 5} Z" fill="${color}"></path>`;
	} else if (lane === "alarms") {
		const st = String(row.state || "");
		color = st === "ALARM" ? "var(--critical)" : st === "OK" ? "var(--good)" : "var(--ink-3)";
		url = awsConsoleUrl("alarm", row.name);
		title = row.name || "alarm";
		lines = [st ? `state: ${st}` : "", row.reason || ""];
		shape = `<path class="tl-mk" data-i="${i}" d="M${px.toFixed(1)},${cy - 6} L${(px + 6).toFixed(1)},${cy} L${px.toFixed(1)},${cy + 6} L${(px - 6).toFixed(1)},${cy} Z" fill="${color}"></path>`;
	} else if (lane === "changes") {
		color = "var(--violet)";
		url = awsConsoleUrl("trail", row.event);
		title = row.event || "change";
		lines = [row.actor ? `by ${row.actor}` : "", row.resource || ""];
		shape = `<circle class="tl-mk" data-i="${i}" cx="${px.toFixed(1)}" cy="${cy}" r="4.2" fill="${color}"></circle>`;
	} else if (lane === "security") {
		const sev = String(row.sev || "low").toLowerCase();
		color = TL_SEV_COLOR[sev] || "var(--ink-3)";
		title = row.title || "finding";
		lines = [`severity: ${sev}`, row.source ? `source: ${row.source}` : "", row.kind || ""];
		shape = `<rect class="tl-mk" data-i="${i}" x="${(px - 5).toFixed(1)}" y="${cy - 5}" width="10" height="10" rx="2" fill="${color}"></rect>`;
	} else if (lane === "lambdaErrors") {
		const n = Math.max(1, +row.count || 1);
		const bh = Math.min(18, 5 + n * 2);
		color = "var(--orange)";
		url = awsConsoleUrl("lambda", row.fn);
		title = row.fn || "lambda";
		lines = [`${n} error${n === 1 ? "" : "s"}`];
		shape = `<rect class="tl-mk" data-i="${i}" x="${(px - 3).toFixed(1)}" y="${(cy + 7 - bh).toFixed(1)}" width="6" height="${bh}" rx="1" fill="${color}"></rect>`;
	}
	if (!shape) return "";
	TL_MK.push({ lane, at: row.at, title, lines: lines.filter(Boolean), url, fp: row.fp, row });
	return url ? `<a class="tl-link" href="${escHtml(url)}" target="_blank" rel="noopener">${shape}</a>` : shape;
}

function renderTlLegend() {
	const el = $("#tlLegend");
	if (!el) return;
	const item = (cls, txt) => `<span class="tl-lg"><i class="${cls}"></i>${escHtml(txt)}</span>`;
	el.innerHTML =
		item("lg-mem", "memory %") +
		item("lg-cpu", "cpu %") +
		item("lg-5xx", "5xx") +
		item("lg-good", "deploy ok") +
		item("lg-crit", "failure / ALARM") +
		item("lg-violet", "account change") +
		item("lg-orange", "lambda errors") +
		`<span class="tl-lg-note">click the chart to place the playhead · drag to select a window</span>`;
}

function tlPositionHead() {
	const head = $("#tlHead");
	if (!head || !TL_GEOM) return;
	if (TL_HEAD_AT == null) { head.style.display = "none"; return; }
	const f = (TL_HEAD_AT - TL_GEOM.from) / (TL_GEOM.to - TL_GEOM.from);
	const px = TL_GEOM.X0 + Math.min(1, Math.max(0, f)) * (TL_GEOM.X1 - TL_GEOM.X0);
	head.style.display = "";
	head.style.left = px.toFixed(1) + "px";
	head.style.height = (TL_GEOM.lanesTop + TL_LANES.length * TL_LANE_H - TL_BAND_T) + "px";
	head.style.top = TL_BAND_T + "px";
	head.setAttribute("aria-valuetext", tlStamp(TL_HEAD_AT));
	const at = $("#tlHeadAt");
	if (at) at.textContent = tlStamp(TL_HEAD_AT);
}

function tlPositionSel() {
	const box = $("#tlSelBox");
	if (!box || !TL_GEOM) return;
	if (!TL_SEL) { box.style.display = "none"; return; }
	const px = (t) => TL_GEOM.X0 + Math.min(1, Math.max(0, (t - TL_GEOM.from) / (TL_GEOM.to - TL_GEOM.from))) * (TL_GEOM.X1 - TL_GEOM.X0);
	const a = px(TL_SEL.from), b = px(TL_SEL.to);
	box.style.display = "";
	box.style.left = Math.min(a, b).toFixed(1) + "px";
	box.style.width = Math.abs(b - a).toFixed(1) + "px";
	box.style.top = TL_BAND_T + "px";
	box.style.height = (TL_GEOM.lanesTop + TL_LANES.length * TL_LANE_H - TL_BAND_T) + "px";
}

function tlTimeAt(clientX) {
	const chart = $("#tlChart");
	if (!chart || !TL_GEOM) return null;
	const r = chart.getBoundingClientRect();
	const px = clientX - r.left;
	const f = (px - TL_GEOM.X0) / (TL_GEOM.X1 - TL_GEOM.X0);
	return TL_GEOM.from + Math.min(1, Math.max(0, f)) * (TL_GEOM.to - TL_GEOM.from);
}

function tlBindChart() {
	const chart = $("#tlChart"), head = $("#tlHead");
	if (!chart) return;
	chart.addEventListener("pointerdown", (e) => {
		if (e.button !== 0) return;
		if (e.target.closest && (e.target.closest("#tlHead") || e.target.closest("a") || e.target.closest(".tl-mk"))) return;
		const startX = e.clientX;
		const startT = tlTimeAt(startX);
		if (startT == null) return;
		TL_DRAG = { startX, startT, moved: false };
		const move = (ev) => {
			if (!TL_DRAG) return;
			if (Math.abs(ev.clientX - TL_DRAG.startX) > 4) TL_DRAG.moved = true;
			if (!TL_DRAG.moved) return;
			const t = tlTimeAt(ev.clientX);
			TL_SEL = { from: Math.min(TL_DRAG.startT, t), to: Math.max(TL_DRAG.startT, t) };
			tlPositionSel();
			renderTlAi();
		};
		const up = (ev) => {
			window.removeEventListener("pointermove", move);
			window.removeEventListener("pointerup", up);
			if (TL_DRAG && !TL_DRAG.moved) { TL_SEL = null; tlPositionSel(); renderTlAi(); tlSeek(TL_DRAG.startT); }
			TL_DRAG = null;
		};
		window.addEventListener("pointermove", move);
		window.addEventListener("pointerup", up);
	});
	if (head) {
		head.addEventListener("pointerdown", (e) => {
			if (e.button !== 0) return;
			e.stopPropagation();
			const move = (ev) => { const t = tlTimeAt(ev.clientX); if (t != null) tlSeek(t); };
			const up = () => {
				window.removeEventListener("pointermove", move);
				window.removeEventListener("pointerup", up);
			};
			window.addEventListener("pointermove", move);
			window.addEventListener("pointerup", up);
		});
		head.addEventListener("keydown", (e) => {
			if (TL_HEAD_AT == null || !TL_GEOM) return;
			const span = TL_GEOM.to - TL_GEOM.from;
			const step = (e.shiftKey ? span / 20 : span / 200);
			if (e.key === "ArrowLeft") { e.preventDefault(); tlSeek(TL_HEAD_AT - step); }
			else if (e.key === "ArrowRight") { e.preventDefault(); tlSeek(TL_HEAD_AT + step); }
			else if (e.key === "Escape") { e.preventDefault(); exitTimeTravel(); }
		});
	}
	chart.addEventListener("pointerover", (e) => {
		const mk = e.target.closest && e.target.closest(".tl-mk");
		if (!mk) return;
		tlShowTip(mk);
	});
	chart.addEventListener("pointerout", (e) => {
		const mk = e.target.closest && e.target.closest(".tl-mk");
		if (!mk) return;
		const tip = $("#tlTip");
		if (tip) tip.style.display = "none";
	});
	chart.addEventListener("click", (e) => {
		const mk = e.target.closest && e.target.closest(".tl-mk");
		if (!mk) return;
		const d = TL_MK[+mk.dataset.i];
		if (!d) return;
		if (d.lane === "security" && !d.url) tlOpenSecurity(d.fp);
	});
}

function tlShowTip(mk) {
	const tip = $("#tlTip"), chart = $("#tlChart");
	const d = TL_MK[+mk.dataset.i];
	if (!tip || !d || !chart) return;
	const laneLabel = (TL_LANES.find(([k]) => k === d.lane) || ["", d.lane])[1];
	tip.innerHTML = `<div class="tl-tip-h"><b>${escHtml(d.title)}</b><span>${escHtml(laneLabel)}</span></div>` +
		`<div class="tl-tip-at">${escHtml(tlStamp(d.at))}</div>` +
		d.lines.map((l) => `<div class="tl-tip-l">${escHtml(l)}</div>`).join("") +
		(d.url ? `<div class="tl-tip-l tl-tip-go">click to open ↗</div>` : d.lane === "security" ? `<div class="tl-tip-l tl-tip-go">click to open in Security</div>` : "");
	tip.style.display = "";
	const cr = chart.getBoundingClientRect();
	const mr = mk.getBoundingClientRect();
	const left = Math.max(4, Math.min((mr.left - cr.left) + 12, Math.max(4, (TL_GEOM?.W || 900) - 240)));
	tip.style.left = left.toFixed(1) + "px";
	tip.style.top = Math.max(4, (mr.top - cr.top) - 8) + "px";
}

function tlOpenSecurity(fp) {
	if (typeof switchTab !== "function") return;
	switchTab("viewSecurity");
	if (!fp) return;
	setTimeout(() => {
		const row = $$(".sec-find").find((e) => e.dataset.fp === String(fp));
		if (!row) return;
		row.scrollIntoView({ block: "center" });
		row.classList.add("tl-flash");
		setTimeout(() => row.classList.remove("tl-flash"), 1600);
	}, 500);
}

function tlSeek(t) {
	if (!TL_GEOM) return;
	TL_HEAD_AT = Math.min(TL_GEOM.to, Math.max(TL_GEOM.from, t));
	tlPositionHead();
	TL_STATE_LOADING = true;
	renderTlState();
	tlBarState();
	if (TL_SEEK_TIMER) clearTimeout(TL_SEEK_TIMER);
	TL_SEEK_TIMER = setTimeout(() => tlFetchState(TL_HEAD_AT), 150);
}

async function tlFetchState(at) {
	const seq = ++TL_STATE_SEQ;
	const envKey = ENV;
	let d = null;
	try {
		const r = await fetch(`/api/timeline/state?env=${encodeURIComponent(envKey)}&at=${encodeURIComponent(new Date(at).toISOString())}`, { cache: "no-store" });
		d = await r.json();
	} catch { d = { ok: false, reason: "server" }; }
	if (seq !== TL_STATE_SEQ || envKey !== ENV) return;
	TL_STATE_LOADING = false;
	TIME_STATE = d && d.ok ? { ...d, at: d.at || new Date(at).toISOString() } : null;
	renderTlState();
	tlBarState();
	tlSyncMap();
}

function tlSyncMap() {
	if (typeof applyNodeStatus === "function") applyNodeStatus();
	const badge = $("#tlTtBadge");
	if (badge) {
		if (TIME_STATE) {
			badge.style.display = "";
			badge.textContent = "time travel · " + tlStamp(TIME_STATE.at) + " · back to live";
		} else badge.style.display = "none";
	}
	document.body.classList.toggle("time-travel", !!TIME_STATE);
}

function timeTravelDots() {
	if (!TIME_STATE) return null;
	const dot = {};
	const e = TIME_STATE.ecs;
	if (e) {
		const running = tlNum(e.running), desired = tlNum(e.desired), memory = tlNum(e.memory);
		dot.ecs = running != null && desired != null && running < desired ? "c" : "g";
		if (dot.ecs === "g" && memory != null) {
			const memWarn = tlNum(SETTINGS?.memWarn) ?? 70, memCrit = tlNum(SETTINGS?.memCrit) ?? 90;
			if (memory >= memCrit) dot.ecs = "c";
			else if (memory > memWarn) dot.ecs = "w";
		}
	}
	const firing = (Array.isArray(TIME_STATE.alarms) ? TIME_STATE.alarms : [])
		.filter((a) => a.state === "ALARM" && !String(a.name || "").startsWith("TargetTracking"));
	dot.obs = firing.length ? "c" : "g";
	dot.alb = (tlNum(TIME_STATE.fivexx) || 0) > 0 ? "c" : "g";
	return dot;
}

function exitTimeTravel() {
	if (TL_SEEK_TIMER) { clearTimeout(TL_SEEK_TIMER); TL_SEEK_TIMER = null; }
	TL_STATE_SEQ++;
	TL_STATE_LOADING = false;
	TL_HEAD_AT = null;
	TIME_STATE = null;
	tlPositionHead();
	renderTlState();
	tlBarState();
	tlSyncMap();
}

function renderTlState() {
	const el = $("#tlState");
	if (!el) return;
	if (TL_HEAD_AT == null) {
		el.className = "tl-state";
		el.innerHTML = `<div class="tl-state-idle">Click anywhere on the chart, or drag the playhead, to reconstruct the system at that moment. The architecture map follows the playhead.</div>`;
		return;
	}
	if (TL_STATE_LOADING || !TIME_STATE) {
		el.className = "tl-state loading";
		el.innerHTML = `<div class="tl-state-h">State at ${escHtml(tlStamp(TL_HEAD_AT))}</div><div class="tl-state-idle">${TL_STATE_LOADING ? "reading the estate at that moment…" : "no reconstructed state for this moment"}</div>`;
		return;
	}
	const s = TIME_STATE;
	const ecs = s.ecs || null;
	const mem = tlNum(ecs?.memory), cpu = tlNum(ecs?.cpu);
	const memWarn = tlNum(SETTINGS?.memWarn) ?? 70, memCrit = tlNum(SETTINGS?.memCrit) ?? 90;
	const firing = (Array.isArray(s.alarms) ? s.alarms : []).filter((a) => a.state === "ALARM");
	const fx = tlNum(s.fivexx) || 0;
	const cell = (num, lbl, cls) => `<div class="tile ${cls || ""}"><div class="num">${num}</div><div class="lbl">${escHtml(lbl)}</div></div>`;
	let tiles = "";
	if (ecs) tiles += cell(`${escHtml(String(ecs.running ?? "–"))}<span class="u">/${escHtml(String(ecs.desired ?? "–"))}</span>`, "tasks running", tlNum(ecs.running) != null && tlNum(ecs.desired) != null && ecs.running < ecs.desired ? "t-crit" : "");
	if (mem != null) tiles += cell(`${mem}<span class="u">%</span>`, "memory (max task)", mem >= memCrit ? "t-crit" : mem > memWarn ? "t-warn" : "");
	if (cpu != null) tiles += cell(`${cpu}<span class="u">%</span>`, "cpu (max task)", cpu > 85 ? "t-warn" : "");
	tiles += cell(String(fx), "5xx in that minute", fx > 0 ? "t-crit" : "");
	tiles += cell(String(firing.length), "alarms firing", firing.length ? "t-crit" : "");
	const dep = s.deployWithin1h;
	let depTxt = "";
	if (dep && dep.at) {
		const mins = Math.max(0, Math.round((+new Date(s.at) - +new Date(dep.at)) / 60000));
		depTxt = `<div class="tl-state-dep">deployed <code>${escHtml(String(dep.sha || "").slice(0, 10))}</code>${dep.pr ? ` · PR #${escHtml(String(dep.pr))}` : ""} ${mins}m before this moment</div>`;
	}
	const alarmTxt = firing.length
		? `<div class="tl-state-al">${firing.map((a) => `<span class="tl-al">${escHtml(a.name)}</span>`).join("")}</div>`
		: "";
	el.className = "tl-state on";
	el.innerHTML = `<div class="tl-state-h">State at ${escHtml(tlStamp(s.at))}
		<button class="btn" id="tlBackLive2">Back to live</button></div>
		<div class="tiles tl-state-tiles">${tiles}</div>${depTxt}${alarmTxt}`;
	const b = $("#tlBackLive2");
	if (b) b.onclick = exitTimeTravel;
}

function tlAiRange() {
	if (TL_SEL) return { from: TL_SEL.from, to: TL_SEL.to, sel: true };
	if (TL_GEOM) return { from: TL_GEOM.from, to: TL_GEOM.to, sel: false };
	const w = tlWindow();
	return { from: w.from, to: w.to, sel: false };
}

function renderTlAi() {
	const r = tlAiRange();
	const lbl = $("#tlAiRange");
	if (lbl) lbl.textContent = `${r.sel ? "selection" : "whole window"} · ${tlStamp(r.from)} → ${tlStamp(r.to)}`;
	const clear = $("#tlClearSel");
	if (clear) clear.style.display = TL_SEL ? "" : "none";
	$$("#tlExplain, #tlPost").forEach((b) => b.classList.toggle("on", !!TL_SEL));
	const ex = $("#tlExplain");
	if (ex) { ex.disabled = TL_AI_BUSY; ex.onclick = () => tlRunIncident(false); }
	const rg = $("#tlRegen");
	if (rg) { rg.style.display = TL_AI_AT ? "" : "none"; rg.disabled = TL_AI_BUSY; rg.onclick = () => tlRunIncident(true); }
	const pm = $("#tlPost");
	if (pm) { pm.disabled = TL_PM_BUSY; pm.onclick = () => tlRunPostmortem(false); }
	const pmr = $("#tlPmRegen");
	if (pmr) { pmr.disabled = TL_PM_BUSY; pmr.onclick = () => tlRunPostmortem(true); }
	const pmc = $("#tlPmCopy");
	if (pmc) pmc.onclick = () => tlCopyPostmortem(pmc);
	if (clear) clear.onclick = () => { TL_SEL = null; tlPositionSel(); renderTlAi(); };
	const box = $("#tlAiBox");
	if (box && !TL_AI_BUSY && TL_AI_MD) { box.style.display = ""; box.className = "ai-box"; box.innerHTML = TL_AI_MD; bindCopies(box); }
	const meta = $("#tlAiMeta");
	if (meta && TL_AI_AT) meta.textContent = "generated " + agoTxt(TL_AI_AT);
	const wrap = $("#tlPmWrap");
	if (wrap && TL_PM_MD && !TL_PM_BUSY) {
		wrap.style.display = "";
		const pb = $("#tlPmBox");
		if (pb) { pb.className = "ai-box"; pb.innerHTML = TL_PM_MD; bindCopies(pb); }
		const pmeta = $("#tlPmMeta");
		if (pmeta && TL_PM_AT) pmeta.textContent = "generated " + agoTxt(TL_PM_AT);
	}
}

async function tlPostAi(path, fresh, body) {
	const r = await fetch(`${path}${fresh ? "?fresh=1" : ""}`, {
		method: "POST",
		headers: { "content-type": "application/json" },
		body: JSON.stringify(body),
	});
	return await r.json();
}

async function tlRunIncident(fresh) {
	if (TL_AI_BUSY) return;
	TL_AI_BUSY = true;
	const r = tlAiRange();
	const box = $("#tlAiBox"), btn = $("#tlExplain"), rg = $("#tlRegen");
	if (btn) btn.disabled = true;
	if (rg) rg.disabled = true;
	if (box) {
		box.style.display = "";
		box.className = "ai-box thinking";
		box.textContent = "Reading deploys, alarms, changes and metrics for this window — writing the narrative…";
	}
	let d = null;
	try {
		d = await tlPostAi("/api/ai/incident", fresh, { env: ENV, from: new Date(r.from).toISOString(), to: new Date(r.to).toISOString() });
	} catch { d = null; }
	TL_AI_BUSY = false;
	if (d && d.ok) { TL_AI_MD = md(d.answer || ""); TL_AI_AT = d.generatedAt || Date.now(); }
	else { TL_AI_MD = escHtml((d && d.detail) || "analysis unavailable — is the sync server running?"); TL_AI_AT = 0; }
	const box2 = $("#tlAiBox");
	if (box2) { box2.className = "ai-box"; box2.style.display = ""; box2.innerHTML = TL_AI_MD; bindCopies(box2); }
	const meta = $("#tlAiMeta");
	if (meta) meta.textContent = TL_AI_AT ? "generated " + agoTxt(TL_AI_AT) : "";
	renderTlAi();
}

async function tlRunPostmortem(fresh) {
	if (TL_PM_BUSY) return;
	TL_PM_BUSY = true;
	const r = tlAiRange();
	const wrap = $("#tlPmWrap"), box = $("#tlPmBox"), btn = $("#tlPost");
	if (btn) btn.disabled = true;
	if (wrap) wrap.style.display = "";
	if (box) { box.className = "ai-box thinking"; box.textContent = "Drafting the postmortem for this window…"; }
	let d = null;
	try {
		d = await tlPostAi("/api/ai/postmortem", fresh, { env: ENV, from: new Date(r.from).toISOString(), to: new Date(r.to).toISOString() });
	} catch { d = null; }
	TL_PM_BUSY = false;
	if (d && d.ok) { TL_PM_RAW = d.markdown || ""; TL_PM_MD = md(TL_PM_RAW); TL_PM_AT = d.generatedAt || Date.now(); }
	else { TL_PM_RAW = ""; TL_PM_MD = escHtml((d && d.detail) || "postmortem unavailable — is the sync server running?"); TL_PM_AT = 0; }
	const box2 = $("#tlPmBox");
	if (box2) { box2.className = "ai-box"; box2.innerHTML = TL_PM_MD; bindCopies(box2); }
	const meta = $("#tlPmMeta");
	if (meta) meta.textContent = TL_PM_AT ? "generated " + agoTxt(TL_PM_AT) : "";
	renderTlAi();
}

function tlCopyPostmortem(btn) {
	if (!TL_PM_RAW) return;
	try { navigator.clipboard.writeText(TL_PM_RAW); } catch { }
	btn.textContent = "Copied ✓";
	setTimeout(() => { btn.textContent = "Copy markdown"; }, 1200);
}

function tlApplyCustom() {
	const f = $("#tlFrom"), t = $("#tlTo");
	if (!f || !t) return;
	const from = +new Date(f.value), to = +new Date(t.value);
	const msg = $("#tlWindow");
	if (!isFinite(from) || !isFinite(to) || to <= from) {
		if (msg) msg.textContent = "pick a valid range — the end must be after the start";
		return;
	}
	TL_CUSTOM = { from, to };
	TL_RANGE = "custom";
	tlSaveRange();
	exitTimeTravel();
	fetchTimeline(false);
}

function tlBindBar() {
	const apply = $("#tlApply");
	if (apply) apply.onclick = tlApplyCustom;
	const refresh = $("#tlRefresh");
	if (refresh) refresh.onclick = () => fetchTimeline(true);
	const live = $("#tlLive");
	if (live) live.onclick = exitTimeTravel;
	const badge = $("#tlTtBadge");
	if (badge) badge.onclick = exitTimeTravel;
	window.addEventListener("resize", () => {
		if (!$("#viewTimeline")?.classList.contains("active") || !TL?.ok) return;
		if (TL_RESIZE) clearTimeout(TL_RESIZE);
		TL_RESIZE = setTimeout(() => drawTimeline(), 120);
	});
}
tlBindBar();
