feat: add Java→Python migration plan with 9 self-contained phase files

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
This commit is contained in:
Khoa (Revenovich) Tran Gia
2026-04-14 15:06:56 +07:00
commit e02db3ddde
10 changed files with 2817 additions and 0 deletions

View File

@@ -0,0 +1,215 @@
# 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:
```python
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
```python
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 SQLAlchemy `TypeDecorator` that serializes `list[int]``"1,2,3"`.
- `@GeneratedValue(IDENTITY)``Integer, primary_key=True, autoincrement=True`
- `LocalDateTime` / `OffsetDateTime``DateTime(timezone=True)`
- `@ManyToMany` user↔authority → explicit association table `aswg_user_authority`
- `@Enumerated(STRING)` → store as `String`, convert in application layer
### Alembic migrations
Replace H2-specific syntax:
- `IDENTITY` / `BIGINT GENERATED BY DEFAULT AS IDENTITY``INTEGER PRIMARY KEY AUTOINCREMENT`
- `TIMESTAMP``DATETIME`
- No sequences needed
Use `alembic/env.py` async pattern:
```python
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
```python
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
```python
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)` (uses `session.merge()`)
- `deleteById(id)``async def delete_by_id(id)`
- Custom `@Query` methods → SQLAlchemy `select()` with `.where()`
---
## Completion Checklist
- [ ] `pyproject.toml` with all dependencies from `PLAN.md`
- [ ] `config/settings.py` covers every `aswg.*` property with correct defaults
- [ ] `src/repository/base.py` creates async SQLite engine
- [ ] `src/repository/models.py` has 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 8085` starts without errors
- [ ] `GET /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 settings`
- `from src.dependencies import DbSession`
- `from src.repository.base import Base, engine, SessionLocal`
- `from src.repository.models import AswgUserEntity, AuthorityEntity, InvalidJwtTokenEntity`
- `from src.repository.user_repo import UserRepository`
- `from src.repository.authority_repo import AuthorityRepository`
- `from src.repository.invalid_jwt_token_repo import InvalidJwtTokenRepository`
- `from src.main import app`