# Monitoring API — Frontend Guide

Интеграция фронта с эндпоинтами мониторинга `badmfck-api-server`. Все эндпоинты живут под префиксом **`sm`**: полный путь `‹baseEndPoint›/sm/‹endpoint›` (обычно `/api/sm/…`).

> **Версия с обязательной 2FA.** Старая авторизация статическим токеном `sha256(login:password)` **удалена**. Доступ только через сессию, полученную в `sm/login`.

---

## 1. Вход, 2FA и сессия

### Шаг 1 — логин

```js
POST sm/login   { login, password }          // + totp, когда 2FA уже подключена
```

Возможны два ответа.

**A. 2FA ещё не подключена** — сессия не выдаётся, `sm/*` недоступен:

```jsonc
{
  "enrollmentRequired": true,
  "enrollmentToken": "…",      // понадобится на шаге 2, живёт 10 минут
  "secret": "JBSWY3DPEHPK3PXP", // для ручного ввода, если нет камеры
  "otpauthUri": "otpauth://totp/‹projectName›:ops?secret=…&issuer=‹projectName›&algorithm=SHA1&digits=6&period=30",
  "expiresInMs": 600000
}
```

**B. 2FA подключена и код верный** — обычная сессия:

```jsonc
{ "token": "…", "login": "ops", "idleTimeoutMs": 900000, "maxLifetimeMs": 28800000,
  "twoFactor": true, "role": "it", "capabilities": ["dashboard","logs"] }
```

### Шаг 2 — подключение 2FA (только при ответе A)

**Нарисуй `otpauthUri` QR-кодом** (любая JS-библиотека QR) и покажи рядом `secret` строкой для ручного ввода. Пользователь сканирует код в Google Authenticator и вводит 6 цифр:

> В аутентификаторе запись подпишется именем проекта — `issuer` берётся из `projectName` на бэке. Поэтому несколько проектов на одном сервере видны в приложении как отдельные строки и не путаются между собой. Задай `projectName` осмысленно: при значении по умолчанию все проекты будут называться одинаково.

```js
POST sm/enroll2fa   { enrollmentToken, totp }
```

Ответ — сразу рабочая сессия (та же форма, что B). Секрет сохраняется на сервере **только после** верного кода.

### Шаг 3 — запросы

```
Authorization: Bearer ‹token из login›
```

### Жизнь сессии

- **Idle-таймаут 15 минут** (`idleTimeoutMs`): каждый успешный запрос продлевает окно. Истекла — ответ `401`, веди на экран логина.
- **Максимум 8 часов** (`maxLifetimeMs`) независимо от активности.
- `POST sm/logout` — отозвать немедленно.
- **10 неудачных попыток → блокировка аккаунта на 30 минут.** Сообщение приходит в `error.details`, покажи его как есть.

### Правила для UI

- Токен держи **в памяти вкладки**, не в localStorage.
- Повторное подключение 2FA через API **невозможно** — это защита от перепривязки чужим человеком, узнавшим пароль. Потерян телефон → администратор удаляет запись в файле 2FA на сервере.
- Пароль короче 12 символов настроить нельзя: вход такой учёткой вернёт `password policy violation` с причиной.

```js
async function sm(endpoint, body = {}, token) {
  const res = await fetch(`/api/sm/${endpoint}`, {
    method: "POST",
    headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) },
    body: JSON.stringify(body),
  });
  const json = await res.json();
  if (json.error) throw Object.assign(new Error(json.error.message), json.error);
  return json.data;
}
```

---

## 2. Роли

У каждой учётки есть роль. Она приходит в ответе логина (`role`, `capabilities`) — **по ней и прячь разделы интерфейса**, не дожидаясь `403`.

| Роль | Дашборд | Логи | Рестарт / уровень логирования |
|---|---|---|---|
| `admin` | ✅ | ✅ | ✅ |
| `it` | ✅ | ✅ | ⛔ |
| `analytic` | ✅ | ⛔ | ⛔ |

Попытка зайти не по правам — `403` с текстом `Role "analytic" is not allowed to access logs endpoints`.

---

## 3. Формат ответа

**Успех:**

```jsonc
{ "data": /* полезная нагрузка */, "responseTime": 4, "core": "5.0.8",
  "endpoint": "/api/sm/metrics", "version": "6.0.7" }
```

**Ошибка:** присутствует поле `error`:

```jsonc
{ "error": { "code": 401, "message": "Unauthorized", "httpStatus": 401, "details": "…" } }
```

Обрабатывай отдельно: `401` → на экран логина; `403` → «нет прав»; остальное → текст из `details`.

---

## 4. Справочник эндпоинтов

| Эндпоинт | Роль | Параметры | Назначение |
|---|---|---|---|
| `login` | — | `login`, `password`, `totp?` | вход, выдача сессии |
| `enroll2fa` | — | `enrollmentToken`, `totp` | подключение 2FA |
| `logout` | любая | — | отозвать сессию |
| `procinfo` | dashboard | — | снимок процесса: память, event-loop, fd, pm2, сокеты |
| `fdreport` | dashboard | — | утечка fd/сокетов: срез + история |
| `httpstats` | dashboard | — | исходящие HTTP по хостам: p50/p95/p99 |
| `sysinfo` | dashboard | — | CPU, RAM, диски, сеть, load |
| `metrics` | dashboard | `range` | история: `hour`/`day`/`week` |
| `metricsWeeks` / `metricsWeek` | dashboard | `weekKey` | архивы с диска |
| `connections` | dashboard | — | активные исходящие соединения |
| `inventory` | dashboard | — | живые сервисы и эндпоинты |
| `db` | dashboard | `range` | healthcheck БД |
| `log` | logs | `lastID?` | логи приложения |
| `netlog` / `outgoingnetlog` | logs | — | входящие / исходящие запросы |
| `logfiles` | logs | — | какие файловые логи доступны |
| `logtail` | logs | `source`, `lines?`, `since?`, `filter?` | хвост файлового лога |
| `threats` | logs | `limit?` | попытки прощупать систему: подозрительные IP и последние события |
| `changeLogLevel` | admin | `level` | сменить уровень логирования |
| `restart` | admin | `confirm`, `password`, `totp?` | перезапуск процесса |

---

## 5. Перезапуск процесса — `restart`

Требует **повторного ввода учётных данных**, даже при живой сессии: это защищает от использования незапертого браузера.

```js
const p = await sm("procinfo", {}, token);
if (!p.identity.pm2.managed) { /* не под pm2 — кнопку скрыть */ }

await sm("restart", {
  confirm: p.identity.pm2.name,   // точное имя процесса в pm2, из procinfo
  password,                       // спросить заново в модальном окне
  totp,                           // и код из приложения
}, token);
// → { restarting:true, name, pmId, by, graceMs }
```

После `restarting:true` покажи «Restarting…», через 3–5 с опрашивай `procinfo`: запросы будут падать, пока процесс поднимается — это ожидаемо. Ответил и `uptimeSec` мал → «Restarted ✓».

---

## 6. Данные: что и как рисовать

### `procinfo`

```jsonc
{ "identity": { "pid":1234, "uptimeSec":84213,
    "pm2": { "managed":true, "name":"gateway-blue", "pmId":"3" } },
  "memory": { "rss":178257920, "heapUsed":74883072, "heapTotal":106086400,
              "external":21430272, "heap": { "sizeLimit":2197815296 } },
  "eventLoop": { "p50Ms":0.9, "p90Ms":1.1, "p99Ms":1.16, "utilizationPct":3.2 },
  "fileDescriptors": { "open":214, "limitSoft":1024, "limitHard":524288 },
  "sockets": { "total":130, "byState": {"ESTABLISHED":40,"CLOSE_WAIT":78}, "topRemotes":[] } }
```

`eventLoop.p99Ms` больше ~50 мс — петля тормозит. `sockets` равен `null` вне Linux.

### `fdreport` — охота за утечкой соединений

```jsonc
{ "now": { "fdOpen":214, "limitSoft":1024,
    "sockets": { "byState": {"CLOSE_WAIT":78}, "topRemotes":[{"host":"17.32.5.7","established":35}] } },
  "sampleEveryMs": 600000,
  "history": [ { "ts":…, "fdOpen":210, "fdSoftLimit":1024, "socketTotal":125,
                 "byState":{…}, "topRemote":{…}, "rss":187654144 } ] }
```

- gauge `fdOpen / limitSoft`: жёлтый от 0.7, красный от 0.9 (`limitSoft === -1` → unlimited, процент не показывать);
- графики по `history`: `fdOpen` с порогом, `socketTotal`, `byState`;
- **`CLOSE_WAIT` монотонно растёт** → приложение не закрывает соединения; один хост растёт в `topRemotes` → зависшие исходящие к нему.

### `httpstats` — p95 по партнёрам

```jsonc
{ "perHost": [ { "host":"https://apple-pay-gateway.apple.com:443", "count":1234,
                 "errors":12, "timeouts":3, "avgMs":210,
                 "p50Ms":180, "p95Ms":1431, "p99Ms":1750, "inFlight":2 } ],
  "topSlow": [ /* топ-20 медленных */ ] }
```

Веди график `p95Ms` по каждому хосту: ползущий вверх p95 — ранний признак деградации, ведущей к таймаутам.

### `metrics` — минутные слоты

```jsonc
[ { "timestamp":1789107000000, "slot": {
    "yyyymmddhhii":202609110910, "cpu":0.26,
    "memory": { "rss":170, "heapUsed":75, "heapTotal":90, "external":20 },   // МБ
    "successEp": {"/api/session/check/:ssid":41}, "failEp": {"/api/apple/pay/:ssid":1},
    "http": {"https://…/startSession":10},
    "http_ok": {"apple-pay-gateway.apple.com:443":9},
    "http_fail": {"apple-pay-gateway.apple.com:443":1},
    "httpP95": {"apple-pay-gateway.apple.com:443":1431},
    "success_sql": {"INSERT INTO …":51}, "fail_sql": {} } } ]
```

`http_ok`/`http_fail`/`httpP95` ключуются по **хосту**, `http` — по url-пути. При укрупнении бакетов счётчики суммируются, `httpP95` берёт **max** (перцентиль усреднять нельзя).

**Долгая история:** `metricsWeeks` → список недель, `metricsWeek {weekKey}` → неделя с диска (живая поминутно, архив почасово). Требует `metricsDir` на бэке.

### Четыре коррелированных графика

Все едут из **одного** запроса `metrics`, общая ось X = `timestamp`:

| # | График | Поле | Как считать |
|---|---|---|---|
| 1 | Входящие OK/FAIL (по endpoint) | `successEp`, `failEp` | сумма значений; без фильтра — total |
| 2 | Исходящие OK/FAIL (по host) | `http_ok`, `http_fail` | то же; список хостов — объединение ключей |
| 3 | Память | `memory` | `rss` и `external` (сигнал утечки) |
| 4 | p95 по host | `httpP95` | нет ключа → **разрыв линии, не ноль** |

Для «все хосты» на графике 4 бери **max**, не сумму.

> Слоты до апгрейда не содержат `http_ok`/`http_fail`/`httpP95` — трактуй пустое как «нет данных».

### `threats` — попытки прощупать систему

Только мониторинг: запросы **не блокируются**. Каждый ответ сервера оценивается по признакам атаки, баллы копятся по IP клиента и со временем затухают (вдвое за 30 минут без новых попыток). Обычный трафик баллов не получает и не хранится.

```js
const t = await sm("threats", { limit: 50 }, token);
```

```jsonc
{
  "settings": { "halfLifeMinutes": 30, "suspiciousAt": 10, "highAt": 30 },
  "totals": { "tracked": 4, "suspicious": 2, "high": 1 },
  "offenders": [                       // по убыванию балла
    { "ip": "6.6.6.6", "score": 66, "level": "high",
      "firstSeen": 1793…, "lastSeen": 1793…, "hits": 12,
      "categories": { "scanner": 6, "not-found": 6 },
      "userAgent": "zgrab/0.x",
      "samples": [ { "ts": 1793…, "method": "GET", "url": "/api/.env", "status": 404, "categories": ["scanner","not-found"] } ] }
  ],
  "recent": [ { "ts": 1793…, "ip": "6.6.6.6", "method": "GET", "url": "/wp-login.php", "status": 404,
                "categories": ["scanner","not-found"], "score": 66 } ]
}
```

**Категории и вес:**

| Категория | Что это | Вес |
|---|---|---|
| `scanner` | пути, которых у Node-API не бывает: `.env`, `.git`, `wp-login`, `phpmyadmin`, `*.php` и т.п. | 10 — хватает одного обращения |
| `enumeration` | 10+ **разных** несуществующих адресов за 5 минут | 10 |
| `injection` | SQL, XSS, обход путей `../`, `${jndi:`, внедрение команд — в URL, а также в теле **упавших** запросов | 8 |
| `method` | `TRACE`, `PROPFIND`, `CONNECT` и подобные | 5 |
| `auth` | 401 / 403 | 2 |
| `not-found` | одиночный 404 | 1 |

**Уровни:** `suspicious` — от 10 баллов, `high` — от 30. Пара 404 от устаревшего фронта уровня не даст; одно обращение к `/.env` — сразу `suspicious`.

**Что рисовать:**
- таблица `offenders`: IP, уровень (жёлтый `suspicious`, красный `high`), балл, категории, первое и последнее обращение, User-Agent, раскрывающийся список `samples`;
- лента `recent` — последние события;
- счётчики из `totals` для бейджа в шапке.

> В `samples` и `recent` хранится только URL (с маскированием секретов), тело запроса не сохраняется никогда. Данные живут в памяти процесса и пропадают при рестарте; у каждого процесса blue-green свои.

### `logfiles` / `logtail` — файловые логи

Источники: `process-out`, `process-err` (логи pm2, определяются автоматически), `nginx-access`, `nginx-error` (пути задаются на бэке). Фронт выбирает только **идентификатор**, путь передать нельзя.

```js
let r = await sm("logtail", { source:"process-out", lines:200 }, token);
r = await sm("logtail", { source:"process-out", since:r.cursor }, token);  // только новое
```

```jsonc
{ "source":"process-out", "path":"…", "size":2068890, "cursor":2068890,
  "rotated":false, "truncated":false, "lines":["INFO server started", …] }
```

- `rotated: true` → файл ротирован, очисти экран;
- `truncated: true` → пропущена середина, покажи разделитель;
- строки **уже замаскированы**: карты, CVV, пароли, токены, JWT. Фильтр работает по замаскированному тексту, поэтому искать по номеру карты нельзя — это намеренно;
- недописанная строка придёт целиком в следующем ответе; за раз читается не больше 512 КБ.

---

## 7. Частота опроса

| Данные | Частота |
|---|---|
| `procinfo`, `connections`, `httpstats`, `metrics`, `sysinfo` | 30–60 с |
| `fdreport` | 30–60 с (история обновляется раз в 10 мин) |
| `logtail` с `since` | 2–5 с, пока вкладка лога открыта |
| `threats` | 30–60 с |
| `metricsWeek`, `logfiles` | по действию пользователя |

Графики должны переживать пустые массивы и `null`; поля, доступные только на Linux (`sockets`), могут отсутствовать.
