Commit Graph

585 Commits

Author SHA1 Message Date
Abhinav Raut b136346b95 fetch only the edited view's count after saving a view 2026-09-02 01:19:50 +05:30
Abhinav Raut 2ba716cb58 Merge remote-tracking branch 'origin/main' into pr/sidebar-counts
# Conflicts:
#	frontend/apps/main/src/stores/conversation.js
#	internal/conversation/conversation.go
2026-09-02 00:45:50 +05:30
Abhinav Raut 5e22ed3f56 keep the thumbnail name when its upload fails 2026-09-02 00:01:46 +05:30
Abhinav Raut 12bb634b0f Merge remote-tracking branch 'origin/main' into shrink-macro-payloads
# Conflicts:
#	frontend/apps/main/src/features/conversation/ReplyBox.vue
#	frontend/apps/main/src/stores/macro.js
#	frontend/apps/main/src/stores/macro.test.js
#	internal/image/image.go
2026-09-02 00:01:42 +05:30
Abhinav Raut 4aacd20261 keep the upload when the thumbnail fails 2026-09-01 23:07:36 +05:30
Abhinav Raut c934758ca8 cap sidebar view counts and move the count badge into a component 2026-09-01 21:18:48 +05:30
Abhinav Raut c1d613c942 Merge pull request #548 from malpou/fix/oidc-redirect-live-lookup
oidc: resolve redirect URL live so Root URL changes without a restart (preferred option)
2026-08-30 23:48:47 +05:30
Abhinav Raut 60cb85494c Merge pull request #544 from malpou/api/source-id
api: accept source_id on conversation and message endpoints
2026-08-30 23:47:26 +05:30
Abhinav Raut baeade99a6 oidc: always read the redirect URL from settings, drop the session copy 2026-08-30 23:26:34 +05:30
Abhinav Raut 9a64fe3303 move canCreateConversationMessage below the handlers 2026-08-30 22:52:48 +05:30
Abhinav Raut c0471ae230 gate message delete on messages:write_private & drop doc str 2026-08-30 22:50:45 +05:30
Marc Vila 08a12801d9 feat: add separate permission for private conversation notes 2026-08-30 10:22:09 +02:00
malpou 1811e89c5f oidc: reject a missing redirect URI from the session in the callback
The callback retrieved oidc_redirect_uri from the session and silently
fell back to an empty string when it was missing, unreadable, or not a
string, then passed that empty value to ExchangeOIDCToken, which would
send no redirect_uri and have the exchange rejected by the IdP. Treat
a missing or unreadable value as a stale session and route it through
the existing session-expired error path, the same way a state mismatch
is handled.
2026-08-29 12:06:57 +02:00
malpou 8eeea388d6 oidc: resolve redirect URI once per login and propagate errors
Address CodeRabbit review on the live-lookup change:

- Resolve the redirect URI live in LoginURL and return it so the caller
  persists it in the session; ExchangeOIDCToken reuses that stored value
  instead of resolving again. The token exchange redirect_uri must match
  the one in the auth request (RFC 6749 4.1.3); resolving twice would let
  a Root URL change between login and callback produce a mismatch the IdP
  rejects.
- Propagate the redirect URL resolution error out of LoginURL rather
  than discarding it, so a setting read failure fails the login loudly
  instead of sending an empty redirect_uri.
- Pass rootURL as a formatting argument in oidc.RedirectURL instead of
  concatenating it into the format string, so a root URL containing a
  %-sequence (e.g. %2F) is not interpreted as a verb.
2026-08-29 11:51:43 +02:00
malpou 02e9d1d3bf oidc: resolve redirect URL live so Root URL changes without a restart
Auth providers captured the redirect URL as a string at build time, so
changing app.root_url in General settings left OIDC providers sending
the old redirect_uri until the process restarted. The identity provider
then rejected login with "redirect_uri is missing in the client
configuration" even though the database and the IdP client both held the
right value.

Make auth.Provider.RedirectURL a closure and resolve it in LoginURL and
ExchangeOIDCToken from the current root URL, matching the media store's
existing rootURL closure. The redirect URL is computed by a new
oidc.Manager.RedirectURL(id) helper so the path format stays in one
place. Other provider fields (client ID, provider URL) remain snapshotted
and are refreshed by reloadAuth on OIDC changes, as before.

This removes the reload requirement for Root URL entirely; the settings
handler no longer needs to know that auth depends on it.
2026-08-29 11:32:28 +02:00
malpou 3b0d3d4415 api: accept source_id on conversation and message endpoints
Add an optional source_id to createConversationRequest and messageReq and
store it on the created contact message, mirroring the IMAP ingestion path
which is the only place source_id is otherwise populated.

BuildEmailThreadingHeaders builds every outgoing reply's References and
In-Reply-To from prior messages' source_id. A desk fed over the API, as
ours is, therefore references only its own previous sends, so the customer's
mail client has nothing of theirs to thread on and every reply arrives
looking standalone. Letting the API set source_id fixes this for any API-fed
inbox without a direct database write.

The value is normalized (whitespace and angle brackets stripped) since
source_id is stored unbracketed and the brackets are re-added when composing
the threading headers. An empty value leaves the column NULL as before, so
existing callers are unaffected.
2026-08-29 10:59:03 +02:00
Abhinav Raut 387071117d cut memory spikes from uploads, i18n and the hourly time trigger
Heap profiles on a large install showed three hot spots: image uploads
decoded the full bitmap twice, every i18n request rebuilt the language
pack, and the hourly time trigger loaded all conversations from the
last 30 days into one slice.
2026-08-28 17:06:08 +05:30
Abhinav Raut 18514cd021 search and paginate agents, teams, tags and macros instead of loading them all at boot 2026-08-28 01:43:59 +05:30
Amberger Moritz 1d88ef13b1 fix: address CodeRabbit review on sidebar counts PR
Restore upstream conversation create/transcript and mobile sidebar behavior that was accidentally overwritten during cherry-pick. Import onMounted in Sidebar.vue and bound view-count queries with a 10s timeout.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 15:57:55 +02:00
Amberger Moritz d12c4446a6 feat: show open conversation counts in the inbox sidebar
Adds badges with the number of open conversations next to My inbox,
Mentions, Unassigned, All and each personal or shared view. "Open" means
a conversation whose status category is open, so snoozed and resolved
conversations are not counted.

Backend:
- GET /api/v1/conversations/sidebar-counts returns the counts, gated by
  the conversations:read permission. Each individual count is additionally
  gated server-side by the permission for that list, so an agent never
  receives a number for a list they cannot open.
- View counts are unioned into a single statement, so the number of views
  does not drive the number of queries.
- The list-type conditions shared by the conversation list and count
  queries are extracted into appendListTypeConditions, so both stay in
  sync on what each list type means.
- An empty team list now renders as IN (NULL) instead of the invalid
  IN (), which an agent holding a team permission without a team hit.
- The feature can be turned off via app.sidebar_counts_enabled, which
  defaults to enabled when the setting is missing.

Frontend:
- Counts refresh when switching inboxes, after the agent's own status or
  assignee changes, and from new_conversation and status-carrying
  conversation_update websocket events.
- Refreshes are throttled and share a short TTL, so bursts of incoming
  conversations collapse into a single request. Re-enabling the feature
  bypasses the TTL, because being disabled leaves a zeroed result cached.

Refs #466

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 16:58:30 +02:00
Abhinav Raut c0c21bc56a raise the response body pool limit to 1mb 2026-08-22 12:51:47 +05:30
Abhinav Raut 5e168eaee1 raise the avatar upload limit to 10mb 2026-08-22 03:21:02 +05:30
Abhinav Raut fe243aa9ab downscale avatars to 256px on upload 2026-08-22 03:08:29 +05:30
Abhinav Raut 19146541e4 use the country code label and gate the conversations list button too
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.
2026-08-21 23:26:40 +05:30
Abhinav Raut a6e7812c68 name the field in pre-chat length errors and hide the start button when blocked
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.
2026-08-21 23:14:34 +05:30
Abhinav Raut 40b86c2dd2 check the widget client's closed flag after refreshing the read deadline
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.
2026-08-21 21:22:16 +05:30
Abhinav Raut 8a881909b8 actually close agent websockets on kick and shutdown
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.
2026-08-21 21:00:39 +05:30
Abhinav Raut 4ddec80ff2 ignore frames from a replaced widget socket
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.
2026-08-21 20:27:33 +05:30
Abhinav Raut e324e136f5 drop widget sockets when a livechat inbox reloads
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.
2026-08-21 20:11:24 +05:30
Abhinav Raut 35ea94a439 set the shutdown budget to 8s 2026-08-21 18:45:36 +05:30
Abhinav Raut 2c52e69bcf reword the shutdown timeout log 2026-08-21 18:41:21 +05:30
Abhinav Raut 1028e688b5 set the shutdown budget to 10s 2026-08-21 18:19:25 +05:30
Abhinav Raut 3393b6e1cb close websockets and bound the http server shutdown 2026-08-21 18:11:44 +05:30
Abhinav Raut 9475e6ddd3 Merge pull request #516 from abhinavxd/geist-and-hc-page-cache
switch to Geist and cache help center pages
2026-08-21 05:16:41 +05:30
Abhinav Raut a9867dce00 move public help center api under /api/v1 and add security headers 2026-08-21 04:55:07 +05:30
Abhinav Raut 01c277159c shorten help center cache helper names 2026-08-21 04:31:52 +05:30
Abhinav Raut 6208b291ce fix cache misses, wrong view counts, and silent cache errors in the help center 2026-08-20 21:56:54 +05:30
Abhinav Raut 3da864b5dd remove comments that narrate rejected designs 2026-08-20 19:11:02 +05:30
Abhinav Raut ae8c6ba6ef fix stale and broken pages in the help center cache
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.
2026-08-20 19:07:36 +05:30
Abhinav Raut c865748ca5 switch to Geist and cache help center pages
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.
2026-08-20 18:01:42 +05:30
Abhinav Raut 6a6a0a818d simplify websocket client and fix handshake error response 2026-08-20 14:46:10 +05:30
Abhinav Raut 1d9c20f343 guard websocket client sends against the channel close
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.
2026-08-20 13:15:31 +05:30
Abhinav Raut a1cae0b896 close and release websocket connections promptly on failure paths
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.
2026-08-20 12:46:38 +05:30
Abhinav Raut 4fce040d6b reduce websocket per-connection memory and hot-path allocations
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.
2026-08-20 12:29:14 +05:30
Abhinav Raut d0daeb8659 Merge remote-tracking branch 'origin/main' into reduce-memory-and-cpu-hotpaths 2026-08-19 21:54:16 +05:30
Abhinav Raut 91f897a767 Merge pull request #509 from abhinavxd/test-coverage
Increase Test coverage
2026-08-19 14:51:48 +05:30
Abhinav Raut 3568aa18c6 restrict private note deletion to the note author or an admin 2026-08-19 14:35:32 +05:30
Abhinav Raut 4277ff501f fix sla apply atomicity and superseded sla cleanup, read static file bytes once 2026-08-19 14:03:29 +05:30
Abhinav Raut 9708f78c79 reduce memory and cpu use in sla, static asset and api key paths
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.
2026-08-19 12:55:52 +05:30
Abhinav Raut f4763dfc12 add api contract specs, form schema tests and validator unit tests 2026-08-19 03:26:05 +05:30