Commit Graph
3 Commits
Author SHA1 Message Date
ogrechkoandClaude Sonnet 5 e42fd4697f Add mobile bottom nav + PWA installability
Same shell pattern as top-buh/top-finance/thbd-portal: desktop top nav
stays >=lg, a fixed bottom tab bar takes over below it. Only 2 nav
links here so both fit directly, no overflow menu needed.

Also adds manifest.webmanifest + 192/512 icons (rendered from the
existing icon.svg) and appleWebApp/viewport metadata so the app can be
installed to a phone's home screen.

Nudged ticket-toasts.tsx's fixed bottom offset so it clears the new
bottom nav bar on mobile instead of overlapping it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gh2UXUQUVBWroWEnn1FLFG
2026-09-07 07:58:52 +00:00
ogrechkoandClaude Sonnet 5 b9a247c21c Add Web Push notifications for backgrounded/frozen browser tabs
The SSE + in-page toast/sound system only works while the tab's JS is
actually running — Chrome (and other browsers) freeze a backgrounded
tab's JS after a stretch of inactivity to save power, so notifications
silently stop regardless of how correct the SSE/toast code is. A Service
Worker is the only mechanism that keeps receiving events independent of
the tab's own lifecycle, so this adds a real Web Push pipeline:

- New `push_subscriptions` table (one row per browser/device a user has
  subscribed from; endpoint is unique so re-subscribing overwrites
  rather than accumulating stale rows).
- VAPID keypair config (lib/push/vapid.ts) — reads
  VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEY/VAPID_SUBJECT from the environment;
  push is silently disabled (never throws) if they're unset, matching
  this codebase's existing "an optional integration outage must never
  break the core flow" pattern (see the LDAP auth comment).
- public/sw.js: a minimal service worker — `push` -> showNotification,
  `notificationclick` -> focus or open the ticket.
- API routes: GET /api/push/vapid-public-key (client needs it to call
  pushManager.subscribe), POST/DELETE /api/push/subscribe.
- lib/push/client.ts: registers the service worker and subscribes,
  called from the notification bell after granting permission and again
  on mount for returning users who already granted it.
- lib/tickets/service.ts: appendMessage() now also fires a push (fire-
  and-forget, never awaited by the caller) to every admin plus the
  ticket's assignee whenever a *customer* message arrives — same "who
  should know" rule as the in-page toast (lib/tickets/visibility.ts).
  Failed sends are inspected: a 404/410 (push service no longer
  recognizes the subscription) prunes the row; anything else is just
  logged, since it might be transient.

Verified server-side end-to-end on this VM: saved a subscription via the
API, created a customer ticket, and confirmed the push attempt actually
fires (web-push validated and rejected a deliberately-malformed test
key, proving the send path is wired correctly) without blocking or
crashing ticket creation. Couldn't verify the full real-browser
subscribe-and-receive path or an actual OS popup from here — this VM has
no desktop/notification service, and headless Chromium's Notification
permission can't be reliably granted in this sandbox (unrelated to the
app code); that last mile needs verifying on a real machine.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012o9j9RezxbZVKQMrB7oRLY
2026-08-07 19:30:27 +00:00
ogrechkoandClaude Sonnet 5 4efce5f307 Add embeddable chat widget for websites
Reuses the existing portal-token/SSE mechanism (customers.portalToken,
/api/portal/events) for anonymous widget visitors instead of building a
parallel auth system. Public API: session bootstrap + messages endpoint,
both scoped by a non-secret siteKey with optional origin allowlist.
Embeddable script (public/widget.js) is a small self-contained vanilla JS
file: floating button + iframe kept mounted for a live SSE connection,
with a postMessage bridge for the unread badge. Admin UI at
Settings -> Виджет manages sites and shows the embed snippet.

Also generalized recordTelegramInboundMessage -> recordChatInboundMessage
(channel param) since Telegram and the widget need the identical
find-or-create-open-ticket heuristic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcXH24ky6zjk2UyK5oZUPH
2026-07-26 13:34:08 +00:00