feat(mobile): installable agent PWA + agents can see the AI queue#211
Merged
Conversation
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
force-pushed
the
claude/agent-login-mobile-pwa-ce014a
branch
from
July 18, 2026 16:18
34d4d12 to
fbcb688
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes the agent dashboard usable on a phone and installable to the home screen.
Mobile UI
?session=query param, so hardware back works and push deep links use the same path. Desktop layout is unchanged.PWA
src/sw.ts) via vite-plugin-pwa, replacing the standalonefirebase-messaging-sw.js+ build-timeswenv.js.controllerchangethat doesn't fire onceclientsClaim()has claimed the client, so the reload is explicit.nginx.confmust not cachesw.js/manifest.webmanifest— without that, SW updates never land.Push
getMessaging()is now lazy behindisSupported(); it threw on iOS Safari outside a standalone install.onMounted— the old placement silently failed on iOS.session_idand JSONmetadata(it was sending a Pythonrepr), 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 queueview_people— read-only people directory, previously gated onview_all_chatsThe 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 madeget_default_role()return whichever row the DB happened to yield first.Two bugs found along the way:
manage_chats, a permission that has never existed indefault_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.group_idNULL, so a transferred chat reached nobody's queue.Deploy notes
add_customer_phone_001); no dependency on any other branch.npm install— addsvite-plugin-pwa.firebase-messaging-sw.js;pwa/register.tsunregisters it once on startup.VITE_FIREBASE_*changes the page config but not the worker.1751 backend tests and 212 frontend tests pass.