added scheduler
This commit is contained in:
+444
-124
@@ -1,8 +1,19 @@
|
||||
const storedMe = (() => {
|
||||
try {
|
||||
const raw = localStorage.getItem("me") ?? localStorage.getItem("user");
|
||||
return raw ? JSON.parse(raw) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
const state = {
|
||||
userId: localStorage.getItem("userId") || "",
|
||||
displayName: localStorage.getItem("displayName") || "",
|
||||
me: storedMe,
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
const $ = (sel) => document.querySelector(sel),
|
||||
setText = (sel, v) => {
|
||||
const el = $(sel);
|
||||
@@ -14,74 +25,314 @@ const authStatusEl = $("#authStatus"),
|
||||
createStatusEl = $("#createStatus"),
|
||||
runNowStatusEl = $("#runNowStatus"),
|
||||
schedulesTbody = $("#schedulesTbody"),
|
||||
templatesUl = $("#templatesUl");
|
||||
templatesUl = $("#templatesUl"),
|
||||
oneShotCheckmark = $("#oneShot"),
|
||||
startAtInput = $("#startAt"),
|
||||
cronInput = $("#cron"),
|
||||
cronError = $("#cronError"),
|
||||
filesInput = $("#scheduleFiles"),
|
||||
filesList = $("#scheduleFilesList");
|
||||
|
||||
// update login ui from state
|
||||
function paintAuth() {
|
||||
$("#userId").value = state.userId || "";
|
||||
$("#displayName").value = state.displayName || "";
|
||||
if (state.userId) {
|
||||
authStatusEl.textContent = `logged in as ${state.displayName ? state.displayName + " · " : ""
|
||||
}${state.userId}`;
|
||||
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 {
|
||||
authStatusEl.textContent = "not logged in";
|
||||
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 = '<li class="muted empty">no files selected</li>';
|
||||
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 = `<span class="perm-val ${getPerm(me, it.path) ? 'perm-yes' : 'perm-no'}">${isadmin || getPerm(me, it.path) ? '✅' : '❌'}</span>`;
|
||||
li.innerHTML += `<span class="perm-key">${escapeHtml(it.label)}</span>`;
|
||||
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 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');
|
||||
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');
|
||||
const icon = document.querySelector("#themeToggleIcon"),
|
||||
label = document.querySelector("#themeToggleLabel");
|
||||
if (icon && label) {
|
||||
const isDark = mode === 'dark';
|
||||
icon.textContent = isDark ? '🌙' : '☀️';
|
||||
label.textContent = isDark ? 'Dark' : 'Light';
|
||||
const isDark = mode === "dark";
|
||||
icon.textContent = isDark ? "🌙" : "☀️";
|
||||
label.textContent = isDark ? "Dark" : "Light";
|
||||
}
|
||||
|
||||
document.querySelector('#themeToggle')?.setAttribute('aria-pressed', String(mode === 'dark'));
|
||||
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';
|
||||
document.querySelector("#themeToggle")?.addEventListener("click", () => {
|
||||
const next = document.body.classList.contains("theme-dark") ? "light" : "dark";
|
||||
applyTheme(next);
|
||||
try { localStorage.setItem('theme', next); } catch (_) { /* ignore */ }
|
||||
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');
|
||||
if (!localStorage.getItem("theme")) applyTheme(e.matches ? "dark" : "light");
|
||||
});
|
||||
}
|
||||
|
||||
// wrap fetch to always attach x-user-id
|
||||
// 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.userId)
|
||||
throw new Error(
|
||||
"no user id set — use the login form first"
|
||||
);
|
||||
headers.set("x-user-id", state.userId); // custom header
|
||||
if (
|
||||
!headers.has("content-type") &&
|
||||
options.body &&
|
||||
!(options.body instanceof FormData)
|
||||
) {
|
||||
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
|
||||
@@ -99,67 +350,117 @@ async function apiFetch(url, options = {}) {
|
||||
function renderSchedules(items = []) {
|
||||
schedulesTbody.innerHTML = "";
|
||||
items.forEach((it) => {
|
||||
const tr = document.createElement("tr");
|
||||
const tRef = it.templateRef
|
||||
? it.templateRef.clusterScope
|
||||
? `(cluster) ${it.templateRef.name}`
|
||||
: it.templateRef.name
|
||||
: "";
|
||||
const tr = document.createElement("tr"),
|
||||
tRef = it.templateRef
|
||||
? it.templateRef.clusterScope
|
||||
? `(cluster) ${it.templateRef.name}`
|
||||
: it.templateRef.name
|
||||
: "";
|
||||
|
||||
tr.innerHTML = `
|
||||
<td>${escapeHtml(it.displayName || it.name || "")}</td>
|
||||
<td>${(it.schedules || []).map(escapeHtml).join("<br/>")}</td>
|
||||
<td>${escapeHtml(it.timezone || "")}</td>
|
||||
<td>${escapeHtml(it.startAt || "")}</td>
|
||||
<td>${escapeHtml(tRef)}</td>
|
||||
<td>${escapeHtml(it.entrypoint || "")}</td>
|
||||
<td>${escapeHtml(it.prompt || "")}</td>
|
||||
<td>${it.oneShot ? "yes" : "no"}</td>
|
||||
<td class="actions">
|
||||
<button data-del="${encodeURIComponent(it.name)}">delete</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
schedulesTbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
// tiny escape helper
|
||||
function escapeHtml(s = "") {
|
||||
return String(s)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">");
|
||||
return String(s).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
||||
}
|
||||
|
||||
// wire up events
|
||||
$("#loginForm").addEventListener("submit", (e) => {
|
||||
$("#loginForm").addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const userId = $("#userId").value.trim(),
|
||||
displayName = $("#displayName").value.trim();
|
||||
if (!userId) {
|
||||
authStatusEl.textContent = "please enter a user id";
|
||||
const username = $("#username").value.trim(),
|
||||
password = $("#password").value;
|
||||
|
||||
if (!username || !password) {
|
||||
authStatusEl.textContent = "please enter username and password";
|
||||
return;
|
||||
}
|
||||
state.userId = userId;
|
||||
state.displayName = displayName;
|
||||
localStorage.setItem("userId", state.userId);
|
||||
localStorage.setItem("displayName", state.displayName);
|
||||
paintAuth();
|
||||
|
||||
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}`;
|
||||
}
|
||||
});
|
||||
|
||||
$("#logoutBtn").addEventListener("click", () => {
|
||||
localStorage.removeItem("userId");
|
||||
localStorage.removeItem("displayName");
|
||||
state.userId = "";
|
||||
state.displayName = "";
|
||||
// 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");
|
||||
const data = await res.json();
|
||||
|
||||
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)`;
|
||||
listStatusEl.textContent = `loaded ${Array.isArray(data.items) ? data.items.length : 0} schedule(s)`;
|
||||
} catch (e) {
|
||||
listStatusEl.textContent = `error: ${e.message}`;
|
||||
}
|
||||
@@ -175,7 +476,7 @@ schedulesTbody.addEventListener("click", async (e) => {
|
||||
|
||||
try {
|
||||
target.disabled = true;
|
||||
const res = await apiFetch(`/schedules/${name}`, {
|
||||
const res = await apiFetch(`/api/schedules/${name}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
@@ -198,17 +499,36 @@ $("#createForm").addEventListener("submit", async (e) => {
|
||||
try {
|
||||
createStatusEl.textContent = "saving...";
|
||||
const name = $("#name").value.trim(),
|
||||
tz = $("#tz").value.trim() || "America/New_York",
|
||||
iso = $("#iso").value
|
||||
? new Date($("#iso").value).toISOString()
|
||||
: "",
|
||||
startAt = startAtInput?.value || "",
|
||||
cron = $("#cron").value.trim(),
|
||||
templateName = $("#templateName").value.trim(),
|
||||
entrypoint = $("#entrypoint").value.trim(),
|
||||
prompt = $("#prompt").value.trim(),
|
||||
clusterScope = $("#clusterScope").checked,
|
||||
oneShot = $("#oneShot").checked,
|
||||
paramsRaw = $("#params").value.trim();
|
||||
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 {
|
||||
@@ -218,64 +538,62 @@ $("#createForm").addEventListener("submit", async (e) => {
|
||||
}
|
||||
}
|
||||
|
||||
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: cron ? { cron } : { iso },
|
||||
tz,
|
||||
when,
|
||||
oneShot,
|
||||
template: { name: templateName, clusterScope },
|
||||
parameters,
|
||||
entrypoint: entrypoint || undefined,
|
||||
prompt,
|
||||
model,
|
||||
tools,
|
||||
features
|
||||
};
|
||||
if (filesPayload.length) payload.files = filesPayload;
|
||||
|
||||
await apiFetch("/schedules", {
|
||||
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}`;
|
||||
}
|
||||
});
|
||||
|
||||
// run now
|
||||
$("#runNowForm").addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
runNowStatusEl.textContent = "starting...";
|
||||
const name = $("#rnName").value.trim() || "ad-hoc",
|
||||
templateName = $("#rnTemplateName").value.trim(),
|
||||
entrypoint = $("#rnEntrypoint").value.trim(),
|
||||
clusterScope = $("#rnClusterScope").checked,
|
||||
paramsRaw = $("#rnParams").value.trim();
|
||||
|
||||
let parameters = {};
|
||||
if (paramsRaw) {
|
||||
try {
|
||||
parameters = JSON.parse(paramsRaw);
|
||||
} catch {
|
||||
throw new Error("parameters must be valid json");
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name,
|
||||
template: { name: templateName, clusterScope },
|
||||
entrypoint: entrypoint || undefined,
|
||||
parameters,
|
||||
};
|
||||
|
||||
await apiFetch("/run-now", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
runNowStatusEl.textContent = "started ✅";
|
||||
} catch (err) {
|
||||
runNowStatusEl.textContent = `error: ${err.message}`;
|
||||
}
|
||||
});
|
||||
|
||||
// load workflow templates for convenience
|
||||
$("#loadTemplatesBtn").addEventListener("click", async () => {
|
||||
@@ -292,14 +610,16 @@ $("#loadTemplatesBtn").addEventListener("click", async () => {
|
||||
templatesUl.appendChild(li);
|
||||
});
|
||||
} catch (e) {
|
||||
templatesUl.innerHTML = `<li class="danger">error: ${escapeHtml(
|
||||
e.message
|
||||
)}</li>`;
|
||||
templatesUl.innerHTML = `<li class="danger">error: ${escapeHtml(e.message)}</li>`;
|
||||
}
|
||||
});
|
||||
|
||||
// boot
|
||||
paintAuth();
|
||||
// window.onbeforeunload = () => CookieStore.delete('token');
|
||||
|
||||
// auto-refresh if already logged in
|
||||
if (state.userId) $("#refreshBtn").click();
|
||||
const reffunc = () => {
|
||||
// auto-refresh if already logged in
|
||||
if (state.me && state.me.id) $("#refreshBtn").click();
|
||||
}
|
||||
|
||||
// boot
|
||||
paintAuth().then(reffunc);
|
||||
|
||||
Reference in New Issue
Block a user