# Viewer System Redesign Proposal

> **Status**: DEFERRED - Validation takes priority over infrastructure work.
>
> **Decision (Jan 2026)**: This work should be deferred until after P1 (WAA episode success)
> is validated. The viewer works well enough for analysis. See "Reviewer Feedback" below.

---

## Reviewer Feedback Summary

### Consensus: Defer until core thesis is validated

**Claude (prioritization)**:
- The viewer redesign is legitimate debt but doesn't advance the demo-conditioned prompting thesis
- Current viewer works - successfully analyzed P0 and P1 results
- Opportunity cost: 1-2 weeks on viewer = 1-2 weeks not validating/scaling demos
- **Recommendation**: Only do Phase 1 quick wins (5-20 min), defer rest to after Phase 2

**GPT (scoping)**:
- Plan is over-scoped. Mixing architecture cleanup with UX modernization is risky
- **REJECT**: Unified `UIState` (premature - different lifecycles, storage contention)
- **REJECT**: Endpoint consolidation now (`local.py` is load-bearing, touching it is dangerous)
- **REJECT**: External JS for all panels (validate on ONE panel first)
- **Recommendation**: Surgical demolition, not reconstruction

### Revised Execution Plan

**Phase 0 (Do Now - 5 min)**:
1. Delete `openadapt_ml/benchmarks/viewer.py` (-1,392 lines)
2. Fix hardcoded paths in `local.py`
3. Delete deprecated functions in `viewer.py`

**Phase 1 (Do after validation - Days 1-2)**:
- Split `benchmark_viewer.py` into `training/benchmark_panels/` directory
- Mechanical move only, no logic changes

**Phase 2 (Do after validation - Days 3-4)**:
- Centralize CSS only (not full UI unification)
- Create `viewer_base.css()` or `viewer_styles.py`

**Phase 3 (Do after validation - Week 2)**:
- Extract JS for ONE panel only (live evaluation SSE)
- Validates JS extraction strategy without boiling the ocean

**Explicitly DEFERRED** (do NOT do yet):
- ❌ Unified `UIState` dataclass
- ❌ Full endpoint consolidation
- ❌ UI config system
- ❌ Tests for HTML generators
- ❌ OSWorld / training dashboards refactor

---

## Executive Summary

The viewer system is **fragmented across 6 files totaling 15,441 lines of code**, with significant duplication of CSS, JavaScript, and HTML generation logic. This document provides a comprehensive assessment of the current state, identifies specific problems, and proposes a pragmatic 80/20 redesign approach.

---

## Part 1: Current State Assessment

### 1.1 File Inventory

| File | Lines | Purpose | Key Functions |
|------|-------|---------|---------------|
| `openadapt_ml/training/benchmark_viewer.py` | **5,945** | Benchmark UI (WAA results, live eval, logs, Azure jobs, VM status) | `generate_benchmark_viewer()`, `generate_multi_run_benchmark_viewer()`, `generate_empty_benchmark_viewer()`, `generate_infrastructure_viewer()` |
| `openadapt_ml/training/viewer.py` | **2,970** | Capture playback, predictions comparison | `generate_unified_viewer_from_output_dir()`, `_generate_unified_viewer_from_extracted_data()` |
| `openadapt_ml/cloud/local.py` | **2,770** | API server, serves all viewers | `cmd_serve()`, 25+ API endpoints |
| `openadapt_ml/training/trainer.py` | **2,203** | Training dashboard, state management | `generate_training_dashboard()`, `TrainingState`, `TrainingLogger` |
| `openadapt_ml/benchmarks/viewer.py` | **1,392** | Alternate benchmark viewer (mostly unused?) | `generate_benchmark_viewer()` (duplicate name!) |
| `openadapt_ml/training/shared_ui.py` | **161** | Shared header CSS/HTML | `get_shared_header_css()`, `generate_shared_header_html()` |

**Total: 15,441 lines** across 6 files for what is essentially a single-page app with 4 tabs.

### 1.2 Viewer Types and Data Sources

| Tab | HTML File | Generated By | Data Source | Refresh Method |
|-----|-----------|--------------|-------------|----------------|
| **Training** | `dashboard.html` | `trainer.py:generate_training_dashboard()` | `training_log.json` | File polling |
| **Viewer** | `viewer.html` | `viewer.py:generate_unified_viewer_from_output_dir()` | `comparison_epoch*.html`, `predictions_*.json` | Static (no polling) |
| **Benchmarks** | `benchmark.html` | `benchmark_viewer.py:generate_*_benchmark_viewer()` | `benchmark_results/*/summary.json`, `/api/benchmark-live`, `/api/tasks` | SSE + polling |
| **Infrastructure** | `infrastructure.html` | `benchmark_viewer.py:generate_infrastructure_viewer()` | `/api/build-log`, `/api/vms` | Polling |

### 1.3 API Endpoints in `local.py`

The server defines **25+ API endpoints** (lines 501-893):

| Endpoint | Method | Purpose | Used By |
|----------|--------|---------|---------|
| `/api/stop` | POST | Stop training | Training tab |
| `/api/run-benchmark` | POST | Start API benchmark | Benchmarks tab |
| `/api/benchmark/start` | POST | Start configurable benchmark | Benchmarks tab |
| `/api/vms/register` | POST | Register VM | Benchmarks tab |
| `/api/benchmark-progress` | GET | Benchmark progress JSON | Benchmarks tab |
| `/api/benchmark-live` | GET | Live eval state JSON | Benchmarks tab |
| `/api/tasks` | GET | Background task status | Benchmarks tab |
| `/api/azure-jobs` | GET | Azure ML job status | Training tab |
| `/api/benchmark-sse` | GET | Real-time SSE stream | Benchmarks tab |
| `/api/benchmark-logs-sse` | GET | Log streaming SSE | Benchmarks tab |
| `/api/benchmark-logs-full` | GET | Full log file | Benchmarks tab |
| `/api/vms` | GET | VM registry | Benchmarks tab |
| `/api/azure-job-logs` | GET | Azure job logs | Training tab |
| `/api/probe-vm` | GET | VM WAA probe | Benchmarks tab |
| `/api/tunnels` | GET | SSH tunnel status | Benchmarks tab |
| `/api/current-run` | GET | Current benchmark info | Benchmarks tab |
| `/api/background-tasks` | GET | Alias for `/api/tasks` | Benchmarks tab |
| `/api/build-log` | GET | Docker build logs | Infrastructure tab |

### 1.4 State Management

**Training State** (`trainer.py:109-219`):
- `TrainingState` dataclass with 30+ fields
- Serialized to `training_log.json`
- Includes losses, evaluations, termination status

**Benchmark State** (scattered):
- `benchmark_live.json` - Written by benchmark runner
- `benchmark_progress.json` - Written by API benchmark runner
- `vm_registry.json` - Written by VM registration API
- `azure_jobs.json` - Cached Azure ML job data (unused?)
- Various task-level JSON files in `benchmark_results/*/tasks/*/`

**No unified state model** - each viewer type has its own state format and storage.

---

## Part 2: Problems Identified

### 2.1 Fragmentation Issues

#### Problem F1: Two `generate_benchmark_viewer()` Functions
- `openadapt_ml/training/benchmark_viewer.py:3556` (5945 lines)
- `openadapt_ml/benchmarks/viewer.py:191` (1392 lines)

Both generate benchmark HTML! The one in `benchmarks/viewer.py` appears to be legacy/unused but creates confusion.

**Evidence**:
```python
# benchmark_viewer.py:3556
def generate_benchmark_viewer(benchmark_dir: Path | str, output_path: Path | str | None = None) -> Path:

# benchmarks/viewer.py:191
def generate_benchmark_viewer(benchmark_dir: Path, output_path: Path | None = None, embed_screenshots: bool = False) -> Path:
```

#### Problem F2: CSS Duplication
The same CSS variables and base styles are defined in multiple places:

- `shared_ui.py:16-104` - Shared header CSS
- `trainer.py:812` - Injects `_get_shared_header_css()` but also defines more CSS
- `viewer.py:300-500` - Defines its own `:root` variables and styles
- `benchmark_viewer.py:3783-3800` - Defines `:root` variables again
- `benchmarks/viewer.py:304-317` - Yet another `:root` definition

**Example duplication** (`:root` CSS variables appear 4+ times):
```css
:root {
    --bg-primary: #0a0a0f;
    --bg-secondary: #12121a;
    --bg-tertiary: #1a1a24;
    --border-color: rgba(255, 255, 255, 0.06);
    --text-primary: #f0f0f0;
    --text-secondary: #888;
    --text-muted: #555;
    --accent: #00d4aa;
    ...
}
```

#### Problem F3: Import Cycles
`benchmark_viewer.py` imports from `trainer.py`:
```python
# benchmark_viewer.py:3637
from openadapt_ml.training.trainer import _get_shared_header_css, _generate_shared_header_html
```

But `trainer.py` imports from `benchmark_viewer.py`:
```python
# trainer.py:18
from openadapt_ml.training.benchmark_viewer import (
    _get_azure_jobs_panel_css,
    _get_azure_jobs_panel_html,
)
```

This creates tight coupling and makes refactoring difficult.

### 2.2 Complexity Issues

#### Problem C1: Monolithic `benchmark_viewer.py` (5,945 lines)
This single file contains:
- Lines 13-290: Background tasks panel CSS
- Lines 293-702: Background tasks panel HTML + JS
- Lines 706-982: Live evaluation panel CSS
- Lines 985-1433: Live evaluation panel HTML + JS (SSE manager)
- Lines 1436-1821: Live logs panel CSS + HTML + JS
- Lines 1824-2500+: Azure jobs panel CSS + HTML + JS
- Lines 2500-3500: VM discovery panel, run benchmark panel
- Lines 3556-3650: `generate_benchmark_viewer()` function
- Lines 3654-3744: `generate_multi_run_benchmark_viewer()`
- Lines 3747-5390: `generate_empty_benchmark_viewer()` with massive inline HTML
- Lines 5391-5945: `generate_infrastructure_viewer()`

**One file doing too much** - combines CSS generation, HTML templating, JavaScript event handling, SSE client code, and multiple panel generators.

#### Problem C2: Inline JavaScript (estimated 2000+ lines)
JavaScript is embedded in Python f-strings, making it:
- Impossible to lint or type-check
- Difficult to debug (no source maps)
- Hard to test
- Messy with escaped braces (`{{` and `}}` everywhere)

**Example from `benchmark_viewer.py:1020-1241`**:
```python
def _get_live_evaluation_panel_html() -> str:
    return '''
    ...
    <script>
        // 220+ lines of JavaScript including:
        class BenchmarkSSEManager {
            constructor() {
                this.eventSource = null;
                this.pollingInterval = null;
                // ...50 more fields and methods
            }
            connect() { ... }
            handleStatusEvent(data) { ... }
            // ...15 more methods
        }
    </script>
    '''
```

#### Problem C3: Redundant API Endpoints
- `/api/tasks` and `/api/background-tasks` are identical (line 827-841)
- `/api/benchmark-progress` vs `/api/benchmark-live` serve similar purposes
- Azure jobs fetched live every request (no caching strategy)

### 2.3 Fragility Issues

#### Problem R1: Broken `comparison_epoch*.html` Extraction
`viewer.py:146-188` extracts data from comparison HTML files using regex:
```python
data_match = re.search(
    r'(?:const\s+|window\.)comparisonData\s*=\s*(\[.*?\]);',
    html_content,
    re.DOTALL
)
```
This is fragile - any change to comparison HTML structure breaks extraction.

#### Problem R2: Hardcoded Azure Experiment ID
`local.py:910`:
```python
experiment_id = "ad29082c-0607-4fda-8cc7-38944eb5a518"
wsid = "/subscriptions/78add6c6-c92a-4a53-b751-eb644ac77e59/..."
```
These are hardcoded UUIDs that will break for any other Azure workspace.

#### Problem R3: File Paths in `/tmp/claude/`
`local.py:847`:
```python
output_files = sorted(glob.glob('/tmp/claude/-Users-abrichr-oa-src/tasks/*.output'), ...)
```
Hardcoded user-specific path that only works on one machine.

#### Problem R4: SSE Connection Recovery
The SSE manager in `benchmark_viewer.py:1020-1241` has complex reconnection logic:
```javascript
handleConnectionError() {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
        this.reconnectAttempts++;
        setTimeout(() => this.reconnect(), this.reconnectDelay * this.reconnectAttempts);
    } else {
        this.startPolling();  // Fallback
    }
}
```
But there's no way to test this - it's embedded in Python strings.

### 2.4 Confusion Issues

#### Problem U1: Tab Names vs File Names
| Tab Label | Expected File | Actual Generator |
|-----------|---------------|------------------|
| "Training" | `dashboard.html` (not `training.html`) | `trainer.py` |
| "Viewer" | `viewer.html` | `viewer.py` |
| "Benchmarks" | `benchmark.html` | `benchmark_viewer.py` |
| "Infrastructure" | `infrastructure.html` | `benchmark_viewer.py` (not its own file!) |

#### Problem U2: Unclear Data Flow
When the user clicks "Start Benchmark":
1. POST to `/api/benchmark/start`
2. Spawns subprocess running CLI command
3. CLI writes to `benchmark_results/`
4. Separate process writes to `benchmark_live.json`
5. SSE endpoint polls `benchmark_live.json`
6. JavaScript SSE client updates UI

This is documented nowhere and spans 4 files.

#### Problem U3: "Mock" vs "Real" Confusion
`local.py:76-115` filters "mock" benchmarks:
```python
def _is_mock_benchmark(benchmark_dir: Path) -> bool:
    # Filter out mock/test/random agent runs
    if any(term in model_id for term in ["random-agent", "scripted-agent"]):
        return True
```
But `waa-mock` evaluations with real API models are **NOT** filtered. This logic is buried and non-obvious.

### 2.5 Technical Debt

#### Debt D1: Deprecated Functions Still Present
`viewer.py` contains:
```python
# Line 2626+ (approx)
def _enhance_comparison_to_unified_viewer():
    """DEPRECATED: Use generate_unified_viewer_from_output_dir instead."""
```
Still present, still importable.

#### Debt D2: `localStorage` State Persistence
`benchmark_viewer.py:508-560` persists UI state to localStorage:
```javascript
const STORAGE_KEY = 'openadapt_task_expanded_states';
function getTaskExpandedStates() {
    const stored = localStorage.getItem(STORAGE_KEY);
    return stored ? JSON.parse(stored) : {};
}
```
Good idea, but implemented only for one panel. Inconsistent UX.

#### Debt D3: Mixed Polling Intervals
- Background tasks: 10 seconds (`benchmark_viewer.py:700`)
- Live evaluation SSE: 2 seconds (`benchmark_viewer.py:1067`)
- Azure jobs: every request (no cache)
- Build logs: 2 seconds (hardcoded in `local.py`)
- VM status: 5x slower than tasks (`benchmark_viewer.py:406`)

No central configuration. No way to tune.

---

## Part 3: Redesign Proposal

### 3.1 Guiding Principles

1. **Single source of truth** for each data type
2. **Separate concerns** - CSS, JS, and HTML in separate files where possible
3. **Unified state model** - one schema, one storage location
4. **Progressive enhancement** - start with what works, improve incrementally
5. **80/20 rule** - focus on changes with maximum impact for minimum effort

### 3.2 Proposed Architecture

```
openadapt_ml/
  ui/                           # NEW: All UI code in one place
    __init__.py
    components/
      __init__.py
      header.py                 # Shared header (from shared_ui.py)
      loss_chart.py             # Training loss visualization
      playback_controls.py      # Step-by-step playback (shared)
      task_list.py              # Task list with filters (shared)
      terminal.py               # Log terminal component
    pages/
      __init__.py
      training.py               # Training tab generator
      viewer.py                 # Viewer tab generator
      benchmarks.py             # Benchmarks tab generator
      infrastructure.py         # Infrastructure tab generator
    static/
      styles.css                # All CSS in one file
      app.js                    # Core JS (ES modules)
      sse.js                    # SSE client manager
      playback.js               # Playback controller
    server.py                   # HTTP server (from local.py)
    state.py                    # Unified state management
```

### 3.3 Phase 1: Quick Wins (Day 1-2)

**Goal**: Reduce confusion and fragility with minimal code changes.

#### 1A: Delete Duplicate `benchmarks/viewer.py`
```bash
rm openadapt_ml/benchmarks/viewer.py
# Update any imports (grep shows it's rarely used)
```
**Impact**: Eliminates confusion, -1392 lines.

#### 1B: Extract CSS to Shared File
Create `openadapt_ml/training/viewer_styles.py`:
```python
def get_base_css() -> str:
    """Return base CSS variables and reset styles."""
    return '''
    :root {
        --bg-primary: #0a0a0f;
        --bg-secondary: #12121a;
        --bg-tertiary: #1a1a24;
        --border-color: rgba(255, 255, 255, 0.06);
        --text-primary: #f0f0f0;
        --text-secondary: #888;
        --text-muted: #555;
        --accent: #00d4aa;
        --accent-dim: rgba(0, 212, 170, 0.15);
        --success: #34d399;
        --error: #ff5f5f;
        --warning: #f59e0b;
    }
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
        font-family: "SF Pro Display", -apple-system, BlinkMacSystemFont, "Inter", sans-serif;
        background: var(--bg-primary);
        color: var(--text-primary);
        min-height: 100vh;
        line-height: 1.5;
    }
    '''
```
Update all generators to import from this one place.

**Impact**: Single source of truth for styling.

#### 1C: Fix Hardcoded Paths
Replace:
```python
# local.py:847
glob.glob('/tmp/claude/-Users-abrichr-oa-src/tasks/*.output')
```
With:
```python
import tempfile
glob.glob(f'{tempfile.gettempdir()}/claude/*/tasks/*.output')
```

**Impact**: Works on any machine.

### 3.4 Phase 2: Consolidation (Day 3-5)

**Goal**: Reduce file count and improve maintainability.

#### 2A: Split `benchmark_viewer.py` into Components

**Current** (5,945 lines in one file):
- Background tasks panel: ~400 lines CSS+JS
- Live evaluation panel: ~450 lines CSS+JS
- Live logs panel: ~400 lines CSS+JS
- Azure jobs panel: ~700 lines CSS+JS
- VM discovery panel: ~300 lines
- Run benchmark panel: ~500 lines
- Viewer generators: ~2000 lines

**Proposed** split:
```
openadapt_ml/training/
  benchmark_viewer.py       # Keep, but slim (generators only, ~500 lines)
  panels/
    __init__.py
    background_tasks.py     # _get_background_tasks_panel_*
    live_evaluation.py      # _get_live_evaluation_panel_*
    live_logs.py            # _get_live_logs_panel_*
    azure_jobs.py           # _get_azure_jobs_panel_*
    vm_discovery.py         # _get_vm_discovery_panel_*
    run_benchmark.py        # _get_run_benchmark_panel_*
```

Each panel file exports:
```python
def get_css() -> str: ...
def get_html() -> str: ...
def get_js() -> str: ...  # Optional, for complex panels
```

**Impact**: -4000 lines from monolithic file, better organization.

#### 2B: Unified State Schema

Create `openadapt_ml/training/ui_state.py`:
```python
from dataclasses import dataclass, field
from typing import Any

@dataclass
class UIState:
    """Unified state for all UI tabs."""

    # Training state
    training_running: bool = False
    training_epoch: int = 0
    training_step: int = 0
    training_loss: float = 0.0
    training_losses: list[dict] = field(default_factory=list)

    # Benchmark state
    benchmark_running: bool = False
    benchmark_tasks_completed: int = 0
    benchmark_tasks_total: int = 0
    benchmark_current_task: str = ""

    # VM state
    vms: list[dict] = field(default_factory=list)
    tunnels: dict[str, dict] = field(default_factory=dict)

    # Infrastructure state
    build_running: bool = False
    build_progress: float = 0.0

    def to_json(self) -> str:
        import json
        from dataclasses import asdict
        return json.dumps(asdict(self))

    @classmethod
    def from_json(cls, data: str) -> "UIState":
        import json
        return cls(**json.loads(data))
```

Single file: `training_output/current/ui_state.json`

**Impact**: One place to understand all state, one file to poll.

#### 2C: Consolidate API Endpoints

Group endpoints in `local.py` by resource:

```python
class APIHandler:
    # Training endpoints
    def handle_training_status(self): ...
    def handle_training_stop(self): ...

    # Benchmark endpoints
    def handle_benchmark_status(self): ...
    def handle_benchmark_start(self): ...
    def handle_benchmark_live(self): ...
    def handle_benchmark_sse(self): ...

    # VM endpoints
    def handle_vm_list(self): ...
    def handle_vm_probe(self): ...
    def handle_vm_tunnels(self): ...

    # Infrastructure endpoints
    def handle_build_log(self): ...
```

Remove duplicates:
- `/api/background-tasks` -> alias to `/api/tasks`
- `/api/benchmark-progress` -> merge into `/api/benchmark-status`

**Impact**: Clearer API surface, easier to document.

### 3.5 Phase 3: Modernization (Week 2)

**Goal**: Better developer experience and testability.

#### 3A: External JavaScript Files

Move JavaScript out of Python strings:
```
openadapt_ml/training/static/
  benchmark_sse.js          # SSE manager class
  playback_controller.js    # Step playback logic
  chart_renderer.js         # Loss chart with Chart.js
  terminal.js               # Log terminal with auto-scroll
```

Python generator includes via script tag:
```python
def get_html() -> str:
    return f'''
    <script type="module">
        import {{ BenchmarkSSEManager }} from '/static/benchmark_sse.js';
        window.sseManager = new BenchmarkSSEManager();
        window.sseManager.connect();
    </script>
    '''
```

**Impact**: Lintable, testable JavaScript. Source maps for debugging.

#### 3B: Configuration File for Intervals

Create `openadapt_ml/training/ui_config.py`:
```python
@dataclass
class UIConfig:
    # Polling intervals (milliseconds)
    poll_training_status: int = 2000
    poll_benchmark_live: int = 2000
    poll_background_tasks: int = 10000
    poll_vm_status: int = 10000
    poll_build_log: int = 2000

    # SSE settings
    sse_heartbeat_interval: int = 30000
    sse_reconnect_delay: int = 2000
    sse_max_reconnect_attempts: int = 5

    # Display settings
    max_log_lines: int = 500
    max_evaluation_samples: int = 50
```

Inject into generated HTML:
```javascript
const UI_CONFIG = {config.to_json()};
```

**Impact**: Single place to tune all intervals.

#### 3C: Unit Tests for Generators

Create `tests/test_viewer_generators.py`:
```python
def test_training_dashboard_generates_valid_html():
    state = TrainingState(job_id="test", losses=[{"epoch": 1, "step": 1, "loss": 0.5}])
    config = TrainingConfig()
    html = generate_training_dashboard(state, config)

    assert '<!DOCTYPE html>' in html
    assert 'Training' in html
    assert 'loss' in html.lower()
    # No unescaped braces
    assert '{{' not in html or '{config' not in html

def test_benchmark_viewer_generates_valid_html():
    # Create temp benchmark dir with test data
    ...
```

**Impact**: Catch regressions, ensure HTML is valid.

---

## Part 4: Implementation Priority

### Immediate (This Week)

| Task | Files | Lines Changed | Impact |
|------|-------|---------------|--------|
| Delete `benchmarks/viewer.py` | 1 | -1392 | High (remove confusion) |
| Fix hardcoded paths | `local.py` | ~10 | Medium (portability) |
| Extract base CSS | New file + updates | ~200 | Medium (DRY) |

### Short-term (Next Week)

| Task | Files | Lines Changed | Impact |
|------|-------|---------------|--------|
| Split `benchmark_viewer.py` into panels | 7 new, 1 modified | ~5000 moved | High (maintainability) |
| Unified UIState | 1 new, updates | ~500 | High (clarity) |
| Consolidate API endpoints | `local.py` | ~200 | Medium (simplicity) |

### Medium-term (Month 1)

| Task | Files | Lines Changed | Impact |
|------|-------|---------------|--------|
| External JS files | 4 new JS, updates | ~2000 moved | High (testability) |
| UI config file | 1 new | ~100 | Medium (tunability) |
| Generator unit tests | 1 new | ~300 | Medium (reliability) |

---

## Part 5: Metrics and Success Criteria

### Current State Metrics

- **Files**: 6 viewer-related files
- **Total LOC**: 15,441
- **Duplicate CSS definitions**: 4+ `:root` blocks
- **Inline JavaScript**: ~2000+ lines in Python strings
- **API endpoints**: 25+ (some duplicates)
- **State files**: 5+ different JSON formats

### Target Metrics (After Phase 3)

- **Files**: 10-15 smaller, focused files
- **Total LOC**: ~12,000 (15% reduction from consolidation)
- **Duplicate CSS definitions**: 1 (single source)
- **Inline JavaScript**: <500 lines (rest in .js files)
- **API endpoints**: 15-18 (no duplicates)
- **State files**: 1-2 (unified schema)

### User-Facing Improvements

1. **Consistency**: All tabs look and behave the same
2. **Reliability**: No hardcoded paths, no extraction failures
3. **Performance**: Configurable polling intervals, efficient SSE
4. **Debuggability**: External JS with source maps

---

## Appendix A: File-by-File Audit

### `benchmark_viewer.py` (5,945 lines)

| Line Range | Content | Assessment |
|------------|---------|------------|
| 1-12 | Docstring, imports | OK |
| 13-290 | `_get_background_tasks_panel_css()` | MOVE to panel file |
| 293-702 | `_get_background_tasks_panel_html()` with 360 lines of JS | MOVE to panel file, extract JS |
| 706-982 | `_get_live_evaluation_panel_css()` | MOVE to panel file |
| 985-1433 | `_get_live_evaluation_panel_html()` with SSE manager (450 lines) | MOVE, extract JS |
| 1436-1560 | `_get_live_logs_panel_css()` | MOVE |
| 1563-1821 | `_get_live_logs_panel_html()` with LogsSSEManager | MOVE, extract JS |
| 1824-2100 | `_get_azure_jobs_panel_css()` | MOVE |
| 2100-2600 | `_get_azure_jobs_panel_html()` | MOVE |
| 2600-2850 | `_get_vm_discovery_panel_css()` | MOVE |
| 2850-3150 | `_get_vm_discovery_panel_html()` | MOVE |
| 3150-3350 | `_get_run_benchmark_panel_css()` | MOVE |
| 3350-3553 | `_get_run_benchmark_panel_html()` + JS | MOVE |
| 3556-3650 | `generate_benchmark_viewer()` | KEEP |
| 3654-3744 | `generate_multi_run_benchmark_viewer()` | KEEP |
| 3747-5390 | `generate_empty_benchmark_viewer()` | SIMPLIFY (too much inline HTML) |
| 5391-5945 | `generate_infrastructure_viewer()` | KEEP |

**Verdict**: Split into 6+ panel files, extract JavaScript, reduce by ~4000 lines.

### `viewer.py` (2,970 lines)

| Line Range | Content | Assessment |
|------------|---------|------------|
| 1-60 | Docstring, imports, helpers | OK |
| 62-257 | `generate_unified_viewer_from_output_dir()` | OK but complex |
| 260-2600 | `_generate_unified_viewer_from_extracted_data()` | Large, has inline JS |
| 2600-2700 | Deprecated functions | DELETE |
| 2700-2970 | More inline CSS/JS | CONSOLIDATE |

**Verdict**: Remove deprecated code, extract JS, consolidate CSS.

### `trainer.py` (2,203 lines)

| Line Range | Content | Assessment |
|------------|---------|------------|
| 1-100 | Imports, setup functions | OK |
| 108-219 | `TrainingState` dataclass | OK |
| 222-334 | `TrainingLogger` class | OK |
| 336-378 | `_generate_termination_status_html()` | OK |
| 380-2029 | `generate_training_dashboard()` | Large, has inline CSS/JS |
| 2029-2203 | `regenerate_local_dashboard()` and helpers | OK |

**Verdict**: Extract CSS to shared file, keep generator logic.

### `local.py` (2,770 lines)

| Line Range | Content | Assessment |
|------------|---------|------------|
| 1-200 | Imports, helpers, viewer regeneration | OK |
| 200-500 | CLI commands (status, train, check, serve) | OK |
| 501-622 | POST handlers | ORGANIZE by resource |
| 624-892 | GET handlers (25+ endpoints) | CONSOLIDATE, remove duplicates |
| 894-2000 | Helper methods for fetching data | ORGANIZE |
| 2000-2770 | SSE streaming, more helpers | OK but scattered |

**Verdict**: Organize handlers by resource, remove duplicate endpoints.

---

## Appendix B: Risk Assessment

| Risk | Probability | Impact | Mitigation |
|------|-------------|--------|------------|
| Breaking existing functionality | Medium | High | Add tests before refactoring |
| Increasing complexity during transition | Medium | Medium | Phased approach, small PRs |
| Performance regression from file restructuring | Low | Low | Profile before/after |
| Team confusion during transition | Medium | Medium | Clear documentation |

---

## Appendix C: References

- Current design doc: `/Users/abrichr/oa/src/openadapt-ml/docs/viewer_consolidation_design.md`
- Existing tests: `test_benchmark_viewer.py`, `test_local_cli.py`
- CLAUDE.md viewer sections: "Viewer Setup Troubleshooting", "Viewer Code Consolidation"
