diff --git a/README.md b/README.md index 010b657..d07e777 100644 --- a/README.md +++ b/README.md @@ -27,12 +27,6 @@ removal, and restart-recovery state appears alongside affected servers. Fleet membership and operation progress refresh every ten seconds, with live server status between refreshes. Failed refreshes keep the last snapshot visible. -**Follow lobby** can notify you when a server is freshly confirmed empty, returns -online, or a selected campaign/server operation completes. Watches run while the -app is open, continue across navigation, and survive reloads in the same tab. -Notifications include a link and remain until dismissed. Watches do not perform -server actions or send browser push notifications after the app closes. - ## Stack - SvelteKit (adapter-node) + TypeScript diff --git a/src/lib/fleet-overview.ts b/src/lib/fleet-overview.ts index e3e7d74..1757d0a 100644 --- a/src/lib/fleet-overview.ts +++ b/src/lib/fleet-overview.ts @@ -87,21 +87,3 @@ export function matchesFleet(server: FleetServer, query: string, filter: FleetFi ...(server.roster ?? []).map(player => player.name)].join(' ').toLocaleLowerCase(); return query.trim().toLocaleLowerCase().split(/\s+/).every(term => text.includes(term)); } - -export interface LobbyWatch { - id: string; - server: string; - kind: 'empty' | 'online' | 'operation'; - operationKey?: string; - href: string; -} - -export function watchSatisfied(watch: LobbyWatch, server: FleetServer | undefined, - operations: readonly FleetOperation[], now: number): boolean { - if (watch.kind === 'operation') { - // Disappearance, failure and replacement by another operation are not completion. - return operations.some(op => op.key === watch.operationKey && op.state === 'done'); - } - if (!server || !freshServer(server, now)) return false; - return watch.kind === 'online' || server.last_players === 0; -} diff --git a/src/lib/ui/stores/fleet.ts b/src/lib/ui/stores/fleet.ts index 3d4ccee..6a4e73a 100644 --- a/src/lib/ui/stores/fleet.ts +++ b/src/lib/ui/stores/fleet.ts @@ -1,9 +1,7 @@ -import { derived, get, writable } from 'svelte/store'; +import { derived, writable } from 'svelte/store'; import { browser } from '$app/environment'; -import { statusByServer, statusReceivedAt, serverTimeNow, syncServerTime, matchEventsByServer } from './servers.js'; -import { watchSatisfied, type FleetServer, type FleetOperation, type LobbyWatch } from '$lib/fleet-overview.js'; -import { serverTitle } from '../server-labels.js'; -import { toast } from '../toast.js'; +import { statusByServer, statusReceivedAt, syncServerTime, matchEventsByServer } from './servers.js'; +import { type FleetServer, type FleetOperation } from '$lib/fleet-overview.js'; export const fleet = writable<{ servers: FleetServer[]; operations: FleetOperation[]; receivedAt: number }>({ servers: [], operations: [], receivedAt: 0 @@ -11,7 +9,6 @@ export const fleet = writable<{ servers: FleetServer[]; operations: FleetOperati export const fleetError = writable(''); export const fleetRefreshing = writable(false); export const fleetIntegrations = writable<{ rcon: boolean; komodo: boolean; rconTimeoutMs: number; defaultRconHost: string } | null>(null); -export const lobbyWatches = writable([]); export const fleetServers = derived([fleet, statusByServer, statusReceivedAt, matchEventsByServer], ([snapshot, live, received, matches]) => snapshot.servers.map(server => { @@ -62,58 +59,18 @@ export function refreshFleet(): Promise { return inFlight; } -export function followLobby(server: string, kind: LobbyWatch['kind'], operation?: FleetOperation): void { - const id = `${server}:${kind}:${operation?.key ?? ''}`; - lobbyWatches.update(watches => watches.some(watch => watch.id === id) ? watches : [...watches, { - id, server, kind, operationKey: operation?.key, - href: operation?.href ?? `/servers/${encodeURIComponent(server)}` - }]); - toast('Watching this lobby while the app is open.'); -} - -export function cancelWatch(id: string): void { - lobbyWatches.update(watches => watches.filter(watch => watch.id !== id)); -} - let stop: (() => void) | null = null; export function startFleetMonitor(): () => void { if (!browser || stop) return () => {}; - try { - const saved: unknown = JSON.parse(sessionStorage.getItem('electric-cherry:lobby-watches') ?? '[]'); - if (Array.isArray(saved)) lobbyWatches.set(saved.filter((watch): watch is LobbyWatch => - watch && typeof watch.id === 'string' && typeof watch.server === 'string' - && ['empty', 'online', 'operation'].includes(watch.kind) - && typeof watch.href === 'string' && /^\/(servers|campaigns|deployments)\//.test(watch.href) - && (watch.kind !== 'operation' || typeof watch.operationKey === 'string'))); - } catch { /* Watches still work with session storage disabled. */ } - const stopSaving = lobbyWatches.subscribe(watches => { - try { sessionStorage.setItem('electric-cherry:lobby-watches', JSON.stringify(watches)); } catch { /* Optional storage. */ } - }); void refreshFleet(); const interval = setInterval(() => { - if (document.visibilityState !== 'hidden' || get(lobbyWatches).length) void refreshFleet(); + if (document.visibilityState !== 'hidden') void refreshFleet(); }, 10_000); const onFocus = () => { if (document.visibilityState === 'visible') void refreshFleet(); }; document.addEventListener('visibilitychange', onFocus); - const check = () => { - // Never complete a watch using an operation snapshot from a failed refresh. - if (get(fleetError)) return; - const snapshot = get(fleet); - if (!snapshot.receivedAt || Date.now() - snapshot.receivedAt > 30_000) return; - const servers = get(fleetServers); - for (const watch of get(lobbyWatches)) { - if (!watchSatisfied(watch, servers.find(server => server.name === watch.server), snapshot.operations, serverTimeNow())) continue; - const message = `${serverTitle(watch.server)} ${watch.kind === 'empty' ? 'is empty' : watch.kind === 'online' ? 'is back online' : 'has finished its operation'}.`; - cancelWatch(watch.id); - toast(message, { variant: 'success', ttl: 0, href: watch.href }); - } - }; - const unsubscribe = fleetServers.subscribe(check); stop = () => { clearInterval(interval); document.removeEventListener('visibilitychange', onFocus); - unsubscribe(); - stopSaving(); stop = null; }; return stop; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 2e2219b..4c1e5d9 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -9,7 +9,7 @@ import SectionHeader from '$ui/components/primitives/SectionHeader.svelte'; import EmptyState from '$ui/components/primitives/EmptyState.svelte'; import StatusPip from '$ui/components/primitives/StatusPip.svelte'; - import { fleet, fleetServers, fleetError, fleetRefreshing, fleetIntegrations, refreshFleet, lobbyWatches, followLobby, cancelWatch } from '$ui/stores/fleet.js'; + import { fleet, fleetServers, fleetError, fleetRefreshing, fleetIntegrations, refreshFleet } from '$ui/stores/fleet.js'; import { telemetryByServer, uiNow } from '$ui/stores/servers.js'; import { currentRound, fleetIdentity, freshServer, matchesFleet } from '$lib/fleet-overview.js'; import type { FleetServer, FleetFilter, FleetSort } from '$lib/fleet-overview.js'; @@ -119,16 +119,6 @@ function launch(selection: { game: ZombieGame; slug: string } | null = null) { initialGame = selection?.game ?? 't5'; initialMapSlug = selection?.slug ?? ''; deployOpen = true; } - function watch(server: FleetServer, event: Event) { - const input = event.currentTarget as HTMLSelectElement; - const value = input.value; - input.value = ''; - if (value === 'empty' || value === 'online') followLobby(server.name, value); - else { - const operation = activeOperations.find(op => op.key === value); - if (operation) followLobby(server.name, 'operation', operation); - } - } function resetFilters() { query = ''; filter = 'all'; game = ''; } @@ -183,18 +173,9 @@ {/if} - {#if activeOperations.length || $lobbyWatches.length} + {#if activeOperations.length}
- Activity · {activeOperations.length} operations{ $lobbyWatches.length ? ` · ${$lobbyWatches.length} lobby watches` : ''}{attentionOperations.length ? ` · ${attentionOperations.length} need attention` : ''} - {#if $lobbyWatches.length} -
-
Following {$lobbyWatches.length} {$lobbyWatches.length === 1 ? 'condition' : 'conditions'}Notifications stay active as you navigate. Keep this app open.
- {#each $lobbyWatches as item (item.id)} - - {/each} -
- {/if} - + Activity · {activeOperations.length} operations{attentionOperations.length ? ` · ${attentionOperations.length} need attention` : ''}
{#each activeOperations as operation (operation.key)} {serverTitle(operation.server)}{operation.label}{operation.detail} {/each}
@@ -269,14 +250,6 @@ {#snippet serverActions(server: FleetServer)} - {/snippet}