feat(feedback): capture the browser UTC offset; Filed time in three zones
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 55s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m20s

The account time zone defaults to UTC until a player saves a profile, so a
report's Filed time could only render in UTC even for a player clearly in
another zone. Capture the client's detected "±HH:MM" offset (browser_tz) with
each submission and show the Filed time in three zones in the operator console
— UTC, the browser offset detected at submit, and the sender's saved profile
zone — each shown "N/A" when not known, so the operator can tell what is
certainly known from what is merely defaulted.

- Thread browser_tz through the fbs envelope (+ Go/TS codegen), the gateway
  transcode + backend client, and the backend feedback service/store; add the
  column via migration 00004 (additive, image-rollback-safe).
- Fix fmtTimeIn to resolve "±HH:MM" offsets via account.ResolveZone;
  time.LoadLocation alone silently fell back to UTC for offset zones, which is
  the second reason a "+02:00" sender showed only UTC.
- Update ARCHITECTURE/FUNCTIONAL(+ru) docs and the feedback integration test.
This commit is contained in:
Ilia Denisov
2026-06-22 18:05:39 +02:00
parent b78ce42922
commit 004aca4e97
20 changed files with 127 additions and 63 deletions
@@ -77,10 +77,16 @@ func (s *Server) consoleFeedbackDetail(c *gin.Context) {
s.consoleError(c, err)
return
}
// Filed time: always UTC; plus the sender's local zone when it is set and not UTC.
localCreated := ""
// Filed time in three zones so the operator can tell what is certainly known from what is
// merely defaulted: always UTC; the client's offset detected at submit (when the build
// reported one); and the sender's saved profile zone (when set beyond the UTC default). An
// empty rendered time makes the template show "N/A" for that line.
browserCreated, userCreated := "", ""
if m.BrowserTZ != "" {
browserCreated = fmtTimeIn(m.CreatedAt, m.BrowserTZ)
}
if m.TimeZone != "" && m.TimeZone != "UTC" {
localCreated = fmtTimeIn(m.CreatedAt, m.TimeZone)
userCreated = fmtTimeIn(m.CreatedAt, m.TimeZone)
}
view := adminconsole.FeedbackDetailView{
ID: m.ID.String(), AccountID: m.AccountID.String(), SenderName: m.SenderName,
@@ -89,7 +95,9 @@ func (s *Server) consoleFeedbackDetail(c *gin.Context) {
HasAttachment: m.HasAttachment, AttachmentName: m.AttachmentName, IsImage: feedback.IsImage(m.AttachmentName),
Read: m.Read, Archived: m.Archived, Replied: m.Replied, ReplyBody: m.ReplyBody,
RepliedAt: fmtTime(m.RepliedAt), CreatedAt: fmtTime(m.CreatedAt),
Version: m.Version, CreatedAtLocal: localCreated, TimeZone: m.TimeZone,
Version: m.Version,
CreatedAtBrowser: browserCreated, BrowserTZ: m.BrowserTZ,
CreatedAtUser: userCreated, UserTZ: m.TimeZone,
}
if banned, err := s.accounts.HasRole(ctx, m.AccountID, account.RoleFeedbackBanned); err == nil {
view.Banned = banned