merge: sync orchestration before queue 00001KVNKD56W
This commit is contained in:
commit
8407ce22b4
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
title: 'Workspace web UI: add sidebar navigation panel'
|
title: 'Workspace web UI: add sidebar navigation panel'
|
||||||
state: 'inprogress'
|
state: 'closed'
|
||||||
created_at: '2026-06-21T16:30:12Z'
|
created_at: '2026-06-21T16:30:12Z'
|
||||||
updated_at: '2026-06-21T16:37:34Z'
|
updated_at: '2026-06-21T17:01:46Z'
|
||||||
assignee: null
|
assignee: null
|
||||||
queued_by: 'workspace-panel'
|
queued_by: 'workspace-panel'
|
||||||
queued_at: '2026-06-21T16:35:06Z'
|
queued_at: '2026-06-21T16:35:06Z'
|
||||||
|
|
|
||||||
26
.yoi/tickets/00001KVNG9B9Z/resolution.md
Normal file
26
.yoi/tickets/00001KVNG9B9Z/resolution.md
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
Workspace web UI に sidebar navigation panel を追加し、Orchestrator worktree の `orchestration` branch に統合した。
|
||||||
|
|
||||||
|
主な成果:
|
||||||
|
- `web/workspace/src/lib/workspace-sidebar/` に sidebar components を追加:
|
||||||
|
- `WorkspaceSidebar.svelte`
|
||||||
|
- `RepositoriesNavSection.svelte`
|
||||||
|
- `ObjectivesNavSection.svelte`
|
||||||
|
- `WorkersNavSection.svelte`
|
||||||
|
- `types.ts`
|
||||||
|
- `web/workspace/src/routes/+page.svelte` を sidebar + main content の responsive two-column layout に更新。
|
||||||
|
- Sidebar header に workspace label/name と disabled settings placeholder を表示。
|
||||||
|
- `repositories`, `objectives`, `workers` sections を追加。
|
||||||
|
- Objectives section は `/api/objectives` を読み、title/state と empty/error states を section-local に表示。
|
||||||
|
- Workers section は `/api/workers` を読み、Worker label/state/status/role を表示し、Pod を primary UI naming として露出しない。
|
||||||
|
- Repository section は future Repository API に繋げやすい placeholder seam として実装。
|
||||||
|
- Host/Worker API/UI merge 後の `+page.svelte` conflict を解消し、main Host/Worker content と sidebar skeleton を両方維持。
|
||||||
|
- Backend/API authority, SSR, mutation/business logic は追加していない。
|
||||||
|
|
||||||
|
統合・検証:
|
||||||
|
- Merge commit: `613f4126 merge: workspace sidebar navigation`
|
||||||
|
- Implementation commits: `d3b8bdfd`, `4ab696b4`
|
||||||
|
- Reviewer final verdict: approve
|
||||||
|
- Validation passed: `git diff --check HEAD^1..HEAD`, `deno task check`, `deno task build`, `cargo test -p yoi-workspace-server`, `cargo run -p yoi -- ticket doctor`, and `nix build .#yoi --no-link`。
|
||||||
|
|
||||||
|
範囲外:
|
||||||
|
- Settings page, Repository CRUD/API, Objective edit/detail UI, Worker start/stop/attach controls, drag/drop/collapsible tree, auth, multi-workspace switcher は実装していない。
|
||||||
|
|
@ -135,3 +135,254 @@ Next action:
|
||||||
- Wait for Coder implementation report, then spawn Reviewer read-only for the implementation diff。
|
- Wait for Coder implementation report, then spawn Reviewer read-only for the implementation diff。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-21T16:54:08Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Coder conflict-resolution report received from `yoi-coder-00001KVNG9B9Z`.
|
||||||
|
|
||||||
|
Final implementation HEAD:
|
||||||
|
- `4ab696b4` merge commit from current `orchestration` into `impl/00001KVNG9B9Z-workspace-sidebar`
|
||||||
|
|
||||||
|
Relevant commits:
|
||||||
|
- `d3b8bdfd feat: add workspace sidebar skeleton`
|
||||||
|
- `4ab696b4` merge/conflict-resolution commit
|
||||||
|
|
||||||
|
Conflict resolution summary:
|
||||||
|
- Resolved `web/workspace/src/routes/+page.svelte` conflict after Host/Worker API/UI from `00001KVNEKH9Q` landed。
|
||||||
|
- Preserved merged Host/Worker main content/cards/table。
|
||||||
|
- Preserved sidebar navigation skeleton。
|
||||||
|
- Updated sidebar Workers section to consume available `/api/workers` response through typed `ListResponse<Worker>`。
|
||||||
|
- Kept Pod naming out of sidebar, presenting workers by label/state/status/role。
|
||||||
|
- Changes remain scoped to Workspace frontend。
|
||||||
|
|
||||||
|
Coder validation reported:
|
||||||
|
- `cd web/workspace && deno task check`: passed
|
||||||
|
- `cd web/workspace && deno task build`: passed
|
||||||
|
- `git diff --check`: passed
|
||||||
|
- `git diff --check orchestration..HEAD`: passed
|
||||||
|
- `cargo run -p yoi -- ticket doctor`: passed
|
||||||
|
- `nix build .#yoi --no-link`: passed
|
||||||
|
- `cargo test -p yoi-workspace-server`: not run because backend code was not changed during conflict resolution
|
||||||
|
|
||||||
|
Generated artifact cleanup:
|
||||||
|
- Coder removed generated/ignored frontend artifacts after validation:
|
||||||
|
- `web/workspace/.svelte-kit`
|
||||||
|
- `web/workspace/build`
|
||||||
|
- `web/workspace/node_modules`
|
||||||
|
|
||||||
|
Orchestrator evidence checked:
|
||||||
|
- Worktree clean on `impl/00001KVNG9B9Z-workspace-sidebar` at `4ab696b4`。
|
||||||
|
- Diff `orchestration..HEAD` touches expected frontend files only: new `web/workspace/src/lib/workspace-sidebar/*` components and `web/workspace/src/routes/+page.svelte`。
|
||||||
|
- `git diff --check orchestration..HEAD`: passed。
|
||||||
|
- Merge-tree conflict scan vs current `orchestration`: no conflict markers / no changed-in-both diagnostics。
|
||||||
|
|
||||||
|
Next action:
|
||||||
|
- Route to read-only Reviewer Pod for acceptance review。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: comment author: yoi-orchestrator at: 2026-06-21T16:54:49Z -->
|
||||||
|
|
||||||
|
## Comment
|
||||||
|
|
||||||
|
Review routing:
|
||||||
|
|
||||||
|
Spawned read-only Reviewer Pod:
|
||||||
|
- `yoi-reviewer-00001KVNG9B9Z-r1`
|
||||||
|
|
||||||
|
Review target:
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/00001KVNG9B9Z-workspace-sidebar`
|
||||||
|
- Branch: `impl/00001KVNG9B9Z-workspace-sidebar`
|
||||||
|
- Current implementation HEAD: `4ab696b4`
|
||||||
|
- Implementation commits:
|
||||||
|
- `d3b8bdfd feat: add workspace sidebar skeleton`
|
||||||
|
- `4ab696b4` merge/conflict-resolution commit from current `orchestration`
|
||||||
|
- Primary review diff: `orchestration..HEAD` inside implementation worktree, so review sees final sidebar changes on top of current Host/Worker API/UI state。
|
||||||
|
|
||||||
|
Reviewer scope:
|
||||||
|
- read-only `/home/hare/Projects/yoi`
|
||||||
|
|
||||||
|
Reviewer focus:
|
||||||
|
- Sidebar visibility/layout/responsive behavior。
|
||||||
|
- Workspace header/settings placeholder。
|
||||||
|
- Repositories/objectives/workers sections。
|
||||||
|
- Objective and worker section bounded loading/empty/error handling。
|
||||||
|
- Worker naming avoids Pod-primary UI。
|
||||||
|
- Main Host/Worker content from `00001KVNEKH9Q` remains preserved after conflict resolution。
|
||||||
|
- No backend authority/SSR/business logic changes。
|
||||||
|
|
||||||
|
Orchestrator will wait for reviewer verdict before integration。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: review author: yoi-reviewer-00001KVNG9B9Z-r1 at: 2026-06-21T17:00:16Z status: approve -->
|
||||||
|
|
||||||
|
## Review: approve
|
||||||
|
|
||||||
|
Verdict: `approve`
|
||||||
|
|
||||||
|
確認範囲:
|
||||||
|
- Ticket authority:
|
||||||
|
- `.yoi/tickets/00001KVNG9B9Z/item.md`
|
||||||
|
- `.yoi/tickets/00001KVNG9B9Z/thread.md`
|
||||||
|
- Implementation diff:
|
||||||
|
- `git diff --name-status orchestration..HEAD`
|
||||||
|
- implementation-only diff against merge-base `a607a1f20d0622b1f556c36cca155da68109fe25..HEAD`
|
||||||
|
- Frontend files:
|
||||||
|
- `web/workspace/src/routes/+page.svelte`
|
||||||
|
- `web/workspace/src/lib/workspace-sidebar/WorkspaceSidebar.svelte`
|
||||||
|
- `web/workspace/src/lib/workspace-sidebar/RepositoriesNavSection.svelte`
|
||||||
|
- `web/workspace/src/lib/workspace-sidebar/ObjectivesNavSection.svelte`
|
||||||
|
- `web/workspace/src/lib/workspace-sidebar/WorkersNavSection.svelte`
|
||||||
|
- `web/workspace/src/lib/workspace-sidebar/types.ts`
|
||||||
|
- API shape spot-check:
|
||||||
|
- `crates/workspace-server/src/server.rs`
|
||||||
|
- `crates/workspace-server/src/hosts.rs`
|
||||||
|
- `crates/workspace-server/src/records.rs`
|
||||||
|
|
||||||
|
Blocking issues:
|
||||||
|
- None。
|
||||||
|
|
||||||
|
Acceptance verification:
|
||||||
|
- Left sidebar is wired into `+page.svelte` and layout is separated from main content。
|
||||||
|
- Sidebar header shows workspace label/name and disabled settings placeholder。
|
||||||
|
- `repositories`, `objectives`, and `workers` sections are present。
|
||||||
|
- Objectives section fetches `/api/objectives` and handles loading/error/empty locally。
|
||||||
|
- Workers section fetches `/api/workers`, uses Worker naming in visible UI, and handles 404/empty/error locally。
|
||||||
|
- Main Host/Worker content from current orchestration branch is preserved。
|
||||||
|
- No backend/API authority changes were introduced by the implementation-only diff。
|
||||||
|
- Responsive CSS uses a two-column layout on wide viewports and single-column layout below 760px, with `min-width: 0`, wrapping, and table overflow containment。
|
||||||
|
|
||||||
|
Non-blocking concerns:
|
||||||
|
- `orchestration..HEAD` currently shows apparent `.yoi/tickets/00001KVNG9B9Z/item.md` / `thread.md` reversions because `orchestration` has newer review-routing/report commits after the implementation branch’s merge-base. The implementation-only diff against the merge-base is frontend-only, and `git merge-tree --write-tree orchestration HEAD` reports a clean merge. This should not block a normal merge, but avoid applying the literal `orchestration..HEAD` patch/squash in a way that drops newer ticket thread entries。
|
||||||
|
|
||||||
|
Reviewer validation:
|
||||||
|
- `git diff --check`: passed
|
||||||
|
- `git diff --check orchestration..HEAD`: passed
|
||||||
|
- `git diff --check $(git merge-base orchestration HEAD)..HEAD`: passed
|
||||||
|
- `cd web/workspace && deno task check`: passed (`svelte-check found 0 errors and 0 warnings`)
|
||||||
|
- `cd web/workspace && deno task build`: passed; static adapter wrote site to `build`
|
||||||
|
- `cargo run -p yoi -- ticket doctor`: passed (`doctor: ok`)
|
||||||
|
- `nix build .#yoi --no-link`: passed
|
||||||
|
- `cargo test -p yoi-workspace-server`: passed (`6 passed; 0 failed`)
|
||||||
|
- `git merge-tree --write-tree orchestration HEAD`: passed with status 0; produced merged tree `24e0e026264333fc63d60c0e2d649839445acb64`
|
||||||
|
|
||||||
|
Generated artifacts:
|
||||||
|
- Reviewer did not clean generated ignored artifacts after validation:
|
||||||
|
- `web/workspace/.svelte-kit/`
|
||||||
|
- `web/workspace/build/`
|
||||||
|
- `web/workspace/node_modules/`
|
||||||
|
- They are ignored and not tracked; `git ls-files --others --exclude-standard` reports `0`。
|
||||||
|
|
||||||
|
Worktree status:
|
||||||
|
- Clean tracked state on `impl/00001KVNG9B9Z-workspace-sidebar` at `4ab696b4`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-21T17:00:46Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Reviewer r1 approvalを受けて、実装ブランチを Orchestrator worktree の `orchestration` branch に統合した。
|
||||||
|
|
||||||
|
統合:
|
||||||
|
- Merge commit: `613f4126 merge: workspace sidebar navigation`
|
||||||
|
- Merged branch: `impl/00001KVNG9B9Z-workspace-sidebar`
|
||||||
|
- Implementation commits:
|
||||||
|
- `d3b8bdfd feat: add workspace sidebar skeleton`
|
||||||
|
- `4ab696b4` merge/conflict-resolution commit from current `orchestration`
|
||||||
|
|
||||||
|
Orchestrator worktree validation:
|
||||||
|
- `git diff --check HEAD^1..HEAD`: passed
|
||||||
|
- `cd web/workspace && rm -rf node_modules .svelte-kit build && deno task check`: passed (`svelte-check found 0 errors and 0 warnings`)
|
||||||
|
- `cd web/workspace && deno task build`: passed
|
||||||
|
- `cargo test -p yoi-workspace-server`: passed (`6 passed`)
|
||||||
|
- `cargo run -p yoi -- ticket doctor`: passed (`doctor: ok`)
|
||||||
|
- `nix build .#yoi --no-link`: passed
|
||||||
|
|
||||||
|
Validation generated ignored frontend artifacts in Orchestrator worktree:
|
||||||
|
- `web/workspace/node_modules/`
|
||||||
|
- `web/workspace/.svelte-kit/`
|
||||||
|
- `web/workspace/build/`
|
||||||
|
|
||||||
|
These were removed after validation. Final Orchestrator worktree status after validation cleanup is clean on `orchestration` at `613f4126`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-21T17:00:59Z from: inprogress to: done reason: implementation_merged_and_validated field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Reviewer approval、Orchestrator worktree への統合、Deno check/build、workspace-server tests、Ticket doctor、Nix build が完了したため `done` に遷移する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: hare at: 2026-06-21T17:01:13Z from: done to: closed reason: closed field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を closed にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: close author: hare at: 2026-06-21T17:01:13Z status: closed -->
|
||||||
|
|
||||||
|
## 完了
|
||||||
|
|
||||||
|
Workspace web UI に sidebar navigation panel を追加し、Orchestrator worktree の `orchestration` branch に統合した。
|
||||||
|
|
||||||
|
主な成果:
|
||||||
|
- `web/workspace/src/lib/workspace-sidebar/` に sidebar components を追加:
|
||||||
|
- `WorkspaceSidebar.svelte`
|
||||||
|
- `RepositoriesNavSection.svelte`
|
||||||
|
- `ObjectivesNavSection.svelte`
|
||||||
|
- `WorkersNavSection.svelte`
|
||||||
|
- `types.ts`
|
||||||
|
- `web/workspace/src/routes/+page.svelte` を sidebar + main content の responsive two-column layout に更新。
|
||||||
|
- Sidebar header に workspace label/name と disabled settings placeholder を表示。
|
||||||
|
- `repositories`, `objectives`, `workers` sections を追加。
|
||||||
|
- Objectives section は `/api/objectives` を読み、title/state と empty/error states を section-local に表示。
|
||||||
|
- Workers section は `/api/workers` を読み、Worker label/state/status/role を表示し、Pod を primary UI naming として露出しない。
|
||||||
|
- Repository section は future Repository API に繋げやすい placeholder seam として実装。
|
||||||
|
- Host/Worker API/UI merge 後の `+page.svelte` conflict を解消し、main Host/Worker content と sidebar skeleton を両方維持。
|
||||||
|
- Backend/API authority, SSR, mutation/business logic は追加していない。
|
||||||
|
|
||||||
|
統合・検証:
|
||||||
|
- Merge commit: `613f4126 merge: workspace sidebar navigation`
|
||||||
|
- Implementation commits: `d3b8bdfd`, `4ab696b4`
|
||||||
|
- Reviewer final verdict: approve
|
||||||
|
- Validation passed: `git diff --check HEAD^1..HEAD`, `deno task check`, `deno task build`, `cargo test -p yoi-workspace-server`, `cargo run -p yoi -- ticket doctor`, and `nix build .#yoi --no-link`。
|
||||||
|
|
||||||
|
範囲外:
|
||||||
|
- Settings page, Repository CRUD/API, Objective edit/detail UI, Worker start/stop/attach controls, drag/drop/collapsible tree, auth, multi-workspace switcher は実装していない。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-21T17:01:46Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Post-close cleanup completed。
|
||||||
|
|
||||||
|
- Stopped child Pods and reclaimed scope:
|
||||||
|
- `yoi-coder-00001KVNG9B9Z`
|
||||||
|
- `yoi-reviewer-00001KVNG9B9Z-r1`
|
||||||
|
- Removed ignored frontend validation artifacts from child worktree before worktree removal:
|
||||||
|
- `web/workspace/node_modules/`
|
||||||
|
- `web/workspace/.svelte-kit/`
|
||||||
|
- `web/workspace/build/`
|
||||||
|
- Removed implementation worktree:
|
||||||
|
- `/home/hare/Projects/yoi/.worktree/00001KVNG9B9Z-workspace-sidebar`
|
||||||
|
- Deleted implementation branch:
|
||||||
|
- `impl/00001KVNG9B9Z-workspace-sidebar`
|
||||||
|
- Orchestrator worktree remains clean on `orchestration` at `c29eba0c`。
|
||||||
|
|
||||||
|
Root/original workspace was not used for merge/validation/cleanup operations。
|
||||||
|
|
||||||
|
Follow-up note:
|
||||||
|
- `00001KVNGJPRG` had been left queued behind this sidebar work due `do_not_parallelize`; this blocker is now cleared for re-routing on top of the settled sidebar/navigation structure。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
{"id":"orch-plan-20260621-164056-1","ticket_id":"00001KVNGJPRG","kind":"do_not_parallelize","related_ticket":"00001KVNG9B9Z","note":"Repository/objective pages and sidebar navigation both modify the Workspace SPA navigation/layout surface, especially `web/workspace/src/routes/+page.svelte` and route/component structure. Start this Ticket after `00001KVNG9B9Z` lands or after its implementation diff is known enough to route a non-conflicting continuation.","author":"yoi-orchestrator","at":"2026-06-21T16:40:56Z"}
|
||||||
|
{"id":"orch-plan-20260621-170253-2","ticket_id":"00001KVNGJPRG","kind":"accepted_plan","accepted_plan":{"summary":"Add read-only current-workspace Repository APIs/pages with bounded Git summary/log and Ticket Kanban, plus Objective list page and sidebar links, preserving filesystem Ticket/Objective authority and static SPA boundaries.","branch":"impl/00001KVNGJPRG-repository-objective-pages","worktree":"/home/hare/Projects/yoi/.worktree/00001KVNGJPRG-repository-objective-pages","role_plan":"Orchestrator creates a dedicated child worktree from current sidebar-integrated `orchestration` and spawns a narrow-scope Coder. Reviewer will be spawned read-only after Coder reports implementation commit(s). After approval, Orchestrator integrates into `orchestration`, validates workspace-server/frontend/Nix, records closure, and cleans only the child worktree/branch."},"author":"yoi-orchestrator","at":"2026-06-21T17:02:53Z"}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
title: 'Workspace web: repository and objective pages'
|
title: 'Workspace web: repository and objective pages'
|
||||||
state: 'queued'
|
state: 'closed'
|
||||||
created_at: '2026-06-21T16:35:19Z'
|
created_at: '2026-06-21T16:35:19Z'
|
||||||
updated_at: '2026-06-21T16:40:35Z'
|
updated_at: '2026-06-21T17:31:43Z'
|
||||||
assignee: null
|
assignee: null
|
||||||
queued_by: 'workspace-panel'
|
queued_by: 'workspace-panel'
|
||||||
queued_at: '2026-06-21T16:40:35Z'
|
queued_at: '2026-06-21T16:40:35Z'
|
||||||
|
|
|
||||||
30
.yoi/tickets/00001KVNGJPRG/resolution.md
Normal file
30
.yoi/tickets/00001KVNGJPRG/resolution.md
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
Workspace web に Repository / Objective pages を追加し、Orchestrator worktree の `orchestration` branch に統合した。
|
||||||
|
|
||||||
|
主な成果:
|
||||||
|
- Read-only Repository backend APIs を追加:
|
||||||
|
- `/api/repositories`
|
||||||
|
- `/api/repositories/local`
|
||||||
|
- `/api/repositories/local/log`
|
||||||
|
- `/api/repositories/local/tickets`
|
||||||
|
- Current workspace root を local Repository として扱う bounded repository summary を追加。
|
||||||
|
- Git repository では bounded branch/head/root/dirty/remote/recent log summary を返す。
|
||||||
|
- Non-Git workspace では `git.status = unavailable` と bounded diagnostics に degrade。
|
||||||
|
- Git log summary は recent commit hash/subject/author/timestamp に限定し、diff/patch/file content/blame/config は読まない。
|
||||||
|
- Remote URL summary は URL-scheme userinfo を redact。
|
||||||
|
- Read-only Ticket Kanban を Ticket state ごとに grouping し、workspace-local Ticket fallback diagnostic を含めた。
|
||||||
|
- Objective list summaries を filesystem Objective records から追加。
|
||||||
|
- Static SPA に hash-navigation Repository / Objectives pages を追加。
|
||||||
|
- Sidebar Repository/Objectives links を新 pages に接続。
|
||||||
|
- Repository page に summary, Git summary/log, diagnostics, read-only Ticket Kanban を表示。
|
||||||
|
- Objective page に title/state/updated_at/summary と detail placeholder links を表示。
|
||||||
|
- Ticket / Objective canonical authority remains filesystem read-through records; mutation API / DB canonical migration は追加していない。
|
||||||
|
|
||||||
|
統合・検証:
|
||||||
|
- Merge commit: `7ee702b1 merge: repository objective pages`
|
||||||
|
- Implementation commit: `ceb1ee3b feat: add repository objective pages`
|
||||||
|
- Reviewer final verdict: approve
|
||||||
|
- Validation passed: `cargo fmt --check`, `git diff --check HEAD^1..HEAD`, `cargo test -p yoi-workspace-server`, `cargo check -p yoi-workspace-server`, `deno task check`, `deno task build`, `cargo run -p yoi -- ticket doctor`, and `nix build .#yoi --no-link`。
|
||||||
|
|
||||||
|
範囲外 / follow-up notes:
|
||||||
|
- Repository CRUD/API, Objective edit/detail mutation, full Git browser/diff/file views, drag/drop Kanban, and write APIs were not implemented。
|
||||||
|
- Reviewer noted possible follow-ups: keep `#/objectives` sidebar link visible even on objective empty/error states, and further tighten remote URL sanitization for query-param or SCP-like token forms if needed。
|
||||||
|
|
@ -30,4 +30,385 @@ Marked ready by `yoi ticket state`.
|
||||||
Ticket を `workspace-panel` が queued にしました。
|
Ticket を `workspace-panel` が queued にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: yoi-orchestrator at: 2026-06-21T16:41:09Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Routing decision: `wait_do_not_parallelize_with_sidebar`
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- Ticket body is implementation-ready in isolation, but it is directly coupled to the currently active sidebar navigation work `00001KVNG9B9Z`。
|
||||||
|
- Relations show this Ticket is related to `00001KVNG9B9Z` with note: “Sidebar navigation should link to repository and objective pages”。
|
||||||
|
- `00001KVNG9B9Z` is already `inprogress`, has a live Coder Pod, and is expected to change `web/workspace/src/routes/+page.svelte` / sidebar component structure。
|
||||||
|
- Starting this Ticket in parallel now would likely produce overlapping route/layout/sidebar changes and non-trivial semantic merge work, rather than independent implementation。
|
||||||
|
- Host/Worker API work `00001KVNEKH9Q` just landed, further increasing the need to base the page/navigation work on current SPA state。
|
||||||
|
|
||||||
|
Decision:
|
||||||
|
- Do not create a worktree or spawn Coder for `00001KVNGJPRG` yet。
|
||||||
|
- Leave Ticket `queued` and record an orchestration `do_not_parallelize` note with `00001KVNG9B9Z`。
|
||||||
|
- Re-route this Ticket after sidebar work lands, or if sidebar implementation report shows a stable component boundary that makes repository/objective pages non-conflicting。
|
||||||
|
|
||||||
|
No implementation side effects were performed for this Ticket。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: yoi-orchestrator at: 2026-06-21T17:02:35Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Routing decision: `implementation_ready_after_sidebar_landed`
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- This Ticket was previously left queued due `do_not_parallelize` with `00001KVNG9B9Z` because repository/objective pages and sidebar navigation both touched the Workspace SPA navigation/layout surface。
|
||||||
|
- `00001KVNG9B9Z` is now `closed` and integrated (`613f4126 merge: workspace sidebar navigation`), so the sequencing blocker is cleared。
|
||||||
|
- Related bootstrap Ticket `00001KVMFFYVX` is also closed/integrated。
|
||||||
|
- Ticket body has concrete backend/API/frontend requirements, acceptance criteria, and validation requirements。
|
||||||
|
- Relations are `related` only; no blocking relation remains。
|
||||||
|
- Orchestrator worktree is clean on `orchestration` at `eb2e5907`; target worktree / branch is not present。
|
||||||
|
|
||||||
|
IntentPacket:
|
||||||
|
|
||||||
|
Intent:
|
||||||
|
- Add Repository and Objective pages reachable from the Workspace sidebar, using filesystem read-through Ticket/Objective authority and bounded read-only Repository/Git summaries。
|
||||||
|
|
||||||
|
Binding decisions / invariants:
|
||||||
|
- Ticket / Objective canonical authority remains existing filesystem records; do not migrate canonical writes to SQLite or add mutation APIs。
|
||||||
|
- Repository page is read-only initial slice for current workspace root as local Repository。
|
||||||
|
- Git info and log summaries must be bounded; do not expose full diffs, file contents, blame, or secret-like config。
|
||||||
|
- Repository Ticket Kanban is read-only and grouped by Ticket state; no drag/drop or state mutation。
|
||||||
|
- Objective list uses existing filesystem read-through `/api/objectives` data, with detail links/placeholders as practical。
|
||||||
|
- Frontend remains static SPA; no SSR/business authority。
|
||||||
|
- Sidebar links should use the now-landed navigation/component structure。
|
||||||
|
- API failures, non-Git repo, and empty state must be section-local。
|
||||||
|
|
||||||
|
Requirements / acceptance criteria:
|
||||||
|
- Add read-only Repository list/detail/summary API or minimal workspace repository summary。
|
||||||
|
- For Git repository: bounded branch/head/root/dirty/remote/recent log summary。
|
||||||
|
- For non-Git repository: safe `kind = local` / git unavailable diagnostic。
|
||||||
|
- Add bounded Git log summary API returning recent N commit hash/subject/author/timestamp only。
|
||||||
|
- Add Repository Ticket Kanban read model grouped by Ticket state, with safe fallback to workspace-local tickets when target metadata is absent。
|
||||||
|
- Add Repository page showing summary, Git summary/log, and Ticket Kanban。
|
||||||
|
- Add Objective list page showing objective title/state/updated_at/summary and detail link/placeholder if feasible。
|
||||||
|
- Sidebar repositories/objectives links navigate to these pages。
|
||||||
|
- Deno check/build and backend validation pass。
|
||||||
|
|
||||||
|
Implementation latitude:
|
||||||
|
- Use SvelteKit static SPA client-side routing/hash/router approach or simple stateful navigation if current skeleton lacks route generation; keep static output compatible。
|
||||||
|
- Backend API names may be pragmatic, e.g. `/api/repositories`, `/api/repositories/{id}`, `/api/repositories/{id}/log`, `/api/repositories/{id}/tickets`。
|
||||||
|
- Current workspace root can be the only repository for this slice。
|
||||||
|
- Ticket Kanban fallback can group all workspace tickets by state with diagnostic that target metadata is not yet available。
|
||||||
|
- Keep UI simple and avoid broad design-system churn。
|
||||||
|
|
||||||
|
Escalate if:
|
||||||
|
- Implementing repository APIs would require reading file contents/diffs or unsafe Git config/secrets。
|
||||||
|
- Objective detail requires mutation or schema changes。
|
||||||
|
- Static SPA route approach conflicts with SvelteKit adapter/static constraints。
|
||||||
|
- Sidebar/navigation architecture from `00001KVNG9B9Z` cannot support pages without broad rewrite。
|
||||||
|
|
||||||
|
Validation plan:
|
||||||
|
- `cargo fmt --check`
|
||||||
|
- `cargo test -p yoi-workspace-server`
|
||||||
|
- `cargo check -p yoi-workspace-server`
|
||||||
|
- `cd web/workspace && deno task check && deno task build`
|
||||||
|
- `git diff --check`
|
||||||
|
- `cargo run -p yoi -- ticket doctor`
|
||||||
|
- `nix build .#yoi --no-link`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-21T17:02:42Z from: queued to: inprogress reason: sidebar_dependency_landed_unblocked_implementation field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Previously recorded `do_not_parallelize` sequencing reason is cleared because sidebar navigation Ticket `00001KVNG9B9Z` is closed and integrated. Human queue authorization remains applicable, and the Ticket has concrete implementation requirements, so Orchestrator accepts implementation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-21T17:03:39Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Implementation start report:
|
||||||
|
|
||||||
|
- Created child implementation worktree:
|
||||||
|
- `/home/hare/Projects/yoi/.worktree/00001KVNGJPRG-repository-objective-pages`
|
||||||
|
- Created branch:
|
||||||
|
- `impl/00001KVNGJPRG-repository-objective-pages`
|
||||||
|
- Base commit:
|
||||||
|
- `0f7e78c1 ticket: accept repository objective pages`
|
||||||
|
- Confirmed tracked Ticket project records are visible in the child worktree。
|
||||||
|
- Confirmed `.yoi/memory` has no tracked/untracked entries in the child worktree check。
|
||||||
|
- Spawned sibling Coder Pod:
|
||||||
|
- `yoi-coder-00001KVNGJPRG`
|
||||||
|
- Coder scope:
|
||||||
|
- read: `/home/hare/Projects/yoi`
|
||||||
|
- write: `/home/hare/Projects/yoi/.worktree/00001KVNGJPRG-repository-objective-pages`
|
||||||
|
|
||||||
|
Next action:
|
||||||
|
- Wait for Coder implementation report, then spawn Reviewer read-only for the implementation diff。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-21T17:23:16Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Coder implementation report received from `yoi-coder-00001KVNGJPRG`.
|
||||||
|
|
||||||
|
Implementation commit:
|
||||||
|
- `ceb1ee3b feat: add repository objective pages`
|
||||||
|
|
||||||
|
Changed areas:
|
||||||
|
- Backend repository read APIs:
|
||||||
|
- Added `crates/workspace-server/src/repositories.rs`。
|
||||||
|
- Added bounded local repository/Git summary and log reader。
|
||||||
|
- Added endpoints:
|
||||||
|
- `/api/repositories`
|
||||||
|
- `/api/repositories/local`
|
||||||
|
- `/api/repositories/local/log`
|
||||||
|
- `/api/repositories/local/tickets`
|
||||||
|
- Added read-only Ticket Kanban grouping by Ticket state with workspace-local fallback diagnostic。
|
||||||
|
- Added Objective list summaries from existing filesystem Objective records。
|
||||||
|
- Frontend Workspace SPA:
|
||||||
|
- Added hash-navigation pages for Repository and Objectives。
|
||||||
|
- Wired sidebar Repository/Objectives links to the new pages。
|
||||||
|
- Added Repository summary, Git summary/log, diagnostics, and read-only Ticket Kanban UI。
|
||||||
|
- Added Objective list with title/state/updated_at/summary and detail placeholder links。
|
||||||
|
- Tests:
|
||||||
|
- Extended workspace-server API tests for repository endpoints and Objective summary。
|
||||||
|
- Added Git URL sanitization/log parsing unit tests。
|
||||||
|
|
||||||
|
Coder validation reported:
|
||||||
|
- `cargo fmt --all`: passed
|
||||||
|
- `cargo fmt --check`: passed
|
||||||
|
- `cargo test -p yoi-workspace-server`: passed
|
||||||
|
- `cargo check -p yoi-workspace-server`: passed
|
||||||
|
- `cd web/workspace && deno task check`: passed
|
||||||
|
- `cd web/workspace && deno task build`: passed
|
||||||
|
- `git diff --check`: passed
|
||||||
|
- `cargo run -p yoi -- ticket doctor`: passed
|
||||||
|
- `nix build .#yoi --no-link`: passed after staging newly added `repositories.rs` so Git-backed flake source filtering included it。
|
||||||
|
|
||||||
|
Generated artifact cleanup:
|
||||||
|
- Coder removed `web/workspace/build` and `web/workspace/.svelte-kit`。
|
||||||
|
- Orchestrator additionally removed ignored `web/workspace/node_modules/` before Reviewer routing。
|
||||||
|
|
||||||
|
Orchestrator evidence checked:
|
||||||
|
- Worktree clean on `impl/00001KVNGJPRG-repository-objective-pages` at `ceb1ee3b`。
|
||||||
|
- Diff `0f7e78c1..HEAD` touches 8 expected backend/frontend files, about 1228 insertions / 142 deletions。
|
||||||
|
- `git diff --check 0f7e78c1..HEAD`: passed。
|
||||||
|
- No generated frontend artifacts remain in child worktree status。
|
||||||
|
|
||||||
|
Next action:
|
||||||
|
- Route to read-only Reviewer Pod for acceptance review。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: comment author: yoi-orchestrator at: 2026-06-21T17:23:58Z -->
|
||||||
|
|
||||||
|
## Comment
|
||||||
|
|
||||||
|
Review routing:
|
||||||
|
|
||||||
|
Spawned read-only Reviewer Pod:
|
||||||
|
- `yoi-reviewer-00001KVNGJPRG-r1`
|
||||||
|
|
||||||
|
Review target:
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/00001KVNGJPRG-repository-objective-pages`
|
||||||
|
- Branch: `impl/00001KVNGJPRG-repository-objective-pages`
|
||||||
|
- Baseline: `0f7e78c1`
|
||||||
|
- Implementation commit: `ceb1ee3b feat: add repository objective pages`
|
||||||
|
- Diff: `0f7e78c1..ceb1ee3b`
|
||||||
|
|
||||||
|
Reviewer scope:
|
||||||
|
- read-only `/home/hare/Projects/yoi`
|
||||||
|
|
||||||
|
Reviewer focus:
|
||||||
|
- Read-only bounded Repository APIs and Git summaries。
|
||||||
|
- No diffs/file contents/blame/secret-like config exposure。
|
||||||
|
- Sanitized remote URL summary。
|
||||||
|
- Non-Git degradation。
|
||||||
|
- Read-only Ticket Kanban grouped by state。
|
||||||
|
- Objective list uses filesystem read-through records。
|
||||||
|
- Static SPA navigation/pages and section-local error handling。
|
||||||
|
- Generated artifact / Nix source filtering hygiene。
|
||||||
|
|
||||||
|
Orchestrator will wait for reviewer verdict before integration。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: review author: yoi-reviewer-00001KVNGJPRG-r1 at: 2026-06-21T17:30:09Z status: approve -->
|
||||||
|
|
||||||
|
## Review: approve
|
||||||
|
|
||||||
|
Verdict: `approve`
|
||||||
|
|
||||||
|
確認範囲:
|
||||||
|
- Ticket authority:
|
||||||
|
- `.yoi/tickets/00001KVNGJPRG/item.md`
|
||||||
|
- `.yoi/tickets/00001KVNGJPRG/thread.md`
|
||||||
|
- Diff `0f7e78c1..ceb1ee3b`:
|
||||||
|
- `crates/workspace-server/src/lib.rs`
|
||||||
|
- `crates/workspace-server/src/records.rs`
|
||||||
|
- `crates/workspace-server/src/repositories.rs`
|
||||||
|
- `crates/workspace-server/src/server.rs`
|
||||||
|
- `web/workspace/src/lib/workspace-sidebar/ObjectivesNavSection.svelte`
|
||||||
|
- `web/workspace/src/lib/workspace-sidebar/RepositoriesNavSection.svelte`
|
||||||
|
- `web/workspace/src/lib/workspace-sidebar/types.ts`
|
||||||
|
- `web/workspace/src/routes/+page.svelte`
|
||||||
|
|
||||||
|
Blocking issues:
|
||||||
|
- None found。
|
||||||
|
|
||||||
|
Acceptance verification:
|
||||||
|
- Repository APIs are read-only `GET` routes only。
|
||||||
|
- Git commands use fixed argument vectors through `Command::new("git").arg("-C").arg(workspace_root).args(args)`; no shell interpolation。
|
||||||
|
- Git summary/log is bounded:
|
||||||
|
- log limit clamps to `1..=50`
|
||||||
|
- command output capped at `4096`
|
||||||
|
- fields capped at `240`, hash at `40`
|
||||||
|
- no diff/patch/file content/blame/config commands are used。
|
||||||
|
- Remote URL summary strips URL-scheme userinfo and marks `redacted`。
|
||||||
|
- Non-Git workspaces return `git.status = "unavailable"` plus diagnostics rather than failing the Repository page model。
|
||||||
|
- Ticket Kanban reads filesystem Ticket summaries and groups by state; no Ticket mutation route/UI was added。
|
||||||
|
- Objective list continues to use filesystem read-through `/api/objectives`; no Objective mutation/API authority added。
|
||||||
|
- Frontend remains static SPA/hash navigation; no SSR/business authority introduced。
|
||||||
|
- Nix build succeeds with the new Rust source file。
|
||||||
|
|
||||||
|
Non-blocking concerns:
|
||||||
|
- `ObjectivesNavSection.svelte` only shows the “All objectives” sidebar link when objectives are successfully loaded and non-empty. In an empty/error state, the page is still reachable from hero links, but the sidebar itself does not provide a stable top-level Objectives link. Consider keeping `#/objectives` visible in all non-loading states in a follow-up。
|
||||||
|
- Remote sanitization removes URL-scheme userinfo, but does not attempt query-param or SCP-like token redaction. Acceptable for the initial bounded summary, but worth tightening if remote URLs may include local secret-bearing config。
|
||||||
|
|
||||||
|
Reviewer validation:
|
||||||
|
- `cargo fmt --check`: passed
|
||||||
|
- `git diff --check 0f7e78c1..HEAD`: passed
|
||||||
|
- `cargo test -p yoi-workspace-server`: passed (`8 passed`)
|
||||||
|
- `cargo check -p yoi-workspace-server`: passed
|
||||||
|
- `cd web/workspace && deno task check`: passed (`svelte-check found 0 errors and 0 warnings`)
|
||||||
|
- `cd web/workspace && deno task build`: passed
|
||||||
|
- `cargo run -p yoi -- ticket doctor`: passed (`doctor: ok`)
|
||||||
|
- `nix build .#yoi --no-link`: passed
|
||||||
|
|
||||||
|
Generated artifacts:
|
||||||
|
- Reviewer did not clean generated ignored artifacts after validation。
|
||||||
|
- Ignored generated artifacts present:
|
||||||
|
- `target/`
|
||||||
|
- `web/workspace/.svelte-kit/`
|
||||||
|
- `web/workspace/build/`
|
||||||
|
- `web/workspace/node_modules/`
|
||||||
|
- No untracked non-ignored files。
|
||||||
|
|
||||||
|
Worktree status:
|
||||||
|
- Clean tracked/non-ignored state on `impl/00001KVNGJPRG-repository-objective-pages` at `ceb1ee3b`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-21T17:30:51Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Reviewer r1 approvalを受けて、実装ブランチを Orchestrator worktree の `orchestration` branch に統合した。
|
||||||
|
|
||||||
|
統合:
|
||||||
|
- Merge commit: `7ee702b1 merge: repository objective pages`
|
||||||
|
- Merged branch: `impl/00001KVNGJPRG-repository-objective-pages`
|
||||||
|
- Implementation commit:
|
||||||
|
- `ceb1ee3b feat: add repository objective pages`
|
||||||
|
|
||||||
|
Orchestrator worktree validation:
|
||||||
|
- `cargo fmt --check`: passed
|
||||||
|
- `git diff --check HEAD^1..HEAD`: passed
|
||||||
|
- `cargo test -p yoi-workspace-server`: passed (`8 passed`)
|
||||||
|
- `cargo check -p yoi-workspace-server`: passed
|
||||||
|
- `cd web/workspace && rm -rf node_modules .svelte-kit build && deno task check`: passed (`svelte-check found 0 errors and 0 warnings`)
|
||||||
|
- `cd web/workspace && deno task build`: passed
|
||||||
|
- `cargo run -p yoi -- ticket doctor`: passed (`doctor: ok`)
|
||||||
|
- `nix build .#yoi --no-link`: passed
|
||||||
|
|
||||||
|
Validation generated ignored frontend artifacts in Orchestrator worktree:
|
||||||
|
- `web/workspace/node_modules/`
|
||||||
|
- `web/workspace/.svelte-kit/`
|
||||||
|
- `web/workspace/build/`
|
||||||
|
|
||||||
|
These were removed after validation. Final Orchestrator worktree status after validation cleanup is clean on `orchestration` at `7ee702b1`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-21T17:30:58Z from: inprogress to: done reason: implementation_merged_and_validated field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Reviewer approval、Orchestrator worktree への統合、workspace-server tests/check、Deno check/build、Ticket doctor、Nix build が完了したため `done` に遷移する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: hare at: 2026-06-21T17:31:14Z from: done to: closed reason: closed field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を closed にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: close author: hare at: 2026-06-21T17:31:14Z status: closed -->
|
||||||
|
|
||||||
|
## 完了
|
||||||
|
|
||||||
|
Workspace web に Repository / Objective pages を追加し、Orchestrator worktree の `orchestration` branch に統合した。
|
||||||
|
|
||||||
|
主な成果:
|
||||||
|
- Read-only Repository backend APIs を追加:
|
||||||
|
- `/api/repositories`
|
||||||
|
- `/api/repositories/local`
|
||||||
|
- `/api/repositories/local/log`
|
||||||
|
- `/api/repositories/local/tickets`
|
||||||
|
- Current workspace root を local Repository として扱う bounded repository summary を追加。
|
||||||
|
- Git repository では bounded branch/head/root/dirty/remote/recent log summary を返す。
|
||||||
|
- Non-Git workspace では `git.status = unavailable` と bounded diagnostics に degrade。
|
||||||
|
- Git log summary は recent commit hash/subject/author/timestamp に限定し、diff/patch/file content/blame/config は読まない。
|
||||||
|
- Remote URL summary は URL-scheme userinfo を redact。
|
||||||
|
- Read-only Ticket Kanban を Ticket state ごとに grouping し、workspace-local Ticket fallback diagnostic を含めた。
|
||||||
|
- Objective list summaries を filesystem Objective records から追加。
|
||||||
|
- Static SPA に hash-navigation Repository / Objectives pages を追加。
|
||||||
|
- Sidebar Repository/Objectives links を新 pages に接続。
|
||||||
|
- Repository page に summary, Git summary/log, diagnostics, read-only Ticket Kanban を表示。
|
||||||
|
- Objective page に title/state/updated_at/summary と detail placeholder links を表示。
|
||||||
|
- Ticket / Objective canonical authority remains filesystem read-through records; mutation API / DB canonical migration は追加していない。
|
||||||
|
|
||||||
|
統合・検証:
|
||||||
|
- Merge commit: `7ee702b1 merge: repository objective pages`
|
||||||
|
- Implementation commit: `ceb1ee3b feat: add repository objective pages`
|
||||||
|
- Reviewer final verdict: approve
|
||||||
|
- Validation passed: `cargo fmt --check`, `git diff --check HEAD^1..HEAD`, `cargo test -p yoi-workspace-server`, `cargo check -p yoi-workspace-server`, `deno task check`, `deno task build`, `cargo run -p yoi -- ticket doctor`, and `nix build .#yoi --no-link`。
|
||||||
|
|
||||||
|
範囲外 / follow-up notes:
|
||||||
|
- Repository CRUD/API, Objective edit/detail mutation, full Git browser/diff/file views, drag/drop Kanban, and write APIs were not implemented。
|
||||||
|
- Reviewer noted possible follow-ups: keep `#/objectives` sidebar link visible even on objective empty/error states, and further tighten remote URL sanitization for query-param or SCP-like token forms if needed。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-21T17:31:43Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Post-close cleanup completed。
|
||||||
|
|
||||||
|
- Stopped child Pods and reclaimed scope:
|
||||||
|
- `yoi-coder-00001KVNGJPRG`
|
||||||
|
- `yoi-reviewer-00001KVNGJPRG-r1`
|
||||||
|
- Removed ignored frontend validation artifacts from child worktree before worktree removal:
|
||||||
|
- `web/workspace/node_modules/`
|
||||||
|
- `web/workspace/.svelte-kit/`
|
||||||
|
- `web/workspace/build/`
|
||||||
|
- Removed implementation worktree:
|
||||||
|
- `/home/hare/Projects/yoi/.worktree/00001KVNGJPRG-repository-objective-pages`
|
||||||
|
- Deleted implementation branch:
|
||||||
|
- `impl/00001KVNGJPRG-repository-objective-pages`
|
||||||
|
- Orchestrator worktree remains clean on `orchestration` at `4b1f1e59`。
|
||||||
|
|
||||||
|
Root/original workspace was not used for merge/validation/cleanup operations。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,17 @@
|
||||||
|
|
||||||
pub mod hosts;
|
pub mod hosts;
|
||||||
pub mod records;
|
pub mod records;
|
||||||
|
pub mod repositories;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
|
|
||||||
pub use records::{
|
pub use records::{
|
||||||
LocalProjectRecordReader, ObjectiveDetail, ObjectiveSummary, TicketDetail, TicketSummary,
|
LocalProjectRecordReader, ObjectiveDetail, ObjectiveSummary, TicketDetail, TicketSummary,
|
||||||
};
|
};
|
||||||
|
pub use repositories::{
|
||||||
|
GitCommitSummary, GitRemoteSummary, GitRepositorySummary, LocalRepositoryReader,
|
||||||
|
RepositoryLogRead, RepositorySummary,
|
||||||
|
};
|
||||||
pub use server::{AuthConfig, ServerConfig, WorkspaceApi, build_router, serve};
|
pub use server::{AuthConfig, ServerConfig, WorkspaceApi, build_router, serve};
|
||||||
pub use store::{ControlPlaneStore, SqliteWorkspaceStore, WorkspaceRecord};
|
pub use store::{ControlPlaneStore, SqliteWorkspaceStore, WorkspaceRecord};
|
||||||
|
|
||||||
|
|
@ -33,6 +38,8 @@ pub enum Error {
|
||||||
MissingFrontmatter(String),
|
MissingFrontmatter(String),
|
||||||
#[error("unknown local host `{0}`")]
|
#[error("unknown local host `{0}`")]
|
||||||
UnknownHost(String),
|
UnknownHost(String),
|
||||||
|
#[error("unknown local repository `{0}`")]
|
||||||
|
UnknownRepository(String),
|
||||||
#[error("store error: {0}")]
|
#[error("store error: {0}")]
|
||||||
Store(String),
|
Store(String),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use ticket::{LocalTicketBackend, TicketFilter, TicketIdOrSlug};
|
||||||
use crate::{Error, Result};
|
use crate::{Error, Result};
|
||||||
|
|
||||||
const DETAIL_BODY_LIMIT: usize = 64 * 1024;
|
const DETAIL_BODY_LIMIT: usize = 64 * 1024;
|
||||||
|
const SUMMARY_BODY_LIMIT: usize = 240;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct LocalProjectRecordReader {
|
pub struct LocalProjectRecordReader {
|
||||||
|
|
@ -201,6 +202,7 @@ pub struct ObjectiveSummary {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub state: String,
|
pub state: String,
|
||||||
pub updated_at: Option<String>,
|
pub updated_at: Option<String>,
|
||||||
|
pub summary: String,
|
||||||
pub linked_tickets: Vec<String>,
|
pub linked_tickets: Vec<String>,
|
||||||
pub record_source: String,
|
pub record_source: String,
|
||||||
}
|
}
|
||||||
|
|
@ -233,13 +235,14 @@ struct ObjectiveFrontmatter {
|
||||||
fn read_objective_summary(path: &Path, id: &str) -> Result<ObjectiveSummary> {
|
fn read_objective_summary(path: &Path, id: &str) -> Result<ObjectiveSummary> {
|
||||||
validate_project_id(id)?;
|
validate_project_id(id)?;
|
||||||
let raw = fs::read_to_string(path.join("item.md"))?;
|
let raw = fs::read_to_string(path.join("item.md"))?;
|
||||||
let (frontmatter, _) = split_frontmatter(&raw, id)?;
|
let (frontmatter, body) = split_frontmatter(&raw, id)?;
|
||||||
let meta: ObjectiveFrontmatter = serde_yaml::from_str(frontmatter)?;
|
let meta: ObjectiveFrontmatter = serde_yaml::from_str(frontmatter)?;
|
||||||
Ok(ObjectiveSummary {
|
Ok(ObjectiveSummary {
|
||||||
id: id.to_string(),
|
id: id.to_string(),
|
||||||
title: meta.title,
|
title: meta.title,
|
||||||
state: meta.state,
|
state: meta.state,
|
||||||
updated_at: meta.updated_at,
|
updated_at: meta.updated_at,
|
||||||
|
summary: summarize_body(body),
|
||||||
linked_tickets: meta.linked_tickets,
|
linked_tickets: meta.linked_tickets,
|
||||||
record_source: "local_yoi_objective".to_string(),
|
record_source: "local_yoi_objective".to_string(),
|
||||||
})
|
})
|
||||||
|
|
@ -259,6 +262,20 @@ fn validate_project_id(id: &str) -> Result<()> {
|
||||||
validate_record_id(id).map_err(|_| Error::InvalidRecordId(id.to_string()))
|
validate_record_id(id).map_err(|_| Error::InvalidRecordId(id.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn summarize_body(body: &str) -> String {
|
||||||
|
let summary = body
|
||||||
|
.lines()
|
||||||
|
.map(str::trim)
|
||||||
|
.find(|line| !line.is_empty() && !line.starts_with('#'))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let (summary, truncated) = truncate_body(summary, SUMMARY_BODY_LIMIT);
|
||||||
|
if truncated {
|
||||||
|
format!("{summary}…")
|
||||||
|
} else {
|
||||||
|
summary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn truncate_body(body: &str, limit: usize) -> (String, bool) {
|
fn truncate_body(body: &str, limit: usize) -> (String, bool) {
|
||||||
if body.len() <= limit {
|
if body.len() <= limit {
|
||||||
return (body.to_string(), false);
|
return (body.to_string(), false);
|
||||||
|
|
|
||||||
336
crates/workspace-server/src/repositories.rs
Normal file
336
crates/workspace-server/src/repositories.rs
Normal file
|
|
@ -0,0 +1,336 @@
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::{Command, Output};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::hosts::RuntimeDiagnostic;
|
||||||
|
|
||||||
|
const LOCAL_REPOSITORY_ID: &str = "local";
|
||||||
|
const MAX_COMMAND_OUTPUT: usize = 4096;
|
||||||
|
const DEFAULT_LOG_LIMIT: usize = 10;
|
||||||
|
const MAX_LOG_LIMIT: usize = 50;
|
||||||
|
const MAX_FIELD_LEN: usize = 240;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct LocalRepositoryReader {
|
||||||
|
workspace_root: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LocalRepositoryReader {
|
||||||
|
pub fn new(workspace_root: impl Into<PathBuf>) -> Self {
|
||||||
|
Self {
|
||||||
|
workspace_root: workspace_root.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list(&self, workspace_display_name: &str) -> Vec<RepositorySummary> {
|
||||||
|
vec![self.summary(workspace_display_name)]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn summary(&self, workspace_display_name: &str) -> RepositorySummary {
|
||||||
|
let git = inspect_git(&self.workspace_root);
|
||||||
|
RepositorySummary {
|
||||||
|
id: LOCAL_REPOSITORY_ID.to_string(),
|
||||||
|
display_name: workspace_display_name.to_string(),
|
||||||
|
kind: "local".to_string(),
|
||||||
|
workspace_root: self.workspace_root.clone(),
|
||||||
|
record_authority: "local_workspace_root".to_string(),
|
||||||
|
git,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recent_log(&self, requested_limit: Option<usize>) -> RepositoryLogRead {
|
||||||
|
let limit = requested_limit
|
||||||
|
.unwrap_or(DEFAULT_LOG_LIMIT)
|
||||||
|
.clamp(1, MAX_LOG_LIMIT);
|
||||||
|
git_log(&self.workspace_root, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_local_repository_id(id: &str) -> bool {
|
||||||
|
id == LOCAL_REPOSITORY_ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct RepositorySummary {
|
||||||
|
pub id: String,
|
||||||
|
pub display_name: String,
|
||||||
|
pub kind: String,
|
||||||
|
pub workspace_root: PathBuf,
|
||||||
|
pub record_authority: String,
|
||||||
|
pub git: GitRepositorySummary,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct GitRepositorySummary {
|
||||||
|
pub status: String,
|
||||||
|
pub root: Option<PathBuf>,
|
||||||
|
pub branch: Option<String>,
|
||||||
|
pub head: Option<String>,
|
||||||
|
pub dirty: Option<bool>,
|
||||||
|
pub dirty_scope: String,
|
||||||
|
pub remote: Option<GitRemoteSummary>,
|
||||||
|
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct GitRemoteSummary {
|
||||||
|
pub name: String,
|
||||||
|
pub url: String,
|
||||||
|
pub redacted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct GitCommitSummary {
|
||||||
|
pub hash: String,
|
||||||
|
pub subject: String,
|
||||||
|
pub author_name: String,
|
||||||
|
pub author_email: String,
|
||||||
|
pub timestamp: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct RepositoryLogRead {
|
||||||
|
pub limit: usize,
|
||||||
|
pub items: Vec<GitCommitSummary>,
|
||||||
|
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inspect_git(workspace_root: &Path) -> GitRepositorySummary {
|
||||||
|
let mut diagnostics = Vec::new();
|
||||||
|
let root = match git_stdout(workspace_root, &["rev-parse", "--show-toplevel"]) {
|
||||||
|
Ok(root) => PathBuf::from(root.trim()),
|
||||||
|
Err(message) => {
|
||||||
|
diagnostics.push(diagnostic(
|
||||||
|
"git_unavailable",
|
||||||
|
"info",
|
||||||
|
format!("Workspace root is not available as a Git repository: {message}"),
|
||||||
|
));
|
||||||
|
return GitRepositorySummary {
|
||||||
|
status: "unavailable".to_string(),
|
||||||
|
root: None,
|
||||||
|
branch: None,
|
||||||
|
head: None,
|
||||||
|
dirty: None,
|
||||||
|
dirty_scope: "tracked_changes_only".to_string(),
|
||||||
|
remote: None,
|
||||||
|
diagnostics,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let branch = git_stdout(workspace_root, &["branch", "--show-current"])
|
||||||
|
.ok()
|
||||||
|
.map(|value| truncate_field(value.trim(), MAX_FIELD_LEN))
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.or_else(|| Some("detached".to_string()));
|
||||||
|
let head = match git_stdout(workspace_root, &["rev-parse", "--verify", "HEAD"]) {
|
||||||
|
Ok(value) => Some(truncate_field(value.trim(), 40)),
|
||||||
|
Err(message) => {
|
||||||
|
diagnostics.push(diagnostic(
|
||||||
|
"git_head_unavailable",
|
||||||
|
"warn",
|
||||||
|
format!("Git HEAD summary is unavailable: {message}"),
|
||||||
|
));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let dirty = match git_stdout(
|
||||||
|
workspace_root,
|
||||||
|
&["status", "--porcelain=v1", "--untracked-files=no"],
|
||||||
|
) {
|
||||||
|
Ok(value) => Some(!value.trim().is_empty()),
|
||||||
|
Err(message) => {
|
||||||
|
diagnostics.push(diagnostic(
|
||||||
|
"git_status_unavailable",
|
||||||
|
"warn",
|
||||||
|
format!("Git dirty status is unavailable: {message}"),
|
||||||
|
));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let remote = match git_stdout(workspace_root, &["remote", "get-url", "origin"]) {
|
||||||
|
Ok(value) => {
|
||||||
|
let (url, redacted) = sanitize_remote_url(value.trim());
|
||||||
|
Some(GitRemoteSummary {
|
||||||
|
name: "origin".to_string(),
|
||||||
|
url,
|
||||||
|
redacted,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
diagnostics.push(diagnostic(
|
||||||
|
"git_origin_remote_missing",
|
||||||
|
"info",
|
||||||
|
"No origin remote is configured or visible through the bounded Git summary."
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
GitRepositorySummary {
|
||||||
|
status: "available".to_string(),
|
||||||
|
root: Some(root),
|
||||||
|
branch,
|
||||||
|
head,
|
||||||
|
dirty,
|
||||||
|
dirty_scope: "tracked_changes_only".to_string(),
|
||||||
|
remote,
|
||||||
|
diagnostics,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn git_log(workspace_root: &Path, limit: usize) -> RepositoryLogRead {
|
||||||
|
let mut diagnostics = Vec::new();
|
||||||
|
if let Err(message) = git_stdout(workspace_root, &["rev-parse", "--show-toplevel"]) {
|
||||||
|
diagnostics.push(diagnostic(
|
||||||
|
"git_unavailable",
|
||||||
|
"info",
|
||||||
|
format!("Recent Git log is unavailable for this local repository: {message}"),
|
||||||
|
));
|
||||||
|
return RepositoryLogRead {
|
||||||
|
limit,
|
||||||
|
items: Vec::new(),
|
||||||
|
diagnostics,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
match git_stdout(
|
||||||
|
workspace_root,
|
||||||
|
&[
|
||||||
|
"log",
|
||||||
|
"--no-show-signature",
|
||||||
|
"--date=iso-strict",
|
||||||
|
"--format=%H%x1f%an%x1f%ae%x1f%aI%x1f%s%x1e",
|
||||||
|
"-n",
|
||||||
|
&limit.to_string(),
|
||||||
|
],
|
||||||
|
) {
|
||||||
|
Ok(output) => RepositoryLogRead {
|
||||||
|
limit,
|
||||||
|
items: parse_log(output.as_str()),
|
||||||
|
diagnostics,
|
||||||
|
},
|
||||||
|
Err(message) => {
|
||||||
|
diagnostics.push(diagnostic(
|
||||||
|
"git_log_unavailable",
|
||||||
|
"warn",
|
||||||
|
format!("Recent Git log is unavailable: {message}"),
|
||||||
|
));
|
||||||
|
RepositoryLogRead {
|
||||||
|
limit,
|
||||||
|
items: Vec::new(),
|
||||||
|
diagnostics,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_log(output: &str) -> Vec<GitCommitSummary> {
|
||||||
|
output
|
||||||
|
.split('\u{1e}')
|
||||||
|
.filter_map(|record| {
|
||||||
|
let record = record.trim_matches('\n');
|
||||||
|
if record.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut fields = record.split('\u{1f}');
|
||||||
|
Some(GitCommitSummary {
|
||||||
|
hash: truncate_field(fields.next()?, 40),
|
||||||
|
author_name: truncate_field(fields.next().unwrap_or_default(), MAX_FIELD_LEN),
|
||||||
|
author_email: truncate_field(fields.next().unwrap_or_default(), MAX_FIELD_LEN),
|
||||||
|
timestamp: truncate_field(fields.next().unwrap_or_default(), MAX_FIELD_LEN),
|
||||||
|
subject: truncate_field(fields.next().unwrap_or_default(), MAX_FIELD_LEN),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn git_stdout(workspace_root: &Path, args: &[&str]) -> Result<String, String> {
|
||||||
|
let output = Command::new("git")
|
||||||
|
.arg("-C")
|
||||||
|
.arg(workspace_root)
|
||||||
|
.args(args)
|
||||||
|
.output()
|
||||||
|
.map_err(|error| truncate_field(&error.to_string(), MAX_FIELD_LEN))?;
|
||||||
|
command_stdout(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_stdout(output: Output) -> Result<String, String> {
|
||||||
|
if output.status.success() {
|
||||||
|
return Ok(truncate_output(
|
||||||
|
String::from_utf8_lossy(&output.stdout).as_ref(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let stderr = truncate_output(String::from_utf8_lossy(&output.stderr).as_ref());
|
||||||
|
if stderr.trim().is_empty() {
|
||||||
|
Err(format!("git exited with status {}", output.status))
|
||||||
|
} else {
|
||||||
|
Err(stderr.trim().to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sanitize_remote_url(raw: &str) -> (String, bool) {
|
||||||
|
let bounded = truncate_field(raw, MAX_FIELD_LEN);
|
||||||
|
let Some(separator) = bounded.find("://") else {
|
||||||
|
return (bounded, false);
|
||||||
|
};
|
||||||
|
let scheme_end = separator + 3;
|
||||||
|
let after_scheme = &bounded[scheme_end..];
|
||||||
|
let Some(at_index) = after_scheme.find('@') else {
|
||||||
|
return (bounded, false);
|
||||||
|
};
|
||||||
|
let host_and_path = &after_scheme[(at_index + 1)..];
|
||||||
|
(format!("{}{}", &bounded[..scheme_end], host_and_path), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn truncate_output(value: &str) -> String {
|
||||||
|
truncate_field(value, MAX_COMMAND_OUTPUT)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn truncate_field(value: &str, limit: usize) -> String {
|
||||||
|
if value.len() <= limit {
|
||||||
|
return value.to_string();
|
||||||
|
}
|
||||||
|
let mut end = limit;
|
||||||
|
while !value.is_char_boundary(end) {
|
||||||
|
end -= 1;
|
||||||
|
}
|
||||||
|
value[..end].to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diagnostic(code: &str, severity: &str, message: String) -> RuntimeDiagnostic {
|
||||||
|
RuntimeDiagnostic {
|
||||||
|
code: code.to_string(),
|
||||||
|
severity: severity.to_string(),
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sanitizes_userinfo_from_url_remotes() {
|
||||||
|
assert_eq!(
|
||||||
|
sanitize_remote_url("https://token@example.com/org/repo.git"),
|
||||||
|
("https://example.com/org/repo.git".to_string(), true)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
sanitize_remote_url("git@example.com:org/repo.git"),
|
||||||
|
("git@example.com:org/repo.git".to_string(), false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_bounded_git_log_records() {
|
||||||
|
let parsed = parse_log(
|
||||||
|
"0123456789abcdef\u{1f}Alice\u{1f}a@example.test\u{1f}2026-01-01T00:00:00+00:00\u{1f}Subject\u{1e}\n",
|
||||||
|
);
|
||||||
|
assert_eq!(parsed.len(), 1);
|
||||||
|
assert_eq!(parsed[0].hash, "0123456789abcdef");
|
||||||
|
assert_eq!(parsed[0].subject, "Subject");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::extract::{Path as AxumPath, State};
|
use axum::extract::{Path as AxumPath, Query, State};
|
||||||
use axum::http::header::CONTENT_TYPE;
|
use axum::http::header::CONTENT_TYPE;
|
||||||
use axum::http::{StatusCode, Uri};
|
use axum::http::{StatusCode, Uri};
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
|
|
@ -11,7 +11,10 @@ use serde::{Deserialize, Serialize};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
use crate::hosts::{HostSummary, LocalRuntimeBridge, RuntimeDiagnostic, WorkerSummary};
|
use crate::hosts::{HostSummary, LocalRuntimeBridge, RuntimeDiagnostic, WorkerSummary};
|
||||||
use crate::records::{LocalProjectRecordReader, ObjectiveDetail, ProjectRecordList, TicketDetail};
|
use crate::records::{
|
||||||
|
LocalProjectRecordReader, ObjectiveDetail, ProjectRecordList, TicketDetail, TicketSummary,
|
||||||
|
};
|
||||||
|
use crate::repositories::{LocalRepositoryReader, RepositoryLogRead, RepositorySummary};
|
||||||
use crate::store::{ControlPlaneStore, RunSummary, WorkspaceRecord};
|
use crate::store::{ControlPlaneStore, RunSummary, WorkspaceRecord};
|
||||||
use crate::{Error, Result};
|
use crate::{Error, Result};
|
||||||
|
|
||||||
|
|
@ -95,6 +98,19 @@ impl WorkspaceApi {
|
||||||
self.config.local_runtime_data_dir.clone(),
|
self.config.local_runtime_data_dir.clone(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn local_repository_reader(&self) -> LocalRepositoryReader {
|
||||||
|
LocalRepositoryReader::new(self.config.workspace_root.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn workspace_display_name(&self) -> String {
|
||||||
|
self.config
|
||||||
|
.workspace_root
|
||||||
|
.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.unwrap_or("workspace")
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_router(api: WorkspaceApi) -> Router {
|
pub fn build_router(api: WorkspaceApi) -> Router {
|
||||||
|
|
@ -104,6 +120,13 @@ pub fn build_router(api: WorkspaceApi) -> Router {
|
||||||
.route("/api/tickets/{id}", get(get_ticket))
|
.route("/api/tickets/{id}", get(get_ticket))
|
||||||
.route("/api/objectives", get(list_objectives))
|
.route("/api/objectives", get(list_objectives))
|
||||||
.route("/api/objectives/{id}", get(get_objective))
|
.route("/api/objectives/{id}", get(get_objective))
|
||||||
|
.route("/api/repositories", get(list_repositories))
|
||||||
|
.route("/api/repositories/{repository_id}", get(repository_detail))
|
||||||
|
.route("/api/repositories/{repository_id}/log", get(repository_log))
|
||||||
|
.route(
|
||||||
|
"/api/repositories/{repository_id}/tickets",
|
||||||
|
get(repository_tickets),
|
||||||
|
)
|
||||||
.route("/api/runs", get(list_runs))
|
.route("/api/runs", get(list_runs))
|
||||||
.route("/api/hosts", get(list_hosts))
|
.route("/api/hosts", get(list_hosts))
|
||||||
.route("/api/workers", get(list_workers))
|
.route("/api/workers", get(list_workers))
|
||||||
|
|
@ -164,6 +187,58 @@ pub struct RuntimeListResponse<T> {
|
||||||
pub diagnostics: Vec<RuntimeDiagnostic>,
|
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct RepositoryListResponse {
|
||||||
|
pub workspace_id: String,
|
||||||
|
pub items: Vec<RepositorySummary>,
|
||||||
|
pub source: String,
|
||||||
|
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct RepositoryDetailResponse {
|
||||||
|
pub workspace_id: String,
|
||||||
|
pub item: RepositorySummary,
|
||||||
|
pub source: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct RepositoryLogResponse {
|
||||||
|
pub workspace_id: String,
|
||||||
|
pub repository_id: String,
|
||||||
|
pub limit: usize,
|
||||||
|
pub items: Vec<crate::repositories::GitCommitSummary>,
|
||||||
|
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct RepositoryTicketsResponse {
|
||||||
|
pub workspace_id: String,
|
||||||
|
pub repository_id: String,
|
||||||
|
pub limit: usize,
|
||||||
|
pub columns: Vec<TicketKanbanColumn>,
|
||||||
|
pub invalid_records: Vec<crate::records::InvalidProjectRecord>,
|
||||||
|
pub record_authority: String,
|
||||||
|
pub source: String,
|
||||||
|
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct TicketKanbanColumn {
|
||||||
|
pub state: String,
|
||||||
|
pub items: Vec<TicketSummary>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct LogQuery {
|
||||||
|
limit: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct TicketKanbanQuery {
|
||||||
|
limit: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_workspace(State(api): State<WorkspaceApi>) -> ApiResult<Json<WorkspaceResponse>> {
|
async fn get_workspace(State(api): State<WorkspaceApi>) -> ApiResult<Json<WorkspaceResponse>> {
|
||||||
let schema_version = api.store.schema_version().await?;
|
let schema_version = api.store.schema_version().await?;
|
||||||
let stored = api.store.get_workspace(api.workspace_id()).await?;
|
let stored = api.store.get_workspace(api.workspace_id()).await?;
|
||||||
|
|
@ -249,6 +324,80 @@ async fn get_objective(
|
||||||
Ok(Json(api.records.objective(&id)?))
|
Ok(Json(api.records.objective(&id)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn list_repositories(
|
||||||
|
State(api): State<WorkspaceApi>,
|
||||||
|
) -> ApiResult<Json<RepositoryListResponse>> {
|
||||||
|
let reader = api.local_repository_reader();
|
||||||
|
let items = reader.list(&api.workspace_display_name());
|
||||||
|
Ok(Json(RepositoryListResponse {
|
||||||
|
workspace_id: api.config.workspace_id,
|
||||||
|
items,
|
||||||
|
source: "local_workspace_root".to_string(),
|
||||||
|
diagnostics: Vec::new(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn repository_detail(
|
||||||
|
State(api): State<WorkspaceApi>,
|
||||||
|
AxumPath(repository_id): AxumPath<String>,
|
||||||
|
) -> ApiResult<Json<RepositoryDetailResponse>> {
|
||||||
|
ensure_local_repository(&repository_id)?;
|
||||||
|
let reader = api.local_repository_reader();
|
||||||
|
Ok(Json(RepositoryDetailResponse {
|
||||||
|
workspace_id: api.config.workspace_id.clone(),
|
||||||
|
item: reader.summary(&api.workspace_display_name()),
|
||||||
|
source: "local_workspace_root".to_string(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn repository_log(
|
||||||
|
State(api): State<WorkspaceApi>,
|
||||||
|
AxumPath(repository_id): AxumPath<String>,
|
||||||
|
Query(query): Query<LogQuery>,
|
||||||
|
) -> ApiResult<Json<RepositoryLogResponse>> {
|
||||||
|
ensure_local_repository(&repository_id)?;
|
||||||
|
let RepositoryLogRead {
|
||||||
|
limit,
|
||||||
|
items,
|
||||||
|
diagnostics,
|
||||||
|
} = api.local_repository_reader().recent_log(query.limit);
|
||||||
|
Ok(Json(RepositoryLogResponse {
|
||||||
|
workspace_id: api.config.workspace_id,
|
||||||
|
repository_id,
|
||||||
|
limit,
|
||||||
|
items,
|
||||||
|
diagnostics,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn repository_tickets(
|
||||||
|
State(api): State<WorkspaceApi>,
|
||||||
|
AxumPath(repository_id): AxumPath<String>,
|
||||||
|
Query(query): Query<TicketKanbanQuery>,
|
||||||
|
) -> ApiResult<Json<RepositoryTicketsResponse>> {
|
||||||
|
ensure_local_repository(&repository_id)?;
|
||||||
|
let limit = query.limit.unwrap_or(api.config.max_records).min(200);
|
||||||
|
let ProjectRecordList {
|
||||||
|
items,
|
||||||
|
invalid_records,
|
||||||
|
record_authority,
|
||||||
|
} = api.records.list_tickets(limit)?;
|
||||||
|
Ok(Json(RepositoryTicketsResponse {
|
||||||
|
workspace_id: api.config.workspace_id,
|
||||||
|
repository_id,
|
||||||
|
limit,
|
||||||
|
columns: ticket_kanban_columns(items),
|
||||||
|
invalid_records,
|
||||||
|
record_authority,
|
||||||
|
source: "workspace_local_ticket_fallback".to_string(),
|
||||||
|
diagnostics: vec![RuntimeDiagnostic {
|
||||||
|
code: "repository_ticket_target_metadata_absent".to_string(),
|
||||||
|
severity: "info".to_string(),
|
||||||
|
message: "Ticket target Repository metadata is not available yet; Kanban groups all workspace-local Tickets by state as a read-only fallback.".to_string(),
|
||||||
|
}],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
async fn list_runs(
|
async fn list_runs(
|
||||||
State(api): State<WorkspaceApi>,
|
State(api): State<WorkspaceApi>,
|
||||||
) -> ApiResult<Json<RuntimeListResponse<RunSummary>>> {
|
) -> ApiResult<Json<RuntimeListResponse<RunSummary>>> {
|
||||||
|
|
@ -308,6 +457,60 @@ fn workers_response(api: WorkspaceApi) -> ApiResult<RuntimeListResponse<WorkerSu
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ensure_local_repository(repository_id: &str) -> Result<()> {
|
||||||
|
if LocalRepositoryReader::is_local_repository_id(repository_id) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::UnknownRepository(repository_id.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ticket_kanban_columns(items: Vec<TicketSummary>) -> Vec<TicketKanbanColumn> {
|
||||||
|
let mut columns = vec![
|
||||||
|
TicketKanbanColumn {
|
||||||
|
state: "planning".to_string(),
|
||||||
|
items: Vec::new(),
|
||||||
|
},
|
||||||
|
TicketKanbanColumn {
|
||||||
|
state: "ready".to_string(),
|
||||||
|
items: Vec::new(),
|
||||||
|
},
|
||||||
|
TicketKanbanColumn {
|
||||||
|
state: "queued".to_string(),
|
||||||
|
items: Vec::new(),
|
||||||
|
},
|
||||||
|
TicketKanbanColumn {
|
||||||
|
state: "inprogress".to_string(),
|
||||||
|
items: Vec::new(),
|
||||||
|
},
|
||||||
|
TicketKanbanColumn {
|
||||||
|
state: "done".to_string(),
|
||||||
|
items: Vec::new(),
|
||||||
|
},
|
||||||
|
TicketKanbanColumn {
|
||||||
|
state: "closed".to_string(),
|
||||||
|
items: Vec::new(),
|
||||||
|
},
|
||||||
|
TicketKanbanColumn {
|
||||||
|
state: "other".to_string(),
|
||||||
|
items: Vec::new(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for item in items {
|
||||||
|
let index = match item.state.as_str() {
|
||||||
|
"planning" => 0,
|
||||||
|
"ready" => 1,
|
||||||
|
"queued" => 2,
|
||||||
|
"inprogress" => 3,
|
||||||
|
"done" => 4,
|
||||||
|
"closed" => 5,
|
||||||
|
_ => 6,
|
||||||
|
};
|
||||||
|
columns[index].items.push(item);
|
||||||
|
}
|
||||||
|
columns
|
||||||
|
}
|
||||||
|
|
||||||
async fn static_or_spa_fallback(State(api): State<WorkspaceApi>, uri: Uri) -> Response {
|
async fn static_or_spa_fallback(State(api): State<WorkspaceApi>, uri: Uri) -> Response {
|
||||||
if uri.path().starts_with("/api/") || uri.path() == "/api" {
|
if uri.path().starts_with("/api/") || uri.path() == "/api" {
|
||||||
return (
|
return (
|
||||||
|
|
@ -407,9 +610,10 @@ impl From<Error> for ApiError {
|
||||||
impl IntoResponse for ApiError {
|
impl IntoResponse for ApiError {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
let status = match &self.0 {
|
let status = match &self.0 {
|
||||||
Error::InvalidRecordId(_) | Error::MissingFrontmatter(_) | Error::UnknownHost(_) => {
|
Error::InvalidRecordId(_)
|
||||||
StatusCode::NOT_FOUND
|
| Error::MissingFrontmatter(_)
|
||||||
}
|
| Error::UnknownHost(_)
|
||||||
|
| Error::UnknownRepository(_) => StatusCode::NOT_FOUND,
|
||||||
Error::Ticket(_) => StatusCode::NOT_FOUND,
|
Error::Ticket(_) => StatusCode::NOT_FOUND,
|
||||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
};
|
};
|
||||||
|
|
@ -468,6 +672,44 @@ mod tests {
|
||||||
|
|
||||||
let objectives = get_json(app.clone(), "/api/objectives").await;
|
let objectives = get_json(app.clone(), "/api/objectives").await;
|
||||||
assert_eq!(objectives["items"][0]["id"], "00000000001J3");
|
assert_eq!(objectives["items"][0]["id"], "00000000001J3");
|
||||||
|
assert_eq!(objectives["items"][0]["summary"], "Objective body.");
|
||||||
|
|
||||||
|
let repositories = get_json(app.clone(), "/api/repositories").await;
|
||||||
|
assert_eq!(repositories["items"][0]["id"], "local");
|
||||||
|
assert_eq!(repositories["items"][0]["kind"], "local");
|
||||||
|
|
||||||
|
let repository_detail = get_json(app.clone(), "/api/repositories/local").await;
|
||||||
|
assert_eq!(repository_detail["item"]["id"], "local");
|
||||||
|
|
||||||
|
let repository_log = get_json(app.clone(), "/api/repositories/local/log?limit=3").await;
|
||||||
|
assert_eq!(repository_log["repository_id"], "local");
|
||||||
|
assert_eq!(repository_log["limit"], 3);
|
||||||
|
|
||||||
|
let repository_tickets = get_json(app.clone(), "/api/repositories/local/tickets").await;
|
||||||
|
assert_eq!(repository_tickets["repository_id"], "local");
|
||||||
|
let ready_column = repository_tickets["columns"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.find(|column| column["state"] == "ready")
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(ready_column["items"][0]["id"], "00000000001J2");
|
||||||
|
assert_eq!(
|
||||||
|
repository_tickets["diagnostics"][0]["code"],
|
||||||
|
"repository_ticket_target_metadata_absent"
|
||||||
|
);
|
||||||
|
|
||||||
|
let unknown_repository_response = app
|
||||||
|
.clone()
|
||||||
|
.oneshot(
|
||||||
|
Request::builder()
|
||||||
|
.uri("/api/repositories/nope")
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(unknown_repository_response.status(), StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
let hosts = get_json(app.clone(), "/api/hosts").await;
|
let hosts = get_json(app.clone(), "/api/hosts").await;
|
||||||
assert_eq!(hosts["items"][0]["host_id"], "local-local-test");
|
assert_eq!(hosts["items"][0]["host_id"], "local-local-test");
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { ObjectiveListResponse, ObjectiveSummary } from './types';
|
||||||
|
|
||||||
|
const MAX_VISIBLE_OBJECTIVES = 6;
|
||||||
|
|
||||||
|
let loading = $state(true);
|
||||||
|
let error = $state<string | null>(null);
|
||||||
|
let objectives = $state<ObjectiveSummary[]>([]);
|
||||||
|
let invalidRecordCount = $state(0);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
void loadObjectives(controller.signal);
|
||||||
|
return () => controller.abort();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadObjectives(signal: AbortSignal) {
|
||||||
|
loading = true;
|
||||||
|
error = null;
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/objectives', { signal });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`objectives request failed (${response.status})`);
|
||||||
|
}
|
||||||
|
const payload = (await response.json()) as ObjectiveListResponse;
|
||||||
|
objectives = Array.isArray(payload.items) ? payload.items.slice(0, MAX_VISIBLE_OBJECTIVES) : [];
|
||||||
|
invalidRecordCount = Array.isArray(payload.invalid_records) ? payload.invalid_records.length : 0;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof DOMException && err.name === 'AbortError') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
error = err instanceof Error ? err.message : 'objectives request failed';
|
||||||
|
objectives = [];
|
||||||
|
invalidRecordCount = 0;
|
||||||
|
} finally {
|
||||||
|
if (!signal.aborted) {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="nav-section" aria-labelledby="objectives-heading">
|
||||||
|
<div class="section-heading-row">
|
||||||
|
<h2 id="objectives-heading">objectives</h2>
|
||||||
|
{#if !loading && !error}
|
||||||
|
<span class="section-count">{objectives.length}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<p class="section-state">Loading objectives…</p>
|
||||||
|
{:else if error}
|
||||||
|
<p class="section-state error">{error}</p>
|
||||||
|
{:else if objectives.length === 0}
|
||||||
|
<p class="section-state">No objectives found.</p>
|
||||||
|
{:else}
|
||||||
|
<ul class="nav-list" aria-label="Objectives">
|
||||||
|
<li>
|
||||||
|
<a class="nav-item active" href="#/objectives">
|
||||||
|
<span class="item-title">All objectives</span>
|
||||||
|
<span class="item-meta">read-only list</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{#each objectives as objective (objective.id)}
|
||||||
|
<li>
|
||||||
|
<a class="nav-item" href={`#/objectives/${objective.id}`}>
|
||||||
|
<span class="item-title">{objective.title}</span>
|
||||||
|
<span class="item-meta">{objective.state}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if invalidRecordCount > 0}
|
||||||
|
<p class="section-note">{invalidRecordCount} invalid objective record(s) hidden.</p>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.nav-section {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-count {
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(15, 23, 42, 0.64);
|
||||||
|
padding: 10px 12px;
|
||||||
|
min-width: 0;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-title,
|
||||||
|
.item-meta {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-title {
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-meta,
|
||||||
|
.section-state,
|
||||||
|
.section-note {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-state,
|
||||||
|
.section-note {
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-state {
|
||||||
|
border: 1px dashed rgba(148, 163, 184, 0.2);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-state.error {
|
||||||
|
border-color: rgba(248, 113, 113, 0.36);
|
||||||
|
color: #fecaca;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { WorkspaceResponse } from './types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
workspace: WorkspaceResponse | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { workspace }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="nav-section" aria-labelledby="repositories-heading">
|
||||||
|
<div class="section-heading-row">
|
||||||
|
<h2 id="repositories-heading">repositories</h2>
|
||||||
|
<span class="section-count">1</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="nav-list" aria-label="Repositories">
|
||||||
|
<li>
|
||||||
|
<a class="nav-item active" href="#/repositories/local">
|
||||||
|
<span class="item-title">{workspace?.display_name ?? 'local workspace'}</span>
|
||||||
|
<span class="item-meta">local repository · read-only</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="section-note">
|
||||||
|
Repository authority remains the current workspace root and canonical project records.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.nav-section {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-count {
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(15, 23, 42, 0.64);
|
||||||
|
padding: 10px 12px;
|
||||||
|
min-width: 0;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
border-color: rgba(56, 189, 248, 0.36);
|
||||||
|
background: rgba(14, 165, 233, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-title,
|
||||||
|
.item-meta {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-title {
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-meta,
|
||||||
|
.section-note {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-note {
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
157
web/workspace/src/lib/workspace-sidebar/WorkersNavSection.svelte
Normal file
157
web/workspace/src/lib/workspace-sidebar/WorkersNavSection.svelte
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { ListResponse, Worker } from './types';
|
||||||
|
|
||||||
|
const MAX_VISIBLE_WORKERS = 6;
|
||||||
|
|
||||||
|
let loading = $state(true);
|
||||||
|
let error = $state<string | null>(null);
|
||||||
|
let workers = $state<Worker[]>([]);
|
||||||
|
let placeholder = $state<string | null>(null);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
void loadWorkers(controller.signal);
|
||||||
|
return () => controller.abort();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadWorkers(signal: AbortSignal) {
|
||||||
|
loading = true;
|
||||||
|
error = null;
|
||||||
|
placeholder = null;
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/workers', { signal });
|
||||||
|
if (response.status === 404) {
|
||||||
|
workers = [];
|
||||||
|
placeholder = 'Worker API is not integrated in this build yet.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`workers request failed (${response.status})`);
|
||||||
|
}
|
||||||
|
const payload = (await response.json()) as ListResponse<Worker>;
|
||||||
|
workers = Array.isArray(payload.items) ? payload.items.slice(0, MAX_VISIBLE_WORKERS) : [];
|
||||||
|
if (workers.length === 0) {
|
||||||
|
placeholder = 'No workers reported by the current API.';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof DOMException && err.name === 'AbortError') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
error = err instanceof Error ? err.message : 'workers request failed';
|
||||||
|
workers = [];
|
||||||
|
} finally {
|
||||||
|
if (!signal.aborted) {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="nav-section" aria-labelledby="workers-heading">
|
||||||
|
<div class="section-heading-row">
|
||||||
|
<h2 id="workers-heading">workers</h2>
|
||||||
|
{#if !loading && !error && workers.length > 0}
|
||||||
|
<span class="section-count">{workers.length}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<p class="section-state">Checking workers…</p>
|
||||||
|
{:else if error}
|
||||||
|
<p class="section-state error">{error}</p>
|
||||||
|
{:else if workers.length === 0}
|
||||||
|
<p class="section-state">{placeholder ?? 'Workers will appear here when an API is connected.'}</p>
|
||||||
|
{:else}
|
||||||
|
<ul class="nav-list" aria-label="Workers">
|
||||||
|
{#each workers as worker (worker.worker_id)}
|
||||||
|
<li class="nav-item">
|
||||||
|
<span class="item-title">{worker.label}</span>
|
||||||
|
<span class="item-meta">
|
||||||
|
{worker.state} · {worker.status}{worker.role ? ` · ${worker.role}` : ''}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.nav-section {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-count {
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(15, 23, 42, 0.64);
|
||||||
|
padding: 10px 12px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-title,
|
||||||
|
.item-meta {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-title {
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-meta,
|
||||||
|
.section-state {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-state {
|
||||||
|
margin: 0;
|
||||||
|
border: 1px dashed rgba(148, 163, 184, 0.2);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-state.error {
|
||||||
|
border-color: rgba(248, 113, 113, 0.36);
|
||||||
|
color: #fecaca;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
124
web/workspace/src/lib/workspace-sidebar/WorkspaceSidebar.svelte
Normal file
124
web/workspace/src/lib/workspace-sidebar/WorkspaceSidebar.svelte
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import ObjectivesNavSection from './ObjectivesNavSection.svelte';
|
||||||
|
import RepositoriesNavSection from './RepositoriesNavSection.svelte';
|
||||||
|
import WorkersNavSection from './WorkersNavSection.svelte';
|
||||||
|
import type { WorkspaceResponse } from './types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
workspace: WorkspaceResponse | null;
|
||||||
|
workspaceError?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { workspace, workspaceError = null }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<aside class="workspace-sidebar" aria-label="Workspace navigation">
|
||||||
|
<header class="sidebar-header">
|
||||||
|
<div class="workspace-label">
|
||||||
|
<span class="eyebrow">workspace</span>
|
||||||
|
<h1>{workspace?.display_name ?? 'Yoi workspace'}</h1>
|
||||||
|
{#if workspaceError}
|
||||||
|
<p class="workspace-status error">Workspace summary unavailable.</p>
|
||||||
|
{:else if workspace}
|
||||||
|
<p class="workspace-status">{workspace.workspace_id}</p>
|
||||||
|
{:else}
|
||||||
|
<p class="workspace-status">Loading workspace…</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="settings-button"
|
||||||
|
type="button"
|
||||||
|
aria-label="Workspace settings"
|
||||||
|
title="Workspace settings placeholder"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
⚙
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav class="sidebar-sections" aria-label="Workspace sections">
|
||||||
|
<RepositoriesNavSection {workspace} />
|
||||||
|
<ObjectivesNavSection />
|
||||||
|
<WorkersNavSection />
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.workspace-sidebar {
|
||||||
|
align-self: stretch;
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
|
border-radius: 26px;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(15, 23, 42, 0.96), rgba(15, 23, 42, 0.82)),
|
||||||
|
rgba(15, 23, 42, 0.88);
|
||||||
|
box-shadow: 0 24px 80px rgba(2, 6, 23, 0.28);
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 14px;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-label {
|
||||||
|
display: grid;
|
||||||
|
gap: 5px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
color: #38bdf8;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
.workspace-status {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
color: #f8fafc;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-status {
|
||||||
|
margin: 0;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-status.error {
|
||||||
|
color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.24);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(15, 23, 42, 0.7);
|
||||||
|
color: #cbd5e1;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-sections {
|
||||||
|
display: grid;
|
||||||
|
gap: 24px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
158
web/workspace/src/lib/workspace-sidebar/types.ts
Normal file
158
web/workspace/src/lib/workspace-sidebar/types.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
export type ExtensionPoint = {
|
||||||
|
status: string;
|
||||||
|
note: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkspaceResponse = {
|
||||||
|
workspace_id: string;
|
||||||
|
display_name: string;
|
||||||
|
record_authority: string;
|
||||||
|
extension_points: {
|
||||||
|
event_stream: ExtensionPoint;
|
||||||
|
host_worker_bridge: ExtensionPoint;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Diagnostic = {
|
||||||
|
code: string;
|
||||||
|
severity: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Host = {
|
||||||
|
host_id: string;
|
||||||
|
label: string;
|
||||||
|
kind: string;
|
||||||
|
status: string;
|
||||||
|
observed_at: string;
|
||||||
|
last_seen_at: string;
|
||||||
|
capabilities: {
|
||||||
|
local_pod_inspection: string;
|
||||||
|
workspace_root: string;
|
||||||
|
os: string;
|
||||||
|
arch: string;
|
||||||
|
max_workers: number;
|
||||||
|
};
|
||||||
|
diagnostics: Diagnostic[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Worker = {
|
||||||
|
worker_id: string;
|
||||||
|
host_id: string;
|
||||||
|
label: string;
|
||||||
|
pod_name: string;
|
||||||
|
role?: string;
|
||||||
|
profile?: string;
|
||||||
|
workspace_root?: string;
|
||||||
|
state: string;
|
||||||
|
status: string;
|
||||||
|
last_seen_at?: string;
|
||||||
|
implementation: { kind: string; pod_name: string };
|
||||||
|
diagnostics: Diagnostic[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ListResponse<T> = {
|
||||||
|
workspace_id: string;
|
||||||
|
limit: number;
|
||||||
|
items: T[];
|
||||||
|
source: string;
|
||||||
|
diagnostics: Diagnostic[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RepositorySummary = {
|
||||||
|
id: string;
|
||||||
|
display_name: string;
|
||||||
|
kind: string;
|
||||||
|
workspace_root: string;
|
||||||
|
record_authority: string;
|
||||||
|
git: GitRepositorySummary;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitRepositorySummary = {
|
||||||
|
status: string;
|
||||||
|
root?: string | null;
|
||||||
|
branch?: string | null;
|
||||||
|
head?: string | null;
|
||||||
|
dirty?: boolean | null;
|
||||||
|
dirty_scope: string;
|
||||||
|
remote?: GitRemoteSummary | null;
|
||||||
|
diagnostics: Diagnostic[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitRemoteSummary = {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
redacted: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GitCommitSummary = {
|
||||||
|
hash: string;
|
||||||
|
subject: string;
|
||||||
|
author_name: string;
|
||||||
|
author_email: string;
|
||||||
|
timestamp: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RepositoryDetailResponse = {
|
||||||
|
workspace_id: string;
|
||||||
|
item: RepositorySummary;
|
||||||
|
source: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RepositoryLogResponse = {
|
||||||
|
workspace_id: string;
|
||||||
|
repository_id: string;
|
||||||
|
limit: number;
|
||||||
|
items: GitCommitSummary[];
|
||||||
|
diagnostics: Diagnostic[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TicketSummary = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
state: string;
|
||||||
|
priority?: string | null;
|
||||||
|
updated_at?: string | null;
|
||||||
|
queued_by?: string | null;
|
||||||
|
queued_at?: string | null;
|
||||||
|
record_source?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TicketKanbanColumn = {
|
||||||
|
state: string;
|
||||||
|
items: TicketSummary[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RepositoryTicketsResponse = {
|
||||||
|
workspace_id: string;
|
||||||
|
repository_id: string;
|
||||||
|
limit: number;
|
||||||
|
columns: TicketKanbanColumn[];
|
||||||
|
invalid_records: InvalidProjectRecord[];
|
||||||
|
record_authority: string;
|
||||||
|
source: string;
|
||||||
|
diagnostics: Diagnostic[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ObjectiveSummary = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
state: string;
|
||||||
|
updated_at?: string | null;
|
||||||
|
summary: string;
|
||||||
|
linked_tickets?: string[];
|
||||||
|
record_source?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InvalidProjectRecord = {
|
||||||
|
label: string;
|
||||||
|
reason: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ObjectiveListResponse = {
|
||||||
|
workspace_id: string;
|
||||||
|
limit: number;
|
||||||
|
items: ObjectiveSummary[];
|
||||||
|
invalid_records: InvalidProjectRecord[];
|
||||||
|
record_authority: string;
|
||||||
|
};
|
||||||
|
|
@ -1,64 +1,32 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
type WorkspaceResponse = {
|
import { onMount } from 'svelte';
|
||||||
workspace_id: string;
|
import WorkspaceSidebar from '$lib/workspace-sidebar/WorkspaceSidebar.svelte';
|
||||||
display_name: string;
|
import type {
|
||||||
record_authority: string;
|
Diagnostic,
|
||||||
extension_points: {
|
Host,
|
||||||
event_stream: { status: string; note: string };
|
ListResponse,
|
||||||
host_worker_bridge: { status: string; note: string };
|
ObjectiveListResponse,
|
||||||
};
|
ObjectiveSummary,
|
||||||
};
|
RepositoryDetailResponse,
|
||||||
|
RepositoryLogResponse,
|
||||||
|
RepositorySummary,
|
||||||
|
RepositoryTicketsResponse,
|
||||||
|
Worker,
|
||||||
|
WorkspaceResponse
|
||||||
|
} from '$lib/workspace-sidebar/types';
|
||||||
|
|
||||||
type Diagnostic = {
|
type RouteState =
|
||||||
code: string;
|
| { page: 'overview'; objectiveId?: undefined }
|
||||||
severity: string;
|
| { page: 'repository'; objectiveId?: undefined }
|
||||||
message: string;
|
| { page: 'objectives'; objectiveId?: string };
|
||||||
};
|
|
||||||
|
|
||||||
type Host = {
|
|
||||||
host_id: string;
|
|
||||||
label: string;
|
|
||||||
kind: string;
|
|
||||||
status: string;
|
|
||||||
observed_at: string;
|
|
||||||
last_seen_at: string;
|
|
||||||
capabilities: {
|
|
||||||
local_pod_inspection: string;
|
|
||||||
workspace_root: string;
|
|
||||||
os: string;
|
|
||||||
arch: string;
|
|
||||||
max_workers: number;
|
|
||||||
};
|
|
||||||
diagnostics: Diagnostic[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type Worker = {
|
|
||||||
worker_id: string;
|
|
||||||
host_id: string;
|
|
||||||
label: string;
|
|
||||||
pod_name: string;
|
|
||||||
role?: string;
|
|
||||||
profile?: string;
|
|
||||||
workspace_root?: string;
|
|
||||||
state: string;
|
|
||||||
status: string;
|
|
||||||
last_seen_at?: string;
|
|
||||||
implementation: { kind: string; pod_name: string };
|
|
||||||
diagnostics: Diagnostic[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type ListResponse<T> = {
|
|
||||||
workspace_id: string;
|
|
||||||
limit: number;
|
|
||||||
items: T[];
|
|
||||||
source: string;
|
|
||||||
diagnostics: Diagnostic[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const endpoints = [
|
const endpoints = [
|
||||||
{ label: 'Workspace', path: '/api/workspace' },
|
{ label: 'Workspace', path: '/api/workspace' },
|
||||||
{ label: 'Tickets', path: '/api/tickets' },
|
{ label: 'Tickets', path: '/api/tickets' },
|
||||||
{ label: 'Objectives', path: '/api/objectives' },
|
{ label: 'Objectives', path: '/api/objectives' },
|
||||||
|
{ label: 'Repositories', path: '/api/repositories' },
|
||||||
|
{ label: 'Repository log', path: '/api/repositories/local/log' },
|
||||||
|
{ label: 'Repository tickets', path: '/api/repositories/local/tickets' },
|
||||||
{ label: 'Runs', path: '/api/runs' },
|
{ label: 'Runs', path: '/api/runs' },
|
||||||
{ label: 'Hosts', path: '/api/hosts' },
|
{ label: 'Hosts', path: '/api/hosts' },
|
||||||
{ label: 'Workers', path: '/api/workers' }
|
{ label: 'Workers', path: '/api/workers' }
|
||||||
|
|
@ -67,7 +35,20 @@
|
||||||
let workspace = $state<WorkspaceResponse | null>(null);
|
let workspace = $state<WorkspaceResponse | null>(null);
|
||||||
let hosts = $state<ListResponse<Host> | null>(null);
|
let hosts = $state<ListResponse<Host> | null>(null);
|
||||||
let workers = $state<ListResponse<Worker> | null>(null);
|
let workers = $state<ListResponse<Worker> | null>(null);
|
||||||
let loadError = $state<string | null>(null);
|
let repository = $state<RepositorySummary | null>(null);
|
||||||
|
let repositoryLog = $state<RepositoryLogResponse | null>(null);
|
||||||
|
let repositoryTickets = $state<RepositoryTicketsResponse | null>(null);
|
||||||
|
let objectives = $state<ObjectiveListResponse | null>(null);
|
||||||
|
|
||||||
|
let workspaceError = $state<string | null>(null);
|
||||||
|
let hostsError = $state<string | null>(null);
|
||||||
|
let workersError = $state<string | null>(null);
|
||||||
|
let repositoryError = $state<string | null>(null);
|
||||||
|
let repositoryLogError = $state<string | null>(null);
|
||||||
|
let repositoryTicketsError = $state<string | null>(null);
|
||||||
|
let objectivesError = $state<string | null>(null);
|
||||||
|
let currentPath = $state('/');
|
||||||
|
let route = $derived(routeFromPath(currentPath));
|
||||||
|
|
||||||
async function getJson<T>(path: string): Promise<T> {
|
async function getJson<T>(path: string): Promise<T> {
|
||||||
const response = await fetch(path);
|
const response = await fetch(path);
|
||||||
|
|
@ -78,18 +59,73 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadWorkspace() {
|
async function loadWorkspace() {
|
||||||
|
workspaceError = null;
|
||||||
try {
|
try {
|
||||||
const [workspaceResponse, hostResponse, workerResponse] = await Promise.all([
|
workspace = await getJson<WorkspaceResponse>('/api/workspace');
|
||||||
getJson<WorkspaceResponse>('/api/workspace'),
|
|
||||||
getJson<ListResponse<Host>>('/api/hosts'),
|
|
||||||
getJson<ListResponse<Worker>>('/api/workers')
|
|
||||||
]);
|
|
||||||
workspace = workspaceResponse;
|
|
||||||
hosts = hostResponse;
|
|
||||||
workers = workerResponse;
|
|
||||||
loadError = null;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
loadError = error instanceof Error ? error.message : String(error);
|
workspaceError = error instanceof Error ? error.message : String(error);
|
||||||
|
workspace = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadHosts() {
|
||||||
|
hostsError = null;
|
||||||
|
try {
|
||||||
|
hosts = await getJson<ListResponse<Host>>('/api/hosts');
|
||||||
|
} catch (error) {
|
||||||
|
hostsError = error instanceof Error ? error.message : String(error);
|
||||||
|
hosts = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadWorkers() {
|
||||||
|
workersError = null;
|
||||||
|
try {
|
||||||
|
workers = await getJson<ListResponse<Worker>>('/api/workers');
|
||||||
|
} catch (error) {
|
||||||
|
workersError = error instanceof Error ? error.message : String(error);
|
||||||
|
workers = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRepository() {
|
||||||
|
repositoryError = null;
|
||||||
|
try {
|
||||||
|
const detail = await getJson<RepositoryDetailResponse>('/api/repositories/local');
|
||||||
|
repository = detail.item;
|
||||||
|
} catch (error) {
|
||||||
|
repositoryError = error instanceof Error ? error.message : String(error);
|
||||||
|
repository = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRepositoryLog() {
|
||||||
|
repositoryLogError = null;
|
||||||
|
try {
|
||||||
|
repositoryLog = await getJson<RepositoryLogResponse>('/api/repositories/local/log?limit=10');
|
||||||
|
} catch (error) {
|
||||||
|
repositoryLogError = error instanceof Error ? error.message : String(error);
|
||||||
|
repositoryLog = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRepositoryTickets() {
|
||||||
|
repositoryTicketsError = null;
|
||||||
|
try {
|
||||||
|
repositoryTickets = await getJson<RepositoryTicketsResponse>('/api/repositories/local/tickets');
|
||||||
|
} catch (error) {
|
||||||
|
repositoryTicketsError = error instanceof Error ? error.message : String(error);
|
||||||
|
repositoryTickets = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadObjectives() {
|
||||||
|
objectivesError = null;
|
||||||
|
try {
|
||||||
|
objectives = await getJson<ObjectiveListResponse>('/api/objectives');
|
||||||
|
} catch (error) {
|
||||||
|
objectivesError = error instanceof Error ? error.message : String(error);
|
||||||
|
objectives = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,8 +133,44 @@
|
||||||
return groups.flatMap((group) => group ?? []);
|
return groups.flatMap((group) => group ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function routeFromPath(path: string): RouteState {
|
||||||
|
if (path.startsWith('/repositories')) {
|
||||||
|
return { page: 'repository' };
|
||||||
|
}
|
||||||
|
if (path.startsWith('/objectives')) {
|
||||||
|
const [, , objectiveId] = path.split('/');
|
||||||
|
return { page: 'objectives', objectiveId: objectiveId || undefined };
|
||||||
|
}
|
||||||
|
return { page: 'overview' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRouteFromHash() {
|
||||||
|
const hashPath = window.location.hash.replace(/^#/, '') || '/';
|
||||||
|
currentPath = hashPath.startsWith('/') ? hashPath : `/${hashPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(value: string | null | undefined): string {
|
||||||
|
return value ?? 'not recorded';
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortHash(hash: string | null | undefined): string {
|
||||||
|
return hash ? hash.slice(0, 12) : 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
void loadWorkspace();
|
void loadWorkspace();
|
||||||
|
void loadHosts();
|
||||||
|
void loadWorkers();
|
||||||
|
void loadRepository();
|
||||||
|
void loadRepositoryLog();
|
||||||
|
void loadRepositoryTickets();
|
||||||
|
void loadObjectives();
|
||||||
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
updateRouteFromHash();
|
||||||
|
window.addEventListener('hashchange', updateRouteFromHash);
|
||||||
|
return () => window.removeEventListener('hashchange', updateRouteFromHash);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -110,18 +182,244 @@
|
||||||
/>
|
/>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main class="shell">
|
<div class="workspace-layout">
|
||||||
|
<WorkspaceSidebar {workspace} {workspaceError} />
|
||||||
|
|
||||||
|
<main class="shell">
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
<p class="eyebrow">Local / single-workspace bootstrap</p>
|
<p class="eyebrow">Local / single-workspace bootstrap</p>
|
||||||
<h1>Yoi Workspace Control Plane</h1>
|
<h1>Yoi Workspace Control Plane</h1>
|
||||||
<p>
|
<p>
|
||||||
Static SPA shell for reading canonical <code>.yoi</code> project records
|
Static SPA shell for reading canonical <code>.yoi</code> project records,
|
||||||
and the local Host / Worker execution view through bounded backend APIs.
|
bounded local Repository summaries, and the local Host / Worker execution
|
||||||
Ticket and Objective lifecycle authority stays in the existing local record
|
view. Ticket and Objective lifecycle authority stays in the existing local
|
||||||
workflow.
|
record workflow.
|
||||||
|
</p>
|
||||||
|
<p class="page-links" aria-label="Workspace page links">
|
||||||
|
<a href="#/">Overview</a>
|
||||||
|
<a href="#/repositories/local">Repository</a>
|
||||||
|
<a href="#/objectives">Objectives</a>
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{#if route.page === 'repository'}
|
||||||
|
<section class="grid runtime">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Repository summary</h2>
|
||||||
|
{#if repository}
|
||||||
|
<dl>
|
||||||
|
<div>
|
||||||
|
<dt>ID</dt>
|
||||||
|
<dd><code>{repository.id}</code></dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Kind</dt>
|
||||||
|
<dd>{repository.kind}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Workspace root</dt>
|
||||||
|
<dd><code>{repository.workspace_root}</code></dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Record authority</dt>
|
||||||
|
<dd>{repository.record_authority}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Git</dt>
|
||||||
|
<dd>{repository.git.status}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
{:else if repositoryError}
|
||||||
|
<p class="error">{repositoryError}</p>
|
||||||
|
{:else}
|
||||||
|
<p>Waiting for <code>/api/repositories/local</code>…</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Git summary</h2>
|
||||||
|
{#if repository}
|
||||||
|
{#if repository.git.status === 'available'}
|
||||||
|
<dl>
|
||||||
|
<div>
|
||||||
|
<dt>Root</dt>
|
||||||
|
<dd><code>{repository.git.root ?? 'unknown'}</code></dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Branch</dt>
|
||||||
|
<dd>{repository.git.branch ?? 'unknown'}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>HEAD</dt>
|
||||||
|
<dd><code>{shortHash(repository.git.head)}</code></dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Dirty</dt>
|
||||||
|
<dd>{repository.git.dirty === null || repository.git.dirty === undefined ? 'unknown' : repository.git.dirty ? 'yes' : 'no'} <small>{repository.git.dirty_scope}</small></dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Remote</dt>
|
||||||
|
<dd>
|
||||||
|
{#if repository.git.remote}
|
||||||
|
<code>{repository.git.remote.name}</code> · {repository.git.remote.url}
|
||||||
|
{#if repository.git.remote.redacted}<small>credentials redacted</small>{/if}
|
||||||
|
{:else}
|
||||||
|
not configured
|
||||||
|
{/if}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
{:else}
|
||||||
|
<p>Git metadata is unavailable for this local Repository.</p>
|
||||||
|
{/if}
|
||||||
|
{:else if repositoryError}
|
||||||
|
<p class="error">{repositoryError}</p>
|
||||||
|
{:else}
|
||||||
|
<p>Waiting for Git summary…</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>Recent Git log</h2>
|
||||||
|
{#if repositoryLog}
|
||||||
|
{#if repositoryLog.items.length === 0}
|
||||||
|
<p>No recent commits are available from the bounded Git log API.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Commit</th>
|
||||||
|
<th>Subject</th>
|
||||||
|
<th>Author</th>
|
||||||
|
<th>Timestamp</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each repositoryLog.items as commit (commit.hash)}
|
||||||
|
<tr>
|
||||||
|
<td><code>{shortHash(commit.hash)}</code></td>
|
||||||
|
<td>{commit.subject}</td>
|
||||||
|
<td>{commit.author_name} <small>{commit.author_email}</small></td>
|
||||||
|
<td>{commit.timestamp}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else if repositoryLogError}
|
||||||
|
<p class="error">{repositoryLogError}</p>
|
||||||
|
{:else}
|
||||||
|
<p>Waiting for <code>/api/repositories/local/log</code>…</p>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>Repository Ticket Kanban</h2>
|
||||||
|
<p class="section-note">
|
||||||
|
Read-only grouping of canonical Ticket records. No drag/drop or lifecycle mutation is exposed.
|
||||||
|
</p>
|
||||||
|
{#if repositoryTickets}
|
||||||
|
<div class="kanban">
|
||||||
|
{#each repositoryTickets.columns as column (column.state)}
|
||||||
|
<article class="kanban-column">
|
||||||
|
<h3>{column.state} <span>{column.items.length}</span></h3>
|
||||||
|
{#if column.items.length === 0}
|
||||||
|
<p class="muted">No tickets.</p>
|
||||||
|
{:else}
|
||||||
|
<ul>
|
||||||
|
{#each column.items as ticket (ticket.id)}
|
||||||
|
<li>
|
||||||
|
<strong>{ticket.title}</strong>
|
||||||
|
<small><code>{ticket.id}</code> · updated {formatDate(ticket.updated_at)}</small>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</article>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else if repositoryTicketsError}
|
||||||
|
<p class="error">{repositoryTicketsError}</p>
|
||||||
|
{:else}
|
||||||
|
<p>Waiting for <code>/api/repositories/local/tickets</code>…</p>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{@const repositoryDiagnostics = diagnosticsFor(repository?.git.diagnostics, repositoryLog?.diagnostics, repositoryTickets?.diagnostics)}
|
||||||
|
{#if repositoryDiagnostics.length > 0}
|
||||||
|
<section class="card diagnostics">
|
||||||
|
<h2>Repository diagnostics</h2>
|
||||||
|
<ul>
|
||||||
|
{#each repositoryDiagnostics as diagnostic}
|
||||||
|
<li>
|
||||||
|
<strong>{diagnostic.severity}</strong>
|
||||||
|
<code>{diagnostic.code}</code>
|
||||||
|
<span>{diagnostic.message}</span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
{:else if route.page === 'objectives'}
|
||||||
|
<section class="card">
|
||||||
|
<h2>Objectives</h2>
|
||||||
|
<p class="section-note">
|
||||||
|
Objectives are read from canonical filesystem records through <code>/api/objectives</code>.
|
||||||
|
</p>
|
||||||
|
{#if objectives}
|
||||||
|
{#if objectives.items.length === 0}
|
||||||
|
<p>No Objective records are present.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="stack">
|
||||||
|
{#each objectives.items as objective (objective.id)}
|
||||||
|
<article class="runtime-card selected-card" class:selected={route.objectiveId === objective.id}>
|
||||||
|
<div class="runtime-heading">
|
||||||
|
<strong>{objective.title}</strong>
|
||||||
|
<span>{objective.state}</span>
|
||||||
|
</div>
|
||||||
|
<p>{objective.summary || 'No summary text is available.'}</p>
|
||||||
|
<dl>
|
||||||
|
<div>
|
||||||
|
<dt>ID</dt>
|
||||||
|
<dd><code>{objective.id}</code></dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Updated</dt>
|
||||||
|
<dd>{formatDate(objective.updated_at)}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Linked tickets</dt>
|
||||||
|
<dd>{objective.linked_tickets?.length ? objective.linked_tickets.join(', ') : 'none'}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
<p><a href={`#/objectives/${objective.id}`}>Detail placeholder</a></p>
|
||||||
|
</article>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if objectives.invalid_records.length > 0}
|
||||||
|
<p class="error">{objectives.invalid_records.length} invalid objective record(s) hidden.</p>
|
||||||
|
{/if}
|
||||||
|
{:else if objectivesError}
|
||||||
|
<p class="error">{objectivesError}</p>
|
||||||
|
{:else}
|
||||||
|
<p>Waiting for <code>/api/objectives</code>…</p>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{#if route.objectiveId}
|
||||||
|
<section class="card detail-placeholder">
|
||||||
|
<h2>Objective detail</h2>
|
||||||
|
<p>
|
||||||
|
Selected Objective <code>{route.objectiveId}</code>. This slice keeps detail navigation as a
|
||||||
|
static SPA placeholder; canonical Objective content remains in the filesystem record.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>Workspace</h2>
|
<h2>Workspace</h2>
|
||||||
{#if workspace}
|
{#if workspace}
|
||||||
|
|
@ -143,8 +441,8 @@
|
||||||
<dd>{workspace.extension_points.host_worker_bridge.status}</dd>
|
<dd>{workspace.extension_points.host_worker_bridge.status}</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
{:else if loadError}
|
{:else if workspaceError}
|
||||||
<p class="error">{loadError}</p>
|
<p class="error">{workspaceError}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<p>Waiting for <code>/api/workspace</code>…</p>
|
<p>Waiting for <code>/api/workspace</code>…</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -195,7 +493,7 @@
|
||||||
<dd>{host.kind}</dd>
|
<dd>{host.kind}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>Local Pod inspection</dt>
|
<dt>Local inspection</dt>
|
||||||
<dd>{host.capabilities.local_pod_inspection}</dd>
|
<dd>{host.capabilities.local_pod_inspection}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -207,8 +505,8 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if loadError}
|
{:else if hostsError}
|
||||||
<p class="error">{loadError}</p>
|
<p class="error">{hostsError}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<p>Waiting for <code>/api/hosts</code>…</p>
|
<p>Waiting for <code>/api/hosts</code>…</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -243,15 +541,15 @@
|
||||||
<td><code>{worker.host_id}</code></td>
|
<td><code>{worker.host_id}</code></td>
|
||||||
<td>{worker.state} · {worker.status}</td>
|
<td>{worker.state} · {worker.status}</td>
|
||||||
<td>{worker.workspace_root ?? 'unknown'}</td>
|
<td>{worker.workspace_root ?? 'unknown'}</td>
|
||||||
<td>{worker.implementation.kind}: {worker.implementation.pod_name}</td>
|
<td>{worker.implementation.kind}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if loadError}
|
{:else if workersError}
|
||||||
<p class="error">{loadError}</p>
|
<p class="error">{workersError}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<p>Waiting for <code>/api/workers</code>…</p>
|
<p>Waiting for <code>/api/workers</code>…</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -275,9 +573,15 @@
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</main>
|
{/if}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
:global(*) {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
:global(body) {
|
:global(body) {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: #0f172a;
|
background: #0f172a;
|
||||||
|
|
@ -286,14 +590,46 @@
|
||||||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shell {
|
.workspace-layout {
|
||||||
width: min(1120px, calc(100vw - 32px));
|
display: grid;
|
||||||
|
grid-template-columns: minmax(240px, 300px) minmax(0, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
width: min(1240px, calc(100vw - 32px));
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 48px 0;
|
padding: 32px 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
display: grid;
|
||||||
|
gap: 24px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero {
|
.hero {
|
||||||
margin-bottom: 24px;
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero p {
|
||||||
|
max-width: 68ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-links {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-links a,
|
||||||
|
.card a {
|
||||||
|
color: #7dd3fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-links a {
|
||||||
|
border: 1px solid rgba(125, 211, 252, 0.28);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.eyebrow {
|
.eyebrow {
|
||||||
|
|
@ -307,12 +643,20 @@
|
||||||
margin: 0 0 16px;
|
margin: 0 0 16px;
|
||||||
font-size: clamp(2.5rem, 8vw, 5rem);
|
font-size: clamp(2.5rem, 8vw, 5rem);
|
||||||
line-height: 0.95;
|
line-height: 0.95;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2,
|
||||||
|
h3 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
li,
|
||||||
|
dd {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
color: #bae6fd;
|
color: #bae6fd;
|
||||||
}
|
}
|
||||||
|
|
@ -320,12 +664,12 @@
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
|
||||||
margin-top: 16px;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.runtime {
|
.runtime {
|
||||||
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
|
|
@ -334,6 +678,12 @@
|
||||||
background: rgba(15, 23, 42, 0.75);
|
background: rgba(15, 23, 42, 0.75);
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.35);
|
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.35);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-note,
|
||||||
|
.muted {
|
||||||
|
color: #94a3b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stack {
|
.stack {
|
||||||
|
|
@ -341,13 +691,19 @@
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.runtime-card {
|
.runtime-card,
|
||||||
|
.kanban-column {
|
||||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: rgba(15, 23, 42, 0.55);
|
background: rgba(15, 23, 42, 0.55);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selected-card.selected {
|
||||||
|
border-color: rgba(56, 189, 248, 0.5);
|
||||||
|
background: rgba(14, 165, 233, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
.runtime-heading {
|
.runtime-heading {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
@ -408,6 +764,36 @@
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kanban {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr));
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-column h3 {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-column ul {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-column li {
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(30, 41, 59, 0.72);
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.diagnostics {
|
.diagnostics {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
@ -418,7 +804,24 @@
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-placeholder {
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
color: #fca5a5;
|
color: #fca5a5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.workspace-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
width: min(100vw - 24px, 620px);
|
||||||
|
gap: 18px;
|
||||||
|
padding: 18px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user