update
This commit is contained in:
parent
8e331e6bdf
commit
abb70bbe3e
13
app/cli.py
13
app/cli.py
@ -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
|
||||
|
||||
@ -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
|
||||
],
|
||||
}
|
||||
|
||||
26
app/db/migrations/0003_system_error_log.sql
Normal file
26
app/db/migrations/0003_system_error_log.sql
Normal 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
209
app/db/system_logs.py
Normal 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()
|
||||
@ -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)
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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},
|
||||
)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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: 触发时效治理,旧形态只作背景,消息触发显式标记'
|
||||
|
||||
@ -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{'&':'&','<':'<','>':'>','"':'"'}[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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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
@ -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; }
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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
181
static/system_logs.html
Normal 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{'&':'&','<':'<','>':'>','"':'"'}[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 %}
|
||||
@ -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}}
|
||||
|
||||
89
tests/test_system_error_logs.py
Normal file
89
tests/test_system_error_logs.py
Normal 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 == []
|
||||
Loading…
Reference in New Issue
Block a user