Add EventDef notices, morning and lesson bells, and info toasts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-20 20:46:33 +03:00
co-authored by Cursor
parent ea3f5c7bf1
commit a554738084
38 changed files with 952 additions and 22 deletions
+13 -13
View File
@@ -12,28 +12,28 @@
## Задачи
- [ ] `EventDef`: id, `severity` (`info` / `warning` / `error`), `pause`, `ttlMs` (0 = не гаснуть),
- [x] `EventDef`: id, `severity` (`info` / `warning` / `error`), `pause`, `ttlMs` (0 = не гаснуть),
`trigger` (`dayStart` / `lessonStart` / `generationFailed`), `action` (`none` в этой фазе).
Локаль как у других def. Валидатор каталога
- [ ] Ваниль `DayStarted` и `LessonStarted`: info, без паузы, TTL 8000, `action` none
- [ ] Симуляция на крае рабочего утра и на звонке (смена периода урока, один факт на школу)
- [x] Ваниль `DayStarted` и `LessonStarted`: info, без паузы, TTL 8000, `action` none
- [x] Симуляция на крае рабочего утра и на звонке (смена периода урока, один факт на школу)
отдаёт факт, без UI. Работник сопоставляет def и кладёт info-`Notice`
- [ ] Новый кадр S→C (уведомление: id, defName, severity, pause, ttlMs, optional personId = 0).
- [x] Новый кадр S→C (уведомление: id, defName, severity, pause, ttlMs, optional personId = 0).
Новый кадр C→S dismiss по id. `ProtocolCodec.cs`, `protocol.ts`, [`protocol.md`](../../protocol.md)
в одном коммите. Версия протокола +1
- [ ] Клиент: тост поверх школы, текст из каталога/`t` по defName, клик шлёт dismiss, TTL
- [x] Клиент: тост поверх школы, текст из каталога/`t` по defName, клик шлёт dismiss, TTL
гасит локально. Строки через `t(...)`. Не колонка «События»
- [ ] Info не писать в сейв. Открытие школы info не повторяет. Гость тост видит
- [ ] `generationFailed` в каталоге можно завести, но не эмитить — фаза 67 / 65
- [x] Info не писать в сейв. Открытие школы info не повторяет. Гость тост видит
- [x] `generationFailed` в каталоге можно завести, но не эмитить — фаза 67 / 65
## Тесты, без которых фаза не закрыта
- [ ] Каталог грузит `DayStarted` / `LessonStarted`; неизвестный trigger — ошибка загрузки
- [ ] Переход через край рабочего утра даёт один факт `dayStart`; повторный тик в ту же минуту — нет
- [ ] Смена периода на урок даёт один `lessonStart` на школу, не по числу классов
- [ ] Круглый трип и байтовая раскладка кадра уведомления и dismiss — на обеих сторонах
- [ ] Клиентский тест: тост показывает локализованный текст, клик вызывает dismiss, без монтирования колонки
- [ ] Хостовый: открытая школа после утра получает кадр; после Close/Open info-тост не висит из сейва
- [x] Каталог грузит `DayStarted` / `LessonStarted`; неизвестный trigger — ошибка загрузки
- [x] Переход через край рабочего утра даёт один факт `dayStart`; повторный тик в ту же минуту — нет
- [x] Смена периода на урок даёт один `lessonStart` на школу, не по числу классов
- [x] Круглый трип и байтовая раскладка кадра уведомления и dismiss — на обеих сторонах
- [x] Клиентский тест: тост показывает локализованный текст, клик вызывает dismiss, без монтирования колонки
- [x] Хостовый: открытая школа после утра получает кадр; после Close/Open info-тост не висит из сейва
## Критерий готовности
+34 -5
View File
@@ -1,4 +1,4 @@
# Wire protocol v9
# Wire protocol v10
The client talks to the server two ways:
@@ -7,7 +7,8 @@ The client talks to the server two ways:
roster (filtered list + one-person card), a short id→name directory, staffing, and the timetable. Those are request/response
by nature, so they are plain REST.
- **A binary WebSocket at `/ws/game`** for the school calendar (20 Hz), a static map snapshot
sent once when a school is opened, and a presence stream (~2 Hz) of who is where.
sent once when a school is opened, a presence stream (~2 Hz) of who is where, and one-shot
notice frames when the world raises an event.
This document covers both. One protocol message per WebSocket frame, no framing header beyond the
message id. **All multi-byte numbers are little-endian.**
@@ -861,12 +862,14 @@ frame is obvious at a glance.
| `0x05` | C → S | SetRunning |
| `0x06` | C → S | SetSpeed |
| `0x07` | C → S | SkipEmpty |
| `0x08` | C → S | DismissNotice |
| `0x81` | S → C | Welcome |
| `0x82` | S → C | Pong |
| `0x83` | S → C | Clock |
| `0x84` | S → C | SchoolGone |
| `0x85` | S → C | MapSnapshot |
| `0x86` | S → C | Presence |
| `0x87` | S → C | Notice |
## Client → server
@@ -935,6 +938,16 @@ when the skip is not legal; the calendar does not move.
Running, speed and skip are **separate messages on purpose**. A button that also resent a
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.
| Offset | Type | Field |
| --- | --- | --- |
| 0 | `u8` | `0x08` |
| 1 | `u32` | notice id |
## Server → client
### `0x81` Welcome — 4 bytes
@@ -1053,6 +1066,22 @@ Member ids are the live circle, including self, sorted by id. Count `0` and an e
the person is not talking — the same id/node/state as before the circle fields. Names are not
on this frame; the client builds «говорит с Машей о футболе» from the HTTP directory and locale.
### `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).
| Offset | Type | Field |
| --- | --- | --- |
| 0 | `u8` | `0x87` |
| 1 | `u32` | notice id |
| 5 | string | `defName` |
| … | `u8` | severity: `0` info, `1` warning, `2` error |
| … | `u8` | `1` pause (later phase), `0` clocks keep running |
| … | `u32` | `ttlMs`; `0` stays until dismiss |
| … | `u32` | `personId`; `0` = none |
## Guarantees and limits
- **Inbound** frames larger than 8 KiB are refused with close status `1009 MessageTooBig`. That
@@ -1064,9 +1093,9 @@ on this frame; the client builds «говорит с Машей о футбол
older clients within the same protocol version.
- Clock delivery is lossy under back pressure: each connection buffers 32 clock frames and drops
the oldest, because a stale clock is worthless once a newer one exists.
- The map snapshot and presence use a separate reliable queue so ticks cannot crowd them out.
- The map snapshot, presence and notices use a separate reliable queue so ticks cannot crowd them out.
## Not in v9 yet
## Not in v10 yet
Authentication, Sit orders, an event log, walk animation, and `OpenLocation` on the server —
Authentication, Sit orders, a year-long event log, walk animation, and `OpenLocation` on the server —
the tree and the location panel are filtered on the client from the snapshot plus presence.