225 lines
8.1 KiB
JavaScript
225 lines
8.1 KiB
JavaScript
const API_BASE = '/web-api';
|
|
|
|
async function api(path, options = {}) {
|
|
const res = await fetch(`${API_BASE}${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('/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('/workspaces');
|
|
const select = document.getElementById('workspaceSelect');
|
|
if (state.workspaces.length === 0) {
|
|
select.innerHTML = '<option value="">Create a workspace first</option>';
|
|
return;
|
|
}
|
|
select.innerHTML = state.workspaces.map((w) => (
|
|
`<option value="${w.id}">${escapeHtml(w.name)} (${escapeHtml(w.unix_user)})</option>`
|
|
)).join('');
|
|
}
|
|
|
|
async function refreshProjects() {
|
|
state.projects = await api('/projects');
|
|
renderProjects();
|
|
}
|
|
|
|
function renderProjects() {
|
|
const root = document.getElementById('projects');
|
|
if (state.projects.length === 0) {
|
|
root.innerHTML = '<p class="muted">No projects yet.</p>';
|
|
return;
|
|
}
|
|
|
|
root.innerHTML = '';
|
|
state.projects.forEach((p) => {
|
|
const div = document.createElement('article');
|
|
div.className = 'project';
|
|
div.innerHTML = `
|
|
<h3>${escapeHtml(p.name)}</h3>
|
|
<p class="meta">workspace: ${escapeHtml(p.workspace)} (${escapeHtml(p.unix_user)})</p>
|
|
<p class="meta">slug: ${escapeHtml(p.slug)} | service: ${escapeHtml(p.service_name)}</p>
|
|
<p class="meta">route host: ${escapeHtml(p.route_host || 'not set')}</p>
|
|
<p class="meta">app url: ${p.app_url ? `<a class="link" href="${escapeHtml(p.app_url)}" target="_blank" rel="noreferrer">${escapeHtml(p.app_url)}</a>` : 'not set'}</p>
|
|
<p class="meta">last deploy: ${p.last_deployed_at ? escapeHtml(new Date(p.last_deployed_at).toLocaleString()) : 'never'}</p>
|
|
<p class="meta">latest commit: <code>${escapeHtml(p.last_deployed_commit || 'n/a')}</code></p>
|
|
<p class="meta">deploy webhook: <code>${escapeHtml(p.webhook_url || 'n/a')}</code></p>
|
|
<p class="meta">provisioning: ${escapeHtml(p.provision_state || 'unknown')}${p.provision_error ? ` | error: ${escapeHtml(p.provision_error)}` : ''}</p>
|
|
<p>${escapeHtml(p.description || '')}</p>
|
|
${p.repo_url ? `<p><a class="link" href="${p.repo_url}" target="_blank" rel="noreferrer">Open Repository</a></p>` : '<p class="muted">No repository URL set.</p>'}
|
|
<div class="env-row">
|
|
<input class="small" placeholder="ENV_KEY" data-k />
|
|
<input class="small" placeholder="value" data-v />
|
|
<button class="small" data-add>Add/Update Env</button>
|
|
<button class="small" data-regen>Regenerate Service</button>
|
|
</div>
|
|
<div class="env-list muted" data-env>Loading env vars...</div>
|
|
<div class="env-row">
|
|
<button class="small" data-status>Refresh Status</button>
|
|
<button class="small" data-logs>Load Logs</button>
|
|
<button class="small" data-reprovision>Reprovision</button>
|
|
</div>
|
|
<div class="meta" data-status-out>status: loading...</div>
|
|
<pre class="meta" data-logs-out style="white-space: pre-wrap; max-height: 220px; overflow: auto;">logs: not loaded</pre>
|
|
`;
|
|
|
|
div.querySelector('[data-add]').addEventListener('click', async () => {
|
|
const key = div.querySelector('[data-k]').value.trim();
|
|
const value = div.querySelector('[data-v]').value;
|
|
await 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(`/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(`/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(`/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 `<div><code>${escapeHtml(e.key)}</code>=<code>${escapeHtml(e.value)}</code> <button class="small danger" data-del="${escapeHtml(e.key)}">Delete</button></div>`;
|
|
}).join('');
|
|
|
|
envRoot.querySelectorAll('[data-del]').forEach((btn) => {
|
|
btn.addEventListener('click', async () => {
|
|
const key = btn.getAttribute('data-del');
|
|
await 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(`/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(`/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('/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('/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('/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 = `<p class="muted">Failed to load: ${escapeHtml(err.message)}</p>`;
|
|
});
|