const storedMe = (() => { try { const raw = localStorage.getItem("me") ?? localStorage.getItem("user"); return raw ? JSON.parse(raw) : null; } catch { return null; } })(); const state = { me: storedMe, }; /** * @returns {HTMLElement} */ const $ = (sel) => document.querySelector(sel), setText = (sel, v) => { const el = $(sel); if (el) el.textContent = v; }; const authStatusEl = $("#authStatus"), listStatusEl = $("#listStatus"), createStatusEl = $("#createStatus"), runNowStatusEl = $("#runNowStatus"), schedulesTbody = $("#schedulesTbody"), templatesUl = $("#templatesUl"), oneShotCheckmark = $("#oneShot"), startAtInput = $("#startAt"), cronInput = $("#cron"), cronError = $("#cronError"), filesInput = $("#scheduleFiles"), filesList = $("#scheduleFilesList"); const cron5Regex = new RegExp( '^' + '(\\*|([0-5]?\\d)(\\/\\d+)?|\\d+(,\\d+)*|\\d+(-\\d+))\\s+' + '(\\*|([01]?\\d|2[0-3])(\\/\\d+)?|\\d+(,\\d+)*|\\d+(-\\d+))\\s+' + '(\\*|([1-9]|[12]\\d|3[01])(\\/\\d+)?|\\d+(,\\d+)*|\\d+(-\\d+))\\s+' + '(\\*|([1-9]|1[0-2])(\\/\\d+)?|\\d+(,\\d+)*|\\d+(-\\d+))\\s+' + '(\\*|[0-7](\\/\\d+)?|\\d+(,\\d+)*|\\d+(-\\d+))' + '$' ); const validateCron = (e) => { if (!cronInput || !cronError) return true; const val = cronInput.value.trim(); if (cron5Regex.test(val)) { cronError.style.display = 'none'; cronError.textContent = ''; return true; } e?.preventDefault(); cronError.textContent = 'please enter a valid cron expression'; cronError.style.display = 'block'; return false; }; cronInput?.addEventListener('input', () => { if (!cronError) return; const val = cronInput.value.trim(); if (cron5Regex.test(val)) { cronError.style.display = 'none'; cronError.textContent = ''; } else { cronError.style.display = 'block'; } }); const formatBytes = (bytes) => { const size = Number(bytes); if (!Number.isFinite(size) || size < 0) return ''; if (size === 0) return '0 B'; const units = ['B', 'KB', 'MB', 'GB']; const idx = Math.min(Math.floor(Math.log(size) / Math.log(1024)), units.length - 1); const scaled = size / Math.pow(1024, idx); return `${scaled.toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`; }; const readFileAsDataUrl = (file) => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result || '')); reader.onerror = () => reject(new Error(`failed to read file ${file?.name || ''}`)); reader.readAsDataURL(file); }); const renderSelectedFiles = () => { if (!filesList) return; const files = filesInput?.files ? Array.from(filesInput.files) : []; if (!files.length) { filesList.innerHTML = '
  • no files selected
  • '; return; } filesList.innerHTML = ''; files.forEach((file) => { const li = document.createElement('li'); const size = formatBytes(file.size); li.textContent = size ? `${file.name} (${size})` : file.name; filesList.appendChild(li); }); }; filesInput?.addEventListener('change', renderSelectedFiles); renderSelectedFiles(); // JSON const paramsEl = $('#params'), options = { mode: 'code', modes: ['code', 'form', 'text'], // allowed modes onModeChange: function (newMode, oldMode) { console.log('Mode switched from', oldMode, 'to', newMode) } }, jInstance = new JSONEditor(paramsEl, options) // set json const initialJson = { "report_kind": "summary", "Numbers": [1, 2, 3] } jInstance.set(initialJson); window.jInstance = jInstance; const togCron = ({ target: { checked } }) => { const cron = cronInput?.parentElement, start = startAtInput?.parentElement, warn = $(".warning-box")?.parentElement; if (!cron || !start || !warn) return; if (checked) { cron.style.display = 'none'; warn.style.display = 'none'; start.style.display = 'block'; if (startAtInput) startAtInput.required = true; if (cronInput) cronInput.required = false; } else { cron.style.display = 'block'; warn.style.display = 'flex'; start.style.display = 'block'; if (startAtInput) startAtInput.required = false; if (cronInput) cronInput.required = true; } } oneShotCheckmark.addEventListener('change', togCron); togCron({ target: { checked: oneShotCheckmark.checked } }); // update login ui from state async function paintAuth() { const meRes = await fetch('/api/me', { headers: { 'credentials': 'include', 'Content-Type': 'application/json' } }); if (meRes.ok) { const me = await meRes.json().catch(console.error); if (me) { localStorage.setItem("me", JSON.stringify(me)); state.me = me; } } if (state.me && state.me.id) { const who = state.me.name || state.me.email || state.me.id; authStatusEl.textContent = `logged in as ${who}`; document.querySelector('#userLogoutBtn').style.display = 'block'; // populate user-card const userCard = $("#userCard"), avatar = $("#userAvatar"), nameEl = $("#userName"), emailEl = $("#userEmail"), roleEl = $("#userRole"), permsEl = $("#userPermissions"); if (state.me.models) renderModelList(state.me.models); if (state.me.tools) renderToolList(state.me.tools); const featurePerms = state.me.permissions?.features; renderFeatureList(featurePerms); if (userCard) userCard.setAttribute("aria-hidden", "false"); if (avatar) { avatar.src = state.me.profile_image_url || ""; avatar.alt = state.me.name || state.me.email || "user avatar"; } if (nameEl) nameEl.textContent = state.me.name || state.me.email || state.me.id; if (emailEl) emailEl.textContent = state.me.email || ""; if (roleEl) roleEl.textContent = state.me.role ? `role: ${state.me.role}` : ""; if (permsEl) { permsEl.hidden = false; renderSelectedPermissions(state.me, permsEl); } document.querySelector('#auth').ariaHidden = 'true'; } else { authStatusEl.textContent = "not logged in"; document.querySelector('#userLogoutBtn').style.display = 'none'; // clear credential inputs if present const u = $("#username"), p = $("#password"); if (u) u.value = ""; if (p) p.value = ""; // hide user-card const userCard = $("#userCard"), avatar = $("#userAvatar"), nameEl = $("#userName"), emailEl = $("#userEmail"), roleEl = $("#userRole"), permsEl = $("#userPermissions"); if (userCard) userCard.setAttribute("aria-hidden", "true"); if (avatar) { avatar.src = ""; avatar.alt = ""; } if (nameEl) nameEl.textContent = "not logged in"; if (emailEl) emailEl.textContent = ""; if (roleEl) roleEl.textContent = ""; if (permsEl) { permsEl.hidden = true; // clear values permsEl.querySelectorAll(".perm-val").forEach(el => el.textContent = ""); } renderFeatureList(); } } // helper to safely resolve nested permission path like "features.web_search" function getPerm(me, path) { if (!me || !me.permissions || !path) return false; return path.split('.').reduce((acc, k) => (acc && typeof acc === 'object') ? acc[k] : undefined, me.permissions) || false; } function renderSelectedPermissions(me, container) { // mapping of displayed items -> permission path const items = [ { path: 'workspace.tools', label: 'Workspace (tools)' }, { path: 'chat.file_upload', label: 'Chat (file_upload)' }, { path: 'features.web_search', label: 'web_search' }, { path: 'features.code_interpreter', label: 'code_interpreter' } ], isadmin = me.role === "admin"; // update all .perm-val placeholders by data-perm attribute when present container.querySelectorAll('.perm-val').forEach(el => { const key = el.getAttribute('data-perm'); if (key) { const v = isadmin || getPerm(me, key); el.textContent = v ? '✅' : '❌'; el.classList.toggle('perm-yes', Boolean(v)); el.classList.toggle('perm-no', !v); el.title = `${key}: ${v ? 'allowed' : 'denied'}`; } }); // fallback placeholders items.forEach(it => { const sel = `[data-perm="${it.path}"]`; if (!container.querySelector(sel)) { const li = document.createElement('li'); li.innerHTML = `${isadmin || getPerm(me, it.path) ? '✅' : '❌'}`; li.innerHTML += `${escapeHtml(it.label)}`; container.querySelector('.perms-list')?.appendChild(li); } }); } // theme toggle stuff! const prefersLight = document.defaultView?.matchMedia?.("(prefers-color-scheme: light)").matches === true, saved = localStorage.getItem("theme"), initial = saved ?? (prefersLight ? "light" : "dark"); const applyTheme = (mode) => { const b = document.body; b.classList.remove("theme-dark", "theme-light"); b.classList.add(mode === "dark" ? "theme-dark" : "theme-light"); const icon = document.querySelector("#themeToggleIcon"), label = document.querySelector("#themeToggleLabel"); if (icon && label) { const isDark = mode === "dark"; icon.textContent = isDark ? "🌙" : "☀️"; label.textContent = isDark ? "Dark" : "Light"; } document.querySelector("#themeToggle")?.setAttribute("aria-pressed", String(mode === "dark")); }; applyTheme(initial); document.querySelector("#themeToggle")?.addEventListener("click", () => { const next = document.body.classList.contains("theme-dark") ? "light" : "dark"; applyTheme(next); try { localStorage.setItem("theme", next); } catch (_) { /* ignore */ } }); if (window.matchMedia) { const mq = window.matchMedia("(prefers-color-scheme: dark)"); mq.addEventListener?.("change", (e) => { // only adapt to system changes when user hasn't explicitly chosen a theme if (!localStorage.getItem("theme")) applyTheme(e.matches ? "dark" : "light"); }); } // wrap fetch to always attach x-user-id and Authorization when available async function apiFetch(url, options = {}) { const headers = new Headers(options.headers || {}); if (!state.me || !state.me.id) { throw new Error("no authenticated user — use the login form first"); } headers.set("x-user-id", state.me.id); // keep existing custom header for server compatibility if (state.me.token) { const typ = state.me.token_type || "Bearer"; headers.set("authorization", `${typ} ${state.me.token}`); } if (!headers.has("content-type") && options.body && !(options.body instanceof FormData)) { headers.set("content-type", "application/json"); } headers.set('credentials', 'include'); const resp = await fetch(url, { ...options, headers }); if (!resp.ok) { // try to surface json error bodies let msg = `${resp.status} ${resp.statusText}`; try { const data = await resp.json(); if (data && data.error) msg = data.error; } catch { } throw new Error(msg); } return resp; } // render list function renderSchedules(items = []) { schedulesTbody.innerHTML = ""; items.forEach((it) => { const tr = document.createElement("tr"), tRef = it.templateRef ? it.templateRef.clusterScope ? `(cluster) ${it.templateRef.name}` : it.templateRef.name : ""; tr.innerHTML = ` ${escapeHtml(it.displayName || it.name || "")} ${(it.schedules || []).map(escapeHtml).join("
    ")} ${escapeHtml(it.startAt || "")} ${escapeHtml(tRef)} ${escapeHtml(it.prompt || "")} ${it.oneShot ? "yes" : "no"} `; schedulesTbody.appendChild(tr); }); } // tiny escape helper function escapeHtml(s = "") { return String(s).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); } // wire up events $("#loginForm").addEventListener("submit", async (e) => { e.preventDefault(); const username = $("#username").value.trim(), password = $("#password").value; if (!username || !password) { authStatusEl.textContent = "please enter username and password"; return; } try { authStatusEl.textContent = "authenticating..."; const res = await fetch("/login", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: username, password }), }); if (!res.ok) { let msg = `${res.status} ${res.statusText}`; try { const d = await res.json(); if (d && d.error) msg = d.error; } catch { } throw new Error(msg); } const data = await res.json(), me = data.me || data.user || data; if (!me || !me.id) { throw new Error("invalid login response (missing id)"); } // // persist cookie if server returned one // if (data.cookie) { // try { // document.cookie = `${document.cookie.replace(/owebucookie=.*;/, '')}owebucookie=${data.cookie}`; // } catch { } // } // store canonical "me" try { localStorage.setItem("me", JSON.stringify(me)); // keep old key for compatibility localStorage.setItem("user", JSON.stringify(me)); } catch { } state.me = me; paintAuth(); // trigger refresh immediately $("#refreshBtn")?.click(); } catch (err) { authStatusEl.textContent = `login error: ${err.message}`; } }); // central logout routine used by multiple buttons function doLogout() { try { localStorage.removeItem("me"); localStorage.removeItem("user"); } catch { } state.me = null; paintAuth(); } // existing logout button (if present) + user card logout const logoutBtn = $("#logoutBtn"), userLogoutBtn = $("#userLogoutBtn"); if (logoutBtn) logoutBtn.addEventListener("click", doLogout); if (userLogoutBtn) userLogoutBtn.addEventListener("click", doLogout); $("#refreshBtn").addEventListener("click", async () => { try { listStatusEl.textContent = "loading..."; const res = await apiFetch("/api/schedules"), data = await res.json(); renderSchedules(data.items || []); listStatusEl.textContent = `loaded ${Array.isArray(data.items) ? data.items.length : 0} schedule(s)`; } catch (e) { listStatusEl.textContent = `error: ${e.message}`; } }); // delete handler (delegated) schedulesTbody.addEventListener("click", async (e) => { const target = e.target; if (!(target instanceof HTMLButtonElement)) return; const name = target.getAttribute("data-del"); if (!name) return; try { target.disabled = true; const res = await apiFetch(`/api/schedules/${name}`, { method: "DELETE", }); if (res.status === 204) { target.closest("tr")?.remove(); listStatusEl.textContent = "deleted"; } else { listStatusEl.textContent = "unexpected response"; } } catch (err) { listStatusEl.textContent = `error: ${err.message}`; } finally { target.disabled = false; } }); // create/update schedule $("#createForm").addEventListener("submit", async (e) => { e.preventDefault(); try { createStatusEl.textContent = "saving..."; const name = $("#name").value.trim(), startAt = startAtInput?.value || "", cron = $("#cron").value.trim(), templateName = $("#templateName").value.trim(), prompt = $("#prompt").value.trim(), clusterScope = $("#clusterScope").checked, oneShot = $("#oneShot").checked, paramsRaw = jInstance.getText(), model = $("#model-select").value.trim(), tools = Array.from(document .querySelectorAll("#tools-select [aria-selected='true']")) .map(o => o.dataset.toolId), featuresInput = $("#features-select-input"); if (!oneShot && !cron) { throw new Error("cron expression is required"); } if (!oneShot && !validateCron(e)) return; if (oneShot && !startAt) { throw new Error("start date is required for one-shot schedules"); } const jErrs = await jInstance.validate(); if (jErrs?.length) { console.error(jErrs); throw new Error(`Please fix your JSON errors!`); } // leave as extra validation let parameters = {}; if (paramsRaw) { try { parameters = JSON.parse(paramsRaw); } catch { throw new Error("parameters must be valid json"); } } let features = {}; if (featuresInput && featuresInput.value) { try { const parsed = JSON.parse(featuresInput.value); if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { features = Object.fromEntries(Object.entries(parsed).map(([k, v]) => [k, Boolean(v)])); } } catch { throw new Error("features selection must be valid json"); } } const attachments = filesInput?.files ? Array.from(filesInput.files) : []; const filesPayload = await Promise.all(attachments.map(async (file) => ({ fname: file.name, content: await readFileAsDataUrl(file) }))); const when = oneShot ? { start: startAt } : { cron }; if (!oneShot && startAt) when.start = startAt; const payload = { name, when, oneShot, template: { name: templateName, clusterScope }, parameters, prompt, model, tools, features }; if (filesPayload.length) payload.files = filesPayload; await apiFetch("/api/schedules", { method: "POST", body: JSON.stringify(payload), }); createStatusEl.textContent = "saved ✅"; $("#refreshBtn").click(); // clear the form $("#createForm").reset(); togCron({ target: { checked: $("#oneShot").checked } }); renderSelectedFiles(); if (cronError) { cronError.style.display = 'none'; cronError.textContent = ''; } } catch (err) { console.error(err); createStatusEl.textContent = `error: ${err.message}`; } }); // load workflow templates for convenience $("#loadTemplatesBtn").addEventListener("click", async () => { try { templatesUl.innerHTML = ""; templatesUl.parentElement.open = true; const res = await apiFetch("/api/workflowtemplates"), data = await res.json(); (data.items || []).forEach((t) => { const li = document.createElement("li"); li.textContent = t.name; templatesUl.appendChild(li); }); } catch (e) { templatesUl.innerHTML = `
  • error: ${escapeHtml(e.message)}
  • `; } }); // window.onbeforeunload = () => CookieStore.delete('token'); const reffunc = () => { // auto-refresh if already logged in if (state.me && state.me.id) $("#refreshBtn").click(); } // boot paintAuth().then(reffunc);