crates/server: update templates with improved dashboard and styling
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I07f9de61588f61aae003f78c30fd6d326a6a6964
This commit is contained in:
parent
92153bf9aa
commit
b4d3c9d501
14 changed files with 1139 additions and 609 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -51,8 +51,12 @@
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
{% if api_keys.is_empty() %}
|
{% if api_keys.is_empty() %}
|
||||||
<p class="empty">No API keys.</p>
|
<div class="empty">
|
||||||
|
<div class="empty-title">No API keys</div>
|
||||||
|
<div class="empty-hint">Create an API key above to enable API access.</div>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Name</th><th>Role</th><th>Created</th><th>Last Used</th>{% if is_admin %}<th>Actions</th>{% endif %}</tr>
|
<tr><th>Name</th><th>Role</th><th>Created</th><th>Last Used</th>{% if is_admin %}<th>Actions</th>{% endif %}</tr>
|
||||||
|
|
@ -71,6 +75,7 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
@ -103,8 +108,12 @@
|
||||||
</details>
|
</details>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if builders.is_empty() %}
|
{% if builders.is_empty() %}
|
||||||
<p class="empty">No remote builders configured.</p>
|
<div class="empty">
|
||||||
|
<div class="empty-title">No remote builders configured</div>
|
||||||
|
<div class="empty-hint">Remote builders distribute builds across multiple machines.</div>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Name</th><th>SSH URI</th><th>Systems</th><th>Max Jobs</th><th>Enabled</th>{% if is_admin %}<th>Actions</th>{% endif %}</tr>
|
<tr><th>Name</th><th>SSH URI</th><th>Systems</th><th>Max Jobs</th><th>Enabled</th>{% if is_admin %}<th>Actions</th>{% endif %}</tr>
|
||||||
|
|
@ -127,6 +136,7 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
|
|
@ -144,22 +154,29 @@ document.getElementById('create-key-form')?.addEventListener('submit', async (e)
|
||||||
role: document.getElementById('key-role').value,
|
role: document.getElementById('key-role').value,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json().catch(() => ({}));
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
msg.innerHTML = '<div class="flash-message flash-success">Key created: <code>' + data.key + '</code><br>Copy this now, it will not be shown again.</div>';
|
msg.innerHTML = '<div class="flash-message flash-success">Key created: <code>' + escapeHtml(data.key || '') + '</code><br>Copy this now, it will not be shown again.</div>';
|
||||||
setTimeout(() => window.location.reload(), 5000);
|
setTimeout(() => window.location.reload(), 5000);
|
||||||
} else {
|
} else {
|
||||||
msg.innerHTML = '<div class="flash-message flash-error">' + (data.error || 'Error') + '</div>';
|
throw new Error(data.error || data.message || 'Unknown error');
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(err) {
|
||||||
msg.innerHTML = '<div class="flash-message flash-error">Request failed</div>';
|
showError(msg, err.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
async function deleteKey(id) {
|
async function deleteKey(id) {
|
||||||
if (!confirm('Delete this API key?')) return;
|
if (!confirm('Delete this API key?')) return;
|
||||||
const res = await fetch('/api/v1/api-keys/' + id, { method: 'DELETE' });
|
try {
|
||||||
if (res.ok) window.location.reload();
|
const res = await fetch('/api/v1/api-keys/' + id, { method: 'DELETE' });
|
||||||
else alert('Failed to delete key');
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
||||||
|
throw new Error(err.error || err.message || 'Failed to delete key');
|
||||||
|
}
|
||||||
|
window.location.reload();
|
||||||
|
} catch(err) {
|
||||||
|
alert(escapeHtml(err.message));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
document.getElementById('create-builder-form')?.addEventListener('submit', async (e) => {
|
document.getElementById('create-builder-form')?.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -175,30 +192,43 @@ document.getElementById('create-builder-form')?.addEventListener('submit', async
|
||||||
max_jobs: parseInt(document.getElementById('builder-maxjobs').value) || 4,
|
max_jobs: parseInt(document.getElementById('builder-maxjobs').value) || 4,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (!res.ok) {
|
||||||
window.location.reload();
|
const data = await res.json().catch(() => ({ error: res.statusText }));
|
||||||
} else {
|
throw new Error(data.error || data.message || 'Unknown error');
|
||||||
const data = await res.json();
|
|
||||||
msg.innerHTML = '<div class="flash-message flash-error">' + (data.error || 'Error') + '</div>';
|
|
||||||
}
|
}
|
||||||
} catch(e) {
|
window.location.reload();
|
||||||
msg.innerHTML = '<div class="flash-message flash-error">Request failed</div>';
|
} catch(err) {
|
||||||
|
showError(msg, err.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
async function toggleBuilder(id, enable) {
|
async function toggleBuilder(id, enable) {
|
||||||
const res = await fetch('/api/v1/admin/builders/' + id, {
|
try {
|
||||||
method: 'PUT',
|
const res = await fetch('/api/v1/admin/builders/' + id, {
|
||||||
headers: {'Content-Type': 'application/json'},
|
method: 'PUT',
|
||||||
body: JSON.stringify({ enabled: enable }),
|
headers: {'Content-Type': 'application/json'},
|
||||||
});
|
body: JSON.stringify({ enabled: enable }),
|
||||||
if (res.ok) window.location.reload();
|
});
|
||||||
else alert('Failed to update builder');
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
||||||
|
throw new Error(err.error || err.message || 'Failed to update builder');
|
||||||
|
}
|
||||||
|
window.location.reload();
|
||||||
|
} catch(err) {
|
||||||
|
alert(escapeHtml(err.message));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async function deleteBuilder(id) {
|
async function deleteBuilder(id) {
|
||||||
if (!confirm('Delete this builder?')) return;
|
if (!confirm('Delete this builder?')) return;
|
||||||
const res = await fetch('/api/v1/admin/builders/' + id, { method: 'DELETE' });
|
try {
|
||||||
if (res.ok) window.location.reload();
|
const res = await fetch('/api/v1/admin/builders/' + id, { method: 'DELETE' });
|
||||||
else alert('Failed to delete builder');
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
||||||
|
throw new Error(err.error || err.message || 'Failed to delete builder');
|
||||||
|
}
|
||||||
|
window.location.reload();
|
||||||
|
} catch(err) {
|
||||||
|
alert(escapeHtml(err.message));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -4,377 +4,7 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}FC CI{% endblock %}</title>
|
<title>{% block title %}FC CI{% endblock %}</title>
|
||||||
<style>
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
/* FC CI Dashboard Styles */
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--bg: #fafafa;
|
|
||||||
--fg: #1a1a1a;
|
|
||||||
--border: #ddd;
|
|
||||||
--accent: #2563eb;
|
|
||||||
--muted: #6b7280;
|
|
||||||
--card-bg: #fff;
|
|
||||||
--green: #16a34a;
|
|
||||||
--red: #dc2626;
|
|
||||||
--yellow: #ca8a04;
|
|
||||||
--gray: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--bg: #111827;
|
|
||||||
--fg: #f3f4f6;
|
|
||||||
--border: #374151;
|
|
||||||
--accent: #60a5fa;
|
|
||||||
--muted: #9ca3af;
|
|
||||||
--card-bg: #1f2937;
|
|
||||||
--green: #4ade80;
|
|
||||||
--red: #f87171;
|
|
||||||
--yellow: #fbbf24;
|
|
||||||
--gray: #9ca3af;
|
|
||||||
}
|
|
||||||
|
|
||||||
code { background: #374151; }
|
|
||||||
th { background: #1f2937; }
|
|
||||||
tbody tr:hover { background: #1f2937; }
|
|
||||||
|
|
||||||
.badge-completed { background: #064e3b; color: var(--green); }
|
|
||||||
.badge-failed { background: #450a0a; color: var(--red); }
|
|
||||||
.badge-running { background: #422006; color: var(--yellow); }
|
|
||||||
.badge-pending { background: #1f2937; color: var(--gray); }
|
|
||||||
.badge-cancelled { background: #1f2937; color: var(--gray); }
|
|
||||||
|
|
||||||
.flash-error { background: #450a0a; border-color: var(--red); color: var(--red); }
|
|
||||||
.flash-success { background: #064e3b; border-color: var(--green); color: var(--green); }
|
|
||||||
}
|
|
||||||
|
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--fg);
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
a { color: var(--accent); text-decoration: none; }
|
|
||||||
a:hover { text-decoration: underline; }
|
|
||||||
|
|
||||||
code {
|
|
||||||
background: #f3f4f6;
|
|
||||||
padding: 0.1em 0.3em;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 2rem;
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
background: var(--card-bg);
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-brand a {
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: var(--fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links { display: flex; gap: 1rem; flex: 1; }
|
|
||||||
.nav-links a { color: var(--muted); font-size: 0.9rem; }
|
|
||||||
.nav-links a:hover { color: var(--fg); }
|
|
||||||
|
|
||||||
.nav-auth { display: flex; gap: 0.75rem; align-items: center; font-size: 0.85rem; }
|
|
||||||
.nav-auth .auth-user { color: var(--muted); }
|
|
||||||
.nav-auth a { color: var(--accent); }
|
|
||||||
.nav-auth form { display: inline; }
|
|
||||||
.nav-auth button { background: none; border: none; color: var(--accent); cursor: pointer; font-size: 0.85rem; }
|
|
||||||
.nav-auth button:hover { text-decoration: underline; }
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1100px;
|
|
||||||
margin: 1.5rem auto;
|
|
||||||
padding: 0 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 { margin-bottom: 1rem; font-size: 1.5rem; }
|
|
||||||
h2 { margin: 1.5rem 0 0.75rem; font-size: 1.2rem; }
|
|
||||||
|
|
||||||
.stats-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
||||||
gap: 0.75rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card {
|
|
||||||
background: var(--card-bg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value { font-size: 1.75rem; font-weight: 700; }
|
|
||||||
.stat-label { font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
background: var(--card-bg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
th, td {
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
text-align: left;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
background: #f9fafb;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.03em;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:last-child td { border-bottom: none; }
|
|
||||||
tbody tr:hover { background: #f9fafb; }
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.15em 0.5em;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-completed { background: #dcfce7; color: var(--green); }
|
|
||||||
.badge-failed { background: #fee2e2; color: var(--red); }
|
|
||||||
.badge-running { background: #fef9c3; color: var(--yellow); }
|
|
||||||
.badge-pending { background: #f3f4f6; color: var(--gray); }
|
|
||||||
.badge-cancelled { background: #f3f4f6; color: var(--gray); }
|
|
||||||
|
|
||||||
.empty { color: var(--muted); font-style: italic; }
|
|
||||||
|
|
||||||
.pagination {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-form {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
align-items: end;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-form label {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.25rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-form select,
|
|
||||||
.filter-form input {
|
|
||||||
padding: 0.35rem 0.5rem;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
background: var(--card-bg);
|
|
||||||
color: var(--fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-form button {
|
|
||||||
padding: 0.4rem 1rem;
|
|
||||||
background: var(--accent);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-form button:hover { opacity: 0.9; }
|
|
||||||
|
|
||||||
.breadcrumbs {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.4rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumbs a { color: var(--accent); }
|
|
||||||
.breadcrumbs .sep { color: var(--muted); }
|
|
||||||
.breadcrumbs .current { color: var(--fg); font-weight: 600; }
|
|
||||||
|
|
||||||
.detail-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto 1fr;
|
|
||||||
gap: 0.35rem 1rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-grid dt { font-weight: 600; color: var(--muted); }
|
|
||||||
.detail-grid dd { color: var(--fg); }
|
|
||||||
|
|
||||||
.step-success { color: var(--green); font-weight: 600; }
|
|
||||||
.step-failure { color: var(--red); font-weight: 600; }
|
|
||||||
|
|
||||||
.tab-nav {
|
|
||||||
display: flex;
|
|
||||||
gap: 0;
|
|
||||||
border-bottom: 2px solid var(--border);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-nav a {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
color: var(--muted);
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
margin-bottom: -2px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-nav a:hover { color: var(--fg); text-decoration: none; }
|
|
||||||
.tab-nav a.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
|
|
||||||
|
|
||||||
.queue-summary {
|
|
||||||
display: inline-flex;
|
|
||||||
gap: 1rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background: var(--card-bg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Form styles */
|
|
||||||
.form-card {
|
|
||||||
background: var(--card-bg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 1.5rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--muted);
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input,
|
|
||||||
.form-group select,
|
|
||||||
.form-group textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group textarea { min-height: 60px; resize: vertical; }
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.5rem 1.25rem;
|
|
||||||
background: var(--accent);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:hover { opacity: 0.9; text-decoration: none; }
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
background: var(--red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-small {
|
|
||||||
padding: 0.25rem 0.75rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flash-message {
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
border: 1px solid;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flash-error {
|
|
||||||
background: #fef2f2;
|
|
||||||
border-color: var(--red);
|
|
||||||
color: var(--red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.flash-success {
|
|
||||||
background: #f0fdf4;
|
|
||||||
border-color: var(--green);
|
|
||||||
color: var(--green);
|
|
||||||
}
|
|
||||||
|
|
||||||
details {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
details summary {
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--accent);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
padding: 0.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
text-align: center;
|
|
||||||
padding: 2rem 1rem;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.8rem;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
margin-top: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.navbar { flex-direction: column; gap: 0.5rem; }
|
|
||||||
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
|
||||||
.filter-form { flex-direction: column; }
|
|
||||||
table { font-size: 0.85rem; }
|
|
||||||
th, td { padding: 0.35rem 0.5rem; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav class="navbar">
|
<nav class="navbar">
|
||||||
|
|
@ -391,13 +21,25 @@ details summary {
|
||||||
{% block auth %}{% endblock %}
|
{% block auth %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<main class="container">
|
<main class="page-main">
|
||||||
{% block breadcrumbs %}{% endblock %}
|
<div class="container">
|
||||||
{% block content %}{% endblock %}
|
{% block breadcrumbs %}{% endblock %}
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<p>FC CI — Nix-based continuous integration</p>
|
<p>FC CI — Nix-based continuous integration</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
<script>
|
||||||
|
function escapeHtml(s) {
|
||||||
|
var d = document.createElement('div');
|
||||||
|
d.textContent = s;
|
||||||
|
return d.innerHTML;
|
||||||
|
}
|
||||||
|
function showError(el, msg) {
|
||||||
|
el.innerHTML = '<div class="flash-message flash-error">' + escapeHtml(msg) + '</div>';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,9 @@
|
||||||
|
|
||||||
<h2>Build Steps</h2>
|
<h2>Build Steps</h2>
|
||||||
{% if steps.is_empty() %}
|
{% if steps.is_empty() %}
|
||||||
<p class="empty">No steps recorded.</p>
|
<div class="empty">No steps recorded.</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>#</th><th>Command</th><th>Exit</th><th>Started</th><th>Completed</th></tr>
|
<tr><th>#</th><th>Command</th><th>Exit</th><th>Started</th><th>Completed</th></tr>
|
||||||
|
|
@ -73,12 +74,14 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h2>Build Products</h2>
|
<h2>Build Products</h2>
|
||||||
{% if products.is_empty() %}
|
{% if products.is_empty() %}
|
||||||
<p class="empty">No products recorded.</p>
|
<div class="empty">No products recorded.</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Name</th><th>Path</th><th>Size</th></tr>
|
<tr><th>Name</th><th>Path</th><th>Size</th></tr>
|
||||||
|
|
@ -93,5 +96,6 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,12 @@
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% if builds.is_empty() %}
|
{% if builds.is_empty() %}
|
||||||
<p class="empty">No builds match filters.</p>
|
<div class="empty">
|
||||||
|
<div class="empty-title">No builds match filters</div>
|
||||||
|
<div class="empty-hint">Try adjusting the filters above or wait for builds to be queued.</div>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Job</th><th>Status</th><th>System</th><th>Created</th></tr>
|
<tr><th>Job</th><th>Status</th><th>System</th><th>Created</th></tr>
|
||||||
|
|
@ -34,7 +38,8 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% endif %}
|
</div>
|
||||||
|
{% if total_pages > 1 %}
|
||||||
<div class="pagination">
|
<div class="pagination">
|
||||||
{% if has_prev %}
|
{% if has_prev %}
|
||||||
<a href="/builds?offset={{ prev_offset }}&limit={{ limit }}&status={{ filter_status }}&system={{ filter_system }}&job_name={{ filter_job }}">« Previous</a>
|
<a href="/builds?offset={{ prev_offset }}&limit={{ limit }}&status={{ filter_status }}&system={{ filter_system }}&job_name={{ filter_job }}">« Previous</a>
|
||||||
|
|
@ -44,4 +49,6 @@
|
||||||
<a href="/builds?offset={{ next_offset }}&limit={{ limit }}&status={{ filter_status }}&system={{ filter_system }}&job_name={{ filter_job }}">Next »</a>
|
<a href="/builds?offset={{ next_offset }}&limit={{ limit }}&status={{ filter_status }}&system={{ filter_system }}&job_name={{ filter_job }}">Next »</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,12 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Channels</h1>
|
<h1>Channels</h1>
|
||||||
{% if channels.is_empty() %}
|
{% if channels.is_empty() %}
|
||||||
<p class="empty">No channels configured.</p>
|
<div class="empty">
|
||||||
|
<div class="empty-title">No channels configured</div>
|
||||||
|
<div class="empty-hint">Channels track successful evaluations for stable release tracking.</div>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Name</th><th>Current Evaluation</th><th>Updated</th></tr>
|
<tr><th>Name</th><th>Current Evaluation</th><th>Updated</th></tr>
|
||||||
|
|
@ -26,5 +30,6 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,9 @@
|
||||||
|
|
||||||
<h2>Builds</h2>
|
<h2>Builds</h2>
|
||||||
{% if builds.is_empty() %}
|
{% if builds.is_empty() %}
|
||||||
<p class="empty">No builds for this evaluation.</p>
|
<div class="empty">No builds for this evaluation.</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Job</th><th>Status</th><th>System</th><th>Created</th></tr>
|
<tr><th>Job</th><th>Status</th><th>System</th><th>Created</th></tr>
|
||||||
|
|
@ -57,5 +58,6 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,12 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Evaluations</h1>
|
<h1>Evaluations</h1>
|
||||||
{% if evals.is_empty() %}
|
{% if evals.is_empty() %}
|
||||||
<p class="empty">No evaluations yet.</p>
|
<div class="empty">
|
||||||
|
<div class="empty-title">No evaluations yet</div>
|
||||||
|
<div class="empty-hint">Evaluations will appear here once a jobset is evaluated.</div>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Commit</th><th>Project</th><th>Jobset</th><th>Status</th><th>Time</th></tr>
|
<tr><th>Commit</th><th>Project</th><th>Jobset</th><th>Status</th><th>Time</th></tr>
|
||||||
|
|
@ -21,7 +25,8 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% endif %}
|
</div>
|
||||||
|
{% if total_pages > 1 %}
|
||||||
<div class="pagination">
|
<div class="pagination">
|
||||||
{% if has_prev %}
|
{% if has_prev %}
|
||||||
<a href="/evaluations?offset={{ prev_offset }}&limit={{ limit }}">« Previous</a>
|
<a href="/evaluations?offset={{ prev_offset }}&limit={{ limit }}">« Previous</a>
|
||||||
|
|
@ -31,4 +36,6 @@
|
||||||
<a href="/evaluations?offset={{ next_offset }}&limit={{ limit }}">Next »</a>
|
<a href="/evaluations?offset={{ next_offset }}&limit={{ limit }}">Next »</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -17,84 +17,132 @@
|
||||||
<div class="stat-label">Total Builds</div>
|
<div class="stat-label">Total Builds</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-value">{{ completed_builds }}</div>
|
<div class="stat-value stat-value-green">{{ completed_builds }}</div>
|
||||||
<div class="stat-label">Completed</div>
|
<div class="stat-label">Completed</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-value">{{ failed_builds }}</div>
|
<div class="stat-value stat-value-red">{{ failed_builds }}</div>
|
||||||
<div class="stat-label">Failed</div>
|
<div class="stat-label">Failed</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-value">{{ running_builds }}</div>
|
<div class="stat-value stat-value-yellow">{{ running_builds }}</div>
|
||||||
<div class="stat-label">Running</div>
|
<div class="stat-label">Running</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-value">{{ pending_builds }}</div>
|
<div class="stat-value">{{ pending_builds }}</div>
|
||||||
<div class="stat-label">Pending</div>
|
<div class="stat-label">Pending</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if total_builds > 0 %}
|
||||||
|
<div class="stat-card">
|
||||||
|
{% let rate = completed_builds * 100 / total_builds %}
|
||||||
|
<div class="stat-value">
|
||||||
|
{% if rate >= 80 %}
|
||||||
|
<span class="success-rate success-rate-high">{{ rate }}%</span>
|
||||||
|
{% else if rate >= 50 %}
|
||||||
|
<span class="success-rate success-rate-mid">{{ rate }}%</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="success-rate success-rate-low">{{ rate }}%</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">Success Rate</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="queue-summary">
|
{% if is_admin %}
|
||||||
<a href="/queue">Queue: {{ pending_builds }} pending, {{ running_builds }} running</a>
|
<div class="quick-actions">
|
||||||
</p>
|
<a href="/projects/new">New Project</a>
|
||||||
|
<a href="/admin">Admin Panel</a>
|
||||||
{% if !projects.is_empty() %}
|
<a href="/queue">Build Queue</a>
|
||||||
<h2>Projects Overview</h2>
|
</div>
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr><th>Project</th><th>Jobsets</th><th>Last Eval</th><th>Time</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for p in projects %}
|
|
||||||
<tr>
|
|
||||||
<td><a href="/project/{{ p.id }}">{{ p.name }}</a></td>
|
|
||||||
<td>{{ p.jobset_count }}</td>
|
|
||||||
<td><span class="badge badge-{{ p.last_eval_class }}">{{ p.last_eval_status }}</span></td>
|
|
||||||
<td>{{ p.last_eval_time }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h2>Recent Builds</h2>
|
<div class="dashboard-grid">
|
||||||
{% if recent_builds.is_empty() %}
|
<div>
|
||||||
<p class="empty">No builds yet.</p>
|
<h2>Recent Builds</h2>
|
||||||
{% else %}
|
{% if recent_builds.is_empty() %}
|
||||||
<table>
|
<div class="empty">
|
||||||
<thead>
|
<div class="empty-title">No builds yet</div>
|
||||||
<tr><th>Job</th><th>Status</th><th>System</th><th>Created</th></tr>
|
<div class="empty-hint">Builds will appear here once an evaluation triggers them.</div>
|
||||||
</thead>
|
</div>
|
||||||
<tbody>
|
{% else %}
|
||||||
{% for b in recent_builds %}
|
<div class="table-wrap">
|
||||||
<tr>
|
<table>
|
||||||
<td><a href="/build/{{ b.id }}">{{ b.job_name }}</a></td>
|
<thead>
|
||||||
<td><span class="badge badge-{{ b.status_class }}">{{ b.status_text }}</span></td>
|
<tr><th>Job</th><th>Status</th><th>System</th><th>Created</th></tr>
|
||||||
<td>{{ b.system }}</td>
|
</thead>
|
||||||
<td>{{ b.created_at }}</td>
|
<tbody>
|
||||||
</tr>
|
{% for b in recent_builds %}
|
||||||
{% endfor %}
|
<tr>
|
||||||
</tbody>
|
<td><a href="/build/{{ b.id }}">{{ b.job_name }}</a></td>
|
||||||
</table>
|
<td><span class="badge badge-{{ b.status_class }}">{{ b.status_text }}</span></td>
|
||||||
{% endif %}
|
<td>{{ b.system }}</td>
|
||||||
|
<td>{{ b.created_at }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<h2>Recent Evaluations</h2>
|
<h2>Recent Evaluations</h2>
|
||||||
{% if recent_evals.is_empty() %}
|
{% if recent_evals.is_empty() %}
|
||||||
<p class="empty">No evaluations yet.</p>
|
<div class="empty">
|
||||||
{% else %}
|
<div class="empty-title">No evaluations yet</div>
|
||||||
<table>
|
<div class="empty-hint">The evaluator will poll configured jobsets automatically.</div>
|
||||||
<thead>
|
</div>
|
||||||
<tr><th>Commit</th><th>Status</th><th>Time</th></tr>
|
{% else %}
|
||||||
</thead>
|
<div class="table-wrap">
|
||||||
<tbody>
|
<table>
|
||||||
{% for e in recent_evals %}
|
<thead>
|
||||||
<tr>
|
<tr><th>Commit</th><th>Status</th><th>Time</th></tr>
|
||||||
<td><a href="/evaluation/{{ e.id }}">{{ e.commit_short }}</a></td>
|
</thead>
|
||||||
<td><span class="badge badge-{{ e.status_class }}">{{ e.status_text }}</span></td>
|
<tbody>
|
||||||
<td>{{ e.time }}</td>
|
{% for e in recent_evals %}
|
||||||
</tr>
|
<tr>
|
||||||
{% endfor %}
|
<td><a href="/evaluation/{{ e.id }}">{{ e.commit_short }}</a></td>
|
||||||
</tbody>
|
<td><span class="badge badge-{{ e.status_class }}">{{ e.status_text }}</span></td>
|
||||||
</table>
|
<td>{{ e.time }}</td>
|
||||||
{% endif %}
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Projects</h2>
|
||||||
|
{% if projects.is_empty() %}
|
||||||
|
<div class="empty">
|
||||||
|
<div class="empty-title">No projects yet</div>
|
||||||
|
{% if is_admin %}
|
||||||
|
<div class="empty-hint"><a href="/projects/new">Create a project</a> to get started.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Project</th><th>Jobsets</th><th>Last Eval</th><th>Time</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for p in projects %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="/project/{{ p.id }}">{{ p.name }}</a></td>
|
||||||
|
<td>{{ p.jobset_count }}</td>
|
||||||
|
<td><span class="badge badge-{{ p.last_eval_class }}">{{ p.last_eval_status }}</span></td>
|
||||||
|
<td>{{ p.last_eval_time }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p class="queue-summary">
|
||||||
|
<a href="/queue">Queue: {{ pending_builds }} pending, {{ running_builds }} running</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,12 @@
|
||||||
|
|
||||||
<h2>Recent Evaluations</h2>
|
<h2>Recent Evaluations</h2>
|
||||||
{% if eval_summaries.is_empty() %}
|
{% if eval_summaries.is_empty() %}
|
||||||
<p class="empty">No evaluations yet.</p>
|
<div class="empty">
|
||||||
|
<div class="empty-title">No evaluations yet</div>
|
||||||
|
<div class="empty-hint">The evaluator will poll this jobset based on the check interval.</div>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Commit</th><th>Status</th><th>Succeeded</th><th>Failed</th><th>Pending</th><th>Time</th></tr>
|
<tr><th>Commit</th><th>Status</th><th>Succeeded</th><th>Failed</th><th>Pending</th><th>Time</th></tr>
|
||||||
|
|
@ -61,5 +65,6 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,24 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Login - FC CI{% endblock %}
|
{% block title %}Login - FC CI{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Login</h1>
|
<div class="login-container">
|
||||||
<div class="form-card">
|
<div class="login-card">
|
||||||
{% match error %}
|
<h1>FC CI</h1>
|
||||||
{% when Some with (msg) %}
|
<p class="login-subtitle">Sign in with your API key</p>
|
||||||
<div class="flash-message flash-error">{{ msg }}</div>
|
{% match error %}
|
||||||
{% when None %}
|
{% when Some with (msg) %}
|
||||||
{% endmatch %}
|
<div class="flash-message flash-error">{{ msg }}</div>
|
||||||
<form method="POST" action="/login">
|
{% when None %}
|
||||||
<div class="form-group">
|
{% endmatch %}
|
||||||
<label for="api_key">API Key</label>
|
<div class="form-card login-form">
|
||||||
<input type="password" id="api_key" name="api_key" placeholder="fc_..." required>
|
<form method="POST" action="/login">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="api_key">API Key</label>
|
||||||
|
<input type="password" id="api_key" name="api_key" placeholder="fc_..." required autocomplete="current-password">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-full">Sign in</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn">Login</button>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
<p><strong>Created:</strong> {{ project.created_at.format("%Y-%m-%d %H:%M") }}</p>
|
<p><strong>Created:</strong> {{ project.created_at.format("%Y-%m-%d %H:%M") }}</p>
|
||||||
|
|
||||||
{% if is_admin %}
|
{% if is_admin %}
|
||||||
<div style="margin-top: 0.5rem;">
|
<div class="action-bar">
|
||||||
<button class="btn btn-danger btn-small" onclick="deleteProject()">Delete Project</button>
|
<button class="btn btn-danger btn-small" onclick="deleteProject()">Delete Project</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -56,8 +56,14 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if jobsets.is_empty() %}
|
{% if jobsets.is_empty() %}
|
||||||
<p class="empty">No jobsets configured.</p>
|
<div class="empty">
|
||||||
|
<div class="empty-title">No jobsets configured</div>
|
||||||
|
{% if is_admin %}
|
||||||
|
<div class="empty-hint">Add a jobset above to start evaluating this project.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Name</th><th>Expression</th><th>Flake</th><th>Enabled</th><th>Interval</th></tr>
|
<tr><th>Name</th><th>Expression</th><th>Flake</th><th>Enabled</th><th>Interval</th></tr>
|
||||||
|
|
@ -74,12 +80,14 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h2>Recent Evaluations</h2>
|
<h2>Recent Evaluations</h2>
|
||||||
{% if recent_evals.is_empty() %}
|
{% if recent_evals.is_empty() %}
|
||||||
<p class="empty">No evaluations yet.</p>
|
<div class="empty">No evaluations yet for this project.</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Commit</th><th>Status</th><th>Time</th></tr>
|
<tr><th>Commit</th><th>Status</th><th>Time</th></tr>
|
||||||
|
|
@ -94,6 +102,7 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
|
|
@ -112,21 +121,27 @@ document.getElementById('create-jobset-form')?.addEventListener('submit', async
|
||||||
flake_mode: document.getElementById('js-flake').checked,
|
flake_mode: document.getElementById('js-flake').checked,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (!res.ok) {
|
||||||
window.location.reload();
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
||||||
} else {
|
throw new Error(err.error || err.message || 'Unknown error');
|
||||||
const err = await res.json();
|
|
||||||
msg.innerHTML = '<div class="flash-message flash-error">' + (err.error || 'Error') + '</div>';
|
|
||||||
}
|
}
|
||||||
} catch(e) {
|
window.location.reload();
|
||||||
msg.innerHTML = '<div class="flash-message flash-error">Request failed</div>';
|
} catch(err) {
|
||||||
|
showError(msg, err.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
async function deleteProject() {
|
async function deleteProject() {
|
||||||
if (!confirm('Delete this project and all its data?')) return;
|
if (!confirm('Delete this project and all its data?')) return;
|
||||||
const res = await fetch('/api/v1/projects/{{ project.id }}', { method: 'DELETE' });
|
try {
|
||||||
if (res.ok) window.location.href = '/projects';
|
const res = await fetch('/api/v1/projects/{{ project.id }}', { method: 'DELETE' });
|
||||||
else alert('Failed to delete project');
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
||||||
|
throw new Error(err.error || err.message || 'Failed to delete project');
|
||||||
|
}
|
||||||
|
window.location.href = '/projects';
|
||||||
|
} catch(err) {
|
||||||
|
alert(escapeHtml(err.message));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,9 @@
|
||||||
<h1>Projects</h1>
|
<h1>Projects</h1>
|
||||||
|
|
||||||
{% if is_admin %}
|
{% if is_admin %}
|
||||||
|
<p class="action-bar"><a href="/projects/new" class="btn">New Project (Setup Wizard)</a></p>
|
||||||
<details>
|
<details>
|
||||||
<summary>New Project</summary>
|
<summary>Quick create (no probe)</summary>
|
||||||
<div class="form-card">
|
<div class="form-card">
|
||||||
<form id="create-project-form">
|
<form id="create-project-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
@ -36,8 +37,16 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if projects.is_empty() %}
|
{% if projects.is_empty() %}
|
||||||
<p class="empty">No projects yet.</p>
|
<div class="empty">
|
||||||
|
<div class="empty-title">No projects yet</div>
|
||||||
|
{% if is_admin %}
|
||||||
|
<div class="empty-hint">Create a project using the button above to get started.</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="empty-hint">Projects will appear here once an administrator creates them.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Name</th><th>Repository</th><th>Created</th></tr>
|
<tr><th>Name</th><th>Repository</th><th>Created</th></tr>
|
||||||
|
|
@ -52,7 +61,8 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% endif %}
|
</div>
|
||||||
|
{% if total_pages > 1 %}
|
||||||
<div class="pagination">
|
<div class="pagination">
|
||||||
{% if has_prev %}
|
{% if has_prev %}
|
||||||
<a href="/projects?offset={{ prev_offset }}&limit={{ limit }}">« Previous</a>
|
<a href="/projects?offset={{ prev_offset }}&limit={{ limit }}">« Previous</a>
|
||||||
|
|
@ -62,6 +72,8 @@
|
||||||
<a href="/projects?offset={{ next_offset }}&limit={{ limit }}">Next »</a>
|
<a href="/projects?offset={{ next_offset }}&limit={{ limit }}">Next »</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{% if is_admin %}
|
{% if is_admin %}
|
||||||
|
|
@ -79,14 +91,13 @@ document.getElementById('create-project-form')?.addEventListener('submit', async
|
||||||
description: document.getElementById('project-desc').value || null,
|
description: document.getElementById('project-desc').value || null,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (!res.ok) {
|
||||||
window.location.reload();
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
||||||
} else {
|
throw new Error(err.error || err.message || 'Unknown error');
|
||||||
const err = await res.json();
|
|
||||||
msg.innerHTML = '<div class="flash-message flash-error">' + (err.error || 'Error') + '</div>';
|
|
||||||
}
|
}
|
||||||
} catch(e) {
|
window.location.reload();
|
||||||
msg.innerHTML = '<div class="flash-message flash-error">Request failed</div>';
|
} catch(err) {
|
||||||
|
showError(msg, err.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,12 @@
|
||||||
|
|
||||||
<h2>Running ({{ running_count }})</h2>
|
<h2>Running ({{ running_count }})</h2>
|
||||||
{% if running_builds.is_empty() %}
|
{% if running_builds.is_empty() %}
|
||||||
<p class="empty">No builds currently running.</p>
|
<div class="empty">
|
||||||
|
<div class="empty-title">No builds currently running</div>
|
||||||
|
<div class="empty-hint">Running builds will appear here when the queue runner picks them up.</div>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Job</th><th>System</th><th>Started</th></tr>
|
<tr><th>Job</th><th>System</th><th>Started</th></tr>
|
||||||
|
|
@ -27,12 +31,17 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h2>Pending ({{ pending_count }})</h2>
|
<h2>Pending ({{ pending_count }})</h2>
|
||||||
{% if pending_builds.is_empty() %}
|
{% if pending_builds.is_empty() %}
|
||||||
<p class="empty">No builds pending.</p>
|
<div class="empty">
|
||||||
|
<div class="empty-title">No builds pending</div>
|
||||||
|
<div class="empty-hint">Pending builds appear after an evaluation discovers new derivations to build.</div>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Job</th><th>System</th><th>Created</th></tr>
|
<tr><th>Job</th><th>System</th><th>Created</th></tr>
|
||||||
|
|
@ -47,5 +56,6 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue