The country-code length error borrowed the phone number label, so a
too-long country code read as a phone number limit. Adds a countryCode
term for it. ConversationsView also offered the new-conversation button
when prevent_multiple_conversations was set, without checking whether
visitors may start one at all, so it now follows the same rule as the
home screen and the server.
An over-long pre-chat value came back as "Must be at most 128 characters"
with no clue which field it was about, so use the existing fieldTooLong
message. The home screen also offered the start button when the inbox says
visitors cannot start conversations, and the visitor only found out when
the send failed, so the button now follows the same rule as the server.
Adds a browser-level livechat suite that embeds the widget on a host page
the way a customer site does, drives the real widget UI, and covers every
livechat config option that has a visible effect. CI needs the widget rate
limit lifted because the suite makes more than 100 widget requests a minute.
CloseChannel marks the client before disconnect expires the deadline, so
checking after the refresh means either this sees the flag, or the expiry
landed after the refresh and ReadJSON returns right away. Checking before
the refresh left a window where the refresh wiped the expiry and the read
blocked until the next ping.
KickUser and CloseAll only wrote a close frame and called Conn.Close,
which fasthttp turns into a no-op on a hijacked conn, so a client that
ignores the frame stayed connected and stayed in the hub. Expire the read
deadline and tear the client down so Serve returns.
The agent app had the same replaced-socket problem as the widget, where a
stale socket's close event cleared the live socket's ping timer and the
server dropped it 60s later. Also check the widget client's closed flag
before refreshing the read deadline, and reset inbox and user id on
re-join.
handleMessage had no stale socket guard, so a socket we already threw
away could still push messages into the chat store. Also only call the
HTTP shutdown a drain timeout when the error is context.DeadlineExceeded,
everything else is a plain shutdown error. Adds unit tests for the
socket guards.
Saving, enabling or disabling a livechat inbox rebuilds the channel,
which calls LiveChat.Close. That only closed each client's outbound
channel. The socket stayed open, so the widget kept exchanging
ping/pong and looked connected while every later agent reply went
nowhere. Only a page reload fixed it.
Calling conn.Close was not enough either. fasthttp makes Close a no-op
on a hijacked conn unless KeepHijackedConns is set, so it returned nil
and the read loop stayed blocked. Client now holds a disconnect func
that expires the read deadline, and the read loop also breaks if its
client was closed.
On the widget side, connect() replaced this.socket without closing the
old one, so a reconnect could leave two live sockets. It now closes the
previous socket and ignores events from it.
Also return input errors from the pre-chat form validation. A blank
required field used to come back as a 500 with no hint about which
field was wrong.
Adds a livechat e2e harness in cypress/support/livechat.js and four
specs: the inbox reload regression, messaging, session and auth, and
settings. 26 tests, and the reload one fails against a binary without
this fix.
The cache namespace was the help center slug, so a rename or delete stranded
the old namespace and kept serving those pages. It is a fixed namespace now,
which also drops a query per admin write.
Headers are set by the cache wrapper instead of the handlers, because the cache
restores only the body and content type. Search results and .md articles were
losing their noindex on every hit. Also cut the TTL to 5 minutes and clear on
agent writes, so a deleted avatar does not leave a broken image on an article.
Geist replaces Instrument Sans and is served from static/ instead of Google
Fonts, so it works on installs with no internet access.
Public help center pages are cached in Redis via fastcache with ETags, and any
admin write clears the group so edits show up on the next load.
SendMessage checked a separate Closed flag before sending, so a close
between the check and the send panicked on a closed channel. SendError
could also close the channel and then let Listen close it again on exit.
Both sends now go through trySend, which takes the same lock the close
takes, and close is idempotent.
A failed join response in the widget socket returned a nil client, so the
caller skipped cleanup and left the forwarder goroutine blocked forever.
It now removes the client and closes its channel first.
Close the widget conn when a write fails since the library marks it unusable, bail out of Listen through the cleanup path if the initial read deadline cannot be set, and nil the trailing slot in RemoveClient so the removed client is garbage-collected right away.
Hijacked websocket connections hold their fasthttp ctx until close, and
fasthttp keeps each recycled response buffer at full capacity, so memory
grew with every large response. Cap the pooled body size at 64 KiB and
share a write-buffer pool across both upgraders instead of allocating
8 KiB per connection at upgrade.
On the hot broadcast path, convToBroadcastMap did a marshal-unmarshal
round trip per event just to drop two per-user fields. Replace it with a
broadcastConv struct that shadows those fields via omitempty. Inbound
frames now decode once through json.RawMessage instead of re-marshaling
map[string]any per handler.
Also fix liveness: agent clients had no read deadline, so a peer that
vanished without closing (slept laptop, dropped wifi) blocked Listen
forever. Add ping/pong (25s ping, 60s pong wait), write deadlines on
both agent and widget writers, a 64 KiB read limit, and delete the
empty clients map entry in RemoveClient so user IDs don't leak.
Profiled the prod alloc with pprof. Three things stood out.
SLA evaluation was refetching about 234k pending rows every 10 minutes and the
count only ever grew. A policy can leave the resolution time blank, which
stores a NULL resolution deadline. The evaluator needs a deadline to compare
against, so it could never set met_at or breached_at, and the row stayed
pending forever. The pending query now skips a metric with no deadline, and a
new set based query closes any pending row whose configured metrics have all
settled. It replaces the old per row status update, so there is one status
writer and the backlog drains itself on the first tick. No migration is needed
because nothing outside the sla package reads that status.
Static assets were copied into fasthttp's pooled per connection response
buffer on every request. That buffer keeps its grown size, so serving the
frontend bundle held about 165MB. SetBodyRaw points at the bytes instead of
copying them.
API key auth ran a full bcrypt compare on every request, which was 62 percent
of cpu samples. API secrets are 64 char random tokens, so bcrypt's work factor
buys nothing there. They are now hashed with sha256 and compared in constant
time. Old bcrypt hashes still verify and get upgraded in place on first use,
so no key needs reissuing. Password login still uses bcrypt.
This also fixes an unrelated bug the work turned up. apply-sla deleted pending
rows by status alone, so re-applying an SLA threw away a first response breach
that was already recorded. It now deletes only rows with nothing recorded and
closes the rest to their terminal status first.
Help center saves now reject what they used to silently drop. name,
page_title and meta_description get length caps on the backend, and the
zod schema mirrors them so the error shows before the request goes out. A
theme that cannot be read returns an error instead of collapsing to an
empty theme and wiping the saved appearance. Protocol-relative URLs like
//evil.com are discarded, nav links with no label are dropped, and an
unknown social platform falls back to website. An announcement link with
no text is a form error now, because save drops the whole banner in that
case. Added tests for theme normalization.
Request validators return envelope errors instead of writing the response
themselves, so the handler decides the status code. They also trim names
and emails before checking them. Same change in the chat, users and help
center handlers.
Automation previous_* filters no longer see post-change values. The by-ID
path passes nil, so a rule filtering on previous_status will not match
instead of comparing a value against itself. The incoming message path
drops its fallback for the same reason. Team assignment also stopped
firing its automation event when an unrelated step failed, for example
fetching the team or applying its SLA. Those errors log and carry on now.
Small UI fixes: slug generation strips accents, a language already picked
in one row is hidden in the others, and a failed submit scrolls the first
error into view without jumping the page.
The update checker used http.Get (http.DefaultClient, no timeout) inside
a synchronous ticker loop, so an endpoint that accepts the connection but
never responds blocks the loop forever -- update checking silently dies
until restart. The response body was also only closed on the success
path, leaking the connection on the non-200 and read-error returns.
Extract the fetch into fetchAppUpdate(client, url): the caller passes a
client with a 10s timeout, and the body is closed via defer on every
path. Add tests covering success, body-close on non-200, and timeout.
Closes#445.
Uptime checkers and link validators probe with HEAD, so register
HEAD on all public routes and treat HEAD as a crawler so probes
do not inflate view counts or write search logs.
The custom-domain host lookup hit the DB before any rate limit.
Both the not-found gate and the host-home redirect now pay the
"public" limit first, and the middleware skips double-charging.
DB errors on help center pages rendered the plain unthemed error
page. Reuse the themed notfound template for them: it now takes
code, title, and text, so 404 and 500 both render inside the theme.
Also cap the sitemap at the URL limit, skip re-embedding unchanged
content in the AI embed reconciler, and autofocus the login email.
Map the generated search_tsv column on the Article struct so RETURNING *
scans stop failing on article create/update. Store /uploads paths without
the base URL (editor inserts them relative, backend strips as backstop).
Start the cleanup workers 60s after boot instead of 10s.
Locales are now a fixed whitelist (internal/helpcenter/locales.go) served via
/api/v1/help-centers/locales, and the admin form uses a combobox instead of a
free-text code input. Validation checks the whitelist instead of a BCP-47 regex.
Search: help_articles gets a generated search_tsv column with per-locale
stemming via a new help_article_search_config() SQL function, weighted
title/excerpt/body (A/B/C), a GIN index, and ts_rank length normalization so
long articles don't win by repetition. Body text is capped at 100K chars to
stay under the 1MB tsvector limit. The trigram ILIKE fallback stays for CJK
and infix matches.
The v2.8.0 migration is consolidated into one-shot CREATE TABLEs matching
schema.sql since the release isn't out yet.