Make UI language, timestamps and live feedback consistent

This commit is contained in:
2026-10-07 19:46:40 -04:00
parent 0ed3cd6b47
commit 70185eb98c
81 changed files with 656 additions and 332 deletions
+11 -10
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import RelativeTime from '$ui/components/RelativeTime.svelte';
import { gameTitle } from '$ui/server-labels.js';
import BackLink from '$ui/components/BackLink.svelte';
import { onMount, tick } from 'svelte';
import { page } from '$app/stores';
@@ -37,8 +39,7 @@
const progressBytes = $derived(Math.min(totalBytes, acceptedBytes + sendingBytes));
const transferring = $derived(uploadBusy && (phase === 'preparing' || phase === 'uploading'));
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();
const gameName = (game: 't5' | 't6') => game === 't5' ? 'Black Ops I' : 'Black Ops II';
async function responseBody(response: Response) {
let body;
@@ -102,7 +103,7 @@
}
if (disposed) return;
if (status.status !== 'published') throw new Error(status.error || 'Verification did not finish. Retry this upload to continue.');
phase = 'published'; folder = null; notice = `${game.toUpperCase()} game files are ready. Active hosts will download and verify the latest files automatically.`;
phase = 'published'; folder = null; notice = `${gameTitle(game, true)} game files are ready. Active hosts will download and verify the latest files automatically.`;
if (folderInput) folderInput.value = '';
await load();
}
@@ -209,7 +210,7 @@
{#if !canManage}<p class="muted">Administrators upload game files. Active hosts prepare verified copies automatically.</p>{/if}
{#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>
<div class="heading"><h2>{phase === 'published' ? `${gameTitle(game, true)} files uploaded` : uploadId ? `Continue ${gameTitle(game, true)} upload` : `Upload ${gameTitle(game, true)} 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. Running servers keep using their existing files.</p>
{#if uploadError}<p class="ui-error" role="alert">{uploadError}</p>{/if}
<form onsubmit={startUpload}>
@@ -227,7 +228,7 @@
</div>{/if}
<div class="upload-actions">
{#if transferring}<Button onclick={pauseUpload} disabled={pauseRequested}>{pauseRequested ? 'Pausing…' : 'Pause upload'}</Button>
{:else if phase !== 'verifying' && phase !== 'published'}<Button type="submit" variant="primary" disabled={uploadBusy || !folder}>{uploadId ? 'Resume upload' : `Upload ${game.toUpperCase()} files`}</Button>{/if}
{:else if phase !== 'verifying' && phase !== 'published'}<Button type="submit" variant="primary" disabled={uploadBusy || !folder}>{uploadId ? 'Resume upload' : `Upload ${gameTitle(game, true)} files`}</Button>{/if}
{#if phase === 'idle' || phase === 'uploading'}<span class="muted">Keep this page open while files transfer. Interrupted uploads can be resumed below.</span>{/if}
</div>
</form>
@@ -241,13 +242,13 @@
<section class="card">
<div class="heading">
<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}
{#if canManage}<Button size="sm" disabled={uploadBusy} onclick={() => newUpload(library)}>{latest ? `Update ${gameTitle(library.game, true)} files` : `Upload ${gameTitle(library.game, true)} files`}</Button>{/if}
</div>
{#if 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>
<p class="muted">Upload the complete {gameName(library.game)} game folder once. Hosts will download and verify it automatically as part of setup.</p>
{/if}
{#if unfinished.length}
<div class="uploads">
@@ -255,7 +256,7 @@
{#each unfinished as item (item.id)}
{@const browserUpload = !item.sourceHostId}
<article class="upload-entry">
<div class="heading"><p>Started {new Date(item.createdAt).toLocaleString()} · {formatGameBytes(item.sizeBytes)} · {item.fileCount.toLocaleString()} files</p><Badge variant={item.state === 'failed' ? 'danger' : 'info'}>{item.state === 'failed' ? 'Failed' : browserUpload ? item.id === uploadId && phase === 'verifying' ? 'Verifying' : 'Uploading' : 'Importing'}</Badge></div>
<div class="heading"><p>Started <RelativeTime value={item.createdAt} /> · {formatGameBytes(item.sizeBytes)} · {item.fileCount.toLocaleString()} files</p><Badge variant={item.state === 'failed' ? 'danger' : 'info'}>{item.state === 'failed' ? 'Failed' : browserUpload ? item.id === uploadId && phase === 'verifying' ? 'Verifying' : 'Uploading' : 'Importing'}</Badge></div>
{#if item.error}<p class="ui-error">{item.error}</p>{/if}
{#if browserUpload}
<p class="muted">Resume with the original unchanged folder. The latest files stay available while this upload completes.</p>
@@ -266,14 +267,14 @@
</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 latest}<p class="muted">Uploaded <RelativeTime value={latest.publishedAt ?? latest.createdAt} /></p>{@render uploadDetails(latest)}{@render downloadControls(latest)}{/if}
{#if history.length}
<details class="history">
<summary>Previous uploads ({history.length})</summary>
<p class="muted">Old host copies are removed once no instances use them. Controller files are removed after all host copies are gone; upload history is kept.</p>
{#each history as item (item.id)}
<article class="upload-entry">
<h3>Uploaded {uploadDate(item)}</h3>
<h3>Uploaded <RelativeTime value={item.publishedAt ?? item.createdAt} /></h3>
{#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)}