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:
@@ -58,6 +58,8 @@ func (s *Server) registerConsole(router *gin.Engine) {
|
||||
gm.POST("/users/:id/grant-hints", s.consoleGrantHints)
|
||||
gm.POST("/users/:id/block", s.consoleBlockUser)
|
||||
gm.POST("/users/:id/unblock", s.consoleUnblockUser)
|
||||
gm.POST("/users/:id/grant-role", s.consoleGrantRole)
|
||||
gm.POST("/users/:id/revoke-role", s.consoleRevokeRole)
|
||||
gm.GET("/reasons", s.consoleReasons)
|
||||
gm.POST("/reasons", s.consoleCreateReason)
|
||||
gm.POST("/reasons/:id/update", s.consoleUpdateReason)
|
||||
@@ -68,6 +70,16 @@ func (s *Server) registerConsole(router *gin.Engine) {
|
||||
gm.GET("/complaints", s.consoleComplaints)
|
||||
gm.GET("/complaints/:id", s.consoleComplaintDetail)
|
||||
gm.POST("/complaints/:id/resolve", s.consoleResolveComplaint)
|
||||
if s.feedback != nil {
|
||||
gm.GET("/feedback", s.consoleFeedback)
|
||||
gm.GET("/feedback/:id", s.consoleFeedbackDetail)
|
||||
gm.GET("/feedback/:id/attachment", s.consoleFeedbackAttachment)
|
||||
gm.POST("/feedback/:id/read", s.consoleFeedbackRead)
|
||||
gm.POST("/feedback/:id/reply", s.consoleFeedbackReply)
|
||||
gm.POST("/feedback/:id/archive", s.consoleFeedbackArchive)
|
||||
gm.POST("/feedback/:id/delete", s.consoleFeedbackDelete)
|
||||
gm.POST("/feedback/:id/delete-all", s.consoleFeedbackDeleteAll)
|
||||
}
|
||||
gm.GET("/messages", s.consoleMessages)
|
||||
gm.GET("/messages.csv", s.consoleMessagesCSV)
|
||||
gm.GET("/dictionary", s.consoleDictionary)
|
||||
@@ -87,6 +99,9 @@ func (s *Server) consoleDashboard(c *gin.Context) {
|
||||
view.Games, _ = s.games.CountGames(ctx, "")
|
||||
view.ActiveGames, _ = s.games.CountGames(ctx, game.StatusActive)
|
||||
view.OpenComplaints, _ = s.games.CountComplaints(ctx, game.StatusComplaintOpen)
|
||||
if s.feedback != nil {
|
||||
view.OpenFeedback, _ = s.feedback.CountUnread(ctx)
|
||||
}
|
||||
if changes, err := s.games.DictionaryChanges(ctx); err == nil {
|
||||
view.PendingChanges = len(changes)
|
||||
}
|
||||
@@ -316,6 +331,10 @@ func (s *Server) consoleUserDetail(c *gin.Context) {
|
||||
view.Reasons = append(view.Reasons, adminconsole.ReasonOption{ID: r.ID.String(), TextEn: r.TextEn, TextRu: r.TextRu})
|
||||
}
|
||||
}
|
||||
if roles, err := s.accounts.ListRoles(ctx, id); err == nil {
|
||||
view.Roles = roles
|
||||
}
|
||||
view.KnownRoles = account.KnownRoles
|
||||
s.renderConsole(c, "user_detail", "users", acc.DisplayName, view)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user