Add Prettier and ESLint tooling (Refs #77)

This commit is contained in:
2026-10-09 15:56:13 -04:00
parent 8ed059b8d3
commit 16469e2d43
711 changed files with 99737 additions and 28286 deletions
+264 -43
View File
@@ -23,7 +23,9 @@
let requestNumber = 0;
let controller: AbortController | null = null;
let disposed = false;
const newEvents = $derived(baselineTotal === null ? 0 : Math.max(0, detail.activity.total - baselineTotal));
const newEvents = $derived(
baselineTotal === null ? 0 : Math.max(0, detail.activity.total - baselineTotal)
);
async function load(more = false) {
if (more && (loading || cursor === null)) return;
@@ -33,7 +35,8 @@
const request = ++requestNumber;
const id = detail.deployment.id;
const current = () => !disposed && request === requestNumber && detail.deployment.id === id;
loading = true; error = '';
loading = true;
error = '';
const requestedQuery = more ? appliedQuery : query;
const requestedOrder = more ? appliedOrder : order;
const params = new URLSearchParams({ order: requestedOrder, limit: '25' });
@@ -43,18 +46,44 @@
params.set('watermark', String(watermark));
}
let timedOut = false;
const timeout = setTimeout(() => { timedOut = true; currentController.abort(); }, 15_000);
const timeout = setTimeout(() => {
timedOut = true;
currentController.abort();
}, 15_000);
try {
const response = await fetch(`/api/provisioning/${encodeURIComponent(id)}/events?${params}`, { signal: currentController.signal, cache: 'no-store' });
const response = await fetch(
`/api/provisioning/${encodeURIComponent(id)}/events?${params}`,
{ signal: currentController.signal, cache: 'no-store' }
);
if (!response.ok) throw new Error('Could not load activity history.');
const body: unknown = await response.json();
if (!isProvisioningEventPage(body)) throw new Error('The activity response was incomplete.');
if (!isProvisioningEventPage(body))
throw new Error('The activity response was incomplete.');
if (!current()) return;
entries = more ? [...entries, ...body.events.filter(event => !entries.some(previous => previous.id === event.id))] : body.events;
total = body.total; watermark = body.watermark; cursor = body.nextCursor; loaded = true;
if (!more) { baselineTotal = body.totalEvents; appliedQuery = requestedQuery; appliedOrder = requestedOrder; }
entries = more
? [
...entries,
...body.events.filter(
(event) => !entries.some((previous) => previous.id === event.id)
)
]
: body.events;
total = body.total;
watermark = body.watermark;
cursor = body.nextCursor;
loaded = true;
if (!more) {
baselineTotal = body.totalEvents;
appliedQuery = requestedQuery;
appliedOrder = requestedOrder;
}
} catch (reason) {
if (current() && (!currentController.signal.aborted || timedOut)) error = timedOut ? 'Activity refresh timed out. Try again.' : reason instanceof Error ? reason.message : 'Could not load activity history.';
if (current() && (!currentController.signal.aborted || timedOut))
error = timedOut
? 'Activity refresh timed out. Try again.'
: reason instanceof Error
? reason.message
: 'Could not load activity history.';
} finally {
clearTimeout(timeout);
if (current()) loading = false;
@@ -65,55 +94,247 @@
if (expanded && !loaded && !loading) void load();
}
function jumpToLatest() {
query = appliedQuery; order = 'newest';
query = appliedQuery;
order = 'newest';
void load();
}
onDestroy(() => { disposed = true; requestNumber++; controller?.abort(); });
onDestroy(() => {
disposed = true;
requestNumber++;
controller?.abort();
});
</script>
<section aria-labelledby="activity-title" class="activity">
<h2 id="activity-title">Activity log</h2>
<details class="history" ontoggle={toggle}>
<summary>Full activity history <span>{countLabel(detail.activity.total, 'event')}</span></summary>
<summary
>Full activity history <span>{countLabel(detail.activity.total, 'event')}</span
></summary
>
<div class="history-content">
{#if newEvents > 0}<div class="new-events" role="status"><span>{newEvents} new event{newEvents === 1 ? '' : 's'} since this view was loaded.</span><Button size="sm" onclick={jumpToLatest}>Jump to latest</Button></div>{/if}
<form class="tools" onsubmit={event => { event.preventDefault(); void load(); }}>
<label class="search">Search activity<input type="search" bind:value={query} placeholder="Find a stage or message" /></label>
<label>Order<select bind:value={order} onchange={() => { void load(); }}><option value="newest">Newest first</option><option value="oldest">Oldest first</option></select></label>
{#if newEvents > 0}<div class="new-events" role="status">
<span
>{newEvents} new event{newEvents === 1 ? '' : 's'} since this view was loaded.</span
><Button size="sm" onclick={jumpToLatest}>Jump to latest</Button>
</div>{/if}
<form
class="tools"
onsubmit={(event) => {
event.preventDefault();
void load();
}}
>
<label class="search"
>Search activity<input
type="search"
bind:value={query}
placeholder="Find a stage or message"
/></label
>
<label
>Order<select
bind:value={order}
onchange={() => {
void load();
}}
><option value="newest">Newest first</option><option value="oldest"
>Oldest first</option
></select
></label
>
<Button type="submit" disabled={loading}>Search</Button>
</form>
{#if error}<div class="history-error" role="alert"><p>{error} {loaded ? 'Your previous history view is still shown.' : ''}</p><Button size="sm" onclick={() => load()}>Retry history</Button></div>{/if}
{#if loaded}<p class="muted">{appliedOrder === 'newest' ? 'Newest first' : 'Oldest first'}{appliedQuery ? ` · Matching “${appliedQuery}”` : ' · All messages'}</p>{/if}
{#if error}<div class="history-error" role="alert">
<p>{error} {loaded ? 'Your previous history view is still shown.' : ''}</p>
<Button size="sm" onclick={() => load()}>Retry history</Button>
</div>{/if}
{#if loaded}<p class="muted">
{appliedOrder === 'newest' ? 'Newest first' : 'Oldest first'}{appliedQuery
? ` · Matching “${appliedQuery}”`
: ' · All messages'}
</p>{/if}
{#if loading && !loaded}<p class="muted" role="status">Loading activity…</p>
{:else if loaded && !entries.length}<p class="empty">{appliedQuery ? 'No activity matches this search.' : 'No detailed activity recorded.'}</p>
{:else if loaded && !entries.length}<p class="empty">
{appliedQuery
? 'No activity matches this search.'
: 'No detailed activity recorded.'}
</p>
{:else}<ol class="timeline">
{#each entries as event (event.id)}<li><time class="time" datetime={timestampISO(event.created_at)}>{$timeFormat.dateTime(event.created_at)}</time><p>{event.message}</p></li>{/each}
</ol>{/if}
{#if loaded}<div class="footer"><span>Showing {entries.length} of {total} matching events</span>{#if cursor !== null}<Button size="sm" loading={loading} disabled={loading} onclick={() => load(true)}>Show more activity</Button>{/if}</div>{/if}
{#each entries as event (event.id)}<li>
<time class="time" datetime={timestampISO(event.created_at)}
>{$timeFormat.dateTime(event.created_at)}</time
>
<p>{event.message}</p>
</li>{/each}
</ol>{/if}
{#if loaded}<div class="footer">
<span>Showing {entries.length} of {total} matching events</span
>{#if cursor !== null}<Button
size="sm"
{loading}
disabled={loading}
onclick={() => load(true)}>Show more activity</Button
>{/if}
</div>{/if}
</div>
</details>
</section>
<style>
.activity { display: grid; gap: var(--space-4); min-width: 0; }
h2 { margin: 0; font-size: var(--text-lg); letter-spacing: var(--tracking-tight); }
p { margin: 0; line-height: 1.65; overflow-wrap: anywhere; font-size: var(--text-sm); }
.muted, .time, .footer { font-size: var(--text-xs); color: var(--text-secondary); }
.empty { padding: var(--space-4); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); color: var(--text-secondary); }
.timeline { list-style: none; margin: 0 0 0 var(--space-1); padding: 0 0 0 var(--space-5); border-left: 1px solid var(--border-strong); }
.timeline li { padding: 0 0 var(--space-5); position: relative; }.timeline li:last-child { padding-bottom: 0; }
.timeline li::before { content: ''; position: absolute; top: 6px; left: -24px; width: 7px; height: 7px; background: var(--border-control); border-radius: var(--radius-pill); }
.timeline p { margin-top: var(--space-1); }
.history { border: 1px solid var(--border-subtle); border-radius: var(--radius-md); background: var(--surface); }
summary { cursor: pointer; padding: var(--space-5); font-size: var(--text-sm); font-weight: var(--weight-semibold); overflow-wrap: anywhere; }
summary span { margin-left: var(--space-2); color: var(--text-tertiary); font-size: var(--text-xs); font-weight: var(--weight-regular); }
summary:focus-visible { outline: 2px solid var(--border-focus); outline-offset: -3px; border-radius: var(--radius-sm); }
.history-content { padding: 0 var(--space-5) var(--space-5); display: grid; gap: var(--space-5); }
.tools { display: flex; flex-wrap: wrap; align-items: end; gap: var(--space-3); }.tools label { display: grid; gap: var(--space-2); font-size: var(--text-xs); color: var(--text-secondary); }.search { flex: 1 1 160px; }
input, select { background: var(--surface-sunken); color: var(--text-primary); border: 1px solid var(--border-control); border-radius: var(--radius-sm); padding: 10px; font: inherit; min-height: 40px; width: 100%; }
input:focus-visible, select:focus-visible { outline: 2px solid var(--border-focus); outline-offset: 2px; }
.new-events { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); padding: var(--space-3); color: var(--text-secondary); font-size: var(--text-sm); }.new-events span { flex: 1; }
.footer { display: flex; justify-content: space-between; align-items: center; gap: var(--space-3); flex-wrap: wrap; line-height: 1.6; }
.history-error { display: grid; justify-items: start; gap: var(--space-3); color: var(--state-danger); }
@media (max-width: 600px) { summary { padding: var(--space-4); }.history-content { padding: 0 var(--space-4) var(--space-4); }.tools { align-items: stretch; }.tools .search { flex-basis: 100%; } }
.activity {
display: grid;
gap: var(--space-4);
min-width: 0;
}
h2 {
margin: 0;
font-size: var(--text-lg);
letter-spacing: var(--tracking-tight);
}
p {
margin: 0;
line-height: 1.65;
overflow-wrap: anywhere;
font-size: var(--text-sm);
}
.muted,
.time,
.footer {
font-size: var(--text-xs);
color: var(--text-secondary);
}
.empty {
padding: var(--space-4);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
color: var(--text-secondary);
}
.timeline {
list-style: none;
margin: 0 0 0 var(--space-1);
padding: 0 0 0 var(--space-5);
border-left: 1px solid var(--border-strong);
}
.timeline li {
padding: 0 0 var(--space-5);
position: relative;
}
.timeline li:last-child {
padding-bottom: 0;
}
.timeline li::before {
content: '';
position: absolute;
top: 6px;
left: -24px;
width: 7px;
height: 7px;
background: var(--border-control);
border-radius: var(--radius-pill);
}
.timeline p {
margin-top: var(--space-1);
}
.history {
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
background: var(--surface);
}
summary {
cursor: pointer;
padding: var(--space-5);
font-size: var(--text-sm);
font-weight: var(--weight-semibold);
overflow-wrap: anywhere;
}
summary span {
margin-left: var(--space-2);
color: var(--text-tertiary);
font-size: var(--text-xs);
font-weight: var(--weight-regular);
}
summary:focus-visible {
outline: 2px solid var(--border-focus);
outline-offset: -3px;
border-radius: var(--radius-sm);
}
.history-content {
padding: 0 var(--space-5) var(--space-5);
display: grid;
gap: var(--space-5);
}
.tools {
display: flex;
flex-wrap: wrap;
align-items: end;
gap: var(--space-3);
}
.tools label {
display: grid;
gap: var(--space-2);
font-size: var(--text-xs);
color: var(--text-secondary);
}
.search {
flex: 1 1 160px;
}
input,
select {
background: var(--surface-sunken);
color: var(--text-primary);
border: 1px solid var(--border-control);
border-radius: var(--radius-sm);
padding: 10px;
font: inherit;
min-height: 40px;
width: 100%;
}
input:focus-visible,
select:focus-visible {
outline: 2px solid var(--border-focus);
outline-offset: 2px;
}
.new-events {
display: flex;
align-items: center;
gap: var(--space-3);
flex-wrap: wrap;
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
padding: var(--space-3);
color: var(--text-secondary);
font-size: var(--text-sm);
}
.new-events span {
flex: 1;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-3);
flex-wrap: wrap;
line-height: 1.6;
}
.history-error {
display: grid;
justify-items: start;
gap: var(--space-3);
color: var(--state-danger);
}
@media (max-width: 600px) {
summary {
padding: var(--space-4);
}
.history-content {
padding: 0 var(--space-4) var(--space-4);
}
.tools {
align-items: stretch;
}
.tools .search {
flex-basis: 100%;
}
}
</style>