# Health

A health check is a small predicate that decides whether something in your workspace is `ok`, `warn`, or `error`. Each check pairs with a one-keystroke remediation that the user can press from the Health pane to fix it.

The cockpit re-runs health checks on three triggers:

| Trigger    | When                                                                  |
|------------|-----------------------------------------------------------------------|
| `startup`  | once, at boot                                                         |
| `fsevent`  | when the watcher reports a file change (debounced ~500 ms)            |
| `lockfile` | when `composer.lock`, `package-lock.json`, or `yarn.lock` changes     |
| `docker`   | when a docker event is signalled (e.g. by the docker subsystem)       |

Transitions (`ok → error`, `error → ok`) fire OS notifications according to the global `notifications:` block, optionally overridden per-check.

## Built-in check vocabulary

| Type                | Required args        | Optional args                | Default triggers          |
|---------------------|----------------------|------------------------------|---------------------------|
| `container-running` | `container`          |                              | `startup`, `docker`       |
| `port-open`         | `port` (number)      | `host` (default `127.0.0.1`) | `startup`                 |
| `http-ok`           | `url`                | `expectStatus` (default 200) | `startup`                 |
| `file-exists`       | `path`               |                              | `startup`, `fsevent`      |
| `exec-zero`         | `command`            | `args[]`, `cwd`              | `startup`                 |

Defaults can be overridden by setting `triggers:` and `severity:` on the entry.

## Config shape

```yaml
health:
  - id: api-up
    label: API responds
    type: http-ok
    url: http://localhost:3000/health
    expectStatus: 200
    remediation:
      key: A
      label: restart api
      command: docker compose restart api

  - id: db-up
    label: DB reachable
    type: port-open
    port: 5432
    triggers: [startup]
    remediation:
      key: D
      label: start db
      command: docker compose up -d db
    notify:
      onTransitionTo: [health-failed]   # only notify on going-down
```

## Remediations

Each health entry declares one remediation. It can be either:

- **Declarative shell command** — `command: "npm install"` (with optional `cwd`). Stdout/stderr stream to the Output pane tagged `health:<id>`.
- **Programmatic function** — only available to checks contributed by a profile (TypeScript code), via `run(ctx, workspaceRoot)`.

The remediation `key` is a single uppercase letter. Pressing the key (lowercase or upper) on the Health pane runs the remediation; the row shows `… (running…)` until completion, with a minimum 1.5 s banner hold so fast-finishing actions stay visible.

## Notification policy

Per-check `notify:` overrides:

| Setting                                       | Effect                                                |
|-----------------------------------------------|-------------------------------------------------------|
| omitted                                       | inherit global `notifications:` policy                |
| `notify: false`                               | always silent for this check                          |
| `notify: { onTransitionTo: [health-failed] }` | only notify on the listed transition events           |

The session toggle (`n` keystroke) and global `notifications.enabled: false` always silence everything — per-check overrides cannot opt back in.

## Profile-contributed checks

A profile bundles pre-built checks (TypeScript with closures over project-specific helpers) via `Profile.healthChecks`. They register **after** config-driven entries and may use any custom `type` id. **Built-in `type` ids cannot be overridden** by profile checks — the registry rejects them with a warning, so the built-in vocabulary stays predictable.

```ts
import type { Profile } from 'dev-cockpit';

export const myProfile: Profile = {
  appName: 'my-cockpit',
  healthChecks: [
    {
      id: 'composer-installed',
      label: 'Composer',
      severity: 'error',
      triggers: ['startup', 'lockfile'],
      predicate: async (ctx) => { /* … */ },
      remediation: { key: 'R', label: 'composer install', run: async (ctx, root) => { /* … */ } },
    },
  ],
};
```

## Wiring

In your `dev` command, pass `health: { … }` to `runCockpit`:

```ts
import { runCockpit, buildHealthContext } from 'dev-cockpit';

await runCockpit({
  profile: myProfile,
  footerLegends: { /* … */ },
  health: {
    workspaceRoot,
    profileChecks: myProfile.healthChecks,
    configEntries: config.health,
    ctx: buildHealthContext(workspaceRoot, appendOutput),
    notifications: config.notifications,
    appName: 'my-cockpit',
    subscribeFsEvents: (listener) => watcherManager.subscribeFsEvents(listener),
  },
}).waitUntilExit();
```

`useHealth` (called internally by `<Cockpit>`) owns the scheduler lifecycle, transition diffing, notification dispatch, and remediation routing. Consumers do not instantiate `HealthScheduler` directly.
