fix(alerts,data): startup grace + sustained offline + atomic JSON writes

- 5 min sustained offline window для VPS/PC/Synology/HA (раньше было только у
  Keenetic) + новый STARTUP_GRACE_SEC=300 — первые 5 минут после старта сервиса
  алёрты offline не отправляются, но состояние трекается. Это убирает массовый
  fantom-alert при перезапуске vps-monitoring.
- Атомарная запись JSON-файлов через tempfile+os.replace и явный encoding=utf-8
  везде (data/keenetic.json, pc_agents.json, synology.json, homeassistant.json,
  servers.json, settings.json, metrics.json). Краш в момент записи больше не
  оставляет битый файл с русскими названиями.
- Чистка: убран неиспользуемый passlib + dead bcrypt-импорт в auth.py
  (verify_password всё равно был обычным сравнением); SECRET_KEY можно
  переопределить через VPS_MONITORING_SECRET; убран мертвый import json там,
  где он больше не нужен.
- Bump app.js cache bust до 20260524c.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Андрей Бобырев
2026-05-24 02:30:15 +03:00
parent d950684d95
commit 50b4dac52e
10 changed files with 107 additions and 78 deletions

View File

@@ -1,13 +1,12 @@
"""Home Assistant monitoring API endpoints."""
import json
from datetime import datetime
from typing import Dict
from fastapi import APIRouter, Request, Depends
from server.auth import require_auth
from server.config import DATA_DIR
from server.config import DATA_DIR, load_json, save_json
router = APIRouter(prefix="/api/ha", tags=["homeassistant"])
@@ -18,15 +17,11 @@ HA_FILE = DATA_DIR / "homeassistant.json"
def _load_ha():
if HA_FILE.exists():
with open(HA_FILE) as f:
return json.load(f)
return []
return load_json(HA_FILE, [])
def _save_ha(data):
with open(HA_FILE, "w") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
save_json(HA_FILE, data)
@router.get("/list")

View File

@@ -1,7 +1,6 @@
"""Keenetic router monitoring API endpoints."""
import asyncio
import json
import logging
from datetime import datetime
from typing import Dict, List
@@ -10,7 +9,7 @@ from urllib.parse import urlparse
from fastapi import APIRouter, Request, Depends
from server.auth import require_auth
from server.config import DATA_DIR, load_settings
from server.config import DATA_DIR, load_settings, load_json, save_json
from server.services.keenetic_client import (
KeeneticClient,
normalize_web_url,
@@ -31,15 +30,11 @@ REFRESH_ALL_GAP_SEC = 2
def _load_keenetic():
if KEENETIC_FILE.exists():
with open(KEENETIC_FILE) as f:
return json.load(f)
return []
return load_json(KEENETIC_FILE, [])
def _save_keenetic(data):
with open(KEENETIC_FILE, "w") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
save_json(KEENETIC_FILE, data)
def _host_from_url(url: str) -> str:

View File

@@ -1,11 +1,10 @@
import json
from datetime import datetime
from typing import Dict
from fastapi import APIRouter, Request, Depends
from server.auth import require_auth
from server.config import DATA_DIR
from server.config import DATA_DIR, load_json, save_json
router = APIRouter(prefix="/api/pc", tags=["pc"])
@@ -16,15 +15,11 @@ PC_FILE = DATA_DIR / "pc_agents.json"
def _load_pc_data():
if PC_FILE.exists():
with open(PC_FILE) as f:
return json.load(f)
return {}
return load_json(PC_FILE, {})
def _save_pc_data(data):
with open(PC_FILE, "w") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
save_json(PC_FILE, data)
@router.post("/heartbeat")

View File

@@ -1,16 +1,15 @@
"""Synology NAS monitoring API endpoints."""
import json
import os
from datetime import datetime
from pathlib import Path
from typing import Dict, List
from typing import Dict
from fastapi import APIRouter, Request, Depends
from fastapi.responses import PlainTextResponse
from server.auth import require_auth
from server.config import DATA_DIR, BASE_DIR
from server.config import DATA_DIR, BASE_DIR, load_json, save_json
router = APIRouter(prefix="/api/synology", tags=["synology"])
@@ -25,15 +24,11 @@ SYNOLOGY_FILE = DATA_DIR / "synology.json"
def _load_synology():
if SYNOLOGY_FILE.exists():
with open(SYNOLOGY_FILE) as f:
return json.load(f)
return []
return load_json(SYNOLOGY_FILE, [])
def _save_synology(data):
with open(SYNOLOGY_FILE, "w") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
save_json(SYNOLOGY_FILE, data)
@router.get("/list")