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,179 @@
# Phase 4 — Server Settings Subdomains
**Status**: PENDING
**Depends on**: Phase 3 complete (CFG parser + `ServerConfigStorage` must exist)
**Next phase**: `phase-05-mod-management.md`
---
## Goal
Five settings services that read/write Arma `.cfg` files and their REST endpoints. All follow the same read→model→return / receive→model→write pattern.
After this phase: general, network, security, logging settings and difficulty profiles work end-to-end.
---
## 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>domain/server/general/GeneralServiceImpl.java` | getGeneralProperties(), saveGeneralProperties() |
| `<JAVA_SRC>domain/server/general/model/GeneralProperties.java` | Field names/types |
| `<JAVA_SRC>domain/server/network/ServerNetworkServiceImpl.java` | getNetworkProperties(), saveNetworkProperties() |
| `<JAVA_SRC>domain/server/network/model/NetworkProperties.java` | Fields |
| `<JAVA_SRC>domain/server/network/model/KickTimeoutType.java` | Enum values |
| `<JAVA_SRC>domain/server/logging/LoggingServiceImpl.java` | getLoggingProperties(), getLatestLogs() |
| `<JAVA_SRC>domain/server/logging/model/LoggingProperties.java` | Fields |
| `<JAVA_SRC>domain/server/difficulty/DifficultyServiceImpl.java` | Full implementation (DB + filesystem) |
| `<JAVA_SRC>domain/server/difficulty/model/DifficultyProfile.java` | Fields |
| `<JAVA_SRC>domain/server/difficulty/DifficultyScanJob.java` | Filesystem scan logic |
| `<JAVA_SRC>web/GeneralController.java` | Exact paths, JSON shapes |
| `<JAVA_SRC>web/ServerNetworkRestController.java` | Exact paths, JSON shapes |
| `<JAVA_SRC>web/LoggingRestController.java` | Properties endpoints (SSE already done in Phase 3) |
| `<JAVA_SRC>web/ServerSecurityRestController.java` | Exact paths, JSON shapes |
| `<JAVA_SRC>web/DifficultyRestController.java` | Exact paths, JSON shapes |
| `<JAVA_SRC>web/request/SaveGeneralProperties.java` | Request fields |
| `<JAVA_SRC>web/request/NetworkPropertiesRequest.java` | Request fields |
| `<JAVA_SRC>web/request/SaveServerSecurityRequest.java` | Request fields |
| `<JAVA_SRC>web/response/GeneralPropertiesResponse.java` | Response fields |
| `<JAVA_SRC>web/response/NetworkPropertiesResponse.java` | Response fields |
| `<JAVA_SRC>web/model/DifficultyProfileApiModel.java` | API model fields |
---
## Output Files to Create
```
src/domain/server/general/__init__.py
src/domain/server/general/general_service.py
src/domain/server/general/models.py
src/domain/server/network/__init__.py
src/domain/server/network/network_service.py
src/domain/server/network/models.py
src/domain/server/logging_settings/__init__.py
src/domain/server/logging_settings/logging_service.py
src/domain/server/logging_settings/models.py
src/domain/server/security_settings/__init__.py
src/domain/server/security_settings/security_service.py
src/domain/server/security_settings/models.py
src/domain/server/difficulty/__init__.py
src/domain/server/difficulty/difficulty_service.py
src/domain/server/difficulty/jobs.py (DifficultyScanJob — registered in Phase 7)
src/domain/server/difficulty/models.py
src/web/schemas/general.py
src/web/schemas/network.py
src/web/schemas/security_settings.py
src/web/schemas/difficulty.py
src/web/general_router.py
src/web/network_router.py
src/web/logging_router.py
src/web/security_router.py
src/web/difficulty_router.py
```
Update `src/main.py`: register all 5 routers.
---
## Implementation Notes
### Common service pattern (general / network / logging / security)
```python
class GeneralService:
def __init__(self, config_storage: ServerConfigStorage):
self.storage = config_storage
async def get_properties(self) -> GeneralProperties:
cfg = self.storage.read()
return GeneralProperties(hostname=cfg.hostname, ...)
async def save_properties(self, props: GeneralProperties) -> None:
cfg = self.storage.read()
cfg.hostname = props.hostname
# ... apply all fields
self.storage.write(cfg)
```
Apply the same pattern to Network, Logging, and Security services.
### Difficulty service — DB + filesystem
`DifficultyServiceImpl.java` manages profiles in both the DB and `<server_dir>/Users/*.Arma3Profile` files.
```python
class DifficultyService:
async def get_profiles(self) -> list[DifficultyProfile]:
# Read from difficulty_profile table + scan filesystem for .Arma3Profile files
async def save_profile(self, profile: DifficultyProfile) -> None:
# Write to DB + write .Arma3Profile via cfg_writer
async def delete_profile(self, id: int | None, name: str | None) -> None:
# Remove from DB + delete file
async def set_active(self, id: int) -> None:
# Mark active in DB; only one can be active at a time
```
`DifficultyScanJob`: scans `<server_dir>/Users/` for new `.Arma3Profile` files and inserts them into the DB if not already present. **Register this job with APScheduler in Phase 7** — do not register it here.
### REST endpoints
**General** — permissions: `GENERAL_SETTINGS_VIEW`, `GENERAL_SETTINGS_SAVE`
```
GET /api/v1/general/properties → GeneralPropertiesResponse
POST /api/v1/general/properties body: SaveGeneralProperties → 200
```
**Network** — permissions: `NETWORK_SETTINGS_VIEW`, `NETWORK_SETTINGS_SAVE`
```
GET /api/v1/network/properties → NetworkPropertiesResponse
POST /api/v1/network/properties body: NetworkPropertiesRequest → 200
```
**Logging** — permission: `LOGS_VIEW`
```
GET /api/v1/logging/properties → LoggingPropertiesResponse
POST /api/v1/logging/properties body: LoggingPropertiesRequest → 200
GET /api/v1/logging/latest-logs → list[str]
```
Note: `GET /api/v1/logging/logs-sse` was registered in Phase 3.
**Security settings** — permissions: `SECURITY_SETTINGS_VIEW`, `SECURITY_SETTINGS_SAVE`
```
GET /api/v1/security → ServerSecurityResponse
POST /api/v1/security body: SaveServerSecurityRequest → 200
```
**Difficulty** — permissions: `DIFFICULTY_VIEW`, `DIFFICULTY_ADD`, `DIFFICULTY_UPDATE`, `DIFFICULTY_DELETE`
```
GET /api/v1/difficulties → list[DifficultyProfileApiModel]
POST /api/v1/difficulties body: DifficultyProfileApiModel → 201
PUT /api/v1/difficulties/{id} body: DifficultyProfileApiModel → 200
DELETE /api/v1/difficulties/{id} → 200
DELETE /api/v1/difficulties?name={n} → 200
```
---
## Completion Checklist
- [ ] `GET /api/v1/general/properties` returns correct camelCase JSON
- [ ] `POST /api/v1/general/properties` persists to .cfg file
- [ ] `GET /api/v1/network/properties` returns correct JSON
- [ ] `POST /api/v1/network/properties` persists to .cfg file
- [ ] `GET /api/v1/logging/properties` returns correct JSON
- [ ] `GET /api/v1/logging/latest-logs` returns list of log line strings
- [ ] `GET /api/v1/security` returns correct JSON
- [ ] `GET /api/v1/difficulties` returns profiles list
- [ ] `POST /api/v1/difficulties` creates profile in DB and .Arma3Profile file
- [ ] `DELETE /api/v1/difficulties/{id}` removes profile from DB and filesystem
- [ ] All endpoints return 403 without required permission
## Contract for Phase 5
Phase 5 has no direct dependency on Phase 4. It imports only from Phases 13.