async function api(path, options = {}) { const res = await fetch(path, { headers: { 'Content-Type': 'application/json', ...(options.headers || {}) }, ...options }); if (!res.ok) throw new Error(await res.text()); return res.json(); } const state = { projects: [], workspaces: [] }; async function init() { const me = await api('/api/me'); document.getElementById('me').textContent = `${me.username} (${me.email || 'no email'})`; document.getElementById('workspaceForm').addEventListener('submit', createWorkspace); document.getElementById('createForm').addEventListener('submit', createProject); document.getElementById('refreshCaddyBtn').addEventListener('click', loadCaddyConfig); await refreshWorkspaces(); await refreshProjects(); await loadCaddyConfig(); } async function refreshWorkspaces() { state.workspaces = await api('/api/workspaces'); const select = document.getElementById('workspaceSelect'); if (state.workspaces.length === 0) { select.innerHTML = ''; return; } select.innerHTML = state.workspaces.map((w) => ( `` )).join(''); } async function refreshProjects() { state.projects = await api('/api/projects'); renderProjects(); } function renderProjects() { const root = document.getElementById('projects'); if (state.projects.length === 0) { root.innerHTML = '

No projects yet.

'; return; } root.innerHTML = ''; state.projects.forEach((p) => { const div = document.createElement('article'); div.className = 'project'; div.innerHTML = `

${escapeHtml(p.name)}

workspace: ${escapeHtml(p.workspace)} (${escapeHtml(p.unix_user)})

slug: ${escapeHtml(p.slug)} | service: ${escapeHtml(p.service_name)}

route host: ${escapeHtml(p.route_host || 'not set')}

app url: ${p.app_url ? `${escapeHtml(p.app_url)}` : 'not set'}

deploy webhook: ${escapeHtml(p.webhook_url || 'n/a')}

provisioning: ${escapeHtml(p.provision_state || 'unknown')}${p.provision_error ? ` | error: ${escapeHtml(p.provision_error)}` : ''}

${escapeHtml(p.description || '')}

${p.repo_url ? `

Open Repository

` : '

No repository URL set.

'}
Loading env vars...
status: loading...
logs: not loaded
`; div.querySelector('[data-add]').addEventListener('click', async () => { const key = div.querySelector('[data-k]').value.trim(); const value = div.querySelector('[data-v]').value; await api(`/api/projects/${p.id}/env`, { method: 'POST', body: JSON.stringify({ key, value }) }); await loadEnvList(div, p.id); }); div.querySelector('[data-regen]').addEventListener('click', async () => { await api(`/api/projects/${p.id}/service`, { method: 'POST' }); alert('Service regenerated and systemctl --user reload attempted.'); }); div.querySelector('[data-status]').addEventListener('click', async () => { await loadStatus(div, p.id); }); div.querySelector('[data-logs]').addEventListener('click', async () => { await loadLogs(div, p.id); }); div.querySelector('[data-reprovision]').addEventListener('click', async () => { await api(`/api/projects/${p.id}/reprovision`, { method: 'POST' }); await refreshProjects(); }); root.appendChild(div); loadEnvList(div, p.id); loadStatus(div, p.id); }); } async function loadEnvList(projectNode, projectID) { const envs = await api(`/api/projects/${projectID}/env`); const envRoot = projectNode.querySelector('[data-env]'); if (envs.length === 0) { envRoot.innerHTML = 'No env vars'; return; } envRoot.innerHTML = envs.map((e) => { return `
${escapeHtml(e.key)}=${escapeHtml(e.value)}
`; }).join(''); envRoot.querySelectorAll('[data-del]').forEach((btn) => { btn.addEventListener('click', async () => { const key = btn.getAttribute('data-del'); await api(`/api/projects/${projectID}/env?key=${encodeURIComponent(key)}`, { method: 'DELETE' }); await loadEnvList(projectNode, projectID); }); }); } async function loadStatus(projectNode, projectID) { const out = projectNode.querySelector('[data-status-out]'); out.textContent = 'status: loading...'; try { const s = await api(`/api/projects/${projectID}/status`); out.textContent = `status: ${s.active_state}/${s.sub_state} pid=${s.main_pid} result=${s.result || 'n/a'} exit=${s.exec_status}`; } catch (err) { out.textContent = `status error: ${err.message}`; } } async function loadLogs(projectNode, projectID) { const out = projectNode.querySelector('[data-logs-out]'); out.textContent = 'loading logs...'; try { const res = await api(`/api/projects/${projectID}/logs?lines=100`); out.textContent = res.logs || '(no logs)'; } catch (err) { out.textContent = `logs error: ${err.message}`; } } async function createProject(e) { e.preventDefault(); const form = e.target; const payload = { workspace_id: Number(form.workspace_id.value), name: form.name.value, description: form.description.value, repo_url: form.repo_url.value.trim(), repo_private: form.repo_private.checked }; const msg = document.getElementById('createMsg'); msg.textContent = 'Creating project...'; try { await api('/api/projects', { method: 'POST', body: JSON.stringify(payload) }); msg.textContent = 'Project created.'; form.reset(); form.repo_private.checked = true; await refreshProjects(); } catch (err) { msg.textContent = `Error: ${err.message}`; } } async function createWorkspace(e) { e.preventDefault(); const form = e.target; const payload = { name: form.name.value.trim(), unix_user: form.unix_user.value.trim() }; const msg = document.getElementById('workspaceMsg'); msg.textContent = 'Creating workspace...'; try { await api('/api/workspaces', { method: 'POST', body: JSON.stringify(payload) }); msg.textContent = 'Workspace created.'; form.reset(); await refreshWorkspaces(); } catch (err) { msg.textContent = `Error: ${err.message}`; } } async function loadCaddyConfig() { const out = document.getElementById('caddyConfigOut'); out.textContent = 'Loading Caddy config...'; try { const cfg = await api('/api/caddy/config'); out.textContent = cfg.content || JSON.stringify(cfg, null, 2); } catch (err) { out.textContent = `Error: ${err.message}`; } } function escapeHtml(s) { return String(s) .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } init().catch((err) => { document.getElementById('projects').innerHTML = `

Failed to load: ${escapeHtml(err.message)}

`; });