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,
|
||||
response.runtime.worker_creation_available,
|
||||
),
|
||||
diagnostics: vec![diagnostic(
|
||||
"remote_runtime_backend_proxy",
|
||||
DiagnosticSeverity::Info,
|
||||
"Remote Runtime is accessed only by backend-owned REST/WS clients".to_string(),
|
||||
)],
|
||||
diagnostics: Vec::new(),
|
||||
},
|
||||
Err(diagnostic) => RuntimeSummary {
|
||||
runtime_id: self.runtime_id.clone(),
|
||||
|
|
@ -1922,11 +1918,7 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
|
|||
observed_at: Utc::now().to_rfc3339(),
|
||||
last_seen_at: None,
|
||||
capabilities: remote_runtime_capabilities(limit, true, false),
|
||||
diagnostics: vec![diagnostic(
|
||||
"remote_runtime_backend_proxy",
|
||||
DiagnosticSeverity::Info,
|
||||
"Remote host endpoint and credentials are backend-private".to_string(),
|
||||
)],
|
||||
diagnostics: Vec::new(),
|
||||
}],
|
||||
Vec::new(),
|
||||
)
|
||||
|
|
@ -2010,11 +2002,7 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
|
|||
kind: "remote_runtime_worker_created".to_string(),
|
||||
detail: "worker-runtime REST create endpoint accepted the Worker".to_string(),
|
||||
}],
|
||||
diagnostics: vec![diagnostic(
|
||||
"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(),
|
||||
)],
|
||||
diagnostics: Vec::new(),
|
||||
},
|
||||
Err(diagnostic) => WorkerSpawnResult {
|
||||
state: WorkerOperationState::Rejected,
|
||||
|
|
|
|||
|
|
@ -1226,28 +1226,74 @@
|
|||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.settings-runtime-card {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
.settings-runtime-table-wrap {
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 1rem;
|
||||
padding: 1rem;
|
||||
background: var(--bg-raised);
|
||||
}
|
||||
|
||||
.settings-runtime-card header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
.settings-runtime-table {
|
||||
width: 100%;
|
||||
min-width: 48rem;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.settings-runtime-card.inactive {
|
||||
opacity: 0.86;
|
||||
.settings-runtime-table th,
|
||||
.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 {
|
||||
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
|
||||
.settings-runtime-table th {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import RepositoryTicketKanban from '$lib/workspace-pages/RepositoryTicketKanban.svelte';
|
||||
import { workerConsoleHref } from '$lib/workspace-console/model';
|
||||
import WorkspaceSidebar from '$lib/workspace-sidebar/WorkspaceSidebar.svelte';
|
||||
|
|
@ -51,48 +52,48 @@
|
|||
let route = $derived(routeFromView(view, objectiveId, repositoryId));
|
||||
let currentPath = $derived(pathFromRoute(route));
|
||||
|
||||
async function getJson<T>(path: string): Promise<T> {
|
||||
const response = await fetch(path);
|
||||
async function getJson<T>(path: string, signal?: AbortSignal): Promise<T> {
|
||||
const response = await fetch(path, { signal });
|
||||
if (!response.ok) {
|
||||
throw new Error(`GET ${path} failed: ${response.status}`);
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
async function loadWorkspace() {
|
||||
async function loadWorkspace(signal?: AbortSignal) {
|
||||
workspaceError = null;
|
||||
try {
|
||||
workspace = await getJson<WorkspaceResponse>('/api/workspace');
|
||||
workspace = await getJson<WorkspaceResponse>('/api/workspace', signal);
|
||||
} catch (error) {
|
||||
workspaceError = error instanceof Error ? error.message : String(error);
|
||||
workspace = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHosts() {
|
||||
async function loadHosts(signal?: AbortSignal) {
|
||||
hostsError = null;
|
||||
try {
|
||||
hosts = await getJson<ListResponse<Host>>('/api/hosts');
|
||||
hosts = await getJson<ListResponse<Host>>('/api/hosts', signal);
|
||||
} catch (error) {
|
||||
hostsError = error instanceof Error ? error.message : String(error);
|
||||
hosts = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWorkers() {
|
||||
async function loadWorkers(signal?: AbortSignal) {
|
||||
workersError = null;
|
||||
try {
|
||||
workers = await getJson<ListResponse<Worker>>('/api/workers');
|
||||
workers = await getJson<ListResponse<Worker>>('/api/workers', signal);
|
||||
} catch (error) {
|
||||
workersError = error instanceof Error ? error.message : String(error);
|
||||
workers = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRepositories() {
|
||||
async function loadRepositories(signal?: AbortSignal) {
|
||||
repositoriesError = null;
|
||||
try {
|
||||
repositories = await getJson<RepositoryListResponse>('/api/repositories');
|
||||
repositories = await getJson<RepositoryListResponse>('/api/repositories', signal);
|
||||
} catch (error) {
|
||||
repositoriesError = error instanceof Error ? error.message : String(error);
|
||||
repositories = null;
|
||||
|
|
@ -124,10 +125,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function loadObjectives() {
|
||||
async function loadObjectives(signal?: AbortSignal) {
|
||||
objectivesError = null;
|
||||
try {
|
||||
objectives = await getJson<ObjectiveListResponse>('/api/objectives');
|
||||
objectives = await getJson<ObjectiveListResponse>('/api/objectives', signal);
|
||||
} catch (error) {
|
||||
objectivesError = error instanceof Error ? error.message : String(error);
|
||||
objectives = null;
|
||||
|
|
@ -189,12 +190,15 @@
|
|||
return value ?? 'not recorded';
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
void loadWorkspace();
|
||||
void loadHosts();
|
||||
void loadWorkers();
|
||||
void loadRepositories();
|
||||
void loadObjectives();
|
||||
onMount(() => {
|
||||
const controller = new AbortController();
|
||||
void loadWorkspace(controller.signal);
|
||||
void loadHosts(controller.signal);
|
||||
void loadWorkers(controller.signal);
|
||||
void loadRepositories(controller.signal);
|
||||
void loadObjectives(controller.signal);
|
||||
|
||||
return () => controller.abort();
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@
|
|||
type RemoteRuntimeTestResponse,
|
||||
type RuntimeConnectionMutationResponse,
|
||||
type RuntimeConnectionSettingsResponse,
|
||||
type RuntimeConnectionSummary,
|
||||
} from "./model";
|
||||
|
||||
type RemoteAddForm = {
|
||||
|
|
@ -33,6 +32,7 @@
|
|||
let deleting = $state<string | null>(null);
|
||||
let testing = $state<string | null>(null);
|
||||
let submitting = $state(false);
|
||||
let showAddRuntimeForm = $state(false);
|
||||
let remoteForm = $state<RemoteAddForm>({
|
||||
runtime_id: "",
|
||||
display_name: "",
|
||||
|
|
@ -117,6 +117,7 @@
|
|||
const data = (await response.json()) as RuntimeConnectionMutationResponse;
|
||||
applyRuntimeMutation(data);
|
||||
remoteForm = { runtime_id: "", display_name: "", endpoint: "" };
|
||||
showAddRuntimeForm = false;
|
||||
} catch (err) {
|
||||
mutationMessage = err instanceof Error ? err.message : "add remote Runtime failed";
|
||||
} finally {
|
||||
|
|
@ -289,76 +290,138 @@
|
|||
{:else if runtimeError}
|
||||
<p class="status-message error">Runtime connection settings unavailable: {runtimeError}</p>
|
||||
{: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(); }}>
|
||||
<h3>Add remote Runtime</h3>
|
||||
<p>Endpoint is submitted to the Backend but not echoed back in settings responses.</p>
|
||||
<label>
|
||||
<span>Runtime id</span>
|
||||
<input bind:value={remoteForm.runtime_id} required maxlength="96" pattern="[A-Za-z0-9_.-]+" placeholder="team-runtime" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Display name</span>
|
||||
<input bind:value={remoteForm.display_name} maxlength="80" placeholder="Team Runtime" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Endpoint</span>
|
||||
<input bind:value={remoteForm.endpoint} required inputmode="url" placeholder="https://runtime.example" />
|
||||
</label>
|
||||
<button type="submit" disabled={submitting}>{submitting ? "Saving…" : "Add Runtime"}</button>
|
||||
</form>
|
||||
{#if showAddRuntimeForm}
|
||||
<form class="settings-runtime-form" onsubmit={(event) => { event.preventDefault(); void submitRemoteRuntime(); }}>
|
||||
<h3>Add remote Runtime</h3>
|
||||
<p>Endpoint is submitted to the Backend but not echoed back in settings responses.</p>
|
||||
<label>
|
||||
<span>Runtime id</span>
|
||||
<input bind:value={remoteForm.runtime_id} required maxlength="96" pattern="[A-Za-z0-9_.-]+" placeholder="team-runtime" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Display name</span>
|
||||
<input bind:value={remoteForm.display_name} maxlength="80" placeholder="Team Runtime" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Endpoint</span>
|
||||
<input bind:value={remoteForm.endpoint} required inputmode="url" placeholder="https://runtime.example" />
|
||||
</label>
|
||||
<button type="submit" disabled={submitting}>{submitting ? "Saving…" : "Add Runtime"}</button>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
{#if mutationMessage}
|
||||
<p class="status-message" class:error={mutationMessage.includes("failed")}>{mutationMessage}</p>
|
||||
{/if}
|
||||
{@render DiagnosticsList({ diagnostics: mutationDiagnostics })}
|
||||
|
||||
<div class="settings-runtime-list" aria-label="Remote Runtime connections">
|
||||
<h3>Remote Runtimes</h3>
|
||||
<div class="settings-runtime-list" aria-label="Runtime connections">
|
||||
<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}
|
||||
<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}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -428,41 +491,6 @@
|
|||
</main>
|
||||
</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[] })}
|
||||
{#if diagnostics.length > 0}
|
||||
<ul class="settings-diagnostics-list">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import ObjectivesNavSection from './ObjectivesNavSection.svelte';
|
||||
import RepositoriesNavSection from './RepositoriesNavSection.svelte';
|
||||
import WorkersNavSection from './WorkersNavSection.svelte';
|
||||
|
|
@ -15,11 +16,43 @@
|
|||
let {
|
||||
workspace,
|
||||
workspaceError = null,
|
||||
repositories = null,
|
||||
repositoriesError = null,
|
||||
repositories,
|
||||
repositoriesError,
|
||||
currentPath = '/'
|
||||
}: 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>
|
||||
|
||||
<aside class="workspace-sidebar" aria-label="Workspace navigation">
|
||||
|
|
@ -40,33 +73,22 @@
|
|||
|
||||
<a
|
||||
class="settings-button"
|
||||
class:active={settingsActive}
|
||||
href="/settings"
|
||||
aria-label="Open Settings / Admin"
|
||||
title="Settings / Admin"
|
||||
aria-current={settingsActive ? 'page' : undefined}
|
||||
>
|
||||
⚙
|
||||
</a>
|
||||
</header>
|
||||
|
||||
<nav class="sidebar-sections" aria-label="Workspace sections">
|
||||
<RepositoriesNavSection {repositories} {repositoriesError} {currentPath} />
|
||||
<RepositoriesNavSection
|
||||
repositories={displayedRepositories}
|
||||
repositoriesError={displayedRepositoriesError}
|
||||
{currentPath}
|
||||
/>
|
||||
<ObjectivesNavSection {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>
|
||||
</aside>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user