Commit d192466d authored by Vitaly Lipatov's avatar Vitaly Lipatov

router: add /api/status endpoint with pending changes detection

New GET /api/status returns: - updated: last apply timestamp - pending: true if list files modified since last apply - remaining: seconds until next route-update cycle JS countdown now uses /api/status (lighter than /api/active) and shows different messages depending on whether there are pending changes. Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 1dd25986
......@@ -17,6 +17,7 @@ import re
import socket
import sys
import tempfile
import time
import urllib.parse
LISTEN_HOST = "0.0.0.0"
......@@ -32,6 +33,7 @@ LIST_FILES = {
ALL_ROUTES_JSON = os.path.join(BASEDIR, "all-routes.json")
MAX_ENTRIES = 500
UPDATE_INTERVAL = 300 # route-update.sh cycle, seconds
# domain, IPv4, IPv4/CIDR, IPv6, IPv6/CIDR
DOMAIN_RE = re.compile(
......@@ -151,6 +153,25 @@ OPENAPI_SPEC = {
},
}
},
"/api/status": {
"get": {
"summary": "Статус применения",
"description": "Время последнего применения, секунды до следующего цикла, наличие неприменённых изменений.",
"responses": {
"200": {
"description": "Статус",
"content": {"application/json": {"schema": {
"type": "object",
"properties": {
"updated": {"type": "integer", "nullable": True, "description": "Unix timestamp последнего применения"},
"pending": {"type": "boolean", "description": "Есть ли неприменённые изменения в list-файлах"},
"remaining": {"type": "integer", "nullable": True, "description": "Секунд до следующего цикла route-update"},
},
}}},
}
},
}
},
"/api/active": {
"get": {
"summary": "Действующие правила",
......@@ -390,9 +411,8 @@ $('domain').addEventListener('keydown', e => {
refresh();
(function(){
const INTERVAL = 300;
const el = $('countdown');
let nextUpdate = null;
let remaining = null, pending = false;
function fmt(sec) {
const m = Math.floor(sec / 60), s = sec % 60;
......@@ -400,23 +420,26 @@ refresh();
}
function tick() {
if (!nextUpdate) return;
const left = Math.round((nextUpdate - Date.now()) / 1000);
if (left > 0) {
el.textContent = 'Применение через ' + fmt(left);
if (remaining === null) return;
if (remaining > 0) {
el.textContent = pending
? 'Применение через ' + fmt(remaining)
: 'Нет изменений · следующая проверка через ' + fmt(remaining);
remaining--;
} else {
el.textContent = 'Применение сейчас\u2026';
el.textContent = pending ? 'Применение сейчас\u2026' : 'Проверка\u2026';
setTimeout(poll, 10000);
}
}
function poll() {
fetch('/api/active').then(r => r.json()).then(d => {
fetch('/api/status').then(r => r.json()).then(d => {
if (d.updated) {
nextUpdate = (d.updated + INTERVAL) * 1000;
const ts = new Date(d.updated * 1000);
$('last-update').textContent = 'Обновлено ' + ts.toLocaleString('ru') + ' · ';
}
remaining = d.remaining;
pending = d.pending;
tick();
}).catch(() => {
el.textContent = 'Изменения применятся в течение 5 минут';
......@@ -551,6 +574,25 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
data[mode] = read_list(fpath)
self.send_json(data)
elif path == "/api/status":
try:
updated = int(os.path.getmtime(ALL_ROUTES_JSON))
except FileNotFoundError:
self.send_json({"updated": None, "pending": False, "remaining": None})
return
# pending = any list file modified after last apply
pending = any(
os.path.getmtime(f) > updated
for f in LIST_FILES.values()
if os.path.exists(f)
)
remaining = max(0, updated + UPDATE_INTERVAL - int(time.time()))
self.send_json({
"updated": updated,
"pending": pending,
"remaining": remaining,
})
elif path == "/api/active":
try:
mtime = os.path.getmtime(ALL_ROUTES_JSON)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment