# Status page

A public /status page and GET /api/status that the instance measures itself: component states, uptime over the days actually measured, the SLA, and incidents.

Liyagent measures its own availability and publishes it at `/status`. This is the platform's own uptime, not the ticket SLAs a service desk keeps.

## What is probed

The leader probes three components every `TICKETIQ_STATUS_PROBE_INTERVAL_S` seconds (default `60`, from 15 to 3600):

| Component | Label | Passes when |
| --- | --- | --- |
| `api` | API and console | The probe is running at all |
| `database` | Database | Postgres answers |
| `vector_store` | Search index | The search index answers |

A fourth series, `platform`, is the check in which every component passed. It is the number the SLA is judged on.

Each component's count of checks run and passed, and the seconds those checks vouch for, is kept per hour.

## Uptime is time, not a count of checks

Each check stands for the seconds since the check before it. Measurement runs from the first check ever (or the start of the look-back) to the last one.

- **A late scheduler is not downtime.** The scheduler runs a job on the first tick after it is due, so a 60-second probe runs every 60 to 65 seconds. A check vouches for the whole gap while it is at most two probe intervals.
- **A missed check is downtime.** After a longer silence, a check vouches for one interval only: the rest was not watched, and a process that is down cannot record that it is down. Once checks have stopped for longer than two intervals, the silence up to now counts as down.
- A check that could not run is a failed check.

## Every figure names the days it covers

The page looks back at most 90 days, but no figure claims more history than exists. Uptime, each component and the SLA each say exactly which days they cover: "since 2026-11-05 (3 days measured)". On a new instance that is a few days, not 90.

- **Daily bars** cover measured days only, each a UTC day's measured seconds against those its passing checks vouch for.
- **Components** show "No data yet" for uptime until their first check, and a component with no probe reads "Not measured yet".
- **The SLA** is judged over `TICKETIQ_STATUS_SLA_WINDOW_DAYS` (default `30`) against `TICKETIQ_STATUS_SLA_TARGET` (default `99.9`). Until the window is fully measured, the page says the figure covers fewer days than the target is judged over. It shows the downtime used against the error budget, the downtime the target allows over the window.

## The public page

`/status` is a server-rendered page with no script, and `GET /api/status` returns the same content as JSON. Both are rate limited and marked `noindex`.

`TICKETIQ_STATUS_PAGE_PUBLIC` (default `true`) decides whether they answer anyone. Set it to `false` and both return 404; the Platform page still shows the instance owner the same and more.

The public view carries states and percentages only: never an error, a host or a version, and not who posted an incident update.

### The JSON

| Field | Meaning |
| --- | --- |
| `status` | `operational`, `degraded`, `outage` or `maintenance`: the worst of the components' states and the open incidents' impacts |
| `checked_at` | When the state was read |
| `components[]` | `id`, `label`, `status` (`operational`, `degraded`, `maintenance`, `down` or `unknown`), `uptime_pct`, `since`, `days_measured` |
| `uptime` | The platform's `pct`, `since`, `days_measured`, `days` (the 90-day look-back), and `daily` bars (`day`, `pct`) for measured days |
| `sla` | `target_pct`, `window_days`, `uptime_pct`, `met`, `budget_minutes`, `down_minutes`, `budget_left_minutes`, `since`, `days_measured`, and `full_window` (whether the whole SLA window has been measured) |
| `incidents` | `active`, and `recent` (resolved in the last 14 days) |

Percentages are `null` for a component never measured.

```bash
curl -s https://<host>/api/status | python3 -m json.tool | grep -A12 '"sla"'
```

## Incidents

Incidents are the operator's to post. Open **Settings** › **Platform** (`/platform`) and choose **Post an incident**:

| Field | Values |
| --- | --- |
| Title | Up to 200 characters |
| Impact | `none`, `minor`, `major`, `critical` or `maintenance` |
| Components | Any of `api`, `database`, `vector_store` |
| Message | The first update (defaults to the title) |

An incident starts as `investigating`. Choose **Post an update** to move it through `identified`, `monitoring` and `resolved`; `resolved` closes it, and a resolved incident takes no more updates (open a new one). An incident holds at most 100 updates.

An open incident with `major` or `critical` impact shows its components as degraded; a `critical` one makes the headline an outage. A `maintenance` incident shows maintenance.

| Task | Endpoint | Permission |
| --- | --- | --- |
| The public status | `GET /api/status`, `/status` | None (when public) |
| The Platform page: the status with who posted each update, plus trace export, tenant isolation, recovery and MFA state | `GET /api/admin/platform` | `admin.write` |
| Post an incident | `POST /api/admin/platform/incidents` | `admin.write` |
| Post an update | `POST /api/admin/platform/incidents/{id}/updates` | `admin.write` |

The platform routes also need a read scope across every tenant. Incidents are audited as `platform.incident.open` and `platform.incident.update`.

## Next steps

- [Crash resume and high availability](/docs/operate/crash-resume-ha)
- [Telemetry](/docs/operate/telemetry)
