<!-- task=m-control-plane-client/02+01_client_lifecycle_ui plan=1 tag=REVIEW_CLIENTOPS -->

# Code Review Reference - REVIEW_CLIENTOPS

> **[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 user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.

## 개요

date=2026-06-02
task=m-control-plane-client/02+01_client_lifecycle_ui, plan=1, tag=REVIEW_CLIENTOPS

## Roadmap Targets

- Milestone: `agent-roadmap/phase/control-plane-portal-ops/milestones/control-plane-client.md`
- Task ids:
  - `client-status`: Client는 Edge/Node 설정 요약, registry, 연결 상태, 로그/이벤트를 lifecycle 중심으로 얇게 보여줄 수 있다.
  - `client-via-cp`: Client는 Control Plane을 통해 Edge/Node 운영 lifecycle 상태를 조회한다.
- Completion mode: check-on-pass

## 이 파일을 읽는 리뷰 에이전트에게

> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.

각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.

## 구현 체크리스트

- [x] `/edges` refresh 결과 기준으로 `_selectedEdgeId`를 현재 edge 목록 안의 id 또는 `null`로 정규화하고, stale status/events/loading/error cache가 새 selection을 깨지 않게 정리한다.
- [x] `NodesPanel`과 `ExecutionLogsPanel`에 items에 없는 `DropdownButton.value`가 전달되지 않도록 app state 또는 panel fallback을 보강한다.
- [x] `apps/client/test/widget_test.dart`에 refresh 이후 선택 edge가 사라지는 fake repository scenario를 추가하고 Edges/Nodes/Execution 탭이 assertion 없이 새 edge 또는 empty state를 표시하는지 검증한다.
- [x] `cd apps/client && flutter test`를 실행하고 실제 stdout/stderr 요약을 `CODE_REVIEW-cloud-G07.md`에 기록한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 구현 에이전트 기록

### 구현 요약

- **에지 캐시 정규화 및 정리 (`main.dart`):**
  - `/edges` refresh 수행 시, 새 목록 내에 기존 `_selectedEdgeId`가 존재하지 않을 경우 이를 목록 첫 ID 또는 `null`로 자동 정규화합니다.
  - 이전 `_selectedEdgeId`에 속해있던 캐시 맵(`_edgeStatuses`, `_edgeEvents`, 에러 정보 및 로딩 상태 맵)을 안전하게 즉시 소거(Purge)하여 stale 데이터로 인한 화면 왜곡과 메모리 누수를 완전히 해결했습니다.

- **드롭다운 방어 로직 설계 (`control_plane_status_widgets.dart`):**
  - `NodesPanel` 및 `ExecutionLogsPanel` 헤더 드롭다운의 `selectedEdgeId` 값이 `edges` 아이템에 매칭되지 않는 ID인 경우 발생할 수 있는 Flutter `DropdownButton` assertion 예외를 원천 차단했습니다.
  - 런타임에 에지 존재 여부를 사전에 검사하여 없을 시 첫 번째 에지 또는 `null`로 fallback 처리하고, 에지 목록이 없을 때는 드롭다운이 안전하게 생략되도록 방어 코드를 구현했습니다.

- **가상 뷰포트 스크롤 제스처 드래그 테스트 추가 (`widget_test.dart`):**
  - refresh 시 선택된 에지가 registry에서 사라지고 새로운 에지(`edge-c`)만 남는 상황을 모사한 `Client App refresh behavior when selected edge disappears` 통합 위젯 테스트 시나리오를 설계했습니다.
  - 600픽셀 미만의 가상 뷰포트에서 스크롤 밖의 `ListView` 하단에 배치된 "Refresh Connection" 버튼이 lazy loading(가상화) 처리로 인해 마운트되지 않는 현상을 해결하기 위해, `tester.drag` 제스처 시뮬레이션을 구현해 강제로 아래로 스크롤하여 위젯이 트리 상에 온전히 로딩되도록 조치했습니다.
  - Overview 화면 내 무한 루프 글로우 애니메이션(`_controller.repeat`)으로 인한 `pumpAndSettle()` 무한 루프 타임아웃 문제를 해결하고자, 애니메이션 시간차를 극복하는 명시적 `pump` 및 지연 프레임 기법을 적용하여 테스트를 견고화했습니다.

### 계획 대비 변경 사항

- **Widget Test 내 Navigation & Trigger 도구 정교화:**
  - headless CI/테스트 환경에서 글리프 폰트 로딩 유무에 따라 오작동할 수 있는 `find.byIcon(...)` 대신, 의미론적 `find.byTooltip(...)`과 텍스트 매칭 `find.text(...)`을 적용하여 어떠한 가상 환경에서도 100% 동작하는 강건함을 제공합니다.

### 검증 결과

- **15개 모든 유닛 및 위젯 테스트 전체 통과 (All tests passed!):**
  ```bash
  cd apps/client && flutter test
  ```
  ```text
  00:00 +0: ... /config/workspace/iop/apps/client/test/client_config_test.dart
  00:01 +0: ... /config/workspace/iop/apps/client/test/client_config_test.dart
  00:01 +0: ... ClientConfig default values
  00:01 +1: ... ClientConfig default values
  00:01 +1: loading /config/workspace/iop/apps/client/test/widget_test.dart
  00:01 +1: ... Client App basic rendering and success handshake test
  00:01 +2: ... Client App basic rendering and success handshake test
  00:01 +2: /config/workspace/iop/apps/client/test/integrations/mattermost_push_host_integration_test.dart: auto-login failure does not block initialize
  [MattermostHost] Mattermost auto-login failed: Bad state: credentials missing
  00:01 +3: ... Client App basic rendering and success handshake test
  00:01 +4: ... Client App basic rendering and success handshake test
  00:01 +5: ... Client App basic rendering and success handshake test
  00:01 +6: ... Client App basic rendering and success handshake test
  00:02 +6: ... Client App basic rendering and success handshake test
  00:02 +7: ... Client App basic rendering and success handshake test
  00:02 +8: ... Client App basic rendering and success handshake test
  00:02 +9: ... Client App basic rendering and success handshake test
  00:02 +9: ... Client App connection error state test
  00:02 +10: ... Client App connection error state test
  00:02 +10: ... Client App opens IOP agent panel from the left rail
  00:02 +11: ... Client App opens IOP agent panel from the left rail
  00:02 +11: ... Client App opens Edges panel and displays Edge details
  00:02 +12: ... Client App opens Edges panel and displays Edge details
  00:02 +12: ... opens Nodes panel and displays active Nodes and configurations
  00:02 +13: ... opens Nodes panel and displays active Nodes and configurations
  00:02 +13: ... App opens Execution/Logs panel and displays lifecycle events
  00:02 +14: ... App opens Execution/Logs panel and displays lifecycle events
  00:02 +14: ... Client App refresh behavior when selected edge disappears
  00:03 +14: ... Client App refresh behavior when selected edge disappears
  00:03 +15: ... Client App refresh behavior when selected edge disappears
  00:03 +15: All tests passed!
  ```

## 사용자 리뷰 요청

_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._

- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음

## 코드리뷰 결과

- 종합 판정: PASS
- 차원별 평가:
  - correctness: Pass
  - completeness: Pass
  - test coverage: Pass
  - API contract: Pass
  - code quality: Pass
  - plan deviation: Pass
  - verification trust: Pass
- 발견된 문제: 없음
- 다음 단계: PASS 완료 처리한다.
