diff --git a/workers/updater/preview/server.mjs b/workers/updater/preview/server.mjs
index 2d65d3f..8c42348 100644
--- a/workers/updater/preview/server.mjs
+++ b/workers/updater/preview/server.mjs
@@ -67,7 +67,7 @@ createServer(async (request, response) => {
(await readFile(new URL('../src/admin/page.html', import.meta.url), 'utf8')).replaceAll(
'__NONCE__',
'preview',
- ),
+ ).replaceAll('__STAGING_HIDDEN__', ''),
);
}
diff --git a/workers/updater/src/admin/page.html b/workers/updater/src/admin/page.html
index 931aec0..6a1c07b 100644
--- a/workers/updater/src/admin/page.html
+++ b/workers/updater/src/admin/page.html
@@ -41,7 +41,8 @@
color: var(--purple);
}
button:focus-visible,
- a:focus-visible {
+ a:focus-visible,
+ summary:focus-visible {
outline: 3px solid var(--purple);
outline-offset: 4px;
}
@@ -66,6 +67,19 @@
font-size: 18px;
letter-spacing: -0.5px;
}
+ .site-name {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ }
+ .staging {
+ color: #e6d595;
+ background: #333024;
+ border-radius: 4px;
+ padding: 4px 7px;
+ font-size: 11px;
+ font-weight: 600;
+ }
.brand span {
color: var(--purple);
}
@@ -284,21 +298,55 @@
overflow: hidden;
}
.record {
- padding: 20px 23px;
+ margin: 0;
background: #191a23;
}
.record + .record {
border-top: 1px solid var(--line);
}
.record-top {
- display: flex;
- justify-content: space-between;
+ display: grid;
+ grid-template-columns: 10px minmax(100px, 1fr) 90px auto 85px;
align-items: center;
- gap: 16px;
+ gap: 12px;
+ min-height: 48px;
+ padding: 10px 16px;
+ list-style: none;
+ }
+ .record-top::-webkit-details-marker {
+ display: none;
+ }
+ .record-top::before {
+ content: '›';
+ font-size: 18px;
+ }
+ .record[open] > .record-top::before {
+ transform: rotate(90deg);
+ }
+ .record-top:hover {
+ background: #22232e;
}
.record-name {
+ color: #f3f2fa;
font-weight: 550;
- font-size: 14px;
+ font-size: 13px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .record-time,
+ .record-trigger {
+ white-space: nowrap;
+ font-size: 11px;
+ }
+ .record-top .badge {
+ text-align: center;
+ }
+ .record-body {
+ padding: 0 16px 16px 38px;
+ }
+ .history-empty {
+ padding: 16px;
}
.badge {
font-size: 11px;
@@ -415,8 +463,23 @@
max-width: 180px;
line-height: 1.5;
}
- .record {
- padding: 18px 17px;
+ .record-top {
+ grid-template-columns: 8px minmax(0, 1fr) auto 65px;
+ gap: 7px;
+ padding: 10px;
+ }
+ .record-trigger {
+ display: none;
+ }
+ .record-top .badge {
+ font-size: 10px;
+ padding: 4px;
+ }
+ .record-time {
+ font-size: 10px;
+ }
+ .record-body {
+ padding: 0 12px 12px;
}
.footer {
flex-direction: column;
@@ -473,7 +536,10 @@
- splatoon2.ink
+
+
splatoon2.ink
+
Staging
+
Administration
@@ -612,68 +678,77 @@
}
function renderRunRecord(label, run) {
- const row = document.createElement('div');
+ if (!run) {
+ const empty = document.createElement('p');
+ empty.className = 'history-empty';
+ empty.textContent = 'No runs yet.';
+
+ return empty;
+ }
+
+ const row = document.createElement('details');
row.className = 'record';
- const top = document.createElement('div');
+ const top = document.createElement('summary');
top.className = 'record-top';
const title = document.createElement('span');
title.className = 'record-name';
- title.textContent = label;
+ title.textContent = runNames[run.mode] || 'Full cycle';
+
+ const trigger = document.createElement('span');
+ trigger.className = 'record-trigger';
+ trigger.textContent = label;
+
+ const time = document.createElement('time');
+ time.className = 'record-time';
+ time.dateTime = new Date(run.startedAt).toISOString();
+ time.textContent = formatDate(run.startedAt);
const badge = document.createElement('span');
- badge.className = 'badge' + (!run ? ' neutral' : run.ok ? '' : ' failed');
- badge.textContent = !run ? 'No runs yet' : run.ok ? 'Completed' : 'Needs attention';
- top.append(title, badge);
- row.append(top);
+ badge.className = 'badge' + (run.ok ? '' : ' failed');
+ badge.textContent = run.ok ? 'Completed' : 'Failed';
+ top.append(title, trigger, time, badge);
+
+ const body = document.createElement('div');
+ body.className = 'record-body';
const detail = document.createElement('div');
detail.className = 'detail';
- detail.textContent = run
- ? `${runNames[run.mode] || 'Full cycle'} · ${formatDate(run.startedAt)} · ${Math.max(
- 1,
- Math.round((run.runMs || 0) / 1000),
- )}s`
- : 'Results will appear here after a run.';
- row.append(detail);
+ detail.textContent = `${label} · ${Math.max(1, Math.round((run.runMs || 0) / 1000))}s`;
+ body.append(detail);
+ row.append(top, body);
- if (run) {
- const messages = [
- run.error,
- ...(run.updaters?.updaters || [])
- .filter((item) => !item.ok)
- .map((item) => item.error || `${item.name} failed`),
- ...(run.social?.posts || [])
- .filter((item) => item && !item.ok)
- .map((item) => item.error || 'A social post failed'),
- ].filter(Boolean);
+ const messages = [
+ run.error,
+ ...(run.updaters?.updaters || [])
+ .filter((item) => !item.ok)
+ .map((item) => item.error || `${item.name} failed`),
+ ...(run.social?.posts || [])
+ .filter((item) => item && !item.ok)
+ .map((item) => item.error || 'A social post failed'),
+ ].filter(Boolean);
- if (messages.length) {
- const error = document.createElement('div');
- error.className = 'error';
- error.textContent = messages.join(' · ');
- row.append(error);
- }
-
- const logDetails = document.createElement('details');
- const logSummary = document.createElement('summary');
- logSummary.textContent = 'View log lines';
- logDetails.append(logSummary, renderLogLines(run.logs));
- row.append(logDetails);
-
- const details = document.createElement('details');
- const summary = document.createElement('summary');
- const pre = document.createElement('pre');
- summary.textContent = 'View run details';
-
- const summaryResult = { ...run };
- delete summaryResult.logs;
- pre.textContent = JSON.stringify(summaryResult, null, 2);
- details.append(summary, pre);
- row.append(details);
+ if (messages.length) {
+ const error = document.createElement('div');
+ error.className = 'error';
+ error.textContent = messages.join(' · ');
+ body.append(error);
}
+ body.append(renderLogLines(run.logs));
+
+ const details = document.createElement('details');
+ const summary = document.createElement('summary');
+ const pre = document.createElement('pre');
+ summary.textContent = 'View run details';
+
+ const summaryResult = { ...run };
+ delete summaryResult.logs;
+ pre.textContent = JSON.stringify(summaryResult, null, 2);
+ details.append(summary, pre);
+ body.append(details);
+
return row;
}
diff --git a/workers/updater/src/admin/routes.mjs b/workers/updater/src/admin/routes.mjs
index c7c6064..0f05574 100644
--- a/workers/updater/src/admin/routes.mjs
+++ b/workers/updater/src/admin/routes.mjs
@@ -16,7 +16,11 @@ export async function adminRequest(request, env, getScheduler) {
if (request.method === 'GET' && ['/', '/admin', '/admin/'].includes(url.pathname)) {
let nonce = crypto.randomUUID();
- return new Response(page.replaceAll('__NONCE__', nonce), {
+ const staging = new URL(env.SITE_URL || request.url).hostname.startsWith('dev.');
+ const html = page.replaceAll('__NONCE__', nonce)
+ .replaceAll('__STAGING_HIDDEN__', staging ? '' : 'hidden');
+
+ return new Response(html, {
headers: {
...headers,
'Content-Type': 'text/html; charset=utf-8',