Commit Graph

22 Commits

Author SHA1 Message Date
10699d1ad1 Mitra Profil: WA/TG contacts + Keluar-only (no Hapus Akun)
Replaces the placeholder "Hubungi Koordinator" row with two real
contacts pulled from backend config (support_handles_json), and drops
the "Hapus Akun" CTA. Mirrors the figma BestieProfile design but uses
the same WA/TG channel as the customer Tanya Admin sheet — business
decided the same ops team triages both audiences.

Backend:
- Promote support-handles route from /api/client to /api/shared
  (renamed file + export). Both apps now consume the same endpoint;
  hitting /api/client/* from mitra would violate the per-app
  convention in mitra_app/CLAUDE.md.
- client_app provider updated to /api/shared/support-handles.

Mitra app:
- New support_handles_provider mirroring the client_app one. Adds a
  `displayHandle` getter that strips the URL scheme for the subtitle
  ("https://wa.me/X" → "wa.me/X", "https://t.me/Y" → "t.me/Y") so the
  row looks like the figma without exposing raw URLs.
- Profil screen now lists: Chat WhatsApp Kami, Chat Telegram Kami,
  Syarat & Ketentuan, Kebijakan Privasi. Danger zone simplified to
  Keluar only — mitras request account deletion through the same
  WA/TG channels (no separate self-service path).
- url_launcher added as a runtime dep, launches deeplinks in
  externalApplication mode with graceful snackbar fallback when
  parsing or launching fails.

Updates [[feedback-mitra-internal-audience]] — pre-login rule still
holds (no admin CTAs on S3a/S3b/AccountInactive), but the post-login
Profil tab now does surface WA/TG. Overrides decided 2026-05-21.

Verified on emulator-5556: Profil tab renders both rows with handles
from `wa.me/6285173310010` + `t.me/halobestie`, Keluar present, no
Hapus Akun button.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:32:21 +08:00
e4bffe1a71 Extension request: WS→FCM fallback + chat-recovery on connect
Today the customer's "Perpanjang" only reaches the mitra via session-
scoped WS. If the mitra is on Home/Undangan, in a different session, or
backgrounded, the WS send no-ops and the 10s safeguard timeout fires
auto-reject (or auto-approve if the mitra happens to also have an
active general WS, depending on config) — either way the mitra never
saw the request.

Backend:
- extension.service.js::requestExtension now falls back to FCM via
  notification.service when the mitra isn't on the session WS. Mirrors
  the pairing notifyMitra pattern (Curhat Baru). Customer display name
  is pulled into the session lookup for the FCM body.
- shared.chat.routes.js: /chat/:sessionId/info now returns
  pending_extension (extension_id, duration_minutes, price,
  requested_at, expires_at, timeout_seconds) so the chat screen can
  rehydrate the accept/reject UI after a cold-start FCM tap. expires_at
  is derived from requested_at + extension_timeout_seconds config.

Mitra app:
- mitra_chat_notifier.dart::connect parses pending_extension from /info
  and seeds MitraChatConnectedData.extensionRequest — the existing
  _buildExtensionView renders unchanged.
- notification_service.dart::_navigateFromMessage handles
  type=extension_request → pushes /chat/session/<id>. Composes with
  the new /info pending_extension to bring the mitra straight into the
  accept/reject view.

Verified end-to-end on dev backend (FCM call returned sent=true; /info
returns pending_extension when within timeout window). Visual delivery
on emulator-5556 deferred — API 24 AVD queues FCM 5-30 min per
feedback-emulator-avd-versions.

Out of scope (follow-ups):
- Customer-side FCM for EXTENSION_RESPONSE (accepted/rejected/timeout)
- Perpanjang tab list endpoint + Flutter provider + UI

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 13:24:40 +08:00
1c9d81d81d Pricing: migrate from app_config JSON to relational tables
Replaces the two `pricing_*_tiers_json` blobs and five `first_session_discount_*`
keys in app_config with dedicated `pricing_tiers` and `pricing_promotions`
tables plus matching `_history` audit tables. UUID PKs, UNIQUE(mode, minutes)
natural-key constraint, optimistic-lock via `updated_at` token returning 409
STALE_WRITE on conflicts. Every mutation writes a history row capturing the
operator (changed_by from request.auth.userId) and change_kind.

CC SettingsPage replaces the JSON-textarea editors with per-row tables —
add / edit / soft-delete / reactivate / reorder, plus a buffered first-session
discount form with the same optimistic-lock contract. `minutes` and `mode` are
read-only on edit since they form the natural key; operators soft-delete and
recreate to change duration.

Stage 5 fixes a latent leak: `client.payment.routes.js` had its own local
`readDiscountConfig` that still read from app_config — would have silently
fallen to hardcoded defaults once the legacy rows were deleted. Now reads from
pricing_promotions via the shared service helper, so CC edits to the first-
session discount affect actual payment pricing on the next request.

Customer-facing GET /api/client/chat/pricing shape unchanged (id values are
now UUIDs instead of "5"/"12"/"60" but lookups happen by (mode, minutes), so
no app changes needed). 27 new backend tests, all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:12:11 +08:00
a09f37135c Phase 4 checkpoint: chat-screen perf refactor + retryable blast-failure + repo-wide dispose-ref guardrail
Chat-screen performance (customer + mitra):
- Parent screens have zero `ref.watch` — only `ref.listen` for side effects
- Body extracted into its own `ConsumerStatefulWidget`; AppBar parts split
  into narrow `.select` consumers (mode, sensitivity, timer)
- Per-second timer ticks routed to dedicated providers
  (`chatRemainingSecondsProvider` + new `mitraChatRemainingSecondsProvider`)
  so WS `session_tick` frames don't invalidate the rest of the chat state

Dispose-in-ref bug fix:
- `home_screen.dart`, `payment_screen.dart`, `mitra_chat_screen.dart` —
  ref-using cleanup moved from `dispose()` to `deactivate()`. Modern
  Riverpod invalidates `ref` the moment `dispose()` runs; the resulting
  silent error corrupts the widget-tree finalize and the next screen
  appears frozen
- `halo_lints` package added at repo root with `no_ref_in_dispose` rule
  to catch this pattern in CI / IDE analysis
- `custom_lint` activated in both apps' `analysis_options.yaml`
  (was installed but never wired in — also brings `riverpod_lint`'s
  `avoid_ref_inside_state_dispose` online)
- CLAUDE.md Pitfalls section added to client_app + mitra_app

Phase 4 §3 retryable blast-failure (Option A):
- Backend `expirePairingRequest` + all-rejected use
  `recordIntermediateFailure` instead of `failPaymentSession` so the
  payment session stays `confirmed` for re-blast
- WS `pairing_failed` payload carries `is_terminal: false` on the
  retryable paths; client parses the flag and exposes `retryBlast()`
- "Coba cari lagi" CTA on S7 Timeout now re-blasts on the same payment
- Pairing service test updated to reflect the new semantics

Customer waiting-payment screen navigation patch:
- `_navigateTerminal` uses `Future.microtask` + `addPostFrameCallback`
  redundancy after a release-mode bug where polling stopped but
  `context.go` never fired, leaving the screen visually stuck on
  "menunggu pembayaran"

See requirement/resume-2026-05-15.md for next-day pickup checklist
(mitra release rebuild + S21 Ultra install + retest is the gating item).

Bundles unrelated in-flight Phase 4 §2.x work that was already on disk
(ESP screen removal, USP one-time gate scaffolding, bestie-availability
public route, OTP service edits, Maestro flow tweaks) — kept together
to avoid a partial-rebase mess.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:12:34 +08:00
a48f108fc0 Phase 4 §2.1: anonymous → existing-user merge breadcrumb
Adds `customers.account_belongs_to UUID NULL` and refactors customer
sign-in (phone/Google/Apple) so an anon row that re-verifies into an
existing customer no longer 409s. Instead the anon row stays intact
with a breadcrumb pointing at the real customer; tokens are issued
for the existing user. Actual data reconciliation onto the existing
row (chat_sessions, customer_transactions, payment_sessions,
pairing_failures) is deferred.

Backend
- migrate.js: ADD COLUMN account_belongs_to UUID REFERENCES customers(id)
  ON DELETE SET NULL.
- customer.service.js: stampAccountBelongsTo helper; account_belongs_to
  exposed in CUSTOMER_SELECT.
- auth.service.js: new shared resolveCustomerForIdentity (4-case logic);
  normalizeIdentityConflict + IDENTITY_ALREADY_LINKED 409 deleted;
  completeCustomerPhoneSignIn / signInWithGoogle / signInWithApple all
  route through the shared helper.
- client.auth.routes.js: new resolveAnonymousCustomerId picks the anon
  prefix ONLY from a verified Bearer JWT — closes the UUID-leak attack
  where a tamper-able body field could mis-route someone else's
  transactions. /otp/verify, /google, /apple all use it; the body field
  `anonymous_customer_id` is no longer accepted on any of them.
- test/services/auth.service.test.js: 9 Vitest cases covering phone +
  Google + Apple, all 4 logic cases + multi-merge accumulation.

Customer app
- auth_notifier.dart::verifyOtp: drop `skipAuth: true` and the dead
  body field so ApiClient auto-attaches the anon's Bearer from
  AuthBridge. Survives the AuthOtpSentData state transition (the
  earlier `_currentAnonymousCustomerId()` state-drop bug is bypassed by
  sourcing the id from the bridge instead of state).
- Google + Apple client paths remain unchanged (gated on provider
  creds; mirror this fix when wiring lands).

Docs
- flow_customer.mermaid.md: new §2.1 sub-section with the merge
  diagram, schema note, replaces-current-behaviour paragraph, and
  Bearer-only security callout.
- phase3.4-testing.md: §1.5 line 76 simplified (no more per-path
  split); new §1.5.1 with the 5-step operator scenario + DB invariants
  + curl recipe + Vitest pointer; new §1.5.2 covering Google/Apple
  parity (deferred client work flagged).

Verification (against live dev backend, before this commit):
- Vitest: 9/9 in auth.service.test.js; 49/51 overall (2 unrelated
  pre-existing failures in session-timer.service.test.js).
- Operator Node smoke: 14/14 in the §1.5.1 scenario; 11/11 in the
  Bearer-precedence cases.
- Real-device UI walkthrough on SM-A530F still pending — see resume
  memory `project_phase4_2_1_resume_test`.

Sister WIP bundled in migrate.js + customer.service.js: `usp_seen`
column + `markCustomerUspSeen` helper (Phase 4 USP one-time gate, was
already uncommitted in the working tree).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 23:57:53 +08:00
350b92f1f3 Phase 4 Stage 10 backend: Chat-tab feeds (pending payments + cursor history)
Backend half of Stage 10 — the new Chat tab in the customer app that
replaces /chat/history with a 3-sub-tab list (Aktif / Pembayaran /
Selesai).

- New GET /api/client/payment-sessions/pending — returns the customer's
  pending initial + extension payment sessions. Filter is status='pending'
  AND expires_at > NOW(). Mitra info comes from session_extensions →
  chat_sessions for extension rows, payment_sessions.targeted_mitra_id
  for targeted-curhat-lagi initial rows. TTL reuses the existing
  payment_session_timeout_minutes app_config row (default 20m) — no new
  config row needed since payment is still mocked.

- getCustomerHistory migrated from offset (page/limit) to cursor
  pagination. Cursor is base64url(`<endedAtIso>|<id>`) with id-tiebreak
  in ORDER BY so rows with identical timestamps don't duplicate or skip
  across pages. SELECT now JOINs payment_sessions to surface `mode`
  (chat/call) for the Selesai-row voice-call pill.

- requirement/flow_customer.mermaid.md: new §7 Chat Tab subgraph + Figma
  cross-ref entry for SChatList.

- requirement/phase4-customer-flow-plan.md: Stage 10 plan section. Also
  carries forward earlier uncommitted "Post-Stage-8 corrections" notes
  from the Stage 9 sweep (boot path / SHome1st / onboarding fixes).

Tests: +7 for getCustomerPendingPayments (initial null mitra,
targeted-mitra fill, extension-via-session JOIN, mixed-newest-first,
expired excluded, non-pending excluded, customer scoping). +10 for
cursor history (empty, exact-fit, multi-page walk, same-timestamp
tiebreak, limit clamp, customer scoping, CLOSING+COMPLETED only).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:04:58 +08:00
d33d4419ea Phase 4 Stage 1: backend foundation (additive endpoints + schema)
Schema (idempotent migration):
- payment_sessions.is_free_trial -> is_first_session_discount (data copied)
- payment_sessions.mode TEXT NOT NULL DEFAULT 'chat' CHECK (chat|call)
- chat_sessions.topics TEXT[] for ESP picks (info-only)

New endpoints:
- GET /api/client/onboarding-state (drives verif sheet + S6 paywall gate)
- GET /api/client/chat-pricing (rewrite: chat+call groups + first-session
  discount block, per-customer eligibility)
- GET /api/shared/auth-providers (env-probed; replaces ENABLE_SOCIAL_AUTH
  build flag — frontend cutover lands in stage 2)
- GET /api/client/support-handles (Tanya Admin handles, CC-config-driven)

session_warning WS event fires once at 180s remaining.

app_config seeds (mock pricing tiers, first-session discount, support
handles, payment method order, end-session 2-step toggle).

CC SettingsPage: 3 new sections (first-session discount, pricing tiers
JSON editors, support handles).

15/15 Vitest passing. chat_sessions.is_free_trial also renamed for
consistency (plan only specified payment_sessions; pairing.service.js
read both).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:56:28 +08:00
d09e50af55 Phase 3.7: paid pairing flow + returning chat + extension flip
- Backend: payment_sessions + pairing_failures tables; payment.service.js
  and pairing-failure.service.js (new); rewritten pairing.service.js
  (payment-gated blast + targeted "Curhat lagi" + cancel + fallback);
  rewritten extension.service.js (data-driven auto-approve with offline
  safeguard, charge-at-approval); pricing.service.js (extension tiers
  without free trial); mitra-status.service.js (countAvailableMitras
  cached path); 60s sweeper for stale payment sessions
- Backend routes: client.payment.routes, client.mitra-availability.routes,
  internal/failed-pairings.routes; client.chat.routes rewritten for
  payment-gated start + /returning + /cancel + /fallback-to-blast;
  internal/config.routes adds 4 new keys with Valkey invalidate publish
- client_app: mitra-availability poll, payment screen + notifier, pairing
  notifier rewrite (PairingTargetedWaiting + PairingFailed states),
  targeted-waiting overlay + bestie-unavailable dialog, "Curhat lagi"
  CTA, failed-pairing terminal, extension via payment-session
- mitra_app: PairingRequestType enum, returning-chat 20s countdown
  auto-dismiss, extension card "otomatis disetujui" copy
- control_center: 4 new config rows in Settings, Failed Pairings page
  (filter + paginate + action menu), sidebar + route registered
- Test infrastructure: Vitest backend (7/7 pass), Playwright CC (4/4
  pass), Maestro mobile scaffold (CLI install pending)
- Bugs found via Playwright + fixed: LoginPage labels not associated
  with inputs (a11y); backend internal CORS missing PATCH/PUT/DELETE
  in allow-methods (silent settings breakage in browsers since Stage 4)
- Docs: phase3.7.md PRD, phase3.7-plan.md, phase3.7-questions.md (Q&A),
  phase3.7-testing.md (E2E checklist), phase3.7-test-run-2026-05-03.md
  (today's run results)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 23:02:49 +08:00
89afd01899 Phase 3.5: Mitra Chat Request History (backend route + mitra app screens)
Replaces the home-screen pending-requests banner with a "Riwayat
Permintaan" CTA that opens a list of the mitra's last 20 chat requests
(any status). Pending rows pin to the top; non-pending rows open a
read-only detail screen with a "Lihat percakapan" CTA on accepted rows.

Backend:
- New service `getRecentRequestsForMitra(mitraId, { limit })` capped at
  20, pending pinned via `(response IS NULL AND status='pending_acceptance')
  DESC`. Customer call_name returned verbatim, with `'Anonim'` only as
  null-safety fallback (no anonymity-flag masking — see project memory).
- New route `GET /api/mitra/chat-requests/recent`. Strictly per-mitra
  scoped via the existing `resolveMitra` preHandler.

Mitra app:
- New `RequestResponse` enum in core/constants.dart.
- New Riverpod notifier `requestHistoryProvider` (AsyncValue<List<...>>,
  keepAlive) — pull-to-refresh + screen-mount fetch only, no WS.
- Two new screens (history list + detail) and two new GoRoutes.
- Home screen: `_PendingRequestsBanner` removed → `_RequestHistoryButton`
  Card with red count badge. Live count comes from the existing
  chatRequestProvider so nothing changes about the WS-driven badge math.

Plan + acceptance criteria in requirement/phase3.5-plan.md. flutter
analyze clean (zero new issues). Backend smoke-tested against real DB.
Real-device E2E pending.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 18:59:17 +08:00
fa7071def5 Phase 3.4: structured rate-limit retry-after + auth error logging
OtpError now carries an optional details object; rate-limit branches in
checkRateLimits compute retry_after_seconds (cooldown delta for OTP_COOLDOWN,
window-roll-out delta for OTP_RATE_LIMIT_PHONE / OTP_RATE_LIMIT_IP) so the
client can disable Kirim OTP / Kirim ulang CTAs with a real countdown.

All four sendAuthError helpers (client, mitra, shared, internal) now surface
err.details and log unhandled (no statusCode) errors at level 50.

New GET /api/shared/config/otp returns the resend cooldown so the OTP screen
can gate the resend countdown without hardcoding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:43:56 +08:00
6801001b64 Phase 3: closing-overlay fix + goodbye-composer dedupe
Customer chat refreshSessionStatus now clears sessionExpired carryover so the
goodbye composer renders correctly when re-opening a closing session from
history. Backend /api/shared/chat/:id/info returns goodbye_submitted_by_me;
both apps suppress the composer for the side that has already submitted and
render an awaiting-banner view instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:43:19 +08:00
f860ab6c85 Phase 3.4: backend self-managed auth cutover
All backend auth now goes through our own token service — Firebase Auth
dependency is fully removed from auth paths. FCM (firebase-admin messaging)
is still used for push.

Schema:
- auth_sessions (multi-device refresh tokens, bcrypt-hashed)
- otp_requests (Fazpass reference + rate-limit history)
- customers.email + google_sub + apple_sub (social identity)
- control_center_users.password_hash + failed_login_count + lockout_until
- firebase_uid columns made nullable (drop in later cleanup migration)
- 6 new app_config keys for OTP + CC lockout tuning

Services:
- password.service.js — bcrypt cost 12 + complexity (min 8, digit + upper +
  lower)
- token.service.js — JWT HS256 access (1h) + opaque refresh (30d, bcrypt-
  hashed, rotated on use); session_id claim pre-wires future Valkey-based
  instant revocation; revokeSession + revokeAllSessionsForUser helpers
- social-identity.service.js — Google via google-auth-library, Apple via
  jwks-rsa + jsonwebtoken
- otp.service.js — Fazpass stub (generates locally, logs the code) clearly
  marked for replacement once real API docs arrive; rate-limit + resend
  cooldown + verify-attempts all configurable via app_config
- auth.service.js — orchestrator: signInAnonymous, completeCustomer/Mitra-
  PhoneSignIn, signInWithGoogle, signInWithApple, signInCcUser, refresh,
  logout; reject-on-existing for identity conflicts
- cc-user.service.js — email+password helpers + lockout counters

Routes & middleware:
- authenticate middleware now verifies our JWT and attaches
  request.auth = { userType, userId, sessionId }
- WebSocket handshake verifies our JWT (no more Firebase lookup)
- All existing routes updated to use request.auth.userId instead of
  request.firebaseUser.uid
- New public routes:
    /api/shared/auth/anonymous /refresh /logout
    /api/client/auth/otp/request /otp/verify /google /apple /me /profile
    /api/mitra/auth/otp/request /otp/verify /me
- New internal routes:
    /internal/auth/login /refresh /logout /me (httpOnly cookie refresh)
    /internal/control-center-users (accepts plain password, bcrypt-hashed)
    /internal/control-center-users/me/password (self-service change)
    /internal/control-center-users/:id/password (admin forced reset)
- Deleted legacy customer.routes.js (anonymous + link handled by auth now)
- app.internal.js: @fastify/cookie + CORS credentials for CC httpOnly cookie

Config:
- AUTH_JWT_SECRET + ACCESS_TOKEN_TTL_SECONDS + REFRESH_TOKEN_TTL_DAYS env
- FAZPASS_* env vars (TBD until real API docs)
- GOOGLE_OAUTH_CLIENT_IDS, APPLE_SERVICES_ID/TEAM_ID/KEY_ID/PRIVATE_KEY
- ADMIN_EMAIL + ADMIN_PASSWORD for seed
- CC_ORIGIN for internal-app CORS origin allowlist

Dependencies:
- Added: bcrypt, jsonwebtoken, jwks-rsa, google-auth-library, @fastify/cookie
- Kept: firebase-admin (messaging only)

Still outstanding: Fazpass API integration (stub in place), Apple Developer
prereqs for end-to-end iOS testing, client_app/mitra_app/control_center auth
flow rewrites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 11:43:25 +08:00
780cade3db Phase 3.3: topic sensitivity + Phase 3.4: auth foundation
Phase 3.3 — Session Topic Sensitivity (complete):
- Backend: topic_sensitivity column + session_sensitivity_log, sensitivity service
  (flip with one-way-latch + audit), PATCH /api/shared/chat/sessions/:id/topic,
  topic carried in pairing + extension WS payloads, CC filter + sensitive stats
  + per-mitra sensitive columns on activity page
- client_app: TopicSelectionBottomSheet before pricing, topic flows through
  pairing request, silent WS handler for session_topic_updated
- mitra_app: SensitivityBadge + SensitivityTheme + sensitivityConfigProvider,
  overlay badge + yellow accent, chat screen app-bar toggle with configurable
  confirmation + latch, extension card shows current flag, history + transcript
  yellow theme
- control_center: Sensitivitas Topik settings section, topic filter + column
  with inline audit log, sensitive stats dashboard card, mitra activity
  sensitive columns with QC flag

Phase 3.4 — Self-Managed Auth (foundation only):
- Migration: auth_sessions + otp_requests tables, social identity columns on
  customers, password_hash + lockout on control_center_users, OTP + CC lockout
  app_config keys
- New services: password (bcrypt + complexity), token (JWT HS256 + refresh
  rotation, session_id claim pre-wires future Valkey revocation),
  social-identity (Google + Apple JWKS), OTP (Fazpass stub — real API TBD)
- Constants: AuthProvider + OtpChannel
- Middleware, auth route rewrites, WS auth update, Firebase → FCM isolation
  still pending (next chunk); Fazpass docs + Apple Developer setup still
  required before E2E testing

Docs:
- requirement/phase3.3.md, phase3.3-plan.md, phase3.3-testing.md
- requirement/phase3.4.md, phase3.4-plan.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 10:15:12 +08:00
1920507ec5 Add pending chat requests CTA on mitra home screen
- Backend: new GET /api/mitra/chat-requests/pending endpoint
- Backend: getPendingRequestsForMitra() queries unresponded notifications
  for sessions still in pending_acceptance status
- Mitra app: loadPendingRequests() fetches on screen load + status toggle
- Mitra app: activeRequestCount getter exposes queue size
- Mitra app: _PendingRequestsBanner widget shows count with tap-to-view

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 21:21:47 +08:00
4158fb9432 Phase 3.2 docs + Phase 3.1 testing fixes
- Add phase3.2.md requirement: overlay UX, mitra activity log
- Add phase3.2-plan.md implementation plan
- Fix stale request validation: add GET /:sessionId/status endpoint
- Fix notification tap flow: setIncomingFromNotification + onChatRequestTapped
- IncomingRequestSheet shows stale message instead of auto-dismiss
- Home screen validates on resume, shows immediately on fresh WS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 22:09:25 +08:00
212e1e8ac6 Fix auth: auto-create customer, display name flow, OTP auto-verify
- Backend: getOrCreateCustomer with phone fallback for re-login
- Backend: PATCH /api/client/auth/profile for display name update
- Client app: AuthNeedsDisplayNameData state + SetDisplayNameScreen
- Client app: ApiClient.patch method
- Both apps: handle verificationCompleted for auto-verify (test numbers)
- Both apps: skip credential sign-in if already auto-verified
- Remove debug prints from mitra auth + OTP screens
- Fix ChatRequestNotifier.startListening skips when accepting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 16:22:28 +08:00
ed765d230c Phase 3.1 WS2: Backend FCM fallback, ping config, unread API
- Add require_mitra_ping + mitra_ping_interval_seconds config keys (migration)
- Add getMitraPingConfig/setMitraPingConfig to config service
- Add GET/PATCH /internal/config/mitra-ping routes for control center
- Update mitra status service: honor ping config in auto-offline sweep,
  include ping config in GET /api/mitra/status response
- Enhance pairing FCM payload with action: 'open_accept' for deep-link
- Add FCM fallback to closure.service (initiateEarlyEnd, completeSession)
- Add FCM fallback to session-timer.service (onSessionExpired)
- Add unread count queries (getActiveSessionByCustomerWithUnread,
  getActiveSessionsByMitraWithUnread)
- Add GET /api/client/chat/session/active-with-unread route
- Add GET /api/mitra/chat-requests/sessions/active-with-unread route

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:22:41 +08:00
b0502ac92b Phase 3 testing fixes: Fastify 5, SSE→WebSocket+FCM, enums, security, session lifecycle
- Upgrade Fastify 4→5 with all plugins (@fastify/websocket 11, cors 11, sensible 6)
- Migrate all SSE endpoints to WebSocket + FCM push (mitra chat requests, customer pairing status)
- Add flutter_local_notifications for foreground push notifications with sound
- Add splash screen to both apps (hide auth loading flash)
- Introduce constants/enums across entire codebase (no raw string literals)
- Move price tiers from hardcoded array to app_config DB (data-driven, includes 1-min test tier)
- Add session ownership validation on all shared chat routes
- Add ownership checks on endSession, respondToExtension, requestExtension
- Fix session timer: auto-complete expired/stale sessions on server restart
- Add 5-min grace period for abandoned closing sessions
- Fix extension flow: proper session_resumed handling, clearExtensionRequest, closure grace timer cleanup
- Fix chat screens: ConnectChat in initState, session status check on connect
- Fix customer expired view: 5-min countdown, closure state priority over expired state
- Fix mitra extension UI: loading spinner, disable buttons, handle EXTENSION_RESOLVED error
- Fix GoRouter navigation consistency (no more Navigator.pushNamed)
- Fix goodbye view keyboard overflow (SingleChildScrollView)
- Add active session card on customer home screen with refresh on navigate back
- Fix PricingBottomSheet extension mode (RequestExtension instead of new pairing)
- Send session_resumed to both parties on extension accept

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 00:17:25 +08:00
b4efcf14c2 Phase 3 scaffold: chat engine (WebSocket, FCM, pricing, timer, extension, history)
- Backend: WebSocket plugin, chat/pricing/timer/extension/closure/notification services
- Client app: ChatBloc, pricing dialog, chat screen with message status, extension/goodbye flow, history
- Mitra app: MitraChatBloc, ExtensionBloc, chat screen, extension accept/reject, history
- Control center: free trial, extension timeout, early end config toggles
- DB migration: chat_messages, session_closures, session_extensions, customer_transactions tables

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:58:11 +08:00
844d7234e6 Phase 2 refinements: Firebase config, dev environment fixes, phase 3 requirement draft
- Integrated Firebase SDK in both Flutter apps (google-services, firebase_options)
- Fixed auth flow, API client, and pairing/status blocs for dev environment
- Added full Flutter project scaffolds (android, ios, web, etc.)
- Added phase 3 chat engine requirement document
- Added bugreport zip pattern to gitignore

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 19:16:34 +08:00
d668112edd Phase 2 scaffold: mitra online status & pairing logic
Add mitra online/offline status with heartbeat-based auto-offline,
customer-mitra pairing via Valkey pub/sub blast, session management,
and control center dashboard with real-time stats.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 23:17:49 +08:00
a7a2a32d27 Phase 1 scaffold: auth for all apps
- Backend: Fastify with two listeners (public + internal), routes, services, DB migration + seed
- client_app: Flutter with BLoC, all auth screens (welcome, display name, register, OTP, force-register)
- mitra_app: Flutter with BLoC, OTP-only login
- control_center: React + Vite, email/password login, mitra/user management, anonymity settings
- Docs: phase1 plan, API contract, client app mockup
- CLAUDE.md and shared memory for all subprojects

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 10:08:42 +08:00