Shared report UI reference for all HPA evaluation components. Component-specific docs (in `components/`) reference this file for data schema and page layout.

## Report Architecture

Each report is a **single self-contained HTML file**. No build step, no Node.js project,
no npm dependencies. The HTML file loads Tailwind CSS and Chart.js from CDN and renders
all charts, tables, and panels from a `DATA` JavaScript object embedded in the file.

### Template + Data Injection

```
assets/copr-worker-report-template.html   # Template with /*__REPORT_DATA__*/ placeholder
         + data.json               # Evaluation data (matches schema below)
         = report.html             # Complete self-contained report
```

The `generate-report.sh` script performs this injection:
1. Reads the template HTML
2. Reads the JSON data file
3. Replaces `/*__REPORT_DATA__*/` (and the sample data after it up to the closing `};`)
   with the JSON data
4. Writes the output HTML

### Technology Stack

| Dependency | Source | Purpose |
|------------|--------|---------|
| Tailwind CSS | CDN (`cdn.tailwindcss.com`) | Styling (dark-first design) |
| Chart.js 4 | CDN (`cdn.jsdelivr.net`) | Timeline charts, latency distribution |
| chartjs-plugin-annotation 3 | CDN (`cdn.jsdelivr.net`) | CPU threshold reference lines |

No local dependencies. Reports work offline after initial CDN load.

### How to Generate a Report

```bash
# 1. Create JSON data file from evaluation results
#    (agent writes /tmp/eval-data.json matching the schema below)

# 2. Resolve skill directory
SKILL_DIR="${HOME}/.config/opencode/skills/autoscaler-dashboard"

# 3. Inject data into template
"${SKILL_DIR}/generate-report.sh" \
  /tmp/eval-data.json \
  "${SKILL_DIR}/assets/copr-worker-report-template.html" \
  /tmp/feb14-report.html

# 4. Open the report
open /tmp/feb14-report.html
```

### Template Files

| Template | Component | Description |
|----------|-----------|-------------|
| `assets/copr-worker-report-template.html` | Coprocessor Worker | Dual-latency timeline (Copr P99 + TiDB P99), CPU sparklines |

To add a new component type, copy an existing template and adjust chart labels
and any component-specific UI sections.

### Example Report

`assets/copr-worker-report.html` is a concrete report with hardcoded sample data. Use it as
a reference implementation to verify the template renders correctly, or as a visual
reference for the expected report layout.

## Page Layout (Scrollable, Top to Bottom)

The report is a **single scrollable page** (NOT tabbed). 8 visual sections:

### Section 1: Dashboard Header

Title, status badge (healthy/warning/critical), overall score, v0 Heuristic badge,
assessment period, cluster ID, HPA target, and dark/light theme toggle.

All values driven from `DATA.summary` and `DATA.metadata`.

### Section 2: Quick Info Bar

Inline card showing region, CPU target with tolerance, and HPA metrics description.

### Section 3: KPI Metrics Cards (4 cards, 1 row)

| Card | Value Source | Color |
|------|-------------|-------|
| Scaling Events | `algorithmAttribution.summary.totalEvents` | rose |
| Thrashing Rate | `algorithmAttribution.summary.thrashingPercent` | amber |
| Max P99 Latency | `performanceMetrics.p99Latency.max` | rose |
| Avg CPU Utilization | `performanceMetrics.cpuUtilization.average` | blue |

### Section 4: Scoring Overview

SVG gauge rings: 1 large overall gauge + 5 smaller category gauges.

### Section 5: Workload Timeline

4-panel stacked synchronized Chart.js timeline:

1. **P99 Latency** (line chart, dual Y-axis: Copr P99 left, TiDB P99 right)
2. **CPU Utilization** (line chart with scale-out/scale-in threshold annotation lines)
3. **Replicas** (bar chart with custom candlestick plugin: whisker + caps + snapshot marker)
4. **Events** (diverging stacked bar: green bars UP = scale-out, red bars DOWN = scale-in)

All panels share a crosshair that syncs across charts on hover.

Below the timeline: **CPU Util Distribution vs HPA Zones** card with horizontal progress bars.

### Section 6: Detailed Scaling Events Table

Filterable table (All / Scale Out / Scale In) with inline SVG sparklines showing CPU
in a +/-2min window around each HPA decision point. Columns:

| Column | Content |
|--------|---------|
| # | Event ID |
| Time | HH:MM:SS timestamp |
| Dir | Arrow icon (green up / red down) |
| Change | fromReplicas -> toReplicas |
| CPU Sparkline | 330x68 SVG with CPU trace, threshold lines, lag window, nearby event markers |
| CPU@Event | CPU% at decision time |
| Deviation | Distance from CPU target |
| Threshold | Scale-out or scale-in threshold |
| Lag | Seconds until pod_ready matches desired |

### Section 7: Analysis

- Score breakdown + Latency distribution (side by side)
- HPA Configuration card + Replica Distribution card (side by side)

### Section 8: Evaluation Criteria + Recommendations + Footers

- Evaluation Criteria: Expandable accordion per scoring category with threshold tables
- Recommendations: Expandable cards with problem/root cause/evidence/config diff
- Data Sources footer + Report Metadata footer

## Data Schema

The JSON data object injected via `/*__REPORT_DATA__*/` must match this shape.
Field names and types are consumed directly by the template's rendering functions.

### V2ScalingEvent (Detailed Events)

```javascript
{
  id: 1,                           // Sequential event number (1-based)
  timestamp: "03:10:00",           // "HH:MM:SS" format
  direction: "UP",                 // "UP" or "DOWN"
  fromReplicas: 2,                 // Replica count before event
  toReplicas: 3,                   // Replica count after event
  cpuAtEvent: 62.3,               // CPU% at t=0 (HPA decision time)
  deviation: 7.3,                  // cpuAtEvent - cpuTarget
  scaleLag: 45,                    // Seconds from desired change to pod_ready match
  cpuWindow: [                     // 17 values at 15s intervals, t=-120 to t=+120
    { t: -120, cpu: 48.2 },
    { t: -105, cpu: 51.3 },
    // ... 15 more entries ...
    { t: 120, cpu: 38.2 }
  ]
}
```

### WorkloadTimelineEntry (5-min Buckets)

```javascript
{
  time: "03:10",          // "HH:MM"
  hour: 3.167,            // Fractional hour (for chart positioning)
  replicas: 3,            // Snapshot replicas at bucket start
  replicasMin: 2,         // Min replicas in bucket (for candlestick)
  replicasMax: 3,         // Max replicas in bucket (for candlestick)
  cpuPercent: 62.3,       // Average CPU% in bucket
  p99Latency: 5.2,        // Average P99 latency (seconds)
  tidbP99Latency: 8.1,    // TiDB P99 latency (milliseconds) -- secondary latency axis
  requestRate: 75.3,      // Average req/s in bucket
  events: "UP 2->3",      // Description or "-"
  eventCount: 1,          // Total events in this 5-min bucket
  scaleOutCount: 1,       // Scale-out events in bucket
  scaleInCount: 0         // Scale-in events in bucket
}
```

### Full DATA Shape

```javascript
const DATA = {
  summary: {
    component: "Coprocessor Worker",   // Component display name
    title: "HPA Scaling Quality Report",
    overallScore: 72,                  // 0-100
    assessmentPeriod: "Feb 8, 2026",
    duration: "24 hours",
    clusterId: "2013516099403907072",
    region: "alicloud-ap-southeast-1",
    hpaTarget: "coprocessor-hpa",
    namespace: "tidb2013516099403907072",
    keyFinding: "...",                 // 1-2 sentence finding
    status: "healthy",                 // "healthy" | "warning" | "critical"
  },

  hpaConfig: {
    cpuTarget: 55,
    tolerance: 10,
    scaleUpZone: "> 60.5%",
    doNothingZone: "49.5% - 60.5%",
    scaleDownZone: "< 49.5%",
    minReplicas: 2,
    maxReplicas: 30,
    scaleUpPolicy: "Max",
    scaleDownPolicy: "Max",
    stabilizationWindow: 180,          // seconds
    cpuRequest: 4,                     // cores
    observedRange: "2-5",
    metricsDescription: "cpu (Resource) + ext_cpu_scaled_ratio (External)",
  },

  scores: [/* array of score category objects -- see V2ScalingEvent above for nested shape */],
  evaluationCriteria: [/* array of evaluation criteria with threshold tables */],

  algorithmAttribution: {
    summary: {
      totalEvents: 14,
      scaleUp: 7,
      scaleDown: 7,
      thrashing: 3,
      thrashingPercent: 42.9,
    }
  },

  workloadTimeline: [/* array of WorkloadTimelineEntry objects, 288 entries for 24h */],
  detailedEvents: [/* array of V2ScalingEvent objects */],

  cpuDistribution: {
    aboveScaleUp: { samples: 18, percent: 0.3, label: "> 60.5%" },
    tolerance: { samples: 12, percent: 0.2, label: "49.5% - 60.5%" },
    belowScaleDown: { samples: 974, percent: 16.9, label: "< 49.5%" },
    idle: { samples: 4756, percent: 82.6, label: "0%" },
  },

  latencyDistribution: [
    { range: "<1s", samples: 5234, percentage: 90.9, assessment: "normal" },
    // ... more ranges: "acceptable", "degraded", "poor", "critical"
  ],

  performanceMetrics: {
    p99Latency: { average: 2.3, max: 45.2, min: 0.01, dataPoints: 5760 },
    requestRate: { average: 12.5, max: 89.3, min: 0 },
    cpuUtilization: { average: 1.9, max: 67.2, target: 55, nonZeroSamples: 1004 },
  },

  replicaDistribution: [
    { replicas: 2, timePercent: 97.4, samples: 5610 },
    // ...
  ],

  recommendations: [
    {
      title: "Increase Scale-Down Stabilization Window",
      priority: "P1",       // "P1" or "P2"
      problem: "...",
      rootCause: "...",
      evidence: "...",
      currentConfig: "stabilizationWindow: 180s",
      recommendedConfig: "stabilizationWindow: 300s (5 minutes)",
      expectedImpact: "...",
    },
  ],

  dataSources: {
    dataplane: { uuid: "...", name: "Dataplane Prometheus" },
    application: { uuid: "...", name: "Application Prometheus" },
  },

  metadata: {
    generatedDate: "Feb 14, 2026",
    revision: "v2.1",
    tool: "scaling-evaluator",
    methodology: "autoscaler-dashboard v2",
    scoringMethodology: "v0 Heuristic",
  },

  insights: {
    cpuDistribution: "...",        // Finding text for CPU distribution insight box
    replicaDistribution: "...",    // Observation text for replica distribution insight box
  },
};
```

## Calculation Logic: Building Event Data from Raw Metrics

### Required Raw Metrics (cached as /tmp/*.json at 15s step)

| File | Metric | Source UUID |
|------|--------|------------|
| `/tmp/desired_replicas_15s.json` | `kube_horizontalpodautoscaler_status_desired_replicas` | K8S_UUID |
| `/tmp/current_replicas_15s.json` | `kube_horizontalpodautoscaler_status_current_replicas` | K8S_UUID |
| `/tmp/cpu_util_15s.json` | `kube_horizontalpodautoscaler_status_target_metric{metric_name="cpu",metric_target_type="utilization"}` | K8S_UUID |
| `/tmp/pod_ready_15s.json` | `sum(kube_pod_status_ready{pod=~".*coprocessor.*",condition="true"})` | K8S_UUID |
| `/tmp/tidb_p99_15s.json` | `histogram_quantile(0.99, sum(rate(tidb_server_handle_query_duration_seconds_bucket[5m])) by (le))` | APP_UUID |

All queried at `step=15s` for 24h, producing ~5760 data points each.

### Step 1: Detect Scaling Events from desired_replicas

Scan `desired_replicas` for VALUE TRANSITIONS (not desired vs current comparison):

```bash
jq -r '
  .data.result[0].values as $v |
  [range(1; $v | length)] |
  map(select($v[.][1] != $v[. - 1][1])) |
  map({
    epoch: $v[.][0],
    time: ($v[.][0] | strftime("%H:%M:%S")),
    from: ($v[. - 1][1] | tonumber),
    to: ($v[.][1] | tonumber),
    direction: (if ($v[.][1] | tonumber) > ($v[. - 1][1] | tonumber) then "UP" else "DOWN" end)
  })
' /tmp/desired_replicas_15s.json
```

### Step 2: Look Up CPU at Event Time

For each event at epoch T, find the CPU utilization value at T (or nearest 15s scrape):

```bash
jq --argjson t $EVENT_EPOCH '
  .data.result[0].values | map(select(.[0] == $t)) | .[0][1] | tonumber
' /tmp/cpu_util_15s.json
```

- `cpuAtEvent` = CPU value at T
- `deviation` = cpuAtEvent - hpaConfig.cpuTarget

### Step 3: Build CPU Window (+/-2min)

For each event at epoch T, extract 17 CPU values from T-120 to T+120 at 15s intervals:

```bash
jq --argjson t $EVENT_EPOCH '
  .data.result[0].values |
  map(select(.[0] >= ($t - 120) and .[0] <= ($t + 120))) |
  map({ t: (.[0] - $t), cpu: (.[1] | tonumber) })
' /tmp/cpu_util_15s.json
```

### Step 4: Compute Scaling Lag from pod_ready

For each event at epoch T with direction UP and target `toReplicas`:

```bash
jq --argjson t $EVENT_EPOCH --argjson target $TO_REPLICAS '
  .data.result[0].values |
  map(select(.[0] >= $t)) |
  map(select((.[1] | tonumber) >= $target)) |
  if length > 0 then .[0][0] - $t else 120 end
' /tmp/pod_ready_15s.json
```

### Step 5: Build workloadTimeline (5-min Buckets)

Group all raw 15s data into 5-min (300s) buckets. Each bucket (20 raw samples):

```
bucket_start = floor(epoch / 300) * 300
time = strftime("%H:%M", bucket_start)

For each bucket:
  replicas     = snapshot of current_replicas at bucket_start
  replicasMin  = min(current_replicas samples in bucket)
  replicasMax  = max(current_replicas samples in bucket)
  cpuPercent   = mean(cpu_util samples in bucket)
  p99Latency   = mean(p99_latency samples in bucket)
  tidbP99Latency = mean(tidb_p99 samples in bucket) * 1000   # seconds -> ms
  events       = join all event descriptions in bucket
  scaleOutCount = count of UP events in bucket
  scaleInCount  = count of DOWN events in bucket
  eventCount   = scaleOutCount + scaleInCount
```

## Template Placeholder

The HTML template contains a single injection point:

```javascript
const DATA = /*__REPORT_DATA__*/{
  // ... sample/default data follows ...
};
```

The `generate-report.sh` script replaces everything from `/*__REPORT_DATA__*/` through
the closing `};` with the contents of the JSON data file. The template includes sample
data so it can be opened directly in a browser for development/testing.

## Sparkline Design (Inline SVG)

Each event row has an inline SVG sparkline showing CPU utilization in a +/-2min window
around the HPA decision point.

**SVG dimensions**: 330 x 68 px

**Visual elements** (drawn in this order):
1. Y-axis labels: "0", "50", "100" at left margin
2. Horizontal grid lines at y=0%, 50%, 100%
3. Scale-up threshold dashed line (red, 60.5%)
4. Scale-down threshold dashed line (blue, 49.5%)
5. Amber lag rectangle from t=0 to t=+scaleLag
6. Dashed gray center line at t=0 (HPA decision point)
7. Filled area (direction-colored at 10% opacity)
8. CPU polyline (direction-colored, 1.5px stroke)
9. Nearby event markers (triangle markers for events within +/-120s)
10. Decision dot at t=0, y=cpuAtEvent (green for UP, red for DOWN)
11. X-axis timestamp labels

**Color mapping**: Scale-out (UP) = `#10b981` (emerald), Scale-in (DOWN) = `#f43f5e` (rose)

## Design Tokens

### Color Semantics

| Meaning | Tailwind |
|---------|----------|
| Good / Valid / Scale-out | `text-emerald-500`, `bg-emerald-500/10` |
| Warning / Marginal / Lag window | `text-amber-500`, `bg-amber-500/10` |
| Critical / Anomaly / Scale-in | `text-rose-500`, `bg-rose-500/10` |
| Info / Neutral | `text-blue-500`, `bg-blue-500/10` |

### Score Color Thresholds

| Score Range | Color | Assessment |
|-------------|-------|------------|
| >= 70 | Emerald | Good |
| 50-69 | Amber | Fair |
| < 50 | Rose | Poor/Critical |

## What NOT to Customize Per Report

- HTML template layout and styling (fixed by design)
- Section order (scrollable, not tabbed)
- Chart types and axes
- Sparkline SVG dimensions and visual element order
- Color semantics

## What DOES Change Per Report

- All values in the DATA object
- Insight box text (derived from data patterns)
- Number of entries in arrays (events, timeline points, recommendations)
- The key finding sentence in the header
- Score values and their rationale/evidence text
