From 7dda89883042a1b7a587302300a88ff226a9dd7f Mon Sep 17 00:00:00 2001 From: Hare Date: Thu, 23 Jul 2026 02:44:20 +0900 Subject: [PATCH] web: add workspace ticket routes --- web/workspace/src/app.css | 46 ++++++++ .../src/lib/workspace/api/http.test.ts | 19 ++-- .../console/worker-console.ui.test.ts | 101 ++++++++++++++---- .../src/lib/workspace/settings/model.ts | 15 +++ .../sidebar/RuntimesNavSection.svelte | 23 ---- .../sidebar/TicketsNavSection.svelte | 22 ++++ .../workspace/sidebar/WorkspaceSidebar.svelte | 4 +- .../src/lib/workspace/sidebar/types.ts | 25 +++++ web/workspace/src/routes/+layout.ts | 5 +- web/workspace/src/routes/+page.svelte | 2 +- .../src/routes/objectives/+page.svelte | 6 -- web/workspace/src/routes/objectives/+page.ts | 3 - .../objectives/[objectiveId]/+page.svelte | 6 -- .../routes/objectives/[objectiveId]/+page.ts | 3 - .../repositories/[repositoryId]/+page.svelte | 6 -- .../repositories/[repositoryId]/+page.ts | 3 - .../workers/[workerId]/console/+page.svelte | 6 -- .../workers/[workerId]/console/+page.ts | 3 - .../src/routes/settings/+page.svelte | 6 -- .../src/routes/w/[workspaceId]/+page.svelte | 17 ++- .../{ => settings}/runtimes/+page.svelte | 10 +- .../{ => settings}/runtimes/+page.ts | 0 .../[runtimeId]/workdirs/+page.svelte | 2 +- .../runtimes/[runtimeId]/workdirs/+page.ts | 0 .../w/[workspaceId]/tickets/+page.svelte | 64 +++++++++++ .../routes/w/[workspaceId]/tickets/+page.ts | 15 +++ .../tickets/[ticketId]/+page.svelte | 82 ++++++++++++++ .../[workspaceId]/tickets/[ticketId]/+page.ts | 17 +++ 28 files changed, 397 insertions(+), 114 deletions(-) delete mode 100644 web/workspace/src/lib/workspace/sidebar/RuntimesNavSection.svelte create mode 100644 web/workspace/src/lib/workspace/sidebar/TicketsNavSection.svelte delete mode 100644 web/workspace/src/routes/objectives/+page.svelte delete mode 100644 web/workspace/src/routes/objectives/+page.ts delete mode 100644 web/workspace/src/routes/objectives/[objectiveId]/+page.svelte delete mode 100644 web/workspace/src/routes/objectives/[objectiveId]/+page.ts delete mode 100644 web/workspace/src/routes/repositories/[repositoryId]/+page.svelte delete mode 100644 web/workspace/src/routes/repositories/[repositoryId]/+page.ts delete mode 100644 web/workspace/src/routes/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte delete mode 100644 web/workspace/src/routes/runtimes/[runtimeId]/workers/[workerId]/console/+page.ts delete mode 100644 web/workspace/src/routes/settings/+page.svelte rename web/workspace/src/routes/w/[workspaceId]/{ => settings}/runtimes/+page.svelte (83%) rename web/workspace/src/routes/w/[workspaceId]/{ => settings}/runtimes/+page.ts (100%) rename web/workspace/src/routes/w/[workspaceId]/{ => settings}/runtimes/[runtimeId]/workdirs/+page.svelte (98%) rename web/workspace/src/routes/w/[workspaceId]/{ => settings}/runtimes/[runtimeId]/workdirs/+page.ts (100%) create mode 100644 web/workspace/src/routes/w/[workspaceId]/tickets/+page.svelte create mode 100644 web/workspace/src/routes/w/[workspaceId]/tickets/+page.ts create mode 100644 web/workspace/src/routes/w/[workspaceId]/tickets/[ticketId]/+page.svelte create mode 100644 web/workspace/src/routes/w/[workspaceId]/tickets/[ticketId]/+page.ts diff --git a/web/workspace/src/app.css b/web/workspace/src/app.css index 9c42b30a..6ebc935c 100644 --- a/web/workspace/src/app.css +++ b/web/workspace/src/app.css @@ -445,12 +445,14 @@ .runtime-card { padding: 0; } + .ticket-list, .objective-list { display: flex; flex-direction: column; gap: var(--space-2); margin-top: var(--space-4); } + .ticket-row, .objective-row { display: grid; grid-template-columns: minmax(0, 1fr) max-content; @@ -463,27 +465,34 @@ text-decoration: none; transition: background-color 140ms ease; } + .ticket-row:hover, + .ticket-row:focus-visible, .objective-row:hover, .objective-row:focus-visible { background: var(--interactive-hover); } + .ticket-row.selected, .objective-row.selected { background: var(--interactive-selected); } + .ticket-row.selected .ticket-title, .objective-row.selected .objective-title { color: var(--accent); } + .ticket-main, .objective-main { display: grid; gap: var(--space-1); min-width: 0; } + .ticket-title-row, .objective-title-row { display: flex; align-items: baseline; gap: var(--space-2); min-width: 0; } + .ticket-title, .objective-title { min-width: 0; overflow: hidden; @@ -500,12 +509,14 @@ letter-spacing: 0.05em; text-transform: uppercase; } + .ticket-summary, .objective-summary { margin: 0; color: var(--text-muted); font-size: 0.88rem; line-height: 1.4; } + .ticket-meta, .objective-meta { display: flex; flex-wrap: wrap; @@ -518,10 +529,12 @@ white-space: nowrap; } @media (max-width: 900px) { + .ticket-row, .objective-row { grid-template-columns: 1fr; gap: var(--space-2); } + .ticket-meta, .objective-meta { justify-content: flex-start; text-align: left; @@ -542,6 +555,9 @@ .runtime-heading span.warn { color: var(--warning); } + .detail-heading.compact { + margin-bottom: var(--space-2); + } .detail-heading h3 { margin: 0; } @@ -549,6 +565,36 @@ color: var(--text-muted); font-size: 0.8rem; } + .ticket-detail-grid { + margin-bottom: var(--space-4); + } + .risk-flags { + display: flex; + flex-wrap: wrap; + gap: var(--space-2); + margin-bottom: var(--space-4); + } + .risk-flags span, + .warning-pill { + border-radius: 999px; + padding: 0.18rem 0.55rem; + background: var(--bg-subtle); + color: var(--warning); + font-size: 0.72rem; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; + } + .ticket-body pre { + overflow-x: auto; + margin: 0; + padding: var(--space-4); + border: 1px solid var(--line); + border-radius: var(--radius-panel); + background: var(--bg-raised); + color: var(--text); + white-space: pre-wrap; + } dl { display: grid; gap: var(--space-1); diff --git a/web/workspace/src/lib/workspace/api/http.test.ts b/web/workspace/src/lib/workspace/api/http.test.ts index cdce78a9..d4e9db62 100644 --- a/web/workspace/src/lib/workspace/api/http.test.ts +++ b/web/workspace/src/lib/workspace/api/http.test.ts @@ -38,7 +38,7 @@ Deno.test("workspace route helpers scope browser routes and API by immutable wor ); }); -Deno.test("unscoped layout bootstraps then redirects instead of loading unscoped workspace data", async () => { +Deno.test("root layout bootstraps only the scoped workspace entry", async () => { const layout = await Deno.readTextFile( new URL("./../../../routes/+layout.ts", import.meta.url), ); @@ -47,24 +47,17 @@ Deno.test("unscoped layout bootstraps then redirects instead of loading unscoped "unscoped layout may use only the workspace-id bootstrap endpoint", ); assert( - layout.includes("throw redirect(307") && layout.includes("workspaceRoute("), - "unscoped layout should redirect to the scoped workspace route", + layout.includes("throw redirect(307") && + layout.includes("workspaceRoute(workspace.data.workspace_id)") && + !layout.includes("scopedCompatibilityRoute") && + !layout.includes("workspaceRoute(workspaceId, pathname)"), + "root layout should redirect only to the scoped workspace entry", ); assert( !layout.includes("`/api${path}`") && !layout.includes('"/api/repositories"'), "layout must not fall back to unscoped workspace-scoped API calls", ); - - const unscopedSettings = await Deno.readTextFile( - new URL("./../../../routes/settings/+page.svelte", import.meta.url), - ); - assert( - unscopedSettings.includes("Redirecting to scoped settings") && - !unscopedSettings.includes("fetch(") && - !unscopedSettings.includes("settingsApiPath"), - "unscoped settings route should remain a thin redirect shim, not a data/control surface", - ); }); Deno.test("Skill API paths use workspace backend scoped endpoints", () => { diff --git a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts index 2c003a7f..73c9bd3d 100644 --- a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts +++ b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts @@ -27,9 +27,13 @@ Deno.test("workspace Worker list lives on the dedicated Workers page", async () ); assert( - workspacePage.includes("href={`/w/${workspaceId}/runtimes`}") && - workspacePage.includes("href={`/w/${workspaceId}/workers`}"), - "top workspace page should link to Runtimes and Workers pages", + workspacePage.includes("ticketsHref") && + workspacePage.includes("runtimeSettingsHref") && + workspacePage.includes("workersHref") && + workspacePage.includes("workspaceRoute(workspaceId, '/tickets')") && + workspacePage.includes("workspaceRoute(workspaceId, '/settings/runtimes')") && + workspacePage.includes("workspaceRoute(workspaceId, '/workers')"), + "top workspace page should link to Tickets, Runtime Inventory under Settings, and the Workers page", ); assert( !workspacePage.includes("workerConsoleHref") && @@ -51,8 +55,66 @@ Deno.test("workspace Worker list lives on the dedicated Workers page", async () ); assert( !sidebar.includes("CompanionNavSection") && + sidebar.includes("TicketsNavSection") && sidebar.includes("WorkersNavSection"), - "standalone Companion/Console navigation should not remain canonical", + "standalone Companion/Console navigation should not remain canonical and Tickets should be primary workspace navigation", + ); +}); + +Deno.test("workspace Tickets surface uses read-only Backend Ticket APIs", async () => { + const ticketsNav = await Deno.readTextFile( + new URL("../sidebar/TicketsNavSection.svelte", import.meta.url), + ); + const ticketsLoad = await Deno.readTextFile( + new URL("./../../../routes/w/[workspaceId]/tickets/+page.ts", import.meta.url), + ); + const ticketsPage = await Deno.readTextFile( + new URL("./../../../routes/w/[workspaceId]/tickets/+page.svelte", import.meta.url), + ); + const ticketDetailLoad = await Deno.readTextFile( + new URL( + "./../../../routes/w/[workspaceId]/tickets/[ticketId]/+page.ts", + import.meta.url, + ), + ); + const ticketDetailPage = await Deno.readTextFile( + new URL( + "./../../../routes/w/[workspaceId]/tickets/[ticketId]/+page.svelte", + import.meta.url, + ), + ); + + assert( + ticketsNav.includes("workspaceRoute(workspaceId, '/tickets')") && + ticketsNav.includes("Open Tickets"), + "Tickets sidebar section should link to the workspace Tickets surface", + ); + assert( + ticketsLoad.includes('workspaceApiPath(params.workspaceId, "/tickets")') && + ticketsPage.includes("This surface is read-only") && + ticketsPage.includes("workspaceRoute(data.workspaceId, `/tickets/${ticket.id}`)"), + "Tickets list should read the workspace-scoped Ticket API and link to Ticket details", + ); + assert( + ticketDetailLoad.includes("`/tickets/${encodeURIComponent(ticketId)}`") && + ticketDetailPage.includes("event_count") && + ticketDetailPage.includes("artifact_count") && + ticketDetailPage.includes("
{data.ticket.data.body"),
+    "Ticket detail should read one Ticket record and expose body plus metadata without mutation controls",
+  );
+});
+
+Deno.test("root layout does not keep legacy unscoped route compatibility", async () => {
+  const layoutLoad = await Deno.readTextFile(
+    new URL("./../../../routes/+layout.ts", import.meta.url),
+  );
+
+  assert(
+    !layoutLoad.includes("scopedCompatibilityRoute") &&
+      !layoutLoad.includes('pathname === "/runtimes"') &&
+      !layoutLoad.includes("return workspaceRoute(workspaceId, pathname)") &&
+      layoutLoad.includes("workspaceRoute(workspace.data.workspace_id)"),
+    "root layout should bootstrap the workspace entry only, not preserve legacy unscoped routes",
   );
 });
 
@@ -188,49 +250,50 @@ Deno.test("Decodal source editor keeps imperative EditorView out of reactive sta
   );
 });
 
-Deno.test("workspace Runtime management pages expose Runtimes and Runtime-owned workdirs", async () => {
+Deno.test("workspace Runtime inventory lives under Settings admin routes", async () => {
   const sidebar = await Deno.readTextFile(
     new URL("../sidebar/WorkspaceSidebar.svelte", import.meta.url),
   );
-  const runtimesNav = await Deno.readTextFile(
-    new URL("../sidebar/RuntimesNavSection.svelte", import.meta.url),
+  const settingsModel = await Deno.readTextFile(
+    new URL("../settings/model.ts", import.meta.url),
   );
   const runtimesPage = await Deno.readTextFile(
     new URL(
-      "./../../../routes/w/[workspaceId]/runtimes/+page.svelte",
+      "./../../../routes/w/[workspaceId]/settings/runtimes/+page.svelte",
       import.meta.url,
     ),
   );
   const workdirsPage = await Deno.readTextFile(
     new URL(
-      "./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workdirs/+page.svelte",
+      "./../../../routes/w/[workspaceId]/settings/runtimes/[runtimeId]/workdirs/+page.svelte",
       import.meta.url,
     ),
   );
   const workdirsLoad = await Deno.readTextFile(
     new URL(
-      "./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workdirs/+page.ts",
+      "./../../../routes/w/[workspaceId]/settings/runtimes/[runtimeId]/workdirs/+page.ts",
       import.meta.url,
     ),
   );
 
   assert(
-    sidebar.includes("RuntimesNavSection") &&
-      runtimesNav.includes("href={runtimesHref}") &&
-      runtimesNav.includes("/runtimes"),
-    "sidebar should expose Runtime management navigation",
+    !sidebar.includes("RuntimesNavSection") &&
+      settingsModel.includes('id: "runtime-inventory"') &&
+      settingsModel.includes('return `${SETTINGS_ROUTE}/runtimes`;'),
+    "Runtime inventory should be admin Settings navigation, not primary workspace sidebar navigation",
   );
   assert(
-    runtimesPage.includes("Open workdirs") &&
+    runtimesPage.includes("Runtime Inventory") &&
+      runtimesPage.includes("Open workdirs") &&
       runtimesPage.includes("runtimes-table") &&
-      runtimesPage.includes("/workdirs"),
-    "Runtimes page should table Runtimes and link to each Runtime's workdirs",
+      runtimesPage.includes("/settings/runtimes/${encodeURIComponent(runtime.runtime_id)}/workdirs"),
+    "Settings Runtime Inventory page should table Runtimes and link to each Runtime's workdirs",
   );
   assert(
-    workdirsPage.includes("Workdirs") &&
+    workdirsPage.includes("Runtime Inventory") &&
       workdirsPage.includes("workdirs-table") &&
       workdirsLoad.includes("/working-directories"),
-    "Workdirs page should read Runtime-owned working-directory API while using workdir UI language",
+    "Runtime workdirs should remain backed by Runtime APIs without legacy Runtime route redirects",
   );
 });
 
diff --git a/web/workspace/src/lib/workspace/settings/model.ts b/web/workspace/src/lib/workspace/settings/model.ts
index 7fad42f4..768f31fc 100644
--- a/web/workspace/src/lib/workspace/settings/model.ts
+++ b/web/workspace/src/lib/workspace/settings/model.ts
@@ -6,6 +6,7 @@ export type Diagnostic = {
 
 export type SettingsSectionId =
   | "runtime-connections"
+  | "runtime-inventory"
   | "profile-sources"
   | "backend-config"
   | "workspace-identity";
@@ -85,6 +86,18 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
       "Test negotiation is an observation only; checked_at, health, compatibility, and capability results are not persisted to local config.",
     ],
   },
+  {
+    id: "runtime-inventory",
+    label: "Runtime Inventory",
+    status: "read-only",
+    summary:
+      "Inspect registered Runtime handles and their materialized workdirs from the admin settings surface instead of the normal workspace navigation.",
+    bullets: [
+      "Runtime state is operational Backend/Runtime context, not a workspace content object like Objectives or Repositories.",
+      "Workdir cleanup remains scoped to typed Runtime APIs and stays outside the primary workspace sidebar.",
+      "Console routes may still target a Runtime handle directly, but Runtime discovery belongs under Settings.",
+    ],
+  },
   {
     id: "profile-sources",
     label: "Profile Sources",
@@ -145,6 +158,8 @@ export function settingsSectionHref(id: SettingsSectionId): string {
   switch (id) {
     case "runtime-connections":
       return `${SETTINGS_ROUTE}/runtime-connections`;
+    case "runtime-inventory":
+      return `${SETTINGS_ROUTE}/runtimes`;
     case "profile-sources":
       return `${SETTINGS_ROUTE}/profiles`;
     case "workspace-identity":
diff --git a/web/workspace/src/lib/workspace/sidebar/RuntimesNavSection.svelte b/web/workspace/src/lib/workspace/sidebar/RuntimesNavSection.svelte
deleted file mode 100644
index 4cace34f..00000000
--- a/web/workspace/src/lib/workspace/sidebar/RuntimesNavSection.svelte
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
diff --git a/web/workspace/src/lib/workspace/sidebar/TicketsNavSection.svelte b/web/workspace/src/lib/workspace/sidebar/TicketsNavSection.svelte
new file mode 100644
index 00000000..f93249d3
--- /dev/null
+++ b/web/workspace/src/lib/workspace/sidebar/TicketsNavSection.svelte
@@ -0,0 +1,22 @@
+
+
+
diff --git a/web/workspace/src/lib/workspace/sidebar/WorkspaceSidebar.svelte b/web/workspace/src/lib/workspace/sidebar/WorkspaceSidebar.svelte
index da01a04a..54326508 100644
--- a/web/workspace/src/lib/workspace/sidebar/WorkspaceSidebar.svelte
+++ b/web/workspace/src/lib/workspace/sidebar/WorkspaceSidebar.svelte
@@ -2,7 +2,7 @@
   import { workspaceRoute } from '$lib/workspace/api/http';
   import ObjectivesNavSection from './ObjectivesNavSection.svelte';
   import RepositoriesNavSection from './RepositoriesNavSection.svelte';
-  import RuntimesNavSection from './RuntimesNavSection.svelte';
+  import TicketsNavSection from './TicketsNavSection.svelte';
   import WorkersNavSection from './WorkersNavSection.svelte';
   import type { RepositoryListResponse, WorkspaceResponse } from './types';
 
@@ -107,7 +107,7 @@
     
   {/if}
diff --git a/web/workspace/src/lib/workspace/sidebar/types.ts b/web/workspace/src/lib/workspace/sidebar/types.ts
index 7091da1f..dc48b11d 100644
--- a/web/workspace/src/lib/workspace/sidebar/types.ts
+++ b/web/workspace/src/lib/workspace/sidebar/types.ts
@@ -320,6 +320,31 @@ export type TicketSummary = {
   record_source?: string;
 };
 
+export type TicketListResponse = {
+  workspace_id: string;
+  limit: number;
+  items: TicketSummary[];
+  invalid_records: InvalidProjectRecord[];
+  record_authority: string;
+};
+
+export type TicketDetail = {
+  id: string;
+  title: string;
+  state: string;
+  priority?: string | null;
+  created_at?: string | null;
+  updated_at?: string | null;
+  queued_by?: string | null;
+  queued_at?: string | null;
+  risk_flags: string[];
+  body: string;
+  body_truncated: boolean;
+  event_count: number;
+  artifact_count: number;
+  record_source: string;
+};
+
 export type TicketKanbanColumn = {
   state: string;
   items: TicketSummary[];
diff --git a/web/workspace/src/routes/+layout.ts b/web/workspace/src/routes/+layout.ts
index 2ff18641..4fb579c5 100644
--- a/web/workspace/src/routes/+layout.ts
+++ b/web/workspace/src/routes/+layout.ts
@@ -12,10 +12,7 @@ export const load: LayoutLoad = async ({ fetch, params, url }) => {
   if (!workspaceId) {
     const workspace = await loadJson(fetch, "/api/workspace");
     if (workspace.data) {
-      const scopedPath = workspaceRoute(
-        workspace.data.workspace_id,
-        url.pathname === "/" ? "" : url.pathname,
-      );
+      const scopedPath = workspaceRoute(workspace.data.workspace_id);
       throw redirect(307, `${scopedPath}${url.search}`);
     }
     return {
diff --git a/web/workspace/src/routes/+page.svelte b/web/workspace/src/routes/+page.svelte
index 87fceed3..cb5bef45 100644
--- a/web/workspace/src/routes/+page.svelte
+++ b/web/workspace/src/routes/+page.svelte
@@ -1,6 +1,6 @@
 

Redirecting to scoped workspace…

-

This compatibility route bootstraps the current workspace id and redirects to the canonical /w/<workspace-id> route.

+

The workspace entry bootstraps the current workspace id and opens the canonical /w/<workspace-id> route.

diff --git a/web/workspace/src/routes/objectives/+page.svelte b/web/workspace/src/routes/objectives/+page.svelte deleted file mode 100644 index c3cbc837..00000000 --- a/web/workspace/src/routes/objectives/+page.svelte +++ /dev/null @@ -1,6 +0,0 @@ -
-
-

Redirecting to scoped objectives…

-

Objectives are available at the canonical /w/<workspace-id>/objectives route.

-
-
diff --git a/web/workspace/src/routes/objectives/+page.ts b/web/workspace/src/routes/objectives/+page.ts deleted file mode 100644 index 7a066b58..00000000 --- a/web/workspace/src/routes/objectives/+page.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { PageLoad } from "./$types"; - -export const load: PageLoad = async () => ({}); diff --git a/web/workspace/src/routes/objectives/[objectiveId]/+page.svelte b/web/workspace/src/routes/objectives/[objectiveId]/+page.svelte deleted file mode 100644 index 37f9891b..00000000 --- a/web/workspace/src/routes/objectives/[objectiveId]/+page.svelte +++ /dev/null @@ -1,6 +0,0 @@ -
-
-

Redirecting to scoped objective…

-

Objective detail routes are canonical under /w/<workspace-id>/objectives/<objective-id>.

-
-
diff --git a/web/workspace/src/routes/objectives/[objectiveId]/+page.ts b/web/workspace/src/routes/objectives/[objectiveId]/+page.ts deleted file mode 100644 index 7a066b58..00000000 --- a/web/workspace/src/routes/objectives/[objectiveId]/+page.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { PageLoad } from "./$types"; - -export const load: PageLoad = async () => ({}); diff --git a/web/workspace/src/routes/repositories/[repositoryId]/+page.svelte b/web/workspace/src/routes/repositories/[repositoryId]/+page.svelte deleted file mode 100644 index c9ddef3c..00000000 --- a/web/workspace/src/routes/repositories/[repositoryId]/+page.svelte +++ /dev/null @@ -1,6 +0,0 @@ -
-
-

Redirecting to scoped repository…

-

Repository routes are canonical under /w/<workspace-id>/repositories/<repository-id>.

-
-
diff --git a/web/workspace/src/routes/repositories/[repositoryId]/+page.ts b/web/workspace/src/routes/repositories/[repositoryId]/+page.ts deleted file mode 100644 index 7a066b58..00000000 --- a/web/workspace/src/routes/repositories/[repositoryId]/+page.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { PageLoad } from "./$types"; - -export const load: PageLoad = async () => ({}); diff --git a/web/workspace/src/routes/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte b/web/workspace/src/routes/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte deleted file mode 100644 index 5469576b..00000000 --- a/web/workspace/src/routes/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte +++ /dev/null @@ -1,6 +0,0 @@ -
-
-

Redirecting to scoped Worker Console…

-

Worker Console routes are canonical under /w/<workspace-id>/runtimes/<runtime-id>/workers/<worker-id>/console.

-
-
diff --git a/web/workspace/src/routes/runtimes/[runtimeId]/workers/[workerId]/console/+page.ts b/web/workspace/src/routes/runtimes/[runtimeId]/workers/[workerId]/console/+page.ts deleted file mode 100644 index 7a066b58..00000000 --- a/web/workspace/src/routes/runtimes/[runtimeId]/workers/[workerId]/console/+page.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { PageLoad } from "./$types"; - -export const load: PageLoad = async () => ({}); diff --git a/web/workspace/src/routes/settings/+page.svelte b/web/workspace/src/routes/settings/+page.svelte deleted file mode 100644 index 18b67741..00000000 --- a/web/workspace/src/routes/settings/+page.svelte +++ /dev/null @@ -1,6 +0,0 @@ -
-
-

Redirecting to scoped settings…

-

Settings are available at the canonical /w/<workspace-id>/settings route.

-
-
diff --git a/web/workspace/src/routes/w/[workspaceId]/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/+page.svelte index ce57122c..d790fc4c 100644 --- a/web/workspace/src/routes/w/[workspaceId]/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/+page.svelte @@ -1,8 +1,12 @@ @@ -39,12 +43,17 @@
- - Runtimes - Manage runtimes and workdirs + + Tickets + Browse workspace tickets + Read typed Ticket records + + + Runtime Inventory + Open admin runtime inventory {data.hosts?.items.length ?? 0} host{(data.hosts?.items.length ?? 0) === 1 ? '' : 's'} visible - + Workers Open worker list Inspect status and attach to consoles diff --git a/web/workspace/src/routes/w/[workspaceId]/runtimes/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/+page.svelte similarity index 83% rename from web/workspace/src/routes/w/[workspaceId]/runtimes/+page.svelte rename to web/workspace/src/routes/w/[workspaceId]/settings/runtimes/+page.svelte index 46e47e08..f2a714b7 100644 --- a/web/workspace/src/routes/w/[workspaceId]/runtimes/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/+page.svelte @@ -10,15 +10,15 @@ - Runtimes · Yoi Workspace - + Runtime Inventory · Settings · Yoi Workspace +
-

Runtimes

-

Execution backends available to this workspace.

+

Runtime Inventory

+

Admin view of execution backends available to this workspace.

@@ -53,7 +53,7 @@ {runtimePlatform(runtime)} {runtime.capabilities.max_workers} workers -
+ Open workdirs diff --git a/web/workspace/src/routes/w/[workspaceId]/runtimes/+page.ts b/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/+page.ts similarity index 100% rename from web/workspace/src/routes/w/[workspaceId]/runtimes/+page.ts rename to web/workspace/src/routes/w/[workspaceId]/settings/runtimes/+page.ts diff --git a/web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workdirs/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/[runtimeId]/workdirs/+page.svelte similarity index 98% rename from web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workdirs/+page.svelte rename to web/workspace/src/routes/w/[workspaceId]/settings/runtimes/[runtimeId]/workdirs/+page.svelte index f0c1155c..884de2af 100644 --- a/web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workdirs/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/[runtimeId]/workdirs/+page.svelte @@ -105,7 +105,7 @@
- +

Workdirs

Workdirs owned by {data.runtimeId}.

{#if data.cleanupPlanError}

{data.cleanupPlanError}

{/if} diff --git a/web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workdirs/+page.ts b/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/[runtimeId]/workdirs/+page.ts similarity index 100% rename from web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workdirs/+page.ts rename to web/workspace/src/routes/w/[workspaceId]/settings/runtimes/[runtimeId]/workdirs/+page.ts diff --git a/web/workspace/src/routes/w/[workspaceId]/tickets/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/tickets/+page.svelte new file mode 100644 index 00000000..c1f27b78 --- /dev/null +++ b/web/workspace/src/routes/w/[workspaceId]/tickets/+page.svelte @@ -0,0 +1,64 @@ + + + + Tickets · Yoi Workspace + + + +
+
+
+

Workspace records

+

Tickets

+
+ {#if data.tickets.data} + {data.tickets.data.items.length} ticket{data.tickets.data.items.length === 1 ? '' : 's'} + {/if} +
+ +

+ Tickets are read from the typed Ticket backend. This surface is read-only; creation and queue operations remain outside the browser UI for now. +

+ + {#if data.tickets.data} + {#if data.tickets.data.items.length === 0} +

No Ticket records are present.

+ {:else} + + {/if} + + {#if data.tickets.data.invalid_records.length > 0} +

{data.tickets.data.invalid_records.length} invalid Ticket record(s) hidden.

+ {/if} + {:else if data.tickets.error} +

{data.tickets.error}

+ {:else} +

Waiting for /api/w/{data.workspaceId}/tickets

+ {/if} +
diff --git a/web/workspace/src/routes/w/[workspaceId]/tickets/+page.ts b/web/workspace/src/routes/w/[workspaceId]/tickets/+page.ts new file mode 100644 index 00000000..c8518614 --- /dev/null +++ b/web/workspace/src/routes/w/[workspaceId]/tickets/+page.ts @@ -0,0 +1,15 @@ +import { loadJson, workspaceApiPath } from "$lib/workspace/api/http"; +import type { TicketListResponse } from "$lib/workspace/sidebar/types"; +import type { PageLoad } from "./$types"; + +export const load = (async ({ fetch, params }) => { + const tickets = await loadJson( + fetch, + workspaceApiPath(params.workspaceId, "/tickets"), + ); + + return { + workspaceId: params.workspaceId, + tickets, + }; +}) satisfies PageLoad; diff --git a/web/workspace/src/routes/w/[workspaceId]/tickets/[ticketId]/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/tickets/[ticketId]/+page.svelte new file mode 100644 index 00000000..bbe3c125 --- /dev/null +++ b/web/workspace/src/routes/w/[workspaceId]/tickets/[ticketId]/+page.svelte @@ -0,0 +1,82 @@ + + + + {data.ticket.data?.title ?? data.ticketId} · Tickets · Yoi Workspace + + + +
+ + + {#if data.ticket.data} +
+
+

{data.ticket.data.id}

+

{data.ticket.data.title}

+
+ {data.ticket.data.state} +
+ +
+
+
Priority
+
{data.ticket.data.priority ?? 'unspecified'}
+
+
+
Updated
+
{data.ticket.data.updated_at ? formatDate(data.ticket.data.updated_at) : 'unknown'}
+
+
+
Created
+
{data.ticket.data.created_at ? formatDate(data.ticket.data.created_at) : 'unknown'}
+
+
+
Events
+
{data.ticket.data.event_count}
+
+
+
Artifacts
+
{data.ticket.data.artifact_count}
+
+
+
Source
+
{data.ticket.data.record_source}
+
+ {#if data.ticket.data.queued_at || data.ticket.data.queued_by} +
+
Queued
+
+ {data.ticket.data.queued_at ? formatDate(data.ticket.data.queued_at) : 'queued'}{data.ticket.data.queued_by ? ` by ${data.ticket.data.queued_by}` : ''} +
+
+ {/if} +
+ + {#if data.ticket.data.risk_flags.length > 0} +
+ {#each data.ticket.data.risk_flags as flag} + {flag} + {/each} +
+ {/if} + +
+
+

Body

+ {#if data.ticket.data.body_truncated} + truncated + {/if} +
+
{data.ticket.data.body || 'No body text is available.'}
+
+ {:else if data.ticket.error} +

{data.ticket.error}

+ {:else} +

Waiting for /api/w/{data.workspaceId}/tickets/{data.ticketId}

+ {/if} +
diff --git a/web/workspace/src/routes/w/[workspaceId]/tickets/[ticketId]/+page.ts b/web/workspace/src/routes/w/[workspaceId]/tickets/[ticketId]/+page.ts new file mode 100644 index 00000000..77e87077 --- /dev/null +++ b/web/workspace/src/routes/w/[workspaceId]/tickets/[ticketId]/+page.ts @@ -0,0 +1,17 @@ +import { loadJson, workspaceApiPath } from "$lib/workspace/api/http"; +import type { TicketDetail } from "$lib/workspace/sidebar/types"; +import type { PageLoad } from "./$types"; + +export const load = (async ({ fetch, params }) => { + const ticketId = params.ticketId; + const ticket = await loadJson( + fetch, + workspaceApiPath(params.workspaceId, `/tickets/${encodeURIComponent(ticketId)}`), + ); + + return { + workspaceId: params.workspaceId, + ticketId, + ticket, + }; +}) satisfies PageLoad;