From 1fff7532603d9f5969f0a43d0879dce6f91b8c55 Mon Sep 17 00:00:00 2001
From: aaron <>
Date: Mon, 18 May 2026 16:16:40 +0800
Subject: [PATCH] uopdate
---
app/server.py | 115 ++++++++++++++++++++++++++++++++++++++----
app/static/styles.css | 93 ++++++++++++++++++++++++++++++++++
docker-compose.yml | 2 +
3 files changed, 201 insertions(+), 9 deletions(-)
diff --git a/app/server.py b/app/server.py
index 0099a9d..4bee7a0 100644
--- a/app/server.py
+++ b/app/server.py
@@ -18,6 +18,9 @@ from app.db import Database, from_json, now_iso, to_json
from app.dispatcher import Dispatcher, ValidationError, build_feishu_message, normalize_alert
+LOG_PAGE_SIZE = 25
+
+
class AppContext:
def __init__(self, settings: Settings):
self.settings = settings
@@ -104,6 +107,47 @@ def template_picker(templates: list[dict[str, Any]]) -> str:
)
+def parse_positive_int(value: str | None, default: int = 1) -> int:
+ try:
+ parsed = int(value or "")
+ except ValueError:
+ return default
+ return parsed if parsed > 0 else default
+
+
+def page_window(current: int, total_pages: int) -> list[int]:
+ start = max(1, current - 2)
+ end = min(total_pages, current + 2)
+ return list(range(start, end + 1))
+
+
+def render_pagination(base_path: str, active_tab: str, page: int, total: int, page_size: int) -> str:
+ total_pages = max(1, (total + page_size - 1) // page_size)
+ if total_pages <= 1:
+ return f'
'
+
+ def item(label: str, target_page: int, disabled: bool = False, current: bool = False) -> str:
+ if disabled:
+ return f'{html.escape(label)}'
+ active_class = " current" if current else ""
+ return (
+ f'{html.escape(label)}'
+ )
+
+ links = [
+ item("上一页", page - 1, disabled=page <= 1),
+ *(item(str(number), number, current=number == page) for number in page_window(page, total_pages)),
+ item("下一页", page + 1, disabled=page >= total_pages),
+ ]
+ return (
+ ''
+ )
+
+
class Handler(BaseHTTPRequestHandler):
context: AppContext
@@ -146,7 +190,7 @@ class Handler(BaseHTTPRequestHandler):
elif parsed.path == "/rules/delete":
self.render_rule_delete(parsed)
elif parsed.path == "/logs":
- self.render_logs()
+ self.render_logs(parsed)
elif parsed.path == "/test":
self.render_test()
elif parsed.path == "/account":
@@ -156,7 +200,7 @@ class Handler(BaseHTTPRequestHandler):
elif parsed.path == "/api/rules":
json_response(self, 200, self.list_rules())
elif parsed.path == "/api/logs":
- json_response(self, 200, self.list_logs())
+ json_response(self, 200, self.list_recent_logs())
else:
self.send_error(404)
@@ -339,7 +383,36 @@ class Handler(BaseHTTPRequestHandler):
rows = conn.execute("SELECT * FROM message_templates ORDER BY id DESC").fetchall()
return [dict(row) for row in rows]
- def list_logs(self) -> dict[str, list[dict[str, Any]]]:
+ def list_logs(
+ self,
+ active_tab: str = "alerts",
+ page: int = 1,
+ page_size: int = LOG_PAGE_SIZE,
+ ) -> dict[str, Any]:
+ offset = (page - 1) * page_size
+ with self.context.db.connect() as conn:
+ alert_total = conn.execute("SELECT COUNT(*) AS c FROM alerts").fetchone()["c"]
+ delivery_total = conn.execute("SELECT COUNT(*) AS c FROM deliveries").fetchone()["c"]
+ alerts = []
+ deliveries = []
+ if active_tab == "alerts":
+ alerts = conn.execute(
+ "SELECT * FROM alerts ORDER BY id DESC LIMIT ? OFFSET ?",
+ (page_size, offset),
+ ).fetchall()
+ else:
+ deliveries = conn.execute(
+ "SELECT * FROM deliveries ORDER BY id DESC LIMIT ? OFFSET ?",
+ (page_size, offset),
+ ).fetchall()
+ return {
+ "alerts": [dict(row) for row in alerts],
+ "deliveries": [dict(row) for row in deliveries],
+ "alert_total": alert_total,
+ "delivery_total": delivery_total,
+ }
+
+ def list_recent_logs(self) -> dict[str, list[dict[str, Any]]]:
with self.context.db.connect() as conn:
alerts = conn.execute("SELECT * FROM alerts ORDER BY id DESC LIMIT 100").fetchall()
deliveries = conn.execute("SELECT * FROM deliveries ORDER BY id DESC LIMIT 200").fetchall()
@@ -601,8 +674,18 @@ class Handler(BaseHTTPRequestHandler):
"""
self.send_html("删除路由规则", body)
- def render_logs(self) -> None:
- logs = self.list_logs()
+ def render_logs(self, parsed: Any) -> None:
+ query = parse_qs(parsed.query)
+ active_tab = query.get("tab", ["alerts"])[-1]
+ if active_tab not in {"alerts", "deliveries"}:
+ active_tab = "alerts"
+ page = parse_positive_int(query.get("page", ["1"])[-1])
+ total_key = "alert_total" if active_tab == "alerts" else "delivery_total"
+ logs = self.list_logs(active_tab=active_tab, page=page)
+ total_pages = max(1, (logs[total_key] + LOG_PAGE_SIZE - 1) // LOG_PAGE_SIZE)
+ if page > total_pages:
+ page = total_pages
+ logs = self.list_logs(active_tab=active_tab, page=page)
alert_rows = ""
for row in logs["alerts"]:
try:
@@ -623,10 +706,24 @@ class Handler(BaseHTTPRequestHandler):
f"| {row['id']} | {row['alert_id']} | {html.escape(row['target_name'])} | {html.escape(row['status'])} | {row['attempts']} | {html.escape(str(row['response_code'] or ''))} | {html.escape(row['error'] or '')} | {html.escape(row['next_attempt_at'] or '')} |
"
for row in logs["deliveries"]
)
- body = f"""
-
-Alert 日志
| ID | 品种 | 周期 | 策略 | 状态 | 错误 | 时间 | 原始 Alert |
{alert_rows}
-Delivery 日志
| ID | Alert | 目标 | 状态 | 次数 | HTTP | 错误 | 下次重试 |
{delivery_rows}
"""
+ alert_empty = '| 暂无 Alert 日志 |
'
+ delivery_empty = '| 暂无分发日志 |
'
+ alert_active = " active" if active_tab == "alerts" else ""
+ delivery_active = " active" if active_tab == "deliveries" else ""
+ active_table = (
+ f"""| ID | 品种 | 周期 | 策略 | 状态 | 错误 | 时间 | 原始 Alert |
{alert_rows or alert_empty}
+{render_pagination("/logs", "alerts", page, logs["alert_total"], LOG_PAGE_SIZE)}"""
+ if active_tab == "alerts"
+ else f"""| ID | Alert | 目标 | 状态 | 次数 | HTTP | 错误 | 下次重试 |
{delivery_rows or delivery_empty}
+{render_pagination("/logs", "deliveries", page, logs["delivery_total"], LOG_PAGE_SIZE)}"""
+ )
+ body = f"""
+
+"""
self.send_html("日志", body)
def render_test(self) -> None:
diff --git a/app/static/styles.css b/app/static/styles.css
index 4ab3da7..672a7d2 100644
--- a/app/static/styles.css
+++ b/app/static/styles.css
@@ -66,6 +66,49 @@ nav a:hover {
background: rgba(255, 255, 255, 0.09);
}
+.tabs {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin: -4px 0 18px;
+ font-family: ui-sans-serif, system-ui, sans-serif;
+}
+
+.tab {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 14px;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ background: #fffdf8;
+ color: var(--ink);
+ text-decoration: none;
+ font-weight: 800;
+}
+
+.tab:hover,
+.tab.active {
+ border-color: var(--accent);
+ background: #eef8f5;
+ color: var(--accent-strong);
+}
+
+.tab span {
+ min-width: 24px;
+ padding: 2px 7px;
+ border-radius: 999px;
+ background: #ece7da;
+ color: var(--muted);
+ text-align: center;
+ font-size: 12px;
+}
+
+.tab.active span {
+ background: var(--accent);
+ color: #fff;
+}
+
.shell {
position: relative;
margin-left: 236px;
@@ -511,6 +554,56 @@ pre {
margin: 10px 0 0;
}
+.log-panel table {
+ margin-bottom: 12px;
+}
+
+.pagination {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+ margin: 12px 0 28px;
+ font-family: ui-sans-serif, system-ui, sans-serif;
+}
+
+.page-total {
+ margin-right: 6px;
+ color: var(--muted);
+ font-size: 13px;
+}
+
+.page-link {
+ display: inline-flex;
+ align-items: center;
+ min-height: 34px;
+ padding: 7px 11px;
+ border: 1px solid var(--line);
+ border-radius: 6px;
+ background: #fffdf8;
+ color: var(--ink);
+ font-size: 13px;
+ font-weight: 800;
+ text-decoration: none;
+}
+
+.page-link.current {
+ border-color: var(--accent);
+ background: var(--accent);
+ color: #fff;
+}
+
+.page-link.disabled {
+ color: #a9a092;
+ background: #eee9dc;
+}
+
+.empty-cell {
+ padding: 28px;
+ color: var(--muted);
+ text-align: center;
+}
+
.inline {
display: inline;
}
diff --git a/docker-compose.yml b/docker-compose.yml
index 4e7dfc2..6e65b33 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,6 +4,7 @@ services:
ports:
- "8030:8000"
environment:
+ TZ: Asia/Shanghai
ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-change-me-now}
SESSION_SECRET: ${SESSION_SECRET:-replace-with-a-long-random-secret}
@@ -18,6 +19,7 @@ services:
build: .
command: ["python", "-m", "app.worker"]
environment:
+ TZ: Asia/Shanghai
ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-change-me-now}
SESSION_SECRET: ${SESSION_SECRET:-replace-with-a-long-random-secret}