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
+5 -4
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { countLabel } from '$ui/plural.js';
import { page } from '$app/stores';
import { contextHref, safeReturnHref } from '$lib/navigation-context.js';
import Breadcrumbs from '$ui/components/primitives/Breadcrumbs.svelte';
@@ -75,7 +76,7 @@
function versionsFor(pick:Pick) { return pick.scriptId===null ? [] : (detailById[pick.scriptId]?.versions??[]).filter(v=>v.id!==detailById[pick.scriptId!]?.script.current_version_id); }
function eligibleFor(index:number) { return eligibleFiles.filter(file=>!picks.some((p,i)=>i!==index&&p.scriptId===file.id)); }
function gameLabel(game:string) { return game==='t5'?'Black Ops I (T5)':game==='t6'?'Black Ops II (T6)':game; }
function gameLabel(game:string) { return game==='t5'?'Black Ops I':game==='t6'?'Black Ops II':game; }
function scopeText(file:FileRow) { return !file.scope_type ? 'Any registered server' : file.scope_type==='map' ? `Map: ${mapLabel(file.scope_value??'')}` : `Game: ${file.scope_value==='t5'?'Black Ops I':file.scope_value==='t6'?'Black Ops II':file.scope_value}`; }
function scopeCount(file:FileRow) { return fleetServers.filter(server=>!file.scope_type || (file.scope_type==='game'?server.game:server.map)===file.scope_value).length; }
function reconcileSelection() {
@@ -229,7 +230,7 @@
<h3>Instances that must pass</h3>
<p>At launch, Electric Cherry chooses compatible servers with the fewest current players. Every requested instance must pass the test plan.</p>
<label class="instance-count">Instances per game<input type="number" min="1" max="1000" step="1" value={instanceCount??''} oninput={event=>instanceCount=event.currentTarget.value===''?null:event.currentTarget.valueAsNumber}/><span class="muted">The same number applies to every game in this beta.</span></label>
<p class="muted">{applicableGames.map(game=>`${gameLabel(game)}: ${candidates.filter(server=>server.game===game).length} compatible registered servers`).join(' · ')}</p>
<p class="muted">{applicableGames.map(game=>`${gameLabel(game)}: ${countLabel(candidates.filter(server=>server.game===game).length, 'compatible registered server')}`).join(' · ')}</p>
<p class="muted">Servers are selected when you start the beta, using fresh player counts and availability checks. Restarts wait for confirmed empty lobbies.</p>
</div>
{:else}<p><strong>{pickedServers.size} selected</strong> · {candidateServers.length} compatible servers. Restarts wait for confirmed empty lobbies.</p>{/if}
@@ -244,7 +245,7 @@
{#if selectionMode==='manual' && candidateServers.length}
<div class="filters"><label>Find servers<input type="search" bind:value={search} placeholder="Name, map, lobby…"/></label><label>Game<select bind:value={serverGame}><option value="">All games</option>{#each [...new Set(fleetServers.map(s=>s.game).filter(Boolean))] as game}<option value={game}>{game==='t5'?'Black Ops I':game==='t6'?'Black Ops II':game}</option>{/each}</select></label><label>Availability<select bind:value={availability}><option value="">All servers</option><option value="empty">Confirmed empty</option><option value="occupied">Occupied</option><option value="unknown">Unknown / stale / offline</option></select></label></div>
<div class="actions"><span>{visibleServers.length} visible</span><Button size="sm" onclick={()=>selectVisible(true)} disabled={!visibleServers.length}>Select visible</Button><Button size="sm" onclick={()=>selectVisible(false)} disabled={!visibleServers.some(s=>pickedServers.has(s.name))}>Deselect visible</Button></div>
{#each groups as group}<fieldset><legend>{fleetIdentity(group.servers[0]).map} · {group.servers[0].game==='t5'?'Black Ops I':'Black Ops II'}</legend><div class="server-picker">{#each group.servers as server}<label class="server-choice"><input type="checkbox" checked={pickedServers.has(server.name)} onchange={()=>toggleServer(server.name)}/><span><ServerIdentity name={server.name}/><small>{freshServer(server,now)&&server.last_players!==null ? `${server.last_players} players · ${server.last_players===0?'Confirmed empty':'Occupied'}` : `Players unknown · ${server.health}`}{#if server.held.length} · In beta #{server.held[0].betaId}{/if}</small></span></label>{/each}</div></fieldset>{/each}
{#each groups as group}<fieldset><legend>{fleetIdentity(group.servers[0]).map} · {group.servers[0].game==='t5'?'Black Ops I':'Black Ops II'}</legend><div class="server-picker">{#each group.servers as server}<label class="server-choice"><input type="checkbox" checked={pickedServers.has(server.name)} onchange={()=>toggleServer(server.name)}/><span><ServerIdentity name={server.name}/><small>{freshServer(server,now)&&server.last_players!==null ? `${countLabel(server.last_players, 'player')} · ${server.last_players===0?'Confirmed empty':'Occupied'}` : `Players unknown · ${server.health}`}{#if server.held.length} · In beta #{server.held[0].betaId}{/if}</small></span></label>{/each}</div></fieldset>{/each}
{#if !visibleServers.length}<p>No servers match these filters. <button type="button" class="text-action" onclick={()=>{search='';serverGame='';availability='';}}>Clear filters</button></p>{/if}
{/if}
</div></details>
@@ -254,7 +255,7 @@
<ul>{#each picks as pick}<li><strong>{scripts.find(file=>file.id===pick.scriptId)?.name}</strong> · {artifactVersionLabel(versionsFor(pick).find(v=>v.id===pick.versionId))} · {versionsFor(pick).find(v=>v.id===pick.versionId)?.sha256.slice(0,12)}{#if pick.scriptId!==null && detailById[pick.scriptId]}<br/><span class="muted">{scopeText(detailById[pick.scriptId].script)}</span>{/if}</li>{/each}</ul>
<div class="review-heading"><h3>Test instances ({serverCount})</h3><Button size="sm" onclick={()=>move(1)}>Edit servers</Button></div>
{#if selectionMode==='automatic'}
<ul>{#each Object.entries(automaticCounts) as [game,count]}<li><strong>{gameLabel(game)}:</strong> {count} instance{count===1?'':'s'} must pass</li>{/each}</ul>
<ul>{#each Object.entries(automaticCounts) as [game,count]}<li><strong>{gameLabel(game)}:</strong> {countLabel(count, 'server')} must pass</li>{/each}</ul>
<p>The least-used available servers are chosen when you start the beta. Selection uses current player counts and ensures every file has a matching participant. If there are not enough available servers, the beta will not start.</p>
{:else}<ul>{#each [...pickedServers] as name}<li><ServerIdentity {name}/></li>{/each}</ul>{/if}
<div class="review-heading"><h3>Pass criteria</h3><Button size="sm" onclick={()=>move(2)}>Edit plan</Button></div><p>{coverageSummary(testPlan)} · {testPlan.timeoutHours}-hour window, starting when this beta opens.</p><ul>{#each testPlan.requirements as requirement}<li>{requirementSummary(requirement)}</li>{/each}</ul><h3>Waves and failure handling</h3><p>{testPlan.waveSize?`${testPlan.waveSize} servers per wave.`:'Deploy to all selected servers.'} {testPlan.failureAction==='rollback'?'Failed checks trigger automatic rollback.':testPlan.failureAction==='pause'?'Failed checks pause testing.':'Failed checks hold the beta for review.'} {testPlan.rollbackRestart==='when-empty'?'Rollback restarts wait for empty servers.':'Rollback restores files; restart servers manually.'}</p><p>Starting installs each selected version only on matching test servers and restarts them when empty. Waiting, pausing, and retrying use the original testing window. Promotion is a separate reviewed action after testing.</p>