let _lastMintedToken = ""; function showNotice(msg, isError) { const banner = document.getElementById("status-banner"); if (!banner) return; banner.textContent = msg; banner.style.display = "block"; banner.style.position = "fixed"; banner.style.top = "1.25rem"; banner.style.right = "1.25rem"; banner.style.zIndex = "99999"; banner.style.minWidth = "280px"; banner.style.maxWidth = "480px"; banner.style.padding = "0.85rem 1.25rem"; banner.style.borderRadius = "var(--radius-md)"; banner.style.boxShadow = "0 10px 25px -5px rgba(0, 0, 0, 0.35)"; banner.style.fontSize = "0.9rem"; banner.style.fontWeight = "600"; banner.style.background = isError ? "var(--danger-bg)" : "var(--success-bg)"; banner.style.color = isError ? "var(--danger-text)" : "var(--success-text)"; banner.style.border = isError ? "1px solid var(--danger-border)" : "1px solid var(--success-border)"; if (banner._timeout) clearTimeout(banner._timeout); banner._timeout = setTimeout(() => { banner.style.display = "none"; }, 4000); } globalThis.showNotice = showNotice; function setTabLock(locked) { const btnDirect = document.getElementById("tabBtnDirectPass"); const btnWorkshop = document.getElementById("tabBtnWorkshopPass"); if (btnDirect) { btnDirect.disabled = locked; btnDirect.style.pointerEvents = locked ? "none" : "auto"; btnDirect.style.opacity = locked ? "0.5" : "1"; } if (btnWorkshop) { btnWorkshop.disabled = locked; btnWorkshop.style.pointerEvents = locked ? "none" : "auto"; btnWorkshop.style.opacity = locked ? "0.5" : "1"; } } function resetDirectPassForm() { const form = document.getElementById("delegateForm"); if (form) form.reset(); const createEl = document.getElementById("singleSessionCreateState"); if (createEl) createEl.style.display = "block"; const handoffEl = document.getElementById("handoffModal"); if (handoffEl) handoffEl.style.display = "none"; setTabLock(false); } globalThis.resetDirectPassForm = resetDirectPassForm; function resetWorkshopForm() { const form = document.getElementById("eventForm"); if (form) form.reset(); const createEl = document.getElementById("eventCreateState"); if (createEl) createEl.style.display = "block"; const handoffEl = document.getElementById("eventHandoffState"); if (handoffEl) handoffEl.style.display = "none"; setTabLock(false); } globalThis.resetWorkshopForm = resetWorkshopForm; function openDelegateDrawer() { const drawer = document.getElementById("delegateDrawer"); if (!drawer) return; drawer.style.display = "block"; drawer.scrollIntoView({ behavior: "smooth" }); resetDirectPassForm(); resetWorkshopForm(); switchDelegateTab("tabDirectPass"); } globalThis.openDelegateDrawer = openDelegateDrawer; function closeDelegateDrawer() { const drawer = document.getElementById("delegateDrawer"); if (drawer) drawer.style.display = "none"; resetDirectPassForm(); resetWorkshopForm(); } globalThis.closeDelegateDrawer = closeDelegateDrawer; function switchDelegateTab(tabId) { const directTab = document.getElementById("tabDirectPass"); const workshopTab = document.getElementById("tabWorkshopPass"); if (directTab) directTab.style.display = "none"; if (workshopTab) workshopTab.style.display = "none"; const btnDirect = document.getElementById("tabBtnDirectPass"); const btnWorkshop = document.getElementById("tabBtnWorkshopPass"); if (btnDirect) { btnDirect.classList.remove("active"); btnDirect.setAttribute("aria-selected", "false"); } if (btnWorkshop) { btnWorkshop.classList.remove("active"); btnWorkshop.setAttribute("aria-selected", "false"); } const activeTab = document.getElementById(tabId); if (activeTab) activeTab.style.display = "block"; if (tabId === "tabDirectPass" && btnDirect) { btnDirect.classList.add("active"); btnDirect.setAttribute("aria-selected", "true"); } else if (btnWorkshop) { btnWorkshop.classList.add("active"); btnWorkshop.setAttribute("aria-selected", "true"); } } globalThis.switchDelegateTab = switchDelegateTab; function selectSeats(btn, count) { document.querySelectorAll(".seat-pill").forEach((b) => b.classList.remove("active") ); btn.classList.add("active"); const input = document.getElementById("eventMaxSeats"); if (input) input.value = count; } globalThis.selectSeats = selectSeats; function selectEventLifespan(btn, hours) { document.querySelectorAll(".event-lifespan-pill").forEach((b) => b.classList.remove("active") ); btn.classList.add("active"); const input = document.getElementById("eventLifespanHours"); if (input) input.value = hours; } globalThis.selectEventLifespan = selectEventLifespan; function selectLifespan(btn, hours) { document.querySelectorAll(".lifespan-pill").forEach((b) => b.classList.remove("active") ); btn.classList.add("active"); const input = document.getElementById("delegateHours"); if (input) input.value = hours; } globalThis.selectLifespan = selectLifespan; function selectMode(btn, mode) { document.querySelectorAll(".mode-pill").forEach((b) => b.classList.remove("active") ); btn.classList.add("active"); const input = document.getElementById("delegateMode"); if (input) input.value = mode; const accordion = document.getElementById("customScopesSection"); if (accordion && mode === "custom") { accordion.open = true; } } globalThis.selectMode = selectMode; function handleTargetAppChange(appName) { if (appName) { const scopeVal = "app:" + appName; document.querySelectorAll(".scope-checkbox").forEach((cb) => { if (cb.value === scopeVal) cb.checked = true; }); } } globalThis.handleTargetAppChange = handleTargetAppChange; async function handleDelegateSession(e) { e.preventDefault(); const labelInput = document.getElementById("delegateLabel"); const label = labelInput ? labelInput.value.trim() : "Delegated Session"; const hoursInput = document.getElementById("delegateHours"); const lifespanHours = parseInt(hoursInput ? hoursInput.value : "1", 10) || 1; const modeInput = document.getElementById("delegateMode"); let mode = modeInput ? modeInput.value : "read"; const targetAppInput = document.getElementById("delegateTargetApp"); const targetApp = targetAppInput ? targetAppInput.value : ""; const customScopes = []; if (targetApp) { customScopes.push("app:" + targetApp); } if (mode === "custom" || targetApp) { document.querySelectorAll(".scope-checkbox:checked").forEach((cb) => { if (!customScopes.includes(cb.value)) { customScopes.push(cb.value); } }); if (targetApp && mode !== "custom" && mode !== "admin") { mode = "custom"; } } const btn = document.getElementById("submitDelegateBtn"); if (btn) { btn.disabled = true; btn.textContent = "Creating Session..."; } try { const res = await fetch("/api/sessions/delegate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ label, lifespanHours, mode, customScopes }), }); const data = await res.json(); if (res.ok) { _lastMintedToken = data.token; const origin = globalThis.location ? globalThis.location.origin : ""; const magicElem = document.getElementById("handoffMagicLinkText"); if (magicElem) { magicElem.textContent = origin + "/pass?token=" + data.token; } const cliElem = document.getElementById("handoffCliText"); if (cliElem) { cliElem.textContent = 'export AUTH_YES_TOKEN="' + data.token + '"'; } const curlElem = document.getElementById("handoffCurlText"); if (curlElem) { curlElem.textContent = '-H "Authorization: Bearer ' + data.token + '"'; } const createEl = document.getElementById("singleSessionCreateState"); if (createEl) createEl.style.display = "none"; const handoffModal = document.getElementById("handoffModal"); if (handoffModal) handoffModal.style.display = "block"; setTabLock(true); showNotice("Delegated session created for: " + data.label, false); } else { showNotice(data.error || "Failed to delegate session", true); } } catch (_err) { showNotice("Network error delegating session", true); } finally { if (btn) { btn.disabled = false; btn.textContent = "Create Session"; } } } globalThis.handleDelegateSession = handleDelegateSession; async function handleCreateEvent(e) { e.preventDefault(); const nameInput = document.getElementById("eventName"); const name = nameInput ? nameInput.value.trim() : ""; const appIdInput = document.getElementById("eventAppId"); const appId = appIdInput ? appIdInput.value || null : null; const roleInput = document.getElementById("eventRole"); const role = roleInput ? roleInput.value || "viewer" : "viewer"; const maxSeatsInput = document.getElementById("eventMaxSeats"); const maxSeats = parseInt(maxSeatsInput ? maxSeatsInput.value : "0", 10) || 0; const lifespanHoursInput = document.getElementById("eventLifespanHours"); const lifespanHours = parseInt(lifespanHoursInput ? lifespanHoursInput.value : "3", 10) || 3; const slugInput = document.getElementById("eventSlug"); const slug = slugInput ? slugInput.value.trim() || undefined : undefined; const pinInput = document.getElementById("eventPinCode"); const pinCode = pinInput ? pinInput.value.trim() || undefined : undefined; const btn = document.getElementById("submitEventBtn"); if (btn) { btn.disabled = true; btn.textContent = "Creating Event..."; } try { const res = await fetch("/api/events", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, appId, role, maxSeats, lifespanHours, slug, pinCode, }), }); const data = await res.json(); if (res.ok && data.success) { const ev = data.event; const origin = globalThis.location ? globalThis.location.origin : ""; const titleElem = document.getElementById("createdEventTitle"); if (titleElem) titleElem.textContent = ev.name + " Live!"; const pinElem = document.getElementById("eventHandoffPin"); if (pinElem) pinElem.textContent = ev.pin_code; const linkElem = document.getElementById("eventHandoffLink"); if (linkElem) linkElem.textContent = origin + "/e/" + ev.slug; const cliElem = document.getElementById("eventHandoffCli"); if (cliElem) { cliElem.textContent = "curl -sSL " + origin + "/join/" + ev.slug + "?format=env | source /dev/stdin"; } const createState = document.getElementById("eventCreateState"); if (createState) createState.style.display = "none"; const handoffState = document.getElementById("eventHandoffState"); if (handoffState) handoffState.style.display = "block"; setTabLock(true); showNotice("Workshop pass created: " + ev.name, false); } else { showNotice(data.error || "Failed to create event pass", true); } } catch (_err) { showNotice("Network error creating event pass", true); } finally { if (btn) { btn.disabled = false; btn.textContent = "Create Event"; } } } globalThis.handleCreateEvent = handleCreateEvent; function copyEventHandoff(type) { let text = ""; if (type === "pin") { const el = document.getElementById("eventHandoffPin"); if (el) text = el.textContent; } if (type === "link") { const el = document.getElementById("eventHandoffLink"); if (el) text = el.textContent; } if (type === "cli") { const el = document.getElementById("eventHandoffCli"); if (el) text = el.textContent; } if (navigator.clipboard) { navigator.clipboard.writeText(text); showNotice("Copied to clipboard: " + text, false); } } globalThis.copyEventHandoff = copyEventHandoff; function closeEventHandoffModal() { closeDelegateDrawer(); if (globalThis.location) globalThis.location.reload(); } globalThis.closeEventHandoffModal = closeEventHandoffModal; function copyHandoff(type) { let text = ""; if (type === "cli") { const el = document.getElementById("handoffCliText"); if (el) text = el.textContent; } if (type === "curl") { const el = document.getElementById("handoffCurlText"); if (el) text = el.textContent; } if (type === "magic") { const el = document.getElementById("handoffMagicLinkText"); if (el) text = el.textContent; } if (navigator.clipboard) { navigator.clipboard.writeText(text); showNotice("Copied to clipboard: " + text, false); } } globalThis.copyHandoff = copyHandoff; function closeHandoffModal() { resetDirectPassForm(); const drawer = document.getElementById("delegateDrawer"); if (drawer) drawer.style.display = "none"; if (globalThis.location) globalThis.location.reload(); } globalThis.closeHandoffModal = closeHandoffModal; async function extendSession(sessionId, hours) { try { const res = await fetch("/api/sessions/" + sessionId + "/extend", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ extendHours: hours }), }); if (res.ok) { showNotice("Session extended by " + hours + " hour(s)!", false); setTimeout(() => { if (globalThis.location) globalThis.location.reload(); }, 600); } else { const data = await res.json(); showNotice(data.error || "Failed to extend session", true); } } catch (_err) { showNotice("Network error extending session", true); } } globalThis.extendSession = extendSession; function openEditScopesModal(sessionId, label, scopesJson) { let scopes = []; try { scopes = typeof scopesJson === "string" ? JSON.parse(scopesJson) : scopesJson; if (typeof scopes === "string") scopes = JSON.parse(scopes); } catch (_e) { scopes = []; } const idInput = document.getElementById("editScopesSessionId"); if (idInput) idInput.value = sessionId; const labelEl = document.getElementById("editScopesLabel"); if (labelEl) labelEl.textContent = label; document.querySelectorAll(".edit-scope-chk").forEach((cb) => { cb.checked = Array.isArray(scopes) && scopes.includes(cb.value); }); const modal = document.getElementById("editScopesModal"); if (modal) modal.style.display = "flex"; } globalThis.openEditScopesModal = openEditScopesModal; function closeEditScopesModal() { const modal = document.getElementById("editScopesModal"); if (modal) modal.style.display = "none"; } globalThis.closeEditScopesModal = closeEditScopesModal; async function saveUpdatedScopes() { const idInput = document.getElementById("editScopesSessionId"); const sessionId = idInput ? idInput.value : ""; const customScopes = []; document.querySelectorAll(".edit-scope-chk:checked").forEach((cb) => { customScopes.push(cb.value); }); try { const res = await fetch("/api/sessions/" + sessionId + "/scopes", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ customScopes }), }); if (res.ok) { showNotice("Session scopes updated!", false); setTimeout(() => { if (globalThis.location) globalThis.location.reload(); }, 500); } else { const data = await res.json(); showNotice(data.error || "Failed to update scopes", true); } } catch (_err) { showNotice("Network error updating scopes", true); } } globalThis.saveUpdatedScopes = saveUpdatedScopes; function copyText(text) { if (navigator.clipboard) { navigator.clipboard.writeText(text); showNotice("Copied to clipboard!", false); } } globalThis.copyText = copyText; async function rotatePin(eventId) { try { const res = await fetch("/api/events/" + eventId + "/rotate-pin", { method: "POST", }); const data = await res.json(); if (res.ok && data.success) { const pinElem = document.getElementById("pin-" + eventId); if (pinElem) pinElem.textContent = data.pinCode; const compactPinElem = document.getElementById( "compact-pin-" + eventId, ); if (compactPinElem) compactPinElem.textContent = data.pinCode; if (data.slug) { const origin = globalThis.location ? globalThis.location.origin : ""; const linkUrl = origin + "/e/" + data.slug; const cliCmd = "curl -sSL " + origin + "/join/" + data.slug + "?format=env | source /dev/stdin"; const linkCodeElem = document.getElementById("link-code-" + eventId); if (linkCodeElem) linkCodeElem.textContent = "/e/" + data.slug; const linkBtnElem = document.getElementById("link-btn-" + eventId); if (linkBtnElem) linkBtnElem.onclick = () => copyText(linkUrl); const cliCodeElem = document.getElementById("cli-code-" + eventId); if (cliCodeElem) cliCodeElem.textContent = cliCmd; const cliBtnElem = document.getElementById("cli-btn-" + eventId); if (cliBtnElem) { cliBtnElem.onclick = (e) => { if (e && e.stopPropagation) e.stopPropagation(); copyText(cliCmd); }; } const compactLinkElem = document.getElementById( "compact-link-" + eventId, ); if (compactLinkElem) { compactLinkElem.onclick = () => copyText(linkUrl); } const compactCliElem = document.getElementById( "compact-cli-" + eventId, ); if (compactCliElem) { compactCliElem.onclick = () => copyText(cliCmd); } } showNotice("PIN rotated successfully", false); } else { showNotice(data.error || "Failed to rotate PIN", true); } } catch (_err) { showNotice("Network error rotating PIN", true); } } globalThis.rotatePin = rotatePin; async function expandSeats(eventId, count) { try { const res = await fetch("/api/events/" + eventId + "/expand", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ addSeats: count }), }); const data = await res.json(); if (res.ok && data.success) { showNotice("Expanded capacity by " + count + " seats", false); setTimeout(() => { if (globalThis.location) globalThis.location.reload(); }, 600); } else { showNotice(data.error || "Failed to expand seats", true); } } catch (_err) { showNotice("Network error expanding seats", true); } } globalThis.expandSeats = expandSeats; function closeAttendeesDrawer() { const drawer = document.getElementById("attendeesDrawer"); if (drawer) { drawer.classList.remove("open"); const panel = drawer.querySelector(".drawer-panel"); if (panel) panel.classList.remove("open"); setTimeout(() => { drawer.style.display = "none"; }, 250); } } globalThis.closeAttendeesDrawer = closeAttendeesDrawer; function formatNaturalExpiry(expiresAt) { const expDate = new Date(expiresAt); const now = new Date(); const diffMs = expDate.getTime() - now.getTime(); const timeStr = expDate.toLocaleTimeString([], { hour: "numeric", minute: "2-digit", }); const fullISO = expDate.toISOString(); let dateStr = ""; const isToday = expDate.getDate() === now.getDate() && expDate.getMonth() === now.getMonth() && expDate.getFullYear() === now.getFullYear(); const tomorrow = new Date(now); tomorrow.setDate(tomorrow.getDate() + 1); const isTomorrow = expDate.getDate() === tomorrow.getDate() && expDate.getMonth() === tomorrow.getMonth() && expDate.getFullYear() === tomorrow.getFullYear(); if (isToday) { dateStr = "Today"; } else if (isTomorrow) { dateStr = "Tomorrow"; } else if (expDate.getFullYear() === now.getFullYear()) { dateStr = expDate.toLocaleDateString([], { month: "short", day: "numeric", }); } else { dateStr = expDate.toLocaleDateString([], { month: "short", day: "numeric", year: "numeric", }); } let badge = ""; if (diffMs <= 0) { badge = 'Expired'; } else { const totalMins = Math.floor(diffMs / 60000); const hours = totalMins / 60; const days = hours / 24; const months = days / 30; const years = days / 365; let timeRemainingStr = ""; let badgeStyle = "background:rgba(34,197,94,0.15);color:#16a34a;padding:2px 6px;border-radius:4px;font-size:0.75rem;font-weight:600;"; if (hours < 1) { timeRemainingStr = totalMins + "m left"; badgeStyle = "background:rgba(245,158,11,0.15);color:#d97706;padding:2px 6px;border-radius:4px;font-size:0.75rem;font-weight:600;"; } else if (hours < 24) { const h = Math.floor(hours); const m = totalMins % 60; timeRemainingStr = h + "h " + m + "m left"; badgeStyle = "background:rgba(245,158,11,0.15);color:#d97706;padding:2px 6px;border-radius:4px;font-size:0.75rem;font-weight:600;"; } else if (days <= 60) { timeRemainingStr = Math.floor(days) + "d left"; } else if (months <= 12) { timeRemainingStr = months.toFixed(1) + " mos left"; } else { timeRemainingStr = years.toFixed(1) + " yrs left"; } badge = '' + timeRemainingStr + ""; } return dateStr + " · " + timeStr + " · " + badge; } globalThis.formatNaturalExpiry = formatNaturalExpiry; function formatNaturalJoinTime(createdAt, isActive) { const createdDate = new Date(createdAt); const now = new Date(); const diffMs = now.getTime() - createdDate.getTime(); const diffMins = Math.floor(diffMs / 60000); const timeStr = createdDate.toLocaleTimeString([], { hour: "numeric", minute: "2-digit", }); const isToday = createdDate.getDate() === now.getDate() && expDateMatches(createdDate, now); const dateStr = isToday ? "Today" : createdDate.toLocaleDateString([], { month: "short", day: "numeric", }); let durationStr = ""; if (diffMins < 60) { durationStr = diffMins + "m"; } else { const h = Math.floor(diffMins / 60); const m = diffMins % 60; durationStr = h + "h " + m + "m"; } const badgeStyle = isActive ? "background:rgba(34,197,94,0.15);color:#16a34a;padding:2px 6px;border-radius:4px;font-size:0.75rem;font-weight:600;" : "background:rgba(234,179,8,0.15);color:#ca8a04;padding:2px 6px;border-radius:4px;font-size:0.75rem;font-weight:600;"; const badgeText = isActive ? "Active " + durationStr : "Paused"; const activeBadge = '' + badgeText + ""; return "Joined " + dateStr + " · " + timeStr + " · " + activeBadge; } function expDateMatches(a, b) { return a.getMonth() === b.getMonth() && a.getFullYear() === b.getFullYear(); } globalThis.formatNaturalJoinTime = formatNaturalJoinTime; function updateAllCountdowns() { const pills = document.querySelectorAll( ".countdown-pill, #guestDrawerCountdown", ); pills.forEach((pill) => { const expiresAtStr = pill.getAttribute("data-expires-at"); if (!expiresAtStr) return; const now = new Date(); const expDate = new Date(expiresAtStr); const diffMs = expDate.getTime() - now.getTime(); if (diffMs <= 0) { pill.textContent = "⏳ Expired"; } else { const totalMins = Math.floor(diffMs / 60000); const hours = Math.floor(totalMins / 60); const mins = totalMins % 60; pill.textContent = "⏳ " + hours + "h " + mins + "m left"; } }); } globalThis.updateAllCountdowns = updateAllCountdowns; async function openAttendeesDrawer(eventId, eventName) { const titleEl = document.getElementById("guestDrawerTitle"); if (titleEl) titleEl.textContent = eventName + " Guests"; const contentEl = document.getElementById("attendeesDrawerContent"); if (contentEl) { contentEl.innerHTML = '
Loading attendees...
'; } const drawer = document.getElementById("attendeesDrawer"); if (drawer) { drawer.style.display = "block"; drawer.offsetHeight; drawer.classList.add("open"); const panel = drawer.querySelector(".drawer-panel"); if (panel) panel.classList.add("open"); } try { const res = await fetch("/api/events/" + eventId + "/attendees"); const data = await res.json(); if (res.ok && data.success) { const attendees = data.attendees || []; const claimedElem = document.getElementById("guestDrawerClaimed"); if (claimedElem) claimedElem.textContent = attendees.length; if (data.event) { const maxElem = document.getElementById("guestDrawerMax"); if (maxElem) maxElem.textContent = data.event.max_seats; const expiresAtElem = document.getElementById("guestDrawerExpiresAt"); if (expiresAtElem && data.event.expires_at) { const expDate = new Date(data.event.expires_at); expiresAtElem.textContent = expDate.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", }); } const countdownElem = document.getElementById("guestDrawerCountdown"); if (countdownElem && data.event.expires_at) { countdownElem.setAttribute("data-expires-at", data.event.expires_at); } updateAllCountdowns(); } const contentElem = document.getElementById("attendeesDrawerContent"); if (attendees.length === 0) { if (contentElem) { contentElem.innerHTML = '
No attendees currently active.
'; } return; } let html = '
'; for (const att of attendees) { const isPaused = att.is_paused === true; const usernameParts = (att.username || "").split("_"); const seatNumber = usernameParts.length > 2 ? usernameParts[usernameParts.length - 1] : "?"; const joinText = formatNaturalJoinTime(att.created_at, !isPaused); const lastAction = att.last_activity_action || "ForwardAuth Ingress"; let timeAgo = "just now"; if (att.last_activity_at) { const lastActDate = new Date(att.last_activity_at); const diffMinsAct = Math.floor( (Date.now() - lastActDate.getTime()) / 60000, ); timeAgo = diffMinsAct < 1 ? "just now" : diffMinsAct + "m ago"; } const clientIcon = lastAction.includes("CLI") ? "📟 CLI" : "💻 Web"; html += `
Seat #${seatNumber}
${isPaused ? "⏸️ Paused" : "🟢 Active"}
${joinText}
Last Action: ${lastAction} · ${timeAgo} · ${clientIcon}
`; } html += "
"; contentElem.innerHTML = html; contentElem.querySelectorAll(".revoke-attendee-btn").forEach((btn) => { btn.addEventListener("click", async (e) => { if (!confirm("Revoke this session immediately?")) return; const sessionId = e.currentTarget.getAttribute("data-session-id"); const originalText = e.currentTarget.textContent; e.currentTarget.textContent = "Revoking..."; e.currentTarget.disabled = true; try { const revokeRes = await fetch("/api/sessions/" + sessionId, { method: "DELETE", }); if (revokeRes.ok) { openAttendeesDrawer(eventId, eventName); } else { const revokeData = await revokeRes.json(); showNotice( revokeData.error || "Failed to revoke session", true, ); e.currentTarget.textContent = originalText; e.currentTarget.disabled = false; } } catch (_err) { showNotice("Network error revoking session", true); e.currentTarget.textContent = originalText; e.currentTarget.disabled = false; } }); }); } else { const contentElem = document.getElementById("attendeesDrawerContent"); if (contentElem) { contentElem.innerHTML = '
Failed to load attendees: ' + (data.error || "HTTP " + res.status) + "
"; } } } catch (err) { const contentElem = document.getElementById("attendeesDrawerContent"); if (contentElem) { contentElem.innerHTML = '
Error loading attendees: ' + (err && err.message ? err.message : "Network failure") + "
"; } } } globalThis.openAttendeesDrawer = openAttendeesDrawer; async function toggleSessionPause(sessionId, shouldPause, eventId, eventName) { try { const res = await fetch("/api/sessions/" + sessionId + "/pause", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ is_paused: shouldPause }), }); const data = await res.json(); if (res.ok && data.success) { showNotice( "Session " + (shouldPause ? "paused" : "unpaused") + " successfully", false, ); openAttendeesDrawer(eventId, eventName); } else { showNotice(data.error || "Failed to toggle pause state", true); } } catch (_err) { showNotice("Network error toggling pause state", true); } } globalThis.toggleSessionPause = toggleSessionPause; function setEventViewMode(mode) { const container = document.getElementById("eventDeckContainer"); const gridBtn = document.getElementById("viewModeGrid"); const compactBtn = document.getElementById("viewModeCompact"); if (container && gridBtn && compactBtn) { if (mode === "compact") { container.classList.remove("grid-view"); container.classList.add("compact-view"); gridBtn.classList.remove("active"); compactBtn.classList.add("active"); try { localStorage.setItem("auth_yes_event_view_mode", "compact"); } catch (_e) {} } else { container.classList.remove("compact-view"); container.classList.add("grid-view"); compactBtn.classList.remove("active"); gridBtn.classList.add("active"); try { localStorage.setItem("auth_yes_event_view_mode", "grid"); } catch (_e) {} } } } globalThis.setEventViewMode = setEventViewMode; function bindEventCockpitActions() { document.querySelectorAll(".extend-event-btn").forEach((btn) => { btn.addEventListener("click", async (e) => { const eventId = e.currentTarget.getAttribute("data-event-id"); e.currentTarget.disabled = true; try { const res = await fetch("/api/events/" + eventId + "/extend", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ extendHours: 1 }), }); const data = await res.json().catch(() => ({})); if (res.ok && data.success) { showNotice("Event extended by 1 hour!", false); const drawer = document.getElementById("attendeesDrawer"); if (drawer && drawer.classList.contains("open")) { const countdownElem = document.getElementById( "guestDrawerCountdown", ); if (countdownElem && data.event && data.event.expires_at) { countdownElem.setAttribute( "data-expires-at", data.event.expires_at, ); const expDate = new Date(data.event.expires_at); const expiresAtElem = document.getElementById( "guestDrawerExpiresAt", ); if (expiresAtElem) { expiresAtElem.textContent = expDate.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", }); } updateAllCountdowns(); } } setTimeout(() => { if (globalThis.location) globalThis.location.reload(); }, 600); } else { showNotice( data.error || "Failed to extend event (HTTP " + res.status + ")", true, ); e.currentTarget.disabled = false; } } catch (_err) { showNotice("Network error extending event", true); e.currentTarget.disabled = false; } }); }); document.querySelectorAll(".end-event-btn").forEach((btn) => { btn.addEventListener("click", async (e) => { if ( !confirm( "Are you sure you want to end this workshop? This will immediately revoke ALL active guest sessions and they will lose access.", ) ) return; const eventId = e.currentTarget.getAttribute("data-event-id"); e.currentTarget.disabled = true; e.currentTarget.textContent = "Revoking..."; try { const res = await fetch("/api/events/" + eventId + "/end", { method: "POST", }); if (res.ok) { showNotice("Workshop ended and all sessions revoked.", false); setTimeout(() => { if (globalThis.location) globalThis.location.reload(); }, 600); } else { const data = await res.json().catch(() => ({})); showNotice(data.error || "Failed to end event", true); e.currentTarget.disabled = false; e.currentTarget.textContent = "End Event"; } } catch (_err) { showNotice("Network error ending event", true); e.currentTarget.disabled = false; e.currentTarget.textContent = "End Event"; } }); }); document.querySelectorAll(".revoke-btn").forEach((btn) => { btn.addEventListener("click", async (e) => { if (!confirm("Revoke this session immediately?")) return; const sessionId = e.currentTarget.getAttribute("data-session-id"); const originalText = e.currentTarget.textContent; e.currentTarget.textContent = "Revoking..."; e.currentTarget.disabled = true; try { const res = await fetch("/api/sessions/" + sessionId, { method: "DELETE", }); if (res.ok) { showNotice("Session revoked successfully.", false); setTimeout(() => { if (globalThis.location) globalThis.location.reload(); }, 500); } else { const data = await res.json().catch(() => ({})); showNotice(data.error || "Failed to revoke session", true); e.currentTarget.textContent = originalText; e.currentTarget.disabled = false; } } catch (_err) { showNotice("Network error revoking session", true); e.currentTarget.textContent = originalText; e.currentTarget.disabled = false; } }); }); } // Initial binding if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => { bindEventCockpitActions(); updateAllCountdowns(); setInterval(updateAllCountdowns, 60000); try { const savedMode = localStorage.getItem("auth_yes_event_view_mode"); if (savedMode) setEventViewMode(savedMode); } catch (_e) {} }); } else { bindEventCockpitActions(); updateAllCountdowns(); setInterval(updateAllCountdowns, 60000); try { const savedMode = localStorage.getItem("auth_yes_event_view_mode"); if (savedMode) setEventViewMode(savedMode); } catch (_e) {} } document.addEventListener("keydown", (e) => { if (e.key === "Escape") { closeDelegateDrawer(); closeEditScopesModal(); closeAttendeesDrawer(); } }); async function handleJoin(e) { if (e && e.preventDefault) e.preventDefault(); const input = document.getElementById("eventCode"); const code = input ? input.value.trim() : ""; if (!code) return; const btn = document.getElementById("joinBtn"); const notice = document.getElementById("joinNotice"); if (btn) { btn.disabled = true; btn.textContent = "Claiming Seat..."; } if (notice) notice.style.display = "none"; try { const res = await fetch("/api/join", { method: "POST", headers: { "Content-Type": "application/json", "Accept": "application/json", }, body: JSON.stringify({ code }), }); const data = await res.json(); if (res.ok && data.success) { showNotice("Seat claimed! Redirecting...", false); globalThis.location.href = data.redirectUrl || "/dashboard"; } else { if (notice) { notice.textContent = data.error || "Invalid event code or workshop capacity reached"; notice.style.display = "block"; notice.style.background = "var(--danger-bg)"; notice.style.color = "var(--danger-text)"; notice.style.border = "1px solid var(--danger-border)"; } showNotice(data.error || "Invalid event code", true); if (btn) { btn.disabled = false; btn.textContent = "⚡ Enter Workshop"; } } } catch (_err) { if (notice) { notice.textContent = "Network error connecting to event"; notice.style.display = "block"; notice.style.background = "var(--danger-bg)"; notice.style.color = "var(--danger-text)"; notice.style.border = "1px solid var(--danger-border)"; } showNotice("Network error connecting to event", true); if (btn) { btn.disabled = false; btn.textContent = "⚡ Enter Workshop"; } } } globalThis.handleJoin = handleJoin; async function handleSplashJoin(slug) { const btn = document.getElementById("joinSplashBtn"); const notice = document.getElementById("splashNotice"); if (btn) { btn.disabled = true; btn.textContent = "Claiming Seat..."; } if (notice) notice.style.display = "none"; try { const res = await fetch("/api/join", { method: "POST", headers: { "Content-Type": "application/json", "Accept": "application/json", }, body: JSON.stringify({ code: slug }), }); const data = await res.json(); if (res.ok && data.success) { showNotice("Seat claimed! Redirecting...", false); globalThis.location.href = data.redirectUrl || "/dashboard"; } else { if (notice) { notice.textContent = data.error || "Invalid event code or workshop is full"; notice.style.display = "block"; notice.style.background = "var(--danger-bg)"; notice.style.color = "var(--danger-text)"; notice.style.border = "1px solid var(--danger-border)"; } showNotice(data.error || "Failed to enter workshop", true); if (btn) { btn.disabled = false; btn.textContent = "⚡ Enter Workshop & Claim Seat"; } } } catch (_err) { if (notice) { notice.textContent = "Network error connecting to event"; notice.style.display = "block"; notice.style.background = "var(--danger-bg)"; notice.style.color = "var(--danger-text)"; notice.style.border = "1px solid var(--danger-border)"; } showNotice("Network error connecting to event", true); if (btn) { btn.disabled = false; btn.textContent = "⚡ Enter Workshop & Claim Seat"; } } } globalThis.handleSplashJoin = handleSplashJoin;