Commit graph

36 commits

Author SHA1 Message Date
Vadym Samoilenko
d8d1dfeff5 Fix migration: parse ISO timestamp strings to datetime objects for asyncpg
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>
2026-03-23 19:58:09 +00:00
Vadym Samoilenko
1a1bc97bfc Update deploy script and .env.example for PostgreSQL
- 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>
2026-03-23 19:53:27 +00:00
Vadym Samoilenko
8da149b84e Migrate storage from JSON files to PostgreSQL (asyncpg)
- 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>
2026-03-23 19:51:37 +00:00
Vadym Samoilenko
b670505956 Fix TypeScript build errors
- 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>
2026-03-23 19:34:11 +00:00
Vadym Samoilenko
8f57c657fa Add Help page with full User Guide
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>
2026-03-23 19:32:17 +00:00
Vadym Samoilenko
6c93915768 Add custom CSV export template (per-client, per-user, global)
- Backend: export template system with priority chain:
  client template > user template > global template > built-in default
- New /api/export/template endpoints for any logged-in user (GET/POST/DELETE)
- Admin endpoints for global and per-client export templates
- detect_csv_template() auto-maps CSV headers to internal fields
- Frontend: ExportTemplateEditor component (upload CSV → map columns → save)
- AdminClientsPage: export template section per client card
- SheetPage: ⚙ button next to "Export CSV" opens inline template editor

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 19:29:22 +00:00
Vadym Samoilenko
fe8ee116ca Pass EMERGENCY_TOKEN and ADMIN_EMAILS through docker-compose env
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 19:16:40 +00:00
Vadym Samoilenko
eaa518b443 Fix emergency token being rejected by axios interceptor
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>
2026-03-23 19:14:31 +00:00
Vadym Samoilenko
4f88c8b565 Show LoginPage instead of auto-redirecting to SSO
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>
2026-03-23 19:10:44 +00:00
Vadym Samoilenko
8050a6a0f6 Add emergency token login as SSO bypass
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>
2026-03-23 19:07:34 +00:00
Vadym Samoilenko
8882286146 Fix voice recording and add Excel column mapping verification
- 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>
2026-03-23 19:05:26 +00:00
Vadym Samoilenko
1b051f4d0d Add per-client category hierarchy, client management, and admin hardcoding
Backend:
- New /api/clients CRUD (create, list, delete, rename)
- dropdowns.py: _load_dropdowns(client_id) — per-client file first, global fallback
- admin.py: per-client dropdown upload/preview/delete endpoints
- ai_command.py: reads sheet's client_id, builds hierarchy from client-specific file
- sheets/manager.py: client_id stored in sheet metadata; get/set_sheet_client_id helpers
- sheets.py: create sheet accepts client_id; PATCH /{id}/client endpoint
- config_runtime.py: CLIENTS_FILE, CLIENTS_DROPDOWNS_DIR, ADMIN_EMAILS list
- user_store.py: bootstrap admin from ADMIN_EMAILS (daveporter + vadymsamoilenko)

Frontend:
- New Client type; SheetMeta gains client_id
- api/clients.ts, stores/useClientStore.ts — client CRUD
- useDropdownStore: re-fetches when client changes (no stale cache)
- SheetPage: client selector in header; fetches per-client categories
- BriefUploadPage: client selector before upload
- AdminClientsPage: create/delete clients, upload per-client .xlsx, preview before apply
- Sidebar: separate admin nav links (Users / Clients / Dropdowns)
- App.tsx: /admin/clients route

Data:
- 4 clients pre-seeded (Adidas, USTUDIO, 3M Colab, Bissell) with custom hierarchy files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 18:56:01 +00:00
Vadym Samoilenko
46ecfe2802 Fix Media dropdown source, enable auto column width
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 17:30:44 +00:00
Vadym Samoilenko
7aea651df5 Fix Category/Media dropdowns: use cells() callback for dynamic sources
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 17:28:24 +00:00
Vadym Samoilenko
5169209c89 Switch Handsontable to dark theme
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 17:11:37 +00:00
Vadym Samoilenko
076675f3f2 Fix Handsontable empty grid: always reload sheet from server, use ResizeObserver for pixel height
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 17:06:52 +00:00
Vadym Samoilenko
72799b64b9 Logout: app-only signout, no Microsoft account redirect
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>
2026-03-23 16:49:11 +00:00
Vadym Samoilenko
fb26d9ab56 Fix Azure AD token expiry — auto-refresh before every request
- 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>
2026-03-23 16:44:02 +00:00
Vadym Samoilenko
5231c8bd37 Fix AI model, Language/Country mapping, and Handsontable rendering
- 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>
2026-03-23 16:38:38 +00:00
Vadym Samoilenko
44a4fb7e06 Revert Google model to gemini-3.1-pro-preview
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>
2026-03-23 15:53:12 +00:00
Vadym Samoilenko
3d6adb2dcc Fix import empty sheet bug, update Google model, improve error messages
- 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>
2026-03-23 15:52:22 +00:00
Vadym Samoilenko
d71a044a3c Fix model config alignment and improve error messaging
- 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>
2026-03-23 15:33:54 +00:00
Vadym Samoilenko
c6025b02e3 fix: align Job type to camelCase matching backend to_dict() response
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>
2026-03-23 15:14:56 +00:00
Vadym Samoilenko
ba9af5f93c fix: align env var names between docker-compose and Python config
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>
2026-03-23 15:09:45 +00:00
Vadym Samoilenko
fc430cc10a fix: add local document extraction fallback when LLAMACLOUD_API_KEY is absent
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>
2026-03-23 15:08:23 +00:00
Vadym Samoilenko
f85d6a6b51 fix: repair brief upload and real-time job progress
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>
2026-03-23 15:05:23 +00:00
Vadym Samoilenko
45c6b2e720 design: comprehensive dark theme UI refresh (ui-ux-pro-max)
- index.css: new OLED-optimised CSS variables (rgba borders, layered
  backgrounds, --ease/--duration tokens), global cursor:pointer on
  buttons, focus-visible ring, input focus glow, .ac-card utility
- Sidebar: replace emoji icons with inline SVGs, add Dashboard/Upload
  nav links with active states, + button for new sheet, better
  sheet-item active treatment, polished context menu
- TopBar: SVG chevron back button, user avatar initials pill,
  fixed 52px height, consistent spacing
- DashboardPage: hover lift on action cards, rgba-border sheet list,
  dashed empty states, SVG icons
- SheetPage: sticky sub-header with saving indicator
- CommandBar: SVG mic + send icons, pill quick-starter chips,
  rounded-lg inputs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 14:55:20 +00:00
Vadym Samoilenko
f42a390e8b fix: improve dark theme contrast and add navigation back button
- 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>
2026-03-23 14:50:07 +00:00
Vadym Samoilenko
0b2b61ee2d fix: call fetchMe after token acquire regardless of loading state
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>
2026-03-23 14:46:43 +00:00
Vadym Samoilenko
08710e1a16 fix: verify JWT signature via JWKS and fix auth dev bypass condition
- 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>
2026-03-23 14:44:22 +00:00
Vadym Samoilenko
dc1add0b1b fix: always call fetchMe() on mount — loading:true blocked initial auth check 2026-03-23 14:37:10 +00:00
Vadym Samoilenko
15bf9d3935 feat: add full AI provider config to .env.example and docker-compose
- All OpenAI, Gemini, Anthropic model/timeout/temperature settings
- Brief extraction processing config (models, cost limits, concurrency)
- File upload and WebSocket settings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 14:30:16 +00:00
Vadym Samoilenko
16bc9d0c0d fix: run git pull as SUDO_USER to use correct SSH keys 2026-03-23 14:25:44 +00:00
Vadym Samoilenko
dad8f7573a Add deploy script, .env.example, and Apache reverse proxy config
- deploy.sh: idempotent Ubuntu deployment (git pull → docker build →
  extract frontend → copy to /var/www/html/ac-helper/ → restart container)
- .env.example: production template with APP_PORT=8100
- docker-compose.yml: port now ${APP_PORT:-8100}:8000, updated proxy
  comment to Apache VirtualHost snippet
- .gitignore: whitelist .env.example

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 14:05:33 +00:00
Vadym Samoilenko
38a550bd5f Phase 8: Polish — error boundaries, loading states, Handsontable v17 fixes
- ErrorBoundary component for top-level render error recovery
- SheetPage: sheetError + loading states before table render
- main.tsx: registerAllModules() for Handsontable v17
- index.html: Montserrat font preconnect
- App.tsx: AdminRoute + ErrorBoundary wrappers
- .gitignore: exclude *.bak files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 13:36:12 +00:00
Vadym Samoilenko
72c50b2c92 Initial commit — AC Tool unified application
Merges ac-helper (PHP Activation Calendar) and brief-extractor (Python AI)
into a single Docker app with React/TypeScript frontend.

Features:
- Brief upload → AI extraction → review → Activation Calendar import
- Handsontable v17 spreadsheet with dependent dropdowns (148 categories)
- AI natural language commands via Gemini (YOLO mode, voice input)
- Azure AD MSAL SPA PKCE authentication, user roles (user/admin)
- CSV Activation Calendar export
- Real-time WebSocket job progress
- Admin: user management, dropdown Excel upload
- Multi-stage Dockerfile, docker-compose, nginx proxy instructions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 13:24:46 +00:00