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
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:
@@ -34,10 +34,10 @@ func NewStore(db *sql.DB) *Store {
|
||||
|
||||
// Insert stores one feedback message from accountID and returns its id. attachment
|
||||
// is the raw file bytes (nil for none); attachmentName, ip and a non-default
|
||||
// channel are stored as given. lang (the sender's interface language) and version (the client
|
||||
// app build) are snapshots taken now, so the operator later sees the state at submit time.
|
||||
// created_at defaults to now() in the database.
|
||||
func (s *Store) Insert(ctx context.Context, accountID uuid.UUID, body string, attachment []byte, attachmentName, channel, lang, version string, ip *string) (uuid.UUID, error) {
|
||||
// channel are stored as given. lang (interface language), version (client app build) and
|
||||
// browserTZ (the client's detected "±HH:MM" UTC offset) are snapshots taken now, so the operator
|
||||
// later sees the state at submit time. created_at defaults to now() in the database.
|
||||
func (s *Store) Insert(ctx context.Context, accountID uuid.UUID, body string, attachment []byte, attachmentName, channel, lang, version, browserTZ string, ip *string) (uuid.UUID, error) {
|
||||
id, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
return uuid.Nil, fmt.Errorf("feedback: new message id: %w", err)
|
||||
@@ -48,9 +48,9 @@ func (s *Store) Insert(ctx context.Context, accountID uuid.UUID, body string, at
|
||||
}
|
||||
if _, err := s.db.ExecContext(ctx,
|
||||
`INSERT INTO backend.feedback_messages
|
||||
(message_id, account_id, body, attachment, attachment_name, channel, lang, app_version, sender_ip)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||||
id, accountID, body, att, nullStr(attachmentName), channel, nullStr(lang), nullStr(version), ip); err != nil {
|
||||
(message_id, account_id, body, attachment, attachment_name, channel, lang, app_version, browser_tz, sender_ip)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
|
||||
id, accountID, body, att, nullStr(attachmentName), channel, nullStr(lang), nullStr(version), nullStr(browserTZ), ip); err != nil {
|
||||
return uuid.Nil, fmt.Errorf("feedback: insert: %w", err)
|
||||
}
|
||||
return id, nil
|
||||
@@ -231,8 +231,11 @@ type AdminMessage struct {
|
||||
Lang string
|
||||
// Version is the client app build the report was sent from, snapshotted at submit time.
|
||||
Version string
|
||||
// TimeZone is the sender account's IANA zone, for rendering CreatedAt in the sender's
|
||||
// local time alongside UTC.
|
||||
// BrowserTZ is the client's detected "±HH:MM" UTC offset at submit time, snapshotted so the
|
||||
// filed time can be shown in the sender's browser-local zone even before they save a profile.
|
||||
BrowserTZ string
|
||||
// TimeZone is the sender account's stored zone ("±HH:MM" offset, IANA name, or ""), for
|
||||
// rendering CreatedAt in the sender's own configured time alongside UTC.
|
||||
TimeZone string
|
||||
SenderIP string
|
||||
HasAttachment bool
|
||||
@@ -348,7 +351,7 @@ func (s *Store) AdminGet(ctx context.Context, id uuid.UUID) (AdminMessage, error
|
||||
var m AdminMessage
|
||||
var repliedAt sql.NullTime
|
||||
q := `SELECT m.message_id, m.account_id, a.display_name, ` + feedbackSource + ` AS source, m.body, m.channel,
|
||||
COALESCE(m.lang, ''), COALESCE(m.app_version, ''), a.time_zone,
|
||||
COALESCE(m.lang, ''), COALESCE(m.app_version, ''), COALESCE(m.browser_tz, ''), a.time_zone,
|
||||
COALESCE(m.sender_ip, ''), (m.attachment IS NOT NULL), COALESCE(m.attachment_name, ''),
|
||||
(m.read_at IS NOT NULL), (m.archived_at IS NOT NULL), (m.reply_body IS NOT NULL),
|
||||
COALESCE(m.reply_body, ''), m.replied_at, m.created_at
|
||||
@@ -357,7 +360,7 @@ func (s *Store) AdminGet(ctx context.Context, id uuid.UUID) (AdminMessage, error
|
||||
WHERE m.message_id = $1`
|
||||
err := s.db.QueryRowContext(ctx, q, id).Scan(
|
||||
&m.ID, &m.AccountID, &m.SenderName, &m.Source, &m.Body, &m.Channel,
|
||||
&m.Lang, &m.Version, &m.TimeZone,
|
||||
&m.Lang, &m.Version, &m.BrowserTZ, &m.TimeZone,
|
||||
&m.SenderIP, &m.HasAttachment, &m.AttachmentName,
|
||||
&m.Read, &m.Archived, &m.Replied, &m.ReplyBody, &repliedAt, &m.CreatedAt)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
|
||||
Reference in New Issue
Block a user