Adjust Discord publication and lobby capacity defaults
CI / checks (push) Failing after 7m21s

This commit is contained in:
2026-10-18 17:23:50 -04:00
parent fd00ef5357
commit d715173719
15 changed files with 304 additions and 89 deletions
+7 -7
View File
@@ -4,7 +4,7 @@ export interface DiscordSettings {
enabled: boolean;
guildId: string;
channels: Record<DiscordGame, string>;
publishedServers: string[];
excludedServers: string[];
showPlayers: boolean;
showAddresses: boolean;
leaderboardEnabled: boolean;
@@ -14,7 +14,7 @@ export const defaultDiscordSettings: DiscordSettings = {
enabled: false,
guildId: '',
channels: { t5: '', t6: '' },
publishedServers: [],
excludedServers: [],
showPlayers: false,
showAddresses: false,
leaderboardEnabled: false
@@ -43,20 +43,20 @@ export function validateDiscordSettings(value: unknown): DiscordSettings {
throw new Error('Channel IDs must be valid Discord IDs or empty.');
}
if (
!Array.isArray(input.publishedServers) ||
input.publishedServers.length > 100 ||
input.publishedServers.some(
!Array.isArray(input.excludedServers) ||
input.excludedServers.length > 100 ||
input.excludedServers.some(
(name) => typeof name !== 'string' || !/^[A-Za-z0-9_.-]{1,128}$/.test(name)
)
)
throw new Error('Choose up to 100 published servers.');
throw new Error('Choose up to 100 servers to exclude from Discord.');
if (input.enabled && (!input.guildId || (!channels.t5 && !channels.t6)))
throw new Error('Choose a Discord server and at least one board channel before enabling.');
return {
enabled: input.enabled as boolean,
guildId: input.guildId,
channels: { t5: channels.t5 as string, t6: channels.t6 as string },
publishedServers: [...new Set(input.publishedServers as string[])].sort(),
excludedServers: [...new Set(input.excludedServers as string[])].sort(),
showPlayers: input.showPlayers as boolean,
showAddresses: input.showAddresses as boolean,
leaderboardEnabled: input.leaderboardEnabled as boolean
+7 -1
View File
@@ -27,6 +27,7 @@ import { migratePlayerScoreTotal } from './player-score-total.js';
import { migrateBank } from './bank.js';
import { migrateAfk } from './afk.js';
import { migrateDiscord } from '../discord/schema.js';
import { migrateDiscordPublicationDefaults } from '../discord/publication-defaults.js';
import { migrateBankCredentials } from './bank-credentials.js';
import { migrateTemperatures } from '../hosts/temperatures.js';
import {
@@ -212,7 +213,12 @@ export const migrations: readonly Migration[] = [
mode: 'foreign-keys-off',
run: migrateModerationCaseStates
},
{ version: 37, name: 'Discord boards and availability subscriptions', run: migrateDiscord }
{ version: 37, name: 'Discord boards and availability subscriptions', run: migrateDiscord },
{
version: 38,
name: 'Discord default server publication',
run: migrateDiscordPublicationDefaults
}
];
export const databaseVersion = migrations.length;
+11 -4
View File
@@ -14,6 +14,8 @@ type SourceServer = FleetServer & {
network?: { publicAddress: string | null; publicPort: number | null } | null;
};
export const DEFAULT_DISCORD_LOBBY_CAPACITY = 4;
/** IDs stay short enough for Discord components, even for long registry names. */
export function discordServerKey(name: string): string {
return createHash('sha256').update(name).digest('hex').slice(0, 20);
@@ -69,19 +71,24 @@ export function projectFleet(
settings: DiscordSettings,
now = Date.now()
): DiscordPublicServer[] {
const published = new Set(settings.publishedServers);
const excluded = new Set(settings.excludedServers);
return servers
.flatMap((server): DiscordPublicServer[] => {
if (!published.has(server.name)) return [];
if (excluded.has(server.name)) return [];
const identity = fleetIdentity(server);
if (identity.game !== 't5' && identity.game !== 't6') return [];
const game = identity.game;
const fresh = freshServer(server, now);
const fresh =
freshServer(server, now) &&
(!server.telemetry_after ||
Date.parse(server.last_seen_at ?? '') > Date.parse(server.telemetry_after));
const players =
fresh && Number.isSafeInteger(server.last_players) && server.last_players! >= 0
? server.last_players
: null;
const capacity = players !== null ? currentCapacity(server, now) : null;
// These lobbies have four slots when the live status omits the limit.
// Occupancy still needs fresh, current-process evidence below.
const capacity = currentCapacity(server, now) ?? DEFAULT_DISCORD_LOBBY_CAPACITY;
const state: DiscordPublicServer['state'] =
server.health === 'starting' || server.health === 'provisioning'
? 'starting'
@@ -0,0 +1,24 @@
import type Database from 'better-sqlite3';
/** Replace the old opt-in default while preserving an existing nonempty selection. */
export function migrateDiscordPublicationDefaults(handle: Database.Database): void {
const row = handle.prepare('SELECT value FROM discord_configuration WHERE id=1').get() as
{ value: string } | undefined;
if (!row) return;
const settings = JSON.parse(row.value) as Record<string, unknown>;
if ('excludedServers' in settings) return;
const published = new Set(settings.publishedServers as string[]);
settings.excludedServers = published.size
? (
handle
.prepare('SELECT name FROM servers WHERE destroyed_at IS NULL ORDER BY name')
.all() as { name: string }[]
)
.map((server) => server.name)
.filter((name) => !published.has(name))
: [];
delete settings.publishedServers;
handle
.prepare('UPDATE discord_configuration SET value=? WHERE id=1')
.run(JSON.stringify(settings));
}
+3 -3
View File
@@ -8,7 +8,7 @@ import {
} from '../../leaderboard-efficiency.js';
import { MIN_RECORDED_ROUND } from '../../match-history.js';
import { gameTitle } from '../../ui/server-labels.js';
import { discordServerKey, discordText } from './projection.js';
import { DEFAULT_DISCORD_LOBBY_CAPACITY, discordServerKey, discordText } from './projection.js';
export const BOARD_PAGE_SIZE = 8;
export const BROWSER_PAGE_SIZE = 5;
@@ -65,7 +65,7 @@ function summary(server: DiscordPublicServer): string {
const count =
server.players === null
? 'Players unknown'
: `${server.players}/${server.capacity ?? '?'} players`;
: `${server.players}/${server.capacity ?? DEFAULT_DISCORD_LOBBY_CAPACITY} players`;
return `${count}${server.round === null ? '' : ` · Round ${server.round}`}\n${states[server.state]}`;
}
@@ -250,7 +250,7 @@ export function renderWatchMaps(
export function renderWatchChoice(server: DiscordPublicServer): DiscordMessage {
return discordMessage([
text(
`## Watch ${discordText(server.map)}\nEmpty means zero observed players. Space means fewer players than the current observed capacity. Neither guarantees that a match accepts new players.\nOne DM · Expires in 24 hours`
`## Watch ${discordText(server.map)}\nEmpty means zero observed players. Space means fewer players than the lobby capacity. Neither guarantees that a match accepts new players.\nOne DM · Expires in 24 hours`
),
row(
button('Empty lobby', `ec:w:empty:${discordServerKey(server.mapKey)}`),
+2 -2
View File
@@ -6,7 +6,7 @@ import type {
DiscordSettings
} from '../../discord.js';
import { requireDiscordCredentials } from './config.js';
import { discordText, publicFleet } from './projection.js';
import { DEFAULT_DISCORD_LOBBY_CAPACITY, discordText, publicFleet } from './projection.js';
import { renderBoard } from './render.js';
import { DiscordStore, discordStore, type DiscordJob, type DiscordSubscription } from './store.js';
import { DiscordDeliveryError, DiscordRest, type DiscordTransport } from './transport.js';
@@ -373,7 +373,7 @@ function matchingServer(
function alertMessage(watch: DiscordSubscription, server: DiscordPublicServer): DiscordMessage {
const safe = (value: string) => discordText(value, 100);
return {
content: `🍒 **${safe(server.map)}** ${watch.mode === 'empty' ? 'has an empty lobby' : 'has an open player slot'}: **${safe(server.label)}** (${server.players}/${server.capacity ?? '?'}).\nThis describes occupancy; joining an in-progress game may still be restricted.\nYour one-time map watch is complete. Use /servers to browse or /watch to watch again.`,
content: `🍒 **${safe(server.map)}** ${watch.mode === 'empty' ? 'has an empty lobby' : 'has an open player slot'}: **${safe(server.label)}** (${server.players}/${server.capacity ?? DEFAULT_DISCORD_LOBBY_CAPACITY}).\nThis describes occupancy; joining an in-progress game may still be restricted.\nYour one-time map watch is complete. Use /servers to browse or /watch to watch again.`,
allowed_mentions: { parse: [] }
};
}
@@ -4,7 +4,6 @@ import { discordAdminState } from '$lib/server/discord/admin.js';
import { requireDiscordCredentials } from '$lib/server/discord/config.js';
import { saveDiscordSettings } from '$lib/server/discord/store.js';
import { jsonBody } from '$lib/server/request-body.js';
import { listServers } from '$lib/server/fleet/servers.js';
import { resetDiscordClient } from '$lib/server/discord/worker.js';
export function GET() {
@@ -15,14 +14,6 @@ export async function PUT({ request }: { request: Request }) {
try {
const settings = validateDiscordSettings(await jsonBody(request, 32 * 1024));
if (settings.enabled) requireDiscordCredentials();
const known = new Set(listServers().map((server) => server.name));
if (settings.publishedServers.some((name) => !known.has(name)))
return json(
{
error: 'One of the selected servers is no longer available. Refresh and try again.'
},
{ status: 400 }
);
saveDiscordSettings(settings);
resetDiscordClient();
return json(discordAdminState());
@@ -43,11 +43,15 @@
adminState.credentials.publicKeyConfigured &&
adminState.credentials.botTokenConfigured
);
const unavailableSelections = $derived(
draft?.publishedServers.filter(
const unavailableExclusions = $derived(
draft?.excludedServers.filter(
(name) => !adminState?.servers.some((server) => server.name === name)
) ?? []
);
const includedServerCount = $derived(
adminState?.servers.filter((server) => !draft?.excludedServers.includes(server.name))
.length ?? 0
);
onMount(() => {
origin = window.location.origin;
@@ -85,9 +89,9 @@
function chooseServer(name: string, checked: boolean) {
if (!draft || !administrator) return;
draft.publishedServers = checked
? [...new Set([...draft.publishedServers, name])].sort()
: draft.publishedServers.filter((selected) => selected !== name);
draft.excludedServers = checked
? draft.excludedServers.filter((excluded) => excluded !== name)
: [...new Set([...draft.excludedServers, name])].sort();
}
function discard() {
@@ -192,7 +196,8 @@
Enable Discord companion
</label>
<p class="help">
Enabling publishes the selected servers to your configured channels.
Enabling publishes all servers except your exclusions to the configured
channels.
</p>
<Field
label="Discord server ID"
@@ -229,8 +234,8 @@
<fieldset class="panel" disabled={!administrator || busy}>
<legend>Published servers</legend>
<p class="help">
Only selected servers appear in Discord browsing, boards, and
availability alerts. New servers remain private until selected.
All current and new servers are published by default. Uncheck a server
to exclude it from Discord browsing, boards, and availability alerts.
</p>
{#if adminState.servers.length === 0}<p class="muted">
No supported game servers are available.
@@ -246,7 +251,7 @@
<label class="server-choice">
<input
type="checkbox"
checked={draft.publishedServers.includes(
checked={!draft.excludedServers.includes(
server.name
)}
onchange={(event) =>
@@ -265,20 +270,21 @@
</div>
{/if}
{/each}
{#if unavailableSelections.length}
{#if unavailableExclusions.length}
<div class="server-group">
<h3>No longer in the fleet</h3>
{#each unavailableSelections as name (name)}
<h3>Excluded servers no longer in the fleet</h3>
{#each unavailableExclusions as name (name)}
<label class="server-choice">
<input
type="checkbox"
checked
checked={false}
onchange={(event) =>
chooseServer(name, event.currentTarget.checked)}
/>
<span
>{name}<small
>Remove this selection to stop publishing it.</small
>Excluded if this server returns. Check to remove
the exclusion.</small
></span
>
</label>
@@ -286,7 +292,7 @@
</div>
{/if}
<p class="help">
{countLabel(draft.publishedServers.length, 'server')} selected.
{countLabel(includedServerCount, 'server')} included.
</p>
</fieldset>
@@ -308,8 +314,8 @@
>
<p class="help">
Leaderboards publish player names and recorded statistics from the
entire fleet, including servers outside the selection above. Keep this
off if those results should stay private.
entire fleet, including excluded servers. Keep this off if those results
should stay private.
</p>
</fieldset>
@@ -477,14 +483,14 @@
<span class="muted">Saved settings</span>
</div>
<p class="help">
This shows the information available to Discord from your saved selection. Empty
slots do not establish whether a game accepts new players.
This shows the information available to Discord with your saved exclusions.
Empty slots do not establish whether a game accepts new players.
</p>
{#if !adminState.settings.enabled}<p class="muted">
Discord is disabled. This preview is visible only in Electric Cherry.
</p>{/if}
{#if !adminState.preview.t5.length && !adminState.preview.t6.length}
<p class="muted">Select servers and save your changes to see a preview.</p>
<p class="muted">No servers are included by the saved settings.</p>
{:else}
<div class="preview-games">
{#each games as game}
@@ -503,7 +509,7 @@
</div>
<p>
{server.map} · {server.players ?? '?'} / {server.capacity ??
'?'} players{server.round === null
4} players{server.round === null
? ''
: ` · Round ${server.round}`}
</p>