This commit is contained in:
@@ -1,17 +1,12 @@
|
||||
import type { ProvisioningAction } from './provisioning.js';
|
||||
import { provisioningStates, type ProvisioningAction } from './provisioning.js';
|
||||
import type { ProvisioningDeployment } from './provisioning-detail.js';
|
||||
import type { ProvisioningSource } from './provisioning-detail.js';
|
||||
|
||||
export const provisioningListStatuses = [
|
||||
...provisioningStates,
|
||||
// Retain the combined filter for existing links and API clients.
|
||||
'pending',
|
||||
'running',
|
||||
'awaiting-health',
|
||||
'done',
|
||||
'attention',
|
||||
'failed',
|
||||
'interrupted',
|
||||
'warning',
|
||||
'cancelled'
|
||||
'attention'
|
||||
] as const;
|
||||
export const provisioningListSources = [
|
||||
'manual',
|
||||
@@ -94,6 +89,7 @@ export type ProvisioningListRow = Pick<
|
||||
phase?: string | null;
|
||||
phaseStartedAt?: string | null;
|
||||
waitCode?: string | null;
|
||||
waitMessage?: string | null;
|
||||
};
|
||||
|
||||
export interface ProvisioningListOptions {
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
export const activeProvisioningStates = ['queued', 'waiting', 'running', 'awaiting-health'];
|
||||
export const provisioningStates = [
|
||||
'queued',
|
||||
'waiting',
|
||||
'running',
|
||||
'awaiting-health',
|
||||
'done',
|
||||
'warning',
|
||||
'failed',
|
||||
'interrupted',
|
||||
'cancelled'
|
||||
] as const;
|
||||
|
||||
export const provisioningActions = [
|
||||
'create',
|
||||
'start',
|
||||
|
||||
@@ -25,7 +25,6 @@ import { committedRebalanceStartup } from './concurrency.js';
|
||||
import { nativeDeployment } from '../backends/native.js';
|
||||
import { safetyOverride, safetyOverrideAvailability } from './safety-override.js';
|
||||
import { serverTitle } from '../../ui/server-labels.js';
|
||||
import { provisioningQueueStatus } from '../../ui/provisioning-queue.js';
|
||||
import {
|
||||
provisioningPageSize,
|
||||
provisioningPageOffset,
|
||||
@@ -617,6 +616,7 @@ const deploymentListColumns = `d.id, d.server, d.server_deleted_at, d.action, d.
|
||||
d.started_at, d.finished_at, d.triggered_by, d.requested_by, d.error, d.warnings,
|
||||
d.phase_key AS phase, d.phase_started_at AS phaseStartedAt,
|
||||
CASE WHEN json_valid(d.observation_json) THEN json_extract(d.observation_json,'$.blocker.code') END AS waitCode,
|
||||
CASE WHEN json_valid(d.observation_json) THEN json_extract(d.observation_json,'$.blocker.message') END AS waitMessage,
|
||||
(d.server_deleted_at IS NULL AND EXISTS(SELECT 1 FROM servers s WHERE s.name = d.server AND s.destroyed_at IS NULL)) AS server_exists,
|
||||
(d.server_deleted_at IS NULL AND d.action = 'destroy' AND d.state = 'waiting' AND d.stage != ?) AS canCancelRemoval,
|
||||
(d.server_deleted_at IS NULL AND d.state IN ('queued','waiting','running','awaiting-health','interrupted')
|
||||
@@ -674,28 +674,9 @@ function deploymentListQuery(options: ProvisioningListOptions) {
|
||||
}
|
||||
|
||||
function activeDeploymentRank(row: ProvisioningListRow): number {
|
||||
const { group } = provisioningQueueStatus(row);
|
||||
if (group === 'verifying') return 0;
|
||||
if (group === 'running') return 1;
|
||||
// Launch preparation keeps its queued/waiting state until command submission.
|
||||
if (row.phase === 'prepare' && !row.waitCode) return 1;
|
||||
// Remote file staging currently records its progress only in the stage message.
|
||||
if (
|
||||
row.waitCode === 'native-host' &&
|
||||
[
|
||||
'Staging and verifying files on the host.',
|
||||
'Waiting for verified remote file staging.'
|
||||
].includes(row.stage)
|
||||
)
|
||||
return 1;
|
||||
if (group === 'queued') return 3;
|
||||
// A submitted command remains in progress while its agent result is pending.
|
||||
if (
|
||||
row.state === 'running' &&
|
||||
row.phase === 'container-action' &&
|
||||
row.waitCode === 'native-host'
|
||||
)
|
||||
return 1;
|
||||
if (row.state === 'awaiting-health') return 0;
|
||||
if (row.state === 'running') return 1;
|
||||
if (row.state === 'queued') return 3;
|
||||
return 2;
|
||||
}
|
||||
|
||||
@@ -714,7 +695,7 @@ function deploymentListRows(
|
||||
...(active ? [] : [query.limit, query.offset])
|
||||
) as ProvisioningListRow[];
|
||||
if (!rows.length) return rows;
|
||||
// Stable sorting keeps request/insertion order within each progress group.
|
||||
// Stable sorting keeps request/insertion order within each lifecycle state.
|
||||
if (active)
|
||||
rows.sort((left, right) => activeDeploymentRank(left) - activeDeploymentRank(right));
|
||||
const links = db()
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import Badge from './primitives/Badge.svelte';
|
||||
import StatusPip from './primitives/StatusPip.svelte';
|
||||
import { provisioningStatus } from '$ui/provisioning-queue.js';
|
||||
|
||||
let { state, size = 'sm' }: { state: string; size?: 'sm' | 'md' } = $props();
|
||||
const status = $derived(provisioningStatus(state));
|
||||
</script>
|
||||
|
||||
<Badge variant={status.tone} {size}><StatusPip tone={status.tone} size="sm" />{status.label}</Badge>
|
||||
@@ -13,7 +13,7 @@
|
||||
import Badge from '../primitives/Badge.svelte';
|
||||
import Button from '../primitives/Button.svelte';
|
||||
import Stat from '../primitives/Stat.svelte';
|
||||
import StatusBadge from '../primitives/StatusBadge.svelte';
|
||||
import ProvisioningStatusBadge from '../ProvisioningStatusBadge.svelte';
|
||||
import Tabs from '../primitives/Tabs.svelte';
|
||||
import ObservationTime from './ObservationTime.svelte';
|
||||
|
||||
@@ -207,7 +207,7 @@
|
||||
{member ? lobbyLabel(member.name) : 'lobby'}{needsAttention
|
||||
? ' job'
|
||||
: ''}</strong
|
||||
><StatusBadge state={operation.state} />
|
||||
><ProvisioningStatusBadge state={operation.state} />
|
||||
</div>
|
||||
<p>{operation.stage || 'Waiting for the next provisioning update.'}</p>
|
||||
</div>
|
||||
@@ -339,7 +339,7 @@
|
||||
: 'Scaling evaluated'}{item.server
|
||||
? ` · ${lobbyLabel(item.server)}`
|
||||
: ''}</strong
|
||||
>{#if item.operationState}<StatusBadge
|
||||
>{#if item.operationState}<ProvisioningStatusBadge
|
||||
state={item.operationState}
|
||||
/>{/if}
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import {
|
||||
actionCompleted,
|
||||
actionLabels,
|
||||
activeProvisioningStates,
|
||||
provisioningActions,
|
||||
type ProvisioningAction
|
||||
} from '$lib/provisioning.js';
|
||||
@@ -37,7 +38,7 @@ export function provisioningRequester(
|
||||
);
|
||||
}
|
||||
|
||||
export const activeProvisioningStates = ['queued', 'waiting', 'running', 'awaiting-health'];
|
||||
export { activeProvisioningStates } from '$lib/provisioning.js';
|
||||
|
||||
/** A finished restart can still release its recovery slot or hand it to a rebuild. */
|
||||
export function provisioningNeedsRefresh(detail: ProvisioningDetail): boolean {
|
||||
@@ -79,6 +80,33 @@ const phaseLabels: Record<ProvisioningPhase, string> = {
|
||||
'verify-plutonium': 'Verify Plutonium revision',
|
||||
'verify-stopped': 'Confirm container is stopped'
|
||||
};
|
||||
|
||||
export function provisioningPhaseLabel(
|
||||
phase: ProvisioningPhase,
|
||||
action: ProvisioningAction
|
||||
): string {
|
||||
if (phase === 'container-action')
|
||||
return action === 'destroy'
|
||||
? 'Remove server instance'
|
||||
: `${actionLabels[action]} container`;
|
||||
return phaseLabels[phase];
|
||||
}
|
||||
|
||||
/** Keep the current step and recorded explanation separate from the lifecycle badge. */
|
||||
export function provisioningContext(row: {
|
||||
state: string;
|
||||
action: ProvisioningAction;
|
||||
stage: string;
|
||||
phase?: string | null;
|
||||
waitMessage?: string | null;
|
||||
}): { step: string; detail: string | null } | null {
|
||||
if (!activeProvisioningStates.includes(row.state)) return null;
|
||||
const step = provisioningPhases.includes(row.phase as ProvisioningPhase)
|
||||
? provisioningPhaseLabel(row.phase as ProvisioningPhase, row.action)
|
||||
: row.stage;
|
||||
const detail = row.waitMessage || row.stage;
|
||||
return { step, detail: detail && detail !== step ? detail : null };
|
||||
}
|
||||
type GroupDefinition = { key: string; label: string; phases: ProvisioningPhase[] };
|
||||
const request: GroupDefinition = {
|
||||
key: 'request',
|
||||
@@ -356,6 +384,9 @@ export function provisioningOutcome(detail: ProvisioningDetail) {
|
||||
description = progress.blocker.automatic
|
||||
? 'The worker will recheck this condition and continue automatically when eligible.'
|
||||
: 'Review the blocking condition below before this operation can continue.';
|
||||
else if (progress?.phase === 'prepare' && ['queued', 'waiting'].includes(operation.state))
|
||||
description =
|
||||
'Preparing the files needed for the host command. The command has not been submitted yet.';
|
||||
else if (operation.state === 'queued')
|
||||
description =
|
||||
'The request is saved and awaiting provisioning evaluation. No capacity blocker has been recorded.';
|
||||
|
||||
@@ -1,64 +1,11 @@
|
||||
import type { ProvisioningListRow } from '../provisioning-list.js';
|
||||
import { statusPresentation, type StatusTone } from './status.js';
|
||||
import { statusPresentation, type StatusPresentation } from './status.js';
|
||||
|
||||
export function provisioningQueueStatus(
|
||||
row: Pick<ProvisioningListRow, 'state' | 'action' | 'stage' | 'waitCode' | 'phase'>
|
||||
): {
|
||||
label: string;
|
||||
tone: StatusTone;
|
||||
group: 'queued' | 'waiting' | 'verifying' | 'running' | 'complete' | 'attention' | 'cancelled';
|
||||
} {
|
||||
if (['failed', 'interrupted', 'warning'].includes(row.state))
|
||||
return { ...statusPresentation(row.state), group: 'attention' };
|
||||
if (row.state === 'done') return { label: 'Complete', tone: 'online', group: 'complete' };
|
||||
if (row.state === 'cancelled')
|
||||
return { label: 'Cancelled', tone: 'neutral', group: 'cancelled' };
|
||||
if (row.state === 'awaiting-health')
|
||||
return { label: 'Verifying health', tone: 'info', group: 'verifying' };
|
||||
if (row.phase === 'cleanup') return { label: 'Cleaning up', tone: 'info', group: 'running' };
|
||||
const waits: Record<string, string> = {
|
||||
'startup-capacity': 'Waiting for capacity',
|
||||
'restart-capacity':
|
||||
row.action === 'restart' ? 'Queued for restart' : 'Waiting for recovery capacity',
|
||||
'operation-conflict': 'Waiting for another operation',
|
||||
'previous-recovery': 'Waiting for recovery'
|
||||
};
|
||||
if (row.waitCode && waits[row.waitCode])
|
||||
return {
|
||||
label: waits[row.waitCode],
|
||||
tone: 'neutral',
|
||||
group: row.waitCode.endsWith('capacity') ? 'queued' : 'waiting'
|
||||
};
|
||||
if (['queued', 'waiting'].includes(row.state) || row.waitCode) {
|
||||
// Older records may have only a stage message; never infer a failure from it.
|
||||
if (/waiting for another server to recover/i.test(row.stage))
|
||||
return { label: 'Queued for restart', tone: 'neutral', group: 'queued' };
|
||||
if (
|
||||
/waiting.*(?:empty|players)|players.*remaining|waiting for \d+ player.*to leave/i.test(
|
||||
row.stage
|
||||
)
|
||||
)
|
||||
return { label: 'Waiting for empty lobby', tone: 'neutral', group: 'waiting' };
|
||||
if (/waiting for .*player count/i.test(row.stage))
|
||||
return { label: 'Checking lobby occupancy', tone: 'neutral', group: 'waiting' };
|
||||
if (/capacity/i.test(row.stage))
|
||||
return { label: 'Waiting for capacity', tone: 'neutral', group: 'queued' };
|
||||
return {
|
||||
label:
|
||||
row.state === 'queued'
|
||||
? 'Queued'
|
||||
: row.waitCode === 'eligibility'
|
||||
? 'Waiting for eligibility'
|
||||
: 'Waiting',
|
||||
tone: 'neutral',
|
||||
group: row.state === 'queued' ? 'queued' : 'waiting'
|
||||
};
|
||||
}
|
||||
return {
|
||||
label: row.phase === 'cleanup' ? 'Cleaning up' : 'In progress',
|
||||
tone: 'info',
|
||||
group: 'running'
|
||||
};
|
||||
/** Lifecycle badges depend only on state; phases and blockers are supporting text. */
|
||||
export function provisioningStatus(state: string): StatusPresentation {
|
||||
const status = statusPresentation(state);
|
||||
if (state === 'running') return { ...status, label: 'Running' };
|
||||
if (state === 'awaiting-health') return { ...status, label: 'Validating' };
|
||||
return status;
|
||||
}
|
||||
|
||||
export function provisioningWarnings(raw: string): string[] {
|
||||
|
||||
Reference in New Issue
Block a user