Pause the school on warning notices until the owner dismisses them.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,25 +11,25 @@
|
||||
|
||||
## Задачи
|
||||
|
||||
- [ ] `Notice` с `pause`: работник ставит `running=false`. `SetRunning(true)` игнор, пока есть
|
||||
- [x] `Notice` с `pause`: работник ставит `running=false`. `SetRunning(true)` игнор, пока есть
|
||||
хотя бы одно такое уведомление (как нелегальный skip — не fatal)
|
||||
- [ ] Модал (не тост): висит, очередь если несколько, на экране один. Закрытие — dismiss
|
||||
- [ ] Sticky в сейве школы; после загрузки и Open — снова кадры. Info по-прежнему не сохранять
|
||||
- [ ] Dismiss pausing — только хозяин; гость кадр видит, dismiss игнор / HTTP `403` `not-owner`
|
||||
- [ ] Потолок 8 sticky; девятый не эмитить. После последнего dismiss часы сами не запускать
|
||||
- [ ] Ванильный `GenerationFailed` (error, pause) можно положить в каталог; эмит с портрета —
|
||||
- [x] Модал (не тост): висит, очередь если несколько, на экране один. Закрытие — dismiss
|
||||
- [x] Sticky в сейве школы; после загрузки и Open — снова кадры. Info по-прежнему не сохранять
|
||||
- [x] Dismiss pausing — только хозяин; гость кадр видит, dismiss игнор / HTTP `403` `not-owner`
|
||||
- [x] Потолок 8 sticky; девятый не эмитить. После последнего dismiss часы сами не запускать
|
||||
- [x] Ванильный `GenerationFailed` (error, pause) можно положить в каталог; эмит с портрета —
|
||||
фаза 67, здесь достаточно тестового/внутреннего поста на доску
|
||||
- [ ] [`protocol.md`](../../protocol.md) — поведение паузы и прав, без лишнего бампа версии
|
||||
- [x] [`protocol.md`](../../protocol.md) — поведение паузы и прав, без лишнего бампа версии
|
||||
(версия уже +1 в 64)
|
||||
|
||||
## Тесты, без которых фаза не закрыта
|
||||
|
||||
- [ ] Пост pausing-notice: `running` становится false; последующий SetRunning(true) оставляет паузу
|
||||
- [ ] Dismiss последнего pausing: SetRunning(true) снова включает часы; без Play сами не идут
|
||||
- [ ] Сейв/загрузка: sticky на месте, info нет
|
||||
- [ ] Гость не снимает паузу dismiss-ом; хозяин снимает
|
||||
- [ ] Клиентский: модал не гаснет по TTL; очередь показывает следующий после закрытия
|
||||
- [ ] Хостовый: Open после рестарта работника отдаёт sticky, часы на паузе
|
||||
- [x] Пост pausing-notice: `running` становится false; последующий SetRunning(true) оставляет паузу
|
||||
- [x] Dismiss последнего pausing: SetRunning(true) снова включает часы; без Play сами не идут
|
||||
- [x] Сейв/загрузка: sticky на месте, info нет
|
||||
- [x] Гость не снимает паузу dismiss-ом; хозяин снимает
|
||||
- [x] Клиентский: модал не гаснет по TTL; очередь показывает следующий после закрытия
|
||||
- [x] Хостовый: Open после рестарта работника отдаёт sticky, часы на паузе
|
||||
|
||||
## Критерий готовности
|
||||
|
||||
|
||||
+25
-4
@@ -797,6 +797,13 @@ rebuilds the rest around it. Same success payload as GET timetable.
|
||||
Query: `classId`, `subject`, `day`, `period`. Drops that lock and rebuilds. Unknown lock is
|
||||
`404` `unknown-lesson`.
|
||||
|
||||
### `POST /api/schools/{id}/notices/{noticeId}/dismiss`
|
||||
|
||||
Owner-only close of a pausing notice. `204` even when that id is already gone. Guests get
|
||||
`403` `not-owner` — the same code as a guest hire. Info is not stored, so this path is for
|
||||
sticky warning/error. Socket dismiss (`0x08`) is the live intent; this HTTP route is the
|
||||
same right check for clients that are not on the socket.
|
||||
|
||||
## Dev endpoints
|
||||
|
||||
These exist only when `HSchool:AllowSaveReload` is true (headless AppHost tests). They are never
|
||||
@@ -848,6 +855,13 @@ read the `World`. Unknown `{id}` is `404` `unknown-school`.
|
||||
`nodeId` is null when the person is off campus. `now` is the lessons occurring at `gameTime`
|
||||
(empty on a break, night, weekend or holiday). `lessons` is the published table.
|
||||
|
||||
### `POST /api/dev/schools/{id}/notices`
|
||||
|
||||
Posts a catalog `EventDef` onto that school's board without going through Swarm. Body:
|
||||
`{ "defName": "GenerationFailed", "personId": 0 }`. Success is `{ "id", "defName", "pause" }`.
|
||||
Unknown `{id}` is `404` `unknown-school`. Unknown def or a full sticky queue is `409`
|
||||
`notice-rejected`. Tests use this; portraits emit `generationFailed` in a later phase.
|
||||
|
||||
## WebSocket message ids
|
||||
|
||||
Client-to-server ids live in `0x00–0x7F`, server-to-client ids in `0x80–0xFF`, so a misrouted
|
||||
@@ -925,6 +939,10 @@ Running and speed are **separate messages on purpose**. A single "set clock" mes
|
||||
button to resend the other field from the client's own copy of the state, which is always at least
|
||||
one tick stale — pressing play and then a speed button would pause the school again.
|
||||
|
||||
`SetRunning(true)` is ignored (not fatal, same as an illegal SkipEmpty) while the school has at
|
||||
least one **pausing** notice. The clocks stay paused until the owner dismisses those notices **and**
|
||||
presses Play — dismiss itself does not start the calendar.
|
||||
|
||||
Speed indexes are `0 = ×½`, `1 = ×1`, `2 = ×2`, `3 = ×5`, `4 = ×10`; out-of-range values are
|
||||
ignored rather than fatal. The base rate is `gameMinutesPerRealSecond` (1), so ×1 is one game
|
||||
minute per real second.
|
||||
@@ -941,7 +959,8 @@ neighbouring field would clobber it with a stale client copy.
|
||||
### `0x08` DismissNotice — 5 bytes
|
||||
|
||||
Closes one notice by id. Info toasts are not stored on the server; the frame still travels so a
|
||||
click is one intent. Pausing dismiss (owner-only) is a later phase.
|
||||
click is one intent. **Pausing** dismiss is owner-only: a guest's frame is ignored, like a guest
|
||||
`SetRunning`. After the last pausing notice is gone the clocks stay paused until Play.
|
||||
|
||||
| Offset | Type | Field |
|
||||
| --- | --- | --- |
|
||||
@@ -1069,8 +1088,10 @@ on this frame; the client builds «говорит с Машей о футбол
|
||||
### `0x87` Notice — variable
|
||||
|
||||
One school event for every connection that has that school open. Not glued to the clock frame.
|
||||
Info is not written to the save and is not resent on OpenSchool. `personId` is `0` when nobody
|
||||
is in frame (the generate-image button is a later phase).
|
||||
Info is not written to the save and is not resent on OpenSchool. Warning/error with `pause=1`
|
||||
stop the clock, stay in the save (at most eight), and are sent again on Open. The ninth sticky
|
||||
notice is not emitted. `personId` is `0` when nobody is in frame (the generate-image button is a
|
||||
later phase).
|
||||
|
||||
| Offset | Type | Field |
|
||||
| --- | --- | --- |
|
||||
@@ -1078,7 +1099,7 @@ is in frame (the generate-image button is a later phase).
|
||||
| 1 | `u32` | notice id |
|
||||
| 5 | string | `defName` |
|
||||
| … | `u8` | severity: `0` info, `1` warning, `2` error |
|
||||
| … | `u8` | `1` pause (later phase), `0` clocks keep running |
|
||||
| … | `u8` | `1` pause (clocks stop until dismiss + Play), `0` clocks keep running |
|
||||
| … | `u32` | `ttlMs`; `0` stays until dismiss |
|
||||
| … | `u32` | `personId`; `0` = none |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user