Files
Claude (The Golden Chronicler #50) 04e9b407d5 feat: Migrate Arbiter and Modpack Version Checker to monorepo
WHAT WAS DONE:
- Migrated Arbiter (discord-oauth-arbiter) code to services/arbiter/
- Migrated Modpack Version Checker code to services/modpack-version-checker/
- Created .env.example for Arbiter with all required environment variables
- Moved systemd service file to services/arbiter/deploy/
- Organized directory structure per Gemini monorepo recommendations

WHY:
- Consolidate all service code in one repository
- Prepare for Gemini code review (Panel v1.12 compatibility check)
- Enable service-prefixed Git tagging (arbiter-v2.1.0, modpack-v1.0.0)
- Support npm workspaces for shared dependencies

SERVICES MIGRATED:
1. Arbiter (Discord OAuth bot) - Originally written by Gemini + Claude
   - Full source code from ops-manual docs/implementation/
   - Created comprehensive .env.example
   - Ready for Panel v1.12 compatibility verification

2. Modpack Version Checker (Python CLI tool)
   - Full source code from ops-manual docs/tasks/
   - Written for Panel v1.11, needs Gemini review for v1.12
   - Never had code review before

STILL TODO:
- Whitelist Manager - Pull from Billing VPS (38.68.14.188)
  - Currently deployed and running
  - Needs Panel v1.12 API compatibility fix (Task #86)
  - Requires SSH access to pull code

NEXT STEPS:
- Gemini code review for Panel v1.12 API compatibility
- Create package.json for each service
- Test npm workspaces integration
- Deploy after verification

FILES:
- services/arbiter/ (25 new files, full application)
- services/modpack-version-checker/ (21 new files, full application)

Signed-off-by: The Golden Chronicler <claude@firefrostgaming.com>
2026-03-31 21:52:42 +00:00

184 lines
6.5 KiB
Bash
Executable File

#!/bin/bash
# Auto-generated script to create all modpack-version-checker files
BASE_DIR="/home/claude/firefrost-operations-manual/docs/tasks/modpack-version-checker/code/modpack-version-checker"
cd "$BASE_DIR"
# Already created: src/modpack_checker/__init__.py, src/modpack_checker/config.py
# Create database.py
cat > src/modpack_checker/database.py << 'EOF'
"""SQLite database layer using SQLAlchemy 2.0 ORM."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import List, Optional
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, create_engine, select
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column, relationship
class _Base(DeclarativeBase):
pass
class _ModpackRow(_Base):
__tablename__ = "modpacks"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
curseforge_id: Mapped[int] = mapped_column(Integer, unique=True, nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
current_version: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
last_checked: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
notification_enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
history: Mapped[List["_CheckHistoryRow"]] = relationship(
back_populates="modpack", cascade="all, delete-orphan"
)
class _CheckHistoryRow(_Base):
__tablename__ = "check_history"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
modpack_id: Mapped[int] = mapped_column(ForeignKey("modpacks.id"), nullable=False)
checked_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
version_found: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
notification_sent: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
modpack: Mapped["_ModpackRow"] = relationship(back_populates="history")
@dataclass
class Modpack:
id: int
curseforge_id: int
name: str
current_version: Optional[str]
last_checked: Optional[datetime]
notification_enabled: bool
@classmethod
def _from_row(cls, row: _ModpackRow) -> "Modpack":
return cls(
id=row.id,
curseforge_id=row.curseforge_id,
name=row.name,
current_version=row.current_version,
last_checked=row.last_checked,
notification_enabled=row.notification_enabled,
)
@dataclass
class CheckHistory:
id: int
modpack_id: int
checked_at: datetime
version_found: Optional[str]
notification_sent: bool
@classmethod
def _from_row(cls, row: _CheckHistoryRow) -> "CheckHistory":
return cls(
id=row.id,
modpack_id=row.modpack_id,
checked_at=row.checked_at,
version_found=row.version_found,
notification_sent=row.notification_sent,
)
class Database:
def __init__(self, db_path: str) -> None:
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
self.engine = create_engine(f"sqlite:///{db_path}", echo=False)
_Base.metadata.create_all(self.engine)
def add_modpack(self, curseforge_id: int, name: str) -> Modpack:
with Session(self.engine) as session:
existing = session.scalar(
select(_ModpackRow).where(_ModpackRow.curseforge_id == curseforge_id)
)
if existing is not None:
raise ValueError(f"Modpack ID {curseforge_id} is already being tracked.")
row = _ModpackRow(curseforge_id=curseforge_id, name=name, notification_enabled=True)
session.add(row)
session.commit()
return Modpack._from_row(row)
def remove_modpack(self, curseforge_id: int) -> bool:
with Session(self.engine) as session:
row = session.scalar(
select(_ModpackRow).where(_ModpackRow.curseforge_id == curseforge_id)
)
if row is None:
return False
session.delete(row)
session.commit()
return True
def update_version(self, curseforge_id: int, version: str, notification_sent: bool = False) -> None:
with Session(self.engine) as session:
row = session.scalar(
select(_ModpackRow).where(_ModpackRow.curseforge_id == curseforge_id)
)
if row is None:
raise ValueError(f"Modpack {curseforge_id} not found in database.")
row.current_version = version
row.last_checked = datetime.utcnow()
session.add(
_CheckHistoryRow(
modpack_id=row.id,
checked_at=datetime.utcnow(),
version_found=version,
notification_sent=notification_sent,
)
)
session.commit()
def toggle_notifications(self, curseforge_id: int, enabled: bool) -> bool:
with Session(self.engine) as session:
row = session.scalar(
select(_ModpackRow).where(_ModpackRow.curseforge_id == curseforge_id)
)
if row is None:
return False
row.notification_enabled = enabled
session.commit()
return True
def get_modpack(self, curseforge_id: int) -> Optional[Modpack]:
with Session(self.engine) as session:
row = session.scalar(
select(_ModpackRow).where(_ModpackRow.curseforge_id == curseforge_id)
)
return Modpack._from_row(row) if row else None
def get_all_modpacks(self) -> List[Modpack]:
with Session(self.engine) as session:
rows = session.scalars(select(_ModpackRow)).all()
return [Modpack._from_row(r) for r in rows]
def get_check_history(self, curseforge_id: int, limit: int = 10) -> List[CheckHistory]:
with Session(self.engine) as session:
modpack_row = session.scalar(
select(_ModpackRow).where(_ModpackRow.curseforge_id == curseforge_id)
)
if modpack_row is None:
return []
rows = session.scalars(
select(_CheckHistoryRow)
.where(_CheckHistoryRow.modpack_id == modpack_row.id)
.order_by(_CheckHistoryRow.checked_at.desc())
.limit(limit)
).all()
return [CheckHistory._from_row(r) for r in rows]
EOF
echo "Created database.py"