From 027970de29a47e74432baaf4089be2932e46469c Mon Sep 17 00:00:00 2001 From: aaron <> Date: Mon, 18 May 2026 20:07:05 +0800 Subject: [PATCH] 1 --- .env.example | 1 + app/config.py | 2 ++ app/server.py | 43 ++++++++++++++++++++++++++++++++++------ docker-compose.yml | 4 ++-- tests/test_dispatcher.py | 8 ++++++++ 5 files changed, 50 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index 5fc0d9e..e8d8222 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ APP_HOST=0.0.0.0 APP_PORT=8030 DATABASE_PATH=data/dispatcher.db +TZ=Asia/Shanghai ADMIN_USERNAME=admin ADMIN_PASSWORD=12345678 diff --git a/app/config.py b/app/config.py index ac3b2af..d8f685d 100644 --- a/app/config.py +++ b/app/config.py @@ -18,6 +18,7 @@ class Settings: max_delivery_attempts: int = 3 retry_backoff_seconds: int = 60 feishu_timeout_seconds: int = 10 + timezone: str = "" def get_settings() -> Settings: @@ -33,4 +34,5 @@ def get_settings() -> Settings: max_delivery_attempts=int(os.getenv("MAX_DELIVERY_ATTEMPTS", "3")), retry_backoff_seconds=int(os.getenv("RETRY_BACKOFF_SECONDS", "60")), feishu_timeout_seconds=int(os.getenv("FEISHU_TIMEOUT_SECONDS", "10")), + timezone=os.getenv("APP_TIMEZONE") or os.getenv("TZ", ""), ) diff --git a/app/server.py b/app/server.py index 4bee7a0..4cbcfdd 100644 --- a/app/server.py +++ b/app/server.py @@ -6,11 +6,13 @@ import mimetypes import os import urllib.error import urllib.request +from datetime import datetime, timezone from http import HTTPStatus from http.cookies import SimpleCookie from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from typing import Any from urllib.parse import parse_qs, urlparse +from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from app.auth import COOKIE_NAME, check_credentials, hash_password, is_valid_session, make_session_cookie from app.config import Settings, get_settings @@ -148,6 +150,33 @@ def render_pagination(base_path: str, active_tab: str, page: int, total: int, pa ) +def display_timezone(settings: Settings) -> timezone: + if settings.timezone: + try: + return ZoneInfo(settings.timezone) + except ZoneInfoNotFoundError: + return datetime.now().astimezone().tzinfo or timezone.utc + return datetime.now().astimezone().tzinfo or timezone.utc + + +def display_timezone_label(settings: Settings) -> str: + if settings.timezone: + return settings.timezone + return str(display_timezone(settings)) + + +def format_display_time(settings: Settings, value: str | None) -> str: + if not value: + return "" + try: + parsed = datetime.fromisoformat(value) + except ValueError: + return value + if parsed.tzinfo is None: + parsed = parsed.replace(tzinfo=timezone.utc) + return parsed.astimezone(display_timezone(settings)).strftime("%Y-%m-%d %H:%M:%S") + + class Handler(BaseHTTPRequestHandler): context: AppContext @@ -452,7 +481,7 @@ class Handler(BaseHTTPRequestHandler): ("Pending", counts["pending"]), ]) rows = "".join( - f"{row['id']}{html.escape(row['symbol'])}{html.escape(row['timeframe'])}{html.escape(row['strategy'])}{html.escape(row['status'])}{row['created_at']}" + f"{row['id']}{html.escape(row['symbol'])}{html.escape(row['timeframe'])}{html.escape(row['strategy'])}{html.escape(row['status'])}{format_display_time(self.context.settings, row['created_at'])}" for row in recent ) self.send_html("概览", f"

概览

结构化 alert 分发、飞书转发和重试状态。

{webhook_panel}
{cards}

最近 Alert

{rows}
ID品种周期策略状态时间
") @@ -692,6 +721,7 @@ class Handler(BaseHTTPRequestHandler): raw_payload = json.dumps(json.loads(row["payload"]), ensure_ascii=False, indent=2) except Exception: raw_payload = row["payload"] or "" + created_at = format_display_time(self.context.settings, row["created_at"]) alert_rows += f""" {row['id']} {html.escape(row['symbol'])} @@ -699,25 +729,26 @@ class Handler(BaseHTTPRequestHandler): {html.escape(row['strategy'])} {html.escape(row['status'])} {html.escape(row['error'] or '')} -{row['created_at']} +{created_at}
查看
{html.escape(raw_payload)}
""" delivery_rows = "".join( - 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 '')}" + f"{row['id']}{row['alert_id']}{html.escape(row['target_name'])}{html.escape(row['status'])}{row['attempts']}{html.escape(str(row['response_code'] or ''))}{format_display_time(self.context.settings, row['last_attempt_at'])}{html.escape(row['error'] or '')}{format_display_time(self.context.settings, row['next_attempt_at'])}" for row in logs["deliveries"] ) alert_empty = '暂无 Alert 日志' - delivery_empty = '暂无分发日志' + delivery_empty = '暂无分发日志' alert_active = " active" if active_tab == "alerts" else "" delivery_active = " active" if active_tab == "deliveries" else "" active_table = ( f"""{alert_rows or alert_empty}
ID品种周期策略状态错误时间原始 Alert
{render_pagination("/logs", "alerts", page, logs["alert_total"], LOG_PAGE_SIZE)}""" if active_tab == "alerts" - else f"""{delivery_rows or delivery_empty}
IDAlert目标状态次数HTTP错误下次重试
+ else f"""{delivery_rows or delivery_empty}
IDAlert目标状态次数HTTP发送时间错误下次重试
{render_pagination("/logs", "deliveries", page, logs["delivery_total"], LOG_PAGE_SIZE)}""" ) - body = f"""