1
This commit is contained in:
parent
1fff753260
commit
027970de29
@ -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
|
||||
|
||||
@ -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", ""),
|
||||
)
|
||||
|
||||
@ -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"<tr><td>{row['id']}</td><td>{html.escape(row['symbol'])}</td><td>{html.escape(row['timeframe'])}</td><td>{html.escape(row['strategy'])}</td><td><span class='status'>{html.escape(row['status'])}</span></td><td>{row['created_at']}</td></tr>"
|
||||
f"<tr><td>{row['id']}</td><td>{html.escape(row['symbol'])}</td><td>{html.escape(row['timeframe'])}</td><td>{html.escape(row['strategy'])}</td><td><span class='status'>{html.escape(row['status'])}</span></td><td>{format_display_time(self.context.settings, row['created_at'])}</td></tr>"
|
||||
for row in recent
|
||||
)
|
||||
self.send_html("概览", f"<header><h1>概览</h1><p>结构化 alert 分发、飞书转发和重试状态。</p></header>{webhook_panel}<section class='metrics'>{cards}</section><section><h2>最近 Alert</h2><table><thead><tr><th>ID</th><th>品种</th><th>周期</th><th>策略</th><th>状态</th><th>时间</th></tr></thead><tbody>{rows}</tbody></table></section>")
|
||||
@ -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"""<tr>
|
||||
<td>{row['id']}</td>
|
||||
<td>{html.escape(row['symbol'])}</td>
|
||||
@ -699,25 +729,26 @@ class Handler(BaseHTTPRequestHandler):
|
||||
<td>{html.escape(row['strategy'])}</td>
|
||||
<td><span class='status'>{html.escape(row['status'])}</span></td>
|
||||
<td>{html.escape(row['error'] or '')}</td>
|
||||
<td>{row['created_at']}</td>
|
||||
<td>{created_at}</td>
|
||||
<td><details class="payload-details"><summary>查看</summary><pre>{html.escape(raw_payload)}</pre></details></td>
|
||||
</tr>"""
|
||||
delivery_rows = "".join(
|
||||
f"<tr><td>{row['id']}</td><td>{row['alert_id']}</td><td>{html.escape(row['target_name'])}</td><td><span class='status'>{html.escape(row['status'])}</span></td><td>{row['attempts']}</td><td>{html.escape(str(row['response_code'] or ''))}</td><td>{html.escape(row['error'] or '')}</td><td>{html.escape(row['next_attempt_at'] or '')}</td></tr>"
|
||||
f"<tr><td>{row['id']}</td><td>{row['alert_id']}</td><td>{html.escape(row['target_name'])}</td><td><span class='status'>{html.escape(row['status'])}</span></td><td>{row['attempts']}</td><td>{html.escape(str(row['response_code'] or ''))}</td><td>{format_display_time(self.context.settings, row['last_attempt_at'])}</td><td>{html.escape(row['error'] or '')}</td><td>{format_display_time(self.context.settings, row['next_attempt_at'])}</td></tr>"
|
||||
for row in logs["deliveries"]
|
||||
)
|
||||
alert_empty = '<tr><td colspan="8" class="empty-cell">暂无 Alert 日志</td></tr>'
|
||||
delivery_empty = '<tr><td colspan="8" class="empty-cell">暂无分发日志</td></tr>'
|
||||
delivery_empty = '<tr><td colspan="9" class="empty-cell">暂无分发日志</td></tr>'
|
||||
alert_active = " active" if active_tab == "alerts" else ""
|
||||
delivery_active = " active" if active_tab == "deliveries" else ""
|
||||
active_table = (
|
||||
f"""<table><thead><tr><th>ID</th><th>品种</th><th>周期</th><th>策略</th><th>状态</th><th>错误</th><th>时间</th><th>原始 Alert</th></tr></thead><tbody>{alert_rows or alert_empty}</tbody></table>
|
||||
{render_pagination("/logs", "alerts", page, logs["alert_total"], LOG_PAGE_SIZE)}"""
|
||||
if active_tab == "alerts"
|
||||
else f"""<table><thead><tr><th>ID</th><th>Alert</th><th>目标</th><th>状态</th><th>次数</th><th>HTTP</th><th>错误</th><th>下次重试</th></tr></thead><tbody>{delivery_rows or delivery_empty}</tbody></table>
|
||||
else f"""<table><thead><tr><th>ID</th><th>Alert</th><th>目标</th><th>状态</th><th>次数</th><th>HTTP</th><th>发送时间</th><th>错误</th><th>下次重试</th></tr></thead><tbody>{delivery_rows or delivery_empty}</tbody></table>
|
||||
{render_pagination("/logs", "deliveries", page, logs["delivery_total"], LOG_PAGE_SIZE)}"""
|
||||
)
|
||||
body = f"""<header class="page-header"><div><h1>日志</h1><p>按类型查看 Alert 和分发记录,每页 {LOG_PAGE_SIZE} 条。</p></div>
|
||||
timezone_label = html.escape(display_timezone_label(self.context.settings))
|
||||
body = f"""<header class="page-header"><div><h1>日志</h1><p>按类型查看 Alert 和分发记录,每页 {LOG_PAGE_SIZE} 条。当前显示时区:{timezone_label}。</p></div>
|
||||
<form method="post" action="/deliveries/retry"><button type="submit">处理到期重试</button></form></header>
|
||||
<nav class="tabs" aria-label="日志类型">
|
||||
<a class="tab{alert_active}" href="/logs?tab=alerts&page=1">Alert 日志 <span>{logs["alert_total"]}</span></a>
|
||||
|
||||
@ -4,7 +4,7 @@ services:
|
||||
ports:
|
||||
- "8030:8000"
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
TZ: ${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}
|
||||
@ -19,7 +19,7 @@ services:
|
||||
build: .
|
||||
command: ["python", "-m", "app.worker"]
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
TZ: ${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}
|
||||
|
||||
@ -7,6 +7,7 @@ import unittest
|
||||
from app.config import Settings
|
||||
from app.db import Database, now_iso, to_json
|
||||
from app.dispatcher import Dispatcher, ValidationError, build_feishu_message
|
||||
from app.server import format_display_time
|
||||
|
||||
|
||||
class DispatcherTest(unittest.TestCase):
|
||||
@ -190,6 +191,13 @@ class DispatcherTest(unittest.TestCase):
|
||||
self.assertEqual(message["card"]["header"]["title"]["content"], "TradingView BTCUSDT buy")
|
||||
self.assertEqual(message["card"]["elements"][0]["text"]["content"], "价格 68000")
|
||||
|
||||
def test_display_time_uses_configured_timezone(self) -> None:
|
||||
settings = Settings(timezone="Asia/Shanghai")
|
||||
|
||||
rendered = format_display_time(settings, "2026-05-18T08:20:00+00:00")
|
||||
|
||||
self.assertEqual(rendered, "2026-05-18 16:20:00")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user