added inventory, updated DB
This commit is contained in:
parent
f218350959
commit
10c51f6825
16528 changed files with 147743 additions and 79 deletions
149
main.py
149
main.py
|
|
@ -20,6 +20,7 @@ from fastapi.staticfiles import StaticFiles
|
|||
from fastapi.encoders import jsonable_encoder
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import httpx
|
||||
|
||||
# Async database support
|
||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
|
|
@ -49,6 +50,9 @@ logger = logging.getLogger(__name__)
|
|||
# Get log level from environment (DEBUG, INFO, WARNING, ERROR)
|
||||
log_level = os.getenv('LOG_LEVEL', 'INFO').upper()
|
||||
logger.setLevel(getattr(logging, log_level, logging.INFO))
|
||||
|
||||
# Inventory service configuration
|
||||
INVENTORY_SERVICE_URL = os.getenv('INVENTORY_SERVICE_URL', 'http://inventory-service:8000')
|
||||
# In-memory caches for REST endpoints
|
||||
_cached_live: dict = {"players": []}
|
||||
_cached_trails: dict = {"trails": []}
|
||||
|
|
@ -293,29 +297,24 @@ async def get_trails(
|
|||
# --- GET Inventory Endpoints ---------------------------------
|
||||
@app.get("/inventory/{character_name}")
|
||||
async def get_character_inventory(character_name: str):
|
||||
"""Get the complete inventory for a specific character from the database."""
|
||||
"""Get the complete inventory for a specific character - inventory service only."""
|
||||
try:
|
||||
query = """
|
||||
SELECT name, icon, object_class, value, burden, has_id_data, item_data, timestamp
|
||||
FROM character_inventories
|
||||
WHERE character_name = :character_name
|
||||
ORDER BY name
|
||||
"""
|
||||
rows = await database.fetch_all(query, {"character_name": character_name})
|
||||
|
||||
if not rows:
|
||||
raise HTTPException(status_code=404, detail=f"No inventory found for character '{character_name}'")
|
||||
|
||||
items = []
|
||||
for row in rows:
|
||||
item = dict(row)
|
||||
items.append(item)
|
||||
|
||||
return JSONResponse(content=jsonable_encoder({
|
||||
"character_name": character_name,
|
||||
"item_count": len(items),
|
||||
"items": items
|
||||
}))
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(
|
||||
f"{INVENTORY_SERVICE_URL}/inventory/{character_name}"
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return JSONResponse(content=response.json())
|
||||
elif response.status_code == 404:
|
||||
raise HTTPException(status_code=404, detail=f"No inventory found for character '{character_name}'")
|
||||
else:
|
||||
logger.error(f"Inventory service returned {response.status_code} for {character_name}")
|
||||
raise HTTPException(status_code=502, detail="Inventory service error")
|
||||
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Could not reach inventory service: {e}")
|
||||
raise HTTPException(status_code=503, detail="Inventory service unavailable")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
|
|
@ -458,18 +457,42 @@ async def _broadcast_to_browser_clients(snapshot: dict):
|
|||
browser_conns.discard(ws)
|
||||
|
||||
|
||||
async def _store_inventory(inventory_msg: FullInventoryMessage):
|
||||
"""Store complete character inventory to database with extracted searchable fields.
|
||||
|
||||
Processes each item to extract key fields for indexing while preserving
|
||||
complete item data in JSONB format. Uses UPSERT to handle item updates.
|
||||
"""
|
||||
async def _forward_to_inventory_service(inventory_msg: FullInventoryMessage):
|
||||
"""Forward inventory data to the inventory microservice for processing."""
|
||||
try:
|
||||
# Create inventory directory if it doesn't exist
|
||||
# Prepare data for inventory service
|
||||
inventory_data = {
|
||||
"character_name": inventory_msg.character_name,
|
||||
"timestamp": inventory_msg.timestamp.isoformat(),
|
||||
"items": inventory_msg.items
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
response = await client.post(
|
||||
f"{INVENTORY_SERVICE_URL}/process-inventory",
|
||||
json=inventory_data
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
logger.info(f"Inventory service processed {result['processed']} items for {inventory_msg.character_name}")
|
||||
else:
|
||||
logger.error(f"Inventory service error {response.status_code}: {response.text}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to forward inventory to service: {e}")
|
||||
# Don't raise - this shouldn't block the main storage
|
||||
|
||||
async def _store_inventory(inventory_msg: FullInventoryMessage):
|
||||
"""Forward inventory data to inventory microservice for processing and storage."""
|
||||
try:
|
||||
# Forward to inventory microservice for enhanced processing and storage
|
||||
await _forward_to_inventory_service(inventory_msg)
|
||||
|
||||
# Optional: Create JSON file for debugging (can be removed in production)
|
||||
inventory_dir = Path("./inventory")
|
||||
inventory_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Store as JSON file for backwards compatibility / debugging
|
||||
file_path = inventory_dir / f"{inventory_msg.character_name}_inventory.json"
|
||||
inventory_data = {
|
||||
"character_name": inventory_msg.character_name,
|
||||
|
|
@ -480,51 +503,9 @@ async def _store_inventory(inventory_msg: FullInventoryMessage):
|
|||
|
||||
with open(file_path, 'w') as f:
|
||||
json.dump(inventory_data, f, indent=2)
|
||||
|
||||
# Store items in database
|
||||
for item in inventory_msg.items:
|
||||
# Extract searchable fields
|
||||
item_id = item.get("Id")
|
||||
if item_id is None:
|
||||
continue # Skip items without ID
|
||||
|
||||
name = item.get("Name", "")
|
||||
icon = item.get("Icon", 0)
|
||||
object_class = item.get("ObjectClass", 0)
|
||||
value = item.get("Value", 0)
|
||||
burden = item.get("Burden", 0)
|
||||
has_id_data = item.get("HasIdData", False)
|
||||
|
||||
# UPSERT item into database
|
||||
stmt = pg_insert(character_inventories).values(
|
||||
character_name=inventory_msg.character_name,
|
||||
item_id=item_id,
|
||||
timestamp=inventory_msg.timestamp,
|
||||
name=name,
|
||||
icon=icon,
|
||||
object_class=object_class,
|
||||
value=value,
|
||||
burden=burden,
|
||||
has_id_data=has_id_data,
|
||||
item_data=item
|
||||
).on_conflict_do_update(
|
||||
constraint="uq_char_item",
|
||||
set_={
|
||||
"timestamp": inventory_msg.timestamp,
|
||||
"name": name,
|
||||
"icon": icon,
|
||||
"object_class": object_class,
|
||||
"value": value,
|
||||
"burden": burden,
|
||||
"has_id_data": has_id_data,
|
||||
"item_data": item
|
||||
}
|
||||
)
|
||||
|
||||
await database.execute(stmt)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to store inventory for {inventory_msg.character_name}: {e}", exc_info=True)
|
||||
logger.error(f"Failed to forward inventory for {inventory_msg.character_name}: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
|
|
@ -636,13 +617,13 @@ async def ws_receive_snapshots(
|
|||
logger.debug(f"Updated kills for {snap.character_name}: +{delta} (total from {last} to {snap.kills})")
|
||||
ws_receive_snapshots._last_kills[key] = snap.kills
|
||||
except Exception as db_error:
|
||||
logger.error(f"Database transaction failed for {snap.character_name}: {db_error}", exc_info=True)
|
||||
logger.error(f"💾 Database transaction failed for {snap.character_name} (session: {snap.session_id[:8]}): {db_error}", exc_info=True)
|
||||
continue
|
||||
# Broadcast updated snapshot to all browser clients
|
||||
await _broadcast_to_browser_clients(snap.dict())
|
||||
logger.debug(f"Processed telemetry from {snap.character_name}")
|
||||
logger.debug(f"✅ Processed telemetry from {snap.character_name} (session: {snap.session_id[:8]}, kills: {snap.kills})")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to process telemetry event: {e}", exc_info=True)
|
||||
logger.error(f"❌ Failed to process telemetry event from {data.get('character_name', 'unknown')}: {e}", exc_info=True)
|
||||
continue
|
||||
# --- Rare event: update total and session counters and persist ---
|
||||
if msg_type == "rare":
|
||||
|
|
@ -892,6 +873,22 @@ async def get_stats(character_name: str):
|
|||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
# -------------------- static frontend ---------------------------
|
||||
# Custom icon handler that prioritizes clean icons over originals
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
@app.get("/icons/{icon_filename}")
|
||||
async def serve_icon(icon_filename: str):
|
||||
"""Serve icons from static/icons directory"""
|
||||
|
||||
# Serve from static/icons directory
|
||||
icon_path = Path("static/icons") / icon_filename
|
||||
if icon_path.exists():
|
||||
return FileResponse(icon_path, media_type="image/png")
|
||||
|
||||
# Icon not found
|
||||
raise HTTPException(status_code=404, detail="Icon not found")
|
||||
|
||||
# Icons are now served from static/icons directory
|
||||
# Serve SPA files (catch-all for frontend routes)
|
||||
# Mount the single-page application frontend (static assets) at root path
|
||||
app.mount("/", StaticFiles(directory="static", html=True), name="static")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue