asyncpg requires datetime instances for TIMESTAMPTZ columns, not strings.
Added _parse_dt() helper that converts ISO strings (with or without tz) to
timezone-aware datetime, falling back to NOW() if the value is missing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- deploy.sh: wait for postgres healthcheck before app healthcheck
- deploy.sh: add one-time JSON→PostgreSQL migration prompt (step 13)
with migration marker file to avoid re-prompting on future deploys
- deploy.sh: update summary to show both app and DB container names
- deploy.sh: check POSTGRES_PASSWORD in required keys
- .env.example: add POSTGRES_PASSWORD, ADMIN_EMAILS, emergency access vars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add asyncpg connection pool (db/pool.py) with JSONB codec registration
- Add schema.sql with users, clients, dropdown_categories, export_templates, sheets tables
- Add migrate_json.py one-time migration script for existing JSON data
- Rewrite user_store, sheets/manager, api/clients, api/dropdowns, api/export as async DB-backed
- Update all callers (auth, sheets, admin, ai_command, export) to await async functions
- Add postgres:16-alpine service to docker-compose with named volume and health check
- App container depends_on postgres; DATABASE_URL injected via env
- Schema applied automatically on startup; global categories seeded if DB is empty
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- HelpPage: remove unused navigate import
- AdminClientsPage: align state type with API response (hasCustomTemplate vs hasCustom)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers: overview, sheets, AI commands, brief extraction, export CSV,
export templates, admin (clients, dropdowns, users), and login/emergency
access. Admin-only sections are hidden for regular users.
Accessible via sidebar "Help" link at /help.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Non-JWT tokens (like emergency access tokens) were treated as expired
by isTokenExpired(), triggering a MSAL silent refresh that fails and
clears the token. Fix: non-JWT tokens are treated as never-expired and
skip the 401-retry refresh path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously AuthGate called loginRedirect() immediately when no MSAL
accounts were present, bypassing the LoginPage entirely. Now it stops
and lets the user choose — enabling emergency token login and explicit
SSO sign-in via the LoginPage button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When EMERGENCY_TOKEN is set in .env, a Bearer token matching it grants
admin access without going through Azure AD / MSAL. Useful when 2FA or
SSO is unavailable. Token is compared in constant-time to prevent timing
attacks. If EMERGENCY_TOKEN is empty (default), the feature is disabled.
Frontend: small "Emergency access" link on login page opens a token input.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Voice: switch CommandBar from PTT (hold) to toggle mode (click), update
to use new useSpeechRecognition toggle/listening API with auto-restart
- Mapping detection: new detect_excel_mapping() reads row 1 headers and
auto-detects name/status/media columns via keyword matching
- Mapping endpoints: POST /api/admin/dropdowns/detect-mapping and
/api/admin/clients/{id}/dropdowns/detect-mapping
- Upload/preview now accept name_col/status_col/media_col form fields to
apply a confirmed mapping override
- Frontend: ColumnMappingStep component shows detected columns + 5-row
sample for confirmation before upload
- AdminDropdownsPage and AdminClientsPage use 3-stage flow:
detect → confirm mapping → preview all → apply
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
clearCache() removes MSAL tokens locally; next visit auto-SSOs silently
if the user is still logged into Microsoft in the browser.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- registerTokenRefresher() lets AuthGate inject acquireTokenSilent into axios
- Request interceptor checks JWT exp, proactively refreshes if within 60s of expiry
- Response interceptor retries on 401 with a fresh token, reloads on double failure
- Previously the idToken was cached once in sessionStorage and never refreshed,
causing all requests to fail after 1 hour with "Token expired"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GEMINI_MODEL for AI commands: gemini-2.0-flash-exp → gemini-3-flash-preview
- Language/Country: handle plain 2-letter codes (EN→Language, UK→Country)
and "EN-UK" split format; previously only split format worked
- Handsontable black screen: add min-h-0 on flex-1 container so height:100%
resolves correctly inside the flexbox chain
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The error was from gemini-2.0-flash-exp (old docker-compose default),
not from gemini-3.1-pro-preview which is valid and working.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BriefReviewPage: call loadSheet() after importDeliverables so store
is refreshed before navigation — fixes 0 items on Sheet page
- Google model: gemini-3.1-pro-preview → gemini-2.5-pro-preview-03-25
(old model name was invalid, caused API errors)
- docker-compose default: gemini-2.0-flash-exp → gemini-2.5-pro-preview-03-25
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace sys.exit(1) with ValueError in _validate_models() — prevents killing the worker process on invalid model keys
- ModelConfiguration now reads defaults from core.config instead of hardcoding model keys
- Fix .env: google-gemini20 → google-gemini31 (align with MODEL_MAPPINGS)
- Improve "No data extracted" error message to explain the document must be a marketing brief
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Frontend Job interface used snake_case (file_name, file_size, progress_pct,
step_label, provider_updates, result_csv_url, created_at) but backend
returns camelCase (fileName, fileSize, progressPct, stepLabel,
providerUpdates, resultCsvUrl, createdAt) — causing all fields to be
undefined and showing 'NaN MB', broken progress bar, empty labels.
Updated types/index.ts Job, ProviderUpdate, JobSummary interfaces and
JobProgressCard.tsx to use the correct camelCase field names.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two key mismatches caused silent failures in production:
1. core/config.py LLAMACLOUD_API_KEY: was reading LLAMACLOUD_API_KEY but
docker-compose passes LLAMA_CLOUD_API_KEY (official SDK name).
Now reads LLAMA_CLOUD_API_KEY with LLAMACLOUD_API_KEY as fallback.
2. core/config.py GOOGLE_API_KEY: was reading GOOGLE_API_KEY but .env /
docker-compose use GEMINI_API_KEY. Now reads GEMINI_API_KEY first.
3. docker-compose.yml: add MSAL_* aliases for AZURE_* vars so
server/config_runtime.py picks them up explicitly (not just via defaults).
4. docker-compose.yml: pass SESSION_SECRET from .env to container.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When LLAMACLOUD_API_KEY is empty the LlamaParse client constructed a Bearer
token with an empty secret, causing Python's HTTP stack to raise
"Illegal header value b'Bearer '" and fail every upload job.
Changes:
- _extract_document_content_local(): new method using PyMuPDF (PDF),
python-pptx (PPTX), python-docx (DOCX), openpyxl (XLSX) — all already
in requirements.txt
- _extract_document_content(): skip LlamaParser entirely if key is not set;
on LlamaParser exception, fall back to local extraction instead of raising
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three bugs fixed:
1. api/jobs.ts: remove manual Content-Type header on FormData upload.
Setting it without the multipart boundary caused Quart to reject the
request body — the root cause of brief upload failures.
2. progress.py: include full job.to_dict() in job.progress / job.completed
/ job.failed WebSocket messages. Frontend checks msg.job to call
updateJob() — without it, job cards never updated in real-time.
3. AppShell: move useWebSocket() here from BriefUploadPage so the WS
connection persists across all pages, not just the upload page.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- index.css: bump text-secondary #888→#b0b0b0, text-muted #555→#787878,
border slightly lighter for better readability on dark backgrounds
- TopBar: add '← Dashboard' back button on all non-dashboard pages
- Sidebar: make AC logo clickable to navigate home
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
loading starts as true in the store, so the previous condition
`!user && !loading` prevented fetchMe from ever being called after
the Azure AD redirect — causing a permanent Loading spinner.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- msal_auth.py: replace verify_signature=False with real JWKS verification
using PyJWKClient; validates RS256 signature, aud=clientId, issuer v2.0
- App.tsx: split DEV bypass from empty-accounts case — in production,
accounts.length === 0 now correctly triggers loginRedirect instead of
calling fetchMe without a token
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>