<!-- task=m-provider-catalog-device-status/04+03_client_catalog_view plan=0 tag=API -->

# PLAN API - Client Provider Catalog View

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

이 plan은 `04+03_client_catalog_view` dependent split task다. 같은 task group의 `03_catalog_status_http`가 `complete.log`를 만들기 전에는 구현을 시작하지 않는다. 구현이 끝나도 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성까지 끝나야 완료다. 검증 명령의 실제 stdout/stderr를 review stub에 붙이고 active 파일을 그대로 둔 뒤 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review 스킬 전용이다.

구현 중 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 막으면 review stub의 `사용자 리뷰 요청` 섹션에 연결 대상, 차단 근거, 실행한 명령, 재개 조건을 기록하고 멈춘다. 직접 사용자에게 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경, secret, service 준비, 일반 범위 변경, 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과 또는 후속 plan 대상으로 기록한다.

## 배경

Control Plane Client는 `/edges/{id}/status` JSON을 소비하지만 `EdgeNodeSnapshotView`가 provider snapshots를 파싱하지 않는다. Nodes panel도 node config adapters만 보여 주므로 운영자가 provider category, status, load, served models, lifecycle capability를 Client에서 확인할 수 없다. 03번 plan이 Control Plane HTTP JSON 계약을 닫으면, 이 plan은 Client DTO와 Nodes view에서 같은 read-only catalog/status를 표시한다.

## 사용자 리뷰 요청 흐름

사용자 리뷰 요청은 선택된 SDD 결정 또는 선택된 Milestone lock decision만 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 중 직접 사용자 프롬프트는 금지하며, code-review가 정당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
- Task ids:
  - `ops-review`: Edge config와 Edge-owned read-only catalog/status를 기준으로 `iop-edge` CLI, Control Plane, Client가 어떤 정보를 소비하는지 확인할 수 있다.
- Completion mode: check-on-pass

## Spec Targets

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/SDD.md`
- Acceptance scenarios:
  - `S10`: task=`ops-review`; evidence=`Edge CLI configured-provider summary, Control Plane provider status JSON, Client Nodes panel provider catalog/status rendering, and resolved user_review_0.log are present.`
- Completion mode: spec-check-on-pass

## 분석 결과

### 읽은 파일

- `AGENTS.md`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/update-roadmap/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`
- `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
- `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/SDD.md`
- `agent-ops/rules/project/domain/client/rules.md`
- `agent-ops/rules/project/domain/control-plane/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/client-smoke.md`
- `agent-test/local/control-plane-smoke.md`
- `apps/client/lib/control_plane_status_client.dart`
- `apps/client/lib/control_plane_status_dto.dart`
- `apps/client/lib/control_plane_status_repository.dart`
- `apps/client/lib/widgets/nodes_panel.dart`
- `apps/client/lib/widgets/edges_panel.dart`
- `apps/client/test/widget_test.dart`
- `apps/client/test/control_plane_status_controller_test.dart`
- `apps/control-plane/cmd/control-plane/http_views.go`
- `proto/iop/control.proto`
- `proto/iop/runtime.proto`

### 테스트 환경 규칙

- test_env: `local`.
- `agent-test/local/rules.md`를 읽었다. Flutter client 포함 검증은 원격 runner 또는 동기화된 Flutter 환경 기준이며, 현재 checkout의 변경분이 원격에 없으면 원격 검증 완료로 보지 않는다고 확인했다.
- 매칭 profile: `agent-test/local/client-smoke.md`를 읽었다. 필수 명령은 `cd apps/client && flutter test`다.
- 관련 upstream profile: `agent-test/local/control-plane-smoke.md`를 읽었다. 이 plan은 Control Plane 코드를 수정하지 않으며 03번 plan의 complete.log를 선행 조건으로 둔다.
- Docker compose, web dev server, analyzer는 이번 DTO/widget 범위의 필수 검증이 아니다. Flutter analyzer 영향이 확인되면 구현 에이전트가 `cd apps/client && flutter analyze --no-fatal-infos`를 추가 실행하고 review stub에 기록한다.

### 테스트 커버리지 공백

- `EdgeNodeSnapshotView.fromJson`은 `provider_snapshots`를 파싱하지 않는다. HTTP repository가 JSON을 받아도 DTO에서 버린다.
- `NodesPanel`은 node config adapters만 표시한다. provider status, category, served models, load ratio, lifecycle capabilities가 UI에 없다.
- `FakeControlPlaneStatusRepository.fetchEdgeStatus` fixture와 `Client App opens Nodes panel...` widget test는 provider catalog/status가 표시되어야 한다는 회귀 조건을 갖고 있지 않다.

### 심볼 참조

- renamed/removed symbols: none.
- 새 DTO 후보: `ProviderSnapshotView`.
- 새 field 후보: `EdgeNodeSnapshotView.providerSnapshots`.
- 변경 call sites: `EdgeNodeSnapshotView` constructor usages in `apps/client/test/widget_test.dart` and `apps/client/test/control_plane_status_controller_test.dart` may need `providerSnapshots: []` only if constructor is kept `required`; prefer optional default list to minimize call-site churn.

### 분할 판단

- split decision policy를 평가했다.
- 공유 task group: `agent-task/m-provider-catalog-device-status/`.
- predecessor: `03_catalog_status_http` - 현재 active plan으로 존재하며 complete.log는 아직 없음.
- current task: `04+03_client_catalog_view` - `03` complete.log가 필요하다.
- 분할 이유: Client DTO/UI는 Control Plane HTTP JSON shape에 의존한다. Go API 변환과 Flutter widget 검증은 서로 다른 ownership/test runner를 가지므로 별도 review unit으로 둔다.

### 범위 결정 근거

- 포함: `apps/client/lib/control_plane_status_dto.dart`, `apps/client/lib/widgets/nodes_panel.dart`, `apps/client/test/widget_test.dart`. 필요 시 DTO parse 단위 test를 기존 client test 파일에 추가할 수 있다.
- 제외: `apps/control-plane/**`. 03번 plan의 PASS가 선행 조건이다.
- 제외: generated Dart proto. Client는 Control Plane HTTP JSON DTO를 소비하므로 proto binding 수정이 아니다.
- 제외: Edge CLI. 작은 직접 작업으로 `iop-edge nodes list` configured provider summary가 추가되어 있다.
- 제외: runtime enable/disable, drain, fallback 우선순위, capacity override. SDD D06과 Milestone 범위 제외에 따라 후속 범위다.

### 빌드 등급

- lane/grade: `local-G05`.
- 근거: Flutter DTO와 widget 표시 변경이며 UI test로 검증 가능하다. 다만 Client runner/Flutter SDK 상태와 layout 회귀 위험이 있어 G04보다 한 단계 높였다.

## 구현 체크리스트

- [ ] 선행 task `agent-task/m-provider-catalog-device-status/03_catalog_status_http/complete.log` 또는 matching archive complete.log가 존재하는지 확인하고, 없으면 구현하지 말고 dependency missing으로 기록한다.
- [ ] Client HTTP DTO에 `ProviderSnapshotView`와 `EdgeNodeSnapshotView.providerSnapshots` parsing을 추가한다.
- [ ] Nodes panel에 read-only Provider Catalog/Status 섹션을 추가해 provider id/type/category, health/status, capacity, in-flight, queued, load ratio, served models, lifecycle capabilities를 표시한다.
- [ ] Fake status repository와 widget test fixture에 provider snapshot을 추가하고 Nodes panel test가 provider catalog/status 표시를 검증하도록 보강한다.
- [ ] `cd apps/client && flutter test`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- 이 subtask dir 이름 `04+03_client_catalog_view`는 predecessor index `03`만 요구한다.
- 구현 시작 전 다음 중 하나를 확인한다.
  - active: `agent-task/m-provider-catalog-device-status/03_catalog_status_http/complete.log`
  - archive: `agent-task/archive/*/*/m-provider-catalog-device-status/03_catalog_status_http/complete.log`
- 현재 계획 작성 시점에는 predecessor complete.log가 없다. 구현 에이전트는 03 PASS 이후 진행한다.

### [API-1] Client Provider Snapshot DTO 추가

#### 문제

[apps/client/lib/control_plane_status_dto.dart](/config/workspace/iop/apps/client/lib/control_plane_status_dto.dart:65)의 `EdgeNodeSnapshotView`는 provider snapshots field가 없다. [apps/client/lib/control_plane_status_dto.dart](/config/workspace/iop/apps/client/lib/control_plane_status_dto.dart:80)의 `fromJson`도 `provider_snapshots`를 읽지 않는다.

현재 코드:

```dart
65 class EdgeNodeSnapshotView {
66   final String nodeId;
67   final String alias;
68   final String label;
69   final bool connected;
70   final NodeConfigSummaryView? config;
...
80   factory EdgeNodeSnapshotView.fromJson(Map<String, dynamic> json) {
81     return EdgeNodeSnapshotView(
82       nodeId: json['node_id'] as String? ?? '',
83       alias: json['alias'] as String? ?? '',
84       label: json['label'] as String? ?? '',
85       connected: json['connected'] as bool? ?? false,
86       config: json['config'] != null
87           ? NodeConfigSummaryView.fromJson(json['config'] as Map<String, dynamic>)
88           : null,
89     );
90   }
91 }
```

#### 해결 방법

`ProviderSnapshotView` DTO를 추가한다. Numeric JSON은 Go에서 `int`/`float`로 올 수 있으므로 `capacity`, `in_flight`, `queued`는 `as num?` 후 `toInt()`, `load_ratio`는 `toDouble()`로 파싱한다. `EdgeNodeSnapshotView` 생성자에는 `List<ProviderSnapshotView> providerSnapshots = const []` default를 둬 기존 fixture churn을 줄인다.

목표 코드:

```dart
class ProviderSnapshotView {
  final String id;
  final String adapter;
  final String type;
  final String category;
  final String status;
  final String health;
  final int capacity;
  final int inFlight;
  final int queued;
  final double loadRatio;
  final List<String> servedModels;
  final List<String> lifecycleCapabilities;

  ProviderSnapshotView({
    required this.id,
    required this.adapter,
    required this.type,
    required this.category,
    required this.status,
    required this.health,
    required this.capacity,
    required this.inFlight,
    required this.queued,
    required this.loadRatio,
    required this.servedModels,
    required this.lifecycleCapabilities,
  });

  factory ProviderSnapshotView.fromJson(Map<String, dynamic> json) {
    return ProviderSnapshotView(
      id: json['id'] as String? ?? '',
      adapter: json['adapter'] as String? ?? '',
      type: json['type'] as String? ?? '',
      category: json['category'] as String? ?? '',
      status: json['status'] as String? ?? '',
      health: json['health'] as String? ?? '',
      capacity: (json['capacity'] as num?)?.toInt() ?? 0,
      inFlight: (json['in_flight'] as num?)?.toInt() ?? 0,
      queued: (json['queued'] as num?)?.toInt() ?? 0,
      loadRatio: (json['load_ratio'] as num?)?.toDouble() ?? 0,
      servedModels: List<String>.from(json['served_models'] ?? []),
      lifecycleCapabilities: List<String>.from(json['lifecycle_capabilities'] ?? []),
    );
  }
}
```

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

- [ ] `apps/client/lib/control_plane_status_dto.dart`: DTO 추가.
- [ ] `apps/client/lib/control_plane_status_dto.dart`: `EdgeNodeSnapshotView.providerSnapshots` 추가 및 JSON parsing.
- [ ] `apps/client/test/widget_test.dart` 또는 기존 client unit test: JSON parse assertion 추가 또는 widget fixture에서 파싱 경로 검증.

#### 테스트 작성

- 작성: `apps/client/test/widget_test.dart`의 fake repository fixture에 provider snapshots를 추가한다. 별도 pure DTO test가 더 간결하면 같은 test file 또는 `control_plane_status_controller_test.dart`에 추가해도 된다.

#### 중간 검증

```bash
cd apps/client && flutter test test/widget_test.dart
```

기대 결과: 기존 app/widget tests와 provider DTO 표시 assertion이 통과한다.

### [API-2] Nodes Panel Provider Catalog 표시

#### 문제

[apps/client/lib/widgets/nodes_panel.dart](/config/workspace/iop/apps/client/lib/widgets/nodes_panel.dart:211)은 config가 있을 때 concurrency와 adapters만 표시한다. provider snapshots가 추가되어도 UI에서 볼 수 없다.

현재 코드:

```dart
211 if (node.config != null) ...[
212   const SizedBox(height: 16),
213   const Divider(color: Color(0xFF334155)),
214   const SizedBox(height: 12),
215   Row(
216     children: [
217       const Icon(Icons.bolt, size: 16, color: Color(0xFF6366F1)),
218       const SizedBox(width: 6),
219       Text(
220         'Concurrency Limit: ${node.config!.concurrency}',
...
226   const Text(
227     'Adapters',
...
234     Wrap(
235       spacing: 8,
236       runSpacing: 8,
237       children: node.config!.adapters.map((adapter) {
```

#### 해결 방법

Node card 안에 `Provider Catalog` 섹션을 추가한다. Cards 안에 별도 `Card`를 중첩하지 말고 기존 dark panel 안에서 `Column`, `Wrap`, `Container`로 compact rows를 만든다. 표시 필드는 다음을 포함한다.

- provider label: `id`가 있으면 id, 없으면 adapter.
- type/category: `type`과 `category`.
- state: `health` 우선, 없으면 `status`.
- load: `inFlight/capacity`, `queued`, `loadRatio`.
- served models: comma-separated first line, 길면 wrap.
- lifecycle capabilities: coarse flags만 표시.

UI text는 기존 Nodes panel 스타일에 맞춰 작고 조밀하게 둔다. 새 marketing/설명 문구는 추가하지 않는다.

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

- [ ] `apps/client/lib/widgets/nodes_panel.dart`: provider section helper 추가.
- [ ] `apps/client/lib/widgets/nodes_panel.dart`: node card에 provider snapshots가 있을 때만 section 렌더링.
- [ ] `apps/client/test/widget_test.dart`: `Provider Catalog`, provider id/type/category/status/load/models/lifecycle text assertion 추가.

#### 테스트 작성

- 작성: `Client App opens Nodes panel and displays active Nodes and configurations`에 provider display assertion을 추가한다.

#### 중간 검증

```bash
cd apps/client && flutter test test/widget_test.dart
```

기대 결과: Nodes panel test가 provider catalog/status 표시까지 검증한다.

### [API-3] Ops Review Evidence 연결

#### 문제

Milestone `ops-review`는 `iop-edge` CLI, Control Plane, Client가 어떤 정보를 소비하는지 확인할 수 있어야 한다. 작은 직접 작업으로 `iop-edge nodes list` configured provider summary는 추가됐지만, Client plan이 PASS될 때 전체 소비 경로 근거가 review/complete log에 남아야 한다.

#### 해결 방법

review stub의 `주요 설계 결정` 또는 `검증 결과`에 다음 증거를 남긴다.

- Edge CLI: `apps/edge/internal/edgecmd/nodes.go`가 configured provider category/type/models/capacity/lifecycle summary를 출력하고 `TestNodesListPrintsConfiguredProviders`가 통과했다.
- Control Plane: predecessor 03 complete.log가 `/edges/{id}/status` provider snapshots JSON을 검증했다.
- Client: 이 plan의 widget test가 provider catalog/status 표시를 검증했다.
- SDD review: `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/user_review_0.log`가 D01-D06 해결 이력으로 존재한다.

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

- [ ] `CODE_REVIEW-local-G05.md`: 구현 후 주요 설계 결정 또는 검증 결과에 위 evidence 연결을 기록한다.
- [ ] source code는 API-1/API-2 범위를 넘기지 않는다.

#### 테스트 작성

- 별도 테스트 없음. API-1/API-2 테스트와 predecessor 03 complete.log가 evidence다.

#### 중간 검증

```bash
test -f agent-task/m-provider-catalog-device-status/03_catalog_status_http/complete.log || find agent-task/archive -path '*/m-provider-catalog-device-status/03_catalog_status_http/complete.log' -print -quit
```

기대 결과: 03 predecessor complete.log 경로가 출력된다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/client/lib/control_plane_status_dto.dart` | API-1 |
| `apps/client/lib/widgets/nodes_panel.dart` | API-2 |
| `apps/client/test/widget_test.dart` | API-1, API-2 |
| `agent-task/m-provider-catalog-device-status/04+03_client_catalog_view/CODE_REVIEW-local-G05.md` | API-3 |

## 최종 검증

```bash
cd apps/client && flutter test
git diff --check
```

기대 결과: Flutter tests가 통과하고 공백 오류가 없다. Go test cache output은 해당 없음. 모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
