Simplify library lists and game file readiness
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
|
||||
import Button from '$ui/components/primitives/Button.svelte';
|
||||
import Badge from '$ui/components/primitives/Badge.svelte';
|
||||
import GameFileTransferProgress from '$ui/components/GameFileTransferProgress.svelte';
|
||||
import GameFileHosts from '$ui/components/GameFileHosts.svelte';
|
||||
|
||||
type OverviewLibrary = Omit<GameLibrary, 'versions'> & { versions: Omit<GameLibraryVersion, 'files'>[] };
|
||||
interface UploadFile { path: string; size: number; lastModified: number; receivedBytes: number; complete: boolean }
|
||||
@@ -39,14 +39,6 @@
|
||||
const hostName = (id: string | null) => id ? hosts.find(host => host.id === id)?.name ?? id : 'Browser upload';
|
||||
const gameName = (game: 't5' | 't6') => game === 't5' ? 'Black Ops I (T5)' : 'Black Ops II (T6)';
|
||||
const uploadDate = (item: OverviewLibrary['versions'][number]) => new Date(item.publishedAt ?? item.createdAt).toLocaleString();
|
||||
function automaticDownloadDetail(host: Host | undefined): string {
|
||||
if (host?.decommission) return 'Automatic setup is paused during host deletion.';
|
||||
if (!host?.enrolled) return 'Waiting for host enrollment.';
|
||||
if (host.mode !== 'active') return 'Automatic setup resumes when the host is active.';
|
||||
if (!host.credentialActive || !host.connected) return 'Automatic setup resumes when the host reconnects.';
|
||||
if (host.readiness === 'degraded') return host.readinessReasons[0] ?? 'The host needs attention before automatic setup can continue.';
|
||||
return 'Download starts automatically after runtime preparation and earlier work finish.';
|
||||
}
|
||||
|
||||
async function responseBody(response: Response) {
|
||||
let body;
|
||||
@@ -210,7 +202,7 @@
|
||||
|
||||
<svelte:head><title>Game Files | Electric Cherry</title></svelte:head>
|
||||
<div class="page">
|
||||
<SectionHeader title="Game Files" subtitle="Upload your T5 and T6 game folders. Hosts automatically keep a verified copy of the latest files for each game." />
|
||||
<SectionHeader title="Game Files" subtitle="Verified game files and host readiness." />
|
||||
{#if $page.url.searchParams.has('returnTo')}<BackLink fallback="/hosts" />{/if}
|
||||
{#if error}<p class="ui-error" role="alert">{error}</p>{/if}
|
||||
{#if notice}<p role="status">{notice}</p>{/if}
|
||||
@@ -218,7 +210,7 @@
|
||||
{#if canManage && showUpload}
|
||||
<section class="card" id="game-upload">
|
||||
<div class="heading"><h2>{phase === 'published' ? `${game.toUpperCase()} files uploaded` : uploadId ? `Continue ${game.toUpperCase()} upload` : `Upload ${game.toUpperCase()} game folder`}</h2><Button size="sm" variant="ghost" disabled={uploadBusy} onclick={() => showUpload = false}>Close</Button></div>
|
||||
<p>Select your complete {gameName(game)} game folder, including its installed DLC. Once verified, this upload becomes the latest files and hosts download it automatically.</p>
|
||||
<p>Select your complete {gameName(game)} game folder, including its installed DLC. Once verified, this upload becomes the latest files and hosts download it automatically. Running servers keep using their existing files.</p>
|
||||
{#if uploadError}<p class="ui-error" role="alert">{uploadError}</p>{/if}
|
||||
<form onsubmit={startUpload}>
|
||||
{#if phase !== 'verifying' && phase !== 'published'}
|
||||
@@ -248,14 +240,12 @@
|
||||
{@const history = library.versions.filter(item => item.state === 'ready' && item.id !== library.latestVersionId)}
|
||||
<section class="card">
|
||||
<div class="heading">
|
||||
<div><h2>{gameName(library.game)}</h2><p class="muted">{latest ? 'Latest verified game files' : 'Game folder not uploaded yet'}</p></div>
|
||||
<div><h2>{gameName(library.game)}</h2>{#if latest}<p class="muted">{formatGameBytes(latest.sizeBytes)} · {latest.fileCount.toLocaleString()} files</p>{/if}</div>
|
||||
{#if canManage}<Button size="sm" disabled={uploadBusy} onclick={() => newUpload(library)}>{latest ? `Update ${library.game.toUpperCase()} files` : `Upload ${library.game.toUpperCase()} files`}</Button>{/if}
|
||||
</div>
|
||||
{#if latest}
|
||||
<div class="heading"><p>{formatGameBytes(latest.sizeBytes)} · {latest.fileCount.toLocaleString()} files · Uploaded {uploadDate(latest)}</p><Badge variant="online">Available</Badge></div>
|
||||
<p class="muted">Active hosts keep these files ready automatically, including hosts added later. Running servers keep using their existing files.</p>
|
||||
{@render hostCopies(latest, true)}
|
||||
{@render uploadDetails(latest)}
|
||||
<div class="heading"><span class="muted">Latest verified upload</span><Badge variant={latest.caches.some(cache => cache.status === 'failed') ? 'danger' : latest.caches.length && latest.caches.every(cache => cache.status === 'ready') ? 'online' : 'neutral'}>{latest.caches.length ? `${latest.caches.filter(cache => cache.status === 'ready').length} / ${latest.caches.length} host copies ready` : 'No hosts'}</Badge></div>
|
||||
<GameFileHosts caches={latest.caches} {hosts} latest />
|
||||
{:else}
|
||||
<p class="muted">Upload the complete {library.game.toUpperCase()} game folder once. Hosts will download and verify it automatically as part of setup.</p>
|
||||
{/if}
|
||||
@@ -275,6 +265,8 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if latest || history.length}<details class="library-details"><summary>Details</summary><div class="detail-content">
|
||||
{#if latest}<p class="muted">Uploaded {uploadDate(latest)}</p>{@render uploadDetails(latest)}{@render downloadControls(latest)}{/if}
|
||||
{#if history.length}
|
||||
<details class="history">
|
||||
<summary>Previous uploads ({history.length})</summary>
|
||||
@@ -285,11 +277,12 @@
|
||||
{#if item.filesRemoved}<p class="muted">Files removed from controller · upload history retained</p>{:else if item.filesRemoving}<p class="muted">Removing unused controller files · upload history retained</p>{/if}
|
||||
<p>{formatGameBytes(item.sizeBytes)} · {item.fileCount.toLocaleString()} files</p>
|
||||
{@render uploadDetails(item)}
|
||||
<details><summary>Host copies</summary>{@render hostCopies(item, false)}</details>
|
||||
<details><summary>Host copies</summary><GameFileHosts caches={item.caches} {hosts} />{@render downloadControls(item)}</details>
|
||||
</article>
|
||||
{/each}
|
||||
</details>
|
||||
{/if}
|
||||
</div></details>{/if}
|
||||
</section>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -298,25 +291,13 @@
|
||||
<details><summary>Upload details</summary><dl><dt>Upload ID</dt><dd>{item.id}</dd><dt>Manifest SHA256</dt><dd>{item.manifestSha256 ?? 'Available after verification'}</dd><dt>Uploaded from</dt><dd>{!item.sourceHostId ? 'Game folder in browser' : `${hostName(item.sourceHostId)} / ${item.sourceVolume}`}</dd></dl><a href={`/api/game-files/${item.id}`} target="_blank" rel="noreferrer">View complete file manifest</a></details>
|
||||
{/snippet}
|
||||
|
||||
{#snippet hostCopies(item: OverviewLibrary['versions'][number], latest: boolean)}
|
||||
{#if !hosts.length}<p class="muted">Files are ready. <a href="/hosts">Add a host</a> to prepare them automatically.</p>{/if}
|
||||
<div class="table-wrap"><table><thead><tr><th>Host</th><th>File status</th><th>Progress</th><th>Detail</th></tr></thead><tbody>
|
||||
{#each item.caches as cache}
|
||||
{@const cacheHost = hosts.find(host => host.id === cache.hostId)}
|
||||
{@const awaitingAutomaticDownload = latest && cache.status === 'missing'}
|
||||
<tr>
|
||||
<td><a href={`/hosts/${cache.hostId}`}>{hostName(cache.hostId)}</a></td>
|
||||
<td>{awaitingAutomaticDownload ? 'Waiting' : cache.status}{!cacheHost?.connected ? ' · offline' : ''}</td>
|
||||
<td>{#if cache.status !== 'missing'}<GameFileTransferProgress {cache} connected={cacheHost?.connected ?? false} />{:else}—{/if}</td>
|
||||
<td>{cache.error ?? (awaitingAutomaticDownload ? automaticDownloadDetail(cacheHost) : '')}{#if awaitingAutomaticDownload || cache.status === 'failed'} <a href={`/hosts/${cache.hostId}`}>View host setup</a>{/if}</td>
|
||||
</tr>
|
||||
{:else}<tr><td colspan="4">No host copies yet.</td></tr>{/each}
|
||||
</tbody></table></div>
|
||||
{#snippet downloadControls(item: OverviewLibrary['versions'][number])}
|
||||
{#if canManage && !item.filesRemoved && !item.filesRemoving}<details><summary>Advanced: download or verify these files</summary><p class="muted">Retry a download or prepare a previous upload on a host. The latest files are downloaded automatically.</p><div class="download"><label>Host<select bind:value={targets[item.id]}><option value="">Select host</option>{#each onlineHosts as host}<option value={host.id}>{host.name} · {formatGameBytes(Number(host.inventory.storageAvailable ?? 0))} free</option>{/each}</select></label><Button size="sm" disabled={busy || !targets[item.id]} onclick={() => download(item.id)}>Download / verify</Button></div></details>{/if}
|
||||
{/snippet}
|
||||
|
||||
<style>
|
||||
.page, .card, .uploads, .upload-entry, form { display:grid; gap:20px; }
|
||||
.page, .card, .uploads, .upload-entry, form { display:grid; gap:16px; }
|
||||
.detail-content { display:grid; gap:16px; padding-top:16px; }
|
||||
.card { padding:24px; min-width:0; border:1px solid var(--border-subtle); background:var(--surface); border-radius:var(--radius-md); }
|
||||
.uploads, .upload-entry { padding-top:20px; border-top:1px solid var(--border-subtle); }
|
||||
.history>p, .history>.upload-entry { margin-top:16px; }
|
||||
@@ -330,9 +311,8 @@
|
||||
.path-list { padding-left:20px; font-family:var(--font-mono); font-size:var(--text-xs); overflow-wrap:anywhere; }
|
||||
.current-file { font-family:var(--font-mono); font-size:var(--text-xs); overflow-wrap:anywhere; } progress { width:100%; accent-color:var(--accent-primary); }
|
||||
h2,h3,p { margin:0; } h2 { font-size:var(--text-md); } h3 { font-size:var(--text-sm); }
|
||||
p,td,th,details { font-size:var(--text-sm); line-height:1.6; } .muted { color:var(--text-secondary); }
|
||||
p,details { font-size:var(--text-sm); line-height:1.6; } .muted { color:var(--text-secondary); }
|
||||
a { color:var(--accent-primary-hover); } summary { cursor:pointer; }
|
||||
dl { display:grid; grid-template-columns:auto 1fr; gap:6px 16px; } dd { margin:0; overflow-wrap:anywhere; }
|
||||
.table-wrap { overflow-x:auto; } table { width:100%; border-collapse:collapse; text-align:left; } th,td { padding:10px; border-bottom:1px solid var(--border-subtle); }
|
||||
@media(max-width:640px) { .card,.folder-summary,.upload-progress { padding:16px; } dl { grid-template-columns:1fr; } .heading>div { min-width:0; } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user