<!-- task=m-architecture-refactor-foundation/08_client_state_split plan=1 tag=REVIEW_REFACTOR -->

# PLAN - REVIEW_REFACTOR dispose-safe client status controller

## 이 파일을 읽는 구현 에이전트에게

구현 완료의 마지막 단계는 `CODE_REVIEW-local-G08.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채우는 것이다. 검증을 실행하고, 출력 원문을 붙이고, active 파일은 그대로 둔 채 리뷰 준비 상태를 보고한다. 종료 처리, 로그 아카이브, `complete.log` 작성, task directory 이동은 code-review skill 전용이다.

구현 중 사용자만 결정할 수 있는 범위 변경, 사용자 소유 외부 환경/secret/서비스 준비, 또는 안전하게 진행할 수 없는 충돌이 생기면 직접 질문하지 말고 `CODE_REVIEW-local-G08.md`의 `사용자 리뷰 요청` 섹션을 증거와 함께 채운 뒤 멈춘다. `request_user_input`, 채팅 선택지 제시, `USER_REVIEW.md` 생성, `complete.log` 작성, active 파일 아카이브는 구현 에이전트가 하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 공백은 사용자 리뷰 요청 사유가 아니다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/architecture-refactor-foundation.md`
- Task ids:
  - `client-state-split`: Flutter client의 bootstrap, status state, DTO, repository, panel widget 경계를 분리하고 Firebase/Mattermost 같은 외부 통합은 선택적으로 초기화되게 한다.
- Completion mode: check-on-pass

## 리뷰 결과 요약

첫 리뷰는 `FAIL`이다. `ControlPlaneStatusController`가 `ClientHomePage.dispose()` 이후에도 진행 중이던 repository Future 완료 결과를 state에 반영하고 `notifyListeners()`를 호출할 수 있다. 이전 widget state 구현은 await 이후 `mounted` guard를 두었으므로, controller 분리 후에도 동일한 lifecycle 안전성이 필요하다.

관련 로그:

- `agent-task/m-architecture-refactor-foundation/08_client_state_split/plan_local_G08_0.log`
- `agent-task/m-architecture-refactor-foundation/08_client_state_split/code_review_cloud_G08_0.log`

## 범위 결정 근거

- 수정 범위는 `apps/client/lib/control_plane_status_controller.dart`와 `apps/client/test/control_plane_status_controller_test.dart`를 우선한다.
- 필요하면 test fixture를 위해 `apps/client/test/widget_test.dart`의 fake repository만 최소 조정한다.
- bootstrap, DTO/repository barrel, panel widget 파일 구조, Control Plane Go 코드는 이 follow-up 범위가 아니다.
- review 중 이미 `apps/client/lib/widgets/execution_logs_panel.dart`, `apps/client/lib/widgets/nodes_panel.dart`의 analyzer-only `unnecessary_const` nit는 정리되어 있다. 이 변경은 유지하되 별도 기능 범위로 확대하지 않는다.

## 빌드 등급

- build lane: `local-G08`
- review lane: `local-G08`
- 이유: Required 이슈가 explicit, deterministic, unit-testable lifecycle bug이므로 넓은 cloud context 없이도 검증 가능하다. 단, client 테스트 규칙상 최종 검증은 원격 runner 또는 동기화된 원격 임시 checkout에서 수행한다.

## 구현 체크리스트

- [ ] `ControlPlaneStatusController`가 dispose 이후 진행 중 fetch 결과를 state에 반영하거나 `notifyListeners()`를 호출하지 않도록 lifecycle guard를 추가한다.
- [ ] `fetchEdges`, `fetchEdgeStatus`, `fetchEdgeEvents`, `fetchEdgeDetails`, `selectEdge`의 success/error path가 disposed state를 안전하게 무시하도록 정리한다.
- [ ] controller unit test에 dispose 중 in-flight future 완료 회귀 테스트를 추가한다.
- [ ] 변경분을 원격 runner 또는 동기화된 원격 임시 checkout에 반영한 뒤 `cd apps/client && flutter test`, `cd apps/client && flutter analyze --no-fatal-infos`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [REVIEW_REFACTOR-1] Dispose-safe status controller lifecycle

### 문제

`ControlPlaneStatusController`는 `ChangeNotifier`이지만 dispose 상태를 추적하지 않는다. `fetchEdges`, `fetchEdgeStatus`, `fetchEdgeEvents`는 await 이후 곧바로 내부 state를 바꾸고 `notifyListeners()`를 호출한다.

Before:

```dart
final status = await statusRepository.fetchEdgeStatus(edgeId);
_edgeStatuses[edgeId] = status;
_loadingStatuses[edgeId] = false;
notifyListeners();
```

`ClientHomePage.dispose()`는 controller를 dispose하므로 화면이 사라진 뒤 Future가 완료되면 disposed notifier가 알림을 보내 debug/test assert 또는 lifecycle 오류를 만들 수 있다.

### 해결 방법

- controller에 private disposed flag를 둔다.
- `dispose()`를 override해 flag를 세운 뒤 `super.dispose()`를 호출한다.
- public mutation entrypoint와 await 이후 continuation에서 disposed 상태면 state mutation과 notify를 중단한다.
- `notifyListeners()` 호출은 helper로 감싸 disposed 상태에서 호출되지 않게 한다.
- dispose 이후 완료된 Future는 성공/실패 모두 state에 반영하지 않는다.

After 방향:

```dart
if (_disposed) return;
final status = await statusRepository.fetchEdgeStatus(edgeId);
if (_disposed) return;
_edgeStatuses[edgeId] = status;
_loadingStatuses[edgeId] = false;
_notifyIfActive();
```

### 수정 파일 및 체크리스트

- [ ] `apps/client/lib/control_plane_status_controller.dart`: disposed guard, safe notify helper, await continuation guard 추가.
- [ ] `apps/client/test/control_plane_status_controller_test.dart`: in-flight fetch가 dispose 후 완료되어도 throw하지 않고 state를 갱신하지 않는 테스트 추가.
- [ ] 필요 시 `apps/client/test/widget_test.dart`: fake repository에 controllable completer fixture를 최소 추가.

### 테스트 작성

- test name 후보: `ignores in-flight fetch completions after dispose`
- fixture: `Completer<FleetStatusResponseView>`, `Completer<EdgeStatusResponseView>`, `Completer<List<EdgeNodeEventView>>`를 사용하는 fake repository.
- assertion:
  - `controller.fetchEdges()` 또는 `controller.fetchEdgeDetails('edge-a')`를 시작한다.
  - controller를 dispose한다.
  - completer를 complete한다.
  - Future가 `completes`이고 disposed `ChangeNotifier` assert가 발생하지 않는다.
  - dispose 이후 완료 결과가 `edges`, `edgeStatuses`, `edgeEvents`, loading/error maps에 새로 반영되지 않는다.

### 검증

원격 runner 또는 동기화된 원격 임시 checkout 기준:

```bash
cd apps/client && flutter test
cd apps/client && flutter analyze --no-fatal-infos
```

검증 출력은 `CODE_REVIEW-local-G08.md`에 원문으로 기록한다.
