Chat agent: pin language to user's messages, not data#47
Merged
Conversation
Admin impersonation (the headline feature): - New `impersonateUser` callable in `functions/src/admin/impersonateUser.ts`: admin-only, mints a Firebase custom token signing the admin in as the target user, refuses to impersonate other admins (privilege confusion), logs the action server-side. - `/admin/users` UserDetailPanel: "Impersonate (opens new tab)" button. - New `/impersonate` route: consumes the token from the URL fragment, signs in via `signInWithCustomToken`, sets a per-tab marker, redirects to `/transactions`. Token never lands in URL/history. - `ImpersonationBanner` in dashboard layout: persistent amber banner showing "Impersonating X · as admin Y", with an Exit button that signs out and clears the marker. sessionStorage scoping keeps the admin's main tab untouched. CSP runtime fixes (CASA Tier 2 prod CSP was too tight): - `frame-src` now allows `blob:` (PDF/file previews fetched as blobs) and `https://firebasestorage.googleapis.com` (issued invoice PDFs). - Dev mode additionally relaxes connect-src/img-src/frame-src for `127.0.0.1:*` + `localhost:*` so Firebase emulators are reachable, and skips `upgrade-insecure-requests` + the `/__/auth/` rewrite that would otherwise route emulator OAuth callbacks to prod. - PDF.js worker self-hosted via `new URL(..., import.meta.url)` instead of loading from `unpkg.com` (CSP would block it; CDN dep removed too). Auth flow simplification: - Switched OAuth from `signInWithRedirect` to `signInWithPopup` everywhere. Firebase's 2026 guidance: redirect needs a custom authDomain or reverse proxy since browsers (Chrome M115+, Firefox 109+, Safari 16.1+) blocked third-party cookies in cross-site iframes. Popup sidesteps that entirely and works with the local Auth Emulator. Dropped the dead `getRedirectResult` effect, the `pendingRedirect` SSR state, and the `fibuki_oauth_provider` sessionStorage dance. - Wrapped `firebaseUser.getIdTokenResult` in try/catch so a transient network failure no longer crashes the dev server with an unhandled rejection. - Emulator host pinned to `127.0.0.1` (was `localhost`) to avoid the macOS IPv6 resolution that emulators don't bind. Settings/identity: - On save, mirror the top-level tax residence country onto the personal entity's address so invoice sender snapshots carry a country. Test plan: - `npm run lint` → 0 errors, 40 warnings (unchanged) - `npx tsc --noEmit` → clean (both root and functions) - `npm run build` → ✓ Compiled successfully - `(cd functions && npm test)` → 469/469 pass Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- New `bulkRetryExtraction` callable (admin or self): processes up to 50
errored files per call in waves of 5 concurrent extractions, returns
{ processed, succeeded, failed, hasMore, sampleErrors }. Mirrors
`retryFileExtraction`'s reset semantics so partner/transaction
matching also re-runs.
- UserDetailPanel: "Rescan all errored files" button — handler polls
the callable up to 10 batches (500 files max) until hasMore is false,
then surfaces a summary toast.
- Resolves the friend's pain point of having to click the spinner on
each errored file individually after a Gemini model deprecation.
Test plan:
- `npm run lint` → 0 errors / 40 warnings
- `npx tsc --noEmit` (root + functions) → clean
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…phan route Background: Google rejected our OAuth verification for the restricted `gmail.readonly` scope on 2026-06-09 (no response within 90 days). For unverified apps, Google silently strips restricted scopes from consent grants for non-test users, leaving users with tokens that 403 on every Gmail API call. Our code was treating these stripped-scope tokens as valid: - `/api/gmail/callback` stored whatever Google returned, no scope check - Refresh paths in `gmailSyncQueue.ts` and `searchGmailCallable.ts` silently accepted refreshed tokens without `gmail.readonly` - Gmail 403 `insufficientPermissions` did not flip `needsReauth`, so the UI showed "Scanning for invoices…" forever instead of a Reconnect CTA Fixes: - Callback: parse `tokens.scope`, redirect with `?error=missing_gmail_scope` if `gmail.readonly` is absent; no integration is created. - Refresh paths (both call sites): parse `scope` on the refresh response, set `needsReauth: true` + `lastError` and abort if `gmail.readonly` is missing, so future calls don't keep using a useless token. - Queue-processor error handler: detect Gmail 403 with reason `insufficientPermissions` (or the equivalent error string) and flip `needsReauth: true` so the integration's UI prompts a reconnect. - Gmail integration card: derive `isStuckInitialSync` when `initialSyncComplete === false`, `lastSyncError` is set, and `initialSyncStartedAt` is >24h old — treat as needsReauth visually (Reconnect Required badge instead of indefinite Syncing spinner). - Delete `/api/gmail/auth/route.ts` — legacy POST endpoint that stored pre-acquired access tokens with an empty refresh_token; nothing in the app or extensions called it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User feedback (Stefan): the assistant drifts into German half the time even when he writes in English. Cause: the previous "match the user's language" instruction was too weak, and the agent reads German invoice text, partner names, and email subjects in the database — drifting to the dominant language in its context window. Strengthen the rule: explicitly anchor on the USER'S messages (latest takes precedence), and call out that transactions/invoices/emails being in another language must NOT cause a language switch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
| @@ -0,0 +1,57 @@ | |||
| import admin from 'firebase-admin'; | |||
| import { execSync } from 'node:child_process'; | |||
| import { decrypt } from '../lib/utils/encryption.js'; | |||
| const auth = admin.auth(); | ||
| const db = admin.firestore(); | ||
|
|
||
| const ENCRYPTION_KEY = execSync( |
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.
Stefan reported the chat assistant drifts to German even when he writes in English. Cause: the previous "match the user's language" instruction was too weak; the agent reads German invoice text, partner names, and email subjects in the user's data and drifts to the dominant language in its context window.
This PR strengthens the system-prompt rule to:
Single-line change in
lib/chat/system-prompt.ts— no API surface changes.🤖 Generated with Claude Code