sync: oto_console shell/executions/runners surfaces refactor and agent-ui/definition updates

This commit is contained in:
toki 2026-07-04 00:09:27 +09:00
parent 9e844ba217
commit 44b9b37d1a
12 changed files with 838 additions and 378 deletions

View file

@ -0,0 +1,222 @@
<!-- task=m-control-plane-jenkins-menu-ia-code-sync/03+01_executions_ops plan=0 tag=JENKINS_IA_OPS -->
# Code Review Reference - JENKINS_IA_OPS
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation.
> Finalization is review-agent-only.
## 개요
date=2026-07-03
task=m-control-plane-jenkins-menu-ia-code-sync/03+01_executions_ops, plan=0, tag=JENKINS_IA_OPS
## Roadmap Targets
- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/control-plane-jenkins-menu-ia-code-sync.md`
- Task ids:
- `execution-ops-menu-sync`: executions/runners/settings 화면이 `Pipeline Overview`, `Console Output`, `Artifacts`, `Build History`, `Nodes/Agents`, `Manage OTO` 중심으로 동기화된다.
- `preview-deploy`: 동기화된 Flutter web build를 원격 preview에 배포한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [JENKINS_IA_OPS-1] Pipeline Graph Run Detail | [x] |
| [JENKINS_IA_OPS-2] Nodes And Agents Runners Surface | [x] |
| [JENKINS_IA_OPS-3] Manage OTO Settings | [x] |
| [JENKINS_IA_OPS-4] Preview Deployment Smoke | [x] |
## 구현 체크리스트
- [x] [JENKINS_IA_OPS-1] executions run detail을 Pipeline Graph View/Stage View 및 run-context left task menu 중심 구조로 개편한다.
- [x] [JENKINS_IA_OPS-2] runners를 Jenkins Nodes/Agents style 운영 목록으로 개편한다.
- [x] [JENKINS_IA_OPS-3] settings fallback을 Manage OTO category 구조로 개편한다.
- [x] [JENKINS_IA_OPS-4] console/client tests를 갱신하고 원격 preview를 재배포한다.
- [x] `cd packages/flutter/oto_console && flutter analyze && flutter test`, `cd apps/client && flutter analyze && flutter test`, `git diff --check`, preview `curl -I`를 실행한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## Agent UI Completion
- Mode: review-pass-status-update
- Agent UI docs:
- `agent-ui/definition/index.md`
- `agent-ui/definition/views/index.md`
- `agent-ui/definition/components/index.md`
- Actual code evidence:
- [x] `packages/flutter/oto_console/lib/src/oto_executions_surface.dart`: `_buildExecutionDetail`가 `_RunTaskMenu`(Console Output/Pipeline Overview/Artifacts/Build History 좌측 task menu)와 `_PipelineStageGraph`(`Stage Graph`, Checkout/Build/Package/Publish 상태 chip, run state 기반 secondary note)를 primary region으로 렌더한다. `Stage summary unavailable` 단독 placeholder는 제거했고, 기존 Logs Preview pane은 `Console Output` 타이틀로 통합했다.
- [x] `packages/flutter/oto_console/lib/src/oto_runners_surface.dart`: `_NodesTableHeader`(Nodes/Executor/Current job/Current run/Manage 컬럼 헤더)와 `_NodeTableRow`(node id/alias, executor status, current job/run, manage 아이콘, 기존 message/self-update panel 유지)로 Runners를 Nodes/Agents 운영 목록으로 재구성했다.
- [x] `packages/flutter/oto_console/lib/src/oto_console_shell.dart`: settings fallback을 `_ManageOtoSettingsSurface`(`Manage OTO` 타이틀 + `System`/`Security` category card)로 교체했다. System 카드는 기존 Runtime endpoints(HTTP API/Wire URL) 값을, Security 카드는 기존 Browser policy(Browser API policy, CORS 안내) 값을 그대로 유지한다.
- Implementation verification evidence:
- [x] `cd packages/flutter/oto_console && flutter analyze && flutter test`: `No issues found!` / `00:01 +14: All tests passed!`
- [x] `cd apps/client && flutter analyze && flutter test`: `No issues found!` / `00:03 +27: All tests passed!`
- [x] `curl -I http://toki-labs.com:13020/`: `HTTP/1.0 200 OK` (Server: SimpleHTTP/0.6 Python/3.9.6)
- Status updates requested on PASS:
- [x] `agent-ui/definition/views/index.md`: `executions`, `runners`, `settings` Jenkins IA criteria `계획` -> `구현됨`
- [x] `agent-ui/definition/components/index.md`: ops component criteria `계획` -> `구현됨`
- Review finalization:
- [x] Review agent updated the listed agent-ui docs and code evidence.
- [x] Review agent ran `validate-agent-ui scope=jenkins-ia-executions-ops` after the status/evidence update.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] active review/plan 파일을 `.log`로 아카이브한다.
- [x] PASS이면 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/m-control-plane-jenkins-menu-ia-code-sync/03+01_executions_ops/`로 이동한다.
- [x] PASS split 작업이면 빈 active parent를 제거하거나 남은 sibling 때문에 유지했다고 확인한다.
- [ ] WARN/FAIL이면 후속 active plan/review 또는 정당한 `USER_REVIEW.md`를 작성한다.
## 계획 대비 변경 사항
- `rsync`가 로컬 환경(sandbox)에 없어 계획의 `rsync -a --delete` 대신 `tar czf - . | ssh ... tar xzf - -C "$preview_dir"`로 동일한 결과(신규 preview dir에 build/web 전체 배포)를 만들었다. 목적지 디렉터리는 계획대로 새 `oto-ui-preview-<timestamp>-jenkins-ia` 경로이며 기존 checkout은 건드리지 않았다.
- `apps/client/test/widget_test.dart:1729` 부근 실행 시 `_DetailPane` 헤더 Row(`Row(mainAxisAlignment: spaceBetween)`)가 새 run-context 좌측 task menu 폭(168px)만큼 줄어든 콘텐츠 폭에서 `RenderFlex` overflow를 냈다. `packages/flutter/oto_console/lib/src/oto_executions_surface.dart`의 `_DetailPane` title을 `Expanded(overflow: ellipsis)`로 감싸 action 버튼과 공존 가능하게 수정했다(계획에 명시되지 않은 부수 수정, `_DetailPane` 자체는 계획 대상 파일 범위 안).
- Settings Manage OTO 카테고리는 계획 체크리스트가 예시로 든 `System/Security/Tools/Nodes/Browser policy` 중 실제 데이터가 있는 `System`(Runtime endpoints)과 `Security`(Browser policy 하위 그룹)만 구현했다. `Tools`/`Nodes`는 OTO Core에 대응 데이터가 없어 뼈대만 있는 placeholder 카테고리를 추가하지 않았다(범위 결정 근거의 "OTO contract에 맞는 항목만 표시" 원칙 적용). 필요한 텍스트(`Manage OTO`/`System`/`Security`/`Browser policy`)는 모두 노출된다.
- Runners 테이블은 계획의 "Nodes, Executor, Current job, Current run, Manage" 컬럼 헤더를 그대로 추가했지만, `apps/client/test/widget_test.dart`가 OPS-2 범위 밖이라 기존 row 텍스트(`ID: $runnerID`, `Alias: ...`, `Message: ...`, `Job: ...`/`Execution: ...`)는 그대로 보존했다. `Manage` 컬럼은 아이콘만 표시하고 기존 Self Update 폼은 행 하단에 그대로 유지했다(플랜이 요구한 콜백/검증 로직 변경 없음).
## 주요 설계 결정
- Run-context 좌측 task menu(`_RunTaskMenu`)는 별도 per-item view가 없어 Jenkins build 페이지 사이드바처럼 정적 라벨 목록으로 구현했다(클릭 동작 없음). Core에 stage/console/artifact를 개별 뷰로 분리할 데이터 소스가 없는 현재 범위에서 과도한 상태 관리를 피하기 위한 선택이다.
- Stage Graph는 실제 stage 데이터가 없으므로 `Checkout/Build/Package/Publish` 고정 스켈레톤을 실행 `state`(succeeded/failed/running/canceled/queued)에서 결정적으로 매핑했다. `Stage summary unavailable`는 제거하고 그래프 하단의 secondary note(“Per-stage telemetry is not reported yet …”)로 대체했다.
- Manage OTO Security 카드는 기존 Browser policy 상세 그룹(값 불변)을 그대로 재사용해 `Security`(카테고리)와 `Browser policy`(하위 그룹) 두 라벨이 자연스럽게 함께 노출되도록 구성했다.
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
- 상태: 없음
- 사유 유형: 없음
- 연결 대상: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- `Stage summary unavailable` 단독 placeholder가 사라지고 `Stage Graph`와 run-context left task menu가 primary run detail이 되었는지 확인한다.
- Runners가 generic card list가 아니라 Nodes/Agents operational list로 보이는지 확인한다.
- Settings fallback이 Manage OTO category menu/surface인지 확인한다.
- Preview deployment가 dirty remote checkout을 source로 사용하지 않았는지 확인한다.
## 검증 결과
### JENKINS_IA_OPS-1 중간 검증
```bash
$ cd packages/flutter/oto_console && flutter test test/oto_console_test.dart
00:01 +9: OtoExecutionsSurface renders loading/empty/error/data states and expanded detail
00:01 +14: All tests passed!
```
(첫 실행 시 apps/client 쪽에서 좌측 task menu로 줄어든 폭 때문에 `_DetailPane` 헤더 Row가 RenderFlex overflow를 냈다. `_DetailPane` title을 `Expanded(overflow: ellipsis)`로 수정한 뒤 재실행하여 통과했다 — 상세는 `계획 대비 변경 사항` 참조.)
### JENKINS_IA_OPS-2 중간 검증
```bash
$ cd packages/flutter/oto_console && flutter test test/oto_console_test.dart
00:00 +6: OtoRunnersSurface renders data state with runners list
00:01 +7: OtoRunnersSurface renders runner action states
00:01 +14: All tests passed!
```
### JENKINS_IA_OPS-3 중간 검증
```bash
$ cd packages/flutter/oto_console && flutter test test/oto_console_test.dart
00:00 +2: renders section surfaces with empty states
00:01 +14: All tests passed!
```
### JENKINS_IA_OPS-4 중간 검증
```bash
$ cd apps/client && flutter build web --dart-define=OTO_SERVER_HTTP_URL=http://toki-labs.com:18020
Compiling lib/main.dart for the Web... 31.9s
✓ Built build/web
$ preview_dir="/Users/toki/agent-work/oto-ui-preview-20260703142823-jenkins-ia"
$ ssh toki@toki-labs.com "mkdir -p '$preview_dir'"
(성공, 출력 없음)
# 로컬 sandbox에 rsync가 없어 tar 파이프로 대체(계획 대비 변경 사항 참조):
$ cd apps/client/build/web && tar czf - . | ssh toki@toki-labs.com "tar xzf - -C '$preview_dir'"
$ ssh toki@toki-labs.com "ls '$preview_dir' | head -5"
assets
canvaskit
favicon.png
flutter_bootstrap.js
flutter_service_worker.js
$ ssh toki@toki-labs.com "pkill -f 'python3 -m http.server 13020' || true; sleep 1; cd '$preview_dir' && nohup python3 -m http.server 13020 >/tmp/oto-ui-preview.log 2>&1 & disown; sleep 1; echo started"
started
$ curl -I http://toki-labs.com:13020/
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.9.6
Date: Fri, 03 Jul 2026 14:28:47 GMT
Content-type: text/html
Content-Length: 1540
```
### 최종 검증
```bash
$ find agent-task/archive/2026/07/m-control-plane-jenkins-menu-ia-code-sync/01_definition_guardrails -name "complete.log"
agent-task/archive/2026/07/m-control-plane-jenkins-menu-ia-code-sync/01_definition_guardrails/complete.log
# (predecessor 01은 이미 archive로 이동되어 완료되었음 — 02+01_shell_jobs도 archive에서 complete.log 확인됨)
$ cd packages/flutter/oto_console && flutter analyze && flutter test
Analyzing oto_console...
No issues found! (ran in 3.4s)
00:01 +14: All tests passed!
$ cd apps/client && flutter analyze && flutter test
Analyzing client...
No issues found! (ran in 2.4s)
00:03 +27: All tests passed!
$ git diff --check
(출력 없음, exit code 0)
$ cd apps/client && flutter build web --dart-define=OTO_SERVER_HTTP_URL=http://toki-labs.com:18020
✓ Built build/web
$ curl -I http://toki-labs.com:13020/
HTTP/1.0 200 OK
```
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
## 코드리뷰 결과
- 종합 판정: PASS
- 차원별 평가:
- correctness: Pass
- completeness: Pass
- test coverage: Pass
- API contract: Pass
- code quality: Pass
- implementation deviation: Pass
- verification trust: Pass
- spec conformance: Pass (Roadmap Targets 확인, SDD 불필요)
- 발견된 문제: 없음
- 검증 근거:
- `git diff --check`: 출력 없음, exit code 0
- `cd packages/flutter/oto_console && flutter analyze && flutter test`: `No issues found!`, `+14: All tests passed!`
- `cd apps/client && flutter analyze && flutter test`: `No issues found!`, `+27: All tests passed!`
- 원격 runner 임시 작업트리 `/Users/toki/agent-work/oto-review-20260703144007-jenkins-ia`: console/client analyze/test 및 `flutter build web --dart-define=OTO_SERVER_HTTP_URL=http://toki-labs.com:18020` PASS
- `curl -I --max-time 15 http://toki-labs.com:13020/`: `HTTP/1.0 200 OK`
- `validate-agent-ui scope=jenkins-ia-executions-ops`: PASS (필수 root 문서 존재, USER_REVIEW 없음, code evidence 경로 존재, listed view/component status 반영 확인)
- 다음 단계: PASS이므로 active plan/review를 `.log`로 아카이브하고 `complete.log` 작성 후 task directory를 archive로 이동한다.

View file

@ -0,0 +1,58 @@
# Complete - m-control-plane-jenkins-menu-ia-code-sync/03+01_executions_ops
## 완료 일시
2026-07-03
## 요약
Jenkins IA executions/ops 화면 동기화 구현을 1회차 리뷰에서 PASS로 종료했다.
## 루프 이력
| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `plan_cloud_G07_0.log` | `code_review_cloud_G07_0.log` | PASS | executions stage graph/run context menu, runners Nodes/Agents surface, Manage OTO settings, preview smoke 검증 완료 |
## 구현/정리 내용
- `packages/flutter/oto_console/lib/src/oto_executions_surface.dart`에 run-context task menu와 deterministic `Stage Graph`를 추가하고 `Console Output`/`Artifacts` detail pane 구조로 정리했다.
- `packages/flutter/oto_console/lib/src/oto_runners_surface.dart`를 Nodes/Agents 운영 목록 형태로 재구성하고 기존 self-update action 흐름을 유지했다.
- `packages/flutter/oto_console/lib/src/oto_console_shell.dart`의 settings fallback을 `Manage OTO` category surface로 교체했다.
- `packages/flutter/oto_console/test/oto_console_test.dart`와 `apps/client/test/widget_test.dart`를 Jenkins IA visible primitive 기준으로 갱신했다.
- `agent-ui/definition/index.md`, `agent-ui/definition/views/index.md`, `agent-ui/definition/components/index.md`의 executions/ops 구현 상태와 code evidence를 PASS 기준으로 반영했다.
## 최종 검증
- `git diff --check` - PASS; 출력 없음, exit code 0
- `cd packages/flutter/oto_console && flutter analyze && flutter test` - PASS; `No issues found!`, `+14: All tests passed!`
- `cd apps/client && flutter analyze && flutter test` - PASS; `No issues found!`, `+27: All tests passed!`
- 원격 runner 임시 작업트리 `/Users/toki/agent-work/oto-review-20260703144007-jenkins-ia`에서 `cd packages/flutter/oto_console && flutter analyze && flutter test`, `cd apps/client && flutter analyze && flutter test`, `flutter build web --dart-define=OTO_SERVER_HTTP_URL=http://toki-labs.com:18020` - PASS; console/client tests 통과, web build 성공
- `cd apps/client && flutter build web --dart-define=OTO_SERVER_HTTP_URL=http://toki-labs.com:18020` - PASS; `Built build/web`
- `curl -I --max-time 15 http://toki-labs.com:13020/` - PASS; `HTTP/1.0 200 OK`
- `validate-agent-ui scope=jenkins-ia-executions-ops` - PASS; 필수 root 문서 존재, USER_REVIEW 없음, code evidence 경로 존재, listed view/component status 반영 확인
## Roadmap Completion
- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/control-plane-jenkins-menu-ia-code-sync.md`
- Completed task ids:
- `execution-ops-menu-sync`: PASS; evidence=`agent-task/archive/2026/07/m-control-plane-jenkins-menu-ia-code-sync/03+01_executions_ops/plan_cloud_G07_0.log`, `agent-task/archive/2026/07/m-control-plane-jenkins-menu-ia-code-sync/03+01_executions_ops/code_review_cloud_G07_0.log`; verification=`cd packages/flutter/oto_console && flutter analyze && flutter test`, `cd apps/client && flutter analyze && flutter test`, `validate-agent-ui scope=jenkins-ia-executions-ops`
- `preview-deploy`: PASS; evidence=`agent-task/archive/2026/07/m-control-plane-jenkins-menu-ia-code-sync/03+01_executions_ops/plan_cloud_G07_0.log`, `agent-task/archive/2026/07/m-control-plane-jenkins-menu-ia-code-sync/03+01_executions_ops/code_review_cloud_G07_0.log`; verification=`cd apps/client && flutter build web --dart-define=OTO_SERVER_HTTP_URL=http://toki-labs.com:18020`, `curl -I --max-time 15 http://toki-labs.com:13020/`
- Not completed task ids: 없음
## Agent UI Completion
- Agent UI docs:
- `agent-ui/definition/index.md`: `구현됨` 근거 반영; code evidence=`packages/flutter/oto_console/lib/src/oto_executions_surface.dart`, `packages/flutter/oto_console/lib/src/oto_runners_surface.dart`, `packages/flutter/oto_console/lib/src/oto_console_shell.dart`; implementation verification=`cd packages/flutter/oto_console && flutter analyze && flutter test`, `cd apps/client && flutter analyze && flutter test`; validation=`validate-agent-ui scope=jenkins-ia-executions-ops` PASS
- `agent-ui/definition/views/index.md`: `executions`, `runners`, `settings` Jenkins IA criteria `구현됨`; code evidence=`packages/flutter/oto_console/lib/src/oto_executions_surface.dart`, `packages/flutter/oto_console/lib/src/oto_runners_surface.dart`, `packages/flutter/oto_console/lib/src/oto_console_shell.dart`; implementation verification=`cd packages/flutter/oto_console && flutter analyze && flutter test`, `cd apps/client && flutter analyze && flutter test`; validation=`validate-agent-ui scope=jenkins-ia-executions-ops` PASS
- `agent-ui/definition/components/index.md`: `section-surface`, `record-list`, `execution-action-toolbar` ops component criteria `구현됨`; code evidence=`packages/flutter/oto_console/lib/src/oto_console_shell.dart`, `packages/flutter/oto_console/lib/src/oto_runners_surface.dart`, `packages/flutter/oto_console/lib/src/oto_executions_surface.dart`; implementation verification=`cd packages/flutter/oto_console && flutter analyze && flutter test`, `cd apps/client && flutter analyze && flutter test`; validation=`validate-agent-ui scope=jenkins-ia-executions-ops` PASS
- Status updates: 완료
- Remaining agent-ui issues: 없음
## 잔여 Nit
- 없음
## 후속 작업
- 없음

View file

@ -1,165 +0,0 @@
<!-- task=m-control-plane-jenkins-menu-ia-code-sync/03+01_executions_ops plan=0 tag=JENKINS_IA_OPS -->
# Code Review Reference - JENKINS_IA_OPS
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation.
> Finalization is review-agent-only.
## 개요
date=2026-07-03
task=m-control-plane-jenkins-menu-ia-code-sync/03+01_executions_ops, plan=0, tag=JENKINS_IA_OPS
## Roadmap Targets
- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/control-plane-jenkins-menu-ia-code-sync.md`
- Task ids:
- `execution-ops-menu-sync`: executions/runners/settings 화면이 `Pipeline Overview`, `Console Output`, `Artifacts`, `Build History`, `Nodes/Agents`, `Manage OTO` 중심으로 동기화된다.
- `preview-deploy`: 동기화된 Flutter web build를 원격 preview에 배포한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [JENKINS_IA_OPS-1] Pipeline Graph Run Detail | [ ] |
| [JENKINS_IA_OPS-2] Nodes And Agents Runners Surface | [ ] |
| [JENKINS_IA_OPS-3] Manage OTO Settings | [ ] |
| [JENKINS_IA_OPS-4] Preview Deployment Smoke | [ ] |
## 구현 체크리스트
- [ ] [JENKINS_IA_OPS-1] executions run detail을 Pipeline Graph View/Stage View 및 run-context left task menu 중심 구조로 개편한다.
- [ ] [JENKINS_IA_OPS-2] runners를 Jenkins Nodes/Agents style 운영 목록으로 개편한다.
- [ ] [JENKINS_IA_OPS-3] settings fallback을 Manage OTO category 구조로 개편한다.
- [ ] [JENKINS_IA_OPS-4] console/client tests를 갱신하고 원격 preview를 재배포한다.
- [ ] `cd packages/flutter/oto_console && flutter analyze && flutter test`, `cd apps/client && flutter analyze && flutter test`, `git diff --check`, preview `curl -I`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## Agent UI Completion
- Mode: review-pass-status-update
- Agent UI docs:
- `agent-ui/definition/index.md`
- `agent-ui/definition/views/index.md`
- `agent-ui/definition/components/index.md`
- Actual code evidence:
- [ ] `packages/flutter/oto_console/lib/src/oto_executions_surface.dart`: <implemented evidence, including Console Output/Pipeline Overview/Artifacts/Build History task menu>
- [ ] `packages/flutter/oto_console/lib/src/oto_runners_surface.dart`: <implemented evidence>
- [ ] `packages/flutter/oto_console/lib/src/oto_console_shell.dart`: <implemented evidence>
- Implementation verification evidence:
- [ ] `cd packages/flutter/oto_console && flutter analyze && flutter test`: <actual result or output path>
- [ ] `cd apps/client && flutter analyze && flutter test`: <actual result or output path>
- [ ] `curl -I http://toki-labs.com:13020/`: <actual result or output path>
- Status updates requested on PASS:
- [ ] `agent-ui/definition/views/index.md`: `executions`, `runners`, `settings` Jenkins IA criteria `계획` -> `구현됨`
- [ ] `agent-ui/definition/components/index.md`: ops component criteria `계획` -> `구현됨`
- Review finalization:
- [ ] Review agent updated the listed agent-ui docs and code evidence.
- [ ] Review agent ran `validate-agent-ui scope=jenkins-ia-executions-ops` after the status/evidence update.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] active review/plan 파일을 `.log`로 아카이브한다.
- [ ] PASS이면 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/m-control-plane-jenkins-menu-ia-code-sync/03+01_executions_ops/`로 이동한다.
- [ ] PASS split 작업이면 빈 active parent를 제거하거나 남은 sibling 때문에 유지했다고 확인한다.
- [ ] WARN/FAIL이면 후속 active plan/review 또는 정당한 `USER_REVIEW.md`를 작성한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
- 상태: 없음
- 사유 유형: 없음
- 연결 대상: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- `Stage summary unavailable` 단독 placeholder가 사라지고 `Stage Graph`와 run-context left task menu가 primary run detail이 되었는지 확인한다.
- Runners가 generic card list가 아니라 Nodes/Agents operational list로 보이는지 확인한다.
- Settings fallback이 Manage OTO category menu/surface인지 확인한다.
- Preview deployment가 dirty remote checkout을 source로 사용하지 않았는지 확인한다.
## 검증 결과
### JENKINS_IA_OPS-1 중간 검증
```bash
$ cd packages/flutter/oto_console && flutter test test/oto_console_test.dart
(output)
```
### JENKINS_IA_OPS-2 중간 검증
```bash
$ cd packages/flutter/oto_console && flutter test test/oto_console_test.dart
(output)
```
### JENKINS_IA_OPS-3 중간 검증
```bash
$ cd packages/flutter/oto_console && flutter test test/oto_console_test.dart
(output)
```
### JENKINS_IA_OPS-4 중간 검증
```bash
$ cd apps/client && flutter build web --dart-define=OTO_SERVER_HTTP_URL=http://toki-labs.com:18020
(output)
$ preview_dir="/Users/toki/agent-work/oto-ui-preview-$(date -u +%Y%m%d%H%M%S)-jenkins-ia"
$ ssh toki@toki-labs.com "mkdir -p '$preview_dir'"
(output)
$ rsync -a --delete apps/client/build/web/ "toki@toki-labs.com:$preview_dir/"
(output)
$ ssh toki@toki-labs.com "pkill -f 'python3 -m http.server 13020' || true; cd '$preview_dir' && nohup python3 -m http.server 13020 >/tmp/oto-ui-preview.log 2>&1 &"
(output)
$ curl -I http://toki-labs.com:13020/
(output)
```
### 최종 검증
```bash
$ test -f agent-task/m-control-plane-jenkins-menu-ia-code-sync/01_definition_guardrails/complete.log
(output)
$ cd packages/flutter/oto_console && flutter analyze && flutter test
(output)
$ cd apps/client && flutter analyze && flutter test
(output)
$ git diff --check
(output)
$ cd apps/client && flutter build web --dart-define=OTO_SERVER_HTTP_URL=http://toki-labs.com:18020
(output)
$ curl -I http://toki-labs.com:13020/
(output)
```
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**

View file

@ -29,17 +29,17 @@ components:
|--------------|--------|----------|-------------|
| `console-shell` | 구현됨 | `oto_console_shell.dart` + Jenkins IA shell/jobs gate | overall console scaffold with integrated header, breadcrumb, action slot, left menu, and selected section content. |
| `console-rail` | 계획 | `oto_console_shell.dart` + Jenkins IA gate | section navigation rail. `New Item`/`Build History`와 job context menu는 구현됨. `Nodes`, `Manage OTO`, run context menu는 03 후속 구현 대기. |
| `section-surface` | 계획 | `oto_console_shell.dart` + Jenkins IA gate | generic titled empty/detail section shell. 필수 primitive: `Manage OTO` 관리 category grouping. Jenkins IA gate 미충족, 후속 구현 대기. |
| `section-surface` | 구현됨 | `oto_console_shell.dart` + Jenkins IA executions/ops gate | generic titled empty/detail section shell. `Manage OTO` 관리 category grouping과 `System`/`Security`/`Browser policy` primitive가 widget 테스트로 확인됨. |
| `state-panel` | 구현됨 | surface files + Jenkins reference baseline | loading, empty, error state blocks. Jenkins monitor/banner-style state 반영 |
| `status-badge` | 구현됨 | overview/runners/jobs/executions surface files + Jenkins reference baseline | compact status label badge. Jenkins result/severity vocabulary 정렬 반영 |
| `endpoint-tile` | 구현됨 | `oto_console_overview.dart` | endpoint or config value tile |
| `connection-status-grid` | 구현됨 | `oto_console_overview.dart` | health/readiness status grid |
| `auth-token-reference` | 구현됨 | `oto_console_overview.dart` | display-only auth token reference |
| `record-list` | 구현됨 | jobs/runners/executions/artifacts surface files + Jenkins IA shell/jobs gate | repeated entity list rows. Pipelines job table은 Jenkins Dashboard density로 구현됨. Build History/Nodes density 보강은 03에서 이어간다. |
| `record-list` | 구현됨 | jobs/runners/executions/artifacts surface files + Jenkins IA shell/jobs/executions/ops gate | repeated entity list rows. Pipelines job table, Build History run detail, Nodes/Agents runner row density가 widget 테스트로 확인됨. |
| `action-feedback` | 구현됨 | runners/jobs/executions surface files + Jenkins reference baseline | success/deferred/failed action message. Jenkins action outcome 정렬 반영 |
| `runner-self-update-form` | 구현됨 | `oto_runners_surface.dart` + Jenkins reference baseline | runner self-update draft form. Jenkins update action density 정렬 반영 |
| `job-create-form` | 구현됨 | `oto_jobs_surface.dart` + Jenkins reference baseline | job create draft form. Jenkins New Item flow 정렬 반영 |
| `execution-action-toolbar` | 계획 | `oto_executions_surface.dart` + Jenkins IA gate | cancel/report/log/artifact execution actions. 필수 primitive: run detail의 `Pipeline Overview`/`Stage Graph`, `Console Output` 연결 action. Jenkins IA gate 미충족, 후속 구현 대기. |
| `execution-action-toolbar` | 구현됨 | `oto_executions_surface.dart` + Jenkins IA executions/ops gate | cancel/report/log/artifact execution actions. run detail의 `Pipeline Overview`/`Stage Graph`, `Console Output`, `Artifacts` 연결 primitive가 widget 테스트로 확인됨. |
| `authorization-state` | 계획 | security/audit SDD | operator/admin allow/deny, disabled, and error state rules for write actions |
| `audit-action-summary` | 계획 | security/audit SDD | display-safe action metadata summary for audit-oriented UI exposure |
| `log-preview` | 구현됨 | `oto_executions_surface.dart` + Jenkins reference baseline | bounded execution log preview. Jenkins Console Output/live log pane 정렬 반영 |

View file

@ -55,7 +55,7 @@ OTO Control Plane Flutter 콘솔의 운영 화면, 반복 component, 상태 표
## Target Layout Status
- Shell/jobs 범위는 통합 header/breadcrumb/action, Dashboard-level `New Item`/`Build History` menu, pipelines dashboard job table, job context `Status`/`Build History`/`Stages` primitive가 코드와 widget 테스트로 확인되어 구현 근거를 갖는다.
- Executions/runners/settings 범위의 run stage graph, `Console Output`, Nodes/Agents, `Manage OTO` 구조는 후속 03 구현 전까지 `계획` 상태로 둔다.
- Executions/runners/settings 범위의 run stage graph, `Console Output`, Nodes/Agents, `Manage OTO` 구조는 03 구현에서 코드와 widget 테스트로 확인되어 구현 근거를 갖는다.
- 목표는 pixel parity가 아니라 header/action/status/list/detail/log/artifact의 정보 배치와 운영 밀도를 Jenkins 최신 UI 흐름에 맞추는 것이다.
- 보안/감사 source of truth처럼 문서 계획만 있는 항목은 계속 `계획` 상태로 두며, Jenkins IA gate 미충족 항목과 섞지 않는다.
@ -98,3 +98,4 @@ Jenkins 정렬 대상 view/component는 아래 structural primitive가 코드와
- 2026-07-03: Jenkins 정렬 레이아웃 패스 구현과 검증 근거를 바탕으로 대상 view/component status를 `구현됨`으로 갱신했다.
- 2026-07-03: Jenkins reference를 러프 정렬에서 `Jenkins IA gate` structural acceptance로 강화하고, gate 미충족 대상 view/component status를 `계획`으로 정정했다. 후속 구현(02/03)이 primitive를 테스트로 확인하면 `구현됨`으로 승격한다.
- 2026-07-03: 02 shell/jobs 구현에서 통합 header/breadcrumb/action, `New Item`/`Build History` menu, pipelines dashboard job table과 job context menu primitive를 코드와 widget 테스트로 확인했다. Executions/runners/settings primitive는 03 범위로 남겼다.
- 2026-07-03: 03 executions/ops 구현에서 run context menu(`Pipeline Overview`, `Console Output`, `Artifacts`, `Build History`), `Stage Graph`, Nodes/Agents table, `Manage OTO` category surface를 코드와 widget 테스트로 확인했다.

View file

@ -17,12 +17,12 @@ views:
| View ID | Status | Evidence | Description |
|---------|--------|----------|-------------|
| `overview` | 계획 | `OtoConsoleOverview` + Jenkins IA gate | Control Plane endpoint, connection, authentication reference overview. 필수: 통합 header/breadcrumb/action(`Dashboard` root)이 shell/header에 존재한다. Jenkins IA gate 미충족, 후속 구현 대기. |
| `runners` | 계획 | `OtoRunnersSurface` + Jenkins IA gate | runner registry state and self-update action surface. 필수: Jenkins Nodes/Agents 목록 구조로 node 상태/현재 작업/연결 메시지를 스캔한다. Jenkins IA gate 미충족, 후속 구현 대기. |
| `runners` | 구현됨 | `OtoRunnersSurface` + Jenkins IA executions/ops gate | runner registry state and self-update action surface. Nodes/Agents 목록 구조와 node 상태/현재 작업/연결 메시지 primitive가 widget 테스트로 확인됨. |
| `pipelines` | 구현됨 | `OtoJobsSurface` + Jenkins IA shell/jobs gate | pipeline/job queue list and create job action surface. Dashboard job table density, `New Item` 생성 entry, job context `Status`/`Build History`/`Stages` primitive가 widget 테스트로 확인됨. |
| `executions` | 계획 | `OtoExecutionsSurface` + Jenkins IA gate | execution list, detail expansion, write actions, log/artifact preview. 필수: Pipeline Graph View 스타일 `Pipeline Overview`/`Stage Graph`, `Console Output`, `Build History`가 run detail에 존재한다. Jenkins IA gate 미충족, 후속 구현 대기. |
| `executions` | 구현됨 | `OtoExecutionsSurface` + Jenkins IA executions/ops gate | execution list, detail expansion, write actions, log/artifact preview. `Pipeline Overview`/`Stage Graph`, `Console Output`, `Artifacts`, `Build History` run context primitive가 widget 테스트로 확인됨. |
| `artifacts` | 구현됨 | `OtoArtifactsSurface` + Jenkins reference baseline | artifact event list. Jenkins artifact browser-style list 반영 |
| `agent` | 구현됨 | `OtoAgentPanel` | embeddable agent-shell panel for OTO capabilities |
| `settings` | 계획 | `OtoConsoleShell` fallback section + Jenkins IA gate | runtime endpoint and browser policy details. 필수: `Manage OTO`(Manage Jenkins 대응) 관리 category grouping이 존재한다. Jenkins IA gate 미충족, 후속 구현 대기. |
| `settings` | 구현됨 | `OtoConsoleShell` Manage OTO fallback + Jenkins IA executions/ops gate | runtime endpoint and browser policy details. `Manage OTO` 관리 category grouping과 `System`/`Security`/`Browser policy` primitive가 widget 테스트로 확인됨. |
## Required Visible Primitives

View file

@ -1730,8 +1730,14 @@ void _registerJobsExecutionsTests() {
await tester.pumpAndSettle();
expect(find.text('Run exec-99'), findsOneWidget);
expect(find.text('Stage summary unavailable'), findsOneWidget);
expect(find.text('Logs Preview'), findsOneWidget);
expect(find.text('Stage Graph'), findsOneWidget);
expect(find.text('Checkout'), findsOneWidget);
expect(find.text('Build'), findsOneWidget);
expect(find.text('Package'), findsOneWidget);
expect(find.text('Publish'), findsOneWidget);
// 'Console Output' appears once in the run-context task menu and once
// as the logs pane title.
expect(find.text('Console Output'), findsNWidgets(2));
expect(find.textContaining('Task 1 done'), findsOneWidget);
expect(find.text('release.tar.gz'), findsOneWidget);

View file

@ -115,40 +115,7 @@ class _OtoConsoleShellState extends State<OtoConsoleShell> {
OtoConsoleSection.agent =>
widget.agent ?? OtoAgentPanel(capabilities: widget.capabilities),
OtoConsoleSection.settings =>
widget.settings ??
OtoConsoleSectionSurface(
icon: Icons.tune,
title: 'Settings',
emptyTitle: 'No local preferences',
emptyMessage: 'Settings are using runtime defaults.',
detailGroups: [
OtoConsoleSectionDetailGroup(
title: 'Runtime endpoints',
details: [
if (widget.config != null)
OtoConsoleSectionDetail(
label: 'HTTP API',
value: widget.config!.serverHttpUrl,
),
if (widget.config != null)
OtoConsoleSectionDetail(
label: 'Wire URL',
value: widget.config!.serverWireUrl,
),
],
),
const OtoConsoleSectionDetailGroup(
title: 'Browser policy',
details: [
OtoConsoleSectionDetail(
label: 'Browser API policy',
value: 'CORS headers or a same-origin proxy are required.',
),
],
),
],
theme: theme,
),
widget.settings ?? _ManageOtoSettingsSurface(theme: theme, config: widget.config),
};
}
}
@ -588,6 +555,172 @@ class _LeftMenuButton extends StatelessWidget {
}
}
/// Jenkins Manage Jenkins-aligned settings surface: category cards limited
/// to the OTO contract data actually available today (runtime endpoints,
/// browser access policy) instead of a generic empty-state fallback.
class _ManageOtoSettingsSurface extends StatelessWidget {
final OtoConsoleThemeAdapter theme;
final OtoConsoleConfig? config;
const _ManageOtoSettingsSurface({required this.theme, this.config});
@override
Widget build(BuildContext context) {
return Container(
color: theme.backgroundColor,
child: Padding(
padding: const EdgeInsets.all(24),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 820),
child: ListView(
shrinkWrap: true,
children: [
Row(
children: [
Icon(Icons.tune, color: theme.primaryColor, size: 24),
const SizedBox(width: 12),
Expanded(
child: Text(
'Manage OTO',
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: theme.textColor,
fontSize: 24,
fontWeight: FontWeight.w800,
),
),
),
],
),
const SizedBox(height: 20),
_ManageCategoryCard(
icon: Icons.dns_outlined,
title: 'System',
description:
'Runtime endpoints this console connects to.',
theme: theme,
details: [
if (config != null)
OtoConsoleSectionDetail(
label: 'HTTP API',
value: config!.serverHttpUrl,
),
if (config != null)
OtoConsoleSectionDetail(
label: 'Wire URL',
value: config!.serverWireUrl,
),
],
),
const SizedBox(height: 16),
_ManageCategoryCard(
icon: Icons.security_outlined,
title: 'Security',
description: 'Browser access policy enforced for this console.',
theme: theme,
detailGroups: const [
OtoConsoleSectionDetailGroup(
title: 'Browser policy',
details: [
OtoConsoleSectionDetail(
label: 'Browser API policy',
value: 'CORS headers or a same-origin proxy are required.',
),
],
),
],
),
],
),
),
),
),
);
}
}
/// A single Manage OTO category card. Supports either a flat `details` list
/// or nested `detailGroups` so categories can subdivide (e.g. Security ->
/// Browser policy) without a new card type.
class _ManageCategoryCard extends StatelessWidget {
final IconData icon;
final String title;
final String description;
final OtoConsoleThemeAdapter theme;
final List<OtoConsoleSectionDetail> details;
final List<OtoConsoleSectionDetailGroup> detailGroups;
const _ManageCategoryCard({
required this.icon,
required this.title,
required this.description,
required this.theme,
this.details = const [],
this.detailGroups = const [],
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.railBackgroundColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: theme.borderColor),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: theme.primaryColor, size: 20),
const SizedBox(width: 10),
Text(
title,
style: TextStyle(
color: theme.textColor,
fontSize: 16,
fontWeight: FontWeight.w800,
),
),
],
),
const SizedBox(height: 6),
Text(
description,
style: TextStyle(color: theme.textSecondaryColor, fontSize: 12),
),
if (detailGroups.isNotEmpty) ...[
for (final group in detailGroups) ...[
const SizedBox(height: 14),
Text(
group.title,
style: TextStyle(
color: theme.textSecondaryColor,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
for (final detail in group.details) ...[
_SectionDetailTile(detail: detail, theme: theme),
const SizedBox(height: 8),
],
],
] else if (details.isNotEmpty) ...[
const SizedBox(height: 14),
for (final detail in details) ...[
_SectionDetailTile(detail: detail, theme: theme),
const SizedBox(height: 8),
],
],
],
),
);
}
}
class OtoConsoleSectionDetail {
final String label;
final String value;

View file

@ -326,40 +326,53 @@ class OtoExecutionsSurface extends StatelessWidget {
borderRadius: BorderRadius.circular(8),
border: Border.all(color: theme.borderColor),
),
child: Column(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildRunHeader(theme, exec),
const SizedBox(height: 16),
_buildActionsSection(context, theme, exec),
const SizedBox(height: 16),
_buildStageSummaryPlaceholder(theme),
const SizedBox(height: 16),
_DetailPane(
title: 'Logs Preview',
theme: theme,
child: _LogPreviewBox(
theme: theme,
child: _buildLogsContent(theme),
),
),
const SizedBox(height: 16),
_DetailPane(
title: 'Artifacts',
theme: theme,
action: onViewArtifactsSection == null
? null
: TextButton.icon(
onPressed: onViewArtifactsSection,
icon: const Icon(Icons.arrow_forward, size: 14),
label: const Text('View Artifacts Tab'),
style: TextButton.styleFrom(
foregroundColor: theme.primaryColor,
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
),
_RunTaskMenu(theme: theme),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildRunHeader(theme, exec),
const SizedBox(height: 16),
_buildActionsSection(context, theme, exec),
const SizedBox(height: 16),
_DetailPane(
title: 'Pipeline Overview',
theme: theme,
child: _PipelineStageGraph(theme: theme, exec: exec),
),
const SizedBox(height: 16),
_DetailPane(
title: 'Console Output',
theme: theme,
child: _LogPreviewBox(
theme: theme,
child: _buildLogsContent(theme),
),
child: _buildArtifactsContent(theme),
),
const SizedBox(height: 16),
_DetailPane(
title: 'Artifacts',
theme: theme,
action: onViewArtifactsSection == null
? null
: TextButton.icon(
onPressed: onViewArtifactsSection,
icon: const Icon(Icons.arrow_forward, size: 14),
label: const Text('View Artifacts Tab'),
style: TextButton.styleFrom(
foregroundColor: theme.primaryColor,
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
),
),
child: _buildArtifactsContent(theme),
),
],
),
),
],
),
@ -403,34 +416,6 @@ class OtoExecutionsSurface extends StatelessWidget {
);
}
Widget _buildStageSummaryPlaceholder(OtoConsoleThemeAdapter theme) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: theme.backgroundColor,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: theme.borderColor),
),
child: Row(
children: [
Icon(
Icons.account_tree_outlined,
color: theme.textSecondaryColor,
size: 16,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'Stage summary unavailable',
style: TextStyle(color: theme.textSecondaryColor, fontSize: 12),
),
),
],
),
);
}
Widget _buildActionsSection(
BuildContext ctx,
OtoConsoleThemeAdapter theme,
@ -882,14 +867,16 @@ class _DetailPane extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: TextStyle(
color: theme.textColor,
fontSize: 14,
fontWeight: FontWeight.bold,
Expanded(
child: Text(
title,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: theme.textColor,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
?action,
@ -925,6 +912,186 @@ class _LogPreviewBox extends StatelessWidget {
}
}
/// Jenkins build-page-style left task menu for the selected run. Presentational
/// only: OTO has no per-item view (console/artifacts/history all render
/// together below), so entries are static labels rather than tab switches.
class _RunTaskMenu extends StatelessWidget {
final OtoConsoleThemeAdapter theme;
const _RunTaskMenu({required this.theme});
static const _items = [
(Icons.terminal, 'Console Output'),
(Icons.account_tree_outlined, 'Pipeline Overview'),
(Icons.inventory_2_outlined, 'Artifacts'),
(Icons.history, 'Build History'),
];
@override
Widget build(BuildContext context) {
return Container(
width: 168,
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: theme.backgroundColor,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: theme.borderColor),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final (icon, label) in _items)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: Row(
children: [
Icon(icon, color: theme.textSecondaryColor, size: 15),
const SizedBox(width: 8),
Expanded(
child: Text(
label,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: theme.textSecondaryColor,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
],
),
);
}
}
enum _StageStatus { succeeded, running, failed, canceled, skipped, pending }
typedef _StageNode = (String name, _StageStatus status);
/// Deterministic Pipeline Graph View placeholder. OTO Core does not report
/// per-stage telemetry yet, so a fixed `Checkout/Build/Package/Publish`
/// skeleton is colored from the run's overall state instead of leaving the
/// region empty.
class _PipelineStageGraph extends StatelessWidget {
final OtoConsoleThemeAdapter theme;
final OtoExecutionViewModel exec;
const _PipelineStageGraph({required this.theme, required this.exec});
static const _stageNames = ['Checkout', 'Build', 'Package', 'Publish'];
List<_StageNode> get _stages {
switch (exec.state.toLowerCase()) {
case 'succeeded':
return [for (final name in _stageNames) (name, _StageStatus.succeeded)];
case 'failed':
return [
(_stageNames[0], _StageStatus.succeeded),
(_stageNames[1], _StageStatus.failed),
(_stageNames[2], _StageStatus.skipped),
(_stageNames[3], _StageStatus.skipped),
];
case 'running':
return [
(_stageNames[0], _StageStatus.succeeded),
(_stageNames[1], _StageStatus.running),
(_stageNames[2], _StageStatus.pending),
(_stageNames[3], _StageStatus.pending),
];
case 'canceled':
return [
(_stageNames[0], _StageStatus.succeeded),
(_stageNames[1], _StageStatus.canceled),
(_stageNames[2], _StageStatus.skipped),
(_stageNames[3], _StageStatus.skipped),
];
case 'queued':
default:
return [for (final name in _stageNames) (name, _StageStatus.pending)];
}
}
Color _colorFor(_StageStatus status) {
switch (status) {
case _StageStatus.succeeded:
return theme.primaryColor;
case _StageStatus.running:
return theme.accentColor;
case _StageStatus.failed:
return Colors.red;
case _StageStatus.canceled:
return Colors.orange;
case _StageStatus.skipped:
case _StageStatus.pending:
return theme.textSecondaryColor;
}
}
@override
Widget build(BuildContext context) {
final stages = _stages;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Stage Graph',
style: TextStyle(
color: theme.textColor,
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 12,
runSpacing: 8,
children: [
for (final stage in stages) _StageChip(stage: stage, color: _colorFor(stage.$2)),
],
),
const SizedBox(height: 8),
Text(
'Per-stage telemetry is not reported yet; the graph reflects the overall run state.',
style: TextStyle(color: theme.textSecondaryColor, fontSize: 11),
),
],
);
}
}
class _StageChip extends StatelessWidget {
final _StageNode stage;
final Color color;
const _StageChip({required this.stage, required this.color});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: color.withValues(alpha: 0.5)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.circle, color: color, size: 8),
const SizedBox(width: 6),
Text(
stage.$1,
style: TextStyle(color: color, fontSize: 12, fontWeight: FontWeight.w700),
),
],
),
);
}
}
/// Shown in the detail region when no run is selected in the list.
class _RunDetailPlaceholder extends StatelessWidget {
final OtoConsoleThemeAdapter theme;

View file

@ -191,13 +191,22 @@ class _OtoRunnersSurfaceState extends State<OtoRunnersSurface> {
OtoConsoleThemeAdapter theme,
List<OtoRunnerViewModel> runners,
) {
return ListView.separated(
itemCount: runners.length,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final runner = runners[index];
return _buildRunnerRow(context, theme, runner);
},
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_NodesTableHeader(theme: theme),
const SizedBox(height: 8),
Expanded(
child: ListView.separated(
itemCount: runners.length,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final runner = runners[index];
return _buildRunnerRow(context, theme, runner);
},
),
),
],
);
}
@ -219,15 +228,12 @@ class _OtoRunnersSurfaceState extends State<OtoRunnersSurface> {
urlIsHttps &&
widget.onSelfUpdateRunner != null;
return _RecordRow(
title: runner.runnerID,
subtitle: runner.alias,
return _NodeTableRow(
runnerID: runner.runnerID,
alias: runner.alias,
status: _StatusBadge(status: runner.status, theme: theme),
metadata: _RunnerContext(
jobID: runner.currentJobID,
executionID: runner.currentExecutionID,
theme: theme,
),
currentJobID: runner.currentJobID,
currentExecutionID: runner.currentExecutionID,
message: runner.message,
theme: theme,
actionPanel: widget.onSelfUpdateRunner == null
@ -375,20 +381,58 @@ class _OtoRunnersSurfaceState extends State<OtoRunnersSurface> {
}
}
class _RecordRow extends StatelessWidget {
final String title;
final String subtitle;
/// Jenkins Nodes/Agents-style table header for the runners list.
class _NodesTableHeader extends StatelessWidget {
final OtoConsoleThemeAdapter theme;
const _NodesTableHeader({required this.theme});
@override
Widget build(BuildContext context) {
final style = TextStyle(
color: theme.textSecondaryColor,
fontSize: 12,
fontWeight: FontWeight.w800,
);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: theme.railBackgroundColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: theme.borderColor),
),
child: Row(
children: [
Expanded(flex: 3, child: Text('Nodes', style: style)),
Expanded(flex: 2, child: Text('Executor', style: style)),
Expanded(flex: 2, child: Text('Current job', style: style)),
Expanded(flex: 2, child: Text('Current run', style: style)),
Expanded(flex: 2, child: Text('Manage', style: style)),
],
),
);
}
}
/// Jenkins Nodes/Agents-style operational row: node identity, executor
/// status, current job/run context, and a manage affordance above the
/// existing Self Update panel.
class _NodeTableRow extends StatelessWidget {
final String runnerID;
final String alias;
final Widget status;
final Widget? metadata;
final String currentJobID;
final String currentExecutionID;
final String message;
final Widget? actionPanel;
final OtoConsoleThemeAdapter theme;
const _RecordRow({
required this.title,
required this.subtitle,
const _NodeTableRow({
required this.runnerID,
required this.alias,
required this.status,
this.metadata,
required this.currentJobID,
required this.currentExecutionID,
required this.message,
this.actionPanel,
required this.theme,
@ -407,43 +451,66 @@ class _RecordRow extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
'ID: $title',
style: TextStyle(
color: theme.textColor,
fontSize: 16,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
flex: 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'ID: $runnerID',
style: TextStyle(
color: theme.textColor,
fontSize: 16,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
if (alias.isNotEmpty) ...[
const SizedBox(height: 6),
Text(
'Alias: $alias',
style: TextStyle(color: theme.textColor, fontSize: 14),
),
],
],
),
),
Expanded(flex: 2, child: status),
Expanded(
flex: 2,
child: Text(
'Job: ${currentJobID.isNotEmpty ? currentJobID : "None"}',
overflow: TextOverflow.ellipsis,
style: TextStyle(color: theme.textColor, fontSize: 12),
),
),
Expanded(
flex: 2,
child: Text(
'Execution: ${currentExecutionID.isNotEmpty ? currentExecutionID : "None"}',
overflow: TextOverflow.ellipsis,
style: TextStyle(color: theme.textColor, fontSize: 12),
),
),
Expanded(
flex: 2,
child: Icon(
Icons.build_circle_outlined,
color: theme.primaryColor,
size: 18,
),
),
const SizedBox(width: 8),
status,
],
),
if (subtitle.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
'Alias: $subtitle',
style: TextStyle(color: theme.textColor, fontSize: 14),
),
],
if (message.isNotEmpty) ...[
const SizedBox(height: 8),
const SizedBox(height: 10),
Text(
'Message: $message',
style: TextStyle(
color: theme.textSecondaryColor,
fontSize: 14,
),
style: TextStyle(color: theme.textSecondaryColor, fontSize: 14),
),
],
if (metadata != null) ...[
const SizedBox(height: 12),
metadata!,
],
if (actionPanel != null) ...[
const SizedBox(height: 12),
actionPanel!,
@ -454,56 +521,6 @@ class _RecordRow extends StatelessWidget {
}
}
class _RunnerContext extends StatelessWidget {
final String jobID;
final String executionID;
final OtoConsoleThemeAdapter theme;
const _RunnerContext({
required this.jobID,
required this.executionID,
required this.theme,
});
@override
Widget build(BuildContext context) {
if (jobID.isEmpty && executionID.isEmpty) return const SizedBox.shrink();
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: theme.primaryColor.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: theme.primaryColor.withValues(alpha: 0.2),
),
),
child: Row(
children: [
Icon(
Icons.play_circle_outline,
color: theme.primaryColor,
size: 16,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'Job: ${jobID.isNotEmpty ? jobID : "None"} | Execution: ${executionID.isNotEmpty ? executionID : "None"}',
style: TextStyle(
color: theme.textColor,
fontSize: 12,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
}
class _StatusBadge extends StatelessWidget {
final String status;
final OtoConsoleThemeAdapter theme;

View file

@ -117,7 +117,6 @@ void main() {
'Pipelines': 'No pipeline jobs',
'Executions': 'No executions',
'Artifacts': 'No artifacts',
'Settings': 'No local preferences',
};
for (final entry in sections.entries) {
@ -127,9 +126,15 @@ void main() {
expect(find.text(entry.value), findsOneWidget);
}
// Settings is no longer a generic empty-state fallback: it renders the
// Manage OTO category surface (System, Security -> Browser policy).
await tester.tap(find.byTooltip('Settings'));
await tester.pumpAndSettle();
expect(find.text('Manage OTO'), findsOneWidget);
expect(find.text('System'), findsOneWidget);
expect(find.text('Security'), findsOneWidget);
expect(find.text('Browser policy'), findsOneWidget);
expect(find.text('Browser API policy'), findsOneWidget);
expect(find.textContaining('same-origin proxy'), findsOneWidget);
});
@ -204,6 +209,13 @@ void main() {
),
);
// Jenkins Nodes/Agents-style table header.
expect(find.text('Nodes'), findsOneWidget);
expect(find.text('Executor'), findsOneWidget);
expect(find.text('Current job'), findsOneWidget);
expect(find.text('Current run'), findsOneWidget);
expect(find.text('Manage'), findsOneWidget);
expect(find.text('ID: runner-1'), findsOneWidget);
expect(find.text('Alias: Main Runner'), findsOneWidget);
expect(find.text('ACTIVE'), findsOneWidget);
@ -533,12 +545,21 @@ void main() {
),
),
);
// Selected run detail: run header + stage summary placeholder + panes.
// Selected run detail: run header + run-context task menu + stage
// graph + console output/artifacts panes.
expect(find.text('Run exec-1'), findsOneWidget);
expect(find.text('Stage summary unavailable'), findsOneWidget);
expect(find.text('Logs Preview'), findsOneWidget);
// 'Console Output', 'Pipeline Overview', and 'Artifacts' each appear
// once in the run-context task menu and once as a detail pane title.
expect(find.text('Console Output'), findsNWidgets(2));
expect(find.text('Pipeline Overview'), findsNWidgets(2));
expect(find.text('Build History'), findsOneWidget);
expect(find.text('Stage Graph'), findsOneWidget);
expect(find.text('Checkout'), findsOneWidget);
expect(find.text('Build'), findsOneWidget);
expect(find.text('Package'), findsOneWidget);
expect(find.text('Publish'), findsOneWidget);
expect(find.textContaining('Starting build...'), findsOneWidget);
expect(find.text('Artifacts'), findsOneWidget);
expect(find.text('Artifacts'), findsNWidgets(2));
expect(find.text('binary.exe'), findsOneWidget);
},
);