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

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:
Ilia Denisov
2026-06-15 12:23:10 +02:00
parent fc848157d6
commit 419ea11b14
75 changed files with 3638 additions and 55 deletions
+60
View File
@@ -42,6 +42,7 @@ type DashboardView struct {
Games int
ActiveGames int
OpenComplaints int
OpenFeedback int
PendingChanges int
// ActiveVersion is the dictionary version new games pin (the persisted active
// version), distinct from the per-variant resident versions.
@@ -142,6 +143,10 @@ type UserDetailView struct {
// form; Reasons is the operator-editable reason picklist offered in the block form.
Suspension SuspensionView
Reasons []ReasonOption
// Roles is the account's current roles (each revocable); KnownRoles is the set the
// grant form offers. The first role is the feedback ban (see internal/account).
Roles []string
KnownRoles []string
}
// SuspensionView is an account's current manual-block state shown on the user card: whether it
@@ -362,3 +367,58 @@ type MessageView struct {
Body string
Back string
}
// FeedbackView is the paginated user-feedback queue. Status is the active
// unread/read/archived filter; NameMask/ExtMask are the sender glob filters;
// UserID pins the list to one account (the per-user link from /users);
// FilterQuery is the active filters URL-encoded for the pager links (already
// escaped, hence template.URL — see UsersView.FilterQuery).
type FeedbackView struct {
Items []FeedbackRow
Status string
NameMask string
ExtMask string
UserID string
Pager Pager
FilterQuery template.URL
}
// FeedbackRow is one feedback message in the queue: its sender (linked to the user
// card), source, channel, whether it has an attachment / a reply, its state and
// time.
type FeedbackRow struct {
ID string
AccountID string
SenderName string
Source string
Channel string
HasAttachment bool
Read bool
Replied bool
Archived bool
CreatedAt string
}
// FeedbackDetailView is one feedback message with its body, attachment, state and
// the reply / archive / delete forms. Body, AttachmentName, SenderName and IP are
// user-controlled and rendered as plain auto-escaped text. IsImage gates the inline
// <img> preview; Banned shows whether the sender already holds the feedback ban.
type FeedbackDetailView struct {
ID string
AccountID string
SenderName string
Source string
Channel string
IP string
Body string
HasAttachment bool
AttachmentName string
IsImage bool
Read bool
Archived bool
Replied bool
ReplyBody string
RepliedAt string
CreatedAt string
Banned bool
}