diff --git a/README.md b/README.md index 45dbe11..5d4ce9e 100644 --- a/README.md +++ b/README.md @@ -259,6 +259,9 @@ - `GET /api/v1/shares/{id}/samples?since=…` — история заполнения хранилища - `GET/POST/PATCH/DELETE /api/v1/services[/{id}]` — health-чеки сервисов (CRUD) - `GET /api/v1/services/{id}/samples?since=…` — история проб +- `GET /api/v1/services/{id}/incidents?limit=50` — журнал моментов + недоступности (периоды down+degraded из истории проб; у идущего периода + `end=null`) - `GET/PATCH /api/v1/me` — профиль + язык (GET), override языка (PATCH; только cookie-сессия — у Bearer-админа нет личности) @@ -281,6 +284,15 @@ чипами в карточке). Сырое тело не хранится; известные поля берутся по белому списку. +Общий график «все сервисы» в списке не показывается (масштабы latency не +сравнимы). У каждого сервиса своя страница (`/services/{id}`): персональный +график времени отклика с периодами 1h/6h/24h/7d (down-точки — разрыв линии), +журнал моментов недоступности — периоды бездействия `down` + `degraded` +по истории проб со «сворачиванием» подряд идущих не-up точек (worst-состояние +в периоде, код/причина из первой точки; идущий период — метка +«продолжается»). Там же — редактирование (имя/URL; смена URL даёт +немедленный повторный замер) и удаление сервиса. + ## Язык интерфейса Три языка: en, uk, ru. По умолчанию панель следует языку аккаунта gnexus-auth diff --git a/panel/backend/app/api/services.py b/panel/backend/app/api/services.py index dea3e0f..7b78c9f 100644 --- a/panel/backend/app/api/services.py +++ b/panel/backend/app/api/services.py @@ -118,6 +118,61 @@ return dict(await cursor.fetchone()) | ({"probe": probe} if probe else {}) +@router.get("/services/{service_id}/incidents") +async def service_incidents( + service_id: int, + limit: int = Query(default=50, ge=1, le=500), +) -> list[dict]: + """Журнал моментов недоступности. + + Период = последовательные не-up пробы (down|degraded); закрывается первой + up-пробой. worst = худшее состояние в периоде (down > degraded); причина — + code/message первой точки периода. end NULL = идёт сейчас. + """ + db = get_db() + cursor = await db.execute("SELECT 1 FROM services WHERE id = ?", (service_id,)) + if not await cursor.fetchone(): + raise HTTPException(status_code=404, detail="service not found") + cursor = await db.execute( + "SELECT ts, state, code, message FROM service_samples WHERE service_id = ?" + " ORDER BY ts ASC", + (service_id,), + ) + rows = await cursor.fetchall() + now_s = datetime.now(timezone.utc).timestamp() + + def duration(start: str, end: str) -> float: + span = (datetime.fromisoformat(end) - datetime.fromisoformat(start)).total_seconds() + return max(0.0, round(span, 1)) + + open_period = None + incidents: list[dict] = [] + for row in rows: + if row["state"] == "up": + if open_period is not None: + open_period["end"] = row["ts"] + open_period["duration_s"] = duration(open_period["start"], row["ts"]) + incidents.append(open_period) + open_period = None + elif open_period is None: + open_period = { + "start": row["ts"], + "end": None, + "duration_s": None, + "worst": row["state"], # начало периода: что случилось + "code": row["code"], + "message": row["message"], + } + elif row["state"] == "down" and open_period["worst"] != "down": + open_period["worst"] = "down" # деградация могла углубиться до down + if open_period is not None: # продолжается сейчас + open_period["duration_s"] = round( + max(0.0, now_s - datetime.fromisoformat(open_period["start"]).timestamp()), 1 + ) + incidents.append(open_period) + return list(reversed(incidents))[:limit] + + @router.delete("/services/{service_id}", status_code=204) async def delete_service(service_id: int) -> None: db = get_db() diff --git a/panel/frontend/src/api.js b/panel/frontend/src/api.js index a7f8902..0ff2c72 100644 --- a/panel/frontend/src/api.js +++ b/panel/frontend/src/api.js @@ -80,6 +80,8 @@ params.set('limit', String(limit)) return request(`/services/${id}/samples?${params}`) }, + serviceIncidents: (id, limit = 50) => + request(`/services/${id}/incidents?limit=${limit}`), // OAuth не под /api/v1 — служебные пути панели logout: async () => { return fetch('/auth/logout', { method: 'POST' }) diff --git a/panel/frontend/src/i18n/messages/en.js b/panel/frontend/src/i18n/messages/en.js index 196abd9..556fb7b 100644 --- a/panel/frontend/src/i18n/messages/en.js +++ b/panel/frontend/src/i18n/messages/en.js @@ -132,9 +132,26 @@ "services.serviceAdded": "Service added", "services.historyFailed": "Service history failed to load", "services.serviceDeleted": "Service deleted", + "services.serviceUpdated": "Service updated", "services.deleteTitle": "Delete service?", "services.deleteMessage": "\"{name}\" will be removed from health monitoring. The service itself is not touched.", - "services.historyTitle": "Response time over 24 h, ms", + "services.back": "Back to list", + "services.notFound": "Service not found", + "services.notFoundText": "It may have been removed from monitoring", + "services.edit": "Edit", + "services.editTitle": "Edit service", + "services.chartTitle": "Response time, ms", + "services.logTitle": "Downtime incidents", + "services.colStart": "Start", + "services.colDuration": "Duration", + "services.colWhat": "What", + "services.colReason": "Reason", + "services.ongoing": "ongoing", + "services.noIncidents": "No downtime incidents recorded", + "services.durD": "d", + "services.durH": "h", + "services.durM": "min", + "services.durS": "s", // settings "settings.title": "Settings", diff --git a/panel/frontend/src/i18n/messages/ru.js b/panel/frontend/src/i18n/messages/ru.js index ce33e7d..ce941ac 100644 --- a/panel/frontend/src/i18n/messages/ru.js +++ b/panel/frontend/src/i18n/messages/ru.js @@ -131,9 +131,26 @@ "services.serviceAdded": "Сервис добавлен", "services.historyFailed": "Не удалось загрузить историю сервисов", "services.serviceDeleted": "Сервис удалён", + "services.serviceUpdated": "Сервис обновлён", "services.deleteTitle": "Удалить сервис?", "services.deleteMessage": "«{name}» будет убран из health-мониторинга. Сам сервис не затрагивается.", - "services.historyTitle": "Время отклика за 24 ч, ms", + "services.back": "К списку", + "services.notFound": "Сервис не найден", + "services.notFoundText": "Возможно, он был убран из мониторинга", + "services.edit": "Редактировать", + "services.editTitle": "Редактировать сервис", + "services.chartTitle": "Время отклика, ms", + "services.logTitle": "Моменты недоступности", + "services.colStart": "Начало", + "services.colDuration": "Длительность", + "services.colWhat": "Что", + "services.colReason": "Причина", + "services.ongoing": "продолжается", + "services.noIncidents": "Недоступности не зафиксировано", + "services.durD": "д", + "services.durH": "ч", + "services.durM": "мин", + "services.durS": "с", // settings "settings.title": "Настройки", diff --git a/panel/frontend/src/i18n/messages/uk.js b/panel/frontend/src/i18n/messages/uk.js index 0f8f01c..bb447a5 100644 --- a/panel/frontend/src/i18n/messages/uk.js +++ b/panel/frontend/src/i18n/messages/uk.js @@ -131,9 +131,26 @@ "services.serviceAdded": "Сервіс додано", "services.historyFailed": "Не вдалося завантажити історію сервісів", "services.serviceDeleted": "Сервіс видалено", + "services.serviceUpdated": "Сервіс оновлено", "services.deleteTitle": "Видалити сервіс?", "services.deleteMessage": "«{name}» буде прибрано з health-моніторингу. Сам сервіс не зачіпається.", - "services.historyTitle": "Час відгуку за 24 год, ms", + "services.back": "До списку", + "services.notFound": "Сервіс не знайдено", + "services.notFoundText": "Можливо, його прибрали з моніторингу", + "services.edit": "Редагувати", + "services.editTitle": "Редагувати сервіс", + "services.chartTitle": "Час відгуку, ms", + "services.logTitle": "Моменти недоступності", + "services.colStart": "Початок", + "services.colDuration": "Тривалість", + "services.colWhat": "Що", + "services.colReason": "Причина", + "services.ongoing": "триває", + "services.noIncidents": "Недоступності не зафіксовано", + "services.durD": "д", + "services.durH": "год", + "services.durM": "хв", + "services.durS": "с", // settings "settings.title": "Налаштування", diff --git a/panel/frontend/src/pages/ServicePage.vue b/panel/frontend/src/pages/ServicePage.vue new file mode 100644 index 0000000..665f898 --- /dev/null +++ b/panel/frontend/src/pages/ServicePage.vue @@ -0,0 +1,357 @@ + + + + + \ No newline at end of file diff --git a/panel/frontend/src/pages/ServicesPage.vue b/panel/frontend/src/pages/ServicesPage.vue index b173adb..59d07d5 100644 --- a/panel/frontend/src/pages/ServicesPage.vue +++ b/panel/frontend/src/pages/ServicesPage.vue @@ -1,32 +1,29 @@ - - - - @@ -246,17 +188,6 @@ - - - \ No newline at end of file diff --git a/panel/frontend/src/router.js b/panel/frontend/src/router.js index 8b2cb4d..245511a 100644 --- a/panel/frontend/src/router.js +++ b/panel/frontend/src/router.js @@ -19,6 +19,12 @@ component: () => import('./pages/ServicesPage.vue'), }, { + path: '/services/:id', + name: 'service', + component: () => import('./pages/ServicePage.vue'), + props: true, + }, + { path: '/settings', name: 'settings', component: () => import('./pages/SettingsPage.vue'),