Preserve originating views through detail navigation
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { contextHref } from '$lib/navigation-context.js';
|
||||
let { data }: { data: import('./$types').PageData } = $props();
|
||||
const snapshot = $derived($fleet.receivedAt ? $fleet : data.fleetSnapshot);
|
||||
const visibleFleet = $derived($fleet.receivedAt ? $fleetServers : data.fleetSnapshot.servers);
|
||||
@@ -181,7 +182,7 @@
|
||||
{/if}
|
||||
|
||||
{#if spotlight}
|
||||
<a class="spotlight" href="/servers/{encodeURIComponent(spotlight.name)}">
|
||||
<a class="spotlight" href={contextHref(`/servers/${encodeURIComponent(spotlight.name)}`, $page.url)}>
|
||||
<div><span class="eyebrow">Longest running match</span><strong>{serverTitle(spotlight.name)}</strong></div>
|
||||
<span class="round">Round {currentRound(spotlight, $telemetryByServer.get(spotlight.name), $uiNow)}</span>
|
||||
<span>{spotlight.last_players} {spotlight.last_players === 1 ? 'survivor' : 'survivors'} · {elapsedSince(spotlight.game_started_at, $uiNow)} <span aria-hidden="true">→</span></span>
|
||||
@@ -239,7 +240,7 @@
|
||||
<div class="table-scroll"><table><caption class="sr-only">{group.title} servers</caption><thead><tr><th>Lobby</th><th>Status</th><th>Players</th><th>Round / match</th><th>Operation</th><th>Follow</th></tr></thead><tbody>
|
||||
{#each group.servers as server (server.name)}
|
||||
{@const round = currentRound(server, $telemetryByServer.get(server.name), $uiNow)}
|
||||
<tr><td><a class="lobby-link" href="/servers/{encodeURIComponent(server.name)}"><ServerIdentity name={server.name} mapLabel={fleetIdentity(server).map} /></a></td>
|
||||
<tr><td><a class="lobby-link" href={contextHref(`/servers/${encodeURIComponent(server.name)}`, $page.url)}><ServerIdentity name={server.name} mapLabel={fleetIdentity(server).map} /></a></td>
|
||||
<td><span class="health"><StatusPip health={server.health} />{server.health}</span></td><td>{freshServer(server, $uiNow) ? server.last_players ?? '—' : '—'}</td>
|
||||
<td>{round !== null ? `Round ${round}` : '—'}{#if matchDuration(server) >= 0}<small>{elapsedSince(server.game_started_at, $uiNow)}</small>{/if}</td>
|
||||
<td>{@render serverOperations(server, true)}</td><td><div class="table-actions">{@render serverActions(server)}</div></td></tr>
|
||||
@@ -255,7 +256,7 @@
|
||||
{#snippet serverOperations(server: FleetServer, showEmpty = false)}
|
||||
{@const operations = activeOperations.filter(op => op.server === server.name)}
|
||||
{#if operations.length}<div class="server-operations">{#each operations as operation (operation.key)}
|
||||
<a href={operation.href} class:issue={operation.state === 'attention'} title={operation.detail}><span>{operation.label} →</span><small>{operation.detail}</small></a>
|
||||
<a href={contextHref(operation.href, $page.url)} class:issue={operation.state === 'attention'} title={operation.detail}><span>{operation.label} →</span><small>{operation.detail}</small></a>
|
||||
{/each}</div>{:else if showEmpty}<span class="hint">No pending work</span>{/if}
|
||||
{/snippet}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { contextHref } from '$lib/navigation-context.js';
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -52,7 +54,19 @@
|
||||
const waiting=beta.servers.find(server=>server.stage!=='ready');
|
||||
return waiting?`${serverTitle(waiting.server)}: ${statusPresentation(waiting.stage).label.toLowerCase()}.`:'Waiting for test evaluation.';
|
||||
}
|
||||
$effect(() => { search; tab; limit = 25; });
|
||||
$effect(() => {
|
||||
const params = $page.url.searchParams;
|
||||
tab = params.get('view') === 'history' ? 'history' : 'active';
|
||||
search = params.get('q') ?? '';
|
||||
limit = Math.min(10000, Math.max(25, Number(params.get('limit')) || 25));
|
||||
});
|
||||
function saveContext() {
|
||||
const url = new URL($page.url);
|
||||
if (tab === 'history') url.searchParams.set('view', tab); else url.searchParams.delete('view');
|
||||
if (search) url.searchParams.set('q', search); else url.searchParams.delete('q');
|
||||
if (limit > 25) url.searchParams.set('limit', String(limit)); else url.searchParams.delete('limit');
|
||||
void goto(url, { replaceState: true, noScroll: true, keepFocus: true });
|
||||
}
|
||||
async function load() {
|
||||
try { const res = await fetch('/api/betas'); if (!res.ok) throw new Error('Could not refresh betas.'); betas = (await res.json()).betas; now=Date.now(); error = ''; }
|
||||
catch (e) { error = (e as Error).message; } finally { loading = false; }
|
||||
@@ -63,22 +77,23 @@
|
||||
return pollVisible(load, 5000);
|
||||
});
|
||||
</script>
|
||||
<svelte:head><title>Betas · Electric Cherry</title></svelte:head>
|
||||
<div class="page">
|
||||
<SectionHeader title="Betas" subtitle="Test selected versions before promoting them to the fleet.">
|
||||
{#snippet actions()}<Button variant="primary" href="/betas/new">New beta</Button>{/snippet}
|
||||
</SectionHeader>
|
||||
<Tabs {tabs} bind:value={tab} idPrefix="beta-tabs" panelId="beta-list" ariaLabel="Beta views" />
|
||||
<div class="ui-toolbar"><input type="search" bind:value={search} aria-label="Search betas" placeholder="Search betas or files…" /></div>
|
||||
<Tabs {tabs} bind:value={tab} onchange={() => { limit = 25; saveContext(); }} idPrefix="beta-tabs" panelId="beta-list" ariaLabel="Beta views" />
|
||||
<div class="ui-toolbar"><input type="search" bind:value={search} oninput={event => { search = event.currentTarget.value; limit = 25; saveContext(); }} aria-label="Search betas" placeholder="Search betas or files…" /></div>
|
||||
{#if error}<div class="ui-error" role="alert">{error} {betas.length ? 'Showing the last loaded results.' : ''}<Button size="sm" onclick={load}>Retry</Button></div>{/if}
|
||||
<div id="beta-list" role="tabpanel" aria-labelledby={'beta-tabs-' + tab}>
|
||||
{#if loading}<EmptyState title="Loading betas…" />
|
||||
{:else if !filtered.length && !error}<EmptyState title={search ? 'No matching betas' : tab === 'active' ? 'No active betas' : 'No beta history'} description={tab === 'active' && !search ? 'Start a beta when you have a version ready to test.' : undefined} />
|
||||
{:else}<div class="rows">{#each filtered.slice(0,limit) as beta (beta.id)}
|
||||
<a href={'/betas/' + beta.id} class="row">
|
||||
<a href={contextHref('/betas/' + beta.id, $page.url)} class="row">
|
||||
<div class="identity"><strong>{beta.notes || `Beta #${beta.id}`}</strong>{#if beta.notes}<span class="beta-id"> · #{beta.id}</span>{/if}<span class="files">{beta.scripts.map(s => s.name + ' · ' + artifactVersionLabel(s.version)).join(', ')}</span><span class="blocker">{blocker(beta)}</span><span class="timing"><time datetime={beta.opened_at} title={$timeFormat.dateTime(beta.opened_at)}>Opened {age(beta.opened_at)}</time>{#if beta.deadlineAt && active(beta)} · Ends {$timeFormat.dateTime(beta.deadlineAt)}{:else if beta.closed_at} · Finished {$timeFormat.dateTime(beta.closed_at)}{/if}</span></div>
|
||||
<div class="meta"><StatusBadge state={beta.state} />{#if beta.testOutcome && beta.testOutcome !== 'active'}<StatusBadge state={beta.testOutcome} />{/if}<span>{beta.servers.filter(server=>server.stage==='ready').length} / {beta.servers.length} servers passed</span></div>
|
||||
</a>
|
||||
{/each}</div><ListFooter total={filtered.length} bind:limit label="betas" />{/if}
|
||||
{/each}</div><ListFooter total={filtered.length} bind:limit onchange={saveContext} label="betas" />{/if}
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { contextHref } from '$lib/navigation-context.js';
|
||||
import BackLink from '$ui/components/BackLink.svelte';
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
@@ -131,6 +133,7 @@
|
||||
{#snippet crumbs()}<Breadcrumbs items={[{label:'Betas',href:'/betas'},{label:`#${id}`}]} />{/snippet}
|
||||
{#snippet actions()}<StatusBadge state={test && !closed ? test.outcome : beta?.state ?? 'running'} />{/snippet}
|
||||
</SectionHeader>
|
||||
<BackLink fallback="/betas" />
|
||||
<Card padding="lg">
|
||||
<div class="summary-heading"><h2>{closed ? beta.state === 'promoted' ? 'Version promoted' : 'Beta rolled back' : `${passed} / ${servers.length} servers passed`}</h2>{#if test}<span>{coverageSummary(test.plan)}</span>{/if}</div>
|
||||
<p>{nextAction}</p>
|
||||
@@ -142,9 +145,9 @@
|
||||
{#if !closed && test}<p class="muted">Waiting for empty servers uses this window. Pausing or retrying does not extend the deadline.</p>{/if}
|
||||
<div class="actions">
|
||||
{#if !closed}<Button variant="primary" onclick={reviewPromotion} disabled={hasDeletedServers || beta.state !== 'ready-promote' || acting !== null}>Review promotion</Button><Button variant="danger" onclick={rollback} disabled={acting !== null} loading={acting === 'rollback'}>{beta.state === 'ready-promote' ? 'Reject beta' : 'Roll back'}</Button>{/if}
|
||||
{#if closed}<Button href={`/betas/new?reuse=${id}`}>Reuse files and test plan</Button>{/if}
|
||||
{#if promotion}<Button href={`/campaigns/${encodeURIComponent(promotion.id)}`}>View campaign</Button>{/if}
|
||||
{#if rollbackCampaign}<Button href={`/campaigns/${encodeURIComponent(rollbackCampaign.id)}`}>View rollback campaign</Button>{/if}
|
||||
{#if closed}<Button href={contextHref(`/betas/new?reuse=${id}`, $page.url)}>Reuse files and test plan</Button>{/if}
|
||||
{#if promotion}<Button href={contextHref(`/campaigns/${encodeURIComponent(promotion.id)}`, $page.url)}>View campaign</Button>{/if}
|
||||
{#if rollbackCampaign}<Button href={contextHref(`/campaigns/${encodeURIComponent(rollbackCampaign.id)}`, $page.url)}>View rollback campaign</Button>{/if}
|
||||
</div>
|
||||
{#if promotion}<p class="muted">{promotion.pending} servers awaiting activation · {promotion.failed} need attention. Promoting a version does not mean it is already running everywhere.</p>{/if}
|
||||
{#if flash}<p role="alert" class="ui-error">{flash}</p>{/if}
|
||||
@@ -152,13 +155,13 @@
|
||||
</Card>
|
||||
{#if !closed && waiting.length}
|
||||
<section aria-label="Outstanding servers"><h2>Outstanding conditions</h2><div class="server-list">
|
||||
{#each waiting as server}<div class="server-row"><HistoricalServerIdentity name={server.server} deletedAt={server.server_deleted_at} href={`/servers/${encodeURIComponent(server.server)}`} /><StatusBadge state={server.stage} /><p>{server.error ?? (server.server_deleted_at ? 'This server was deleted. Recorded progress is retained.' : outstanding(server.server))}</p>{#if server.error && !server.server_deleted_at}<a href={`/servers/${encodeURIComponent(server.server)}?tab=diagnostics`}>Diagnose</a>{:else if test}<a href="#test-results">View checks</a>{/if}</div>{/each}
|
||||
{#each waiting as server}<div class="server-row"><HistoricalServerIdentity name={server.server} deletedAt={server.server_deleted_at} href={contextHref(`/servers/${encodeURIComponent(server.server)}`, $page.url)} /><StatusBadge state={server.stage} /><p>{server.error ?? (server.server_deleted_at ? 'This server was deleted. Recorded progress is retained.' : outstanding(server.server))}</p>{#if server.error && !server.server_deleted_at}<a href={contextHref(`/servers/${encodeURIComponent(server.server)}?tab=diagnostics`, $page.url)}>Diagnose</a>{:else if test}<a href="#test-results">View checks</a>{/if}</div>{/each}
|
||||
</div></section>
|
||||
{/if}
|
||||
{#if test}<div id="test-results"><Card padding="lg"><BetaTestProgress {test} betaId={id} {closed} deletedParticipants={hasDeletedServers} deadline={deadline?.toISOString()} retryBlocked={servers.some(server=>server.error?.startsWith('deploy:')) ? 'Deployment was incomplete. Roll back before opening a fresh beta.' : undefined} onUpdate={load} /></Card></div>{/if}
|
||||
<details class="ui-panel"><summary>Files and versions ({scripts.length})</summary><div class="file-list">{#each scripts as file}<div><a href={`/${file.kind === 'baseline' ? 'configs' : file.kind + 's'}/${file.id}`}>{file.name}</a><strong>{artifactVersionLabel(file.version)}</strong><code>{file.target_rel_path}</code><span>{file.version?.sha256.slice(0,12)}</span></div>{/each}</div></details>
|
||||
<details class="ui-panel"><summary>Files and versions ({scripts.length})</summary><div class="file-list">{#each scripts as file}<div><a href={contextHref(`/${file.kind === 'baseline' ? 'configs' : file.kind + 's'}/${file.id}`, $page.url)}>{file.name}</a><strong>{artifactVersionLabel(file.version)}</strong><code>{file.target_rel_path}</code><span>{file.version?.sha256.slice(0,12)}</span></div>{/each}</div></details>
|
||||
<details class="ui-panel"><summary>Deployment progress ({servers.length} servers)</summary><div class="server-list">
|
||||
{#each ranked as server}<div class="server-row"><HistoricalServerIdentity name={server.server} deletedAt={server.server_deleted_at} href={`/servers/${encodeURIComponent(server.server)}`}/><StatusBadge state={server.stage}/><p>Files {server.deployed_at ? 'installed' : 'pending'} · Restart {server.restarted_at ? 'submitted' : 'pending'}{#if beta.wait_for_health} · Health {server.healthy_at ? 'verified' : 'pending'}{/if}{#if beta.wait_for_match} · Match {server.match_seen_at ? 'observed' : 'pending'}{/if}</p>{#if server.provisioning_id}<a href={`/provisioning/${encodeURIComponent(server.provisioning_id)}`}>View provisioning</a>{/if}{#if server.error}<p class="ui-error">{server.error}</p>{/if}{#if server.stage === 'failed' && !server.server_deleted_at && !test && !closed}<Button size="sm" onclick={() => retry(server.server)} disabled={acting !== null} loading={acting === `retry:${server.server}`}>Retry server</Button>{/if}</div>{/each}
|
||||
{#each ranked as server}<div class="server-row"><HistoricalServerIdentity name={server.server} deletedAt={server.server_deleted_at} href={contextHref(`/servers/${encodeURIComponent(server.server)}`, $page.url)}/><StatusBadge state={server.stage}/><p>Files {server.deployed_at ? 'installed' : 'pending'} · Restart {server.restarted_at ? 'submitted' : 'pending'}{#if beta.wait_for_health} · Health {server.healthy_at ? 'verified' : 'pending'}{/if}{#if beta.wait_for_match} · Match {server.match_seen_at ? 'observed' : 'pending'}{/if}</p>{#if server.provisioning_id}<a href={contextHref(`/provisioning/${encodeURIComponent(server.provisioning_id)}`, $page.url)}>View provisioning</a>{/if}{#if server.error}<p class="ui-error">{server.error}</p>{/if}{#if server.stage === 'failed' && !server.server_deleted_at && !test && !closed}<Button size="sm" onclick={() => retry(server.server)} disabled={acting !== null} loading={acting === `retry:${server.server}`}>Retry server</Button>{/if}</div>{/each}
|
||||
</div></details>
|
||||
<OperationHistory {events} active={!closed} showOverview={false}/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { contextHref } from '$lib/navigation-context.js';
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
@@ -217,7 +218,7 @@
|
||||
{#if flash && flashId === d.id}<div class="result" class:result-error={flashError} role={flashError ? 'alert' : 'status'}>{flash}</div>{/if}
|
||||
<div class="row-head">
|
||||
<div class="row-head-l">
|
||||
<a class="action" href="/campaigns/{d.id}">{d.action} campaign <span class="sr-only">{d.id}</span></a>
|
||||
<a class="action" href={contextHref(`/campaigns/${d.id}`, $page.url)}>{d.action} campaign <span class="sr-only">{d.id}</span></a>
|
||||
{#if d.action === 'promote' || d.action === 'rollback'}
|
||||
<span class="target">{scriptSummary(d)}</span>
|
||||
{/if}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { contextHref } from '$lib/navigation-context.js';
|
||||
import BackLink from '$ui/components/BackLink.svelte';
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
import { onMount } from 'svelte';
|
||||
import { pollVisible } from '$ui/poll.js';
|
||||
@@ -136,6 +138,7 @@
|
||||
]} />
|
||||
{/snippet}
|
||||
</SectionHeader>
|
||||
<BackLink fallback="/campaigns" />
|
||||
|
||||
<section class="outcome" aria-label="Campaign outcome">
|
||||
<h2>{completed} / {detail.servers.length} servers activated{unresolved ? ` · ${unresolved} unresolved` : ''}{withdrawn ? ` · ${withdrawn} withdrawn` : ''}{deletedIncomplete ? ` · ${deletedIncomplete} deleted before activation` : ''}</h2>
|
||||
@@ -145,7 +148,7 @@
|
||||
{:else}<p>Every recorded server completed activation.</p>{/if}
|
||||
</section>
|
||||
|
||||
{#if detail.watchdogIncidentId}<p class="watchdog-source">Started by Maintenance · <a href={`/maintenance/${encodeURIComponent(detail.watchdogIncidentId)}`}>View incident and recovery history</a></p>{/if}
|
||||
{#if detail.watchdogIncidentId}<p class="watchdog-source">Started by Maintenance · <a href={contextHref(`/maintenance/${encodeURIComponent(detail.watchdogIncidentId)}`, $page.url)}>View incident and recovery history</a></p>{/if}
|
||||
|
||||
{#if detail.servers.some(server => !server.server_deleted_at && ['held', 'manual'].includes(server.restart_state))}
|
||||
<div><Button onclick={resume} loading={resuming} disabled={resuming}>Resume when empty</Button><p>Apply held changes and restart after the safety checks pass.</p></div>
|
||||
@@ -170,7 +173,7 @@
|
||||
{#each rankedServers as s (s.server)}
|
||||
<div class="srv-row">
|
||||
<div class="srv-l">
|
||||
<span class="srv-name"><HistoricalServerIdentity name={s.server} deletedAt={s.server_deleted_at} href={`/servers/${encodeURIComponent(s.server)}`} /></span>
|
||||
<span class="srv-name"><HistoricalServerIdentity name={s.server} deletedAt={s.server_deleted_at} href={contextHref(`/servers/${encodeURIComponent(s.server)}`, $page.url)} /></span>
|
||||
{#if s.error}
|
||||
<span class="err">{s.error}</span>
|
||||
{/if}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { contextHref } from '$lib/navigation-context.js';
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { beforeNavigate, afterNavigate, goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
@@ -95,7 +96,7 @@
|
||||
<div class="section-heading"><div><h2 id="host-capacity-title">Host capacity</h2><p>Find the resource that limits the next launch.</p></div><span>{summary.eligibleHosts} / {summary.totalHosts} eligible</span></div>
|
||||
{#if !report.current.hosts.length}<div class="empty-hosts"><h3>{report.query.hostId ? 'This host is unavailable' : 'No hosts yet'}</h3><p>{report.query.hostId ? 'Choose another host to view its capacity.' : 'Connect a host to start seeing capacity and history.'}</p><Button href="/hosts">Open hosts</Button></div>
|
||||
{:else}<div class="host-grid">{#each report.current.hosts as host (host.id)}<article class="host-card">
|
||||
<div class="host-title"><h3><a href={`/hosts/${host.id}`}>{host.name} <span aria-hidden="true">↗</span></a></h3><span class="status" class:eligible={host.eligible}>{!host.connected ? 'Offline' : hostReadinessLabel(host.readiness)}</span></div>
|
||||
<div class="host-title"><h3><a href={contextHref(`/hosts/${host.id}`, page.url)}>{host.name} <span aria-hidden="true">↗</span></a></h3><span class="status" class:eligible={host.eligible}>{!host.connected ? 'Offline' : hostReadinessLabel(host.readiness)}</span></div>
|
||||
<div class="host-estimate"><strong>{count(host.estimatedServers)}</strong><span>additional servers{#if host.eligible && host.limitingResource}<small>Limited by {host.limitingResource.toLowerCase()}</small>{/if}</span></div>
|
||||
<div class="game-tags">{#if host.games.length}{#each host.games as game}<span>{game === 't5' ? 'Black Ops I' : 'Black Ops II'}</span>{/each}{:else}<span>No games ready</span>{/if}</div>
|
||||
<dl class="host-resources"><div><dt>CPU</dt><dd>{count(host.cpuCoresAvailable)} <small>cores</small></dd><span>{count(host.limits.cpu)} servers fit</span></div><div><dt>Memory</dt><dd>{memory(host.memoryBytesAvailable)} <small>GiB</small></dd><span>{count(host.limits.memory)} servers fit</span></div><div><dt>Ports</dt><dd>{host.portsAvailable} <small>/ {host.totalPorts}</small></dd><span>{host.limits.ports} servers fit</span></div></dl>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { contextHref } from '$lib/navigation-context.js';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -134,7 +135,7 @@
|
||||
{#if launchesLoading}<p>Loading collection launches…</p>
|
||||
{:else if visibleLaunches.length}
|
||||
<div class="launches">{#each visibleLaunches.slice(0,launchLimit) as collection (collection.id)}
|
||||
<a class="collection-row" href="/collections/launches/{collection.id}">
|
||||
<a class="collection-row" href={contextHref(`/collections/launches/${collection.id}`, $page.url)}>
|
||||
<div class="row-head"><strong>{collection.name}</strong><Badge variant={collection.active ? 'info' : collection.failed ? 'danger' : 'online'}>{collection.active ? collection.provisioning + collection.verifying ? collection.mode === 'exact' ? 'Applying collection' : 'Launching' : 'Queued' : collection.failed ? 'Needs attention' : 'Completed'}</Badge></div>
|
||||
<div class="collection-counts"><span><strong>{collection.done} / {collection.total}</strong> {collection.mode === 'exact' ? 'completed' : 'ready'}</span>{#if collection.provisioning + collection.verifying}<span>{collection.provisioning + collection.verifying} {collection.mode === 'exact' ? 'in progress' : 'launching'}</span>{/if}{#if collection.queued}<span>{collection.queued} queued</span>{/if}{#if collection.failed}<span class="error">{collection.failed} need attention</span>{/if}</div>
|
||||
<div class="collection-meter" aria-hidden="true"><span class="verified" style:width={`${collection.done / (collection.total || 1) * 100}%`}></span><span class="unsuccessful" style:width={`${collection.failed / (collection.total || 1) * 100}%`}></span></div>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { contextHref } from '$lib/navigation-context.js';
|
||||
import BackLink from '$ui/components/BackLink.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { pollVisible } from '$ui/poll.js';
|
||||
import ServerIdentity from '$ui/components/ServerIdentity.svelte';
|
||||
@@ -54,6 +56,7 @@
|
||||
<SectionHeader title={collection?.name ?? 'Collection launch'}>
|
||||
{#snippet crumbs()}<Breadcrumbs items={[{ label: 'Collections', href: '/collections' }, { label: 'Collection launch' }]} />{/snippet}
|
||||
</SectionHeader>
|
||||
<BackLink fallback="/collections?view=active" />
|
||||
{#if error}<p class="error" role="alert">{error} <button onclick={load}>Retry</button></p>{/if}
|
||||
{#if collection && detail}
|
||||
<section class="summary" aria-label="Collection progress">
|
||||
@@ -78,7 +81,7 @@
|
||||
<section class="current-launches" aria-label="In progress">
|
||||
<span class="eyebrow">In progress</span>
|
||||
{#each currentServers as server (server.id)}
|
||||
<a class="current-launch" href="/provisioning/{server.id}">
|
||||
<a class="current-launch" href={contextHref(`/provisioning/${server.id}`, $page.url)}>
|
||||
<span class="activity" aria-hidden="true"></span>
|
||||
<div><strong>{server.action === 'destroy' ? 'Removing: ' : ''}<HistoricalServerIdentity name={server.server} deletedAt={server.server_deleted_at} /></strong><p>{server.state === 'awaiting-health' ? 'Checking the container, connection, and expected map.' : server.stage}</p></div>
|
||||
<span class="operation-link">Follow progress <span aria-hidden="true">→</span></span>
|
||||
@@ -92,7 +95,7 @@
|
||||
<p>{queueHint(firstQueued)} Provisioning starts each server when its host and capacity are ready. Progress updates automatically.</p>
|
||||
{#if otherLaunches.length}
|
||||
<p>Other servers currently launching:</p>
|
||||
<div class="other-launches">{#each otherLaunches as active (active.id)}<a href="/provisioning/{active.id}"><ServerIdentity name={active.server} /> <span aria-hidden="true">→</span></a>{/each}</div>
|
||||
<div class="other-launches">{#each otherLaunches as active (active.id)}<a href={contextHref(`/provisioning/${active.id}`, $page.url)}><ServerIdentity name={active.server} /> <span aria-hidden="true">→</span></a>{/each}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</aside>
|
||||
@@ -108,7 +111,7 @@
|
||||
<ol class="servers">
|
||||
{#each detail.servers as server, index (server.id)}
|
||||
<li class:launching={launching(server)} class:attention={needsAttention(server)}>
|
||||
<a class="server" href="/provisioning/{server.id}">
|
||||
<a class="server" href={contextHref(`/provisioning/${server.id}`, $page.url)}>
|
||||
<span class="order" class:ready={server.state === 'done'} aria-hidden="true">{server.state === 'done' ? '✓' : String(index + 1).padStart(2, '0')}</span>
|
||||
<div class="server-content">
|
||||
<div class="server-heading"><strong><HistoricalServerIdentity name={server.server} deletedAt={server.server_deleted_at} /></strong><Badge variant={needsAttention(server) ? 'danger' : server.state === 'done' ? 'online' : launching(server) ? 'info' : 'neutral'}>{stateLabel(server)}</Badge></div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { contextHref } from '$lib/navigation-context.js';
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { pollVisible } from '$ui/poll.js';
|
||||
let { data }: { data: import('./$types').PageData } = $props();
|
||||
@@ -55,7 +56,7 @@
|
||||
{#if enrollment}<div class="enrollment" role="status"><h3>Install the agent</h3><p>This single-use command expires {new Date(enrollment.expiresAt).toLocaleString()}. Run it with administrator access on your game host.</p><pre>{enrollment.command}</pre><a href={`/hosts/${enrollment.hostId}`}>Open host setup →</a></div>{/if}
|
||||
</section>{:else}<p>Administrators enroll hosts and manage their settings and credentials.</p>{/if}
|
||||
{#if loading}<p role="status">Loading hosts…</p>{:else if !hosts.length}<p>No hosts connected yet.{canManage ? ' Add a host to begin.' : ''}</p>{:else}
|
||||
<div class="host-grid">{#each hosts as host (host.id)}<article class="panel host"><div class="row"><h2><a href={`/hosts/${host.id}`}>{host.name}</a></h2><span class:ready={host.readiness === 'ready'} class="state">{hostReadinessLabel(host.readiness)}</span></div><p>{host.hostname || 'Awaiting enrollment'} · {host.connectionState}</p><p>{host.inventory.os ?? 'OS pending'} · {host.inventory.architecture ?? 'Architecture pending'}</p><p>Game ports {host.portMin}–{host.portMax}</p>{#if host.enrolled}<p>Agent {host.inventory.agentVersion ?? 'unknown'} · Auto updates: {hostAgentUpdateLabel(host.inventory)}</p>{/if}{#if host.decommission}<p class="reason">{hostDeletionMessage(host)}</p>{#if host.decommission.error}<p class="ui-error">{host.decommission.error}</p>{/if}{:else}{#if host.preparation}<p>Automatic setup: {host.preparation.state === 'preparing' ? 'In progress' : host.preparation.state === 'blocked' ? 'Needs attention' : host.preparation.state === 'ready' ? 'Ready' : 'Waiting'}</p>{#if host.preparation.state === 'blocked'}<p class="ui-error">{host.preparation.items.find(item => item.state === 'blocked')?.detail}</p>{/if}{/if}{#if host.readinessReasons.length}<p class="reason">{host.readinessReasons[0]}</p>{/if}{/if}<div class="host-actions"><Button size="sm" href={`/hosts/${host.id}`}>{host.decommission ? 'View cleanup' : 'Manage host'}</Button>{#if canManage && (!host.decommission || host.decommission.retryable)}<Button size="sm" variant="danger" onclick={() => { deletingHost = host; deleteOpen = true; }}>{host.decommission ? 'Retry cleanup' : 'Delete host'}</Button>{/if}</div></article>{/each}</div>
|
||||
<div class="host-grid">{#each hosts as host (host.id)}<article class="panel host"><div class="row"><h2><a href={contextHref(`/hosts/${host.id}`, $page.url)}>{host.name}</a></h2><span class:ready={host.readiness === 'ready'} class="state">{hostReadinessLabel(host.readiness)}</span></div><p>{host.hostname || 'Awaiting enrollment'} · {host.connectionState}</p><p>{host.inventory.os ?? 'OS pending'} · {host.inventory.architecture ?? 'Architecture pending'}</p><p>Game ports {host.portMin}–{host.portMax}</p>{#if host.enrolled}<p>Agent {host.inventory.agentVersion ?? 'unknown'} · Auto updates: {hostAgentUpdateLabel(host.inventory)}</p>{/if}{#if host.decommission}<p class="reason">{hostDeletionMessage(host)}</p>{#if host.decommission.error}<p class="ui-error">{host.decommission.error}</p>{/if}{:else}{#if host.preparation}<p>Automatic setup: {host.preparation.state === 'preparing' ? 'In progress' : host.preparation.state === 'blocked' ? 'Needs attention' : host.preparation.state === 'ready' ? 'Ready' : 'Waiting'}</p>{#if host.preparation.state === 'blocked'}<p class="ui-error">{host.preparation.items.find(item => item.state === 'blocked')?.detail}</p>{/if}{/if}{#if host.readinessReasons.length}<p class="reason">{host.readinessReasons[0]}</p>{/if}{/if}<div class="host-actions"><Button size="sm" href={contextHref(`/hosts/${host.id}`, $page.url)}>{host.decommission ? 'View cleanup' : 'Manage host'}</Button>{#if canManage && (!host.decommission || host.decommission.retryable)}<Button size="sm" variant="danger" onclick={() => { deletingHost = host; deleteOpen = true; }}>{host.decommission ? 'Retry cleanup' : 'Delete host'}</Button>{/if}</div></article>{/each}</div>
|
||||
{/if}
|
||||
</section>
|
||||
<DeleteHostDialog bind:open={deleteOpen} host={deletingHost} ondeleted={deleted} />
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Breadcrumbs from '$ui/components/primitives/Breadcrumbs.svelte';
|
||||
import BackLink from '$ui/components/BackLink.svelte';
|
||||
import { contextHref } from '$lib/navigation-context.js';
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
import { goto, invalidateAll } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
@@ -61,9 +64,10 @@
|
||||
|
||||
<section class="profile">
|
||||
<SectionHeader title={profile.name} eyebrow="Player profile" subtitle="Black Ops I & Black Ops II · Lifetime Zombies stats">
|
||||
{#snippet crumbs()}<a href={data.returnTo}>← Leaderboard</a>{/snippet}
|
||||
{#snippet crumbs()}<Breadcrumbs items={[{ label: 'Leaderboard', href: '/leaderboard' }, { label: profile.name }]} />{/snippet}
|
||||
{#snippet actions()}<Button size="sm" loading={refreshing} onclick={refresh}>Refresh stats</Button>{/snippet}
|
||||
</SectionHeader>
|
||||
<BackLink fallback={data.returnTo} />
|
||||
<div class="identity"><CopyValue value={profile.guid} label="Player GUID" /><span>First seen {date(profile.firstSeen)}</span><span>Last seen {date(profile.lastSeen)}</span></div>
|
||||
{#if refreshError}<p role="alert">{refreshError}</p>{/if}
|
||||
|
||||
@@ -125,7 +129,7 @@
|
||||
<table>
|
||||
<thead><tr><th scope="col">Map / server</th><th scope="col">First observed</th><th scope="col">Match status</th>{#each historyColumns as key}<th scope="col">{playerStatMetrics.find(metric => metric.key === key)!.label}</th>{/each}</tr></thead>
|
||||
<tbody>{#each profile.matches as match (match.id)}
|
||||
<tr><th scope="row">{#if match.status === 'completed'}<a href={`/matches/${match.id}`} aria-label={`View ${match.mapName} match from ${date(match.startedAt)}`}>{match.mapName}</a>{:else}{match.mapName}{/if}<small>{gameName(match.game)}</small><small>{match.server} {#if match.server_deleted_at}<Badge>Deleted</Badge>{/if}</small></th><td>{date(match.startedAt)}</td>
|
||||
<tr><th scope="row">{#if match.status === 'completed'}<a href={contextHref(`/matches/${match.id}`, $page.url)} aria-label={`View ${match.mapName} match from ${date(match.startedAt)}`}>{match.mapName}</a>{:else}{match.mapName}{/if}<small>{gameName(match.game)}</small><small>{match.server} {#if match.server_deleted_at}<Badge>Deleted</Badge>{/if}</small></th><td>{date(match.startedAt)}</td>
|
||||
<td class="status">{match.status}<small>{match.completed ? 'Completion credited' : 'No completion credit'}</small></td>
|
||||
{#each historyColumns as key}<td class:best={key === 'highestRound'}>{number.format(match[key])}</td>{/each}
|
||||
</tr>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { contextHref } from '$lib/navigation-context.js';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -190,7 +191,7 @@
|
||||
<div class="row-heading"><h3>{item.title} · <HistoricalServerIdentity name={item.server} deletedAt={item.server_deleted_at} /></h3><Badge variant={item.state === 'attention' ? 'danger' : item.state === 'active' ? 'info' : 'neutral'}>{item.label}</Badge></div>
|
||||
{#if item.detail}<p>{item.detail}</p>{/if}
|
||||
<p class="muted">{$timeFormat.dateTime(item.at)}{item.job?.target_revision ? ` · Expected r${item.job.target_revision} or newer` : ''}</p>
|
||||
<div class="links">{#if item.href}<a href={item.href}>{item.job ? 'View restart operation' : 'View evidence and recovery'}</a>{/if}
|
||||
<div class="links">{#if item.href}<a href={contextHref(item.href, $page.url)}>{item.job ? 'View restart operation' : 'View evidence and recovery'}</a>{/if}
|
||||
{#if item.job?.state === 'queued' && !item.server_deleted_at}<Button size="sm" disabled={busy} onclick={() => act({ action: 'cancel', id: item.job!.id })}>Cancel restart</Button>{/if}
|
||||
{#if item.job?.state === 'failed' && !item.server_deleted_at && item.job.operation_state === 'done'}<Button size="sm" disabled={busy || data.policies.paused} onclick={() => act({ action: 'retry', id: item.job!.id })}>Retry after inspection</Button>{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { contextHref } from '$lib/navigation-context.js';
|
||||
import BackLink from '$ui/components/BackLink.svelte';
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import type { WatchdogDetail, WatchdogEvidence } from '$lib/watchdog.js';
|
||||
@@ -101,6 +103,7 @@
|
||||
{#if canDismiss}<Button variant="ghost" disabled={!!busy || activeOperation} loading={busy === 'dismiss'} onclick={() => update('dismiss')}>Dismiss incident</Button>{/if}
|
||||
{/snippet}
|
||||
</SectionHeader>
|
||||
<BackLink fallback="/maintenance" />
|
||||
|
||||
{#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}
|
||||
@@ -108,7 +111,7 @@
|
||||
|
||||
{#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={`/servers/${encodeURIComponent(incident.server)}`}>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}
|
||||
@@ -120,7 +123,7 @@
|
||||
{#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={`/servers/${encodeURIComponent(incident.server)}?tab=diagnostics`}>Diagnose connection</a><a href={`/servers/${encodeURIComponent(incident.server)}?tab=logs`}>Open server logs</a></div>{/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">
|
||||
@@ -133,7 +136,7 @@
|
||||
{#if action.operation_stage}<p>{action.operation_stage}</p>{/if}
|
||||
{#if action.operation_error}<p class="error">{action.operation_error}</p>{/if}
|
||||
<time datetime={action.created_at}>{$timeFormat.dateTime(action.created_at)}</time>
|
||||
<div class="links"><a href={`/provisioning/${encodeURIComponent(action.operation_id)}`}>View provisioning</a>{#if action.campaign_id}<a href={`/campaigns/${encodeURIComponent(action.campaign_id)}`}>View repair campaign</a>{/if}</div>
|
||||
<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}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { contextHref } from '$lib/navigation-context.js';
|
||||
import Breadcrumbs from '$ui/components/primitives/Breadcrumbs.svelte';
|
||||
import BackLink from '$ui/components/BackLink.svelte';
|
||||
import type { MatchDetail } from '$lib/match-history.js';
|
||||
import { playerProfileHref } from '$lib/leaderboard.js';
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
@@ -29,9 +33,10 @@
|
||||
|
||||
<div class="match-page">
|
||||
<SectionHeader title={match.map} eyebrow={`Match #${match.id}`} subtitle={`${gameName} · Recorded match results`}>
|
||||
{#snippet crumbs()}<a href="/matches">← Match History</a>{/snippet}
|
||||
{#snippet crumbs()}<Breadcrumbs items={[{ label: 'Match History', href: '/matches' }, { label: `Match #${match.id}` }]} />{/snippet}
|
||||
{#snippet actions()}<Badge variant="online" size="md">Completed</Badge>{/snippet}
|
||||
</SectionHeader>
|
||||
<BackLink fallback="/matches" />
|
||||
|
||||
<section class="summary" aria-label="Match summary">
|
||||
<Card><Stat label="Final round" value={match.finalRound} tone="accent" size="lg" tween={false} /></Card>
|
||||
@@ -67,7 +72,7 @@
|
||||
{#each match.players as player (player.guid)}
|
||||
<tr>
|
||||
<th scope="row">
|
||||
{#if player.canViewProfile}<a href={playerProfileHref(player.guid)}>{player.name}</a>{:else}{player.name}{/if}
|
||||
{#if player.canViewProfile}<a href={contextHref(playerProfileHref(player.guid), $page.url)}>{player.name}</a>{:else}{player.name}{/if}
|
||||
<small class="guid">{player.guid}</small>
|
||||
</th>
|
||||
<td class="participation"><Badge variant={player.presentAtEnd ? 'online' : 'neutral'}>{player.presentAtEnd ? 'At match end' : 'Not at match end'}</Badge>{#if !player.hasStats}<small>No stats recorded</small>{/if}</td>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { contextHref } from '$lib/navigation-context.js';
|
||||
import { onMount } from 'svelte';
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
import Dialog from '$ui/components/primitives/Dialog.svelte';
|
||||
@@ -199,7 +201,7 @@
|
||||
{:else}
|
||||
<div class="rows">
|
||||
{#each visible.slice(0,limit) as a (a.id)}
|
||||
<a class="row" href="/plugins/{a.id}">
|
||||
<a class="row" href={contextHref(`/plugins/${a.id}`, $page.url)}>
|
||||
<div class="row-top">
|
||||
<span class="name">{a.name}</span>
|
||||
<Badge
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { safeReturnHref, returnLabel } from '$lib/navigation-context.js';
|
||||
import { onDestroy, onMount, untrack } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import type { PageData } from './$types';
|
||||
@@ -38,7 +39,7 @@
|
||||
const outcome = $derived(detail ? provisioningOutcome(detail) : null);
|
||||
const returnTo = $derived.by(() => {
|
||||
const requested = $page.url.searchParams.get('returnTo');
|
||||
if (requested) return provisioningReturn(requested);
|
||||
if (requested) { const href = safeReturnHref(requested, '/provisioning'); return { href, label: returnLabel(href) }; }
|
||||
if (detail?.collection) return { href: `/collections/launches/${encodeURIComponent(detail.collection.id)}`, label: `Back to ${detail.collection.name}` };
|
||||
if (detail?.sources.length === 1 && detail.sources[0].href) return { href: detail.sources[0].href, label: `Back to ${detail.sources[0].label}` };
|
||||
return provisioningReturn(null);
|
||||
@@ -86,7 +87,7 @@
|
||||
|
||||
<div class="page">
|
||||
<SectionHeader title={deployment ? `${actionLabels[deployment.action]} · ${serverTitle(deployment.server)}` : 'Provisioning operation'} subtitle={deployment?.server}>
|
||||
{#snippet crumbs()}<Breadcrumbs items={[{ label: 'Provisioning', href: returnTo.href.startsWith('/provisioning') ? returnTo.href : '/provisioning' }, { label: 'Operation' }]} />{/snippet}
|
||||
{#snippet crumbs()}<Breadcrumbs items={[{ label: 'Provisioning', href: '/provisioning' }, { label: 'Operation' }]} />{/snippet}
|
||||
{#snippet actions()}{#if serverHref}<Button href={serverHref}>View server</Button>{/if}{/snippet}
|
||||
</SectionHeader>
|
||||
<div class="identity-row">
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { contextHref } from '$lib/navigation-context.js';
|
||||
import { onMount } from 'svelte';
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
import ListFooter from '$ui/components/primitives/ListFooter.svelte';
|
||||
@@ -129,7 +131,7 @@
|
||||
<article class="script-row" class:selected={selected.includes(s.id)}>
|
||||
<input type="checkbox" aria-label={`Select ${s.name} for a beta`} checked={selected.includes(s.id)} disabled={!canTest(s)} onchange={() => toggle(s.id)} />
|
||||
<div class="script-info">
|
||||
<div class="name-row"><a class="name" href="/scripts/{s.id}">{s.name}</a><StatusBadge state={state} /></div>
|
||||
<div class="name-row"><a class="name" href={contextHref(`/scripts/${s.id}`, $page.url)}>{s.name}</a><StatusBadge state={state} /></div>
|
||||
<div class="path">{s.target_rel_path}</div>
|
||||
<div class="meta"><span title={s.scope_value ?? 'Choose targets explicitly when opening a beta'}>{scopeLabel(s)}</span><span>{s.scope_type ? `${s.servers.length} scope matches` : 'Explicit beta targets'}</span><span>{s.versionCount} version{s.versionCount === 1 ? '' : 's'}</span></div>
|
||||
</div>
|
||||
@@ -138,8 +140,8 @@
|
||||
{#if s.latestVersion && s.latestVersion.id !== s.current_version_id}<strong>Latest v{s.latestVersion.version} · {s.latestVersion.state === 'untested' ? 'awaiting test' : s.latestVersion.state.replaceAll('-', ' ')}</strong>{/if}
|
||||
</div>
|
||||
<div class="row-action">
|
||||
{#if s.runningBetaId !== null}<Button size="sm" href="/betas/{s.runningBetaId}">View beta →</Button>
|
||||
{:else if state === 'failed'}<Button size="sm" href="/scripts/{s.id}">Review version</Button>
|
||||
{#if s.runningBetaId !== null}<Button size="sm" href={contextHref(`/betas/${s.runningBetaId}`, $page.url)}>View beta →</Button>
|
||||
{:else if state === 'failed'}<Button size="sm" href={contextHref(`/scripts/${s.id}`, $page.url)}>Review version</Button>
|
||||
{:else if canTest(s)}<Button size="sm" href={betaHref([s])}>Test version →</Button>
|
||||
{:else}<Button size="sm" onclick={() => uploader.open(s.id)}>Upload version</Button>{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import BackLink from '$ui/components/BackLink.svelte';
|
||||
let { data }: { data: import('./$types').PageData } = $props();
|
||||
import RuntimeVersion from '$ui/components/RuntimeVersion.svelte';
|
||||
import MatchHistory from '$ui/components/MatchHistory.svelte';
|
||||
@@ -221,6 +222,7 @@
|
||||
<details class="server-actions"><summary>Server actions</summary>{#each runtimeActions as action}<Button onclick={() => { lifecycleAction = action; lifecycleOpen = true; }}>{actionLabels[action]}</Button>{/each}<Button variant="danger" onclick={() => destroyOpen = true}>Queue server removal</Button></details>
|
||||
{/snippet}
|
||||
</SectionHeader>
|
||||
<BackLink fallback="/" />
|
||||
|
||||
<CopyValue value={name} label="Server ID" />
|
||||
<RuntimeVersion {name} version={server.plutonium} after={server.telemetry_after} ready={effectiveHealth === 'online'} />
|
||||
|
||||
Reference in New Issue
Block a user