# Clinic Health Check Report Sync API

SQL Catalog reads completed Clinic Health Check reports through Clinic's canonical Data Proxy API,
then converts supported SQL check results into the normal Finding intake model. The browser never
receives or stores the Clinic access key.

## Dependencies

Configure the SQL Catalog backend with:

```text
CLINIC_API_KEY=<internal Clinic access key>
CLINIC_ENV=prod
```

The selected Clinic environment must deploy both `internal-api` and `clinic-data-service` with these
canonical routes:

```text
GET /data-proxy/api/v1/orgs/{orgId}/clusters
GET /data-proxy/api/v1/orgs/{orgId}/clusters/{clusterId}/health-check/reports
GET /data-proxy/api/v1/orgs/{orgId}/clusters/{clusterId}/health-check/reports/{reportId}
```

`orgId` is the canonical TiDB Cloud/DBAAS Organization ID resolved from Clinic cluster metadata.

## List Organization Clusters

The Onboarding Organization option resolves its members through:

```http
GET /api/v1/clinic/organizations/{orgId}/clusters?page=1&page_size=200
```

Optional `q` filters clusters and `dbaas_org_id` supplies the canonical Organization ID when the
user-facing Clinic Organization ID differs. Results are cached in the local Cluster Directory so
the saved scope and later Health Check calls retain cluster and Organization metadata.

## List Reports

```http
GET /api/v1/clusters/{clusterId}/health-check/reports?page=1&page_size=20
```

Optional query parameters:

| Field | Type | Description |
| --- | --- | --- |
| `page` | integer | One-based page, default `1` |
| `page_size` | integer | `1` to `100`, default `20` |
| `status` | string, repeatable | Clinic report status |
| `created_from` | RFC3339 string | Inclusive report creation time |
| `created_to` | RFC3339 string | Inclusive report creation time |

The backend resolves the cluster's canonical Organization, reads the Clinic report manifest, and
adds a local `sync` projection to every report. For legacy `running` or `partial` database rows,
Clinic reconciles the report and check-item statuses from object storage using the same rules as
Clinic Portal and safely repairs stale status fields.

```json
{
  "items": [
    {
      "report_id": "00000000-0000-4000-8000-000000000001",
      "org_id": "org-example-001",
      "portal_org_id": "portal-org-example-001",
      "cluster_id": "cluster-example-001",
      "status": "success",
      "created_at": "2026-07-27T07:05:00Z",
      "time_range_a_start": "2026-07-26T07:00:00Z",
      "time_range_a_end": "2026-07-27T07:00:00Z",
      "importable_item_names": [
        "slow_query_hugeResourceUsage",
        "tablefullscan_slow_query",
        "slowlog_top_digest"
      ],
      "sync": {
        "state": "not_synced",
        "finding_count": 0,
        "catalog_created_count": 0
      }
    }
  ],
  "page": 1,
  "page_size": 20,
  "total": 15
}
```

`org_id` remains the canonical TiDB Cloud/DBAAS Organization ID used by Data Proxy. The separate
`portal_org_id` is Clinic's UI Organization ID and is used only to build `source_url`; callers must
not substitute it into canonical Data Proxy paths.

`source_url` follows Clinic Portal's own route contract:
`/portal/#/orgs/{portal_org_id}/clusters/{cluster_id}/health-check/{report_id}?from={start}&to={end}`.
SQL Catalog first uses the report's `portalOrgId`. When a deployed report API does not expose that
field yet, it performs an exact legacy Dashboard lookup by `cluster_id`, verifies that the returned
`tenantID` equals the canonical Data Proxy org ID, and caches the corresponding Dashboard `orgID`.
The legacy lookup supplies only the Portal route identity; report data still comes from Data Proxy.
If neither source can establish a verified mapping, `source_url` remains `null`; SQL Catalog never
substitutes the canonical `org_id` into a Portal URL.

The sync projection also recognizes reports imported by the earlier offline importer through their
existing `Finding Run.source_run_id`, even when no sync-audit row exists.

## Read One Report

```http
GET /api/v1/clusters/{clusterId}/health-check/reports/{reportId}
GET /api/v1/clusters/{clusterId}/health-check/reports/{reportId}?item_name=slowlog_top_digest
```

Repeat `item_name` to request a subset. Omitting it returns all report items exposed by Clinic. This
endpoint is primarily for adapter debugging; the normal UI uses the manifest and server-side sync.

## Import Reports

```http
POST /api/v1/clusters/{clusterId}/health-check/report-syncs
Content-Type: application/json
```

```json
{
  "reports": [
    {
      "report_id": "00000000-0000-4000-8000-000000000001",
      "item_names": [
        "slow_query_hugeResourceUsage",
        "tablefullscan_slow_query",
        "slowlog_top_digest",
        "sql_tikv_high_duration",
        "sql_tikv_high_cpu"
      ]
    }
  ]
}
```

| Field | Required | Rules |
| --- | --- | --- |
| `reports` | yes | Non-empty array, at most 20 reports per request |
| `reports[].report_id` | yes | Clinic report ID |
| `reports[].item_names` | no | Supported SQL checks from the report manifest; omit to read all items |

Only completed `success`, `partial`, or `partial_failed` reports can be imported. Pending,
running, and failed reports remain visible in the list but cannot be selected or imported.

Import is asynchronous. The POST validates the selection, records every report in the durable
database queue, wakes the application worker, and returns HTTP `202` without waiting for Clinic
report reads or Finding writes:

```json
{
  "cluster_id": "cluster-example-001",
  "org_id": "org-example-001",
  "summary": {
    "reports": 1,
    "queued": 1,
    "in_progress": 0
  },
  "items": [
    {
      "report_id": "00000000-0000-4000-8000-000000000001",
      "state": "queued",
      "selected_item_names": ["slowlog_top_digest"]
    }
  ]
}
```

Use the report-list endpoint to observe `sync.state`: `queued`, `syncing`, `succeeded`, `partial`,
or `failed`. The UI polls while any visible report is queued or running. The worker processes one
report at a time to avoid races when reports share SQL Digests. If the application restarts, rows
left in `syncing` are returned to `queued`; replay remains safe because report and Finding IDs are
idempotent.

## Finding Mapping

Supported Clinic SQL checks:

| Clinic item | Finding Symptom |
| --- | --- |
| `slow_query_hugeResourceUsage` | `high_total_latency` |
| `tablefullscan_slow_query` | `large_key_scan` |
| `slowlog_top_digest` | Depends on ranking dimension |
| `sql_tikv_high_duration` | `high_total_latency` |
| `sql_tikv_high_cpu` | `tikv_cpu_hotspot` |

One report produces one Finding per SQL Digest. Multiple check items or ranking dimensions for the
same Digest become multiple Symptoms, metrics, and evidence references on that Finding.

## Idempotency and Time Semantics

- `Finding Run.source_run_id` is the Clinic `reportId`.
- `Finding.source_event_id` is `clinic:healthcheck:{reportId}:sql_digest:{digest}`.
- Repeating a sync returns existing Findings instead of inserting duplicates.
- A previously unseen Digest creates its Catalog Item through the normal Finding intake path.
- `Finding.created_at`, `Finding.ingested_at`, and the Finding Run ingestion time use the report's
  Clinic `createdAt`, not the time the operator clicked **Sync**.
- Observation windows remain the report's `timeRangeAStart` and `timeRangeAEnd`.

These rules preserve the historical Finding feed and Catalog registration sequence while still
recording the operator's current sync attempt in `health_check_report_syncs`.
