diff --git a/README.md b/README.md index e285b7e..461c59a 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ game-status, and selected-map verification. No token, no session, no auth freeze. If Komodo or RCON aren't configured, the sweep marks pending rows `manual` so nothing hangs forever. -## Provisioning collections and launch previews +## Collections, provisioning, and launch previews Provisioning also offers **House Party Protocol**: review the entire fleet and queue destruction in one action. This includes queued creations and reuses pending @@ -189,7 +189,12 @@ repeating Komodo deletion. Requesting destruction again also resumes that cleanu Recovery is blocked if a newer operation exists for the same server name. **Operations → Collections** includes a builder for whole server lineups: choose -multiple T5/T6 maps and set the desired total lobbies for each. **Conform fleet** +multiple T5/T6 maps and set the desired total lobbies for each. Collections also +has **Active launches** and **Launch history** tabs, with progress at +`/collections/launches/:id`. +Each server operation remains in Provisioning and links back to its collection +launch. Existing collection launch bookmarks redirect to the new location. +**Conform fleet** counts existing and queued servers and creates only the shortfall. The preview shows target/existing/missing counts and IDs, ports, and key requirements for new servers only. Already-satisfied targets create nothing; excess servers are kept. diff --git a/docs/deployment-blueprints.md b/docs/deployment-blueprints.md index 28d1ca6..e8da3f8 100644 --- a/docs/deployment-blueprints.md +++ b/docs/deployment-blueprints.md @@ -1,6 +1,8 @@ # Deployment collections -Open **Operations → Collections** to manage reusable server lineups. A +Open **Operations → Collections** to manage reusable server lineups. Use +**Active launches** to follow progress and **Launch history** to review completed +launches and failures. Both views refresh automatically. A collection describes a whole lineup: for example, two Kino lobbies, one Moon lobby, and two Town lobbies. Counts are desired fleet totals: existing and queued servers count toward each target, and only missing servers are created. @@ -105,11 +107,13 @@ A lost startup response also keeps the slot because the container may already be running. A confirmed creation failure is recorded for review, allowing later queued servers to proceed on subsequent passes without undoing successful launches. -The collection page separates ready, launching, queued, and failed servers. It +The collection launch page at `/collections/launches/:id` separates ready, +launching, queued, and failed servers. Its breadcrumb returns to Collections. It highlights the current startup, shows each waiting server's queue position, and links to earlier launches when another collection occupies the available capacity. -Every server links to its operation timeline and recovery options, and each child -operation links back to its collection. +Every server links to its Provisioning operation timeline and recovery options, +and each child operation links back to its collection launch. Individual operations +remain visible in Provisioning activity and history. ## Create and review a launch @@ -204,6 +208,9 @@ repeats container creation or deployment as part of this recovery. Collections retain the existing `/api/blueprints` endpoints and `blueprintId` / `blueprintName` fields for compatibility. Saved data needs no migration. Old `/provisioning?view=blueprints` links redirect to `/collections`. +Collection launch details live at `/collections/launches/:id`; old +`/provisioning/collections/:id` and `/deployments/collections/:id` links redirect +there while preserving query parameters. - `GET /api/blueprints`: list lineup summaries with game/map selections and counts. - `POST /api/blueprints`: save `{ name, description?, previewId }` or diff --git a/docs/provisioning.md b/docs/provisioning.md index 6c99a21..ee01193 100644 --- a/docs/provisioning.md +++ b/docs/provisioning.md @@ -1,7 +1,9 @@ # Provisioning -Open **Operations → Provisioning** for instance lifecycle work, blueprints, and -persistent activity. Provisioning owns the queues, safety checks, execution, and +Open **Operations → Provisioning** for instance lifecycle work and persistent +activity. **Operations → Collections** owns reusable lineups and collection launch +progress; their individual operations remain visible in Provisioning. +Provisioning owns the queues, safety checks, execution, and recovery for all six actions. Create a server using **Create server**. For an existing server, choose **Manage server** or open its **Server actions** menu. @@ -75,9 +77,10 @@ requester closes. Upgrades adopt existing restart intent for observation only. ## Routes and compatibility -- `/provisioning`: active operations, blueprints, and history filtered by action. +- `/provisioning`: active operations and history filtered by action. - `/provisioning/:id`: operation stages, errors, timestamps, and activity. -- `/provisioning/collections/:id`: collection progress. +- `/collections`: saved lineups, active launches, and launch history. +- `/collections/launches/:id`: collection progress, with links to each operation. - `GET /api/provisioning`: lifecycle history (`deployments` response property). - `POST /api/provisioning`: queue `{ "server": "t5_zombie_theater_1", "action": "start" }`. Supported actions are `start`, `restart`, `rebuild`, and `stop`. Returns HTTP 202 @@ -88,6 +91,8 @@ requester closes. Upgrades adopt existing restart intent for observation only. launch preview, and fleet destruction APIs. Old `/deployments` pages redirect to their `/provisioning` equivalents, preserving +query parameters. Collection launch links at `/provisioning/collections/:id` or +`/deployments/collections/:id` redirect to `/collections/launches/:id` and preserve query parameters. `/api/deployments` remains compatible, including old campaign detail endpoints. No history or IDs are renamed or discarded. diff --git a/src/lib/ui/components/DeployCollectionDialog.svelte b/src/lib/ui/components/DeployCollectionDialog.svelte index 4582771..ad90ebf 100644 --- a/src/lib/ui/components/DeployCollectionDialog.svelte +++ b/src/lib/ui/components/DeployCollectionDialog.svelte @@ -60,7 +60,7 @@ const body = await res.json(); if (!res.ok) throw new Error(body.error ?? 'Could not queue the collection.'); open = false; - if (body.collectionId) await goto(`/provisioning/collections/${encodeURIComponent(body.collectionId)}`); + if (body.collectionId) await goto(`/collections/launches/${encodeURIComponent(body.collectionId)}`); } catch (err) { error = (err as Error).message; } finally { busy = false; } } diff --git a/src/routes/collections/+page.svelte b/src/routes/collections/+page.svelte index df95744..23a6a01 100644 --- a/src/routes/collections/+page.svelte +++ b/src/routes/collections/+page.svelte @@ -3,12 +3,17 @@ import { page } from '$app/stores'; import { goto } from '$app/navigation'; import { confirmAction } from '$ui/confirm.js'; + import { pollVisible } from '$ui/poll.js'; + import { timeFormat } from '$ui/stores/time-zone.js'; import SectionHeader from '$ui/components/primitives/SectionHeader.svelte'; import Button from '$ui/components/primitives/Button.svelte'; import ListFooter from '$ui/components/primitives/ListFooter.svelte'; + import Tabs from '$ui/components/primitives/Tabs.svelte'; + import Badge from '$ui/components/primitives/Badge.svelte'; + import EmptyState from '$ui/components/primitives/EmptyState.svelte'; import BlueprintBuilder from '$ui/components/BlueprintBuilder.svelte'; import DeployCollectionDialog from '$ui/components/DeployCollectionDialog.svelte'; - import type { BlueprintSummary } from '$lib/deployment-plan.js'; + import type { BlueprintSummary, CollectionSummary } from '$lib/deployment-plan.js'; let blueprintId = $state(''); let builderOpen = $state(false); let editingBlueprint = $state(null); @@ -17,14 +22,38 @@ let blueprintsLoading = $state(true); let blueprintError = $state(''); let removing = $state(''); + let launches = $state([]); + let launchesLoading = $state(true); + let launchError = $state(''); + let launchesRefreshing = false; + let tab = $state('collections'); let limit = $state(25); - $effect(() => { limit = Math.max(25, Number($page.url.searchParams.get('limit')) || 25); }); + let launchLimit = $state(10); + const activeLaunches = $derived(launches.filter(launch => launch.active > 0)); + const launchHistory = $derived(launches.filter(launch => launch.active === 0)); + const visibleLaunches = $derived(tab === 'active' ? activeLaunches : launchHistory); + const tabs = $derived([ + { id: 'collections', label: 'Your collections' }, + { id: 'active', label: 'Active launches', badge: launchesLoading ? undefined : activeLaunches.length }, + { id: 'history', label: 'Launch history' } + ]); + $effect(() => { + const params = $page.url.searchParams; + tab = ['active', 'history'].includes(params.get('view') ?? '') ? params.get('view')! : 'collections'; + limit = Math.max(25, Number(params.get('limit')) || 25); + launchLimit = Math.max(10, Number(params.get('launches')) || 10); + }); function saveContext() { const url = new URL($page.url); if (limit > 25) url.searchParams.set('limit', String(limit)); else url.searchParams.delete('limit'); + if (tab !== 'collections') url.searchParams.set('view', tab); else url.searchParams.delete('view'); + if (launchLimit > 10) url.searchParams.set('launches', String(launchLimit)); else url.searchParams.delete('launches'); void goto(url, { replaceState: true, noScroll: true, keepFocus: true }); } - onMount(() => { void loadBlueprints(); }); + onMount(() => { + void loadBlueprints(); + return pollVisible(loadLaunches, 5000); + }); function blueprintSaved(blueprint: BlueprintSummary, launch: boolean) { blueprints = [...blueprints.filter(item => item.id !== blueprint.id), blueprint].sort((a, b) => a.name.localeCompare(b.name)); if (launch) { blueprintId = blueprint.id; collectionOpen = true; } @@ -38,6 +67,17 @@ } catch (err) { blueprintError = (err as Error).message; } finally { blueprintsLoading = false; } } + async function loadLaunches() { + if (launchesRefreshing) return; + launchesRefreshing = true; + try { + const res = await fetch('/api/deployment-collections'); + if (!res.ok) throw new Error('Could not load collection launches.'); + launches = (await res.json()).collections; + launchError = ''; + } catch (err) { launchError = (err as Error).message; } + finally { launchesLoading = false; launchesRefreshing = false; } + } async function removeBlueprint(id: string) { if (!await confirmAction({title:'Remove collection?', message:'Existing servers and launch history will be kept.', confirmLabel:'Remove collection', danger:true})) return; removing = id; blueprintError = ''; @@ -51,10 +91,14 @@ Collections | Electric Cherry
- + + {#snippet actions()}{/snippet} + -
+ { launchLimit = 10; saveContext(); }} ariaLabel="Collection views" idPrefix="collection-tabs" panelId="collection-panel" /> +
+ {#if tab === 'collections'}

Your collections {blueprints.length}

{#if blueprintError}{/if} @@ -82,13 +126,46 @@ {/if}
saveContext()} label="collections" /> + {:else} +
+

{tab === 'active' ? 'Collection launches' : 'Launch history'} {visibleLaunches.length}

+ {#if launchError}{/if} + {#if launchesLoading}

Loading collection launches…

+ {:else if visibleLaunches.length} + + {:else if !launchError} + + {/if} + saveContext()} step={10} label="launches" /> +
+ {/if} +