added inventory, updated DB

This commit is contained in:
erik 2025-06-10 19:21:21 +00:00
parent f218350959
commit 10c51f6825
16528 changed files with 147743 additions and 79 deletions

149
main.py
View file

@ -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")