- Market tab now calls GET /grandexchange/orders (all public orders) instead of /my/grandexchange/orders (own orders only), fixing the empty exchange issue - Fix capture_prices reading "type" field instead of wrong "order" field - Add proper pagination to all GE queries via _get_paginated - Separate My Orders (active own orders) from Trade History (transaction log) - Add GEHistoryEntry type matching GeOrderHistorySchema (order_id, seller, buyer, sold_at) - Add /api/exchange/my-orders and /api/exchange/sell-history endpoints - Exchange page now has 4 tabs: Market, My Orders, Trade History, Price History
119 lines
4 KiB
Python
119 lines
4 KiB
Python
"""Character logs and analytics API router."""
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, Query, Request
|
|
from sqlalchemy import select
|
|
|
|
from app.database import async_session_factory
|
|
from app.models.automation import AutomationConfig, AutomationLog, AutomationRun
|
|
from app.services.analytics_service import AnalyticsService
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/logs", tags=["logs"])
|
|
|
|
|
|
@router.get("/")
|
|
async def get_logs(
|
|
character: str = Query(default="", description="Character name to filter logs"),
|
|
limit: int = Query(default=50, ge=1, le=200, description="Max entries to return"),
|
|
) -> dict[str, Any]:
|
|
"""Get automation action logs from the database.
|
|
|
|
Joins automation_logs -> automation_runs -> automation_configs
|
|
to include character_name with each log entry.
|
|
"""
|
|
async with async_session_factory() as db:
|
|
stmt = (
|
|
select(
|
|
AutomationLog.id,
|
|
AutomationLog.action_type,
|
|
AutomationLog.details,
|
|
AutomationLog.success,
|
|
AutomationLog.created_at,
|
|
AutomationConfig.character_name,
|
|
)
|
|
.join(AutomationRun, AutomationLog.run_id == AutomationRun.id)
|
|
.join(AutomationConfig, AutomationRun.config_id == AutomationConfig.id)
|
|
.order_by(AutomationLog.created_at.desc())
|
|
.limit(limit)
|
|
)
|
|
|
|
if character:
|
|
stmt = stmt.where(AutomationConfig.character_name == character)
|
|
|
|
result = await db.execute(stmt)
|
|
rows = result.all()
|
|
|
|
return {
|
|
"logs": [
|
|
{
|
|
"id": row.id,
|
|
"character_name": row.character_name,
|
|
"action_type": row.action_type,
|
|
"details": row.details,
|
|
"success": row.success,
|
|
"created_at": row.created_at.isoformat(),
|
|
}
|
|
for row in rows
|
|
],
|
|
}
|
|
|
|
|
|
@router.get("/analytics")
|
|
async def get_analytics(
|
|
request: Request,
|
|
character: str = Query(default="", description="Character name (empty for all)"),
|
|
hours: int = Query(default=24, ge=1, le=168, description="Hours of history"),
|
|
) -> dict[str, Any]:
|
|
"""Get analytics aggregations for a character.
|
|
|
|
Returns XP history, gold history, and estimated actions per hour.
|
|
If no character is specified, aggregates across all characters with snapshots.
|
|
"""
|
|
analytics = AnalyticsService()
|
|
|
|
async with async_session_factory() as db:
|
|
if character:
|
|
characters = [character]
|
|
else:
|
|
characters = await analytics.get_tracked_characters(db)
|
|
|
|
all_xp: list[dict[str, Any]] = []
|
|
all_gold: list[dict[str, Any]] = []
|
|
total_actions_per_hour = 0.0
|
|
|
|
for char_name in characters:
|
|
xp_history = await analytics.get_xp_history(db, char_name, hours)
|
|
gold_history = await analytics.get_gold_history(db, char_name, hours)
|
|
actions_rate = await analytics.get_actions_per_hour(db, char_name)
|
|
|
|
# Transform xp_history to TimeSeriesPoint format
|
|
for point in xp_history:
|
|
all_xp.append({
|
|
"timestamp": point["timestamp"],
|
|
"value": point["xp"],
|
|
"label": f"{char_name} XP" if not character else "XP",
|
|
})
|
|
|
|
# Transform gold_history to TimeSeriesPoint format
|
|
for point in gold_history:
|
|
all_gold.append({
|
|
"timestamp": point["timestamp"],
|
|
"value": point["gold"],
|
|
"label": char_name if not character else None,
|
|
})
|
|
|
|
total_actions_per_hour += actions_rate.get("estimated_actions_per_hour", 0)
|
|
|
|
# Sort by timestamp
|
|
all_xp.sort(key=lambda p: p["timestamp"] or "")
|
|
all_gold.sort(key=lambda p: p["timestamp"] or "")
|
|
|
|
return {
|
|
"xp_history": all_xp,
|
|
"gold_history": all_gold,
|
|
"actions_per_hour": round(total_actions_per_hour, 1),
|
|
}
|