fix: refine workspace runtime UI
This commit is contained in:
parent
5279f8fc0d
commit
b6029220ff
33
crates/worker-runtime/README.md
Normal file
33
crates/worker-runtime/README.md
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# worker-runtime
|
||||||
|
|
||||||
|
`worker-runtime` owns the Runtime authority surface for Worker management. A Runtime process bundles Worker lifecycle management, the HTTP/WebSocket control API, and the Worker execution backend.
|
||||||
|
|
||||||
|
## Run the local Runtime server
|
||||||
|
|
||||||
|
From the repository root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run -p worker-runtime \
|
||||||
|
--features ws-server,fs-store \
|
||||||
|
--bin worker-runtime-rest-server \
|
||||||
|
-- --workspace .
|
||||||
|
```
|
||||||
|
|
||||||
|
By default the server listens on:
|
||||||
|
|
||||||
|
```text
|
||||||
|
127.0.0.1:38800
|
||||||
|
```
|
||||||
|
|
||||||
|
To bind another address explicitly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run -p worker-runtime \
|
||||||
|
--features ws-server,fs-store \
|
||||||
|
--bin worker-runtime-rest-server \
|
||||||
|
-- --workspace . --bind 127.0.0.1:38800
|
||||||
|
```
|
||||||
|
|
||||||
|
`--workspace` is currently a legacy bootstrap input for the v0 local materializer / Worker profile resolution path. It is not intended to be the long-term Runtime identity or a single-workspace binding. Future Runtime launches should receive Workspace / Repository context through Worker launch requests and config bundles instead.
|
||||||
|
|
||||||
|
The REST server is intended for a trusted Backend/proxy, not direct browser access.
|
||||||
|
|
@ -1885,11 +1885,7 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
|
||||||
true,
|
true,
|
||||||
response.runtime.worker_creation_available,
|
response.runtime.worker_creation_available,
|
||||||
),
|
),
|
||||||
diagnostics: vec![diagnostic(
|
diagnostics: Vec::new(),
|
||||||
"remote_runtime_backend_proxy",
|
|
||||||
DiagnosticSeverity::Info,
|
|
||||||
"Remote Runtime is accessed only by backend-owned REST/WS clients".to_string(),
|
|
||||||
)],
|
|
||||||
},
|
},
|
||||||
Err(diagnostic) => RuntimeSummary {
|
Err(diagnostic) => RuntimeSummary {
|
||||||
runtime_id: self.runtime_id.clone(),
|
runtime_id: self.runtime_id.clone(),
|
||||||
|
|
@ -1922,11 +1918,7 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
|
||||||
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),
|
||||||
diagnostics: vec![diagnostic(
|
diagnostics: Vec::new(),
|
||||||
"remote_runtime_backend_proxy",
|
|
||||||
DiagnosticSeverity::Info,
|
|
||||||
"Remote host endpoint and credentials are backend-private".to_string(),
|
|
||||||
)],
|
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
)
|
)
|
||||||
|
|
@ -2010,11 +2002,7 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
|
||||||
kind: "remote_runtime_worker_created".to_string(),
|
kind: "remote_runtime_worker_created".to_string(),
|
||||||
detail: "worker-runtime REST create endpoint accepted the Worker".to_string(),
|
detail: "worker-runtime REST create endpoint accepted the Worker".to_string(),
|
||||||
}],
|
}],
|
||||||
diagnostics: vec![diagnostic(
|
diagnostics: Vec::new(),
|
||||||
"remote_runtime_backend_proxy",
|
|
||||||
DiagnosticSeverity::Info,
|
|
||||||
"Remote create used a backend-owned REST client; browser-facing payload exposes only runtime_id plus worker_id".to_string(),
|
|
||||||
)],
|
|
||||||
},
|
},
|
||||||
Err(diagnostic) => WorkerSpawnResult {
|
Err(diagnostic) => WorkerSpawnResult {
|
||||||
state: WorkerOperationState::Rejected,
|
state: WorkerOperationState::Rejected,
|
||||||
|
|
|
||||||
|
|
@ -1226,28 +1226,74 @@
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: rgba(255, 255, 255, 0.03);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-runtime-card {
|
.settings-runtime-table-wrap {
|
||||||
display: grid;
|
overflow-x: auto;
|
||||||
gap: 0.75rem;
|
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
padding: 1rem;
|
|
||||||
background: var(--bg-raised);
|
background: var(--bg-raised);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-runtime-card header {
|
.settings-runtime-table {
|
||||||
display: flex;
|
width: 100%;
|
||||||
justify-content: space-between;
|
min-width: 48rem;
|
||||||
gap: 1rem;
|
border-collapse: collapse;
|
||||||
align-items: flex-start;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-runtime-card.inactive {
|
.settings-runtime-table th,
|
||||||
opacity: 0.86;
|
.settings-runtime-table td {
|
||||||
|
padding: 0.8rem 0.9rem;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-identity-list.compact {
|
.settings-runtime-table th {
|
||||||
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
|
color: var(--text-muted);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-runtime-table tr:last-child td {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-runtime-table tr.inactive {
|
||||||
|
opacity: 0.78;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-runtime-table td {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-runtime-table td > strong,
|
||||||
|
.settings-runtime-table td > span,
|
||||||
|
.settings-runtime-table td > code,
|
||||||
|
.settings-runtime-table td > small {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-runtime-table strong {
|
||||||
|
color: var(--text-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-runtime-table code {
|
||||||
|
margin-top: 0.15rem;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-runtime-table small {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-runtime-detail-row td {
|
||||||
|
padding-top: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.025);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-muted-action {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.82rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-action-row {
|
.settings-action-row {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import RepositoryTicketKanban from '$lib/workspace-pages/RepositoryTicketKanban.svelte';
|
import RepositoryTicketKanban from '$lib/workspace-pages/RepositoryTicketKanban.svelte';
|
||||||
import { workerConsoleHref } from '$lib/workspace-console/model';
|
import { workerConsoleHref } from '$lib/workspace-console/model';
|
||||||
import WorkspaceSidebar from '$lib/workspace-sidebar/WorkspaceSidebar.svelte';
|
import WorkspaceSidebar from '$lib/workspace-sidebar/WorkspaceSidebar.svelte';
|
||||||
|
|
@ -51,48 +52,48 @@
|
||||||
let route = $derived(routeFromView(view, objectiveId, repositoryId));
|
let route = $derived(routeFromView(view, objectiveId, repositoryId));
|
||||||
let currentPath = $derived(pathFromRoute(route));
|
let currentPath = $derived(pathFromRoute(route));
|
||||||
|
|
||||||
async function getJson<T>(path: string): Promise<T> {
|
async function getJson<T>(path: string, signal?: AbortSignal): Promise<T> {
|
||||||
const response = await fetch(path);
|
const response = await fetch(path, { signal });
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`GET ${path} failed: ${response.status}`);
|
throw new Error(`GET ${path} failed: ${response.status}`);
|
||||||
}
|
}
|
||||||
return response.json() as Promise<T>;
|
return response.json() as Promise<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadWorkspace() {
|
async function loadWorkspace(signal?: AbortSignal) {
|
||||||
workspaceError = null;
|
workspaceError = null;
|
||||||
try {
|
try {
|
||||||
workspace = await getJson<WorkspaceResponse>('/api/workspace');
|
workspace = await getJson<WorkspaceResponse>('/api/workspace', signal);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
workspaceError = error instanceof Error ? error.message : String(error);
|
workspaceError = error instanceof Error ? error.message : String(error);
|
||||||
workspace = null;
|
workspace = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadHosts() {
|
async function loadHosts(signal?: AbortSignal) {
|
||||||
hostsError = null;
|
hostsError = null;
|
||||||
try {
|
try {
|
||||||
hosts = await getJson<ListResponse<Host>>('/api/hosts');
|
hosts = await getJson<ListResponse<Host>>('/api/hosts', signal);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
hostsError = error instanceof Error ? error.message : String(error);
|
hostsError = error instanceof Error ? error.message : String(error);
|
||||||
hosts = null;
|
hosts = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadWorkers() {
|
async function loadWorkers(signal?: AbortSignal) {
|
||||||
workersError = null;
|
workersError = null;
|
||||||
try {
|
try {
|
||||||
workers = await getJson<ListResponse<Worker>>('/api/workers');
|
workers = await getJson<ListResponse<Worker>>('/api/workers', signal);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
workersError = error instanceof Error ? error.message : String(error);
|
workersError = error instanceof Error ? error.message : String(error);
|
||||||
workers = null;
|
workers = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadRepositories() {
|
async function loadRepositories(signal?: AbortSignal) {
|
||||||
repositoriesError = null;
|
repositoriesError = null;
|
||||||
try {
|
try {
|
||||||
repositories = await getJson<RepositoryListResponse>('/api/repositories');
|
repositories = await getJson<RepositoryListResponse>('/api/repositories', signal);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
repositoriesError = error instanceof Error ? error.message : String(error);
|
repositoriesError = error instanceof Error ? error.message : String(error);
|
||||||
repositories = null;
|
repositories = null;
|
||||||
|
|
@ -124,10 +125,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadObjectives() {
|
async function loadObjectives(signal?: AbortSignal) {
|
||||||
objectivesError = null;
|
objectivesError = null;
|
||||||
try {
|
try {
|
||||||
objectives = await getJson<ObjectiveListResponse>('/api/objectives');
|
objectives = await getJson<ObjectiveListResponse>('/api/objectives', signal);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
objectivesError = error instanceof Error ? error.message : String(error);
|
objectivesError = error instanceof Error ? error.message : String(error);
|
||||||
objectives = null;
|
objectives = null;
|
||||||
|
|
@ -189,12 +190,15 @@
|
||||||
return value ?? 'not recorded';
|
return value ?? 'not recorded';
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
onMount(() => {
|
||||||
void loadWorkspace();
|
const controller = new AbortController();
|
||||||
void loadHosts();
|
void loadWorkspace(controller.signal);
|
||||||
void loadWorkers();
|
void loadHosts(controller.signal);
|
||||||
void loadRepositories();
|
void loadWorkers(controller.signal);
|
||||||
void loadObjectives();
|
void loadRepositories(controller.signal);
|
||||||
|
void loadObjectives(controller.signal);
|
||||||
|
|
||||||
|
return () => controller.abort();
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@
|
||||||
type RemoteRuntimeTestResponse,
|
type RemoteRuntimeTestResponse,
|
||||||
type RuntimeConnectionMutationResponse,
|
type RuntimeConnectionMutationResponse,
|
||||||
type RuntimeConnectionSettingsResponse,
|
type RuntimeConnectionSettingsResponse,
|
||||||
type RuntimeConnectionSummary,
|
|
||||||
} from "./model";
|
} from "./model";
|
||||||
|
|
||||||
type RemoteAddForm = {
|
type RemoteAddForm = {
|
||||||
|
|
@ -33,6 +32,7 @@
|
||||||
let deleting = $state<string | null>(null);
|
let deleting = $state<string | null>(null);
|
||||||
let testing = $state<string | null>(null);
|
let testing = $state<string | null>(null);
|
||||||
let submitting = $state(false);
|
let submitting = $state(false);
|
||||||
|
let showAddRuntimeForm = $state(false);
|
||||||
let remoteForm = $state<RemoteAddForm>({
|
let remoteForm = $state<RemoteAddForm>({
|
||||||
runtime_id: "",
|
runtime_id: "",
|
||||||
display_name: "",
|
display_name: "",
|
||||||
|
|
@ -117,6 +117,7 @@
|
||||||
const data = (await response.json()) as RuntimeConnectionMutationResponse;
|
const data = (await response.json()) as RuntimeConnectionMutationResponse;
|
||||||
applyRuntimeMutation(data);
|
applyRuntimeMutation(data);
|
||||||
remoteForm = { runtime_id: "", display_name: "", endpoint: "" };
|
remoteForm = { runtime_id: "", display_name: "", endpoint: "" };
|
||||||
|
showAddRuntimeForm = false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
mutationMessage = err instanceof Error ? err.message : "add remote Runtime failed";
|
mutationMessage = err instanceof Error ? err.message : "add remote Runtime failed";
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -289,76 +290,138 @@
|
||||||
{:else if runtimeError}
|
{:else if runtimeError}
|
||||||
<p class="status-message error">Runtime connection settings unavailable: {runtimeError}</p>
|
<p class="status-message error">Runtime connection settings unavailable: {runtimeError}</p>
|
||||||
{:else if runtimeSettings}
|
{:else if runtimeSettings}
|
||||||
{@render RuntimeConnectionCard({ connection: runtimeSettings.embedded })}
|
<div class="settings-action-row">
|
||||||
|
<button type="button" onclick={() => showAddRuntimeForm = !showAddRuntimeForm}>
|
||||||
|
{showAddRuntimeForm ? "Cancel adding Runtime" : "Add remote Runtime"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form class="settings-runtime-form" onsubmit={(event) => { event.preventDefault(); void submitRemoteRuntime(); }}>
|
{#if showAddRuntimeForm}
|
||||||
<h3>Add remote Runtime</h3>
|
<form class="settings-runtime-form" onsubmit={(event) => { event.preventDefault(); void submitRemoteRuntime(); }}>
|
||||||
<p>Endpoint is submitted to the Backend but not echoed back in settings responses.</p>
|
<h3>Add remote Runtime</h3>
|
||||||
<label>
|
<p>Endpoint is submitted to the Backend but not echoed back in settings responses.</p>
|
||||||
<span>Runtime id</span>
|
<label>
|
||||||
<input bind:value={remoteForm.runtime_id} required maxlength="96" pattern="[A-Za-z0-9_.-]+" placeholder="team-runtime" />
|
<span>Runtime id</span>
|
||||||
</label>
|
<input bind:value={remoteForm.runtime_id} required maxlength="96" pattern="[A-Za-z0-9_.-]+" placeholder="team-runtime" />
|
||||||
<label>
|
</label>
|
||||||
<span>Display name</span>
|
<label>
|
||||||
<input bind:value={remoteForm.display_name} maxlength="80" placeholder="Team Runtime" />
|
<span>Display name</span>
|
||||||
</label>
|
<input bind:value={remoteForm.display_name} maxlength="80" placeholder="Team Runtime" />
|
||||||
<label>
|
</label>
|
||||||
<span>Endpoint</span>
|
<label>
|
||||||
<input bind:value={remoteForm.endpoint} required inputmode="url" placeholder="https://runtime.example" />
|
<span>Endpoint</span>
|
||||||
</label>
|
<input bind:value={remoteForm.endpoint} required inputmode="url" placeholder="https://runtime.example" />
|
||||||
<button type="submit" disabled={submitting}>{submitting ? "Saving…" : "Add Runtime"}</button>
|
</label>
|
||||||
</form>
|
<button type="submit" disabled={submitting}>{submitting ? "Saving…" : "Add Runtime"}</button>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if mutationMessage}
|
{#if mutationMessage}
|
||||||
<p class="status-message" class:error={mutationMessage.includes("failed")}>{mutationMessage}</p>
|
<p class="status-message" class:error={mutationMessage.includes("failed")}>{mutationMessage}</p>
|
||||||
{/if}
|
{/if}
|
||||||
{@render DiagnosticsList({ diagnostics: mutationDiagnostics })}
|
{@render DiagnosticsList({ diagnostics: mutationDiagnostics })}
|
||||||
|
|
||||||
<div class="settings-runtime-list" aria-label="Remote Runtime connections">
|
<div class="settings-runtime-list" aria-label="Runtime connections">
|
||||||
<h3>Remote Runtimes</h3>
|
<h3>Runtimes</h3>
|
||||||
|
<div class="settings-runtime-table-wrap">
|
||||||
|
<table class="settings-runtime-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Runtime</th>
|
||||||
|
<th scope="col">Source</th>
|
||||||
|
<th scope="col">Connection</th>
|
||||||
|
<th scope="col">Status</th>
|
||||||
|
<th scope="col">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class:inactive={!runtimeSettings.embedded.active}>
|
||||||
|
<td>
|
||||||
|
<strong>{runtimeSettings.embedded.display_name}</strong>
|
||||||
|
<code>{runtimeSettings.embedded.runtime_id}</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<strong>embedded</strong>
|
||||||
|
<span>Workspace backend process</span>
|
||||||
|
</td>
|
||||||
|
<td>Local Backend runtime</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge" class:success={runtimeSettings.embedded.active} class:warning={!runtimeSettings.embedded.active}>{runtimeSettings.embedded.status}</span>
|
||||||
|
{#if runtimeSettings.embedded.restart_required}
|
||||||
|
<span class="badge warning">restart required</span>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td><span class="settings-muted-action">Managed by backend</span></td>
|
||||||
|
</tr>
|
||||||
|
{#if runtimeSettings.embedded.diagnostics.length > 0}
|
||||||
|
<tr class="settings-runtime-detail-row">
|
||||||
|
<td colspan="5">{@render DiagnosticsList({ diagnostics: runtimeSettings.embedded.diagnostics })}</td>
|
||||||
|
</tr>
|
||||||
|
{/if}
|
||||||
|
{#each runtimeSettings.remotes as remote (remote.runtime_id)}
|
||||||
|
<tr class:inactive={!remote.active}>
|
||||||
|
<td>
|
||||||
|
<strong>{remote.display_name}</strong>
|
||||||
|
<code>{remote.runtime_id}</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<strong>remote</strong>
|
||||||
|
<span>Configured Runtime endpoint</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span>Endpoint: {remote.endpoint_configured ? "configured" : "not configured"}</span>
|
||||||
|
{#if remote.endpoint_configured}<small>hidden</small>{/if}
|
||||||
|
<span>Token: {remote.token_ref_configured ? "configured" : "not configured"}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge" class:success={remote.active} class:warning={!remote.active}>{remote.status}</span>
|
||||||
|
{#if remote.restart_required}
|
||||||
|
<span class="badge warning">restart required</span>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="settings-action-row">
|
||||||
|
<button type="button" onclick={() => void testRemoteRuntime(remote.runtime_id)} disabled={testing === remote.runtime_id}>
|
||||||
|
{testing === remote.runtime_id ? "Testing…" : "Test"}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="danger" onclick={() => void deleteRemoteRuntime(remote.runtime_id)} disabled={deleting === remote.runtime_id}>
|
||||||
|
{deleting === remote.runtime_id ? "Deleting…" : "Delete"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{#if remote.diagnostics.length > 0}
|
||||||
|
<tr class="settings-runtime-detail-row">
|
||||||
|
<td colspan="5">{@render DiagnosticsList({ diagnostics: remote.diagnostics })}</td>
|
||||||
|
</tr>
|
||||||
|
{/if}
|
||||||
|
{#if tests[remote.runtime_id]}
|
||||||
|
{@const test = tests[remote.runtime_id]}
|
||||||
|
{@const available = capabilityOperations(test, "available")}
|
||||||
|
{@const unchecked = capabilityOperations(test, "unknown")}
|
||||||
|
<tr class="settings-runtime-detail-row">
|
||||||
|
<td colspan="5">
|
||||||
|
<div class="settings-test-result">
|
||||||
|
<strong>Test: {test.state}</strong>
|
||||||
|
<span>{test.health_result} · {test.checked_at}</span>
|
||||||
|
<p>{test.compatibility_basis}</p>
|
||||||
|
{#if available.length > 0}
|
||||||
|
<p class="settings-test-verified">Verified areas: {available.join(', ')}</p>
|
||||||
|
{/if}
|
||||||
|
{#if unchecked.length > 0}
|
||||||
|
<p class="settings-test-verified">Unchecked warning areas: {unchecked.join(', ')}</p>
|
||||||
|
{/if}
|
||||||
|
{@render DiagnosticsList({ diagnostics: test.diagnostics })}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
{#if runtimeSettings.remotes.length === 0}
|
{#if runtimeSettings.remotes.length === 0}
|
||||||
<p class="status-message">No remote Runtime connections configured.</p>
|
<p class="status-message">No remote Runtime connections configured.</p>
|
||||||
{:else}
|
|
||||||
{#each runtimeSettings.remotes as remote (remote.runtime_id)}
|
|
||||||
<article class="settings-runtime-card">
|
|
||||||
{@render RuntimeConnectionCard({ connection: remote })}
|
|
||||||
<dl class="settings-identity-list compact">
|
|
||||||
<div>
|
|
||||||
<dt>Endpoint</dt>
|
|
||||||
<dd>{remote.endpoint_configured ? "configured (hidden)" : "not configured"}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Token ref</dt>
|
|
||||||
<dd>{remote.token_ref_configured ? "configured (hidden)" : "not configured"}</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
<div class="settings-action-row">
|
|
||||||
<button type="button" onclick={() => void testRemoteRuntime(remote.runtime_id)} disabled={testing === remote.runtime_id}>
|
|
||||||
{testing === remote.runtime_id ? "Testing…" : "Test"}
|
|
||||||
</button>
|
|
||||||
<button type="button" class="danger" onclick={() => void deleteRemoteRuntime(remote.runtime_id)} disabled={deleting === remote.runtime_id}>
|
|
||||||
{deleting === remote.runtime_id ? "Deleting…" : "Delete"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{#if tests[remote.runtime_id]}
|
|
||||||
{@const test = tests[remote.runtime_id]}
|
|
||||||
{@const available = capabilityOperations(test, "available")}
|
|
||||||
{@const unchecked = capabilityOperations(test, "unknown")}
|
|
||||||
<div class="settings-test-result">
|
|
||||||
<strong>Test: {test.state}</strong>
|
|
||||||
<span>{test.health_result} · {test.checked_at}</span>
|
|
||||||
<p>{test.compatibility_basis}</p>
|
|
||||||
{#if available.length > 0}
|
|
||||||
<p class="settings-test-verified">Verified areas: {available.join(', ')}</p>
|
|
||||||
{/if}
|
|
||||||
{#if unchecked.length > 0}
|
|
||||||
<p class="settings-test-verified">Unchecked warning areas: {unchecked.join(', ')}</p>
|
|
||||||
{/if}
|
|
||||||
{@render DiagnosticsList({ diagnostics: test.diagnostics })}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</article>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -428,41 +491,6 @@
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#snippet RuntimeConnectionCard({ connection }: { connection: RuntimeConnectionSummary | RemoteRuntimeConnectionSummary })}
|
|
||||||
<article class="settings-runtime-card embedded" class:inactive={!connection.active}>
|
|
||||||
<header>
|
|
||||||
<div>
|
|
||||||
<h3>{connection.display_name}</h3>
|
|
||||||
<p><code>{connection.runtime_id}</code></p>
|
|
||||||
</div>
|
|
||||||
<span class="badge" class:success={connection.active} class:warning={!connection.active}>{connection.status}</span>
|
|
||||||
</header>
|
|
||||||
<dl class="settings-identity-list compact">
|
|
||||||
<div>
|
|
||||||
<dt>Kind</dt>
|
|
||||||
<dd>{connection.kind}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Built in</dt>
|
|
||||||
<dd>{connection.built_in ? "yes" : "no"}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Config managed</dt>
|
|
||||||
<dd>{connection.config_managed ? "yes" : "no"}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Spawn</dt>
|
|
||||||
<dd>{connection.can_spawn_worker ? "available" : "unavailable"}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Restart required</dt>
|
|
||||||
<dd>{connection.restart_required ? "yes" : "no"}</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
{@render DiagnosticsList({ diagnostics: connection.diagnostics })}
|
|
||||||
</article>
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
{#snippet DiagnosticsList({ diagnostics }: { diagnostics: Diagnostic[] })}
|
{#snippet DiagnosticsList({ diagnostics }: { diagnostics: Diagnostic[] })}
|
||||||
{#if diagnostics.length > 0}
|
{#if diagnostics.length > 0}
|
||||||
<ul class="settings-diagnostics-list">
|
<ul class="settings-diagnostics-list">
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import ObjectivesNavSection from './ObjectivesNavSection.svelte';
|
import ObjectivesNavSection from './ObjectivesNavSection.svelte';
|
||||||
import RepositoriesNavSection from './RepositoriesNavSection.svelte';
|
import RepositoriesNavSection from './RepositoriesNavSection.svelte';
|
||||||
import WorkersNavSection from './WorkersNavSection.svelte';
|
import WorkersNavSection from './WorkersNavSection.svelte';
|
||||||
|
|
@ -15,11 +16,43 @@
|
||||||
let {
|
let {
|
||||||
workspace,
|
workspace,
|
||||||
workspaceError = null,
|
workspaceError = null,
|
||||||
repositories = null,
|
repositories,
|
||||||
repositoriesError = null,
|
repositoriesError,
|
||||||
currentPath = '/'
|
currentPath = '/'
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
let settingsActive = $derived(currentPath.startsWith("/settings"));
|
|
||||||
|
let fallbackRepositories = $state<RepositoryListResponse | null>(null);
|
||||||
|
let fallbackRepositoriesError = $state<string | null>(null);
|
||||||
|
let displayedRepositories = $derived(repositories === undefined ? fallbackRepositories : repositories);
|
||||||
|
let displayedRepositoriesError = $derived(
|
||||||
|
repositoriesError === undefined ? fallbackRepositoriesError : repositoriesError
|
||||||
|
);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (repositories !== undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const controller = new AbortController();
|
||||||
|
void loadFallbackRepositories(controller.signal);
|
||||||
|
return () => controller.abort();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadFallbackRepositories(signal?: AbortSignal) {
|
||||||
|
fallbackRepositoriesError = null;
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/repositories', { signal });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`repositories request failed (${response.status})`);
|
||||||
|
}
|
||||||
|
fallbackRepositories = (await response.json()) as RepositoryListResponse;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof DOMException && error.name === 'AbortError') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fallbackRepositoriesError = error instanceof Error ? error.message : 'repositories request failed';
|
||||||
|
fallbackRepositories = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside class="workspace-sidebar" aria-label="Workspace navigation">
|
<aside class="workspace-sidebar" aria-label="Workspace navigation">
|
||||||
|
|
@ -40,33 +73,22 @@
|
||||||
|
|
||||||
<a
|
<a
|
||||||
class="settings-button"
|
class="settings-button"
|
||||||
class:active={settingsActive}
|
|
||||||
href="/settings"
|
href="/settings"
|
||||||
aria-label="Open Settings / Admin"
|
aria-label="Open Settings / Admin"
|
||||||
title="Settings / Admin"
|
title="Settings / Admin"
|
||||||
aria-current={settingsActive ? 'page' : undefined}
|
|
||||||
>
|
>
|
||||||
⚙
|
⚙
|
||||||
</a>
|
</a>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav class="sidebar-sections" aria-label="Workspace sections">
|
<nav class="sidebar-sections" aria-label="Workspace sections">
|
||||||
<RepositoriesNavSection {repositories} {repositoriesError} {currentPath} />
|
<RepositoriesNavSection
|
||||||
|
repositories={displayedRepositories}
|
||||||
|
repositoriesError={displayedRepositoriesError}
|
||||||
|
{currentPath}
|
||||||
|
/>
|
||||||
<ObjectivesNavSection {currentPath} />
|
<ObjectivesNavSection {currentPath} />
|
||||||
<WorkersNavSection {currentPath} />
|
<WorkersNavSection {currentPath} />
|
||||||
|
|
||||||
<section class="nav-section" aria-labelledby="settings-heading">
|
|
||||||
<div class="section-heading-row">
|
|
||||||
<h2 id="settings-heading">settings</h2>
|
|
||||||
</div>
|
|
||||||
<ul class="nav-list" aria-label="Settings">
|
|
||||||
<li>
|
|
||||||
<a class="nav-item" class:active={settingsActive} href="/settings" aria-current={settingsActive ? 'page' : undefined}>
|
|
||||||
<span class="item-title">Settings / Admin</span>
|
|
||||||
<span class="item-meta">Backend shell and diagnostics</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user