workspace: refine runtime and cleanup lists
This commit is contained in:
parent
c5aae4b234
commit
d8c0853d55
|
|
@ -50,6 +50,10 @@ impl Default for RuntimeOptions {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn unknown_platform_component() -> String {
|
||||||
|
"unknown".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
/// Management-plane summary for a Runtime.
|
/// Management-plane summary for a Runtime.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct RuntimeSummary {
|
pub struct RuntimeSummary {
|
||||||
|
|
@ -63,6 +67,10 @@ pub struct RuntimeSummary {
|
||||||
pub cancelled_worker_count: usize,
|
pub cancelled_worker_count: usize,
|
||||||
pub diagnostic_count: usize,
|
pub diagnostic_count: usize,
|
||||||
pub limits: RuntimeLimits,
|
pub limits: RuntimeLimits,
|
||||||
|
#[serde(default = "unknown_platform_component")]
|
||||||
|
pub os: String,
|
||||||
|
#[serde(default = "unknown_platform_component")]
|
||||||
|
pub arch: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub worker_creation_available: bool,
|
pub worker_creation_available: bool,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,8 @@ impl Runtime {
|
||||||
cancelled_worker_count,
|
cancelled_worker_count,
|
||||||
diagnostic_count: state.diagnostics.len(),
|
diagnostic_count: state.diagnostics.len(),
|
||||||
limits: state.limits.clone(),
|
limits: state.limits.clone(),
|
||||||
|
os: std::env::consts::OS.to_string(),
|
||||||
|
arch: std::env::consts::ARCH.to_string(),
|
||||||
worker_creation_available: state.execution_backend.is_some(),
|
worker_creation_available: state.execution_backend.is_some(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1765,7 +1765,9 @@ impl RemoteRuntimeConfig {
|
||||||
display_name: display_name.into(),
|
display_name: display_name.into(),
|
||||||
base_url: base_url.into(),
|
base_url: base_url.into(),
|
||||||
bearer_token,
|
bearer_token,
|
||||||
cached_capabilities: remote_runtime_capabilities(200, false, false),
|
cached_capabilities: remote_runtime_capabilities(
|
||||||
|
200, false, false, "unknown", "unknown",
|
||||||
|
),
|
||||||
cached_status: "configured".to_string(),
|
cached_status: "configured".to_string(),
|
||||||
timeout: Duration::from_secs(10),
|
timeout: Duration::from_secs(10),
|
||||||
}
|
}
|
||||||
|
|
@ -2033,6 +2035,8 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
|
||||||
limit,
|
limit,
|
||||||
true,
|
true,
|
||||||
response.runtime.worker_creation_available,
|
response.runtime.worker_creation_available,
|
||||||
|
response.runtime.os,
|
||||||
|
response.runtime.arch,
|
||||||
),
|
),
|
||||||
diagnostics: Vec::new(),
|
diagnostics: Vec::new(),
|
||||||
},
|
},
|
||||||
|
|
@ -2066,7 +2070,7 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
|
||||||
status: "configured".to_string(),
|
status: "configured".to_string(),
|
||||||
observed_at: Utc::now().to_rfc3339(),
|
observed_at: Utc::now().to_rfc3339(),
|
||||||
last_seen_at: None,
|
last_seen_at: None,
|
||||||
capabilities: remote_runtime_capabilities(limit, true, false),
|
capabilities: remote_runtime_capabilities(limit, true, false, "unknown", "unknown"),
|
||||||
diagnostics: Vec::new(),
|
diagnostics: Vec::new(),
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
|
|
@ -2872,6 +2876,8 @@ fn remote_runtime_capabilities(
|
||||||
limit: usize,
|
limit: usize,
|
||||||
available: bool,
|
available: bool,
|
||||||
worker_creation_available: bool,
|
worker_creation_available: bool,
|
||||||
|
os: impl Into<String>,
|
||||||
|
arch: impl Into<String>,
|
||||||
) -> RuntimeCapabilitySummary {
|
) -> RuntimeCapabilitySummary {
|
||||||
RuntimeCapabilitySummary {
|
RuntimeCapabilitySummary {
|
||||||
can_list_hosts: true,
|
can_list_hosts: true,
|
||||||
|
|
@ -2887,8 +2893,8 @@ fn remote_runtime_capabilities(
|
||||||
supports_backend_internal_tools: false,
|
supports_backend_internal_tools: false,
|
||||||
workspace_scope: "remote_runtime_backend_private".to_string(),
|
workspace_scope: "remote_runtime_backend_private".to_string(),
|
||||||
max_workers: limit,
|
max_workers: limit,
|
||||||
os: "remote".to_string(),
|
os: os.into(),
|
||||||
arch: "remote".to_string(),
|
arch: arch.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,10 +77,6 @@
|
||||||
<dt>Runtime</dt>
|
<dt>Runtime</dt>
|
||||||
<dd><code>{host.runtime_id}</code></dd>
|
<dd><code>{host.runtime_id}</code></dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<dt>Scope</dt>
|
|
||||||
<dd>{host.capabilities.workspace_scope}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<dt>Platform</dt>
|
<dt>Platform</dt>
|
||||||
<dd>{host.capabilities.os} / {host.capabilities.arch}</dd>
|
<dd>{host.capabilities.os} / {host.capabilities.arch}</dd>
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,6 @@
|
||||||
|
|
||||||
let { data }: PageProps = $props();
|
let { data }: PageProps = $props();
|
||||||
|
|
||||||
function runtimeScope(runtime: Runtime): string {
|
|
||||||
return runtime.capabilities.workspace_scope;
|
|
||||||
}
|
|
||||||
|
|
||||||
function runtimePlatform(runtime: Runtime): string {
|
function runtimePlatform(runtime: Runtime): string {
|
||||||
return `${runtime.capabilities.os} / ${runtime.capabilities.arch}`;
|
return `${runtime.capabilities.os} / ${runtime.capabilities.arch}`;
|
||||||
}
|
}
|
||||||
|
|
@ -40,7 +36,6 @@
|
||||||
<th>Runtime</th>
|
<th>Runtime</th>
|
||||||
<th>Kind</th>
|
<th>Kind</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Scope</th>
|
|
||||||
<th>Platform</th>
|
<th>Platform</th>
|
||||||
<th>Capacity</th>
|
<th>Capacity</th>
|
||||||
<th>Workdirs</th>
|
<th>Workdirs</th>
|
||||||
|
|
@ -55,7 +50,6 @@
|
||||||
</td>
|
</td>
|
||||||
<td>{runtime.kind}</td>
|
<td>{runtime.kind}</td>
|
||||||
<td>{runtime.status}</td>
|
<td>{runtime.status}</td>
|
||||||
<td>{runtimeScope(runtime)}</td>
|
|
||||||
<td>{runtimePlatform(runtime)}</td>
|
<td>{runtimePlatform(runtime)}</td>
|
||||||
<td>{runtime.capabilities.max_workers} workers</td>
|
<td>{runtime.capabilities.max_workers} workers</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,25 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { workspaceApiPath } from '$lib/workspace-api/http';
|
import { workspaceApiPath } from '$lib/workspace-api/http';
|
||||||
import type { CleanupWorkdirCandidate, WorkingDirectorySummary } from '$lib/workspace-sidebar/types';
|
import type {
|
||||||
|
CleanupWorkdirCandidate,
|
||||||
|
RuntimeCleanupExecutionResponse,
|
||||||
|
RuntimeCleanupPlanResponse,
|
||||||
|
WorkingDirectorySummary,
|
||||||
|
} from '$lib/workspace-sidebar/types';
|
||||||
import type { PageProps } from './$types';
|
import type { PageProps } from './$types';
|
||||||
|
|
||||||
let { data }: PageProps = $props();
|
let { data }: PageProps = $props();
|
||||||
let selectedCleanupTargets = $state(new Set<string>());
|
|
||||||
let selectedWorkerCleanupTargets = $state(new Set<string>());
|
|
||||||
let confirmedDirtyTargets = $state(new Set<string>());
|
|
||||||
let cleanupStatus = $state<string | null>(null);
|
let cleanupStatus = $state<string | null>(null);
|
||||||
let cleanupBusy = $state(false);
|
let cleanupBusyTarget = $state<string | null>(null);
|
||||||
let cleanupCandidates = $derived(data.cleanupPlan?.workdirs ?? []);
|
let cleanupPlan = $state<RuntimeCleanupPlanResponse | null>(null);
|
||||||
let workerCleanupCandidates = $derived(data.cleanupPlan?.workers ?? []);
|
|
||||||
let runtimeLabel = $derived(
|
let runtimeLabel = $derived(
|
||||||
data.runtimes?.items.find((runtime) => runtime.runtime_id === data.runtimeId)?.label ?? data.runtimeId,
|
data.runtimes?.items.find((runtime) => runtime.runtime_id === data.runtimeId)?.label ?? data.runtimeId,
|
||||||
);
|
);
|
||||||
|
let cleanupCandidates = $derived(cleanupPlan?.workdirs ?? []);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
cleanupPlan = data.cleanupPlan ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
function commitLabel(workdir: WorkingDirectorySummary): string {
|
function commitLabel(workdir: WorkingDirectorySummary): string {
|
||||||
return workdir.resolved_commit ? workdir.resolved_commit.slice(0, 12) : '—';
|
return workdir.resolved_commit ? workdir.resolved_commit.slice(0, 12) : '—';
|
||||||
|
|
@ -25,36 +31,23 @@
|
||||||
|
|
||||||
function cleanupLabel(candidate: CleanupWorkdirCandidate): string {
|
function cleanupLabel(candidate: CleanupWorkdirCandidate): string {
|
||||||
if (candidate.action === 'workdir_dirty_discard') {
|
if (candidate.action === 'workdir_dirty_discard') {
|
||||||
return candidate.cleanliness === 'dirty' ? 'Discard dirty workdir' : 'Discard unknown-state workdir';
|
return candidate.cleanliness === 'dirty' ? 'Discard' : 'Discard unknown';
|
||||||
}
|
}
|
||||||
if (candidate.action === 'workdir_record_delete') return 'Delete missing/removed record';
|
if (candidate.action === 'workdir_record_delete') return 'Delete record';
|
||||||
return 'Clean up verified-clean workdir';
|
return 'Clean up';
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSelected(targetId: string): void {
|
function cleanupCandidate(workdir: WorkingDirectorySummary): CleanupWorkdirCandidate | undefined {
|
||||||
const next = new Set(selectedCleanupTargets);
|
return cleanupCandidates.find((candidate) => candidate.workdir_id === workdir.working_directory_id);
|
||||||
if (next.has(targetId)) next.delete(targetId);
|
|
||||||
else next.add(targetId);
|
|
||||||
selectedCleanupTargets = next;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleWorkerSelected(targetId: string): void {
|
async function executeWorkdirCleanup(candidate: CleanupWorkdirCandidate): Promise<void> {
|
||||||
const next = new Set(selectedWorkerCleanupTargets);
|
if (!cleanupPlan) return;
|
||||||
if (next.has(targetId)) next.delete(targetId);
|
if (candidate.action === 'workdir_dirty_discard') {
|
||||||
else next.add(targetId);
|
const confirmed = window.confirm(`${cleanupLabel(candidate)} ${candidate.workdir_id}? This explicitly discards the Workdir contents.`);
|
||||||
selectedWorkerCleanupTargets = next;
|
if (!confirmed) return;
|
||||||
}
|
}
|
||||||
|
cleanupBusyTarget = candidate.target_id;
|
||||||
function toggleDirtyConfirmation(targetId: string): void {
|
|
||||||
const next = new Set(confirmedDirtyTargets);
|
|
||||||
if (next.has(targetId)) next.delete(targetId);
|
|
||||||
else next.add(targetId);
|
|
||||||
confirmedDirtyTargets = next;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executeCleanup(): Promise<void> {
|
|
||||||
if (!data.cleanupPlan || (selectedCleanupTargets.size === 0 && selectedWorkerCleanupTargets.size === 0)) return;
|
|
||||||
cleanupBusy = true;
|
|
||||||
cleanupStatus = null;
|
cleanupStatus = null;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
|
|
@ -63,24 +56,22 @@
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'content-type': 'application/json' },
|
headers: { 'content-type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
expected_plan_revision: data.cleanupPlan.revision,
|
expected_plan_revision: cleanupPlan.revision,
|
||||||
expected_plan_digest: data.cleanupPlan.digest,
|
expected_plan_digest: cleanupPlan.digest,
|
||||||
worker_target_ids: Array.from(selectedWorkerCleanupTargets),
|
worker_target_ids: [],
|
||||||
workdir_target_ids: Array.from(selectedCleanupTargets),
|
workdir_target_ids: [candidate.target_id],
|
||||||
confirm_dirty_discard_target_ids: Array.from(confirmedDirtyTargets),
|
confirm_dirty_discard_target_ids: candidate.action === 'workdir_dirty_discard' ? [candidate.target_id] : [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const payload = await response.json().catch(() => null);
|
const payload = (await response.json().catch(() => null)) as RuntimeCleanupExecutionResponse | { message?: string; error?: string } | null;
|
||||||
if (!response.ok) throw new Error(payload?.message ?? payload?.error ?? response.statusText);
|
if (!response.ok) throw new Error(payload && 'message' in payload ? (payload.message ?? payload.error) : response.statusText);
|
||||||
cleanupStatus = `Executed ${payload?.results?.length ?? 0} cleanup action(s). Refresh to see the latest plan.`;
|
if (payload && 'plan_after' in payload) cleanupPlan = payload.plan_after;
|
||||||
selectedCleanupTargets = new Set();
|
cleanupStatus = `Executed cleanup for ${candidate.workdir_id}. Refresh to see the latest Workdir list.`;
|
||||||
selectedWorkerCleanupTargets = new Set();
|
|
||||||
confirmedDirtyTargets = new Set();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
cleanupStatus = error instanceof Error ? error.message : 'Cleanup failed';
|
cleanupStatus = error instanceof Error ? error.message : 'Workdir cleanup failed';
|
||||||
} finally {
|
} finally {
|
||||||
cleanupBusy = false;
|
cleanupBusyTarget = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -96,6 +87,8 @@
|
||||||
<p class="breadcrumb"><a href={`/w/${data.workspaceId}/runtimes`}>Runtimes</a> / {runtimeLabel}</p>
|
<p class="breadcrumb"><a href={`/w/${data.workspaceId}/runtimes`}>Runtimes</a> / {runtimeLabel}</p>
|
||||||
<h1 id="workdirs-heading">Workdirs</h1>
|
<h1 id="workdirs-heading">Workdirs</h1>
|
||||||
<p>Workdirs owned by <code>{data.runtimeId}</code>.</p>
|
<p>Workdirs owned by <code>{data.runtimeId}</code>.</p>
|
||||||
|
{#if data.cleanupPlanError}<p class="section-state error">{data.cleanupPlanError}</p>{/if}
|
||||||
|
{#if cleanupStatus}<p>{cleanupStatus}</p>{/if}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
@ -106,68 +99,6 @@
|
||||||
{:else if data.workdirs.items.length === 0}
|
{:else if data.workdirs.items.length === 0}
|
||||||
<p class="section-state">No workdirs are visible for this Runtime.</p>
|
<p class="section-state">No workdirs are visible for this Runtime.</p>
|
||||||
{:else}
|
{:else}
|
||||||
<section class="cleanup-panel">
|
|
||||||
<div>
|
|
||||||
<h2>Manual cleanup preview</h2>
|
|
||||||
<p class="muted">Select explicit Workdir targets. Raw Runtime paths are intentionally not shown.</p>
|
|
||||||
</div>
|
|
||||||
{#if data.cleanupPlanError}
|
|
||||||
<p class="section-state error">{data.cleanupPlanError}</p>
|
|
||||||
{:else if cleanupCandidates.length === 0 && workerCleanupCandidates.length === 0}
|
|
||||||
<p>No cleanup candidates.</p>
|
|
||||||
{:else}
|
|
||||||
<div class="cleanup-list">
|
|
||||||
{#each workerCleanupCandidates as candidate (candidate.target_id)}
|
|
||||||
<label class:blocked={!!candidate.blocking_reason}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={selectedWorkerCleanupTargets.has(candidate.target_id)}
|
|
||||||
disabled={!!candidate.blocking_reason}
|
|
||||||
onchange={() => toggleWorkerSelected(candidate.target_id)}
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
<strong>Delete Worker registry row:</strong> <code>{candidate.runtime_worker_id}</code>
|
|
||||||
<small>{candidate.retention_state}; linked Workdirs {candidate.linked_workdir_ids.length}</small>
|
|
||||||
{#if candidate.blocking_reason}<small class="error">Blocked: {candidate.blocking_reason}</small>{/if}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
{/each}
|
|
||||||
{#each cleanupCandidates as candidate (candidate.target_id)}
|
|
||||||
<label class:blocked={!!candidate.blocking_reason} class:dirty={candidate.action === 'workdir_dirty_discard'}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={selectedCleanupTargets.has(candidate.target_id)}
|
|
||||||
disabled={!!candidate.blocking_reason}
|
|
||||||
onchange={() => toggleSelected(candidate.target_id)}
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
<strong>{cleanupLabel(candidate)}:</strong> <code>{candidate.workdir_id}</code>
|
|
||||||
<small>
|
|
||||||
file {candidate.file_status}; {candidate.cleanliness}; linked Workers {candidate.linked_worker_ids.length}
|
|
||||||
</small>
|
|
||||||
{#if candidate.blocking_reason}
|
|
||||||
<small class="error">Blocked: {candidate.blocking_reason}</small>
|
|
||||||
{:else if candidate.action === 'workdir_dirty_discard'}
|
|
||||||
<label class="confirm-dirty">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={confirmedDirtyTargets.has(candidate.target_id)}
|
|
||||||
onchange={() => toggleDirtyConfirmation(candidate.target_id)}
|
|
||||||
/>
|
|
||||||
Confirm discard
|
|
||||||
</label>
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
<button type="button" onclick={executeCleanup} disabled={cleanupBusy || (selectedCleanupTargets.size === 0 && selectedWorkerCleanupTargets.size === 0)}>
|
|
||||||
{cleanupBusy ? 'Executing…' : 'Execute selected cleanup'}
|
|
||||||
</button>
|
|
||||||
{#if cleanupStatus}<p>{cleanupStatus}</p>{/if}
|
|
||||||
{/if}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table class="workdirs-table">
|
<table class="workdirs-table">
|
||||||
<thead>
|
<thead>
|
||||||
|
|
@ -178,10 +109,12 @@
|
||||||
<th>Commit</th>
|
<th>Commit</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Policy</th>
|
<th>Policy</th>
|
||||||
|
<th>Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each data.workdirs.items as workdir}
|
{#each data.workdirs.items as workdir}
|
||||||
|
{@const cleanup = cleanupCandidate(workdir)}
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>{workdir.working_directory_id}</code></td>
|
<td><code>{workdir.working_directory_id}</code></td>
|
||||||
<td>{workdir.repository_id}</td>
|
<td>{workdir.repository_id}</td>
|
||||||
|
|
@ -192,6 +125,21 @@
|
||||||
<span>{workdir.dirty_state_policy}</span>
|
<span>{workdir.dirty_state_policy}</span>
|
||||||
<small>{workdir.cleanup_policy}</small>
|
<small>{workdir.cleanup_policy}</small>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{#if cleanup}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!!cleanup.blocking_reason || cleanupBusyTarget === cleanup.target_id}
|
||||||
|
title={cleanup.blocking_reason ?? cleanup.reason}
|
||||||
|
onclick={() => executeWorkdirCleanup(cleanup)}
|
||||||
|
>
|
||||||
|
{cleanupBusyTarget === cleanup.target_id ? 'Executing…' : cleanupLabel(cleanup)}
|
||||||
|
</button>
|
||||||
|
{#if cleanup.blocking_reason}<small class="error">{cleanup.blocking_reason}</small>{/if}
|
||||||
|
{:else}
|
||||||
|
<span class="muted">—</span>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,20 @@
|
||||||
import { workspaceApiPath } from '$lib/workspace-api/http';
|
import { workspaceApiPath } from '$lib/workspace-api/http';
|
||||||
import { workerConsoleHref } from '$lib/workspace-console/model';
|
import { workerConsoleHref } from '$lib/workspace-console/model';
|
||||||
import { canOpenWorkerConsole } from '$lib/workspace-sidebar/workers';
|
import { canOpenWorkerConsole } from '$lib/workspace-sidebar/workers';
|
||||||
import type { Worker } from '$lib/workspace-sidebar/types';
|
import type { CleanupWorkerCandidate, RuntimeCleanupExecutionResponse, RuntimeCleanupPlanResponse, Worker } from '$lib/workspace-sidebar/types';
|
||||||
import type { PageProps } from './$types';
|
import type { PageProps } from './$types';
|
||||||
|
|
||||||
let { data }: PageProps = $props();
|
let { data }: PageProps = $props();
|
||||||
let retentionStatus = $state<string | null>(null);
|
let statusMessage = $state<string | null>(null);
|
||||||
|
let cleanupPlans = $state<Record<string, RuntimeCleanupPlanResponse>>({});
|
||||||
|
let busyCleanupTarget = $state<string | null>(null);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
cleanupPlans = data.cleanupPlans;
|
||||||
|
});
|
||||||
|
|
||||||
async function setPinned(worker: Worker, pinned: boolean): Promise<void> {
|
async function setPinned(worker: Worker, pinned: boolean): Promise<void> {
|
||||||
retentionStatus = null;
|
statusMessage = null;
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
workspaceApiPath(
|
workspaceApiPath(
|
||||||
data.workspaceId,
|
data.workspaceId,
|
||||||
|
|
@ -19,12 +25,56 @@
|
||||||
);
|
);
|
||||||
const payload = await response.json().catch(() => null);
|
const payload = await response.json().catch(() => null);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
retentionStatus = payload?.message ?? payload?.error ?? response.statusText;
|
statusMessage = payload?.message ?? payload?.error ?? response.statusText;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
worker.pinned = Boolean(payload?.pinned);
|
worker.pinned = Boolean(payload?.pinned);
|
||||||
worker.retention_state = payload?.retention_state ?? (worker.pinned ? 'pinned' : 'normal');
|
worker.retention_state = payload?.retention_state ?? (worker.pinned ? 'pinned' : 'normal');
|
||||||
retentionStatus = `${worker.label} ${worker.pinned ? 'pinned' : 'unpinned'}.`;
|
statusMessage = `${worker.label} ${worker.pinned ? 'pinned' : 'unpinned'}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupCandidate(worker: Worker): CleanupWorkerCandidate | undefined {
|
||||||
|
return cleanupPlans?.[worker.runtime_id]?.workers.find(
|
||||||
|
(candidate) => candidate.runtime_id === worker.runtime_id && candidate.runtime_worker_id === worker.worker_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteWorkerRegistryRow(worker: Worker, candidate: CleanupWorkerCandidate): Promise<void> {
|
||||||
|
if (!cleanupPlans?.[worker.runtime_id]) return;
|
||||||
|
statusMessage = null;
|
||||||
|
busyCleanupTarget = candidate.target_id;
|
||||||
|
try {
|
||||||
|
const plan = cleanupPlans[worker.runtime_id];
|
||||||
|
const response = await fetch(
|
||||||
|
workspaceApiPath(data.workspaceId, `/runtimes/${encodeURIComponent(worker.runtime_id)}/cleanup-executions`),
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
expected_plan_revision: plan.revision,
|
||||||
|
expected_plan_digest: plan.digest,
|
||||||
|
worker_target_ids: [candidate.target_id],
|
||||||
|
workdir_target_ids: [],
|
||||||
|
confirm_dirty_discard_target_ids: [],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const payload = (await response.json().catch(() => null)) as RuntimeCleanupExecutionResponse | { message?: string; error?: string } | null;
|
||||||
|
if (!response.ok) throw new Error(payload && 'message' in payload ? (payload.message ?? payload.error) : response.statusText);
|
||||||
|
if (payload && 'plan_after' in payload) {
|
||||||
|
cleanupPlans = { ...cleanupPlans, [worker.runtime_id]: payload.plan_after };
|
||||||
|
}
|
||||||
|
if (data.workers) {
|
||||||
|
data.workers.items = data.workers.items.filter(
|
||||||
|
(item) => !(item.runtime_id === worker.runtime_id && item.worker_id === worker.worker_id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
statusMessage = `Deleted Worker registry row for ${worker.label}.`;
|
||||||
|
} catch (error) {
|
||||||
|
statusMessage = error instanceof Error ? error.message : 'Worker cleanup failed';
|
||||||
|
} finally {
|
||||||
|
busyCleanupTarget = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function workerStatus(worker: Worker): string {
|
function workerStatus(worker: Worker): string {
|
||||||
|
|
@ -54,7 +104,7 @@
|
||||||
<div>
|
<div>
|
||||||
<h1 id="workers-heading">Workers</h1>
|
<h1 id="workers-heading">Workers</h1>
|
||||||
<p>Workers running or persisted for this workspace. Pinning only updates Backend retention.</p>
|
<p>Workers running or persisted for this workspace. Pinning only updates Backend retention.</p>
|
||||||
{#if retentionStatus}<p>{retentionStatus}</p>{/if}
|
{#if statusMessage}<p>{statusMessage}</p>{/if}
|
||||||
</div>
|
</div>
|
||||||
<a class="section-action" href={`/w/${data.workspaceId}/workers/new`}>New Worker</a>
|
<a class="section-action" href={`/w/${data.workspaceId}/workers/new`}>New Worker</a>
|
||||||
</header>
|
</header>
|
||||||
|
|
@ -81,6 +131,7 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each data.workers.items as worker}
|
{#each data.workers.items as worker}
|
||||||
|
{@const cleanup = cleanupCandidate(worker)}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<strong>{worker.label}</strong>
|
<strong>{worker.label}</strong>
|
||||||
|
|
@ -100,6 +151,16 @@
|
||||||
<button type="button" onclick={() => setPinned(worker, !worker.pinned)}>
|
<button type="button" onclick={() => setPinned(worker, !worker.pinned)}>
|
||||||
{worker.pinned ? 'Unpin' : 'Pin'}
|
{worker.pinned ? 'Unpin' : 'Pin'}
|
||||||
</button>
|
</button>
|
||||||
|
{#if cleanup}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!!cleanup.blocking_reason || busyCleanupTarget === cleanup.target_id}
|
||||||
|
title={cleanup.blocking_reason ?? cleanup.reason}
|
||||||
|
onclick={() => deleteWorkerRegistryRow(worker, cleanup)}
|
||||||
|
>
|
||||||
|
{busyCleanupTarget === cleanup.target_id ? 'Deleting…' : 'Delete row'}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { loadJson, workspaceApiPath } from "$lib/workspace-api/http";
|
import { loadJson, workspaceApiPath } from "$lib/workspace-api/http";
|
||||||
import type { ListResponse, Worker } from "$lib/workspace-sidebar/types";
|
import type { ListResponse, RuntimeCleanupPlanResponse, Worker } from "$lib/workspace-sidebar/types";
|
||||||
import type { PageLoad } from "./$types";
|
import type { PageLoad } from "./$types";
|
||||||
|
|
||||||
export const load: PageLoad = async ({ fetch, params }) => {
|
export const load: PageLoad = async ({ fetch, params }) => {
|
||||||
|
|
@ -7,10 +7,28 @@ export const load: PageLoad = async ({ fetch, params }) => {
|
||||||
fetch,
|
fetch,
|
||||||
workspaceApiPath(params.workspaceId, "/workers"),
|
workspaceApiPath(params.workspaceId, "/workers"),
|
||||||
);
|
);
|
||||||
|
const runtimeIds = Array.from(new Set(workers.data?.items.map((worker) => worker.runtime_id) ?? []));
|
||||||
|
const cleanupPlanEntries = await Promise.all(
|
||||||
|
runtimeIds.map(async (runtimeId) => {
|
||||||
|
const cleanupPlan = await loadJson<RuntimeCleanupPlanResponse>(
|
||||||
|
fetch,
|
||||||
|
workspaceApiPath(params.workspaceId, `/runtimes/${encodeURIComponent(runtimeId)}/cleanup-plan`),
|
||||||
|
);
|
||||||
|
return [runtimeId, cleanupPlan] as const;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const cleanupPlans: Record<string, RuntimeCleanupPlanResponse> = {};
|
||||||
|
const cleanupPlanErrors: Record<string, string> = {};
|
||||||
|
for (const [runtimeId, cleanupPlan] of cleanupPlanEntries) {
|
||||||
|
if (cleanupPlan.data) cleanupPlans[runtimeId] = cleanupPlan.data;
|
||||||
|
if (cleanupPlan.error) cleanupPlanErrors[runtimeId] = cleanupPlan.error;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
workspaceId: params.workspaceId,
|
workspaceId: params.workspaceId,
|
||||||
workers: workers.data,
|
workers: workers.data,
|
||||||
workersError: workers.error,
|
workersError: workers.error,
|
||||||
|
cleanupPlans,
|
||||||
|
cleanupPlanErrors,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user