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
180 lines
7.3 KiB
Markdown
180 lines
7.3 KiB
Markdown
# 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 1–3.
|