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."""
import argparse
import sys
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__":
main()
try:
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()
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
JOIN app_user u ON u.id = ua.user_id
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()
conn.close()
@ -734,7 +736,7 @@ def get_admin_stats():
"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],
"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
],
}

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_interval,
)
from app.db.system_logs import get_system_error, get_system_error_stats, list_system_errors
from app.web.shared import (
SchedulerIntervalRequest,
SchedulerToggleRequest,
@ -29,7 +30,7 @@ def build_router(templates):
require_admin(altcoin_session)
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 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")
async def api_admin_check(altcoin_session: str = Cookie(default="")):
@ -54,6 +55,32 @@ def build_router(templates):
require_admin(altcoin_session)
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")
async def api_scheduler_jobs(altcoin_session: str = Cookie(default="")):
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):
router = APIRouter()
def render_page(template_name: str, request: Request, **kwargs):
def render_page(template_name: str, request: Request, active_nav: str = "", **kwargs):
try:
user = auth_db.get_user_by_session_token(request.cookies.get("altcoin_session", ""))
if user:
@ -22,7 +22,8 @@ def build_router(templates, repo_root: Path, stock_report_template: str):
)
except Exception:
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)
async def index():
@ -39,21 +40,21 @@ def build_router(templates, repo_root: Path, stock_report_template: str):
user, redirect = require_page_user(request)
if redirect:
return redirect
return render_page("watchlist.html", request)
return render_page("watchlist.html", request, active_nav="watchlist")
@router.get("/pipeline", response_class=HTMLResponse)
async def pipeline_page(request: Request):
user, redirect = require_page_user(request)
if 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)
async def llm_insights_page(request: Request):
user, redirect = require_page_user(request)
if 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)
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", ""))
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("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)
async def strategy_page(request: Request):
user, redirect = require_page_user(request)
if redirect:
return redirect
return render_page("strategy.html", request)
return render_page("strategy.html", request, active_nav="strategy")
@router.get("/subscription", response_class=HTMLResponse)
async def subscription_page(request: Request):
user, redirect = require_page_user(request, require_subscription=False)
if redirect:
return redirect
return render_page("subscription.html", request)
return render_page("subscription.html", request, active_nav="subscription")
@router.get("/referral", response_class=HTMLResponse)
async def referral_page(request: Request, altcoin_session: str = Cookie(default="")):
user, redirect = require_page_user(request)
if redirect:
return redirect
return render_page("referral.html", request)
return render_page("referral.html", request, active_nav="referral")
@router.get("/app", response_class=HTMLResponse)
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 "")
except Exception:
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["Pragma"] = "no-cache"
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)
if redirect:
return redirect
return render_page("market.html", request)
return render_page("market.html", request, active_nav="market")
@router.get("/sentiment", response_class=HTMLResponse)
async def sentiment_page(request: Request):
user, redirect = require_page_user(request)
if redirect:
return redirect
return render_page("sentiment.html", request)
return render_page("sentiment.html", request, active_nav="sentiment")
@router.get("/onchain", response_class=HTMLResponse)
async def onchain_page(request: Request):
user, redirect = require_page_user(request)
if redirect:
return redirect
return render_page("onchain.html", request)
return render_page("onchain.html", request, active_nav="onchain")
@router.get("/iteration", response_class=HTMLResponse)
async def iteration_page(request: Request):
user, redirect = require_page_user(request)
if 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)
async def stock_report_page():

View File

@ -6,10 +6,12 @@ from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.templating import Jinja2Templates
from app.db.schema import init_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.recommendation_queries import get_active_recommendations, get_active_recommendations_deduped
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)
finally:
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_runtime,
)
from app.db.system_logs import record_system_error
PYTHON = sys.executable
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)
if output_tail:
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(
name,
status=status,

View File

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

View File

@ -1,22 +1,6 @@
{% extends "base.html" %}
{% 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 %}
<style>
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>
</div>
</main>
{% 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 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();
</script>

View File

@ -171,21 +171,20 @@ a { color: inherit; text-decoration: none; }
<span class="brand-name">AlphaX Agent Crypto</span>
</a>
<nav class="sidebar-nav">
{% block nav_links %}
<a class="sidebar-link active" 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 %}
<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 {% if active_nav == 'market' %}active{% endif %}" 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 {% if active_nav == 'onchain' %}active{% endif %}" 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 {% if active_nav == 'referral' %}active{% endif %}" 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 {% 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 {% 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 {% 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 {% 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 {% 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 {% if active_nav == 'iteration' %}active{% endif %}" href="/iteration" style="display:none"><svg class="link-icon"><use href="#svg-iterate"/></svg>迭代</a>
<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>
<div class="sidebar-user" onclick="toggleUserMenu()">
<span class="user-avatar" id="userInitial">?</span>

View File

@ -1,20 +1,5 @@
{% extends "base.html" %}
{% 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 %}
<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}}

View File

@ -1,21 +1,5 @@
{% extends "base.html" %}
{% 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 %}
<style>
.shell { width:min(100% - 40px,1180px); margin:0 auto; padding:28px 0 48px; }

View File

@ -1,20 +1,5 @@
{% extends "base.html" %}
{% 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 %}
<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}}

View File

@ -1,20 +1,5 @@
{% extends "base.html" %}
{% 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 %}
<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}}

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" %}
{% 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 %}
<style>
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" %}
{% 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 %}
<style>

View File

@ -1,20 +1,5 @@
{% extends "base.html" %}
{% 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 %}
<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}}

View File

@ -1,20 +1,5 @@
{% extends "base.html" %}
{% 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 %}
<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" %}
{% 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 %}
<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}}

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 == []