feat(feedback): in-app user feedback with admin review and account roles
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Successful in 47s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m12s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Successful in 47s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m12s
User-facing Feedback screen (Settings -> Info, registered accounts only): a message (<=1024 runes) plus one optional attachment, an anti-spam gate (one unreviewed message at a time), and the operator's inline reply with a Settings/Info badge. Server-rendered admin console section (/_gm/feedback): unread/read/archived queue with per-user search, detail with read/reply/ archive/delete/delete-all, safe attachment serving (nosniff, images inline via <img>, others download-only). Introduces account_roles, the first per-account role table; feedback_banned blocks only feedback submission, granted/revoked from /users and the delete-with-block action. - migration 00004_feedback (feedback_messages + account_roles) + jetgen - backend internal/feedback (store+service), internal/account/roles.go - wire: FlatBuffers feedback.submit/get/unread; gateway guest gate (Op.NonGuest, is_guest via session resolve) -> guest_forbidden before any backend call - reply push reuses NotificationEvent with a new admin_reply sub-kind - UI: /feedback route + screen, attachment picker, badge, channel detection, i18n - tests: feedback unit (Go+UI), gateway guest-gate, inttest lifecycle, e2e - docs: PLAN stage 19, ARCHITECTURE s15, FUNCTIONAL(+ru), TESTING, READMEs
This commit is contained in:
@@ -52,6 +52,7 @@ independent (see ARCHITECTURE §9.1).
|
||||
| 16 | Deploy infra & test contour (Dockerfiles, gateway static UI, compose, observability) | **done** |
|
||||
| 17 | Test-contour verification & defect fixes | **done** |
|
||||
| 18 | Prod contour deploy (SSH export/import, manual after merge) | todo |
|
||||
| 19 | User feedback (in-app submit + attachment, admin review/reply, account roles) | **done** |
|
||||
|
||||
Scaffolding is incremental: `go.work` lists only existing modules; each stage
|
||||
adds the modules it needs.
|
||||
@@ -419,6 +420,37 @@ parameterised for this; the test contour leaves it `:80` behind the host caddy).
|
||||
Open details (re-interview): export/import vs a registry trade-off; prod domain/cert source (ACME vs a
|
||||
provided cert) at the contour caddy; prod VPN; rollback.
|
||||
|
||||
### Stage 19 — User feedback *(done)*
|
||||
A user→operator feedback channel, sequenced after the numbered stages but shipped **before** the Stage 18
|
||||
prod deploy. Scope: a **Feedback** screen under Settings → Info where a registered player sends a message
|
||||
(≤1024 runes) with an optional single attachment; the operator reviews and replies in the server-rendered
|
||||
**admin console**; the reply is delivered back in-app with a Settings/Info badge. Introduces the project's
|
||||
**first per-account role** (`account_roles`), the reusable replacement for per-feature flags: the
|
||||
`feedback_banned` role blocks **only** feedback submission (not the whole account, unlike a suspension),
|
||||
granted from the feedback section's delete-with-block and granted/revoked from `/users`.
|
||||
|
||||
- **Data:** migration `00004_feedback` — `feedback_messages` (body, attachment `bytea`, sender_ip, channel,
|
||||
read/archived/reply/reply-read timestamps) + `account_roles` (account_id, role). New backend domain
|
||||
`internal/feedback` (store + service), modelled on `internal/social` + the admin chat-moderation surface.
|
||||
- **Anti-spam gate:** a player with an unreviewed message cannot send another ("Ожидаем рассмотрения вашего
|
||||
последнего обращения"); enforced server-side. This *is* the rate limit — no separate one.
|
||||
- **Reply lifecycle:** the reply lives on the message row; shown on the feedback screen ("Ответ на ваше
|
||||
последнее сообщение") for the player's latest replied message; it becomes read the moment the screen
|
||||
fetches it (delivery = read) and is hidden one week after. The badge (lobby ⚙️, combined with friend
|
||||
requests; Settings → Info "1") rides the existing `NotificationEvent` with a new `admin_reply` sub-kind —
|
||||
no wire-schema change for the push.
|
||||
- **Wire:** new FlatBuffers `feedback.submit` / `feedback.get` / `feedback.unread`; `feedback.submit` is
|
||||
**guest-gated at the gateway** via a new `Op.NonGuest` flag (the session resolve now carries `is_guest`),
|
||||
so a guest's submission never reaches the backend; the backend rejects guests too.
|
||||
- **Security:** body + attachment limits enforced server-side (the UI limits are UX only); attachments
|
||||
served with `X-Content-Type-Options: nosniff`, images inline-previewed via `<img>` (which never
|
||||
executes), everything else download-only; the UI gates the attachment by extension (allowed types not
|
||||
listed) and the backend mirrors the allow-list + size as the trust boundary; the console renders user
|
||||
content as auto-escaped text (`html/template`).
|
||||
Open details settled at planning: attachment ≤1,000,000 B (keeps the 1 MiB edge cap); allowed types
|
||||
(images + pdf/txt/log/doc/docx/rtf/zip/gz/7z); channel from the client (telegram/ios/android/web); guests
|
||||
cannot submit; three-way admin filter.
|
||||
|
||||
## Refinements logged during implementation
|
||||
|
||||
- **Stage 0**: solver `replace` deferred to Stage 2 (nothing imports it yet;
|
||||
@@ -1476,6 +1508,19 @@ provided cert) at the contour caddy; prod VPN; rollback.
|
||||
`proactiveNudgeGap` unit test, the retimed `TestRobotProactiveNudge`, `TestVariantLanguage`,
|
||||
emit (`your_turn`/`game_over` language) and a `TestNudgeRoutedByGameLanguage` integration test.
|
||||
|
||||
### Stage 19 — user feedback
|
||||
- Dropped the planned `feedback_messages.attachment_type` column: the safe content-type is derived from the
|
||||
file-name extension at both validate and serve time (one source of truth), so a stored column was
|
||||
redundant.
|
||||
- The admin filter is **three-way** (Непрочитанные / Прочитанные / Архив) rather than the two first
|
||||
sketched, mirroring complaints' `open·resolved·all`, so a read-but-unarchived message is never
|
||||
unreachable (owner-approved at planning).
|
||||
- The guest gate was extended to the **gateway** at the owner's request: a new `Op.NonGuest` flag plus
|
||||
`is_guest` threaded through the session resolve + gateway cache reject a guest's `feedback.submit` with
|
||||
the `guest_forbidden` result code before any backend call (the backend keeps its own guest check as the
|
||||
backstop). `handleResolveSession` now loads the account best-effort to report `is_guest` (a read error
|
||||
falls back to false so the auth hot path stays resilient).
|
||||
|
||||
## Deferred TODOs (cross-stage)
|
||||
|
||||
- ~~**TODO-1 — publish & version the solver.**~~ **Done in Stage 14.** `scrabble-solver` is
|
||||
|
||||
Reference in New Issue
Block a user