Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
+821
-188
File diff suppressed because it is too large
Load Diff
@@ -29,22 +29,44 @@
|
||||
try {
|
||||
const value = JSON.parse(incident?.details ?? '{}');
|
||||
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
||||
} catch { return {}; }
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
const files = $derived(evidence.files ?? []);
|
||||
const actions = $derived(detail?.actions.slice().reverse() ?? []);
|
||||
const events = $derived(detail?.events.slice().reverse() ?? []);
|
||||
const activeOperation = $derived(actions.some(action => ['queued', 'waiting', 'running', 'awaiting-health'].includes(action.operation_state ?? '')));
|
||||
const canRetry = $derived(!incident?.server_deleted_at && (incident?.state === 'attention' || incident?.state === 'dismissed'));
|
||||
const canDismiss = $derived(!incident?.server_deleted_at && (incident?.state === 'open' || incident?.state === 'attention'));
|
||||
const activeOperation = $derived(
|
||||
actions.some((action) =>
|
||||
['queued', 'waiting', 'running', 'awaiting-health'].includes(
|
||||
action.operation_state ?? ''
|
||||
)
|
||||
)
|
||||
);
|
||||
const canRetry = $derived(
|
||||
!incident?.server_deleted_at &&
|
||||
(incident?.state === 'attention' || incident?.state === 'dismissed')
|
||||
);
|
||||
const canDismiss = $derived(
|
||||
!incident?.server_deleted_at &&
|
||||
(incident?.state === 'open' || incident?.state === 'attention')
|
||||
);
|
||||
const explanation = $derived.by(() => {
|
||||
if (!incident) return '';
|
||||
if (incident.server_deleted_at) return 'This server was deleted. Its recorded finding, evidence, and recovery results are preserved below.';
|
||||
if (incident.state === 'resolved') return incident.kind === 'drift' ? 'The observed file drift has cleared.' : 'The server passed the checks for this incident.';
|
||||
if (incident.state === 'dismissed') return 'This incident was dismissed. Retry to allow automatic recovery again.';
|
||||
if (incident.state === 'attention') return 'Automatic recovery needs attention. Review the failed operation and server diagnostics before retrying.';
|
||||
if (incident.state === 'recovering') return 'Recovery is in progress. Follow the provisioning operations below for eligibility checks, progress, and health verification.';
|
||||
if (incident.kind === 'health' && evidence.containerState?.toLowerCase() === 'missing') return 'Maintenance confirmed that the container is missing. When health recovery is enabled and Maintenance is not paused, three observations over at least two minutes trigger a direct rebuild without a player-count check. Capacity limits and protected work still apply.';
|
||||
if (incident.server_deleted_at)
|
||||
return 'This server was deleted. Its recorded finding, evidence, and recovery results are preserved below.';
|
||||
if (incident.state === 'resolved')
|
||||
return incident.kind === 'drift'
|
||||
? 'The observed file drift has cleared.'
|
||||
: 'The server passed the checks for this incident.';
|
||||
if (incident.state === 'dismissed')
|
||||
return 'This incident was dismissed. Retry to allow automatic recovery again.';
|
||||
if (incident.state === 'attention')
|
||||
return 'Automatic recovery needs attention. Review the failed operation and server diagnostics before retrying.';
|
||||
if (incident.state === 'recovering')
|
||||
return 'Recovery is in progress. Follow the provisioning operations below for eligibility checks, progress, and health verification.';
|
||||
if (incident.kind === 'health' && evidence.containerState?.toLowerCase() === 'missing')
|
||||
return 'Maintenance confirmed that the container is missing. When health recovery is enabled and Maintenance is not paused, three observations over at least two minutes trigger a direct rebuild without a player-count check. Capacity limits and protected work still apply.';
|
||||
return 'Maintenance detected this problem. Automatic recovery requires a confirmed stopped container or a fresh zero-player count from RCON or telemetry before interrupting the server. Offline hosts and unknown container states wait for another check.';
|
||||
});
|
||||
|
||||
@@ -54,10 +76,16 @@
|
||||
const current = ++request;
|
||||
try {
|
||||
const response = await fetch(`/api/maintenance/${encodeURIComponent(id)}`);
|
||||
if (!response.ok) throw new Error(response.status === 404 ? 'Maintenance finding not found.' : 'Could not refresh this incident.');
|
||||
const body = await response.json() as WatchdogDetail;
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
response.status === 404
|
||||
? 'Maintenance finding not found.'
|
||||
: 'Could not refresh this incident.'
|
||||
);
|
||||
const body = (await response.json()) as WatchdogDetail;
|
||||
if (current !== request || id !== $page.params.id) return;
|
||||
detail = body; loadError = '';
|
||||
detail = body;
|
||||
loadError = '';
|
||||
} catch (error) {
|
||||
if (current === request && id === $page.params.id) loadError = (error as Error).message;
|
||||
}
|
||||
@@ -67,97 +95,260 @@
|
||||
if (busy || !incident) return;
|
||||
const id = incident.id;
|
||||
const current = ++request;
|
||||
busy = action; actionError = ''; notice = '';
|
||||
busy = action;
|
||||
actionError = '';
|
||||
notice = '';
|
||||
try {
|
||||
const response = await fetch(`/api/maintenance/${encodeURIComponent(id)}`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action })
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action })
|
||||
});
|
||||
const body = await response.json();
|
||||
if (!response.ok) throw new Error(body.error ?? 'Could not update this incident.');
|
||||
if (current !== request || id !== $page.params.id) return;
|
||||
detail = body; loadError = '';
|
||||
notice = action === 'retry' ? 'Recovery retry requested. Maintenance will recheck the server before taking action.' : 'Incident dismissed.';
|
||||
detail = body;
|
||||
loadError = '';
|
||||
notice =
|
||||
action === 'retry'
|
||||
? 'Recovery retry requested. Maintenance will recheck the server before taking action.'
|
||||
: 'Incident dismissed.';
|
||||
} catch (error) {
|
||||
if (current === request && id === $page.params.id) actionError = (error as Error).message;
|
||||
} finally { if (current === request) busy = null; }
|
||||
if (current === request && id === $page.params.id)
|
||||
actionError = (error as Error).message;
|
||||
} finally {
|
||||
if (current === request) busy = null;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
$page.params.id;
|
||||
request++; detail = null; loadError = ''; actionError = ''; notice = ''; busy = null;
|
||||
eventLimit = 25; fileLimit = 25;
|
||||
untrack(() => { void load(); });
|
||||
void $page.params.id;
|
||||
request++;
|
||||
detail = null;
|
||||
loadError = '';
|
||||
actionError = '';
|
||||
notice = '';
|
||||
busy = null;
|
||||
eventLimit = 25;
|
||||
fileLimit = 25;
|
||||
untrack(() => {
|
||||
void load();
|
||||
});
|
||||
});
|
||||
onMount(() => {
|
||||
const stop = pollVisible(load, 5000, false);
|
||||
return () => { stop(); request++; };
|
||||
return () => {
|
||||
stop();
|
||||
request++;
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head><title>{incident ? `${serverTitle(incident.server)} · Maintenance` : 'Maintenance finding'} | Electric Cherry</title></svelte:head>
|
||||
<svelte:head
|
||||
><title
|
||||
>{incident ? `${serverTitle(incident.server)} · Maintenance` : 'Maintenance finding'} | Electric
|
||||
Cherry</title
|
||||
></svelte:head
|
||||
>
|
||||
|
||||
<div class="page">
|
||||
<SectionHeader title={incident ? `${incident.kind === 'drift' ? 'File drift' : 'Server health'} · ${serverTitle(incident.server)}` : 'Maintenance finding'} subtitle={incident?.id}>
|
||||
{#snippet crumbs()}<Breadcrumbs items={[{ label: 'Maintenance', href: '/maintenance' }, { label: 'Incident' }]} />{/snippet}
|
||||
<SectionHeader
|
||||
title={incident
|
||||
? `${incident.kind === 'drift' ? 'File drift' : 'Server health'} · ${serverTitle(incident.server)}`
|
||||
: 'Maintenance finding'}
|
||||
subtitle={incident?.id}
|
||||
>
|
||||
{#snippet crumbs()}<Breadcrumbs
|
||||
items={[{ label: 'Maintenance', href: '/maintenance' }, { label: 'Incident' }]}
|
||||
/>{/snippet}
|
||||
{#snippet actions()}
|
||||
{#if canRetry}<Button disabled={!!busy || activeOperation} loading={busy === 'retry'} onclick={() => update('retry')}>Retry recovery</Button>{/if}
|
||||
{#if canDismiss}<Button variant="ghost" disabled={!!busy || activeOperation} loading={busy === 'dismiss'} onclick={() => update('dismiss')}>Dismiss incident</Button>{/if}
|
||||
{#if canRetry}<Button
|
||||
disabled={!!busy || activeOperation}
|
||||
loading={busy === 'retry'}
|
||||
onclick={() => update('retry')}>Retry recovery</Button
|
||||
>{/if}
|
||||
{#if canDismiss}<Button
|
||||
variant="ghost"
|
||||
disabled={!!busy || activeOperation}
|
||||
loading={busy === 'dismiss'}
|
||||
onclick={() => update('dismiss')}>Dismiss incident</Button
|
||||
>{/if}
|
||||
{/snippet}
|
||||
</SectionHeader>
|
||||
|
||||
{#if loadError}<div class="ui-error" role="alert">{loadError} {detail ? 'Showing the last successful refresh.' : ''}<Button size="sm" disabled={!!busy} onclick={load}>Retry refresh</Button></div>{/if}
|
||||
{#if loadError}<div class="ui-error" role="alert">
|
||||
{loadError}
|
||||
{detail ? 'Showing the last successful refresh.' : ''}<Button
|
||||
size="sm"
|
||||
disabled={!!busy}
|
||||
onclick={load}>Retry refresh</Button
|
||||
>
|
||||
</div>{/if}
|
||||
{#if actionError}<p class="ui-error" role="alert">{actionError}</p>{/if}
|
||||
{#if notice}<p class="notice" role="status">{notice}</p>{/if}
|
||||
|
||||
{#if incident && detail}
|
||||
<section class="summary" aria-label="Incident status">
|
||||
<div class="row-heading"><WatchdogStatus state={incident.state} />{#if incident.server_deleted_at}<Badge>Deleted</Badge>{:else}<a href={contextHref(`/servers/${encodeURIComponent(incident.server)}`, $page.url)}>View server</a>{/if}</div>
|
||||
<div class="row-heading">
|
||||
<WatchdogStatus state={incident.state} />{#if incident.server_deleted_at}<Badge
|
||||
>Deleted</Badge
|
||||
>{:else}<a
|
||||
href={contextHref(
|
||||
`/servers/${encodeURIComponent(incident.server)}`,
|
||||
$page.url
|
||||
)}>View server</a
|
||||
>{/if}
|
||||
</div>
|
||||
<h2>{incident.summary}</h2>
|
||||
<p>{explanation}</p>
|
||||
{#if activeOperation && (canRetry || canDismiss)}<p>Complete or cancel the active provisioning operation before retrying or dismissing this incident.</p>{/if}
|
||||
{#if !incident.server_deleted_at}{#if incident.kind === 'drift'}<p><strong>Drift repair deletes the existing instance and all of its files, then rebuilds it with the latest promoted setup.</strong> The repair is recorded as a campaign.</p>{:else if incident.state !== 'resolved'}<p>Automatic health recovery restarts an existing container, then rebuilds if needed. A confirmed missing container goes directly to rebuild. Rebuilding deletes any remaining instance files and installs the latest promoted setup.</p>{/if}{/if}
|
||||
{#if activeOperation && (canRetry || canDismiss)}<p>
|
||||
Complete or cancel the active provisioning operation before retrying or
|
||||
dismissing this incident.
|
||||
</p>{/if}
|
||||
{#if !incident.server_deleted_at}{#if incident.kind === 'drift'}<p>
|
||||
<strong
|
||||
>Drift repair deletes the existing instance and all of its files, then
|
||||
rebuilds it with the latest promoted setup.</strong
|
||||
> The repair is recorded as a campaign.
|
||||
</p>{:else if incident.state !== 'resolved'}<p>
|
||||
Automatic health recovery restarts an existing container, then rebuilds if
|
||||
needed. A confirmed missing container goes directly to rebuild. Rebuilding
|
||||
deletes any remaining instance files and installs the latest promoted setup.
|
||||
</p>{/if}{/if}
|
||||
<dl>
|
||||
<div><dt>First detected</dt><dd>{$timeFormat.dateTime(incident.first_seen_at)}</dd></div>
|
||||
<div><dt>Last observed</dt><dd>{$timeFormat.dateTime(incident.last_seen_at)}</dd></div>
|
||||
{#if incident.finished_at}<div><dt>Finished</dt><dd>{$timeFormat.dateTime(incident.finished_at)}</dd></div>{/if}
|
||||
{#if evidence.containerState}<div><dt>Observed container state</dt><dd>{evidence.containerState}</dd></div>{/if}
|
||||
<div>
|
||||
<dt>First detected</dt>
|
||||
<dd>{$timeFormat.dateTime(incident.first_seen_at)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Last observed</dt>
|
||||
<dd>{$timeFormat.dateTime(incident.last_seen_at)}</dd>
|
||||
</div>
|
||||
{#if incident.finished_at}<div>
|
||||
<dt>Finished</dt>
|
||||
<dd>{$timeFormat.dateTime(incident.finished_at)}</dd>
|
||||
</div>{/if}
|
||||
{#if evidence.containerState}<div>
|
||||
<dt>Observed container state</dt>
|
||||
<dd>{evidence.containerState}</dd>
|
||||
</div>{/if}
|
||||
</dl>
|
||||
{#if evidence.reason && evidence.reason !== incident.summary}<p class="observed-reason">Observed reason: {evidence.reason}</p>{/if}
|
||||
{#if !incident.server_deleted_at}<div class="links"><a href={contextHref(`/servers/${encodeURIComponent(incident.server)}?tab=diagnostics`, $page.url)}>Diagnose connection</a><a href={contextHref(`/servers/${encodeURIComponent(incident.server)}?tab=logs`, $page.url)}>Open server logs</a></div>{/if}
|
||||
{#if evidence.reason && evidence.reason !== incident.summary}<p class="observed-reason">
|
||||
Observed reason: {evidence.reason}
|
||||
</p>{/if}
|
||||
{#if !incident.server_deleted_at}<div class="links">
|
||||
<a
|
||||
href={contextHref(
|
||||
`/servers/${encodeURIComponent(incident.server)}?tab=diagnostics`,
|
||||
$page.url
|
||||
)}>Diagnose connection</a
|
||||
><a
|
||||
href={contextHref(
|
||||
`/servers/${encodeURIComponent(incident.server)}?tab=logs`,
|
||||
$page.url
|
||||
)}>Open server logs</a
|
||||
>
|
||||
</div>{/if}
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="recovery-title">
|
||||
<h2 id="recovery-title">Recovery operations</h2>
|
||||
{#if !actions.length}<EmptyState title="No recovery operations yet" description="Provisioning operations and drift campaigns will appear here when recovery starts." />
|
||||
{#if !actions.length}<EmptyState
|
||||
title="No recovery operations yet"
|
||||
description="Provisioning operations and drift campaigns will appear here when recovery starts."
|
||||
/>
|
||||
{:else}<div class="operations">
|
||||
{#each actions as action (action.id)}
|
||||
<article class="operation">
|
||||
<div class="row-heading"><h3>{action.action === 'restart' ? 'Restart server' : 'Rebuild server'}</h3>{#if action.operation_state}<StatusBadge state={action.operation_state} />{/if}</div>
|
||||
{#if action.operation_stage}<p>{action.operation_stage}</p>{/if}
|
||||
{#if action.operation_error}<p class="error">{action.operation_error}</p>{/if}
|
||||
<span class="time"><RelativeTime value={action.created_at} /></span>
|
||||
<div class="links"><a href={contextHref(`/provisioning/${encodeURIComponent(action.operation_id)}`, $page.url)}>View provisioning</a>{#if action.campaign_id}<a href={contextHref(`/campaigns/${encodeURIComponent(action.campaign_id)}`, $page.url)}>View repair campaign</a>{/if}</div>
|
||||
</article>
|
||||
{/each}
|
||||
</div>{/if}
|
||||
{#each actions as action (action.id)}
|
||||
<article class="operation">
|
||||
<div class="row-heading">
|
||||
<h3>
|
||||
{action.action === 'restart'
|
||||
? 'Restart server'
|
||||
: 'Rebuild server'}
|
||||
</h3>
|
||||
{#if action.operation_state}<StatusBadge
|
||||
state={action.operation_state}
|
||||
/>{/if}
|
||||
</div>
|
||||
{#if action.operation_stage}<p>{action.operation_stage}</p>{/if}
|
||||
{#if action.operation_error}<p class="error">
|
||||
{action.operation_error}
|
||||
</p>{/if}
|
||||
<span class="time"><RelativeTime value={action.created_at} /></span>
|
||||
<div class="links">
|
||||
<a
|
||||
href={contextHref(
|
||||
`/provisioning/${encodeURIComponent(action.operation_id)}`,
|
||||
$page.url
|
||||
)}>View provisioning</a
|
||||
>{#if action.campaign_id}<a
|
||||
href={contextHref(
|
||||
`/campaigns/${encodeURIComponent(action.campaign_id)}`,
|
||||
$page.url
|
||||
)}>View repair campaign</a
|
||||
>{/if}
|
||||
</div>
|
||||
</article>
|
||||
{/each}
|
||||
</div>{/if}
|
||||
</section>
|
||||
|
||||
{#if incident.kind === 'drift'}
|
||||
<section aria-labelledby="files-title">
|
||||
<h2 id="files-title">Observed file drift <span class="muted">{files.length}</span></h2>
|
||||
<p class="muted">These differences describe the observed problem. Recovery rebuilds the whole server instance.</p>
|
||||
<h2 id="files-title">
|
||||
Observed file drift <span class="muted">{files.length}</span>
|
||||
</h2>
|
||||
<p class="muted">
|
||||
These differences describe the observed problem. Recovery rebuilds the whole
|
||||
server instance.
|
||||
</p>
|
||||
{#if !files.length}<EmptyState title="No file differences recorded" />
|
||||
{:else}<div class="files">
|
||||
{#each files.slice(0, fileLimit) as file (`${file.kind}:${file.driftKind}:${file.relPath}`)}
|
||||
<article class="file">
|
||||
<div class="file-heading"><Badge variant={file.driftKind === 'untracked' ? 'danger' : 'warning'}>{file.driftKind === 'replace' ? 'Modified' : file.driftKind === 'missing' ? 'Missing' : 'Untracked'}</Badge><code>{file.relPath}</code><span class="muted">{file.kind}</span></div>
|
||||
<details><summary>Observed and expected file</summary><dl>
|
||||
<div><dt>Observed</dt><dd>{file.beforeSize === null ? 'Absent' : `${file.beforeSize} bytes`}{#if file.beforeSha256}<code>{file.beforeSha256}</code>{/if}</dd></div>
|
||||
<div><dt>Expected</dt><dd>{file.afterSize === null ? 'Absent' : `${file.afterSize} bytes`}{#if file.afterSha256}<code>{file.afterSha256}</code>{/if}</dd></div>
|
||||
</dl></details>
|
||||
</article>
|
||||
{/each}
|
||||
</div><ListFooter total={files.length} bind:limit={fileLimit} label="files" />{/if}
|
||||
{#each files.slice(0, fileLimit) as file (`${file.kind}:${file.driftKind}:${file.relPath}`)}
|
||||
<article class="file">
|
||||
<div class="file-heading">
|
||||
<Badge
|
||||
variant={file.driftKind === 'untracked'
|
||||
? 'danger'
|
||||
: 'warning'}
|
||||
>{file.driftKind === 'replace'
|
||||
? 'Modified'
|
||||
: file.driftKind === 'missing'
|
||||
? 'Missing'
|
||||
: 'Untracked'}</Badge
|
||||
><code>{file.relPath}</code><span class="muted"
|
||||
>{file.kind}</span
|
||||
>
|
||||
</div>
|
||||
<details>
|
||||
<summary>Observed and expected file</summary>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>Observed</dt>
|
||||
<dd>
|
||||
{file.beforeSize === null
|
||||
? 'Absent'
|
||||
: `${file.beforeSize} bytes`}{#if file.beforeSha256}<code
|
||||
>{file.beforeSha256}</code
|
||||
>{/if}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Expected</dt>
|
||||
<dd>
|
||||
{file.afterSize === null
|
||||
? 'Absent'
|
||||
: `${file.afterSize} bytes`}{#if file.afterSha256}<code
|
||||
>{file.afterSha256}</code
|
||||
>{/if}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</details>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
<ListFooter total={files.length} bind:limit={fileLimit} label="files" />{/if}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
@@ -166,44 +357,164 @@
|
||||
<p class="muted">Newest first · updates automatically</p>
|
||||
{#if !events.length}<EmptyState title="No activity recorded yet" />
|
||||
{:else}<ol class="timeline">
|
||||
{#each events.slice(0, eventLimit) as event (event.id)}
|
||||
<li><span class="time"><RelativeTime value={event.created_at} /></span><p>{event.message}</p></li>
|
||||
{/each}
|
||||
</ol><ListFooter total={events.length} bind:limit={eventLimit} label="events" />{/if}
|
||||
{#each events.slice(0, eventLimit) as event (event.id)}
|
||||
<li>
|
||||
<span class="time"><RelativeTime value={event.created_at} /></span>
|
||||
<p>{event.message}</p>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
<ListFooter total={events.length} bind:limit={eventLimit} label="events" />{/if}
|
||||
</section>
|
||||
{:else if !loadError}<p class="muted" role="status">Loading incident…</p>{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page, section, .operations, .files { display: grid; gap: 20px; }
|
||||
.page { gap: 28px; }
|
||||
.summary, .operation, .file { padding: 24px; border: 1px solid var(--border-subtle); border-radius: var(--radius-md); background: var(--surface); }
|
||||
.operation, .file { display: grid; gap: 12px; }
|
||||
.summary { gap: 16px; }
|
||||
.row-heading, .links, .file-heading { display: flex; gap: 12px 20px; align-items: center; flex-wrap: wrap; }
|
||||
.row-heading { justify-content: space-between; }
|
||||
h2, h3, p { margin: 0; }
|
||||
h2 { font-size: var(--text-md); }
|
||||
h3 { font-size: var(--text-sm); }
|
||||
p { line-height: 1.6; overflow-wrap: anywhere; }
|
||||
h2 .muted { margin-left: 8px; }
|
||||
.summary p, .operation p { font-size: var(--text-sm); color: var(--text-secondary); }
|
||||
.summary strong { color: var(--text-primary); font-weight: var(--weight-medium); }
|
||||
.links, .row-heading a { font-size: var(--text-sm); }
|
||||
a { color: var(--accent-primary-hover); }
|
||||
dl { display: flex; gap: 16px 32px; flex-wrap: wrap; margin: 0; }
|
||||
dt, .muted, .time, summary, .notice { color: var(--text-secondary); font-size: var(--text-xs); }
|
||||
dd { margin: 6px 0 0; font-size: var(--text-sm); overflow-wrap: anywhere; }
|
||||
code { font-family: var(--font-mono); font-size: var(--text-xs); overflow-wrap: anywhere; }
|
||||
dd code { display: block; margin-top: 8px; color: var(--text-secondary); }
|
||||
details { border-top: 1px solid var(--border-subtle); padding-top: 12px; }
|
||||
summary { cursor: pointer; }
|
||||
details[open] summary { margin-bottom: 16px; }
|
||||
.operation .error { color: var(--state-danger); }
|
||||
.timeline { list-style: none; padding: 0 0 0 18px; margin: 0 0 0 6px; border-left: 2px solid var(--border-strong); }
|
||||
.timeline li { position: relative; padding: 0 0 24px 12px; }
|
||||
.timeline li:last-child { padding-bottom: 0; }
|
||||
.timeline li::before { content: ''; position: absolute; left: -24px; top: 5px; width: 10px; height: 10px; border-radius: 50%; background: var(--accent-primary); }
|
||||
.timeline p { margin-top: 6px; font-size: var(--text-sm); }
|
||||
@media (max-width: 640px) { .summary, .operation, .file { padding: 18px; } }
|
||||
.page,
|
||||
section,
|
||||
.operations,
|
||||
.files {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
.page {
|
||||
gap: 28px;
|
||||
}
|
||||
.summary,
|
||||
.operation,
|
||||
.file {
|
||||
padding: 24px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--surface);
|
||||
}
|
||||
.operation,
|
||||
.file {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
.summary {
|
||||
gap: 16px;
|
||||
}
|
||||
.row-heading,
|
||||
.links,
|
||||
.file-heading {
|
||||
display: flex;
|
||||
gap: 12px 20px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.row-heading {
|
||||
justify-content: space-between;
|
||||
}
|
||||
h2,
|
||||
h3,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
h2 {
|
||||
font-size: var(--text-md);
|
||||
}
|
||||
h3 {
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
p {
|
||||
line-height: 1.6;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
h2 .muted {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.summary p,
|
||||
.operation p {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.summary strong {
|
||||
color: var(--text-primary);
|
||||
font-weight: var(--weight-medium);
|
||||
}
|
||||
.links,
|
||||
.row-heading a {
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
a {
|
||||
color: var(--accent-primary-hover);
|
||||
}
|
||||
dl {
|
||||
display: flex;
|
||||
gap: 16px 32px;
|
||||
flex-wrap: wrap;
|
||||
margin: 0;
|
||||
}
|
||||
dt,
|
||||
.muted,
|
||||
.time,
|
||||
summary,
|
||||
.notice {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
dd {
|
||||
margin: 6px 0 0;
|
||||
font-size: var(--text-sm);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-xs);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
dd code {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
details {
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
padding-top: 12px;
|
||||
}
|
||||
summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
details[open] summary {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.operation .error {
|
||||
color: var(--state-danger);
|
||||
}
|
||||
.timeline {
|
||||
list-style: none;
|
||||
padding: 0 0 0 18px;
|
||||
margin: 0 0 0 6px;
|
||||
border-left: 2px solid var(--border-strong);
|
||||
}
|
||||
.timeline li {
|
||||
position: relative;
|
||||
padding: 0 0 24px 12px;
|
||||
}
|
||||
.timeline li:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.timeline li::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -24px;
|
||||
top: 5px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
.timeline p {
|
||||
margin-top: 6px;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.summary,
|
||||
.operation,
|
||||
.file {
|
||||
padding: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user