Add Prettier and ESLint tooling (Refs #77)

This commit is contained in:
2026-10-09 15:56:13 -04:00
parent 8ed059b8d3
commit 16469e2d43
711 changed files with 99737 additions and 28286 deletions
+299 -68
View File
@@ -7,8 +7,22 @@
import Button from './primitives/Button.svelte';
import Badge from './primitives/Badge.svelte';
let { pluginId, targetPath, disabled = false, busy = $bindable(false), onimport }: {
pluginId?: number; targetPath?: string; disabled?: boolean; busy?: boolean; onimport?: (result: { pluginId: number; betaUrl: string; alreadyImported: boolean }) => void | Promise<void>;
let {
pluginId,
targetPath,
disabled = false,
busy = $bindable(false),
onimport
}: {
pluginId?: number;
targetPath?: string;
disabled?: boolean;
busy?: boolean;
onimport?: (result: {
pluginId: number;
betaUrl: string;
alreadyImported: boolean;
}) => void | Promise<void>;
} = $props();
let url = $state('');
let name = $state('');
@@ -18,55 +32,89 @@
let update = $state<PluginUpdate | null>(null);
let checking = $state(false);
let error = $state('');
let success = $state<{ pluginId: number; betaUrl: string; sha256: string; alreadyImported: boolean } | null>(null);
let success = $state<{
pluginId: number;
betaUrl: string;
sha256: string;
alreadyImported: boolean;
} | null>(null);
let active = true;
onDestroy(() => { active = false; });
onDestroy(() => {
active = false;
});
function showRelease(value: GithubPluginRelease) {
release = value;
const preferred = update?.source?.asset_name ?? targetPath?.split('/').pop();
assetId = String(value.assets.find(a => a.name === preferred)?.id ?? (value.assets.length === 1 ? value.assets[0].id : ''));
assetId = String(
value.assets.find((a) => a.name === preferred)?.id ??
(value.assets.length === 1 ? value.assets[0].id : '')
);
if (!pluginId && !name) name = value.repository.split('/')[1];
url = value.url;
error = ''; success = null;
error = '';
success = null;
}
async function checkUpdates() {
if (!pluginId || checking) return;
checking = true; error = '';
checking = true;
error = '';
try {
const response = await fetch(`/api/plugins/updates?pluginId=${pluginId}`);
const body = await response.json();
if (!response.ok) throw new Error(body.error ?? 'Could not check for updates.');
update = body.updates[0];
if (!url && update?.source) url = `https://github.com/${update.source.repository}`;
} catch (e) { error = (e as Error).message; }
finally { checking = false; }
} catch (e) {
error = (e as Error).message;
} finally {
checking = false;
}
}
onMount(() => { void checkUpdates(); });
onMount(() => {
void checkUpdates();
});
async function preview(event: SubmitEvent) {
event.preventDefault();
if (busy || disabled) return;
busy = true; error = ''; release = null; success = null;
busy = true;
error = '';
release = null;
success = null;
try {
const response = await fetch(`/api/plugins/github/release?url=${encodeURIComponent(url.trim())}`);
const response = await fetch(
`/api/plugins/github/release?url=${encodeURIComponent(url.trim())}`
);
const body = await response.json();
if (!response.ok) throw new Error(body.error ?? 'Could not load release.');
showRelease(body.release);
} catch (e) { error = (e as Error).message; }
finally { busy = false; }
} catch (e) {
error = (e as Error).message;
} finally {
busy = false;
}
}
async function importRelease(startBeta: boolean) {
if (!release || !assetId || busy || disabled) return;
const startPath = window.location.pathname;
busy = true; error = ''; success = null;
busy = true;
error = '';
success = null;
try {
const response = await fetch('/api/plugins/github/import', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: release.url, assetId: Number(assetId), pluginId,
name: name.trim(), scopeType: game ? 'game' : null, scopeValue: game || null })
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: release.url,
assetId: Number(assetId),
pluginId,
name: name.trim(),
scopeType: game ? 'game' : null,
scopeValue: game || null
})
});
const body = await response.json();
if (!response.ok) throw new Error(body.error ?? 'Import failed.');
@@ -75,29 +123,57 @@
await onimport?.(body);
if (startBeta && window.location.pathname === startPath) await goto(body.betaUrl);
else if (pluginId) await checkUpdates();
} catch (e) { error = (e as Error).message; }
finally { busy = false; }
} catch (e) {
error = (e as Error).message;
} finally {
busy = false;
}
}
</script>
<Card padding="lg">
<section class="github" aria-label="GitHub plugin releases">
<div class="heading">
<div><h2>{pluginId ? 'GitHub releases' : 'Import from GitHub'}</h2><p>Choose a release and DLL, then test it with your fleet.</p></div>
{#if pluginId}<Button size="sm" onclick={checkUpdates} loading={checking} disabled={busy}>Check for updates</Button>{/if}
<div>
<h2>{pluginId ? 'GitHub releases' : 'Import from GitHub'}</h2>
<p>Choose a release and DLL, then test it with your fleet.</p>
</div>
{#if pluginId}<Button
size="sm"
onclick={checkUpdates}
loading={checking}
disabled={busy}>Check for updates</Button
>{/if}
</div>
{#if checking}<p role="status">Checking GitHub…</p>{/if}
{#if update?.source}
<p>Following <a href={`https://github.com/${update.source.repository}`} target="_blank" rel="noreferrer">{update.source.repository}</a> · {update.source.asset_name}</p>
<p>
Following <a
href={`https://github.com/${update.source.repository}`}
target="_blank"
rel="noreferrer">{update.source.repository}</a
>
· {update.source.asset_name}
</p>
{/if}
{#if update?.status === 'available' && update.release}
<div class="update">
<Badge variant="info">New release: {update.release.tag}</Badge>
<Button size="sm" disabled={busy || disabled} onclick={() => showRelease(update!.release!)}>Review update</Button>
<Button
size="sm"
disabled={busy || disabled}
onclick={() => showRelease(update!.release!)}>Review update</Button
>
</div>
{:else if update?.status === 'imported' && update.release}
<div class="update"><span>{update.release.tag} is already imported and available for testing.</span>
<Button size="sm" href={`/betas?pluginId=${pluginId}&versionId=${update.versionId}`} disabled={disabled}>Start beta</Button></div>
<div class="update">
<span>{update.release.tag} is already imported and available for testing.</span>
<Button
size="sm"
href={`/betas?pluginId=${pluginId}&versionId=${update.versionId}`}
{disabled}>Start beta</Button
>
</div>
{:else if update?.status === 'current'}
<p role="status">The latest stable release is promoted.</p>
{:else if update?.status === 'ahead'}
@@ -108,44 +184,105 @@
<p>Import a release below to track updates for this plugin.</p>
{/if}
<form onsubmit={preview} class="lookup">
<label>Repository or release URL
<input type="url" bind:value={url} oninput={() => { release = null; success = null; }} required
placeholder="https://github.com/alicealys/t6-gsc-utils/releases/tag/v3.1.0" disabled={busy || disabled} />
<label
>Repository or release URL
<input
type="url"
bind:value={url}
oninput={() => {
release = null;
success = null;
}}
required
placeholder="https://github.com/alicealys/t6-gsc-utils/releases/tag/v3.1.0"
disabled={busy || disabled}
/>
</label>
<Button type="submit" loading={busy && !release} disabled={busy || disabled || !url.trim()}>Preview release</Button>
<Button
type="submit"
loading={busy && !release}
disabled={busy || disabled || !url.trim()}>Preview release</Button
>
</form>
{#if disabled}<p>Finish the running beta before importing a release.</p>{/if}
{#if release}
<div class="preview">
<div class="heading"><h3>{release.name || release.tag}</h3><Badge variant={release.prerelease ? 'warning' : 'neutral'}>{release.prerelease ? 'Prerelease' : 'Release'} · {release.tag}</Badge></div>
<p><a href={release.url} target="_blank" rel="noreferrer">View on GitHub ↗</a> · Published {$timeFormat.date(release.publishedAt)}</p>
<details open><summary>Release notes</summary><pre>{release.notes || 'No release notes provided.'}</pre></details>
<div class="heading">
<h3>{release.name || release.tag}</h3>
<Badge variant={release.prerelease ? 'warning' : 'neutral'}
>{release.prerelease ? 'Prerelease' : 'Release'} · {release.tag}</Badge
>
</div>
<p>
<a href={release.url} target="_blank" rel="noreferrer">View on GitHub ↗</a> ·
Published {$timeFormat.date(release.publishedAt)}
</p>
<details open>
<summary>Release notes</summary>
<pre>{release.notes || 'No release notes provided.'}</pre>
</details>
{#if release.assets.length === 0}
<p>No importable DLL assets found. Choose another release or upload a DLL manually. Direct DLL assets up to 64 MB are supported.</p>
<p>
No importable DLL assets found. Choose another release or upload a DLL
manually. Direct DLL assets up to 64 MB are supported.
</p>
{:else}
<div class="fields">
<label>DLL asset<select bind:value={assetId} disabled={busy || disabled}>
<option value="" disabled>Choose a DLL…</option>
{#each release.assets as asset}<option value={String(asset.id)}>{asset.name} · {(asset.size / 1024).toFixed(1)} KB</option>{/each}
</select></label>
<label
>DLL asset<select bind:value={assetId} disabled={busy || disabled}>
<option value="" disabled>Choose a DLL…</option>
{#each release.assets as asset}<option value={String(asset.id)}
>{asset.name} · {(asset.size / 1024).toFixed(1)} KB</option
>{/each}
</select></label
>
{#if !pluginId}
<label>Plugin name<input bind:value={name} disabled={busy} placeholder="t6-gsc-utils" /></label>
<label>Deploy scope<select bind:value={game} disabled={busy}><option value="">Unscoped — select servers in a beta</option><option value="t5">Black Ops I</option><option value="t6">Black Ops II</option></select></label>
<label
>Plugin name<input
bind:value={name}
disabled={busy}
placeholder="t6-gsc-utils"
/></label
>
<label
>Deploy scope<select bind:value={game} disabled={busy}
><option value="">Unscoped — select servers in a beta</option
><option value="t5">Black Ops I</option><option value="t6"
>Black Ops II</option
></select
></label
>
{/if}
</div>
{#if targetPath}<p>New versions deploy to <code>{targetPath}</code>.</p>{/if}
<div class="actions">
<Button variant="primary" disabled={busy || disabled || !assetId || (!pluginId && !name.trim())} loading={busy} onclick={() => importRelease(true)}>Import and test</Button>
<Button disabled={busy || disabled || !assetId || (!pluginId && !name.trim())} onclick={() => importRelease(false)}>Import only</Button>
<Button
variant="primary"
disabled={busy || disabled || !assetId || (!pluginId && !name.trim())}
loading={busy}
onclick={() => importRelease(true)}>Import and test</Button
>
<Button
disabled={busy || disabled || !assetId || (!pluginId && !name.trim())}
onclick={() => importRelease(false)}>Import only</Button
>
</div>
<p>Imports are saved for testing. Choose servers and a test plan before starting a beta.</p>
<p>
Imports are saved for testing. Choose servers and a test plan before
starting a beta.
</p>
{/if}
</div>
{/if}
{#if success}
<div class="success" role="status">
<strong>{success.alreadyImported ? 'This binary is already imported.' : 'Release imported.'}</strong>
<a href={`/plugins/${success.pluginId}`}>View plugin</a> · <a href={success.betaUrl}>Start beta</a>
<strong
>{success.alreadyImported
? 'This binary is already imported.'
: 'Release imported.'}</strong
>
<a href={`/plugins/${success.pluginId}`}>View plugin</a> ·
<a href={success.betaUrl}>Start beta</a>
<span class="checksum">SHA-256: {success.sha256}</span>
</div>
{/if}
@@ -154,27 +291,121 @@
</Card>
<style>
.github, .preview { display: grid; gap: var(--space-4); }
.heading, .actions, .update { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap; }
.actions { justify-content: flex-start; }
h2, h3, p { margin: 0; }
h2 { font-size: var(--text-md); font-weight: var(--weight-semibold); }
h3 { font-size: var(--text-md); }
p { color: var(--text-secondary); font-size: var(--text-sm); line-height: 1.6; }
.heading p { margin-top: var(--space-2); }
.lookup { display: flex; align-items: flex-end; gap: var(--space-3); flex-wrap: wrap; }
.lookup label { flex: 1 1 300px; }
label { display: grid; gap: var(--space-2); font-size: var(--text-sm); }
input, select { width: 100%; min-width: 0; padding: 10px 12px; background: var(--surface-sunken); border: 1px solid var(--border-strong); border-radius: var(--radius-md); color: var(--text-primary); font: inherit; }
input:focus-visible, select:focus-visible { outline: 2px solid var(--accent-primary); outline-offset: 2px; }
.fields { display: grid; gap: var(--space-3); grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr)); }
.preview { padding-top: var(--space-4); border-top: 1px solid var(--border-subtle); }
a { color: var(--accent-primary-hover); }
pre { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 280px; overflow-y: auto; font: inherit; font-size: var(--text-sm); color: var(--text-secondary); line-height: 1.6; }
summary { cursor: pointer; font-size: var(--text-sm); }
.error { color: var(--state-danger); }
.success { display: block; padding: var(--space-3); background: var(--surface-sunken); border-radius: var(--radius-md); font-size: var(--text-sm); }
.success strong, .checksum { display: block; margin-bottom: var(--space-2); }
.checksum { overflow-wrap: anywhere; font: var(--text-xs) var(--font-mono); margin-top: var(--space-2); }
input:not([type='checkbox']):not([type='file']), select { border-color: var(--border-control); }
.github,
.preview {
display: grid;
gap: var(--space-4);
}
.heading,
.actions,
.update {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
flex-wrap: wrap;
}
.actions {
justify-content: flex-start;
}
h2,
h3,
p {
margin: 0;
}
h2 {
font-size: var(--text-md);
font-weight: var(--weight-semibold);
}
h3 {
font-size: var(--text-md);
}
p {
color: var(--text-secondary);
font-size: var(--text-sm);
line-height: 1.6;
}
.heading p {
margin-top: var(--space-2);
}
.lookup {
display: flex;
align-items: flex-end;
gap: var(--space-3);
flex-wrap: wrap;
}
.lookup label {
flex: 1 1 300px;
}
label {
display: grid;
gap: var(--space-2);
font-size: var(--text-sm);
}
input,
select {
width: 100%;
min-width: 0;
padding: 10px 12px;
background: var(--surface-sunken);
border: 1px solid var(--border-strong);
border-radius: var(--radius-md);
color: var(--text-primary);
font: inherit;
}
input:focus-visible,
select:focus-visible {
outline: 2px solid var(--accent-primary);
outline-offset: 2px;
}
.fields {
display: grid;
gap: var(--space-3);
grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
}
.preview {
padding-top: var(--space-4);
border-top: 1px solid var(--border-subtle);
}
a {
color: var(--accent-primary-hover);
}
pre {
white-space: pre-wrap;
overflow-wrap: anywhere;
max-height: 280px;
overflow-y: auto;
font: inherit;
font-size: var(--text-sm);
color: var(--text-secondary);
line-height: 1.6;
}
summary {
cursor: pointer;
font-size: var(--text-sm);
}
.error {
color: var(--state-danger);
}
.success {
display: block;
padding: var(--space-3);
background: var(--surface-sunken);
border-radius: var(--radius-md);
font-size: var(--text-sm);
}
.success strong,
.checksum {
display: block;
margin-bottom: var(--space-2);
}
.checksum {
overflow-wrap: anywhere;
font: var(--text-xs) var(--font-mono);
margin-top: var(--space-2);
}
input:not([type='checkbox']):not([type='file']),
select {
border-color: var(--border-control);
}
</style>