Merge branch 'phase/39-school-owners'
ci / server (push) Failing after 3m51s
ci / client (push) Successful in 18s

This commit is contained in:
Leonid Pershin
2026-08-20 09:56:13 +03:00
34 changed files with 1107 additions and 267 deletions
+14 -14
View File
@@ -11,31 +11,31 @@
## Задачи
- [ ] `SimulationOptions.MaxSchools` — слоты **игрока**, умолчание 2.
- [x] `SimulationOptions.MaxSchools` — слоты **игрока**, умолчание 2.
`MaxSchoolsTotal` — воркеры процесса, умолчание 16. Подъём с диска режет по Total, не по
слотам игрока
- [ ] Сейв несёт `owner`. Нет поля — бесхозная. Формат не бампить
- [ ] Create пишет текущего пользователя; отказ: свои слоты кончились (`limit-reached`) или
- [x] Сейв несёт `owner`. Нет поля — бесхозная. Формат не бампить
- [x] Create пишет текущего пользователя; отказ: свои слоты кончились (`limit-reached`) или
сервер полон (`server-full`)
- [ ] `GET /api/schools`: `maxSchools` — слоты игрока, `maxSchoolsTotal`, `schools` — свои,
- [x] `GET /api/schools`: `maxSchools` — слоты игрока, `maxSchoolsTotal`, `schools` — свои,
`others` — чужие и бесхозные (`owner` имя или `null`). Карточка школы несёт `mine`
- [ ] Welcome.`MaxSchools` — слоты игрока (байт тот же, смысл новый)
- [ ] Мутации чужой — HTTP `403` `not-owner`. Бесхозную может удалить любой залогиненный.
- [x] Welcome.`MaxSchools` — слоты игрока (байт тот же, смысл новый)
- [x] Мутации чужой — HTTP `403` `not-owner`. Бесхозную может удалить любой залогиненный.
`OpenSchool` — всем с сессией. Пауза / скорость / пропуск от гостя до работника не доходят.
**Исключение:** `POST .../portrait` — гость генерирует теми же пресетами школы; это не управление
- [ ] Меню: блок «Мои» и блок «Чужие». У бесхозной в чужих — удалить, у чужой с хозяином — нет
- [ ] Внутри чужой школы нет вкладки «Управление» и нет кнопок часов (пауза, скорость, пропуск).
- [x] Меню: блок «Мои» и блок «Чужие». У бесхозной в чужих — удалить, у чужой с хозяином — нет
- [x] Внутри чужой школы нет вкладки «Управление» и нет кнопок часов (пауза, скорость, пропуск).
Карта и люди остаются
- [ ] `docs/protocol.md` — списки и коды ошибок. Версию сокета не бампить
- [x] `docs/protocol.md` — списки и коды ошибок. Версию сокета не бампить
## Тесты, без которых фаза не закрыта
- [ ] Два пользователя: у каждого по две школы, третья своего — `limit-reached`; чужая в
- [x] Два пользователя: у каждого по две школы, третья своего — `limit-reached`; чужая в
`others`, не в `schools`
- [ ] Восьмая школа на сервере с `MaxSchoolsTotal` = 7 — `server-full`, даже если у игрока слот есть
- [ ] Гость: `POST` найма — `403`; `SetRunning` не меняет `running` хозяина
- [ ] Сейв без `owner` поднимается, лежит в `others`, удаляется вторым пользователем
- [ ] Клиентский тест: чужая карточка без кнопки удаления; в чужой школе нет «Управление»
- [x] Восьмая школа на сервере с `MaxSchoolsTotal` = 7 — `server-full`, даже если у игрока слот есть
- [x] Гость: `POST` найма — `403`; `SetRunning` не меняет `running` хозяина
- [x] Сейв без `owner` поднимается, лежит в `others`, удаляется вторым пользователем
- [x] Клиентский тест: чужая карточка без кнопки удаления; в чужой школе нет «Управление»
## Критерий готовности
+1 -1
View File
@@ -175,7 +175,7 @@
| Фаза | Статус | Зачем |
| --- | --- | --- |
| [39. Хозяин школы](39-school-owners.md) | 🔄 | `owner`, 2 слота на игрока, мои / чужие, гость без управления |
| [39. Хозяин школы](39-school-owners.md) | | `owner`, 2 слота на игрока, мои / чужие, гость без управления |
39 стоит на 38.
+22 -7
View File
@@ -76,16 +76,26 @@ people; it does not change on a living school.
```json
{
"maxSchools": 6,
"maxSchools": 2,
"maxSchoolsTotal": 16,
"defaultStartDate": "2012-03-31T06:00:00Z",
"gameMinutesPerRealSecond": 1,
"schoolWeekDays": 5,
"schools": [
{ "id": 1, "name": "Гимназия №14", "gameTime": "2012-03-31T07:35:00Z", "running": false, "speedIndex": 1, "modIds": ["core"], "seed": 1847291 }
{ "id": 1, "name": "Гимназия №14", "gameTime": "2012-03-31T07:35:00Z", "running": false, "speedIndex": 1, "modIds": ["core"], "seed": 1847291, "mine": true }
],
"others": [
{ "id": 2, "name": "Лицей", "gameTime": "2012-03-31T06:00:00Z", "running": true, "speedIndex": 1, "modIds": ["core"], "seed": 9912, "owner": "Leo" }
]
}
```
`maxSchools` is how many schools **this player** may own; `maxSchoolsTotal` is how many workers the
process runs. `schools` lists yours (`mine: true` on each card). `others` lists every other save on
the server — `owner` is the display name, or `null` when the save has no owner (any logged-in player
may delete an ownerless card). The WebSocket welcome frame still carries one byte for
`maxSchools`; it now means the same per-player limit, not the process total.
### `GET /api/schools/random-name`
`{ "name": "Лицей «Северная»" }` — a suggestion that is not already taken.
@@ -197,13 +207,15 @@ the global file, so a guest watching the school draws with the same model.
| `400` `unknown-country` | `countryId` is not a placeable `CountryDef` in those packs. |
| `400` `unknown-native-language` | `nativeLanguage` is not in that country's `nativeLanguages`. |
| `400` `invalid-portrait-settings` | `portraitSettings` failed validation (empty presets, bad age rule, out of range). |
| `409` `school-limit-reached` | `maxSchools` schools already exist. |
| `409` `school-limit-reached` | This player already owns `maxSchools` schools. |
| `409` `server-full` | The process already runs `maxSchoolsTotal` schools. |
Failures are RFC 7807 problem details with an extra `code` field — that is what the UI switches on.
### `DELETE /api/schools/{id}`
`204` when deleted, `404` when the id is unknown. Anyone watching that school over a WebSocket
`204` when deleted, `404` when the id is unknown, `403` `not-owner` when the school belongs to
another player. Ownerless saves may be deleted by any logged-in session. Anyone watching that school over a WebSocket
gets a `SchoolGone` frame.
### `GET /api/schools/{id}/people`
@@ -497,7 +509,8 @@ is off or unreachable, `connected` is false and the lists are empty.
### `GET /api/schools/{id}/dress-rules`
Student and staff dress-code pairs for the school. Unknown `{id}` is `404` `unknown-school`.
Student and staff dress-code pairs for the school. Only the owner may read this (`403` `not-owner`).
Unknown `{id}` is `404` `unknown-school`.
`form` is one of `regular`, `short`, `strict`. `color` is one of `noBright`, `whiteTopBlackBottom`,
`free`. When a `POST` has been accepted but not yet applied, `pendingStudents` and/or `pendingStaff`
@@ -515,7 +528,7 @@ not immediately.
### `POST /api/schools/{id}/dress-rules`
Queues a change for the next work morning. Either or both of `students` and `staff` may be sent;
Queues a change for the next work morning. Only the owner may post (`403` `not-owner`). Either or both of `students` and `staff` may be sent;
omitted sides keep their current rule. Unknown `{id}` is `404` `unknown-school`. Unknown `form` or
`color` is `400` `unknown-form` / `400` `unknown-color`. Response body matches `GET`.
@@ -525,7 +538,8 @@ omitted sides keep their current rule. Unknown `{id}` is `404` `unknown-school`.
### `GET /api/schools/{id}/staffing`
Money, uncovered subjects, the applicant pool and current staff. Reads the **published**
Money, uncovered subjects, the applicant pool and current staff. Only the owner may read this
(`403` `not-owner`). Reads the **published**
roster, applicant snapshot and catalog — it does not post to the worker. Unknown `{id}` is
`404` `unknown-school`. `?lang=ru|en` labels subjects and positions.
@@ -619,6 +633,7 @@ of that kind.
| Status | `code` | When |
| --- | --- | --- |
| `403` | `not-owner` | The session is not the school's owner. |
| `404` | `unknown-school` | No school with that id. |
| `404` | `unknown-applicant` | `personId` is not in the pool. |
| `409` | `already-hired` | That person is already staff. |