This commit is contained in:
aaron 2026-05-16 15:45:59 +08:00
parent 8e331e6bdf
commit abb70bbe3e
24 changed files with 636 additions and 221 deletions

View File

@ -1,6 +1,7 @@
"""Unified CLI entrypoint for AlphaX Agent Crypto jobs.""" """Unified CLI entrypoint for AlphaX Agent Crypto jobs."""
import argparse import argparse
import sys
from app.services import altcoin_confirm, altcoin_screener, event_driven_screener, onchain_monitor, price_tracker, review_engine, sentiment_monitor from app.services import altcoin_confirm, altcoin_screener, event_driven_screener, onchain_monitor, price_tracker, review_engine, sentiment_monitor
@ -90,4 +91,14 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
try:
main() main()
except Exception as exc:
try:
from app.db.system_logs import record_exception
command = " ".join(sys.argv[1:]) or "unknown"
record_exception(exc, source="cli", context={"argv": sys.argv, "command": command})
except Exception:
pass
raise

View File

@ -712,11 +712,13 @@ def get_admin_stats():
""", (thirty_ago,)).fetchall() """, (thirty_ago,)).fetchall()
today_active = conn.execute(""" today_active = conn.execute("""
SELECT DISTINCT u.id, u.email, u.created_at, u.last_login_at SELECT u.id, u.email, u.created_at, u.last_login_at, MAX(ua.created_at) AS last_activity_at
FROM user_activity ua FROM user_activity ua
JOIN app_user u ON u.id = ua.user_id JOIN app_user u ON u.id = ua.user_id
WHERE ua.created_at LIKE %s WHERE ua.created_at LIKE %s
ORDER BY ua.created_at DESC LIMIT 20 GROUP BY u.id, u.email, u.created_at, u.last_login_at
ORDER BY last_activity_at DESC
LIMIT 20
""", (today + "%",)).fetchall() """, (today + "%",)).fetchall()
conn.close() conn.close()
@ -734,7 +736,7 @@ def get_admin_stats():
"pv_trend": [{"day": r[0], "count": r[1]} for r in pv_trend], "pv_trend": [{"day": r[0], "count": r[1]} for r in pv_trend],
"dau_trend": [{"day": r[0], "count": r[1]} for r in dau_trend], "dau_trend": [{"day": r[0], "count": r[1]} for r in dau_trend],
"today_active": [ "today_active": [
{"id": r[0], "email": r[1], "created_at": r[2], "last_login_at": r[3]} {"id": r[0], "email": r[1], "created_at": r[2], "last_login_at": r[3], "last_activity_at": r[4]}
for r in today_active for r in today_active
], ],
} }

View File

@ -0,0 +1,26 @@
CREATE TABLE IF NOT EXISTS system_error_log (
id BIGSERIAL PRIMARY KEY,
created_at TEXT NOT NULL,
level TEXT DEFAULT 'error',
source TEXT DEFAULT 'app',
error_type TEXT DEFAULT '',
message TEXT DEFAULT '',
stack_trace TEXT DEFAULT '',
request_method TEXT DEFAULT '',
request_path TEXT DEFAULT '',
query_string TEXT DEFAULT '',
user_email TEXT DEFAULT '',
user_id BIGINT DEFAULT 0,
status_code INTEGER DEFAULT 0,
fingerprint TEXT DEFAULT '',
context_json TEXT DEFAULT '{}',
host TEXT DEFAULT '',
pid INTEGER DEFAULT 0,
resolved_at TEXT DEFAULT '',
resolved_by TEXT DEFAULT '',
resolution_note TEXT DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_system_error_log_created ON system_error_log(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_system_error_log_level_source ON system_error_log(level, source, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_system_error_log_fingerprint ON system_error_log(fingerprint, created_at DESC);

209
app/db/system_logs.py Normal file
View File

@ -0,0 +1,209 @@
"""System error log storage."""
from __future__ import annotations
import hashlib
import json
import os
import socket
import traceback
from datetime import datetime, timedelta
from app.db.schema import get_conn
def _now() -> str:
return datetime.now().isoformat()
def _truncate(value, limit: int) -> str:
text = str(value or "")
return text[:limit]
def _json(value) -> str:
try:
return json.dumps(value or {}, ensure_ascii=False, default=str)
except Exception:
return "{}"
def _fingerprint(error_type: str, message: str, stack_trace: str, path: str = "") -> str:
basis = "\n".join([
str(error_type or ""),
str(message or "")[:500],
str(path or ""),
str(stack_trace or "").splitlines()[-12:][0] if stack_trace else "",
])
return hashlib.sha256(basis.encode("utf-8", errors="ignore")).hexdigest()[:32]
def record_system_error(
*,
source: str,
level: str = "error",
message: str = "",
error_type: str = "",
stack_trace: str = "",
request_method: str = "",
request_path: str = "",
query_string: str = "",
user_email: str = "",
user_id: int = 0,
status_code: int = 500,
context: dict | None = None,
fingerprint: str = "",
) -> int:
"""Persist a system error. Logging must never raise into the caller."""
try:
error_type = error_type or "Error"
stack_trace = stack_trace or ""
fingerprint = fingerprint or _fingerprint(error_type, message, stack_trace, request_path)
conn = get_conn()
try:
row = conn.execute(
"""
INSERT INTO system_error_log (
created_at, level, source, error_type, message, stack_trace,
request_method, request_path, query_string, user_email, user_id,
status_code, fingerprint, context_json, host, pid
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id
""",
(
_now(),
_truncate(level or "error", 20),
_truncate(source or "app", 80),
_truncate(error_type, 160),
_truncate(message, 2000),
_truncate(stack_trace, 60000),
_truncate(request_method, 16),
_truncate(request_path, 500),
_truncate(query_string, 1000),
_truncate(user_email, 255),
int(user_id or 0),
int(status_code or 0),
fingerprint,
_json(context),
_truncate(socket.gethostname(), 120),
int(os.getpid() or 0),
),
)
log_id = row.fetchone()["id"]
conn.commit()
return int(log_id)
finally:
conn.close()
except Exception:
return 0
def record_exception(exc: BaseException, *, source: str, context: dict | None = None, **kwargs) -> int:
return record_system_error(
source=source,
error_type=exc.__class__.__name__,
message=str(exc),
stack_trace="".join(traceback.format_exception(type(exc), exc, exc.__traceback__)),
context=context or {},
**kwargs,
)
def list_system_errors(
*,
offset: int = 0,
limit: int = 50,
level: str = "",
source: str = "",
search: str = "",
hours: int = 168,
) -> dict:
limit = max(1, min(int(limit or 50), 200))
offset = max(0, int(offset or 0))
where = []
params = []
if level and level != "all":
where.append("level=%s")
params.append(level)
if source and source != "all":
where.append("source=%s")
params.append(source)
if search:
like = f"%{search.strip()}%"
where.append("(message ILIKE %s OR error_type ILIKE %s OR request_path ILIKE %s OR user_email ILIKE %s)")
params.extend([like, like, like, like])
if hours and int(hours) > 0:
cutoff = (datetime.now() - timedelta(hours=int(hours))).isoformat()
where.append("created_at >= %s")
params.append(cutoff)
clause = "WHERE " + " AND ".join(where) if where else ""
conn = get_conn()
try:
total = conn.execute(f"SELECT COUNT(*) AS n FROM system_error_log {clause}", tuple(params)).fetchone()["n"]
rows = conn.execute(
f"""
SELECT id, created_at, level, source, error_type, message, request_method,
request_path, user_email, user_id, status_code, fingerprint, host, pid
FROM system_error_log
{clause}
ORDER BY id DESC
LIMIT %s OFFSET %s
""",
tuple(params + [limit, offset]),
).fetchall()
return {
"items": [dict(row) for row in rows],
"total": int(total or 0),
"limit": limit,
"offset": offset,
"has_more": offset + limit < int(total or 0),
}
finally:
conn.close()
def get_system_error(log_id: int) -> dict | None:
conn = get_conn()
try:
row = conn.execute("SELECT * FROM system_error_log WHERE id=%s", (int(log_id),)).fetchone()
if not row:
return None
item = dict(row)
try:
item["context"] = json.loads(item.get("context_json") or "{}")
except Exception:
item["context"] = {}
return item
finally:
conn.close()
def get_system_error_stats(hours: int = 24) -> dict:
cutoff = (datetime.now() - timedelta(hours=int(hours or 24))).isoformat()
conn = get_conn()
try:
rows = conn.execute(
"""
SELECT level, source, COUNT(*) AS n
FROM system_error_log
WHERE created_at >= %s
GROUP BY level, source
ORDER BY n DESC
""",
(cutoff,),
).fetchall()
latest = conn.execute(
"""
SELECT created_at, level, source, error_type, message
FROM system_error_log
ORDER BY id DESC LIMIT 1
"""
).fetchone()
return {
"hours": int(hours or 24),
"total": sum(int(row["n"] or 0) for row in rows),
"groups": [dict(row) for row in rows],
"latest": dict(latest) if latest else None,
}
finally:
conn.close()

View File

@ -10,6 +10,7 @@ from app.db.scheduler_db import (
set_job_enabled, set_job_enabled,
set_job_interval, set_job_interval,
) )
from app.db.system_logs import get_system_error, get_system_error_stats, list_system_errors
from app.web.shared import ( from app.web.shared import (
SchedulerIntervalRequest, SchedulerIntervalRequest,
SchedulerToggleRequest, SchedulerToggleRequest,
@ -29,7 +30,7 @@ def build_router(templates):
require_admin(altcoin_session) require_admin(altcoin_session)
except HTTPException as e: except HTTPException as e:
return HTMLResponse(content=f"<meta charset=utf-8><h2>需要管理员权限</h2><p>{e.detail}</p><a href=/app>返回看板</a>", status_code=e.status_code) return HTMLResponse(content=f"<meta charset=utf-8><h2>需要管理员权限</h2><p>{e.detail}</p><a href=/app>返回看板</a>", status_code=e.status_code)
return templates.TemplateResponse(request=request, name="admin.html", context={"show_nav": True}) return templates.TemplateResponse(request=request, name="admin.html", context={"show_nav": True, "active_nav": "admin"})
@router.get("/api/admin/check") @router.get("/api/admin/check")
async def api_admin_check(altcoin_session: str = Cookie(default="")): async def api_admin_check(altcoin_session: str = Cookie(default="")):
@ -54,6 +55,32 @@ def build_router(templates):
require_admin(altcoin_session) require_admin(altcoin_session)
return auth_db.get_admin_orders(search=search, offset=offset, limit=limit, status=status) return auth_db.get_admin_orders(search=search, offset=offset, limit=limit, status=status)
@router.get("/api/admin/system-errors")
async def api_admin_system_errors(
search: str = "",
offset: int = 0,
limit: int = 50,
level: str = "all",
source: str = "all",
hours: int = 168,
altcoin_session: str = Cookie(default=""),
):
require_admin(altcoin_session)
return list_system_errors(search=search, offset=offset, limit=limit, level=level, source=source, hours=hours)
@router.get("/api/admin/system-errors/stats")
async def api_admin_system_error_stats(hours: int = 24, altcoin_session: str = Cookie(default="")):
require_admin(altcoin_session)
return get_system_error_stats(hours=hours)
@router.get("/api/admin/system-errors/{log_id}")
async def api_admin_system_error_detail(log_id: int, altcoin_session: str = Cookie(default="")):
require_admin(altcoin_session)
item = get_system_error(log_id)
if not item:
raise HTTPException(status_code=404, detail="日志不存在")
return item
@router.get("/api/scheduler/jobs") @router.get("/api/scheduler/jobs")
async def api_scheduler_jobs(altcoin_session: str = Cookie(default="")): async def api_scheduler_jobs(altcoin_session: str = Cookie(default="")):
require_admin(altcoin_session) require_admin(altcoin_session)

View File

@ -10,7 +10,7 @@ from app.web.shared import require_admin, require_page_user
def build_router(templates, repo_root: Path, stock_report_template: str): def build_router(templates, repo_root: Path, stock_report_template: str):
router = APIRouter() router = APIRouter()
def render_page(template_name: str, request: Request, **kwargs): def render_page(template_name: str, request: Request, active_nav: str = "", **kwargs):
try: try:
user = auth_db.get_user_by_session_token(request.cookies.get("altcoin_session", "")) user = auth_db.get_user_by_session_token(request.cookies.get("altcoin_session", ""))
if user: if user:
@ -22,7 +22,8 @@ def build_router(templates, repo_root: Path, stock_report_template: str):
) )
except Exception: except Exception:
pass pass
return templates.TemplateResponse(request=request, name=template_name, context={"show_nav": True, **kwargs}) nav = active_nav or template_name.replace(".html", "").replace("-", "_")
return templates.TemplateResponse(request=request, name=template_name, context={"show_nav": True, "active_nav": nav, **kwargs})
@router.get("/", response_class=HTMLResponse) @router.get("/", response_class=HTMLResponse)
async def index(): async def index():
@ -39,21 +40,21 @@ def build_router(templates, repo_root: Path, stock_report_template: str):
user, redirect = require_page_user(request) user, redirect = require_page_user(request)
if redirect: if redirect:
return redirect return redirect
return render_page("watchlist.html", request) return render_page("watchlist.html", request, active_nav="watchlist")
@router.get("/pipeline", response_class=HTMLResponse) @router.get("/pipeline", response_class=HTMLResponse)
async def pipeline_page(request: Request): async def pipeline_page(request: Request):
user, redirect = require_page_user(request) user, redirect = require_page_user(request)
if redirect: if redirect:
return redirect return redirect
return render_page("pipeline.html", request) return render_page("pipeline.html", request, active_nav="pipeline")
@router.get("/llm-insights", response_class=HTMLResponse) @router.get("/llm-insights", response_class=HTMLResponse)
async def llm_insights_page(request: Request): async def llm_insights_page(request: Request):
user, redirect = require_page_user(request) user, redirect = require_page_user(request)
if redirect: if redirect:
return redirect return redirect
return render_page("llm_insights.html", request) return render_page("llm_insights.html", request, active_nav="llm_insights")
@router.get("/cron", response_class=HTMLResponse) @router.get("/cron", response_class=HTMLResponse)
async def cron_page(request: Request): async def cron_page(request: Request):
@ -64,28 +65,39 @@ def build_router(templates, repo_root: Path, stock_report_template: str):
require_admin(request.cookies.get("altcoin_session", "")) require_admin(request.cookies.get("altcoin_session", ""))
except HTTPException as exc: except HTTPException as exc:
return HTMLResponse(content=f"<meta charset=utf-8><h2>需要管理员权限</h2><p>{exc.detail}</p><a href=/app>返回看板</a>", status_code=exc.status_code) return HTMLResponse(content=f"<meta charset=utf-8><h2>需要管理员权限</h2><p>{exc.detail}</p><a href=/app>返回看板</a>", status_code=exc.status_code)
return render_page("cron.html", request) return render_page("cron.html", request, active_nav="cron")
@router.get("/system-logs", response_class=HTMLResponse)
async def system_logs_page(request: Request):
user, redirect = require_page_user(request)
if redirect:
return redirect
try:
require_admin(request.cookies.get("altcoin_session", ""))
except HTTPException as exc:
return HTMLResponse(content=f"<meta charset=utf-8><h2>需要管理员权限</h2><p>{exc.detail}</p><a href=/app>返回看板</a>", status_code=exc.status_code)
return render_page("system_logs.html", request, active_nav="system_logs")
@router.get("/strategy", response_class=HTMLResponse) @router.get("/strategy", response_class=HTMLResponse)
async def strategy_page(request: Request): async def strategy_page(request: Request):
user, redirect = require_page_user(request) user, redirect = require_page_user(request)
if redirect: if redirect:
return redirect return redirect
return render_page("strategy.html", request) return render_page("strategy.html", request, active_nav="strategy")
@router.get("/subscription", response_class=HTMLResponse) @router.get("/subscription", response_class=HTMLResponse)
async def subscription_page(request: Request): async def subscription_page(request: Request):
user, redirect = require_page_user(request, require_subscription=False) user, redirect = require_page_user(request, require_subscription=False)
if redirect: if redirect:
return redirect return redirect
return render_page("subscription.html", request) return render_page("subscription.html", request, active_nav="subscription")
@router.get("/referral", response_class=HTMLResponse) @router.get("/referral", response_class=HTMLResponse)
async def referral_page(request: Request, altcoin_session: str = Cookie(default="")): async def referral_page(request: Request, altcoin_session: str = Cookie(default="")):
user, redirect = require_page_user(request) user, redirect = require_page_user(request)
if redirect: if redirect:
return redirect return redirect
return render_page("referral.html", request) return render_page("referral.html", request, active_nav="referral")
@router.get("/app", response_class=HTMLResponse) @router.get("/app", response_class=HTMLResponse)
async def app_page(altcoin_session: str = Cookie(default=""), request: Request = None): async def app_page(altcoin_session: str = Cookie(default=""), request: Request = None):
@ -96,7 +108,7 @@ def build_router(templates, repo_root: Path, stock_report_template: str):
auth_db.log_user_activity(user["id"], "page_view", "app", ip=request.client.host if request and request.client else "") auth_db.log_user_activity(user["id"], "page_view", "app", ip=request.client.host if request and request.client else "")
except Exception: except Exception:
pass pass
resp = templates.TemplateResponse(request=request, name="app.html", context={"show_nav": True}) resp = templates.TemplateResponse(request=request, name="app.html", context={"show_nav": True, "active_nav": "app"})
resp.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" resp.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
resp.headers["Pragma"] = "no-cache" resp.headers["Pragma"] = "no-cache"
resp.headers["Expires"] = "0" resp.headers["Expires"] = "0"
@ -107,28 +119,28 @@ def build_router(templates, repo_root: Path, stock_report_template: str):
user, redirect = require_page_user(request) user, redirect = require_page_user(request)
if redirect: if redirect:
return redirect return redirect
return render_page("market.html", request) return render_page("market.html", request, active_nav="market")
@router.get("/sentiment", response_class=HTMLResponse) @router.get("/sentiment", response_class=HTMLResponse)
async def sentiment_page(request: Request): async def sentiment_page(request: Request):
user, redirect = require_page_user(request) user, redirect = require_page_user(request)
if redirect: if redirect:
return redirect return redirect
return render_page("sentiment.html", request) return render_page("sentiment.html", request, active_nav="sentiment")
@router.get("/onchain", response_class=HTMLResponse) @router.get("/onchain", response_class=HTMLResponse)
async def onchain_page(request: Request): async def onchain_page(request: Request):
user, redirect = require_page_user(request) user, redirect = require_page_user(request)
if redirect: if redirect:
return redirect return redirect
return render_page("onchain.html", request) return render_page("onchain.html", request, active_nav="onchain")
@router.get("/iteration", response_class=HTMLResponse) @router.get("/iteration", response_class=HTMLResponse)
async def iteration_page(request: Request): async def iteration_page(request: Request):
user, redirect = require_page_user(request) user, redirect = require_page_user(request)
if redirect: if redirect:
return redirect return redirect
return render_page("iteration.html", request) return render_page("iteration.html", request, active_nav="iteration")
@router.get("/stock-report", response_class=HTMLResponse) @router.get("/stock-report", response_class=HTMLResponse)
async def stock_report_page(): async def stock_report_page():

View File

@ -6,10 +6,12 @@ from contextlib import asynccontextmanager
from pathlib import Path from pathlib import Path
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from app.db.schema import init_db from app.db.schema import init_db
from app.db import auth_db from app.db import auth_db
from app.db.system_logs import record_exception
from app.db.analytics import get_all_recommendations, get_cron_run_logs, get_cron_run_summary, get_review_stats, get_stats from app.db.analytics import get_all_recommendations, get_cron_run_logs, get_cron_run_summary, get_review_stats, get_stats
from app.db.recommendation_queries import get_active_recommendations, get_active_recommendations_deduped from app.db.recommendation_queries import get_active_recommendations, get_active_recommendations_deduped
from app.web.routes_admin import build_router as build_admin_router from app.web.routes_admin import build_router as build_admin_router
@ -57,3 +59,30 @@ async def bind_current_request(request: Request, call_next):
return await call_next(request) return await call_next(request)
finally: finally:
current_request.reset(token) current_request.reset(token)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
user = None
try:
user = auth_db.get_user_by_session_token(request.cookies.get("altcoin_session", ""))
except Exception:
user = None
log_id = record_exception(
exc,
source="web",
request_method=request.method,
request_path=request.url.path,
query_string=request.url.query,
user_email=(user or {}).get("email", ""),
user_id=(user or {}).get("id", 0),
status_code=500,
context={
"client": request.client.host if request.client else "",
"user_agent": request.headers.get("user-agent", ""),
},
)
return JSONResponse(
status_code=500,
content={"detail": "系统内部错误", "error_id": log_id},
)

View File

@ -24,6 +24,7 @@ from app.db.scheduler_db import (
update_manual_trigger, update_manual_trigger,
update_runtime, update_runtime,
) )
from app.db.system_logs import record_system_error
PYTHON = sys.executable PYTHON = sys.executable
DRY_RUN = os.getenv("ALPHAX_SCHEDULER_DRY_RUN", "1").strip() not in {"0", "false", "False", "no", "NO"} DRY_RUN = os.getenv("ALPHAX_SCHEDULER_DRY_RUN", "1").strip() not in {"0", "false", "False", "no", "NO"}
@ -224,6 +225,16 @@ def finish_running_jobs(running: dict[str, RunningJob]) -> None:
print(f"[{now_str()}] [scheduler] done {name} exit={exit_code} duration={duration_ms/1000:.1f}s", flush=True) print(f"[{now_str()}] [scheduler] done {name} exit={exit_code} duration={duration_ms/1000:.1f}s", flush=True)
if output_tail: if output_tail:
print(output_tail, flush=True) print(output_tail, flush=True)
if exit_code != 0:
record_system_error(
source="scheduler",
level="error",
error_type=f"{name}_exit_{exit_code}",
message=f"scheduler job {name} failed with exit={exit_code}",
stack_trace=output_tail,
status_code=exit_code,
context={"job_name": name, "run_kind": item.run_kind, "trigger_id": item.trigger_id},
)
update_runtime( update_runtime(
name, name,
status=status, status=status,

View File

@ -407,11 +407,11 @@ event_driven:
note: Solana meme主题扩散 note: Solana meme主题扩散
meta: meta:
version: 1 version: 1
last_review: '2026-05-16T11:57:01.719794' last_review: '2026-05-16T15:25:43.236681'
last_reverse_analysis: '2026-05-16T11:57:32.488216' last_reverse_analysis: '2026-05-16T15:27:11.686080'
total_reviews: 52 total_reviews: 53
total_rules_learned: 37 total_rules_learned: 37
iteration_count: 57 iteration_count: 58
strategy_version: v1.7.11 strategy_version: v1.7.11
strategy_revision_started_at: '2026-05-09T01:20:00' strategy_revision_started_at: '2026-05-09T01:20:00'
strategy_revision_note: 'v1.7.11: 触发时效治理,旧形态只作背景,消息触发显式标记' strategy_revision_note: 'v1.7.11: 触发时效治理,旧形态只作背景,消息触发显式标记'

View File

@ -1,22 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}管理看板 · AlphaX Agent Crypto{% endblock %} {% block title %}管理看板 · AlphaX Agent Crypto{% endblock %}
{% block nav_links %}
<a class="sidebar-link" href="/app"><svg class="link-icon"><use href="#svg-dashboard"/></svg>看板</a>
<a class="sidebar-link" href="/market"><svg class="link-icon"><use href="#svg-target"/></svg>市场总览</a>
<a class="sidebar-link" href="/sentiment"><svg class="link-icon"><use href="#svg-sentiment"/></svg>舆情</a>
<a class="sidebar-link" href="/onchain"><svg class="link-icon"><use href="#svg-onchain"/></svg>链上异动</a>
<a class="sidebar-link" href="/subscription"><svg class="link-icon"><use href="#svg-subscribe"/></svg>订阅</a>
<a class="sidebar-link" href="/referral"><svg class="link-icon"><use href="#svg-referral"/></svg>推荐</a>
<div class="sidebar-section-label admin-link" style="display:none">研发</div>
<a class="sidebar-link admin-link" href="/pipeline" style="display:none"><svg class="link-icon"><use href="#svg-pipeline"/></svg>链路日志</a>
<a class="sidebar-link admin-link" href="/cron" style="display:none"><svg class="link-icon"><use href="#svg-cron"/></svg>调度中心</a>
<a class="sidebar-link admin-link" href="/llm-insights" style="display:none"><svg class="link-icon"><use href="#svg-ai"/></svg>AI 记录</a>
<a class="sidebar-link admin-link" href="/strategy" style="display:none"><svg class="link-icon"><use href="#svg-target"/></svg>策略</a>
<a class="sidebar-link admin-link" href="/iteration" style="display:none"><svg class="link-icon"><use href="#svg-iterate"/></svg>迭代</a>
<a class="sidebar-link active admin-link" href="/admin.html" style="display:none"><svg class="link-icon"><use href="#svg-admin"/></svg>管理</a>
{% endblock %}
{% block extra_head_css %} {% block extra_head_css %}
<style> <style>
main { max-width: 1280px; margin: 0 auto; width: 100%; padding: 24px; display: flex; flex-direction: column; gap: 24px; } main { max-width: 1280px; margin: 0 auto; width: 100%; padding: 24px; display: flex; flex-direction: column; gap: 24px; }
@ -139,6 +123,7 @@ tr:hover td { background: var(--surface); }
<div class="pagination" id="orderPagination" style="padding-bottom:16px"></div> <div class="pagination" id="orderPagination" style="padding-bottom:16px"></div>
</div> </div>
</div> </div>
</main> </main>
{% endblock %} {% endblock %}
@ -291,6 +276,7 @@ function shortText(s,n){s=String(s||'');return s.length>n?s.slice(0,n)+'…':s}
function esc(s){return String(s||'').replace(/[&<>"]/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]})} function esc(s){return String(s||'').replace(/[&<>"]/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]})}
function fmtDate(ts){if(!ts)return'—';var m=String(ts).match(/^(\d{4})-(\d{2})-(\d{2})/);return m?m[2]+'/'+m[3]:ts.slice(0,10)} function fmtDate(ts){if(!ts)return'—';var m=String(ts).match(/^(\d{4})-(\d{2})-(\d{2})/);return m?m[2]+'/'+m[3]:ts.slice(0,10)}
function fmtDateTime(ts){if(!ts)return'—';var d=new Date(ts);if(isNaN(d.getTime()))return String(ts).slice(0,19).replace('T',' ');return (d.getMonth()+1)+'/'+d.getDate()+' '+('0'+d.getHours()).slice(-2)+':'+('0'+d.getMinutes()).slice(-2)+':'+('0'+d.getSeconds()).slice(-2)}
init(); init();
</script> </script>

View File

@ -171,21 +171,20 @@ a { color: inherit; text-decoration: none; }
<span class="brand-name">AlphaX Agent Crypto</span> <span class="brand-name">AlphaX Agent Crypto</span>
</a> </a>
<nav class="sidebar-nav"> <nav class="sidebar-nav">
{% block nav_links %} <a class="sidebar-link {% if active_nav | default('app') == 'app' %}active{% endif %}" href="/app"><svg class="link-icon"><use href="#svg-dashboard"/></svg>看板</a>
<a class="sidebar-link active" href="/app"><svg class="link-icon"><use href="#svg-dashboard"/></svg>看板</a> <a class="sidebar-link {% if active_nav == 'market' %}active{% endif %}" href="/market"><svg class="link-icon"><use href="#svg-target"/></svg>市场总览</a>
<a class="sidebar-link" href="/market"><svg class="link-icon"><use href="#svg-target"/></svg>市场总览</a> <a class="sidebar-link {% if active_nav == 'sentiment' %}active{% endif %}" href="/sentiment"><svg class="link-icon"><use href="#svg-sentiment"/></svg>舆情</a>
<a class="sidebar-link" href="/sentiment"><svg class="link-icon"><use href="#svg-sentiment"/></svg>舆情</a> <a class="sidebar-link {% if active_nav == 'onchain' %}active{% endif %}" href="/onchain"><svg class="link-icon"><use href="#svg-onchain"/></svg>链上异动</a>
<a class="sidebar-link" href="/onchain"><svg class="link-icon"><use href="#svg-onchain"/></svg>链上异动</a> <a class="sidebar-link {% if active_nav == 'subscription' %}active{% endif %}" href="/subscription"><svg class="link-icon"><use href="#svg-subscribe"/></svg>订阅</a>
<a class="sidebar-link" href="/subscription"><svg class="link-icon"><use href="#svg-subscribe"/></svg>订阅</a> <a class="sidebar-link {% if active_nav == 'referral' %}active{% endif %}" href="/referral"><svg class="link-icon"><use href="#svg-referral"/></svg>推荐</a>
<a class="sidebar-link" href="/referral"><svg class="link-icon"><use href="#svg-referral"/></svg>推荐</a>
<div class="sidebar-section-label admin-link" style="display:none">研发</div> <div class="sidebar-section-label admin-link" style="display:none">研发</div>
<a class="sidebar-link admin-link" href="/pipeline" style="display:none"><svg class="link-icon"><use href="#svg-pipeline"/></svg>链路日志</a> <a class="sidebar-link admin-link {% if active_nav == 'pipeline' %}active{% endif %}" href="/pipeline" style="display:none"><svg class="link-icon"><use href="#svg-pipeline"/></svg>链路日志</a>
<a class="sidebar-link admin-link" href="/cron" style="display:none"><svg class="link-icon"><use href="#svg-cron"/></svg>调度中心</a> <a class="sidebar-link admin-link {% if active_nav == 'cron' %}active{% endif %}" href="/cron" style="display:none"><svg class="link-icon"><use href="#svg-cron"/></svg>调度中心</a>
<a class="sidebar-link admin-link" href="/llm-insights" style="display:none"><svg class="link-icon"><use href="#svg-ai"/></svg>AI 记录</a> <a class="sidebar-link admin-link {% if active_nav == 'llm_insights' %}active{% endif %}" href="/llm-insights" style="display:none"><svg class="link-icon"><use href="#svg-ai"/></svg>AI 记录</a>
<a class="sidebar-link admin-link" href="/strategy" style="display:none"><svg class="link-icon"><use href="#svg-target"/></svg>策略</a> <a class="sidebar-link admin-link {% if active_nav == 'system_logs' %}active{% endif %}" href="/system-logs" style="display:none"><svg class="link-icon"><use href="#svg-admin"/></svg>系统日志</a>
<a class="sidebar-link admin-link" href="/iteration" style="display:none"><svg class="link-icon"><use href="#svg-iterate"/></svg>迭代</a> <a class="sidebar-link admin-link {% if active_nav == 'strategy' %}active{% endif %}" href="/strategy" style="display:none"><svg class="link-icon"><use href="#svg-target"/></svg>策略</a>
<a class="sidebar-link admin-link" href="/admin.html" style="display:none"><svg class="link-icon"><use href="#svg-admin"/></svg>管理</a> <a class="sidebar-link admin-link {% if active_nav == 'iteration' %}active{% endif %}" href="/iteration" style="display:none"><svg class="link-icon"><use href="#svg-iterate"/></svg>迭代</a>
{% endblock %} <a class="sidebar-link admin-link {% if active_nav == 'admin' %}active{% endif %}" href="/admin.html" style="display:none"><svg class="link-icon"><use href="#svg-admin"/></svg>管理</a>
</nav> </nav>
<div class="sidebar-user" onclick="toggleUserMenu()"> <div class="sidebar-user" onclick="toggleUserMenu()">
<span class="user-avatar" id="userInitial">?</span> <span class="user-avatar" id="userInitial">?</span>

View File

@ -1,20 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}AlphaX Agent Crypto — 调度中心{% endblock %} {% block title %}AlphaX Agent Crypto — 调度中心{% endblock %}
{% block nav_links %}
<a class="sidebar-link" href="/app"><svg class="link-icon"><use href="#svg-dashboard"/></svg>看板</a>
<a class="sidebar-link" href="/market"><svg class="link-icon"><use href="#svg-target"/></svg>市场总览</a>
<a class="sidebar-link" href="/sentiment"><svg class="link-icon"><use href="#svg-sentiment"/></svg>舆情</a>
<a class="sidebar-link" href="/onchain"><svg class="link-icon"><use href="#svg-onchain"/></svg>链上异动</a>
<a class="sidebar-link" href="/subscription"><svg class="link-icon"><use href="#svg-subscribe"/></svg>订阅</a>
<a class="sidebar-link" href="/referral"><svg class="link-icon"><use href="#svg-referral"/></svg>推荐</a>
<div class="sidebar-section-label admin-link" style="display:none">研发</div>
<a class="sidebar-link admin-link" href="/pipeline" style="display:none"><svg class="link-icon"><use href="#svg-pipeline"/></svg>链路日志</a>
<a class="sidebar-link active admin-link" href="/cron" style="display:none"><svg class="link-icon"><use href="#svg-cron"/></svg>调度中心</a>
<a class="sidebar-link admin-link" href="/llm-insights" style="display:none"><svg class="link-icon"><use href="#svg-ai"/></svg>AI 记录</a>
<a class="sidebar-link admin-link" href="/strategy" style="display:none"><svg class="link-icon"><use href="#svg-target"/></svg>策略</a>
<a class="sidebar-link admin-link" href="/iteration" style="display:none"><svg class="link-icon"><use href="#svg-iterate"/></svg>迭代</a>
<a class="sidebar-link admin-link" href="/admin.html" style="display:none"><svg class="link-icon"><use href="#svg-admin"/></svg>管理</a>
{% endblock %}
{% block extra_head_css %} {% block extra_head_css %}
<style> <style>
.shell{width:min(100% - 40px,1280px);margin:0 auto;padding:24px 0 44px}.page-head{display:flex;justify-content:space-between;align-items:flex-end;gap:14px;flex-wrap:wrap;margin-bottom:16px}.page-head h1{font-size:26px;font-weight:900;color:var(--ink)}.page-head p{font-size:13px;color:var(--stone);margin-top:4px}.actions{display:flex;gap:8px;align-items:center}.btn{height:36px;border:1px solid var(--hairline-strong);background:var(--canvas);border-radius:var(--radius-md);padding:0 12px;font-size:12px;font-weight:900;color:var(--ink);cursor:pointer}.btn.primary{background:var(--primary);border-color:var(--primary);color:var(--on-primary)}.btn.warn{color:var(--red)}.btn:disabled{opacity:.45;cursor:default}.grid{display:grid;grid-template-columns:1fr;gap:14px}.panel{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-md);overflow:hidden}.panel-head{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:13px 14px;border-bottom:1px solid var(--hairline-soft)}.panel-title{font-size:14px;font-weight:900;color:var(--ink)}.panel-note{font-size:11px;font-weight:800;color:var(--stone)}.job-table{width:100%;border-collapse:collapse;min-width:1040px}.job-table th,.job-table td{padding:11px 10px;border-bottom:1px solid var(--hairline-soft);text-align:left;font-size:12px;vertical-align:middle}.job-table th{font-size:11px;color:var(--stone);font-weight:900;background:var(--surface)}.job-table tr:last-child td{border-bottom:0}.table-wrap{overflow:auto}.job-name{font-weight:900;color:var(--ink);font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.desc{color:var(--stone);font-size:11px;margin-top:3px}.badge{display:inline-flex;align-items:center;height:24px;border-radius:999px;padding:0 8px;font-size:11px;font-weight:900;border:1px solid var(--hairline-soft);background:var(--surface);color:var(--slate);white-space:nowrap}.badge.ok{background:var(--green-light);border-color:rgba(0,180,115,.18);color:var(--green)}.badge.err{background:var(--red-light);border-color:rgba(229,62,62,.18);color:var(--red)}.badge.run{background:rgba(66,98,255,.08);border-color:rgba(66,98,255,.18);color:var(--blue)}.badge.wait{background:rgba(245,158,11,.1);border-color:rgba(245,158,11,.22);color:#b7791f}.interval{width:82px;height:34px;border:1px solid var(--hairline-strong);border-radius:var(--radius-md);padding:0 8px;font-size:12px;font-weight:800;background:var(--canvas);color:var(--ink)}.mini{font-size:11px;color:var(--stone);line-height:1.5}.switch{display:inline-flex;align-items:center;gap:7px}.switch input{width:34px;height:20px;appearance:none;border-radius:999px;background:var(--hairline);position:relative;cursor:pointer}.switch input:checked{background:var(--green)}.switch input:before{content:"";position:absolute;top:3px;left:3px;width:14px;height:14px;border-radius:50%;background:white;transition:.15s}.switch input:checked:before{left:17px}.trigger-list{display:grid;gap:8px;padding:12px}.trigger{display:grid;grid-template-columns:140px 90px 1fr auto;gap:10px;align-items:center;border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);padding:10px}.out{color:var(--slate);font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.empty,.loading{padding:28px 16px;text-align:center;color:var(--stone);font-size:13px}.toast{position:fixed;right:18px;bottom:18px;background:var(--ink);color:white;border-radius:var(--radius-md);padding:10px 12px;font-size:12px;font-weight:800;opacity:0;transform:translateY(8px);transition:.18s;z-index:20}.toast.show{opacity:1;transform:translateY(0)}@media(max-width:760px){.shell{width:min(100% - 24px,1280px)}.trigger{grid-template-columns:1fr}.actions{width:100%;justify-content:flex-start}} .shell{width:min(100% - 40px,1280px);margin:0 auto;padding:24px 0 44px}.page-head{display:flex;justify-content:space-between;align-items:flex-end;gap:14px;flex-wrap:wrap;margin-bottom:16px}.page-head h1{font-size:26px;font-weight:900;color:var(--ink)}.page-head p{font-size:13px;color:var(--stone);margin-top:4px}.actions{display:flex;gap:8px;align-items:center}.btn{height:36px;border:1px solid var(--hairline-strong);background:var(--canvas);border-radius:var(--radius-md);padding:0 12px;font-size:12px;font-weight:900;color:var(--ink);cursor:pointer}.btn.primary{background:var(--primary);border-color:var(--primary);color:var(--on-primary)}.btn.warn{color:var(--red)}.btn:disabled{opacity:.45;cursor:default}.grid{display:grid;grid-template-columns:1fr;gap:14px}.panel{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-md);overflow:hidden}.panel-head{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:13px 14px;border-bottom:1px solid var(--hairline-soft)}.panel-title{font-size:14px;font-weight:900;color:var(--ink)}.panel-note{font-size:11px;font-weight:800;color:var(--stone)}.job-table{width:100%;border-collapse:collapse;min-width:1040px}.job-table th,.job-table td{padding:11px 10px;border-bottom:1px solid var(--hairline-soft);text-align:left;font-size:12px;vertical-align:middle}.job-table th{font-size:11px;color:var(--stone);font-weight:900;background:var(--surface)}.job-table tr:last-child td{border-bottom:0}.table-wrap{overflow:auto}.job-name{font-weight:900;color:var(--ink);font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.desc{color:var(--stone);font-size:11px;margin-top:3px}.badge{display:inline-flex;align-items:center;height:24px;border-radius:999px;padding:0 8px;font-size:11px;font-weight:900;border:1px solid var(--hairline-soft);background:var(--surface);color:var(--slate);white-space:nowrap}.badge.ok{background:var(--green-light);border-color:rgba(0,180,115,.18);color:var(--green)}.badge.err{background:var(--red-light);border-color:rgba(229,62,62,.18);color:var(--red)}.badge.run{background:rgba(66,98,255,.08);border-color:rgba(66,98,255,.18);color:var(--blue)}.badge.wait{background:rgba(245,158,11,.1);border-color:rgba(245,158,11,.22);color:#b7791f}.interval{width:82px;height:34px;border:1px solid var(--hairline-strong);border-radius:var(--radius-md);padding:0 8px;font-size:12px;font-weight:800;background:var(--canvas);color:var(--ink)}.mini{font-size:11px;color:var(--stone);line-height:1.5}.switch{display:inline-flex;align-items:center;gap:7px}.switch input{width:34px;height:20px;appearance:none;border-radius:999px;background:var(--hairline);position:relative;cursor:pointer}.switch input:checked{background:var(--green)}.switch input:before{content:"";position:absolute;top:3px;left:3px;width:14px;height:14px;border-radius:50%;background:white;transition:.15s}.switch input:checked:before{left:17px}.trigger-list{display:grid;gap:8px;padding:12px}.trigger{display:grid;grid-template-columns:140px 90px 1fr auto;gap:10px;align-items:center;border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);padding:10px}.out{color:var(--slate);font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.empty,.loading{padding:28px 16px;text-align:center;color:var(--stone);font-size:13px}.toast{position:fixed;right:18px;bottom:18px;background:var(--ink);color:white;border-radius:var(--radius-md);padding:10px 12px;font-size:12px;font-weight:800;opacity:0;transform:translateY(8px);transition:.18s;z-index:20}.toast.show{opacity:1;transform:translateY(0)}@media(max-width:760px){.shell{width:min(100% - 24px,1280px)}.trigger{grid-template-columns:1fr}.actions{width:100%;justify-content:flex-start}}

View File

@ -1,21 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}AlphaX Agent Crypto — 策略进化{% endblock %} {% block title %}AlphaX Agent Crypto — 策略进化{% endblock %}
{% block nav_links %}
<a class="sidebar-link" href="/app"><svg class="link-icon"><use href="#svg-dashboard"/></svg>看板</a>
<a class="sidebar-link" href="/market"><svg class="link-icon"><use href="#svg-target"/></svg>市场总览</a>
<a class="sidebar-link" href="/sentiment"><svg class="link-icon"><use href="#svg-sentiment"/></svg>舆情</a>
<a class="sidebar-link" href="/onchain"><svg class="link-icon"><use href="#svg-onchain"/></svg>链上异动</a>
<a class="sidebar-link" href="/subscription"><svg class="link-icon"><use href="#svg-subscribe"/></svg>订阅</a>
<a class="sidebar-link" href="/referral"><svg class="link-icon"><use href="#svg-referral"/></svg>推荐</a>
<div class="sidebar-section-label admin-link" style="display:none">研发</div>
<a class="sidebar-link admin-link" href="/pipeline" style="display:none"><svg class="link-icon"><use href="#svg-pipeline"/></svg>链路日志</a>
<a class="sidebar-link admin-link" href="/cron" style="display:none"><svg class="link-icon"><use href="#svg-cron"/></svg>调度中心</a>
<a class="sidebar-link admin-link" href="/llm-insights" style="display:none"><svg class="link-icon"><use href="#svg-ai"/></svg>AI 记录</a>
<a class="sidebar-link admin-link" href="/strategy" style="display:none"><svg class="link-icon"><use href="#svg-target"/></svg>策略</a>
<a class="sidebar-link active admin-link" href="/iteration" style="display:none"><svg class="link-icon"><use href="#svg-iterate"/></svg>迭代</a>
<a class="sidebar-link admin-link" href="/admin.html" style="display:none"><svg class="link-icon"><use href="#svg-admin"/></svg>管理</a>
{% endblock %}
{% block extra_head_css %} {% block extra_head_css %}
<style> <style>
.shell { width:min(100% - 40px,1180px); margin:0 auto; padding:28px 0 48px; } .shell { width:min(100% - 40px,1180px); margin:0 auto; padding:28px 0 48px; }

View File

@ -1,20 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}AlphaX Agent Crypto — AI 记录{% endblock %} {% block title %}AlphaX Agent Crypto — AI 记录{% endblock %}
{% block nav_links %}
<a class="sidebar-link" href="/app"><svg class="link-icon"><use href="#svg-dashboard"/></svg>看板</a>
<a class="sidebar-link" href="/market"><svg class="link-icon"><use href="#svg-target"/></svg>市场总览</a>
<a class="sidebar-link" href="/sentiment"><svg class="link-icon"><use href="#svg-sentiment"/></svg>舆情</a>
<a class="sidebar-link" href="/onchain"><svg class="link-icon"><use href="#svg-onchain"/></svg>链上异动</a>
<a class="sidebar-link" href="/subscription"><svg class="link-icon"><use href="#svg-subscribe"/></svg>订阅</a>
<a class="sidebar-link" href="/referral"><svg class="link-icon"><use href="#svg-referral"/></svg>推荐</a>
<div class="sidebar-section-label admin-link" style="display:none">研发</div>
<a class="sidebar-link admin-link" href="/pipeline" style="display:none"><svg class="link-icon"><use href="#svg-pipeline"/></svg>链路日志</a>
<a class="sidebar-link admin-link" href="/cron" style="display:none"><svg class="link-icon"><use href="#svg-cron"/></svg>调度中心</a>
<a class="sidebar-link active admin-link" href="/llm-insights" style="display:none"><svg class="link-icon"><use href="#svg-ai"/></svg>AI 记录</a>
<a class="sidebar-link admin-link" href="/strategy" style="display:none"><svg class="link-icon"><use href="#svg-target"/></svg>策略</a>
<a class="sidebar-link admin-link" href="/iteration" style="display:none"><svg class="link-icon"><use href="#svg-iterate"/></svg>迭代</a>
<a class="sidebar-link admin-link" href="/admin.html" style="display:none"><svg class="link-icon"><use href="#svg-admin"/></svg>管理</a>
{% endblock %}
{% block extra_head_css %} {% block extra_head_css %}
<style> <style>
.shell{width:min(100% - 40px,1280px);margin:0 auto;padding:24px 0 44px}.page-head{display:flex;justify-content:space-between;align-items:flex-end;gap:14px;margin-bottom:16px;flex-wrap:wrap}.page-head h1{font-size:26px;font-weight:900;letter-spacing:-.6px;color:var(--ink)}.page-head p{margin-top:4px;color:var(--stone);font-size:13px;line-height:1.6}.controls{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.select,.btn{height:38px;border:1px solid var(--hairline-strong);background:var(--canvas);border-radius:var(--radius-md);padding:0 12px;font-size:13px;font-weight:800;color:var(--ink)}.btn{cursor:pointer}.layout{display:grid;grid-template-columns:430px minmax(0,1fr);gap:14px;align-items:start}.panel{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-md);min-width:0;overflow:hidden}.panel-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:13px 14px;border-bottom:1px solid var(--hairline-soft)}.panel-title{font-size:14px;font-weight:900;color:var(--ink)}.panel-note{font-size:11px;color:var(--stone);font-weight:800}.list{max-height:calc(100vh - 178px);overflow:auto}.row{padding:13px 14px;border-bottom:1px solid var(--hairline-soft);cursor:pointer;transition:.12s;background:var(--canvas)}.row:hover{background:var(--surface)}.row.active{background:rgba(66,98,255,.06);box-shadow:inset 3px 0 0 var(--blue)}.row-top{display:flex;align-items:center;justify-content:space-between;gap:8px}.type{font-size:12px;font-weight:900;color:var(--blue);background:rgba(66,98,255,.08);border-radius:999px;padding:4px 8px;white-space:nowrap}.type.failed{color:var(--red);background:var(--red-light)}.type.skipped{color:var(--stone);background:var(--surface);border:1px solid var(--hairline-soft)}.subject{margin-top:8px;font-size:14px;font-weight:900;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.summary{margin-top:5px;color:var(--slate);font-size:12px;line-height:1.55;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:8px;color:var(--stone);font-size:11px;font-weight:800}.detail{min-height:560px}.detail-body{padding:16px}.hero-card{border:1px solid var(--hairline-soft);border-radius:var(--radius-md);background:var(--surface);padding:15px;margin-bottom:12px}.hero-title{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.hero-title b{font-size:18px;color:var(--ink)}.badge{display:inline-flex;border-radius:999px;padding:4px 9px;font-size:11px;font-weight:900;background:var(--canvas);border:1px solid var(--hairline-soft);color:var(--slate)}.badge.ok{color:var(--green);background:var(--green-light);border-color:rgba(0,180,115,.18)}.badge.fail{color:var(--red);background:var(--red-light);border-color:rgba(229,62,62,.18)}.plain{margin-top:10px;color:var(--slate);font-size:13px;line-height:1.7}.cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-bottom:12px}.mini{border:1px solid var(--hairline-soft);border-radius:var(--radius-md);background:var(--surface);padding:11px;min-width:0}.mini span{display:block;color:var(--stone);font-size:10px;font-weight:900}.mini b{display:block;margin-top:4px;color:var(--ink);font-size:13px;word-break:break-word}.section{border:1px solid var(--hairline-soft);border-radius:var(--radius-md);background:var(--canvas);margin-bottom:12px;overflow:hidden}.section h3{font-size:13px;font-weight:900;color:var(--ink);padding:12px 13px;border-bottom:1px solid var(--hairline-soft);background:var(--surface)}.kv{display:grid;gap:8px;padding:12px}.kv-row{display:grid;grid-template-columns:140px minmax(0,1fr);gap:10px;font-size:12px}.kv-row label{color:var(--stone);font-weight:900}.kv-row div{color:var(--slate);line-height:1.55;word-break:break-word}.chips{display:flex;gap:5px;flex-wrap:wrap}.chip{display:inline-flex;border:1px solid var(--hairline-soft);background:var(--surface);border-radius:999px;padding:4px 8px;font-size:11px;font-weight:800;color:var(--slate)}.json-box{max-height:320px;overflow:auto;background:#101423;color:#dce6ff;border-radius:var(--radius-md);padding:12px;font-size:12px;line-height:1.55;white-space:pre-wrap;word-break:break-word}.empty,.loading{padding:34px 20px;text-align:center;color:var(--stone);font-size:13px}.pager{display:flex;gap:8px;align-items:center}.page-btn{height:34px;border:1px solid var(--hairline-strong);border-radius:var(--radius-md);background:var(--canvas);font-size:12px;font-weight:900;color:var(--ink);padding:0 10px;cursor:pointer}.page-btn:disabled{opacity:.45;cursor:default}@media(max-width:980px){.layout{grid-template-columns:1fr}.list{max-height:none}.cards{grid-template-columns:1fr}}@media(max-width:560px){.shell{width:min(100% - 24px,1280px);padding-top:18px}.kv-row{grid-template-columns:1fr}.page-head h1{font-size:22px}} .shell{width:min(100% - 40px,1280px);margin:0 auto;padding:24px 0 44px}.page-head{display:flex;justify-content:space-between;align-items:flex-end;gap:14px;margin-bottom:16px;flex-wrap:wrap}.page-head h1{font-size:26px;font-weight:900;letter-spacing:-.6px;color:var(--ink)}.page-head p{margin-top:4px;color:var(--stone);font-size:13px;line-height:1.6}.controls{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.select,.btn{height:38px;border:1px solid var(--hairline-strong);background:var(--canvas);border-radius:var(--radius-md);padding:0 12px;font-size:13px;font-weight:800;color:var(--ink)}.btn{cursor:pointer}.layout{display:grid;grid-template-columns:430px minmax(0,1fr);gap:14px;align-items:start}.panel{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-md);min-width:0;overflow:hidden}.panel-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:13px 14px;border-bottom:1px solid var(--hairline-soft)}.panel-title{font-size:14px;font-weight:900;color:var(--ink)}.panel-note{font-size:11px;color:var(--stone);font-weight:800}.list{max-height:calc(100vh - 178px);overflow:auto}.row{padding:13px 14px;border-bottom:1px solid var(--hairline-soft);cursor:pointer;transition:.12s;background:var(--canvas)}.row:hover{background:var(--surface)}.row.active{background:rgba(66,98,255,.06);box-shadow:inset 3px 0 0 var(--blue)}.row-top{display:flex;align-items:center;justify-content:space-between;gap:8px}.type{font-size:12px;font-weight:900;color:var(--blue);background:rgba(66,98,255,.08);border-radius:999px;padding:4px 8px;white-space:nowrap}.type.failed{color:var(--red);background:var(--red-light)}.type.skipped{color:var(--stone);background:var(--surface);border:1px solid var(--hairline-soft)}.subject{margin-top:8px;font-size:14px;font-weight:900;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.summary{margin-top:5px;color:var(--slate);font-size:12px;line-height:1.55;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:8px;color:var(--stone);font-size:11px;font-weight:800}.detail{min-height:560px}.detail-body{padding:16px}.hero-card{border:1px solid var(--hairline-soft);border-radius:var(--radius-md);background:var(--surface);padding:15px;margin-bottom:12px}.hero-title{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.hero-title b{font-size:18px;color:var(--ink)}.badge{display:inline-flex;border-radius:999px;padding:4px 9px;font-size:11px;font-weight:900;background:var(--canvas);border:1px solid var(--hairline-soft);color:var(--slate)}.badge.ok{color:var(--green);background:var(--green-light);border-color:rgba(0,180,115,.18)}.badge.fail{color:var(--red);background:var(--red-light);border-color:rgba(229,62,62,.18)}.plain{margin-top:10px;color:var(--slate);font-size:13px;line-height:1.7}.cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-bottom:12px}.mini{border:1px solid var(--hairline-soft);border-radius:var(--radius-md);background:var(--surface);padding:11px;min-width:0}.mini span{display:block;color:var(--stone);font-size:10px;font-weight:900}.mini b{display:block;margin-top:4px;color:var(--ink);font-size:13px;word-break:break-word}.section{border:1px solid var(--hairline-soft);border-radius:var(--radius-md);background:var(--canvas);margin-bottom:12px;overflow:hidden}.section h3{font-size:13px;font-weight:900;color:var(--ink);padding:12px 13px;border-bottom:1px solid var(--hairline-soft);background:var(--surface)}.kv{display:grid;gap:8px;padding:12px}.kv-row{display:grid;grid-template-columns:140px minmax(0,1fr);gap:10px;font-size:12px}.kv-row label{color:var(--stone);font-weight:900}.kv-row div{color:var(--slate);line-height:1.55;word-break:break-word}.chips{display:flex;gap:5px;flex-wrap:wrap}.chip{display:inline-flex;border:1px solid var(--hairline-soft);background:var(--surface);border-radius:999px;padding:4px 8px;font-size:11px;font-weight:800;color:var(--slate)}.json-box{max-height:320px;overflow:auto;background:#101423;color:#dce6ff;border-radius:var(--radius-md);padding:12px;font-size:12px;line-height:1.55;white-space:pre-wrap;word-break:break-word}.empty,.loading{padding:34px 20px;text-align:center;color:var(--stone);font-size:13px}.pager{display:flex;gap:8px;align-items:center}.page-btn{height:34px;border:1px solid var(--hairline-strong);border-radius:var(--radius-md);background:var(--canvas);font-size:12px;font-weight:900;color:var(--ink);padding:0 10px;cursor:pointer}.page-btn:disabled{opacity:.45;cursor:default}@media(max-width:980px){.layout{grid-template-columns:1fr}.list{max-height:none}.cards{grid-template-columns:1fr}}@media(max-width:560px){.shell{width:min(100% - 24px,1280px);padding-top:18px}.kv-row{grid-template-columns:1fr}.page-head h1{font-size:22px}}

View File

@ -1,20 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}AlphaX Agent Crypto — 市场总览{% endblock %} {% block title %}AlphaX Agent Crypto — 市场总览{% endblock %}
{% block nav_links %}
<a class="sidebar-link" href="/app"><svg class="link-icon"><use href="#svg-dashboard"/></svg>看板</a>
<a class="sidebar-link active" href="/market"><svg class="link-icon"><use href="#svg-target"/></svg>市场总览</a>
<a class="sidebar-link" href="/sentiment"><svg class="link-icon"><use href="#svg-sentiment"/></svg>舆情</a>
<a class="sidebar-link" href="/onchain"><svg class="link-icon"><use href="#svg-onchain"/></svg>链上异动</a>
<a class="sidebar-link" href="/subscription"><svg class="link-icon"><use href="#svg-subscribe"/></svg>订阅</a>
<a class="sidebar-link" href="/referral"><svg class="link-icon"><use href="#svg-referral"/></svg>推荐</a>
<div class="sidebar-section-label admin-link" style="display:none">研发</div>
<a class="sidebar-link admin-link" href="/pipeline" style="display:none"><svg class="link-icon"><use href="#svg-pipeline"/></svg>链路日志</a>
<a class="sidebar-link admin-link" href="/cron" style="display:none"><svg class="link-icon"><use href="#svg-cron"/></svg>调度中心</a>
<a class="sidebar-link admin-link" href="/llm-insights" style="display:none"><svg class="link-icon"><use href="#svg-ai"/></svg>AI 记录</a>
<a class="sidebar-link admin-link" href="/strategy" style="display:none"><svg class="link-icon"><use href="#svg-target"/></svg>策略</a>
<a class="sidebar-link admin-link" href="/iteration" style="display:none"><svg class="link-icon"><use href="#svg-iterate"/></svg>迭代</a>
<a class="sidebar-link admin-link" href="/admin.html" style="display:none"><svg class="link-icon"><use href="#svg-admin"/></svg>管理</a>
{% endblock %}
{% block extra_head_css %} {% block extra_head_css %}
<style> <style>
.shell{width:min(100% - 40px,1280px);margin:0 auto;padding:24px 0 44px}.page-head{display:flex;align-items:flex-end;justify-content:space-between;gap:14px;margin-bottom:16px;flex-wrap:wrap}.page-head h1{font-size:28px;font-weight:950;letter-spacing:-.8px;color:var(--ink)}.page-head p{margin-top:5px;color:var(--stone);font-size:13px;line-height:1.55;max-width:880px}.head-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.select,.btn{height:38px;border:1px solid var(--hairline-strong);background:var(--canvas);border-radius:var(--radius-md);padding:0 12px;font-size:13px;font-weight:850;color:var(--ink)}.btn{cursor:pointer}.hint{padding:10px 12px;border:1px solid rgba(66,98,255,.14);background:rgba(66,98,255,.045);border-radius:var(--radius-md);color:var(--slate);font-size:12px;line-height:1.55;margin-bottom:14px}.kpis{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-bottom:14px}.kpi{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-md);padding:13px;min-width:0}.kpi span{display:block;color:var(--stone);font-size:11px;font-weight:900}.kpi b{display:block;margin-top:7px;color:var(--ink);font-size:22px;line-height:1;font-weight:950;letter-spacing:-.5px}.kpi b.green{color:var(--green)}.kpi b.red{color:var(--red)}.kpi b.blue{color:var(--blue)}.kpi b.yellow{color:var(--yellow-dark)}.grid{display:grid;grid-template-columns:1.1fr .9fr;gap:12px;margin-bottom:14px}.panel{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-md);overflow:hidden;min-width:0}.panel-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:13px 14px;border-bottom:1px solid var(--hairline-soft)}.panel-title{font-size:14px;font-weight:950;color:var(--ink)}.panel-note{font-size:11px;color:var(--stone);font-weight:850}.panel-body{padding:12px}.mini-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.mini{border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);padding:10px;min-width:0}.mini span{display:block;color:var(--stone);font-size:10px;font-weight:900}.mini b{display:block;margin-top:4px;color:var(--ink);font-size:15px;font-weight:950;line-height:1.3}.line{display:flex;justify-content:space-between;gap:10px;align-items:center;border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);padding:10px 11px;margin-bottom:8px}.line .lbl{font-size:12px;font-weight:900;color:var(--ink)}.line .val{font-size:12px;color:var(--slate);font-weight:850;text-align:right}.chips{display:flex;flex-wrap:wrap;gap:8px}.chip{display:inline-flex;align-items:center;gap:6px;padding:7px 10px;border-radius:999px;background:var(--surface);border:1px solid var(--hairline-soft);font-size:12px;font-weight:850;color:var(--slate)}.chip.hot{background:var(--green-light);border-color:rgba(0,180,115,.18);color:var(--green)}.chip.risk{background:var(--red-light);border-color:rgba(229,62,62,.18);color:var(--red)}.chip.blue{background:rgba(66,98,255,.06);border-color:rgba(66,98,255,.16);color:var(--blue)}.raw-list{display:grid;gap:8px}.raw-item{border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);padding:11px}.raw-item h3{font-size:12px;font-weight:950;color:var(--ink);line-height:1.45}.raw-item .sub{margin-top:6px;color:var(--stone);font-size:11px;line-height:1.45}.raw-tags{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px}.tag{display:inline-flex;padding:3px 7px;border-radius:999px;background:var(--canvas);border:1px solid var(--hairline-soft);font-size:10px;font-weight:900;color:var(--blue)}.tag.risk{color:var(--red)}.tag.hot{color:var(--green)}.empty,.loading{padding:34px 16px;text-align:center;color:var(--stone);font-size:13px}.soft-note{padding:10px 12px;border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);color:var(--slate);font-size:12px;line-height:1.5}.compact-note{color:var(--stone);font-size:12px;line-height:1.5}.status-pill{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px;border-radius:999px;border:1px solid var(--hairline-soft);background:var(--surface);font-size:12px;font-weight:900;color:var(--slate)}.status-pill.ok{background:var(--green-light);border-color:rgba(0,180,115,.18);color:var(--green)}.status-pill.warn{background:var(--yellow-light);border-color:rgba(252,185,0,.22);color:var(--yellow-dark)}.status-pill.bad{background:var(--red-light);border-color:rgba(229,62,62,.18);color:var(--red)}@media(max-width:1080px){.kpis{grid-template-columns:repeat(2,minmax(0,1fr))}.grid{grid-template-columns:1fr}}@media(max-width:620px){.shell{width:min(100% - 24px,1280px)}.page-head h1{font-size:22px}.mini-grid{grid-template-columns:1fr}} .shell{width:min(100% - 40px,1280px);margin:0 auto;padding:24px 0 44px}.page-head{display:flex;align-items:flex-end;justify-content:space-between;gap:14px;margin-bottom:16px;flex-wrap:wrap}.page-head h1{font-size:28px;font-weight:950;letter-spacing:-.8px;color:var(--ink)}.page-head p{margin-top:5px;color:var(--stone);font-size:13px;line-height:1.55;max-width:880px}.head-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.select,.btn{height:38px;border:1px solid var(--hairline-strong);background:var(--canvas);border-radius:var(--radius-md);padding:0 12px;font-size:13px;font-weight:850;color:var(--ink)}.btn{cursor:pointer}.hint{padding:10px 12px;border:1px solid rgba(66,98,255,.14);background:rgba(66,98,255,.045);border-radius:var(--radius-md);color:var(--slate);font-size:12px;line-height:1.55;margin-bottom:14px}.kpis{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-bottom:14px}.kpi{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-md);padding:13px;min-width:0}.kpi span{display:block;color:var(--stone);font-size:11px;font-weight:900}.kpi b{display:block;margin-top:7px;color:var(--ink);font-size:22px;line-height:1;font-weight:950;letter-spacing:-.5px}.kpi b.green{color:var(--green)}.kpi b.red{color:var(--red)}.kpi b.blue{color:var(--blue)}.kpi b.yellow{color:var(--yellow-dark)}.grid{display:grid;grid-template-columns:1.1fr .9fr;gap:12px;margin-bottom:14px}.panel{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-md);overflow:hidden;min-width:0}.panel-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:13px 14px;border-bottom:1px solid var(--hairline-soft)}.panel-title{font-size:14px;font-weight:950;color:var(--ink)}.panel-note{font-size:11px;color:var(--stone);font-weight:850}.panel-body{padding:12px}.mini-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.mini{border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);padding:10px;min-width:0}.mini span{display:block;color:var(--stone);font-size:10px;font-weight:900}.mini b{display:block;margin-top:4px;color:var(--ink);font-size:15px;font-weight:950;line-height:1.3}.line{display:flex;justify-content:space-between;gap:10px;align-items:center;border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);padding:10px 11px;margin-bottom:8px}.line .lbl{font-size:12px;font-weight:900;color:var(--ink)}.line .val{font-size:12px;color:var(--slate);font-weight:850;text-align:right}.chips{display:flex;flex-wrap:wrap;gap:8px}.chip{display:inline-flex;align-items:center;gap:6px;padding:7px 10px;border-radius:999px;background:var(--surface);border:1px solid var(--hairline-soft);font-size:12px;font-weight:850;color:var(--slate)}.chip.hot{background:var(--green-light);border-color:rgba(0,180,115,.18);color:var(--green)}.chip.risk{background:var(--red-light);border-color:rgba(229,62,62,.18);color:var(--red)}.chip.blue{background:rgba(66,98,255,.06);border-color:rgba(66,98,255,.16);color:var(--blue)}.raw-list{display:grid;gap:8px}.raw-item{border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);padding:11px}.raw-item h3{font-size:12px;font-weight:950;color:var(--ink);line-height:1.45}.raw-item .sub{margin-top:6px;color:var(--stone);font-size:11px;line-height:1.45}.raw-tags{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px}.tag{display:inline-flex;padding:3px 7px;border-radius:999px;background:var(--canvas);border:1px solid var(--hairline-soft);font-size:10px;font-weight:900;color:var(--blue)}.tag.risk{color:var(--red)}.tag.hot{color:var(--green)}.empty,.loading{padding:34px 16px;text-align:center;color:var(--stone);font-size:13px}.soft-note{padding:10px 12px;border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);color:var(--slate);font-size:12px;line-height:1.5}.compact-note{color:var(--stone);font-size:12px;line-height:1.5}.status-pill{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px;border-radius:999px;border:1px solid var(--hairline-soft);background:var(--surface);font-size:12px;font-weight:900;color:var(--slate)}.status-pill.ok{background:var(--green-light);border-color:rgba(0,180,115,.18);color:var(--green)}.status-pill.warn{background:var(--yellow-light);border-color:rgba(252,185,0,.22);color:var(--yellow-dark)}.status-pill.bad{background:var(--red-light);border-color:rgba(229,62,62,.18);color:var(--red)}@media(max-width:1080px){.kpis{grid-template-columns:repeat(2,minmax(0,1fr))}.grid{grid-template-columns:1fr}}@media(max-width:620px){.shell{width:min(100% - 24px,1280px)}.page-head h1{font-size:22px}.mini-grid{grid-template-columns:1fr}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,22 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}推荐好友 · AlphaX Agent Crypto{% endblock %} {% block title %}推荐好友 · AlphaX Agent Crypto{% endblock %}
{% block nav_links %}
<a class="sidebar-link" href="/app"><svg class="link-icon"><use href="#svg-dashboard"/></svg>看板</a>
<a class="sidebar-link" href="/market"><svg class="link-icon"><use href="#svg-target"/></svg>市场总览</a>
<a class="sidebar-link" href="/sentiment"><svg class="link-icon"><use href="#svg-sentiment"/></svg>舆情</a>
<a class="sidebar-link" href="/onchain"><svg class="link-icon"><use href="#svg-onchain"/></svg>链上异动</a>
<a class="sidebar-link" href="/subscription"><svg class="link-icon"><use href="#svg-subscribe"/></svg>订阅</a>
<a class="sidebar-link active" href="/referral"><svg class="link-icon"><use href="#svg-referral"/></svg>推荐</a>
<div class="sidebar-section-label admin-link" style="display:none">研发</div>
<a class="sidebar-link admin-link" href="/pipeline" style="display:none"><svg class="link-icon"><use href="#svg-pipeline"/></svg>链路日志</a>
<a class="sidebar-link admin-link" href="/cron" style="display:none"><svg class="link-icon"><use href="#svg-cron"/></svg>调度中心</a>
<a class="sidebar-link admin-link" href="/llm-insights" style="display:none"><svg class="link-icon"><use href="#svg-ai"/></svg>AI 记录</a>
<a class="sidebar-link admin-link" href="/strategy" style="display:none"><svg class="link-icon"><use href="#svg-target"/></svg>策略</a>
<a class="sidebar-link admin-link" href="/iteration" style="display:none"><svg class="link-icon"><use href="#svg-iterate"/></svg>迭代</a>
<a class="sidebar-link admin-link" href="/admin.html" style="display:none"><svg class="link-icon"><use href="#svg-admin"/></svg>管理</a>
{% endblock %}
{% block extra_head_css %} {% block extra_head_css %}
<style> <style>
main { max-width: 680px; margin: 0 auto; width: 100%; padding: 32px 20px; display: flex; flex-direction: column; gap: 28px; } main { max-width: 680px; margin: 0 auto; width: 100%; padding: 32px 20px; display: flex; flex-direction: column; gap: 28px; }

View File

@ -1,20 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}AlphaX Agent Crypto — 舆情雷达{% endblock %} {% block title %}AlphaX Agent Crypto — 舆情雷达{% endblock %}
{% block nav_links %}
<a class="sidebar-link" href="/app"><svg class="link-icon"><use href="#svg-dashboard"/></svg>看板</a>
<a class="sidebar-link" href="/market"><svg class="link-icon"><use href="#svg-target"/></svg>市场总览</a>
<a class="sidebar-link active" href="/sentiment"><svg class="link-icon"><use href="#svg-sentiment"/></svg>舆情</a>
<a class="sidebar-link" href="/onchain"><svg class="link-icon"><use href="#svg-onchain"/></svg>链上异动</a>
<a class="sidebar-link" href="/subscription"><svg class="link-icon"><use href="#svg-subscribe"/></svg>订阅</a>
<a class="sidebar-link" href="/referral"><svg class="link-icon"><use href="#svg-referral"/></svg>推荐</a>
<div class="sidebar-section-label admin-link" style="display:none">研发</div>
<a class="sidebar-link admin-link" href="/pipeline" style="display:none"><svg class="link-icon"><use href="#svg-pipeline"/></svg>链路日志</a>
<a class="sidebar-link admin-link" href="/cron" style="display:none"><svg class="link-icon"><use href="#svg-cron"/></svg>调度中心</a>
<a class="sidebar-link admin-link" href="/llm-insights" style="display:none"><svg class="link-icon"><use href="#svg-ai"/></svg>AI 记录</a>
<a class="sidebar-link admin-link" href="/strategy" style="display:none"><svg class="link-icon"><use href="#svg-target"/></svg>策略</a>
<a class="sidebar-link admin-link" href="/iteration" style="display:none"><svg class="link-icon"><use href="#svg-iterate"/></svg>迭代</a>
<a class="sidebar-link admin-link" href="/admin.html" style="display:none"><svg class="link-icon"><use href="#svg-admin"/></svg>管理</a>
{% endblock %}
{% block extra_head_css %} {% block extra_head_css %}
<style> <style>

View File

@ -1,20 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}策略 — AlphaX Agent Crypto{% endblock %} {% block title %}策略 — AlphaX Agent Crypto{% endblock %}
{% block nav_links %}
<a class="sidebar-link" href="/app"><svg class="link-icon"><use href="#svg-dashboard"/></svg>看板</a>
<a class="sidebar-link" href="/market"><svg class="link-icon"><use href="#svg-target"/></svg>市场总览</a>
<a class="sidebar-link" href="/sentiment"><svg class="link-icon"><use href="#svg-sentiment"/></svg>舆情</a>
<a class="sidebar-link" href="/onchain"><svg class="link-icon"><use href="#svg-onchain"/></svg>链上异动</a>
<a class="sidebar-link" href="/subscription"><svg class="link-icon"><use href="#svg-subscribe"/></svg>订阅</a>
<a class="sidebar-link" href="/referral"><svg class="link-icon"><use href="#svg-referral"/></svg>推荐</a>
<div class="sidebar-section-label admin-link" style="display:none">研发</div>
<a class="sidebar-link admin-link" href="/pipeline" style="display:none"><svg class="link-icon"><use href="#svg-pipeline"/></svg>链路日志</a>
<a class="sidebar-link admin-link" href="/cron" style="display:none"><svg class="link-icon"><use href="#svg-cron"/></svg>调度中心</a>
<a class="sidebar-link admin-link" href="/llm-insights" style="display:none"><svg class="link-icon"><use href="#svg-ai"/></svg>AI 记录</a>
<a class="sidebar-link active admin-link" href="/strategy" style="display:none"><svg class="link-icon"><use href="#svg-target"/></svg>策略</a>
<a class="sidebar-link admin-link" href="/iteration" style="display:none"><svg class="link-icon"><use href="#svg-iterate"/></svg>迭代</a>
<a class="sidebar-link admin-link" href="/admin.html" style="display:none"><svg class="link-icon"><use href="#svg-admin"/></svg>管理</a>
{% endblock %}
{% block extra_head_css %} {% block extra_head_css %}
<style> <style>
.shell{width:min(100% - 40px,1180px);margin:0 auto;padding:24px 0 48px}.page-head{margin-bottom:20px}.page-head h1{font-size:28px;letter-spacing:-.8px}.page-head p{color:var(--stone);font-size:14px;margin-top:4px}.metrics{display:grid;grid-template-columns:repeat(6,1fr);gap:12px;margin-bottom:12px}.metric{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-xl);padding:18px;text-align:center}.metric .num{font-size:30px;font-weight:900;letter-spacing:-.8px}.metric .lbl{font-size:12px;color:var(--stone);font-weight:700;margin-top:4px}.disclaimer{font-size:12px;color:var(--stone);background:var(--surface);border:1px solid var(--hairline-soft);border-radius:var(--radius-lg);padding:10px 14px;margin-bottom:16px}.flow{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:16px}.flow-step{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-lg);padding:12px}.flow-step b{display:block;color:var(--ink);font-size:13px;margin-bottom:5px}.flow-step span{display:block;color:var(--stone);font-size:12px;line-height:1.55}.flow-link{color:var(--primary);font-weight:900;text-decoration:none}.panel{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-xl);padding:18px;margin-bottom:16px}.panel h2{font-size:16px;margin-bottom:12px}.grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}.table{width:100%;border-collapse:collapse}.table th,.table td{padding:9px 8px;border-bottom:1px solid var(--hairline-soft);font-size:12px;text-align:left}.table th{color:var(--stone);font-weight:800}.pos{color:var(--green);font-weight:800}.neg{color:var(--red);font-weight:800}.tag{display:inline-flex;border-radius:var(--radius-full);background:var(--surface);padding:3px 8px;font-weight:800}.empty{color:var(--stone);font-size:13px;padding:16px;background:var(--surface);border-radius:var(--radius-lg)}@media(max-width:980px){.metrics{grid-template-columns:repeat(2,1fr)}.flow{grid-template-columns:repeat(2,1fr)}}@media(max-width:820px){.grid{grid-template-columns:1fr}.shell{width:min(100% - 24px,1180px)}}@media(max-width:520px){.flow{grid-template-columns:1fr}} .shell{width:min(100% - 40px,1180px);margin:0 auto;padding:24px 0 48px}.page-head{margin-bottom:20px}.page-head h1{font-size:28px;letter-spacing:-.8px}.page-head p{color:var(--stone);font-size:14px;margin-top:4px}.metrics{display:grid;grid-template-columns:repeat(6,1fr);gap:12px;margin-bottom:12px}.metric{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-xl);padding:18px;text-align:center}.metric .num{font-size:30px;font-weight:900;letter-spacing:-.8px}.metric .lbl{font-size:12px;color:var(--stone);font-weight:700;margin-top:4px}.disclaimer{font-size:12px;color:var(--stone);background:var(--surface);border:1px solid var(--hairline-soft);border-radius:var(--radius-lg);padding:10px 14px;margin-bottom:16px}.flow{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:16px}.flow-step{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-lg);padding:12px}.flow-step b{display:block;color:var(--ink);font-size:13px;margin-bottom:5px}.flow-step span{display:block;color:var(--stone);font-size:12px;line-height:1.55}.flow-link{color:var(--primary);font-weight:900;text-decoration:none}.panel{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-xl);padding:18px;margin-bottom:16px}.panel h2{font-size:16px;margin-bottom:12px}.grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}.table{width:100%;border-collapse:collapse}.table th,.table td{padding:9px 8px;border-bottom:1px solid var(--hairline-soft);font-size:12px;text-align:left}.table th{color:var(--stone);font-weight:800}.pos{color:var(--green);font-weight:800}.neg{color:var(--red);font-weight:800}.tag{display:inline-flex;border-radius:var(--radius-full);background:var(--surface);padding:3px 8px;font-weight:800}.empty{color:var(--stone);font-size:13px;padding:16px;background:var(--surface);border-radius:var(--radius-lg)}@media(max-width:980px){.metrics{grid-template-columns:repeat(2,1fr)}.flow{grid-template-columns:repeat(2,1fr)}}@media(max-width:820px){.grid{grid-template-columns:1fr}.shell{width:min(100% - 24px,1180px)}}@media(max-width:520px){.flow{grid-template-columns:1fr}}

View File

@ -1,20 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}订阅中心 — AlphaX Agent Crypto{% endblock %} {% block title %}订阅中心 — AlphaX Agent Crypto{% endblock %}
{% block nav_links %}
<a class="sidebar-link" href="/app"><svg class="link-icon"><use href="#svg-dashboard"/></svg>看板</a>
<a class="sidebar-link" href="/market"><svg class="link-icon"><use href="#svg-target"/></svg>市场总览</a>
<a class="sidebar-link" href="/sentiment"><svg class="link-icon"><use href="#svg-sentiment"/></svg>舆情</a>
<a class="sidebar-link" href="/onchain"><svg class="link-icon"><use href="#svg-onchain"/></svg>链上异动</a>
<a class="sidebar-link active" href="/subscription"><svg class="link-icon"><use href="#svg-subscribe"/></svg>订阅</a>
<a class="sidebar-link" href="/referral"><svg class="link-icon"><use href="#svg-referral"/></svg>推荐</a>
<div class="sidebar-section-label admin-link" style="display:none">研发</div>
<a class="sidebar-link admin-link" href="/pipeline" style="display:none"><svg class="link-icon"><use href="#svg-pipeline"/></svg>链路日志</a>
<a class="sidebar-link admin-link" href="/cron" style="display:none"><svg class="link-icon"><use href="#svg-cron"/></svg>调度中心</a>
<a class="sidebar-link admin-link" href="/llm-insights" style="display:none"><svg class="link-icon"><use href="#svg-ai"/></svg>AI 记录</a>
<a class="sidebar-link admin-link" href="/strategy" style="display:none"><svg class="link-icon"><use href="#svg-target"/></svg>策略</a>
<a class="sidebar-link admin-link" href="/iteration" style="display:none"><svg class="link-icon"><use href="#svg-iterate"/></svg>迭代</a>
<a class="sidebar-link admin-link" href="/admin.html" style="display:none"><svg class="link-icon"><use href="#svg-admin"/></svg>管理</a>
{% endblock %}
{% block extra_head_css %} {% block extra_head_css %}
<style> <style>

181
static/system_logs.html Normal file
View File

@ -0,0 +1,181 @@
{% extends "base.html" %}
{% block title %}系统日志 · AlphaX Agent Crypto{% endblock %}
{% block extra_head_css %}
<style>
main { max-width: 1320px; margin: 0 auto; width: 100%; padding: 24px; display: flex; flex-direction: column; gap: 16px; }
.page-head { display:flex; align-items:flex-end; justify-content:space-between; gap:14px; flex-wrap:wrap; }
.page-title { font-size: 24px; font-weight: 900; color: var(--ink); letter-spacing: -.4px; }
.page-sub { margin-top:4px; font-size:13px; color:var(--stone); }
.log-summary { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:10px; }
.log-chip { padding:14px 15px; border:1px solid var(--hairline-soft); border-radius:var(--radius-md); background:var(--canvas); min-width:0; }
.log-chip span { display:block; color:var(--stone); font-size:11px; font-weight:900; }
.log-chip b { display:block; margin-top:6px; color:var(--ink); font-size:24px; line-height:1; font-weight:900; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.log-layout { display: grid; grid-template-columns: minmax(0, 1fr) 430px; gap: 14px; align-items:start; }
.card { background: var(--canvas); border: 1px solid var(--hairline-soft); border-radius: var(--radius-md); overflow: hidden; }
.log-toolbar { display:flex; gap:8px; flex-wrap:wrap; padding:14px; border-bottom:1px solid var(--hairline-soft); }
.log-toolbar input,.log-toolbar select { min-height:38px; padding:8px 12px; background:var(--surface); border:1px solid var(--hairline); border-radius:var(--radius-md); color:var(--ink); font-size:13px; outline:none; }
.log-toolbar input { flex:1; min-width:220px; }
.log-toolbar button { padding:8px 16px; border:none; border-radius:var(--radius-md); background:var(--primary); color:var(--on-primary); font-size:13px; font-weight:800; cursor:pointer; }
.table-wrap { overflow-x:auto; }
table { width:100%; border-collapse:collapse; min-width:840px; font-size:13px; }
th { text-align:left; padding:10px 12px; color:var(--stone); font-weight:900; border-bottom:1px solid var(--hairline-soft); font-size:11px; text-transform:uppercase; letter-spacing:.4px; background:var(--surface); }
td { padding:11px 12px; border-bottom:1px solid var(--hairline-soft); color:var(--ink); vertical-align:top; }
tr:hover td { background:var(--surface); }
.badge { display:inline-flex; align-items:center; height:22px; padding:0 8px; border-radius:var(--radius-full); font-size:11px; font-weight:900; border:1px solid var(--hairline-soft); background:var(--surface); color:var(--stone); white-space:nowrap; }
.badge-red { background:rgba(229,62,62,.10); color:var(--red); border-color:rgba(229,62,62,.18); }
.badge-yellow { background:rgba(255,208,47,.14); color:var(--yellow-dark); border-color:rgba(255,208,47,.25); }
.msg-cell { max-width:390px; line-height:1.45; word-break:break-word; }
.pagination { display:flex; justify-content:center; align-items:center; gap:12px; padding:14px; font-size:13px; color:var(--stone); }
.pagination button { padding:6px 14px; background:var(--surface); border:1px solid var(--hairline); border-radius:var(--radius-md); color:var(--ink); font-size:13px; cursor:pointer; }
.pagination button:disabled { opacity:.4; cursor:default; }
.log-detail { position:sticky; top:18px; max-height:calc(100vh - 40px); overflow:auto; padding:16px; }
.log-title { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:12px; }
.log-title b { font-size:15px; color:var(--ink); }
.log-meta { display:grid; grid-template-columns:86px minmax(0,1fr); gap:7px 10px; font-size:12px; color:var(--stone); margin-bottom:12px; }
.log-meta span:nth-child(2n) { color:var(--ink); overflow:hidden; text-overflow:ellipsis; }
.stack-box { white-space:pre-wrap; word-break:break-word; background:#15171d; color:#eef0f5; border-radius:var(--radius-md); padding:12px; font-size:12px; line-height:1.55; max-height:520px; overflow:auto; }
.empty { text-align:center; padding:34px 14px; color:var(--stone); font-size:13px; }
@media(max-width:960px){main{padding:18px}.log-layout{grid-template-columns:1fr}.log-detail{position:static;max-height:none}.log-summary{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:560px){.log-summary{grid-template-columns:1fr}.page-title{font-size:21px}}
</style>
{% endblock %}
{% block content %}
<main>
<div class="page-head">
<div>
<div class="page-title">系统日志</div>
<div class="page-sub">集中查看 Web、CLI、Scheduler 的内部错误、上下文和堆栈信息。</div>
</div>
</div>
<div class="log-summary" id="logSummary">
<div class="log-chip"><span>统计</span><b>加载中</b></div>
</div>
<div class="log-layout">
<div class="card">
<div class="log-toolbar">
<input type="text" id="logSearch" placeholder="搜索错误、路径、用户..." onkeydown="if(event.key==='Enter')loadLogs(0)">
<select id="logLevel" onchange="loadLogs(0)">
<option value="all">全部级别</option>
<option value="error">Error</option>
<option value="warning">Warning</option>
</select>
<select id="logSource" onchange="loadLogs(0)">
<option value="all">全部来源</option>
<option value="web">Web</option>
<option value="cli">CLI</option>
<option value="scheduler">Scheduler</option>
</select>
<select id="logHours" onchange="loadLogs(0)">
<option value="24">近 24h</option>
<option value="168" selected>近 7 天</option>
<option value="720">近 30 天</option>
<option value="0">全部</option>
</select>
<button onclick="loadLogs(0)">查询</button>
</div>
<div class="table-wrap">
<table>
<thead><tr>
<th>时间</th><th>来源</th><th>类型</th><th>消息</th><th>路径 / 用户</th><th>状态</th>
</tr></thead>
<tbody id="logTable"><tr><td colspan="6" class="empty">加载中...</td></tr></tbody>
</table>
</div>
<div class="pagination" id="logPagination"></div>
</div>
<div class="card log-detail" id="logDetail">
<div style="color:var(--stone);font-size:13px">选择一条日志查看堆栈。</div>
</div>
</div>
</main>
{% endblock %}
{% block password_modal %}{% endblock %}
{% block extra_script %}
<script>
var API = '';
var LOG_PAGE_SIZE=50,logOffset=0,logTotal=0;
async function init(){
try{var chk=await fetch(API+'/api/admin/check');if(!chk.ok){window.location.href='/subscription';return}
var info=await chk.json();if(!info.is_admin){window.location.href='/subscription';return}}catch(e){window.location.href='/subscription';return}
loadLogs(0);
}
async function loadLogSummary(){
try{
var h=document.getElementById('logHours').value||24;
var r=await fetch(API+'/api/admin/system-errors/stats?hours='+encodeURIComponent(h));
if(!r.ok)throw new Error(r.status);
var d=await r.json(), groups=d.groups||[];
var cards=[
'<div class="log-chip"><span>窗口</span><b>'+esc(d.hours)+'h</b></div>',
'<div class="log-chip"><span>总错误</span><b>'+esc(d.total||0)+'</b></div>'
];
groups.slice(0,6).forEach(function(g){cards.push('<div class="log-chip"><span>'+esc(g.source)+' / '+esc(g.level)+'</span><b>'+esc(g.n)+'</b></div>')});
document.getElementById('logSummary').innerHTML=cards.join('');
}catch(e){document.getElementById('logSummary').innerHTML='<div class="log-chip"><span>统计</span><b>加载失败</b></div>'}
}
async function loadLogs(offset){
logOffset=offset;loadLogSummary();
var q=document.getElementById('logSearch').value.trim();
var level=document.getElementById('logLevel').value;
var source=document.getElementById('logSource').value;
var hours=document.getElementById('logHours').value;
document.getElementById('logTable').innerHTML='<tr><td colspan="6" class="empty">加载中...</td></tr>';
try{
var url=API+'/api/admin/system-errors?search='+encodeURIComponent(q)+'&offset='+offset+'&limit='+LOG_PAGE_SIZE+'&level='+encodeURIComponent(level)+'&source='+encodeURIComponent(source)+'&hours='+encodeURIComponent(hours);
var r=await fetch(url);if(!r.ok)throw new Error(r.status);
var d=await r.json();logTotal=d.total||0;renderLogs(d.items||[]);renderLogPagination();
}catch(e){
document.getElementById('logTable').innerHTML='<tr><td colspan="6" class="empty" style="color:var(--red)">加载失败</td></tr>';
}
}
function renderLogs(items){
var tb=document.getElementById('logTable');
if(!items.length){tb.innerHTML='<tr><td colspan="6" class="empty">暂无系统错误</td></tr>';return}
tb.innerHTML=items.map(function(x){
var badge=x.level==='error'?'badge-red':x.level==='warning'?'badge-yellow':'';
return '<tr onclick="loadLogDetail('+esc(x.id)+')" style="cursor:pointer">'+
'<td style="color:var(--stone);font-size:12px">'+fmtDateTime(x.created_at)+'</td>'+
'<td><span class="badge">'+esc(x.source||'app')+'</span></td>'+
'<td>'+esc(x.error_type||'Error')+'</td>'+
'<td class="msg-cell">'+esc(shortText(x.message||'--',120))+'</td>'+
'<td style="color:var(--stone);font-size:12px">'+esc(shortText((x.request_path||'--')+(x.user_email?' · '+x.user_email:''),80))+'</td>'+
'<td><span class="badge '+badge+'">'+esc(x.status_code||0)+'</span></td>'+
'</tr>';
}).join('');
}
function renderLogPagination(){
var pg=document.getElementById('logPagination'),totalPages=Math.ceil(logTotal/LOG_PAGE_SIZE),cur=Math.floor(logOffset/LOG_PAGE_SIZE)+1;
pg.innerHTML='<button '+(logOffset===0?'disabled':'')+' onclick="loadLogs('+(logOffset-LOG_PAGE_SIZE)+')">上一页</button>'+
'<span>第 '+cur+' / '+Math.max(1,totalPages)+' 页 · 共 '+logTotal+' 条</span>'+
'<button '+((logOffset+LOG_PAGE_SIZE>=logTotal)?'disabled':'')+' onclick="loadLogs('+(logOffset+LOG_PAGE_SIZE)+')">下一页</button>';
}
async function loadLogDetail(id){
document.getElementById('logDetail').innerHTML='<div style="color:var(--stone);font-size:13px">加载详情...</div>';
try{
var r=await fetch(API+'/api/admin/system-errors/'+id);if(!r.ok)throw new Error(r.status);
var d=await r.json();
document.getElementById('logDetail').innerHTML=
'<div class="log-title"><b>#'+esc(d.id)+' · '+esc(d.error_type||'Error')+'</b><span class="badge '+(d.level==='error'?'badge-red':'')+'">'+esc(d.level)+'</span></div>'+
'<div class="log-meta">'+
'<span>时间</span><span>'+fmtDateTime(d.created_at)+'</span>'+
'<span>来源</span><span>'+esc(d.source||'app')+' · '+esc(d.host||'')+' · PID '+esc(d.pid||0)+'</span>'+
'<span>路径</span><span>'+esc((d.request_method||'')+' '+(d.request_path||'--'))+'</span>'+
'<span>用户</span><span>'+esc(d.user_email||'--')+'</span>'+
'<span>指纹</span><span>'+esc(d.fingerprint||'--')+'</span>'+
'<span>消息</span><span>'+esc(d.message||'--')+'</span>'+
'</div><div class="stack-box">'+esc(d.stack_trace||'无堆栈信息')+'</div>';
}catch(e){document.getElementById('logDetail').innerHTML='<div style="color:var(--red);font-size:13px">详情加载失败</div>'}
}
function esc(s){return String(s||'').replace(/[&<>"]/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]})}
function shortText(s,n){s=String(s||'');return s.length>n?s.slice(0,n)+'…':s}
function fmtDateTime(ts){if(!ts)return'--';var d=new Date(ts);if(isNaN(d.getTime()))return String(ts).slice(0,19).replace('T',' ');return (d.getMonth()+1)+'/'+d.getDate()+' '+('0'+d.getHours()).slice(-2)+':'+('0'+d.getMinutes()).slice(-2)+':'+('0'+d.getSeconds()).slice(-2)}
init();
</script>
{% endblock %}

View File

@ -1,20 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}关注 — AlphaX Agent Crypto{% endblock %} {% block title %}关注 — AlphaX Agent Crypto{% endblock %}
{% block nav_links %}
<a class="sidebar-link" href="/app"><svg class="link-icon"><use href="#svg-dashboard"/></svg>看板</a>
<a class="sidebar-link" href="/market"><svg class="link-icon"><use href="#svg-target"/></svg>市场总览</a>
<a class="sidebar-link" href="/sentiment"><svg class="link-icon"><use href="#svg-sentiment"/></svg>舆情</a>
<a class="sidebar-link" href="/onchain"><svg class="link-icon"><use href="#svg-onchain"/></svg>链上异动</a>
<a class="sidebar-link" href="/subscription"><svg class="link-icon"><use href="#svg-subscribe"/></svg>订阅</a>
<a class="sidebar-link" href="/referral"><svg class="link-icon"><use href="#svg-referral"/></svg>推荐</a>
<div class="sidebar-section-label admin-link" style="display:none">研发</div>
<a class="sidebar-link admin-link" href="/pipeline" style="display:none"><svg class="link-icon"><use href="#svg-pipeline"/></svg>链路日志</a>
<a class="sidebar-link admin-link" href="/cron" style="display:none"><svg class="link-icon"><use href="#svg-cron"/></svg>调度中心</a>
<a class="sidebar-link admin-link" href="/llm-insights" style="display:none"><svg class="link-icon"><use href="#svg-ai"/></svg>AI 记录</a>
<a class="sidebar-link admin-link" href="/strategy" style="display:none"><svg class="link-icon"><use href="#svg-target"/></svg>策略</a>
<a class="sidebar-link admin-link" href="/iteration" style="display:none"><svg class="link-icon"><use href="#svg-iterate"/></svg>迭代</a>
<a class="sidebar-link admin-link" href="/admin.html" style="display:none"><svg class="link-icon"><use href="#svg-admin"/></svg>管理</a>
{% endblock %}
{% block extra_head_css %} {% block extra_head_css %}
<style> <style>
.shell{width:min(100% - 40px,1180px);margin:0 auto;padding:24px 0 48px}.page-head{margin-bottom:20px}.page-head h1{font-size:28px;letter-spacing:-.8px}.page-head p{color:var(--stone);font-size:14px;margin-top:4px}.grid{display:grid;grid-template-columns:1fr;gap:16px}.panel{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-xl);padding:18px}.panel h2{font-size:16px;margin-bottom:12px}.actions{display:flex;gap:8px;flex-wrap:wrap}.input{height:42px;border:1px solid var(--hairline-strong);border-radius:var(--radius-full);padding:0 14px;outline:none;min-width:220px}.btn{border:1px solid var(--hairline-strong);background:var(--canvas);border-radius:var(--radius-full);padding:9px 14px;font-size:13px;font-weight:700;cursor:pointer}.btn.primary{background:var(--primary);color:var(--on-primary);border-color:var(--primary)}.tokens{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}.token{display:inline-flex;align-items:center;gap:6px;background:var(--surface);border-radius:var(--radius-full);padding:6px 10px;font-size:13px;font-weight:800}.token button{border:0;background:transparent;color:var(--stone);cursor:pointer;font-weight:900}.watch-cards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-top:14px}.watch-card{border:1px solid var(--hairline-soft);border-radius:var(--radius-lg);padding:14px;background:var(--surface)}.watch-card b{font-size:16px}.meta{font-size:12px;color:var(--stone);margin-top:6px}.status{display:inline-flex;margin-top:8px;border-radius:var(--radius-full);padding:3px 8px;font-size:11px;font-weight:800}.status.buy{color:var(--green);background:var(--green-light)}.status.wait{color:var(--yellow-dark);background:var(--yellow-light)}.status.obs{color:var(--blue);background:rgba(66,98,255,.06)}.empty{color:var(--stone);font-size:13px;padding:12px 0}@media(max-width:820px){.watch-cards{grid-template-columns:1fr}.shell{width:min(100% - 24px,1180px);padding-top:16px}.actions .input{flex:1;min-width:160px}.btn{min-height:44px}} .shell{width:min(100% - 40px,1180px);margin:0 auto;padding:24px 0 48px}.page-head{margin-bottom:20px}.page-head h1{font-size:28px;letter-spacing:-.8px}.page-head p{color:var(--stone);font-size:14px;margin-top:4px}.grid{display:grid;grid-template-columns:1fr;gap:16px}.panel{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-xl);padding:18px}.panel h2{font-size:16px;margin-bottom:12px}.actions{display:flex;gap:8px;flex-wrap:wrap}.input{height:42px;border:1px solid var(--hairline-strong);border-radius:var(--radius-full);padding:0 14px;outline:none;min-width:220px}.btn{border:1px solid var(--hairline-strong);background:var(--canvas);border-radius:var(--radius-full);padding:9px 14px;font-size:13px;font-weight:700;cursor:pointer}.btn.primary{background:var(--primary);color:var(--on-primary);border-color:var(--primary)}.tokens{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}.token{display:inline-flex;align-items:center;gap:6px;background:var(--surface);border-radius:var(--radius-full);padding:6px 10px;font-size:13px;font-weight:800}.token button{border:0;background:transparent;color:var(--stone);cursor:pointer;font-weight:900}.watch-cards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-top:14px}.watch-card{border:1px solid var(--hairline-soft);border-radius:var(--radius-lg);padding:14px;background:var(--surface)}.watch-card b{font-size:16px}.meta{font-size:12px;color:var(--stone);margin-top:6px}.status{display:inline-flex;margin-top:8px;border-radius:var(--radius-full);padding:3px 8px;font-size:11px;font-weight:800}.status.buy{color:var(--green);background:var(--green-light)}.status.wait{color:var(--yellow-dark);background:var(--yellow-light)}.status.obs{color:var(--blue);background:rgba(66,98,255,.06)}.empty{color:var(--stone);font-size:13px;padding:12px 0}@media(max-width:820px){.watch-cards{grid-template-columns:1fr}.shell{width:min(100% - 24px,1180px);padding-top:16px}.actions .input{flex:1;min-width:160px}.btn{min-height:44px}}

View File

@ -0,0 +1,89 @@
import os
import sys
from fastapi.testclient import TestClient
PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if PROJECT_DIR not in sys.path:
sys.path.insert(0, PROJECT_DIR)
from app.db.system_logs import get_system_error, list_system_errors, record_system_error
from app.web import web_server
def test_record_and_query_system_error():
log_id = record_system_error(
source="web",
error_type="RuntimeError",
message="boom",
stack_trace="Traceback\nRuntimeError: boom",
request_method="GET",
request_path="/api/test",
status_code=500,
context={"case": "unit"},
)
assert log_id > 0
detail = get_system_error(log_id)
assert detail["message"] == "boom"
assert detail["context"]["case"] == "unit"
rows = list_system_errors(search="boom", limit=10)
assert rows["total"] >= 1
assert rows["items"][0]["id"] == log_id
def test_admin_system_error_api_uses_local_admin():
log_id = record_system_error(
source="scheduler",
error_type="job_exit_1",
message="job failed",
stack_trace="exit=1",
status_code=1,
)
client = TestClient(web_server.app)
listing = client.get("/api/admin/system-errors?search=job%20failed")
assert listing.status_code == 200
assert listing.json()["items"][0]["id"] == log_id
detail = client.get(f"/api/admin/system-errors/{log_id}")
assert detail.status_code == 200
assert detail.json()["stack_trace"] == "exit=1"
stats = client.get("/api/admin/system-errors/stats")
assert stats.status_code == 200
assert stats.json()["total"] >= 1
def test_system_logs_page_is_separate_from_admin_dashboard():
client = TestClient(web_server.app)
logs_page = client.get("/system-logs")
assert logs_page.status_code == 200
assert "系统日志" in logs_page.text
assert 'href="/system-logs"' in logs_page.text
assert 'active admin-link' in logs_page.text
assert "logTable" in logs_page.text
admin_page = client.get("/admin.html")
assert admin_page.status_code == 200
assert 'data-admin-tab="logs"' not in admin_page.text
assert 'id="logsPanel"' not in admin_page.text
assert "系统日志" not in admin_page.text
def test_sidebar_navigation_is_owned_by_base_template():
static_dir = os.path.join(PROJECT_DIR, "static")
page_files = [
name
for name in os.listdir(static_dir)
if name.endswith(".html") and name not in {"base.html", "auth.html", "index.html"}
]
offenders = []
for name in page_files:
with open(os.path.join(static_dir, name), "r", encoding="utf-8") as f:
if "{% block nav_links %}" in f.read():
offenders.append(name)
assert offenders == []