Converts Spring Boot 4.0.3 ARMA Server Web GUI to FastAPI/Python. Each phase file is fully self-contained: lists Java source files to read, output files to create, implementation patterns, REST endpoint contracts, and a completion checklist. A future agent can execute any single phase without rescanning the Java project. Phases: - 01: Foundation — SQLAlchemy models, Alembic, settings, base schemas - 02: Auth & Users — JWT middleware, RBAC, user CRUD - 03: CFG parser + server process — server.cfg round-trip, start/stop - 04: Server settings — general/network/logging/security/difficulty - 05: Mod management — mod CRUD, presets, settings, WebSocket progress - 06: Steam integration — SteamCMD queue, Workshop API, python-a2s - 07: Missions, CDLC, Discord, APScheduler jobs - 08: Middleware & polish — global exception handler, SPA redirect, structlog - 09: Testing — pytest-asyncio, respx, 80% coverage target
8.0 KiB
Phase 1 — Project Skeleton + Database
Status: PENDING
Depends on: nothing
Next phase: phase-02-auth-security.md
Goal
Produce a runnable FastAPI app that starts, connects to SQLite, runs Alembic migrations, and responds to health checks. All ORM models and repositories must exist. No business logic yet.
Java Source Files to Read
<JAVA_SRC> = E:\TestScript\ARMA-Server-Web-Gui\src\main\java\pl\bartlomiejstepien\armaserverwebgui\
| File | What to extract |
|---|---|
<JAVA_SRC>application/config/ASWGConfig.java |
Every @Value field name and default |
<JAVA_SRC>ArmaServerWebGuiApplication.java |
Startup hooks |
<JAVA_SRC>DefaultConfigGenerator.java |
How default config file is written on first run |
<JAVA_SRC>domain/server/mod/model/InstalledModEntity.java |
JPA mapping, ListOfLongsConverter for dependencies_ids |
<JAVA_SRC>domain/server/mod/model/ModPresetEntity.java |
JPA + relations |
<JAVA_SRC>domain/server/mod/model/ModSettingsEntity.java |
JPA mapping |
<JAVA_SRC>domain/server/mission/model/MissionEntity.java |
JPA mapping |
<JAVA_SRC>domain/server/difficulty/model/DifficultyProfileEntity.java |
JPA mapping |
<JAVA_SRC>domain/server/cdlc/model/CdlcEntity.java |
JPA mapping |
<JAVA_SRC>application/security/jwt/model/InvalidJwtTokenEntity.java |
JPA mapping |
<JAVA_SRC>application/scheduling/model/JobExecutionEntity.java |
JPA mapping |
<JAVA_SRC>application/scheduling/model/JobExecutionStatus.java |
Enum values |
<JAVA_SRC>repository/ (all .java files) |
Custom query methods beyond findById/findAll |
src/main/resources/application.properties |
server.port, multipart size limits, datasource |
src/main/resources/aswg-default-config.properties |
All aswg.* keys and defaults |
src/main/resources/db/changelog/ (all files) |
Full DDL for all 14 migration sets |
Output Files to Create
Relative to E:\TestScript\Arma_Server_Web_Manager\:
pyproject.toml
alembic.ini
alembic/env.py
alembic/versions/001_init.py
alembic/versions/002_installed_mod_last_workshop_update.py
alembic/versions/003_mission.py
alembic/versions/004_mod_settings.py
alembic/versions/005_invalid_jwt_token.py
alembic/versions/006_aswg_user.py
alembic/versions/007_overwrite_startup_params_authority.py
alembic/versions/008_cdlc.py
alembic/versions/009_aswg_user_last_login_column.py
alembic/versions/010_job_last_execution_time.py
alembic/versions/011_add_authorities.py
alembic/versions/012_last_mod_update_attempt.py
alembic/versions/013_job_execution_history.py
alembic/versions/014_mod_dependencies.py
config/settings.py
config/aswg_default_config.properties (copy content from Java resources file)
src/__init__.py
src/main.py
src/dependencies.py
src/repository/__init__.py
src/repository/base.py
src/repository/models.py
src/repository/installed_mod_repo.py
src/repository/mod_preset_repo.py
src/repository/mod_settings_repo.py
src/repository/mission_repo.py
src/repository/difficulty_profile_repo.py
src/repository/cdlc_repo.py
src/repository/user_repo.py
src/repository/authority_repo.py
src/repository/invalid_jwt_token_repo.py
src/repository/job_execution_repo.py
Implementation Notes
config/settings.py
Use pydantic-settings BaseSettings. The properties file uses Java .properties format (key=value), so implement a custom customise_sources that reads via jproperties.Properties.
Map every key in aswg-default-config.properties as a field with alias="aswg.the-key-name". Key fields:
server_directory_path: str = Field(default="arma-server", alias="aswg.server-directory-path")
security_enabled: bool = Field(default=True, alias="aswg.security.enabled")
jwt_expiration_time: str = Field(default="PT2H", alias="aswg.security.jwt.expiration-time")
default_user_username: str = Field(default="user", alias="aswg.default-user.username")
default_user_password: str = Field(default="changeme", alias="aswg.default-user.password")
steamcmd_path: str = Field(default="", alias="aswg.steamcmd.path")
src/repository/base.py
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase
DATABASE_URL = "sqlite+aiosqlite:///./data/aswg.db"
engine = create_async_engine(DATABASE_URL, echo=False)
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
class Base(DeclarativeBase):
pass
src/repository/models.py — Key mapping notes
InstalledModEntity.dependencies_ids: Java stores as comma-separated long string. Create a SQLAlchemyTypeDecoratorthat serializeslist[int]↔"1,2,3".@GeneratedValue(IDENTITY)→Integer, primary_key=True, autoincrement=TrueLocalDateTime/OffsetDateTime→DateTime(timezone=True)@ManyToManyuser↔authority → explicit association tableaswg_user_authority@Enumerated(STRING)→ store asString, convert in application layer
Alembic migrations
Replace H2-specific syntax:
IDENTITY/BIGINT GENERATED BY DEFAULT AS IDENTITY→INTEGER PRIMARY KEY AUTOINCREMENTTIMESTAMP→DATETIME- No sequences needed
Use alembic/env.py async pattern:
import asyncio
from alembic import context
from src.repository.base import engine, Base
def run_migrations_online():
async def _run():
async with engine.begin() as conn:
await conn.run_sync(context.run_migrations)
asyncio.run(_run())
src/main.py — minimal skeleton
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from alembic.config import Config
from alembic import command
@asynccontextmanager
async def lifespan(app: FastAPI):
Config("alembic.ini")
command.upgrade(Config("alembic.ini"), "head")
# Phase 2 will add: create default user
# Phase 6 will add: start scheduler
yield
# Phase 6 will add: stop scheduler
app = FastAPI(lifespan=lifespan)
@app.get("/api/v1/actuator/health")
async def health():
return {"status": "UP"}
@app.get("/api/v1/actuator/info")
async def info():
return {"application": {"name": "ASWG"}}
# Mount Angular SPA — must be last
app.mount("/", StaticFiles(directory="static", html=True), name="static")
src/dependencies.py
from typing import Annotated
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from src.repository.base import SessionLocal
async def get_db():
async with SessionLocal() as session:
yield session
DbSession = Annotated[AsyncSession, Depends(get_db)]
Repository pattern
Plain async classes receiving AsyncSession. Port every Spring Data method name:
findAll()→async def find_all()findById(id)→async def find_by_id(id)save(entity)→async def save(entity)(usessession.merge())deleteById(id)→async def delete_by_id(id)- Custom
@Querymethods → SQLAlchemyselect()with.where()
Completion Checklist
pyproject.tomlwith all dependencies fromPLAN.mdconfig/settings.pycovers everyaswg.*property with correct defaultssrc/repository/base.pycreates async SQLite enginesrc/repository/models.pyhas all 12 ORM entity classes- All 14 Alembic migrations run cleanly (
alembic upgrade head) - All 10 repository classes exist with full method set
uvicorn src.main:app --port 8085starts without errorsGET /api/v1/actuator/health→{"status":"UP"}GET /api/v1/actuator/info→ returns JSON
Contract for Phase 2
Phase 2 will import:
from config.settings import settingsfrom src.dependencies import DbSessionfrom src.repository.base import Base, engine, SessionLocalfrom src.repository.models import AswgUserEntity, AuthorityEntity, InvalidJwtTokenEntityfrom src.repository.user_repo import UserRepositoryfrom src.repository.authority_repo import AuthorityRepositoryfrom src.repository.invalid_jwt_token_repo import InvalidJwtTokenRepositoryfrom src.main import app