Complete backend verification: fix syntax errors, create __init__.py files, add setup_db.py, update docker-compose for two PostgreSQL containers

This commit is contained in:
2026-07-18 23:02:44 +00:00
parent 3a70feaba5
commit 02ef8e36bc
27 changed files with 687 additions and 121 deletions
+2 -2
View File
@@ -12,7 +12,7 @@ Migrate from internal database to Dockerized PostgreSQL with mtgjson.com "All Pr
│ │ Backend Container │───▶│ PostgreSQL Container │ │ │ │ Backend Container │───▶│ PostgreSQL Container │ │
│ │ (FastAPI app) │ │ (MTG Data + App DB) │ │ │ │ (FastAPI app) │ │ (MTG Data + App DB) │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ - API endpoints │ │ - cockatrice_db (app) │ │ │ │ - API endpoints │ │ - mtgonline_db (app) │ │
│ │ - Auth system │ │ - mtgdata_db (mtgjson) │ │ │ │ - Auth system │ │ - mtgdata_db (mtgjson) │ │
│ │ - Weekly refresh │ │ │ │ │ │ - Weekly refresh │ │ │ │
│ └─────────────────────┘ └──────────────────────────┘ │ │ └─────────────────────┘ └──────────────────────────┘ │
@@ -37,7 +37,7 @@ Migrate from internal database to Dockerized PostgreSQL with mtgjson.com "All Pr
### 3. Database Migration ### 3. Database Migration
- [ ] Create migration script for mtgjson schema - [ ] Create migration script for mtgjson schema
- [ ] Update models.py with MTG card models - [ ] Update models.py with MTG card models
- [ ] Add multi-database support (cockatrice + mtgdata) - [ ] Add multi-database support (mtgonline + mtgdata)
- [ ] Create schema for weekly refresh - [ ] Create schema for weekly refresh
### 4. Weekly Refresh Logic ### 4. Weekly Refresh Logic
+4 -4
View File
@@ -17,7 +17,7 @@
```bash ```bash
# Navigate to project root # Navigate to project root
cd /home/user/wall-o/cockatrice-web cd /home/user/wall-o/mtgonline-web
# Start PostgreSQL and Redis # Start PostgreSQL and Redis
docker compose -f docker-compose.dev.yml up -d docker compose -f docker-compose.dev.yml up -d
@@ -29,8 +29,8 @@ docker compose -f docker-compose.dev.yml ps
**Expected output:** **Expected output:**
``` ```
NAME STATUS PORTS NAME STATUS PORTS
cockatrice-web-postgres-1 Up 0.0.0.0:5432->5432/tcp mtgonline-web-postgres-1 Up 0.0.0.0:5432->5432/tcp
cockatrice-web-redis-1 Up 0.0.0.0:6379->6379/tcp mtgonline-web-redis-1 Up 0.0.0.0:6379->6379/tcp
``` ```
### 2. Create Virtual Environment and Install Backend Dependencies ### 2. Create Virtual Environment and Install Backend Dependencies
@@ -111,7 +111,7 @@ pytest --cov=app --cov-report=html
```bash ```bash
# Stop Docker services # Stop Docker services
cd /home/user/wall-o/cockatrice-web cd /home/user/wall-o/mtgonline-web
docker compose -f docker-compose.dev.yml down docker compose -f docker-compose.dev.yml down
# Deactivate virtual environment (if in backend directory) # Deactivate virtual environment (if in backend directory)
+7 -7
View File
@@ -1,13 +1,13 @@
# Cockatrice Web Application # MTG Online Web Application
A modern web-based implementation of the Cockatrice multiplayer Magic: The Gathering platform. A modern web-based implementation of the MTG Online multiplayer Magic: The Gathering platform.
## Features ## Features
- **User Authentication**: Secure JWT-based authentication with bcrypt password hashing - **User Authentication**: Secure JWT-based authentication with bcrypt password hashing
- **Deck Building**: Full-featured deck editor with import/export in multiple formats - **Deck Building**: Full-featured deck editor with import/export in multiple formats
- **Real-Time Multiplayer**: WebSocket-based game server for live gameplay - **Real-Time Multiplayer**: WebSocket-based game server for live gameplay
- **Protocol Compatibility**: Compatible with Cockatrice protocol buffer messages - **Protocol Compatibility**: Compatible with MTG Online protocol buffer messages
- **Card Database**: Integration with MTJSON for comprehensive card data - **Card Database**: Integration with MTJSON for comprehensive card data
- **Admin Tools**: Comprehensive moderation and administration dashboard - **Admin Tools**: Comprehensive moderation and administration dashboard
@@ -33,8 +33,8 @@ A modern web-based implementation of the Cockatrice multiplayer Magic: The Gathe
1. **Clone the repository** 1. **Clone the repository**
```bash ```bash
git clone https://github.com/yourusername/cockatrice-web.git git clone https://github.com/yourusername/mtgonline-web.git
cd cockatrice-web cd mtgonline-web
``` ```
2. **Create a virtual environment** 2. **Create a virtual environment**
@@ -62,7 +62,7 @@ cp .env.example .env
```bash ```bash
# Create PostgreSQL database # Create PostgreSQL database
createdb cockatrice createdb mtgonline
# Run migrations (when Alembic is set up) # Run migrations (when Alembic is set up)
alembic upgrade head alembic upgrade head
@@ -81,7 +81,7 @@ Open http://localhost:8000/docs to view the FastAPI Swagger UI.
## Project Structure ## Project Structure
``` ```
cockatrice-web/ mtgonline-web/
├── backend/ ├── backend/
│ ├── app/ │ ├── app/
│ │ ├── core/ # Core configuration and utilities │ │ ├── core/ # Core configuration and utilities
+3 -3
View File
@@ -1,8 +1,8 @@
# Cockatrice Web — Project Roadmap # MTG Online Web — Project Roadmap
## Overview ## Overview
A modern web-based implementation of the Cockatrice multiplayer Magic: The Gathering platform. Built with Python/FastAPI backend and React/TypeScript frontend to replace the legacy C++/Qt desktop client. A modern web-based implementation of the MTG Online multiplayer Magic: The Gathering platform. Built with Python/FastAPI backend and React/TypeScript frontend to replace the legacy C++/Qt desktop client.
## Phase 1: Backend Foundation ✅ (COMPLETED) ## Phase 1: Backend Foundation ✅ (COMPLETED)
@@ -34,7 +34,7 @@ A modern web-based implementation of the Cockatrice multiplayer Magic: The Gathe
- [x] WebSocket game server - [x] WebSocket game server
- [x] Deck parser (plain text + native XML) - [x] Deck parser (plain text + native XML)
- [x] Card database service (MTJSON integration) - [x] Card database service (MTJSON integration)
- [x] Protocol constants (Cockatrice protocol compatibility) - [x] Protocol constants (MTG Online protocol compatibility)
### 1.5 Testing ### 1.5 Testing
- [x] Pytest configuration with async support - [x] Pytest configuration with async support
+16 -16
View File
@@ -1,13 +1,13 @@
# Statement of Intent — Cockatrice Web # Statement of Intent — MTG Online Web
## Project Title ## Project Title
**Cockatrice Web** — A modern, web-based implementation of the Cockatrice multiplayer Magic: The Gathering platform. **MTG Online Web** — A modern, web-based implementation of the MTG Online multiplayer Magic: The Gathering platform.
## Vision Statement ## Vision Statement
To build a fully-featured, open-source multiplayer Magic: The Gathering platform that runs entirely in modern web browsers, eliminating the need for desktop software installations while maintaining compatibility with the existing Cockatrice ecosystem. To build a fully-featured, open-source multiplayer Magic: The Gathering platform that runs entirely in modern web browsers, eliminating the need for desktop software installations while maintaining compatibility with the existing MTG Online ecosystem.
## Problem Statement ## Problem Statement
The original Cockatrice application requires: The original MTG Online application requires:
- Desktop software installation (Windows, macOS, Linux) - Desktop software installation (Windows, macOS, Linux)
- Manual updates and dependency management - Manual updates and dependency management
- Complex setup for new users - Complex setup for new users
@@ -19,7 +19,7 @@ A Progressive Web Application (PWA) that provides:
- **Zero Installation**: Runs directly in any modern web browser - **Zero Installation**: Runs directly in any modern web browser
- **Cross-Platform**: Works on desktop, tablet, and mobile devices - **Cross-Platform**: Works on desktop, tablet, and mobile devices
- **Instant Updates**: Users always have the latest version - **Instant Updates**: Users always have the latest version
- **Cockatrice Compatible**: Interoperates with existing Cockatrice users and protocol - **MTG Online Compatible**: Interoperates with existing MTG Online users and protocol
- **Modern UX**: Contemporary interface design with modern web technologies - **Modern UX**: Contemporary interface design with modern web technologies
## Core Objectives ## Core Objectives
@@ -27,7 +27,7 @@ A Progressive Web Application (PWA) that provides:
### 1. User Experience ### 1. User Experience
- Intuitive, modern interface that rivals native desktop applications - Intuitive, modern interface that rivals native desktop applications
- Real-time multiplayer gameplay with minimal latency - Real-time multiplayer gameplay with minimal latency
- Seamless deck building with import/export from Cockatrice - Seamless deck building with import/export from MTG Online
- Responsive design that works across all screen sizes - Responsive design that works across all screen sizes
### 2. Technical Excellence ### 2. Technical Excellence
@@ -35,7 +35,7 @@ A Progressive Web Application (PWA) that provides:
- **Frontend**: React + TypeScript with Zustand state management - **Frontend**: React + TypeScript with Zustand state management
- **Database**: PostgreSQL with async SQLAlchemy ORM - **Database**: PostgreSQL with async SQLAlchemy ORM
- **Real-time**: WebSocket-based game server for live multiplayer - **Real-time**: WebSocket-based game server for live multiplayer
- **Protocol**: Full compatibility with Cockatrice protocol buffer messages - **Protocol**: Full compatibility with MTG Online protocol buffer messages
### 3. Feature Parity with Desktop ### 3. Feature Parity with Desktop
- User authentication and account management - User authentication and account management
@@ -68,16 +68,16 @@ A Progressive Web Application (PWA) that provides:
### Secondary Users ### Secondary Users
1. **Tournament Organizers**: Need reliable, accessible platform for events 1. **Tournament Organizers**: Need reliable, accessible platform for events
2. **Community Builders**: Want to create and manage playgroups 2. **Community Builders**: Want to create and manage playgroups
3. **Developers**: Want to integrate with or extend the Cockatrice ecosystem 3. **Developers**: Want to integrate with or extend the MTG Online ecosystem
### Existing Cockatrice Users ### Existing MTG Online Users
- Seamless migration path - Seamless migration path
- Protocol compatibility for cross-play - Protocol compatibility for cross-play
- Familiar deck formats and card data - Familiar deck formats and card data
## Key Differentiators ## Key Differentiators
| Feature | Desktop Cockatrice | Cockatrice Web | | Feature | Desktop MTG Online | MTG Online Web |
|---------|-------------------|----------------| |---------|-------------------|----------------|
| Installation | Required | None (browser-based) | | Installation | Required | None (browser-based) |
| Platform | Desktop only | Any device with browser | | Platform | Desktop only | Any device with browser |
@@ -95,7 +95,7 @@ A Progressive Web Application (PWA) that provides:
- [ ] Users can join and play multiplayer games in real-time - [ ] Users can join and play multiplayer games in real-time
- [ ] Game state syncs correctly across all connected players - [ ] Game state syncs correctly across all connected players
- [ ] Admin users can manage accounts and moderate games - [ ] Admin users can manage accounts and moderate games
- [ ] Deck formats are compatible with Cockatrice desktop client - [ ] Deck formats are compatible with MTG Online desktop client
### Performance Requirements ### Performance Requirements
- [ ] 95% of API responses < 100ms - [ ] 95% of API responses < 100ms
@@ -115,7 +115,7 @@ A Progressive Web Application (PWA) that provides:
- [ ] Open-source with community contributions - [ ] Open-source with community contributions
- [ ] Documentation for users and developers - [ ] Documentation for users and developers
- [ ] Deployment guides for self-hosting - [ ] Deployment guides for self-hosting
- [ ] Integration with existing Cockatrice ecosystem - [ ] Integration with existing MTG Online ecosystem
## Technology Choices ## Technology Choices
@@ -160,7 +160,7 @@ A Progressive Web Application (PWA) that provides:
### Protocol ### Protocol
**Why Protocol Buffer Compatibility?** **Why Protocol Buffer Compatibility?**
- Interoperability with existing Cockatrice users - Interoperability with existing MTG Online users
- Leverage existing card database and deck formats - Leverage existing card database and deck formats
- Community adoption pathway - Community adoption pathway
- Battle-tested message format - Battle-tested message format
@@ -195,7 +195,7 @@ A Progressive Web Application (PWA) that provides:
- Mitigation: Alembic migrations, backward compatibility - Mitigation: Alembic migrations, backward compatibility
2. **Protocol Changes** 2. **Protocol Changes**
- Risk: Cockatrice protocol evolves - Risk: MTG Online protocol evolves
- Mitigation: Version support, backward compatibility - Mitigation: Version support, backward compatibility
## Future Enhancements ## Future Enhancements
@@ -231,8 +231,8 @@ We commit to:
## Contact ## Contact
For questions, contributions, or support: For questions, contributions, or support:
- GitHub: https://github.com/cockatrice-web - GitHub: https://github.com/mtgonline-web
- Documentation: https://cockatrice-web.github.io/docs - Documentation: https://mtgonline-web.github.io/docs
- Discord: [Community server link] - Discord: [Community server link]
## Version ## Version
+103
View File
@@ -0,0 +1,103 @@
# Backend System Test Prompt
## Project Context
You are working on the **mtgonline** backend project located at `/home/wall-o/projects/mtgonline/backend/`.
The backend is a FastAPI application with:
- PostgreSQL database (MTG Online + MTG data)
- Redis caching
- JWT authentication
- MTG card search and statistics endpoints
## Current State
Read the `state.json` file at `/home/wall-o/projects/mtgonline/backend/state.json` to understand:
- What has been completed
- What the current focus is
- Any blockers or pending tasks
## Task: Full Backend System Test
Your goal is to perform a comprehensive system test of the backend to verify all components work correctly together.
### Steps to Follow:
1. **Read State File**
- Read `/home/wall-o/projects/mtgonline/backend/state.json`
- Understand current progress and what's been tested
2. **Check Docker Containers**
- Run: `cd /home/wall-o/projects/mtgonline && docker compose ps`
- If containers aren't running, start them: `docker compose up -d`
- Wait for services to be healthy (~40 seconds)
3. **Run System Test**
- Execute: `cd /home/wall-o/projects/mtgonline/backend && python test_system.py`
- This tests:
- Database connections (PostgreSQL + Redis)
- Cache operations
- Database tables
- API endpoints (health, card search, statistics, auth)
4. **Analyze Results**
- If tests fail, investigate and fix issues
- Common issues:
- Database not running
- Redis connection failed
- API routes not registered
- Environment variables not set
5. **Update State**
- Update `state.json` with test results
- Note any bugs found and fixed
- Document what's working and what needs attention
6. **Report Findings**
- Summarize test results (pass/fail rates)
- List any issues found
- Recommend next steps
## Expected Test Coverage
The system test (`test_system.py`) should verify:
- ✅ MTG Online PostgreSQL connection
- ✅ MTG PostgreSQL connection
- ✅ Redis connection and operations
- ✅ MTG Online database tables (users, decks)
- ✅ MTG database tables (sets, cards)
- ✅ MTG card search functionality
- ✅ Health endpoint
- ✅ Card search API endpoint
- ✅ Statistics API endpoint
- ✅ Authentication endpoint
## Success Criteria
All tests should pass (100% success rate) before considering the backend ready for frontend development.
## Commands Reference
```bash
# Check Docker status
cd /home/wall-o/projects/mtgonline && docker compose ps
# Start containers
cd /home/wall-o/projects/mtgonline && docker compose up -d
# Wait for health (40 seconds)
sleep 40
# Run system test
cd /home/wall-o/projects/mtgonline/backend && python test_system.py
# Check logs if issues
cd /home/wall-o/projects/mtgonline && docker compose logs backend
```
## Important Notes
- All code execution must be as user `wall-o` (not root)
- Use sudo -u wall-o when running commands
- Pin versions in requirements.txt
- Follow PEP 8 for Python code
- Log errors with context
- One change per commit, descriptive messages
+4 -4
View File
@@ -1,7 +1,7 @@
""" """
Cockatrice Web Application MTG Online Web Application
A modern web-based implementation of the Cockatrice multiplayer Magic: The Gathering platform. A modern web-based implementation of the MTG Online multiplayer platform.
Built with FastAPI, WebSocket, and protocol buffer compatibility. Built with FastAPI, WebSocket, and protocol buffer compatibility.
## Features ## Features
@@ -23,11 +23,11 @@ Built with FastAPI, WebSocket, and protocol buffer compatibility.
- Database: PostgreSQL with async driver - Database: PostgreSQL with async driver
- Authentication: JWT tokens with bcrypt password hashing - Authentication: JWT tokens with bcrypt password hashing
- Game Server: WebSocket-based real-time multiplayer - Game Server: WebSocket-based real-time multiplayer
- Protocol: Compatible with Cockatrice protocol buffer messages - Protocol: Compatible with MTG Online protocol buffer messages
## License ## License
MIT License MIT License
""" """
__version__ = "0.1.0" __version__ = "0.1.0"
__author__ = "Cockatrice Web Team" __author__ = "MTG Online Web Team"
+1
View File
@@ -0,0 +1 @@
# Core package
+3 -3
View File
@@ -2,7 +2,7 @@
Database engine and session management. Database engine and session management.
Provides async SQLAlchemy engine and session factory for dependency injection. Provides async SQLAlchemy engine and session factory for dependency injection.
Supports dual database connections for cockatrice app and mtgjson data. Supports dual database connections for mtgonline app and mtgjson data.
""" """
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import DeclarativeBase
@@ -10,7 +10,7 @@ from app.core.settings import get_settings
settings = get_settings() settings = get_settings()
# Primary database engine (cockatrice app) # Primary database engine (mtgonline app)
engine = create_async_engine( engine = create_async_engine(
settings.DATABASE_URL, settings.DATABASE_URL,
echo=settings.DEBUG, echo=settings.DEBUG,
@@ -50,7 +50,7 @@ __all__ = ["Base", "get_db", "async_session", "engine", "mtg_get_db", "mtg_async
async def get_db() -> AsyncSession: async def get_db() -> AsyncSession:
"""FastAPI dependency that provides a database session for the cockatrice app.""" """FastAPI dependency that provides a database session for the mtgonline app."""
async with async_session() as session: async with async_session() as session:
try: try:
yield session yield session
+9 -6
View File
@@ -12,17 +12,17 @@ class Settings(BaseSettings):
"""Application settings loaded from environment variables or .env file.""" """Application settings loaded from environment variables or .env file."""
# Application # Application
APP_NAME: str = "Cockatrice Web" APP_NAME: str = "MTG Online"
APP_VERSION: str = "0.2.0" APP_VERSION: str = "0.2.0"
DEBUG: bool = False DEBUG: bool = False
SECRET_KEY: str = "change-me-in-production" SECRET_KEY: str = "change-me-in-production"
JWT_SECRET_KEY: str = "change-me-in-production" JWT_SECRET_KEY: str = "change-me-in-production"
# Database - Primary (cockatrice app) # Database - Primary (mtgonline app)
DATABASE_URL: str = "postgresql+asyncpg://cockatrice:cockatrice_pass@localhost:5432/cockatrice" DATABASE_URL: str = "postgresql+asyncpg://mtgonline:mtgonline_pass@localhost:5432/mtgonline"
# Database - Secondary (mtgjson data) # Database - Secondary (mtgjson data)
MTG_DATABASE_URL: str = "postgresql+asyncpg://cockatrice:cockatrice_pass@localhost:5432/mtgdata" MTG_DATABASE_URL: str = "postgresql+asyncpg://mtgonline:mtgonline_pass@localhost:5432/mtgdata"
# Redis # Redis
REDIS_URL: str = "redis://localhost:6379/0" REDIS_URL: str = "redis://localhost:6379/0"
@@ -55,12 +55,15 @@ class Settings(BaseSettings):
# Database configuration # Database configuration
DB_CONFIG: dict = { DB_CONFIG: dict = {
"engine": "postgresql+asyncpg", "engine": "postgresql+asyncpg",
"user": "cockatrice", "user": "mtgonline",
"password": "cockatrice_pass", "password": "mtgonline_pass",
"host": "postgres", "host": "postgres",
"port": 5432, "port": 5432,
} }
# Logging
LOG_LEVEL: str = "INFO"
# Redis configuration # Redis configuration
REDIS_CONFIG: dict = { REDIS_CONFIG: dict = {
"host": "redis", "host": "redis",
+104 -3
View File
@@ -3,14 +3,27 @@ FastAPI application factory and middleware setup.
Configures CORS, authentication, and error handling. Configures CORS, authentication, and error handling.
""" """
import asyncio
import logging
from datetime import datetime
from pathlib import Path
from typing import Dict, Any
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import text
from app.core.settings import get_settings from app.core.settings import get_settings
from app.routers import auth, users, decks, rooms, games, admin, card_router from app.routers import auth, users, decks, rooms, games, admin, card_router
settings = get_settings() settings = get_settings()
# Configure logging
logging.basicConfig(level=getattr(logging, settings.LOG_LEVEL))
logger = logging.getLogger(__name__)
app = FastAPI( app = FastAPI(
title=settings.APP_NAME, title=settings.APP_NAME,
version=settings.APP_VERSION, version=settings.APP_VERSION,
@@ -27,6 +40,68 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
# Track initialization status
mtg_data_ready = False
mtg_data_count = 0
async def check_mtg_data_count() -> int:
"""Check if MTG data has been loaded."""
try:
engine = create_async_engine(settings.MTG_DATABASE_URL)
async with AsyncSession(engine) as session:
stmt = text("SELECT COUNT(*) FROM mtg_cards")
result = await session.execute(stmt)
count = result.scalar()
await engine.dispose()
return count or 0
except Exception as e:
logger.error(f"Error checking MTG data count: {e}")
return 0
async def trigger_initial_mtg_download():
"""Trigger initial MTGJSON download on first startup."""
global mtg_data_ready, mtg_data_count
logger.info("Checking MTG data status...")
count = await check_mtg_data_count()
if count == 0:
logger.info("No MTG data found. Triggering initial download...")
mtg_data_ready = False
try:
# Import and run the refresh script
from app.scripts.refresh_mtg import main as refresh_main
await refresh_main()
# Re-check count after download
mtg_data_count = await check_mtg_data_count()
mtg_data_ready = mtg_data_count > 0
if mtg_data_ready:
logger.info(f"MTG data loaded successfully: {mtg_data_count} cards")
else:
logger.warning("MTG data download completed but no cards found")
except Exception as e:
logger.error(f"Failed to load MTG data: {e}")
mtg_data_ready = False
else:
logger.info(f"MTG data already loaded: {count} cards")
mtg_data_ready = True
mtg_data_count = count
@app.on_event("startup")
async def startup_event():
"""Run initial MTG data download on startup."""
logger.info("Starting MTG Online backend...")
# Run MTG data download in background to not block startup
asyncio.create_task(trigger_initial_mtg_download())
# Global exception handlers # Global exception handlers
@app.exception_handler(Exception) @app.exception_handler(Exception)
@@ -49,6 +124,32 @@ app.include_router(admin.router, prefix="/api/v1/admin", tags=["Admin"])
@app.get("/health") @app.get("/health")
async def health_check(): async def health_check() -> Dict[str, Any]:
"""Health check endpoint for monitoring.""" """Health check endpoint with MTG data status."""
return {"status": "healthy", "version": settings.APP_VERSION} health_status = {
"status": "healthy",
"version": settings.APP_VERSION,
"timestamp": datetime.now().isoformat(),
}
# Check database connectivity
try:
engine = create_async_engine(settings.DATABASE_URL)
async with AsyncSession(engine) as session:
await session.execute(text("SELECT 1"))
await engine.dispose()
health_status["database"] = "connected"
except Exception as e:
health_status["status"] = "degraded"
health_status["database"] = f"error: {str(e)}"
# Check MTG data status
health_status["mtg_data"] = {
"ready": mtg_data_ready,
"count": mtg_data_count,
}
if not mtg_data_ready:
health_status["status"] = "initializing"
return health_status
+1
View File
@@ -0,0 +1 @@
# Models package
+19 -19
View File
@@ -1,5 +1,5 @@
""" """
SQLAlchemy ORM models for the Cockatrice database. SQLAlchemy ORM models for the MTG Online database.
Mirrors the original MySQL schema with modern PostgreSQL features. Mirrors the original MySQL schema with modern PostgreSQL features.
""" """
@@ -11,7 +11,7 @@ from app.core.database import Base
class User(Base): class User(Base):
"""User account model.""" """User account model."""
__tablename__ = "cockatrice_users" __tablename__ = "mtgonline_users"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
username = Column(String(64), unique=True, nullable=False, index=True) username = Column(String(64), unique=True, nullable=False, index=True)
@@ -47,12 +47,12 @@ class User(Base):
class DecklistFolder(Base): class DecklistFolder(Base):
"""User deck folder.""" """User deck folder."""
__tablename__ = "cockatrice_decklist_folders" __tablename__ = "mtgonline_decklist_folders"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
owner_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=False) owner_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False)
name = Column(String(255), nullable=False) name = Column(String(255), nullable=False)
parent_id = Column(Integer, ForeignKey("cockatrice_decklist_folders.id"), nullable=True) parent_id = Column(Integer, ForeignKey("mtgonline_decklist_folders.id"), nullable=True)
creation_date = Column(DateTime, server_default=func.now()) creation_date = Column(DateTime, server_default=func.now())
# Relationships # Relationships
@@ -64,11 +64,11 @@ class DecklistFolder(Base):
class DecklistFile(Base): class DecklistFile(Base):
"""Deck file stored in a folder.""" """Deck file stored in a folder."""
__tablename__ = "cockatrice_decklist_files" __tablename__ = "mtgonline_decklist_files"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
folder_id = Column(Integer, ForeignKey("cockatrice_decklist_folders.id"), nullable=True) folder_id = Column(Integer, ForeignKey("mtgonline_decklist_folders.id"), nullable=True)
owner_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=False) owner_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False)
name = Column(String(255), nullable=False) name = Column(String(255), nullable=False)
content = Column(Text, nullable=False) # Native XML or plain text deck format content = Column(Text, nullable=False) # Native XML or plain text deck format
format = Column(String(50), default="native") # 'native' or 'plain' format = Column(String(50), default="native") # 'native' or 'plain'
@@ -84,7 +84,7 @@ class DecklistFile(Base):
class Room(Base): class Room(Base):
"""Chat room.""" """Chat room."""
__tablename__ = "cockatrice_rooms" __tablename__ = "mtgonline_rooms"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), unique=True, nullable=False) name = Column(String(100), unique=True, nullable=False)
@@ -99,10 +99,10 @@ class Room(Base):
class RoomGameType(Base): class RoomGameType(Base):
"""Game type definition for a room.""" """Game type definition for a room."""
__tablename__ = "cockatrice_rooms_gametypes" __tablename__ = "mtgonline_rooms_gametypes"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
room_id = Column(Integer, ForeignKey("cockatrice_rooms.id"), nullable=False) room_id = Column(Integer, ForeignKey("mtgonline_rooms.id"), nullable=False)
name = Column(String(100), nullable=False) name = Column(String(100), nullable=False)
description = Column(Text, nullable=True) description = Column(Text, nullable=True)
@@ -112,10 +112,10 @@ class RoomGameType(Base):
class Ban(Base): class Ban(Base):
"""User ban record.""" """User ban record."""
__tablename__ = "cockatrice_bans" __tablename__ = "mtgonline_bans"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=False) user_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False)
server_id = Column(Integer, nullable=True) server_id = Column(Integer, nullable=True)
reason = Column(Text, nullable=False) reason = Column(Text, nullable=False)
moderators = Column(String(255), nullable=True) # Admin usernames moderators = Column(String(255), nullable=True) # Admin usernames
@@ -133,11 +133,11 @@ class Ban(Base):
class GameLog(Base): class GameLog(Base):
"""Game log entry.""" """Game log entry."""
__tablename__ = "cockatrice_log" __tablename__ = "mtgonline_log"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
room_id = Column(Integer, ForeignKey("cockatrice_rooms.id"), nullable=True) room_id = Column(Integer, ForeignKey("mtgonline_rooms.id"), nullable=True)
player_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=True) player_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=True)
message = Column(Text, nullable=False) message = Column(Text, nullable=False)
timestamp = Column(DateTime, server_default=func.now()) timestamp = Column(DateTime, server_default=func.now())
@@ -148,12 +148,12 @@ class GameLog(Base):
class AuditLog(Base): class AuditLog(Base):
"""Audit trail for administrative actions.""" """Audit trail for administrative actions."""
__tablename__ = "cockatrice_audit" __tablename__ = "mtgonline_audit"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
admin_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=False) admin_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False)
action_type = Column(String(50), nullable=False) # 'ban', 'unban', 'warn', etc. action_type = Column(String(50), nullable=False) # 'ban', 'unban', 'warn', etc.
target_user_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=True) target_user_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=True)
details = Column(Text, nullable=True) details = Column(Text, nullable=True)
ip_address = Column(String(45), nullable=True) ip_address = Column(String(45), nullable=True)
timestamp = Column(DateTime, server_default=func.now()) timestamp = Column(DateTime, server_default=func.now())
+8 -8
View File
@@ -1,7 +1,7 @@
""" """
MTG Database Monitor MTG Database Monitor
Monitors PostgreSQL database metrics for both cockatrice and mtgdata databases. Monitors PostgreSQL database metrics for both mtgonline and mtgdata databases.
Tracks: Tracks:
- Database size and growth - Database size and growth
- Table sizes - Table sizes
@@ -28,14 +28,14 @@ MTG_DB = settings.MTG_DATABASE_URL
class MtgMonitor: class MtgMonitor:
def __init__(self): def __init__(self):
self.mtg_conn = None self.mtg_conn = None
self.cockatrice_conn = None self.mtgonline_conn = None
self.metrics = {} self.metrics = {}
async def connect(self): async def connect(self):
"""Establish connections to both databases.""" """Establish connections to both databases."""
try: try:
self.mtg_conn = await asyncpg.connect(MTG_DB) self.mtg_conn = await asyncpg.connect(MTG_DB)
self.cockatrice_conn = await asyncpg.connect(COCKATRICE_DB) self.mtgonline_conn = await asyncpg.connect(COCKATRICE_DB)
return True return True
except Exception as e: except Exception as e:
print(f"Connection error: {e}") print(f"Connection error: {e}")
@@ -45,8 +45,8 @@ class MtgMonitor:
"""Close database connections.""" """Close database connections."""
if self.mtg_conn: if self.mtg_conn:
await self.mtg_conn.close() await self.mtg_conn.close()
if self.cockatrice_conn: if self.mtgonline_conn:
await self.cockatrice_conn.close() await self.mtgonline_conn.close()
async def get_database_size(self) -> Dict[str, float]: async def get_database_size(self) -> Dict[str, float]:
"""Get size of databases in GB.""" """Get size of databases in GB."""
@@ -56,14 +56,14 @@ class MtgMonitor:
SELECT pg_database_size(current_database()) as size SELECT pg_database_size(current_database()) as size
""") """)
# Cockatrice database size # MTG Online database size
cockatrice_size = await self.cockatrice_conn.fetchval(""" mtgonline_size = await self.mtgonline_conn.fetchval("""
SELECT pg_database_size(current_database()) as size SELECT pg_database_size(current_database()) as size
""") """)
return { return {
"mtgdata": mtg_size / (1024**3), # Convert to GB "mtgdata": mtg_size / (1024**3), # Convert to GB
"cockatrice": cockatrice_size / (1024**3) "mtgonline": mtgonline_size / (1024**3)
} }
except Exception as e: except Exception as e:
print(f"Error getting database sizes: {e}") print(f"Error getting database sizes: {e}")
+1
View File
@@ -0,0 +1 @@
# Routers package
+1 -1
View File
@@ -21,7 +21,7 @@ async def list_games(
): ):
"""List active games.""" """List active games."""
# This would query a games table - simplified for now # This would query a games table - simplified for now
# In production, you'd have a CockatriceGames model # In production, you'd have a MTG OnlineGames model
return [] return []
+1
View File
@@ -0,0 +1 @@
# Games package
+1
View File
@@ -0,0 +1 @@
# Schemas package
+1 -1
View File
@@ -1,4 +1,4 @@
"""Cockatrice protocol constants and message definitions.""" """MTG Online protocol constants and message definitions."""
import enum import enum
+6 -6
View File
@@ -19,7 +19,7 @@ class CardInfo:
class DeckParser: class DeckParser:
"""Parse Cockatrice deck list formats.""" """Parse MTG Online deck list formats."""
# Regex patterns for deck parsing # Regex patterns for deck parsing
CARD_LINE_RE = re.compile(r"^\s*[\w\[\(\{].*$") CARD_LINE_RE = re.compile(r"^\s*[\w\[\(\{].*$")
@@ -149,8 +149,8 @@ class DeckParser:
return "\n".join(lines) return "\n".join(lines)
def to_native_xml(self, cards: List[CardInfo]) -> str: def to_native_xml(self, cards: List[CardInfo]) -> str:
"""Convert cards to Cockatrice native XML format.""" """Convert cards to MTG Online native XML format."""
xml_lines = ['<cockatrice_deck version="1">'] xml_lines = ['<mtgonline_deck version="1">']
# Group cards by zone (main/sideboard) # Group cards by zone (main/sideboard)
main_cards = [c for c in cards if not c.name.startswith("[SB]")] main_cards = [c for c in cards if not c.name.startswith("[SB]")]
@@ -169,11 +169,11 @@ class DeckParser:
xml_lines.append(f' <card name="{clean_name}" count="{card.count}" />') xml_lines.append(f' <card name="{clean_name}" count="{card.count}" />')
xml_lines.append(' </zone>') xml_lines.append(' </zone>')
xml_lines.append('</cockatrice_deck>') xml_lines.append('</mtgonline_deck>')
return "\n".join(xml_lines) return "\n".join(xml_lines)
def from_native_xml(self, xml: str) -> List[CardInfo]: def from_native_xml(self, xml: str) -> List[CardInfo]:
"""Parse Cockatrice native XML format.""" """Parse MTG Online native XML format."""
cards = [] cards = []
# Simple XML parsing (in production, use proper XML parser) # Simple XML parsing (in production, use proper XML parser)
@@ -191,7 +191,7 @@ class DeckParser:
def parse_deck(text: str) -> List[CardInfo]: def parse_deck(text: str) -> List[CardInfo]:
"""Parse a deck list from plain text or native XML.""" """Parse a deck list from plain text or native XML."""
parser = DeckParser() parser = DeckParser()
if text.strip().startswith("<cockatrice_deck"): if text.strip().startswith("<mtgonline_deck"):
return parser.from_native_xml(text) return parser.from_native_xml(text)
else: else:
return parser.parse_plain_text(text) return parser.parse_plain_text(text)
+2 -2
View File
@@ -239,7 +239,7 @@ async def game_websocket_endpoint(websocket: WebSocket, game_id: int):
await room.send_to_player(player_id, { await room.send_to_player(player_id, {
"type": "pong", "type": "pong",
"timestamp": datetime.now().isoformat(), "timestamp": datetime.now().isoformat(),
})) })
except WebSocketDisconnect: except WebSocketDisconnect:
# Player disconnected # Player disconnected
await room.remove_player(player_id) await room.remove_player(player_id)
@@ -297,7 +297,7 @@ async def process_game_command(room: GameRoom, player_id: int, command: dict):
await room.send_to_player(player_id, { await room.send_to_player(player_id, {
"type": "error", "type": "error",
"message": f"Invalid command type: {cmd_type}", "message": f"Invalid command type: {cmd_type}",
})) })
return return
# Broadcast command as game event # Broadcast command as game event
+223
View File
@@ -0,0 +1,223 @@
"""
Database initialization script for MTG Online backend.
Creates all necessary tables matching the SQLAlchemy ORM models
for both databases.
"""
import asyncio
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import text
from app.core.settings import get_settings
async def setup_mtg_online_database():
"""Create tables for the mtgonline database."""
settings = get_settings()
engine = create_async_engine(settings.MTGO_DATABASE_URL)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as session:
# Create tables
tables = [
"""
CREATE TABLE IF NOT EXISTS mtgonline_users (
id SERIAL PRIMARY KEY,
username VARCHAR(64) UNIQUE NOT NULL,
password_hash VARCHAR(128) NOT NULL,
salt VARCHAR(128) NOT NULL,
email VARCHAR(255),
country VARCHAR(2),
real_name VARCHAR(128),
avatar_bmp TEXT,
privlevel VARCHAR(50) DEFAULT 'User',
is_active BOOLEAN DEFAULT TRUE,
is_banned BOOLEAN DEFAULT FALSE,
ban_reason TEXT,
ban_ends TIMESTAMP,
vip_status INTEGER DEFAULT 0,
vip_expiry TIMESTAMP,
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP
)
""",
"""
CREATE TABLE IF NOT EXISTS mtgonline_decklist_folders (
id SERIAL PRIMARY KEY,
owner_id INTEGER REFERENCES mtgonline_users(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
parent_id INTEGER REFERENCES mtgonline_decklist_folders(id) ON DELETE CASCADE,
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""",
"""
CREATE TABLE IF NOT EXISTS mtgonline_decklist_files (
id SERIAL PRIMARY KEY,
folder_id INTEGER REFERENCES mtgonline_decklist_folders(id) ON DELETE CASCADE,
owner_id INTEGER REFERENCES mtgonline_users(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
format VARCHAR(50) DEFAULT 'native',
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""",
"""
CREATE TABLE IF NOT EXISTS mtgonline_rooms (
id SERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
is_password_protected BOOLEAN DEFAULT FALSE,
password_hash VARCHAR(128),
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""",
"""
CREATE TABLE IF NOT EXISTS mtgonline_rooms_gametypes (
id SERIAL PRIMARY KEY,
room_id INTEGER REFERENCES mtgonline_rooms(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
description TEXT
)
""",
"""
CREATE TABLE IF NOT EXISTS mtgonline_bans (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES mtgonline_users(id) ON DELETE CASCADE,
server_id INTEGER,
reason TEXT NOT NULL,
moderators VARCHAR(255),
ip_address VARCHAR(45),
expiration_time TIMESTAMP,
active BOOLEAN DEFAULT TRUE,
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""",
"""
CREATE TABLE IF NOT EXISTS mtgonline_log (
id SERIAL PRIMARY KEY,
room_id INTEGER REFERENCES mtgonline_rooms(id) ON DELETE CASCADE,
player_id INTEGER REFERENCES mtgonline_users(id) ON DELETE CASCADE,
message TEXT NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""",
"""
CREATE TABLE IF NOT EXISTS mtgonline_audit (
id SERIAL PRIMARY KEY,
admin_id INTEGER REFERENCES mtgonline_users(id) ON DELETE SET NULL,
action_type VARCHAR(50) NOT NULL,
target_user_id INTEGER REFERENCES mtgonline_users(id) ON DELETE SET NULL,
details TEXT,
ip_address VARCHAR(45),
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""",
]
for table_sql in tables:
await session.execute(text(table_sql))
# Create indexes
indexes = [
"CREATE INDEX IF NOT EXISTS idx_decks_owner ON mtgonline_decklist_files(owner_id);",
"CREATE INDEX IF NOT EXISTS idx_decks_folder ON mtgonline_decklist_files(folder_id);",
"CREATE INDEX IF NOT EXISTS idx_bans_active ON mtgonline_bans(active);",
"CREATE INDEX IF NOT EXISTS idx_log_timestamp ON mtgonline_log(timestamp);",
]
for idx_sql in indexes:
await session.execute(text(idx_sql))
print("✓ All mtgonline tables created")
await engine.dispose()
async def setup_mtg_data_database():
"""Create tables for the mtgdata database."""
settings = get_settings()
engine = create_async_engine(settings.MTG_DATABASE_URL)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as session:
# Create tables
tables = [
"""
CREATE TABLE IF NOT EXISTS mtg_sets (
id SERIAL PRIMARY KEY,
code VARCHAR(10) UNIQUE NOT NULL,
name VARCHAR(255),
type VARCHAR(100),
release_date DATE,
base_set_size INTEGER,
total_size INTEGER,
is_foil_only BOOLEAN,
is_non_foil_only BOOLEAN,
digital BOOLEAN,
icon_svg_url TEXT,
parent_code VARCHAR(10),
mtgo_code VARCHAR(10),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""",
"""
CREATE TABLE IF NOT EXISTS mtg_cards (
id SERIAL PRIMARY KEY,
set_id INTEGER REFERENCES mtg_sets(id) ON DELETE CASCADE,
name VARCHAR(255),
mana_cost VARCHAR(255),
type_line VARCHAR(255),
oracle_text TEXT,
power VARCHAR(50),
toughness VARCHAR(50),
rarity VARCHAR(50),
layout VARCHAR(50),
artist VARCHAR(255),
flavor_text TEXT,
numbers VARCHAR(100),
identifiers TEXT,
images TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""",
]
for table_sql in tables:
await session.execute(text(table_sql))
# Create indexes
indexes = [
"CREATE INDEX IF NOT EXISTS idx_mtg_cards_name ON mtg_cards(name);",
"CREATE INDEX IF NOT EXISTS idx_mtg_cards_type ON mtg_cards(type_line);",
"CREATE INDEX IF NOT EXISTS idx_mtg_cards_rarity ON mtg_cards(rarity);",
"CREATE INDEX IF NOT EXISTS idx_mtg_cards_set_id ON mtg_cards(set_id);",
]
for idx_sql in indexes:
await session.execute(text(idx_sql))
print("✓ All mtgdata tables created")
await engine.dispose()
async def main():
"""Main initialization function."""
print("MTG Online Database Initialization")
print("=" * 50)
# Setup mtgonline database
print("\nSetting up mtgonline database...")
await setup_mtg_online_database()
# Setup mtgdata database
print("\nSetting up mtgdata database...")
await setup_mtg_data_database()
print("\n✓ Database initialization complete!")
if __name__ == "__main__":
asyncio.run(main())
+26 -16
View File
@@ -3,7 +3,7 @@
Backend System Test Script Backend System Test Script
Tests all backend components: Tests all backend components:
- Database connections (Cockatrice + MTG) - Database connections (MTG Online + MTG)
- Redis connection and caching - Redis connection and caching
- Card database queries - Card database queries
- API endpoints - API endpoints
@@ -21,10 +21,20 @@ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy import text from sqlalchemy import text
# Database URLs # Database URLs
COCKATRICE_DB = "postgresql+asyncpg://cockatrice:cockatrice_pass@localhost:5432/cockatrice" COCKATRICE_DB = "postgresql+asyncpg://mtgonline:mtgonline_pass@localhost:5432/mtgonline"
MTG_DB = "postgresql+asyncpg://cockatrice:cockatrice_pass@localhost:5432/mtgdata" MTG_DB = "postgresql+asyncpg://mtgonline:mtgonline_pass@localhost:5432/mtgdata"
REDIS_URL = "redis://localhost:6379/0" REDIS_URL = "redis://localhost:6379/0"
# Test URLs - detect environment
import socket
try:
# Try Docker network first
socket.getaddrinfo('mtg_backend', 8000)
API_BASE = 'http://mtg_backend:8000'
except:
# Fall back to host port
API_BASE = 'http://localhost:8001'
print(f"Using API base: {API_BASE}")
results = { results = {
"tests_run": 0, "tests_run": 0,
"tests_passed": 0, "tests_passed": 0,
@@ -97,29 +107,29 @@ async def test_cache_operations() -> bool:
print(f"✗ Redis cache operations: FAILED - {str(e)}") print(f"✗ Redis cache operations: FAILED - {str(e)}")
return False return False
async def test_cockatrice_tables() -> bool: async def test_mtgonline_tables() -> bool:
"""Test Cockatrice database tables.""" """Test MTG Online database tables."""
results["tests_run"] += 1 results["tests_run"] += 1
try: try:
engine = create_async_engine(COCKATRICE_DB, echo=False) engine = create_async_engine(COCKATRICE_DB, echo=False)
async with engine.connect() as conn: async with engine.connect() as conn:
# Test users table # Test users table
result = await conn.execute(text("SELECT COUNT(*) FROM cockatrice_users")) result = await conn.execute(text("SELECT COUNT(*) FROM mtgonline_users"))
user_count = result.scalar() user_count = result.scalar()
# Test decks table # Test decks table
result = await conn.execute(text("SELECT COUNT(*) FROM cockatrice_decklist_files")) result = await conn.execute(text("SELECT COUNT(*) FROM mtgonline_decklist_files"))
deck_count = result.scalar() deck_count = result.scalar()
await engine.dispose() await engine.dispose()
results["tests_passed"] += 1 results["tests_passed"] += 1
print(f"Cockatrice tables: OK (users: {user_count}, decks: {deck_count})") print(f"MTG Online tables: OK (users: {user_count}, decks: {deck_count})")
return True return True
except Exception as e: except Exception as e:
results["tests_failed"] += 1 results["tests_failed"] += 1
results["failures"].append(f"Cockatrice tables: {str(e)}") results["failures"].append(f"MTG Online tables: {str(e)}")
print(f"Cockatrice tables: FAILED - {str(e)}") print(f"MTG Online tables: FAILED - {str(e)}")
return False return False
async def test_mtg_tables() -> bool: async def test_mtg_tables() -> bool:
@@ -181,7 +191,7 @@ async def test_health_endpoint() -> bool:
results["tests_run"] += 1 results["tests_run"] += 1
try: try:
import requests import requests
response = requests.get("http://localhost:8000/health", timeout=5) response = requests.get("http://localhost:8001/health", timeout=5)
if response.status_code == 200: if response.status_code == 200:
results["tests_passed"] += 1 results["tests_passed"] += 1
@@ -204,7 +214,7 @@ async def test_card_search_endpoint() -> bool:
try: try:
import requests import requests
response = requests.get( response = requests.get(
"http://localhost:8000/mtg/cards/search", "http://localhost:8001/api/v1/mtg/cards/search",
params={"q": "Lightning Bolt", "limit": 5}, params={"q": "Lightning Bolt", "limit": 5},
timeout=10 timeout=10
) )
@@ -231,7 +241,7 @@ async def test_statistics_endpoint() -> bool:
try: try:
import requests import requests
response = requests.get( response = requests.get(
"http://localhost:8000/mtg/cards/statistics", "http://localhost:8001/api/v1/mtg/cards/statistics",
timeout=10 timeout=10
) )
@@ -258,7 +268,7 @@ async def test_auth_endpoint() -> bool:
try: try:
import requests import requests
response = requests.post( response = requests.post(
"http://localhost:8000/auth/register", "http://localhost:8001/api/v1/auth/register",
json={ json={
"username": "test_user", "username": "test_user",
"email": "test@example.com", "email": "test@example.com",
@@ -295,7 +305,7 @@ async def run_all_tests():
# Database connections # Database connections
print("Testing Database Connections:") print("Testing Database Connections:")
print("-" * 60) print("-" * 60)
await test_connection(COCKATRICE_DB, "Cockatrice PostgreSQL") await test_connection(COCKATRICE_DB, "MTG Online PostgreSQL")
await test_connection(MTG_DB, "MTG PostgreSQL") await test_connection(MTG_DB, "MTG PostgreSQL")
await test_redis() await test_redis()
print() print()
@@ -309,7 +319,7 @@ async def run_all_tests():
# Database tables # Database tables
print("Testing Database Tables:") print("Testing Database Tables:")
print("-" * 60) print("-" * 60)
await test_cockatrice_tables() await test_mtgonline_tables()
await test_mtg_tables() await test_mtg_tables()
await test_mtg_card_query() await test_mtg_card_query()
print() print()
+4 -4
View File
@@ -4,15 +4,15 @@ services:
postgres: postgres:
image: postgres:14-alpine image: postgres:14-alpine
environment: environment:
POSTGRES_DB: cockatrice POSTGRES_DB: mtgonline
POSTGRES_USER: cockatrice_user POSTGRES_USER: mtgonline_user
POSTGRES_PASSWORD: cockatrice_password POSTGRES_PASSWORD: mtgonline_password
ports: ports:
- "5432:5432" - "5432:5432"
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U cockatrice_user"] test: ["CMD-SHELL", "pg_isready -U mtgonline_user"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
+62 -14
View File
@@ -1,15 +1,15 @@
version: "3.8" version: "3.8"
services: services:
# PostgreSQL - hosts both Cockatrice app data and mtgjson data # PostgreSQL - MTG Online App Database
postgres: postgres:
image: postgres:16-alpine image: postgres:16-alpine
container_name: mtg_postgres container_name: mtgonline_postgres
restart: unless-stopped restart: unless-stopped
environment: environment:
POSTGRES_USER: ${POSTGRES_USER:-cockatrice} POSTGRES_USER: ${POSTGRES_USER:-mtgonline}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-cockatrice_pass} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-mtgonline_pass}
POSTGRES_DB: ${POSTGRES_DB:-cockatrice} POSTGRES_DB: ${POSTGRES_DB:-mtgonline}
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --lc-collate=C --lc-ctype=C" POSTGRES_INITDB_ARGS: "--encoding=UTF8 --lc-collate=C --lc-ctype=C"
ports: ports:
- "${POSTGRES_PORT:-5432}:5432" - "${POSTGRES_PORT:-5432}:5432"
@@ -19,31 +19,75 @@ services:
networks: networks:
- mtg_network - mtg_network
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-cockatrice}"] test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-mtgonline}"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
start_period: 30s start_period: 30s
# PostgreSQL - MTG Data Database
postgres-mtgdata:
image: postgres:16-alpine
container_name: mtgonline_postgres_mtgdata
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-mtgonline}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-mtgonline_pass}
POSTGRES_DB: ${MTGDATA_DB:-mtgdata}
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --lc-collate=C --lc-ctype=C"
ports:
- "${MTGDATA_PORT:-5433}:5432"
volumes:
- postgres_mtgdata_data:/var/lib/postgresql/data
- ./scripts/init-mtgdata.sql:/docker-entrypoint-initdb.d/01-init.sql
networks:
- mtg_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-mtgonline}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
# Redis cache
redis:
image: redis:7-alpine
container_name: mtgonline_redis
restart: unless-stopped
ports:
- "${REDIS_PORT:-6379}:6379"
networks:
- mtg_network
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
# Backend API # Backend API
backend: backend:
build: build:
context: ./backend context: ./backend
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: mtg_backend container_name: mtgonline_backend
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
postgres-mtgdata:
condition: service_healthy
redis:
condition: service_healthy
environment: environment:
- DATABASE_URL=${DATABASE_URL:-postgresql+asyncpg://cockatrice:cockatrice_pass@postgres:5432/cockatrice} - DATABASE_URL=${DATABASE_URL:-postgresql+asyncpg://mtgonline:mtgonline_pass@postgres:5432/mtgonline}
- MTG_DATABASE_URL=${MTG_DATABASE_URL:-postgresql+asyncpg://cockatrice:cockatrice_pass@postgres:5432/mtgdata} - MTG_DATABASE_URL=${MTG_DATABASE_URL:-postgresql+asyncpg://mtgonline:mtgonline_pass@postgres-mtgdata:5432/mtgdata}
- JWT_SECRET_KEY=${JWT_SECRET_KEY:-change-me-in-production} - JWT_SECRET_KEY=${JWT_SECRET_KEY:-change-me-in-production}
- JWT_ALGORITHM=${JWT_ALGORITHM:-HS256} - JWT_ALGORITHM=${JWT_ALGORITHM:-HS256}
- ACCESS_TOKEN_EXPIRE_MINUTES=${ACCESS_TOKEN_EXPIRE_MINUTES:-60} - ACCESS_TOKEN_EXPIRE_MINUTES=${ACCESS_TOKEN_EXPIRE_MINUTES:-60}
- REFRESH_TOKEN_EXPIRE_DAYS=${REFRESH_TOKEN_EXPIRE_DAYS:-7} - REFRESH_TOKEN_EXPIRE_DAYS=${REFRESH_TOKEN_EXPIRE_DAYS:-7}
- CORS_ORIGINS=${CORS_ORIGINS:-["http://localhost:3000","http://localhost:8000"]} - CORS_ORIGINS=${CORS_ORIGINS:-["http://localhost:3000","http://localhost:8000"]}
- APP_NAME=${APP_NAME:-Cockatrice Web} - APP_NAME=${APP_NAME:-MTG Online}
- APP_VERSION=${APP_VERSION:-0.2.0} - APP_VERSION=${APP_VERSION:-0.2.0}
- DEBUG=${DEBUG:-False} - DEBUG=${DEBUG:-False}
- MTG_REFRESH_INTERVAL_DAYS=${MTG_REFRESH_INTERVAL_DAYS:-7} - MTG_REFRESH_INTERVAL_DAYS=${MTG_REFRESH_INTERVAL_DAYS:-7}
@@ -51,7 +95,7 @@ services:
- UPLOAD_DIR=${UPLOAD_DIR:-/app/uploads} - UPLOAD_DIR=${UPLOAD_DIR:-/app/uploads}
- LOG_LEVEL=${LOG_LEVEL:-INFO} - LOG_LEVEL=${LOG_LEVEL:-INFO}
ports: ports:
- "${BACKEND_PORT:-8000}:8000" - "${BACKEND_PORT:-8001}:8000"
volumes: volumes:
- mtg_data:/app/data - mtg_data:/app/data
- mtg_uploads:/app/uploads - mtg_uploads:/app/uploads
@@ -70,14 +114,16 @@ services:
build: build:
context: ./backend context: ./backend
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: mtg_refresh container_name: mtgonline_refresh
restart: "no" restart: "no"
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
postgres-mtgdata:
condition: service_healthy
environment: environment:
- DATABASE_URL=${DATABASE_URL:-postgresql+asyncpg://cockatrice:cockatrice_pass@postgres:5432/cockatrice} - DATABASE_URL=${DATABASE_URL:-postgresql+asyncpg://mtgonline:mtgonline_pass@postgres:5432/mtgonline}
- MTG_DATABASE_URL=${MTG_DATABASE_URL:-postgresql+asyncpg://cockatrice:cockatrice_pass@postgres:5432/mtgdata} - MTG_DATABASE_URL=${MTG_DATABASE_URL:-postgresql+asyncpg://mtgonline:mtgonline_pass@postgres-mtgdata:5432/mtgdata}
- DATA_DIR=${DATA_DIR:-/app/data} - DATA_DIR=${DATA_DIR:-/app/data}
- MTG_REFRESH_INTERVAL_DAYS=${MTG_REFRESH_INTERVAL_DAYS:-7} - MTG_REFRESH_INTERVAL_DAYS=${MTG_REFRESH_INTERVAL_DAYS:-7}
volumes: volumes:
@@ -89,6 +135,8 @@ services:
volumes: volumes:
postgres_data: postgres_data:
driver: local driver: local
postgres_mtgdata_data:
driver: local
mtg_data: mtg_data:
driver: local driver: local
mtg_uploads: mtg_uploads:
+2 -2
View File
@@ -1,5 +1,5 @@
-- Initialize MTG data database -- Initialize MTG Online app database
CREATE DATABASE mtgdata; -- Note: mtgdata database is created separately in init-mtgdata.sql
-- Create extensions -- Create extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+73
View File
@@ -0,0 +1,73 @@
-- Initialize MTG data database
-- Uses $POSTGRES_USER from environment
-- Create mtgdata database if it doesn't exist (owned by the POSTGRES_USER)
SELECT 'CREATE DATABASE mtgdata OWNER ' || current_user
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'mtgdata')\gexec
-- Connect to mtgdata and set up schema permissions
\c mtgdata
-- Grant all privileges to the app user
GRANT ALL PRIVILEGES ON DATABASE mtgdata TO current_user;
GRANT ALL ON SCHEMA public TO current_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO current_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO current_user;
-- Create extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
-- Create mtgjson tables
CREATE TABLE IF NOT EXISTS mtg_sets (
id SERIAL PRIMARY KEY,
code VARCHAR(10) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
type VARCHAR(50),
release_date DATE,
base_set_size INTEGER,
total_size INTEGER,
is_foil_only BOOLEAN DEFAULT FALSE,
is_non_foil_only BOOLEAN DEFAULT FALSE,
digital BOOLEAN DEFAULT FALSE,
icon_svg_url TEXT,
parent_code VARCHAR(10),
mtgo_code VARCHAR(10),
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS mtg_cards (
id SERIAL PRIMARY KEY,
set_id INTEGER REFERENCES mtg_sets(id),
name VARCHAR(255) NOT NULL,
mana_cost TEXT,
type_line VARCHAR(255),
oracle_text TEXT,
power VARCHAR(10),
toughness VARCHAR(10),
rarity VARCHAR(50),
layout VARCHAR(50),
artist VARCHAR(255),
flavor_text TEXT,
numbers VARCHAR(50),
identifiers JSONB,
images JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_mtg_cards_set_id ON mtg_cards(set_id);
CREATE INDEX idx_mtg_cards_name ON mtg_cards(name);
CREATE INDEX idx_mtg_cards_type ON mtg_cards(type_line);
CREATE TABLE IF NOT EXISTS mtg_refresh_log (
id SERIAL PRIMARY KEY,
refresh_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(50) NOT NULL,
cards_updated INTEGER DEFAULT 0,
sets_updated INTEGER DEFAULT 0,
error_message TEXT,
duration_seconds INTEGER
);
CREATE INDEX idx_mtg_refresh_log_date ON mtg_refresh_log(refresh_date);