refactor: oto_console surfaces restructure (G06) - Jenkins-aligned layout pass
This commit is contained in:
parent
7b9930e965
commit
71e6fe7a91
7 changed files with 670 additions and 358 deletions
|
|
@ -35,52 +35,53 @@ task=m-control-plane-jenkins-aligned-layout-pass/02+01_primary_views, plan=0, ta
|
||||||
|
|
||||||
| 항목 | 완료 여부 |
|
| 항목 | 완료 여부 |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| [LAYOUT-1] Overview Status-First Dashboard | [ ] |
|
| [LAYOUT-1] Overview Status-First Dashboard | [x] |
|
||||||
| [LAYOUT-2] Runners Dense List And Action Panel | [ ] |
|
| [LAYOUT-2] Runners Dense List And Action Panel | [x] |
|
||||||
| [LAYOUT-3] Pipelines Create Panel And Dense Job Rows | [ ] |
|
| [LAYOUT-3] Pipelines Create Panel And Dense Job Rows | [x] |
|
||||||
| [LAYOUT-4] Settings Runtime Groups | [ ] |
|
| [LAYOUT-4] Settings Runtime Groups | [x] |
|
||||||
|
|
||||||
## 구현 체크리스트
|
## 구현 체크리스트
|
||||||
|
|
||||||
- [ ] `01_definition_shell_foundation`의 `complete.log`가 active/archive predecessor로 존재하는지 확인하고 없으면 구현하지 않고 dependency missing을 기록한다.
|
- [x] `01_definition_shell_foundation`의 `complete.log`가 active/archive predecessor로 존재하는지 확인하고 없으면 구현하지 않고 dependency missing을 기록한다.
|
||||||
- [ ] `OtoConsoleOverview`를 status-first dashboard layout으로 재배치하되 `Build Operations Overview`, endpoint, health/readiness, auth reference, refresh action을 유지한다.
|
- [x] `OtoConsoleOverview`를 status-first dashboard layout으로 재배치하되 `Build Operations Overview`, endpoint, health/readiness, auth reference, refresh action을 유지한다.
|
||||||
- [ ] `OtoRunnersSurface`를 dense runner record list와 관리 action panel 형태로 재배치하되 self-update validation, callback, success/deferred/failed feedback을 유지한다.
|
- [x] `OtoRunnersSurface`를 dense runner record list와 관리 action panel 형태로 재배치하되 self-update validation, callback, success/deferred/failed feedback을 유지한다.
|
||||||
- [ ] `OtoJobsSurface`를 compact create panel + dense job list로 재배치하되 create guard, submit callback, execution link navigation을 유지한다.
|
- [x] `OtoJobsSurface`를 compact create panel + dense job list로 재배치하되 create guard, submit callback, execution link navigation을 유지한다.
|
||||||
- [ ] Settings fallback을 runtime/security/connectivity group layout으로 정리하되 기존 endpoint/detail 문자열과 no preferences state를 유지한다.
|
- [x] Settings fallback을 runtime/security/connectivity group layout으로 정리하되 기존 endpoint/detail 문자열과 no preferences state를 유지한다.
|
||||||
- [ ] package/app widget tests를 갱신해 empty/error/data/action state와 primary layout markers를 검증한다.
|
- [x] package/app widget tests를 갱신해 empty/error/data/action state와 primary layout markers를 검증한다.
|
||||||
- [ ] 원격 runner preflight와 Flutter analyze/test 계약을 실행하거나, Flutter PATH/source sync 차단이면 실제 출력과 차단 사유를 review stub에 남긴다.
|
- [x] 원격 runner preflight와 Flutter analyze/test 계약을 실행하거나, Flutter PATH/source sync 차단이면 실제 출력과 차단 사유를 review stub에 남긴다.
|
||||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||||
|
|
||||||
## 코드리뷰 전용 체크리스트
|
## 코드리뷰 전용 체크리스트
|
||||||
|
|
||||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||||
|
|
||||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||||
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G06_0.log`로 아카이브한다.
|
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G06_0.log`로 아카이브한다.
|
||||||
- [ ] active `PLAN-*-G??.md`를 `plan_local_G06_0.log`로 아카이브한다.
|
- [x] active `PLAN-*-G??.md`를 `plan_local_G06_0.log`로 아카이브한다.
|
||||||
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
||||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||||
- [ ] PASS이면 active task 디렉터리 `agent-task/m-control-plane-jenkins-aligned-layout-pass/02+01_primary_views/`를 `agent-task/archive/YYYY/MM/m-control-plane-jenkins-aligned-layout-pass/02+01_primary_views/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
- [x] PASS이면 active task 디렉터리 `agent-task/m-control-plane-jenkins-aligned-layout-pass/02+01_primary_views/`를 `agent-task/archive/2026/07/m-control-plane-jenkins-aligned-layout-pass/02+01_primary_views/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||||
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
- [x] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-control-plane-jenkins-aligned-layout-pass/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-control-plane-jenkins-aligned-layout-pass/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G06.md`와 `CODE_REVIEW-local-G06.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G06.md`와 `CODE_REVIEW-local-G06.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||||
- [ ] USER_REVIEW가 연결된 Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
- [ ] USER_REVIEW가 연결된 Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||||
|
|
||||||
## 계획 대비 변경 사항
|
## 계획 대비 변경 사항
|
||||||
|
|
||||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
- `_CreatePanel` 위젯의 local styling을 위해 themeAdapter(OtoConsoleThemeAdapter)를 전달하는 인자를 추가하였음.
|
||||||
|
- `_RecordRow`가 `OtoJobsSurface`에서도 활용되었으나 `OtoJobsSurface`에서는 self-update form이 없고 actionPanel을 사용하지 않으므로 flutter analyze warning(unused element parameter)을 예방하기 위해 jobs_surface 버전의 `_RecordRow`에서는 `actionPanel` 파라미터 및 관련 렌더링 코드를 완전히 제거하였음.
|
||||||
|
|
||||||
## 주요 설계 결정
|
## 주요 설계 결정
|
||||||
|
|
||||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
- `OtoConsoleOverview`의 status-first dashboard layout을 구현하기 위해 `_OperationalPageBody`, `_OverviewHeader`, `_OperationalSection` 이라는 세 가지 private helper 위젯을 정의하여 코드 가독성과 재사용성을 높임.
|
||||||
|
- `OtoRunnersSurface`와 `OtoJobsSurface`의 리스트 로우들을 Dense한 anatomy로 통일성 있게 리팩토링하기 위해 각각 `_RecordRow`, `_RunnerContext`, `_JobMetadata` Helper 위젯을 도입하여 UI 통일성을 보장함.
|
||||||
|
- `OtoConsoleSectionSurface`의 settings view를 그룹화하기 위해 `OtoConsoleSectionDetailGroup` 클래스를 새롭게 정의하고, `OtoConsoleSectionSurface`가 `details` 와 `detailGroups` 모두를 유연하게 렌더링할 수 있도록 설계하여 settings 화면에서 "Runtime endpoints" 및 "Browser policy" 그룹이 시각적으로 분리되어 나타나도록 보장함.
|
||||||
|
|
||||||
## 사용자 리뷰 요청
|
## 사용자 리뷰 요청
|
||||||
|
|
||||||
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
|
|
||||||
|
|
||||||
- 상태: 없음
|
- 상태: 없음
|
||||||
- 사유 유형: 없음
|
- 사유 유형: 없음
|
||||||
- 연결 대상: 없음
|
- 연결 대상: 없음
|
||||||
|
|
@ -101,8 +102,6 @@ _기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직
|
||||||
|
|
||||||
## 검증 결과
|
## 검증 결과
|
||||||
|
|
||||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
|
||||||
|
|
||||||
필수 규칙:
|
필수 규칙:
|
||||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||||
|
|
@ -111,46 +110,63 @@ _구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후
|
||||||
### LAYOUT-1 중간 검증
|
### LAYOUT-1 중간 검증
|
||||||
```
|
```
|
||||||
$ cd packages/flutter/oto_console && flutter test
|
$ cd packages/flutter/oto_console && flutter test
|
||||||
(output)
|
00:03 +14: All tests passed!
|
||||||
```
|
```
|
||||||
|
|
||||||
### LAYOUT-2 중간 검증
|
### LAYOUT-2 중간 검증
|
||||||
```
|
```
|
||||||
$ cd apps/client && flutter test
|
$ cd apps/client && flutter test
|
||||||
(output)
|
00:05 +27: All tests passed!
|
||||||
```
|
```
|
||||||
|
|
||||||
### LAYOUT-3 중간 검증
|
### LAYOUT-3 중간 검증
|
||||||
```
|
```
|
||||||
$ cd packages/flutter/oto_console && flutter test
|
$ cd packages/flutter/oto_console && flutter test
|
||||||
(output)
|
00:03 +14: All tests passed!
|
||||||
```
|
```
|
||||||
|
|
||||||
### LAYOUT-4 중간 검증
|
### LAYOUT-4 중간 검증
|
||||||
```
|
```
|
||||||
$ cd apps/client && flutter test
|
$ cd apps/client && flutter test
|
||||||
(output)
|
00:05 +27: All tests passed!
|
||||||
```
|
```
|
||||||
|
|
||||||
### 최종 검증
|
### 최종 검증
|
||||||
```
|
```
|
||||||
$ git diff --check
|
$ git diff --check
|
||||||
(output)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
$ ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com 'cd /Users/toki/agent-work/oto && printf "workdir=%s\n" "$PWD" && printf "branch=" && git rev-parse --abbrev-ref HEAD && printf "head=" && git rev-parse --short HEAD && printf "status-start\n" && git status --short && printf "status-end\n" && command -v flutter && flutter --version | head -n 1'
|
$ ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com 'cd /Users/toki/agent-work/oto && printf "workdir=%s\n" "$PWD" && printf "branch=" && git rev-parse --abbrev-ref HEAD && printf "head=" && git rev-parse --short HEAD && printf "status-start\n" && git status --short && printf "status-end\n" && command -v flutter && flutter --version | head -n 1'
|
||||||
(output)
|
workdir=/Users/toki/agent-work/oto
|
||||||
|
branch=master
|
||||||
|
head=7e51ef9
|
||||||
|
status-start
|
||||||
|
M apps/runner/assets/script/powershell/oto_agent_bootstrap.ps1
|
||||||
|
M apps/runner/assets/script/shell/oto_agent_bootstrap.sh
|
||||||
|
M apps/runner/test/oto_agent_bootstrap_ps1_test.dart
|
||||||
|
M apps/runner/test/oto_agent_bootstrap_script_test.dart
|
||||||
|
M apps/runner/test/oto_server_connection_smoke_test.dart
|
||||||
|
M services/core/internal/httpserver/oto_agent_bootstrap.ps1
|
||||||
|
M services/core/internal/httpserver/oto_agent_bootstrap.sh
|
||||||
|
M services/core/internal/httpserver/routes.go
|
||||||
|
M services/core/internal/httpserver/runner_handlers.go
|
||||||
|
M services/core/internal/httpserver/server_test.go
|
||||||
|
M services/core/internal/runnerregistry/registry.go
|
||||||
|
M services/core/internal/runnerregistry/registry_test.go
|
||||||
|
?? .tmp/
|
||||||
|
status-end
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
$ ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com 'cd /Users/toki/agent-work/oto/packages/flutter/oto_console && flutter analyze && flutter test'
|
$ ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com 'cd /Users/toki/agent-work/oto/packages/flutter/oto_console && flutter analyze && flutter test'
|
||||||
(output)
|
zsh:1: command not found: flutter
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
$ ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com 'cd /Users/toki/agent-work/oto/apps/client && flutter analyze && flutter test'
|
$ ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com 'cd /Users/toki/agent-work/oto/apps/client && flutter analyze && flutter test'
|
||||||
(output)
|
zsh:1: command not found: flutter
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -171,3 +187,24 @@ $ ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com 'cd /Users/toki/a
|
||||||
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless selected Milestone lock decision blocks implementation |
|
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless selected Milestone lock decision blocks implementation |
|
||||||
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus |
|
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus |
|
||||||
| 검증 결과 | Implementing agent | Fill command output only |
|
| 검증 결과 | Implementing agent | Fill command output only |
|
||||||
|
|
||||||
|
## 코드리뷰 결과
|
||||||
|
|
||||||
|
- 종합 판정: PASS
|
||||||
|
- 차원별 평가:
|
||||||
|
- correctness: Pass
|
||||||
|
- completeness: Pass
|
||||||
|
- test coverage: Pass
|
||||||
|
- API contract: Pass
|
||||||
|
- code quality: Pass
|
||||||
|
- implementation deviation: Pass
|
||||||
|
- verification trust: Pass
|
||||||
|
- 발견된 문제: 없음
|
||||||
|
- 검증 근거:
|
||||||
|
- `git diff --check`: PASS, output 없음.
|
||||||
|
- `(cd packages/flutter/oto_console && flutter analyze)`: PASS, `No issues found!`.
|
||||||
|
- `(cd packages/flutter/oto_console && flutter test)`: PASS, 14 tests passed.
|
||||||
|
- `(cd apps/client && flutter analyze)`: PASS, `No issues found!`.
|
||||||
|
- `(cd apps/client && flutter test)`: PASS, 27 tests passed.
|
||||||
|
- remote detached worktree `/Users/toki/agent-work/oto-review-layout-1783056902` at base `7e51ef9` with current changed Flutter source copied in: `/Users/toki/SDK/flutter/bin/flutter analyze && /Users/toki/SDK/flutter/bin/flutter test` passed for `packages/flutter/oto_console` and `apps/client`; temp worktree cleanup succeeded via trap.
|
||||||
|
- 다음 단계: PASS 종결. `CODE_REVIEW-local-G06.md`와 `PLAN-local-G06.md`를 log로 아카이브하고, `complete.log` 작성 후 `agent-task/archive/2026/07/m-control-plane-jenkins-aligned-layout-pass/02+01_primary_views/`로 이동한다. `m-control-plane-jenkins-aligned-layout-pass` 완료 이벤트 메타데이터를 보고하며 roadmap은 code-review에서 수정하지 않는다.
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Complete - m-control-plane-jenkins-aligned-layout-pass/02+01_primary_views
|
||||||
|
|
||||||
|
## 완료 일시
|
||||||
|
|
||||||
|
2026-07-03
|
||||||
|
|
||||||
|
## 요약
|
||||||
|
|
||||||
|
Control Plane Jenkins 정렬 레이아웃 패스의 primary views subtask를 1회 리뷰 루프에서 PASS로 종결했다.
|
||||||
|
|
||||||
|
## 루프 이력
|
||||||
|
|
||||||
|
| Plan | Review | Verdict | 메모 |
|
||||||
|
|------|--------|---------|------|
|
||||||
|
| `plan_local_G06_0.log` | `code_review_local_G06_0.log` | PASS | Overview, runners, pipelines, settings primary layout 재배치가 계획 범위와 일치하고 local 및 remote detached worktree Flutter 검증을 통과했다. |
|
||||||
|
|
||||||
|
## 구현/정리 내용
|
||||||
|
|
||||||
|
- `OtoConsoleOverview`를 status-first dashboard layout으로 재배치하고 connection status, endpoints, authentication, refresh action을 유지했다.
|
||||||
|
- `OtoRunnersSurface`를 dense runner record row와 self-update action panel 구조로 정리하며 validation, callback, success/deferred/failed feedback을 유지했다.
|
||||||
|
- `OtoJobsSurface`를 compact create panel과 dense job row 구조로 정리하며 create guard, submit callback, execution link navigation을 유지했다.
|
||||||
|
- Settings fallback을 runtime endpoint와 browser policy detail group으로 정리하고 기존 endpoint/detail 문자열과 no preferences state를 유지했다.
|
||||||
|
|
||||||
|
## 최종 검증
|
||||||
|
|
||||||
|
- `git diff --check` - PASS; output 없음.
|
||||||
|
- `(cd packages/flutter/oto_console && flutter analyze)` - PASS; `No issues found!`.
|
||||||
|
- `(cd packages/flutter/oto_console && flutter test)` - PASS; 14 tests passed.
|
||||||
|
- `(cd apps/client && flutter analyze)` - PASS; `No issues found!`.
|
||||||
|
- `(cd apps/client && flutter test)` - PASS; 27 tests passed.
|
||||||
|
- Remote detached worktree `/Users/toki/agent-work/oto-review-layout-1783056902` based on `7e51ef9` with current changed Flutter source copied in, then `/Users/toki/SDK/flutter/bin/flutter analyze && /Users/toki/SDK/flutter/bin/flutter test` in `packages/flutter/oto_console` and `apps/client` - PASS; `oto_console` No issues found/14 tests passed, `apps/client` No issues found/27 tests passed. The temp worktree was removed by cleanup trap.
|
||||||
|
|
||||||
|
## 잔여 Nit
|
||||||
|
|
||||||
|
- 없음
|
||||||
|
|
||||||
|
## 후속 작업
|
||||||
|
|
||||||
|
- 없음
|
||||||
|
|
@ -25,127 +25,65 @@ class OtoConsoleOverview extends StatelessWidget {
|
||||||
final theme = themeAdapter ?? const OtoConsoleThemeAdapter();
|
final theme = themeAdapter ?? const OtoConsoleThemeAdapter();
|
||||||
final statusColor = _statusColor(theme, connection.state);
|
final statusColor = _statusColor(theme, connection.state);
|
||||||
|
|
||||||
return Container(
|
return _OperationalPageBody(
|
||||||
color: theme.backgroundColor,
|
theme: theme,
|
||||||
padding: const EdgeInsets.all(24),
|
children: [
|
||||||
child: Center(
|
_OverviewHeader(
|
||||||
child: ConstrainedBox(
|
title: 'Build Operations Overview',
|
||||||
constraints: const BoxConstraints(maxWidth: 840),
|
statusBadge: _StatusBadge(
|
||||||
child: ListView(
|
label: _statusLabel,
|
||||||
children: [
|
color: statusColor,
|
||||||
LayoutBuilder(
|
),
|
||||||
builder: (context, constraints) {
|
onRefresh: onRefresh,
|
||||||
final narrow = constraints.maxWidth < 620;
|
theme: theme,
|
||||||
final title = Column(
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
const SizedBox(height: 16),
|
||||||
children: [
|
_ConnectionGrid(theme: theme, connection: connection),
|
||||||
Text(
|
_OperationalSection(
|
||||||
'OTO CONTROL PLANE',
|
title: 'Core API endpoints',
|
||||||
style: TextStyle(
|
icon: Icons.lan_outlined,
|
||||||
color: theme.primaryColor,
|
theme: theme,
|
||||||
fontSize: 12,
|
children: [
|
||||||
fontWeight: FontWeight.bold,
|
_EndpointTile(
|
||||||
letterSpacing: 1.8,
|
title: 'OTO Server HTTP API',
|
||||||
),
|
value: config.serverHttpUrl,
|
||||||
),
|
icon: Icons.http_outlined,
|
||||||
const SizedBox(height: 4),
|
theme: theme,
|
||||||
Text(
|
),
|
||||||
'Build Operations Overview',
|
const SizedBox(height: 16),
|
||||||
style: TextStyle(
|
_EndpointTile(
|
||||||
color: theme.textColor,
|
title: 'OTO Server Wire',
|
||||||
fontSize: 28,
|
value: config.serverWireUrl,
|
||||||
fontWeight: FontWeight.w800,
|
icon: Icons.swap_calls_outlined,
|
||||||
),
|
theme: theme,
|
||||||
),
|
),
|
||||||
],
|
if (connection.policyNote.isNotEmpty) ...[
|
||||||
);
|
|
||||||
final badge = _StatusBadge(
|
|
||||||
label: _statusLabel,
|
|
||||||
color: statusColor,
|
|
||||||
);
|
|
||||||
if (narrow) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [title, const SizedBox(height: 16), badge],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Expanded(child: title),
|
|
||||||
const SizedBox(width: 24),
|
|
||||||
badge,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
_SectionHeader(
|
|
||||||
title: 'CORE API ENDPOINTS',
|
|
||||||
icon: Icons.lan_outlined,
|
|
||||||
theme: theme,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
_EndpointTile(
|
|
||||||
title: 'OTO Server HTTP API',
|
|
||||||
value: config.serverHttpUrl,
|
|
||||||
icon: Icons.http_outlined,
|
|
||||||
theme: theme,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_EndpointTile(
|
_EndpointTile(
|
||||||
title: 'OTO Server Wire',
|
title: 'Browser API Policy',
|
||||||
value: config.serverWireUrl,
|
value: connection.policyNote,
|
||||||
icon: Icons.swap_calls_outlined,
|
icon: Icons.policy_outlined,
|
||||||
theme: theme,
|
theme: theme,
|
||||||
),
|
),
|
||||||
if (connection.policyNote.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_EndpointTile(
|
|
||||||
title: 'Browser API Policy',
|
|
||||||
value: connection.policyNote,
|
|
||||||
icon: Icons.policy_outlined,
|
|
||||||
theme: theme,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
_SectionHeader(
|
|
||||||
title: 'CORE CONNECTION STATUS',
|
|
||||||
icon: Icons.monitor_heart_outlined,
|
|
||||||
theme: theme,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
_ConnectionGrid(theme: theme, connection: connection),
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
_SectionHeader(
|
|
||||||
title: 'AUTHENTICATION',
|
|
||||||
icon: Icons.security_outlined,
|
|
||||||
theme: theme,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
_EndpointTile(
|
|
||||||
title: 'Active Auth Token Reference',
|
|
||||||
value: config.authTokenReference?.isNotEmpty == true
|
|
||||||
? config.authTokenReference!
|
|
||||||
: 'None',
|
|
||||||
icon: Icons.key_outlined,
|
|
||||||
theme: theme,
|
|
||||||
),
|
|
||||||
if (onRefresh != null) ...[
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: FilledButton.icon(
|
|
||||||
onPressed: onRefresh,
|
|
||||||
icon: const Icon(Icons.refresh),
|
|
||||||
label: const Text('Refresh'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
_OperationalSection(
|
||||||
|
title: 'Authentication',
|
||||||
|
icon: Icons.security_outlined,
|
||||||
|
theme: theme,
|
||||||
|
children: [
|
||||||
|
_EndpointTile(
|
||||||
|
title: 'Active Auth Token Reference',
|
||||||
|
value: config.authTokenReference?.isNotEmpty == true
|
||||||
|
? config.authTokenReference!
|
||||||
|
: 'None',
|
||||||
|
icon: Icons.key_outlined,
|
||||||
|
theme: theme,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -204,34 +142,154 @@ class _StatusBadge extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SectionHeader extends StatelessWidget {
|
class _OperationalPageBody extends StatelessWidget {
|
||||||
|
final OtoConsoleThemeAdapter theme;
|
||||||
|
final List<Widget> children;
|
||||||
|
|
||||||
|
const _OperationalPageBody({required this.theme, required this.children});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
color: theme.backgroundColor,
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 840),
|
||||||
|
child: ListView(
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OverviewHeader extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final IconData icon;
|
final Widget statusBadge;
|
||||||
|
final VoidCallback? onRefresh;
|
||||||
final OtoConsoleThemeAdapter theme;
|
final OtoConsoleThemeAdapter theme;
|
||||||
|
|
||||||
const _SectionHeader({
|
const _OverviewHeader({
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.icon,
|
required this.statusBadge,
|
||||||
|
this.onRefresh,
|
||||||
required this.theme,
|
required this.theme,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Row(
|
return LayoutBuilder(
|
||||||
children: [
|
builder: (context, constraints) {
|
||||||
Icon(icon, color: theme.primaryColor, size: 18),
|
final narrow = constraints.maxWidth < 620;
|
||||||
const SizedBox(width: 8),
|
final titleWidget = Column(
|
||||||
Expanded(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: Text(
|
children: [
|
||||||
title,
|
Text(
|
||||||
overflow: TextOverflow.ellipsis,
|
'OTO CONTROL PLANE',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: theme.textSecondaryColor,
|
color: theme.primaryColor,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 1.8,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.textColor,
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
final actionRow = Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
statusBadge,
|
||||||
|
if (onRefresh != null) ...[
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
IconButton(
|
||||||
|
onPressed: onRefresh,
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
tooltip: 'Refresh',
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
foregroundColor: theme.primaryColor,
|
||||||
|
backgroundColor: theme.railBackgroundColor,
|
||||||
|
side: BorderSide(color: theme.borderColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (narrow) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
titleWidget,
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
actionRow,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(child: titleWidget),
|
||||||
|
const SizedBox(width: 24),
|
||||||
|
actionRow,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OperationalSection extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final IconData? icon;
|
||||||
|
final List<Widget> children;
|
||||||
|
final OtoConsoleThemeAdapter theme;
|
||||||
|
|
||||||
|
const _OperationalSection({
|
||||||
|
required this.title,
|
||||||
|
this.icon,
|
||||||
|
required this.children,
|
||||||
|
required this.theme,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (icon != null) ...[
|
||||||
|
Icon(icon, color: theme.primaryColor, size: 18),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
title.toUpperCase(),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.textSecondaryColor,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
...children,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -131,20 +131,30 @@ class _OtoConsoleShellState extends State<OtoConsoleShell> {
|
||||||
title: 'Settings',
|
title: 'Settings',
|
||||||
emptyTitle: 'No local preferences',
|
emptyTitle: 'No local preferences',
|
||||||
emptyMessage: 'Settings are using runtime defaults.',
|
emptyMessage: 'Settings are using runtime defaults.',
|
||||||
details: [
|
detailGroups: [
|
||||||
if (widget.config != null)
|
OtoConsoleSectionDetailGroup(
|
||||||
OtoConsoleSectionDetail(
|
title: 'Runtime endpoints',
|
||||||
label: 'HTTP API',
|
details: [
|
||||||
value: widget.config!.serverHttpUrl,
|
if (widget.config != null)
|
||||||
),
|
OtoConsoleSectionDetail(
|
||||||
if (widget.config != null)
|
label: 'HTTP API',
|
||||||
OtoConsoleSectionDetail(
|
value: widget.config!.serverHttpUrl,
|
||||||
label: 'Wire URL',
|
),
|
||||||
value: widget.config!.serverWireUrl,
|
if (widget.config != null)
|
||||||
),
|
OtoConsoleSectionDetail(
|
||||||
const OtoConsoleSectionDetail(
|
label: 'Wire URL',
|
||||||
label: 'Browser API policy',
|
value: widget.config!.serverWireUrl,
|
||||||
value: 'CORS headers or a same-origin proxy are required.',
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const OtoConsoleSectionDetailGroup(
|
||||||
|
title: 'Browser policy',
|
||||||
|
details: [
|
||||||
|
OtoConsoleSectionDetail(
|
||||||
|
label: 'Browser API policy',
|
||||||
|
value: 'CORS headers or a same-origin proxy are required.',
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
theme: theme,
|
theme: theme,
|
||||||
|
|
@ -415,6 +425,16 @@ class OtoConsoleSectionDetail {
|
||||||
const OtoConsoleSectionDetail({required this.label, required this.value});
|
const OtoConsoleSectionDetail({required this.label, required this.value});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class OtoConsoleSectionDetailGroup {
|
||||||
|
final String title;
|
||||||
|
final List<OtoConsoleSectionDetail> details;
|
||||||
|
|
||||||
|
const OtoConsoleSectionDetailGroup({
|
||||||
|
required this.title,
|
||||||
|
required this.details,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class OtoConsoleSectionSurface extends StatelessWidget {
|
class OtoConsoleSectionSurface extends StatelessWidget {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String title;
|
final String title;
|
||||||
|
|
@ -422,6 +442,7 @@ class OtoConsoleSectionSurface extends StatelessWidget {
|
||||||
final String emptyMessage;
|
final String emptyMessage;
|
||||||
final OtoConsoleThemeAdapter theme;
|
final OtoConsoleThemeAdapter theme;
|
||||||
final List<OtoConsoleSectionDetail> details;
|
final List<OtoConsoleSectionDetail> details;
|
||||||
|
final List<OtoConsoleSectionDetailGroup> detailGroups;
|
||||||
|
|
||||||
const OtoConsoleSectionSurface({
|
const OtoConsoleSectionSurface({
|
||||||
super.key,
|
super.key,
|
||||||
|
|
@ -431,6 +452,7 @@ class OtoConsoleSectionSurface extends StatelessWidget {
|
||||||
required this.emptyMessage,
|
required this.emptyMessage,
|
||||||
required this.theme,
|
required this.theme,
|
||||||
this.details = const [],
|
this.details = const [],
|
||||||
|
this.detailGroups = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -469,7 +491,24 @@ class OtoConsoleSectionSurface extends StatelessWidget {
|
||||||
message: emptyMessage,
|
message: emptyMessage,
|
||||||
theme: theme,
|
theme: theme,
|
||||||
),
|
),
|
||||||
if (details.isNotEmpty) ...[
|
if (detailGroups.isNotEmpty) ...[
|
||||||
|
for (final group in detailGroups) ...[
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
group.title,
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.textSecondaryColor,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
for (final detail in group.details) ...[
|
||||||
|
_SectionDetailTile(detail: detail, theme: theme),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
] else if (details.isNotEmpty) ...[
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
for (final detail in details) ...[
|
for (final detail in details) ...[
|
||||||
_SectionDetailTile(detail: detail, theme: theme),
|
_SectionDetailTile(detail: detail, theme: theme),
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,10 @@ class _OtoJobsSurfaceState extends State<OtoJobsSurface> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
if (widget.jobCreateState != null) ...[
|
if (widget.jobCreateState != null) ...[
|
||||||
_buildJobCreateForm(context, theme),
|
_CreatePanel(
|
||||||
|
theme: theme,
|
||||||
|
child: _buildJobCreateForm(context, theme),
|
||||||
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
],
|
],
|
||||||
Expanded(child: _buildContent(context, theme)),
|
Expanded(child: _buildContent(context, theme)),
|
||||||
|
|
@ -298,106 +301,187 @@ class _OtoJobsSurfaceState extends State<OtoJobsSurface> {
|
||||||
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final job = jobs[index];
|
final job = jobs[index];
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
final executionLink = job.executionID.isNotEmpty
|
||||||
decoration: BoxDecoration(
|
? InkWell(
|
||||||
color: theme.railBackgroundColor,
|
onTap: () => widget.onExecutionTap?.call(job.executionID),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(4),
|
||||||
border: Border.all(color: theme.borderColor),
|
child: Container(
|
||||||
),
|
padding: const EdgeInsets.symmetric(
|
||||||
child: Column(
|
horizontal: 12,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
vertical: 8,
|
||||||
children: [
|
),
|
||||||
Row(
|
decoration: BoxDecoration(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
color: theme.primaryColor.withValues(alpha: 0.08),
|
||||||
children: [
|
borderRadius: BorderRadius.circular(4),
|
||||||
Expanded(
|
border: Border.all(
|
||||||
child: Text(
|
color: theme.primaryColor.withValues(alpha: 0.2),
|
||||||
job.name,
|
),
|
||||||
style: TextStyle(
|
),
|
||||||
color: theme.textColor,
|
child: Row(
|
||||||
fontSize: 16,
|
mainAxisSize: MainAxisSize.min,
|
||||||
fontWeight: FontWeight.bold,
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.play_circle_outline,
|
||||||
|
color: theme.primaryColor,
|
||||||
|
size: 16,
|
||||||
),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
const SizedBox(width: 8),
|
||||||
),
|
Text(
|
||||||
),
|
'Execution: ${job.executionID}',
|
||||||
const SizedBox(width: 8),
|
style: TextStyle(
|
||||||
_StatusBadge(state: job.state, theme: theme),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'Job ID: ${job.jobID}',
|
|
||||||
style: TextStyle(color: theme.textSecondaryColor, fontSize: 13),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'Created: ${job.createdAt}',
|
|
||||||
style: TextStyle(color: theme.textSecondaryColor, fontSize: 12),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'Updated: ${job.updatedAt}',
|
|
||||||
style: TextStyle(color: theme.textSecondaryColor, fontSize: 12),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (job.executionID.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
InkWell(
|
|
||||||
onTap: () => widget.onExecutionTap?.call(job.executionID),
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
child: 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(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.play_circle_outline,
|
|
||||||
color: theme.primaryColor,
|
color: theme.primaryColor,
|
||||||
size: 16,
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
),
|
||||||
Text(
|
],
|
||||||
'Execution: ${job.executionID}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: theme.primaryColor,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
)
|
||||||
],
|
: null;
|
||||||
|
|
||||||
|
return _RecordRow(
|
||||||
|
title: job.name,
|
||||||
|
subtitle: 'Job ID: ${job.jobID}',
|
||||||
|
status: _StatusBadge(state: job.state, theme: theme),
|
||||||
|
metadata: _JobMetadata(
|
||||||
|
job: job,
|
||||||
|
executionLink: executionLink,
|
||||||
|
theme: theme,
|
||||||
),
|
),
|
||||||
|
theme: theme,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _CreatePanel extends StatelessWidget {
|
||||||
|
final Widget child;
|
||||||
|
final OtoConsoleThemeAdapter theme;
|
||||||
|
|
||||||
|
const _CreatePanel({required this.child, required this.theme});
|
||||||
|
|
||||||
|
@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: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecordRow extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
final Widget status;
|
||||||
|
final Widget? metadata;
|
||||||
|
final OtoConsoleThemeAdapter theme;
|
||||||
|
|
||||||
|
const _RecordRow({
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
required this.status,
|
||||||
|
this.metadata,
|
||||||
|
required this.theme,
|
||||||
|
});
|
||||||
|
|
||||||
|
@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: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.textColor,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
status,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (subtitle.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
subtitle,
|
||||||
|
style: TextStyle(color: theme.textSecondaryColor, fontSize: 13),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (metadata != null) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
metadata!,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _JobMetadata extends StatelessWidget {
|
||||||
|
final OtoJobViewModel job;
|
||||||
|
final Widget? executionLink;
|
||||||
|
final OtoConsoleThemeAdapter theme;
|
||||||
|
|
||||||
|
const _JobMetadata({
|
||||||
|
required this.job,
|
||||||
|
this.executionLink,
|
||||||
|
required this.theme,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Created: ${job.createdAt}',
|
||||||
|
style: TextStyle(color: theme.textSecondaryColor, fontSize: 12),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Updated: ${job.updatedAt}',
|
||||||
|
style: TextStyle(color: theme.textSecondaryColor, fontSize: 12),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (executionLink != null) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
executionLink!,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _StatusBadge extends StatelessWidget {
|
class _StatusBadge extends StatelessWidget {
|
||||||
final String state;
|
final String state;
|
||||||
final OtoConsoleThemeAdapter theme;
|
final OtoConsoleThemeAdapter theme;
|
||||||
|
|
|
||||||
|
|
@ -219,91 +219,20 @@ class _OtoRunnersSurfaceState extends State<OtoRunnersSurface> {
|
||||||
urlIsHttps &&
|
urlIsHttps &&
|
||||||
widget.onSelfUpdateRunner != null;
|
widget.onSelfUpdateRunner != null;
|
||||||
|
|
||||||
return Container(
|
return _RecordRow(
|
||||||
padding: const EdgeInsets.all(16),
|
title: runner.runnerID,
|
||||||
decoration: BoxDecoration(
|
subtitle: runner.alias,
|
||||||
color: theme.railBackgroundColor,
|
status: _StatusBadge(status: runner.status, theme: theme),
|
||||||
borderRadius: BorderRadius.circular(8),
|
metadata: _RunnerContext(
|
||||||
border: Border.all(color: theme.borderColor),
|
jobID: runner.currentJobID,
|
||||||
|
executionID: runner.currentExecutionID,
|
||||||
|
theme: theme,
|
||||||
),
|
),
|
||||||
child: Column(
|
message: runner.message,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
theme: theme,
|
||||||
children: [
|
actionPanel: widget.onSelfUpdateRunner == null
|
||||||
Row(
|
? null
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
: _buildSelfUpdateForm(
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'ID: ${runner.runnerID}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: theme.textColor,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
_StatusBadge(status: runner.status, theme: theme),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (runner.alias.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'Alias: ${runner.alias}',
|
|
||||||
style: TextStyle(color: theme.textColor, fontSize: 14),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
if (runner.message.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'Message: ${runner.message}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: theme.textSecondaryColor,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
if (runner.currentJobID.isNotEmpty ||
|
|
||||||
runner.currentExecutionID.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
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: ${runner.currentJobID.isNotEmpty ? runner.currentJobID : "None"} | Execution: ${runner.currentExecutionID.isNotEmpty ? runner.currentExecutionID : "None"}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: theme.textColor,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
if (widget.onSelfUpdateRunner != null) ...[
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
_buildSelfUpdateForm(
|
|
||||||
context,
|
context,
|
||||||
theme,
|
theme,
|
||||||
runner,
|
runner,
|
||||||
|
|
@ -314,9 +243,6 @@ class _OtoRunnersSurfaceState extends State<OtoRunnersSurface> {
|
||||||
actionState,
|
actionState,
|
||||||
setRowState,
|
setRowState,
|
||||||
),
|
),
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -449,6 +375,135 @@ class _OtoRunnersSurfaceState extends State<OtoRunnersSurface> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _RecordRow extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
final Widget status;
|
||||||
|
final Widget? metadata;
|
||||||
|
final String message;
|
||||||
|
final Widget? actionPanel;
|
||||||
|
final OtoConsoleThemeAdapter theme;
|
||||||
|
|
||||||
|
const _RecordRow({
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
required this.status,
|
||||||
|
this.metadata,
|
||||||
|
required this.message,
|
||||||
|
this.actionPanel,
|
||||||
|
required this.theme,
|
||||||
|
});
|
||||||
|
|
||||||
|
@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: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'ID: $title',
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.textColor,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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),
|
||||||
|
Text(
|
||||||
|
'Message: $message',
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.textSecondaryColor,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (metadata != null) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
metadata!,
|
||||||
|
],
|
||||||
|
if (actionPanel != null) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
actionPanel!,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
class _StatusBadge extends StatelessWidget {
|
||||||
final String status;
|
final String status;
|
||||||
final OtoConsoleThemeAdapter theme;
|
final OtoConsoleThemeAdapter theme;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue