Skip to content

feat(mobile): installable agent PWA + agents can see the AI queue#211

Merged
chattermate merged 13 commits into
mainfrom
claude/agent-login-mobile-pwa-ce014a
Jul 18, 2026
Merged

feat(mobile): installable agent PWA + agents can see the AI queue#211
chattermate merged 13 commits into
mainfrom
claude/agent-login-mobile-pwa-ce014a

Conversation

@chattermate

Copy link
Copy Markdown
Owner

Makes the agent dashboard usable on a phone and installable to the home screen.

Mobile UI

  • Bottom nav (Inbox · People · AI Agents · Analytics · More) with a More sheet that keeps every sidebar destination — a unit test pins the parity so a new sidebar item can't silently go missing on mobile.
  • Inbox stacks list → chat → details, driven by the existing ?session= query param, so hardware back works and push deep links use the same path. Desktop layout is unchanged.
  • Responsive passes on People, Human Agents and Help center.

PWA

  • Manifest + icons; single service worker (src/sw.ts) via vite-plugin-pwa, replacing the standalone firebase-messaging-sw.js + build-time swenv.js.
  • New builds now prompt "a new version is available" instead of activating underneath an open tab. The plugin's own refresh depends on a controllerchange that doesn't fire once clientsClaim() has claimed the client, so the reload is explicit.
  • nginx.conf must not cache sw.js / manifest.webmanifest — without that, SW updates never land.

Push

  • getMessaging() is now lazy behind isSupported(); it threw on iOS Safari outside a standalone install.
  • Permission is requested from a user gesture (an in-app card), not onMounted — the old placement silently failed on iOS.
  • Backend FCM payload is data-only with a flat session_id and JSON metadata (it was sending a Python repr), so notification clicks deep-link into the conversation.

Access
Agents couldn't see unclaimed AI chats, so the queue was invisible and "Take over" unreachable. Two new permissions rather than widening view_assigned_chats, so granting the queue never exposes another agent's conversations:

  • view_unassigned_chats — the unclaimed queue
  • view_people — read-only people directory, previously gated on view_all_chats

The migration backfills by capability, not role name, so orgs that renamed or hand-built their agent role are covered. It also repairs seeding that created both Admin and Agent with is_default=true, which made get_default_role() return whichever row the DB happened to yield first.

Two bugs found along the way:

  • The takeover endpoint checked manage_chats, a permission that has never existed in default_permissions() — and did no org scoping, so any authenticated user could claim any unassigned session in any org by guessing a session UUID. Test fixtures had encoded the bug.
  • Workflow transfers left group_id NULL, so a transferred chat reached nobody's queue.

Deploy notes

  • The Alembic migration slots onto the current head (add_customer_phone_001); no dependency on any other branch.
  • npm install — adds vite-plugin-pwa.
  • Existing clients still hold the old firebase-messaging-sw.js; pwa/register.ts unregisters it once on startup.
  • Known limitation, same as today: the SW's Firebase config is build-time, so a docker env override of VITE_FIREBASE_* changes the page config but not the worker.

1751 backend tests and 212 frontend tests pass.

Mobile-responsive agent experience with a native-app feel:

- Bottom nav (Inbox / People / AI Agents / Analytics / More) on <=768px,
  permission-gated via shared navItems; More sheet with availability
  toggle, theme, overflow links, notifications and logout
- Inbox stacks into full-screen list -> chat -> details panes driven by
  the ?session= query so hardware back and push deep links share one
  path; keyboard-aware composer (visualViewport), safe-area insets, dvh
- Installable PWA: manifest + maskable icons, single service worker
  (vite-plugin-pwa injectManifest) that absorbs FCM background push;
  old firebase-messaging-sw.js/swenv.js pipeline removed
- Push hardening: data-only FCM messages (no duplicate SDK
  notification), notification clicks deep-link to the conversation,
  FCM token re-minted for already-granted users after the SW migration,
  gesture-driven enable prompt (iOS-safe), isSupported guards
- nginx: no-cache for sw.js/config.js/manifest so SW updates deploy;
  full security headers re-added on exact-match locations
- Desktop layout unchanged; both themes supported

Signed-off-by: chattermate <admin@chattermate.chat>
…rity

- Shared SW<->page push contract (src/pwa/pushContract.ts): message event
  names + conversation deep-link builder imported by both sw.ts and
  useNotifications (was matching string literals in two files); page-side
  OPEN_CONVERSATION handler now allowlists /conversations URLs
- config.js SW caching switched to NetworkFirst so online clients always
  boot on the current environment's config; cache only serves offline
- FCM token POST deduped per user via localStorage (was a redundant
  backend write on every page load); isSupported() memoized;
  SW-ready timeout named and raised to 8s
- visualViewport handling rAF-coalesced with settled onChange, killing
  scroll-to-bottom jank during keyboard/URL-bar animation
- Icon set extracted to navIcons.ts with cached svg builder; theme trio,
  bell and chevron glyphs now shared (were 3-4 inline copies);
  getInitials util replaces the fourth hand-rolled initials impl;
  THEME_COLORS constant shared by useTheme + manifest
- z-index scale tokens (--z-*) replace scattered literals across the
  five overlay components; breakpoint constants cross-referenced
- Shared permission-mock fixture for specs

Signed-off-by: chattermate <admin@chattermate.chat>
The header stacked into four lines on phones: .user-info had no flex
layout, so the block-level h2, the channel badge and the status each
claimed their own line and "Handled by AI" wrapped mid-phrase.

- .user-info is a flex column: name + channel badge on one row (name
  truncates with an ellipsis), status line beneath, no wrapping
- header actions never shrink; text buttons collapse to 40px icon
  buttons on phones so the customer name keeps its width
- header height is now a constant 54px regardless of name length

Signed-off-by: chattermate <admin@chattermate.chat>
The 6-column table was unreadable on a phone — values truncated to
"a…"/"loc…", the header labels overlapped each other, and the KPI
strip and stage tabs overflowed the viewport.

- Rows become stacked cards on <=768px: avatar + name/contact and the
  stage badge on the first line, then a labelled meta line (source,
  captured, active). Sync is dropped on phones — it's CRM plumbing.
  Column labels live in the markup, desktop-hidden, since the mobile
  card layout has no table header to name the values.
- KPIs go 4-across to a 2x2 grid; stage tabs scroll horizontally;
  search takes the full width with a 16px font so iOS doesn't zoom
- PersonDetailDrawer becomes a full-screen sheet instead of a 440px
  drawer with a dead sliver, with safe-area padding and the shared
  --z-drawer token
- PeopleView's local initials() now uses the shared getInitials util

Desktop table layout is unchanged.

Signed-off-by: chattermate <admin@chattermate.chat>
The More sheet listed the overflow destinations as one flat list, which
made it hard to confirm at a glance that mobile carries the same menu as
the web sidebar.

- moreNavGroups keeps the sidebar's Main Menu / Settings headings (empty
  groups dropped), and MoreSheet renders a card per section
- new test asserts the bottom nav plus More sheet expose exactly the
  sidebar's set of destinations, so adding a nav item can never leave
  phones without it

No items were actually missing: mobile already surfaced all 12 links.
Subscription is enterprise-gated and absent from the sidebar too wherever
src/modules/enterprise isn't checked out.

Signed-off-by: chattermate <admin@chattermate.chat>
Correctness
- navItems: section membership is now explicit (groups own their items)
  instead of inferred from array position. A permission-hidden 'Settings'
  header used to orphan the always-visible User Settings link into Main
  Menu — in the desktop sidebar as well as the More sheet. Sections are
  visible when they have visible items, so the header can no longer drift
  from its contents. Covered by two new specs.
- getInitials split names on hyphens, so 'Jean-Luc Picard' became JL
  instead of JP. Names now split on whitespace; the ./_/- separators
  apply only to emails, where they are the only word boundary. New spec.
- text-overflow was set on flex containers in the People meta cells and
  the chat 'Taken over by' status, which clips instead of ellipsizing.
  The value now sits in its own span; the status also gained a title.

Duplication
- moreNavItems derives from moreNavGroups, so 'overflow link' has one
  definition rather than two that can drift apart.
- Nav section heading typography moved to a shared .nav-section-heading
  (components.css) used by both the sidebar and the More sheet.
- People column names come from one COLUMNS map feeding both the desktop
  header and the mobile card labels, which had already diverged
  ('Last activity' vs 'Active' — now an explicit short form).

Hardcoding / accessibility
- Dropped dead token fallbacks (--scrim, --z-drawer, --safe-*, --faint);
  the rgba(0,0,0,.4) scrim fallback was wrong for the dark theme anyway.
- Replaced the guessed 'flex: 1 1 calc(100% - 110px)' on People rows with
  a zero-height flex break, so a longer stage label can't wrap the row.
- Mobile touch targets back to 44px (Apple HIG / WCAG 2.5.8); the chat
  header reclaims the space from its padding instead.

Signed-off-by: chattermate <admin@chattermate.chat>
Human Agents was a 7-column table and a 4-across KPI strip on a phone:
values were clipped off-screen and the KPI text wrapped a word per line.

- Extracted the table→cards pattern into a shared .rcards-* utility in
  components.css (the review flagged it as one-off; Human Agents is now
  the second consumer) and migrated People onto it. Each table keeps its
  own desktop grid, now scoped to min-width:769px — a scoped SFC rule
  carries a [data-v-*] attribute and would otherwise outrank the global
  utility below the breakpoint.
- Human Agents: KPIs 2x2, rows become cards (agent + status on line 1,
  labelled role/teams/load/resolved below, row menu tailing the meta
  line), compact chip sizing. Row height 180px -> 144px.
- Header: 9-10px padding so the 38px controls clear the bar's bottom
  border (the 480px block was overriding the 768px one), and the topbar
  divider is hidden on phones where it read as a line touching the
  avatar.

Desktop layouts verified unchanged on both tables.

Signed-off-by: chattermate <admin@chattermate.chat>
…sponsive

Access model (agreed in discussion):
- New view_unassigned_chats permission: an agent's inbox now includes the
  unclaimed AI queue (session_to_agents.user_id IS NULL) alongside their own
  and their group's chats. A conversation another human holds stays private.
  Separate permission rather than widening view_assigned_chats, so granting
  the queue can never expose another agent's chats.
- New view_people permission decouples the directory from view_all_chats:
  agents get read-only People, while mark-customer and edit stay with the
  org-wide grant. Router meta added for /people, /conversations and
  /human-agents so a blocked user lands on /403 instead of a page that 403s
  on every request.
- Migration seeds both permissions and backfills every existing role by
  capability (any role that can view chats), not by role name, so orgs that
  renamed or hand-built their agent role are covered.

Security fix riding along: the takeover endpoint checked "manage_chats", a
permission that has never existed, so it fell through to manage_assigned_chats
with no organization scoping and no visibility check — any user with that
permission could claim any unassigned session in any org by guessing a UUID.
It now uses real permissions, honours super_admin, scopes to the caller's
organization, and requires the session to be one they can actually see.

Also fixed:
- Both default roles were seeded is_default=true, making get_default_role()
  non-deterministic while the roles API allows only one. Admin is now
  explicit, Agent is the default for newly invited users.
- A workflow transfer with no group id set status TRANSFERRED but left
  group_id NULL, hiding the handover from every agent it was meant for; it
  now falls back to the agent's default group.
- One canTakeOverChat definition shared by the chat pane and the info panel.
  The pane required status 'transferred', so claiming an AI-handled chat was
  only possible via the info panel — on mobile that meant the action was
  hidden behind the details screen.

Help center responsiveness: FAQ card controls drop to their own line so the
question gets the full width (it was wrapping a word per line), the category
header drops its decorative rule, and the three header actions share one row.

Signed-off-by: chattermate <admin@chattermate.chat>
The topic select and status filter sat at their content width while the
search box stretched, so the toolbar wrapped into a ragged column and the
select's dropdown arrow landed mid-row instead of at the edge.

Below 768px each control takes the full row, giving all three identical
edges, and the status buttons share the width evenly. Inputs go to 16px so
iOS doesn't zoom the page on focus. Desktop keeps them side by side.

Signed-off-by: chattermate <admin@chattermate.chat>
Category header: the previous fix relied on flex-wrap ordering, so a narrow
enough row could push the chevron onto a line of its own above the name. It's
now a two-column grid — chevron in column 1, name and count stacked in
column 2 — which holds at any width or category-name length (verified 320px).

Toggle switches: .switch__track is a 38px flex item with no flex-shrink, so
in a narrow container it squashed and the knob spilled over the label text,
reading as the toggle overlapping the words. Pinned the track and gave the
label min-width: 0 so long copy wraps instead. Both switch implementations
(HelpCenterPublic, HelpCenterAppearance) had the same bug.

Signed-off-by: chattermate <admin@chattermate.chat>
An open tab kept running the previous build's CSS and JS after a deploy.
sw.ts called skipWaiting() at module scope, so a new worker activated and
precached the new assets while the document that was already loaded carried
on with the old ones — the UI looked unchanged until someone happened to
hard-refresh. For a console agents leave open all day that means shipped
fixes silently don't arrive.

The new worker now waits and the page offers a toast with a Reload action,
rather than reloading by itself and yanking an agent out of a half-typed
reply. Reload posts SKIP_WAITING and then reloads the document explicitly:
the plugin's own refresh hangs off a controllerchange that does not fire
when the worker has already claimed the client, which is exactly the case
here (clientsClaim), so the worker swapped but the page never did.

Verified end to end against two frozen builds served behind a swappable
symlink: install A, deploy B, update detected, worker parks in waiting,
prompt appears, Reload swaps the live stylesheet from A's hash to B's.

Signed-off-by: chattermate <admin@chattermate.chat>
Clearing Admin is what leaves Agent as the sole default; the opposite
spelling would make every newly invited user an admin. Also drops an
inaccurate claim that the roles API rejects a second default -- nothing
enforces it, which is why the data repair is needed at all.

Signed-off-by: chattermate <admin@chattermate.chat>
Resolves the ChatInfoPanel conflict: keeps main's askRating alongside the
shared canTakeOverChat helper.

Signed-off-by: chattermate <admin@chattermate.chat>
@chattermate
chattermate force-pushed the claude/agent-login-mobile-pwa-ce014a branch from 34d4d12 to fbcb688 Compare July 18, 2026 16:18
@chattermate
chattermate merged commit 5ead9a6 into main Jul 18, 2026
7 checks passed
@chattermate
chattermate deleted the claude/agent-login-mobile-pwa-ce014a branch July 18, 2026 16:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant