# OLIVER DeckForge — Implementation Plan

> Reference: [concept.md](concept.md) for full project concept and decisions.
> Base: [presenton-main/](presenton-main/) (fork + restructure)

---

## Phase 1: Foundation

### 1. Project Setup & Restructure

Fork Presenton, reorganize file structure, strip Electron shell, configure for web-only deployment.

**Backend restructure:**
- [ ] Copy `presenton-main/servers/fastapi/` → `backend/`
- [ ] Copy `presenton-main/servers/nextjs/` → `frontend/`
- [ ] Remove `presenton-main/electron/` (Electron shell — not needed)
- [ ] Remove Mixpanel tracking references (`MixpanelInitializer.tsx`, `mixpanel.ts`, `mixpanel-browser` dep)
- [ ] Update all internal import paths after restructure
- [ ] Create new root `docker-compose.yml` with services:
  - `web` (Next.js frontend, port 3000)
  - `api` (FastAPI backend, port 8000)
  - `worker` (ARQ worker, same image as `api`, different entrypoint)
  - `postgres` (PostgreSQL 16 with TDE-ready config)
  - `redis` (Redis 7 for job queue)
  - `nginx` (reverse proxy, port 80/443)
- [ ] Create new root `Dockerfile` (multi-stage: Python + Node)
- [ ] Create `Dockerfile.dev` for development with hot-reload
- [ ] Update `nginx.conf`: route `/api/*` → FastAPI, `/admin/*` and `/*` → Next.js
- [ ] Create `.env.example` with all required variables
- [ ] Create `Makefile` with common commands (`make dev`, `make build`, `make migrate`, `make test`)
- [ ] Verify `docker compose up` boots all services and health checks pass

**Verification:**
- `docker compose up` starts all 5 services
- `http://localhost` loads Next.js app
- `http://localhost/api/docs` loads FastAPI Swagger UI

---

### 2. Database Schema & Migrations

Set up PostgreSQL as primary database, add Alembic for migrations, create all new tables.

**Setup:**
- [ ] Add `alembic>=1.15` to `pyproject.toml`
- [ ] Run `alembic init migrations` in `backend/`
- [ ] Configure `alembic.ini` and `env.py` to use async PostgreSQL + SQLModel metadata
- [ ] Set `DATABASE_URL=postgresql+asyncpg://deckforge:deckforge@postgres:5432/deckforge` in `.env`
- [ ] Remove SQLite container DB logic (`container.db`, `get_container_session`)

**New models** (create in `backend/models/sql/`):

- [ ] `user.py` — `UserModel`
  ```
  id: UUID (PK)
  azure_oid: str (unique, from Azure AD)
  email: str (unique)
  display_name: str
  role: Enum(super_admin, client_admin, user)
  is_active: bool (default True)
  last_login_at: datetime (nullable)
  created_at: datetime
  updated_at: datetime
  ```

- [ ] `client.py` — `ClientModel`
  ```
  id: UUID (PK)
  name: str
  slug: str (unique, URL-safe)
  logo_path: str (nullable)
  retention_days: int (nullable — null = unlimited)
  review_policy: Enum(self_approve, require_reviewer) (default self_approve)
  is_active: bool (default True)
  created_at: datetime
  updated_at: datetime
  ```

- [ ] `team.py` — `TeamModel`
  ```
  id: UUID (PK)
  name: str
  client_id: UUID (FK → ClientModel, nullable for Oliver Team)
  is_default: bool (default False — True only for Oliver Team)
  created_at: datetime
  ```

- [ ] `team_membership.py` — `TeamMembershipModel`
  ```
  id: UUID (PK)
  user_id: UUID (FK → UserModel)
  team_id: UUID (FK → TeamModel)
  assigned_by: UUID (FK → UserModel, nullable)
  assigned_at: datetime
  UNIQUE(user_id, team_id)
  ```

- [ ] `brand_config.py` — `BrandConfigModel`
  ```
  id: UUID (PK)
  client_id: UUID (FK → ClientModel, unique)
  primary_colors: JSON (list of hex codes)
  secondary_colors: JSON (list of hex codes)
  fonts: JSON ({heading: str, body: str, accent: str})
  logo_paths: JSON (list of file paths)
  voice_rules: text (nullable — brand voice text rules)
  voice_examples: JSON (nullable — [{good: str, bad: str}])
  guideline_doc_path: str (nullable — uploaded brand guide PDF/DOCX)
  created_at: datetime
  updated_at: datetime
  ```

- [ ] `master_deck.py` — `MasterDeckModel`
  ```
  id: UUID (PK)
  client_id: UUID (FK → ClientModel)
  name: str
  description: str (nullable)
  original_file_path: str
  thumbnail_path: str (nullable)
  parsed_config: JSON (nullable — extracted themes, colors, fonts)
  layouts: JSON (nullable — list of {id, name, type, description, json_schema, react_code})
  parse_status: Enum(pending, processing, completed, failed)
  is_active: bool (default True)
  created_at: datetime
  updated_at: datetime
  ```

- [ ] `audit_log.py` — `AuditLogModel`
  ```
  id: UUID (PK)
  user_id: UUID (FK → UserModel, nullable — system actions)
  action: str (e.g., "presentation.create", "master_deck.upload", "user.role_change")
  resource_type: str (e.g., "presentation", "master_deck", "user")
  resource_id: UUID (nullable)
  client_id: UUID (nullable — for client-scoped actions)
  details: JSON (nullable — action-specific metadata)
  ip_address: str (nullable)
  created_at: datetime (indexed)
  ```

- [ ] `job.py` — `JobModel`
  ```
  id: UUID (PK)
  user_id: UUID (FK → UserModel)
  client_id: UUID (FK → ClientModel)
  presentation_id: UUID (FK → PresentationModel, nullable — set after creation)
  job_type: str (e.g., "generate_presentation", "parse_master_deck")
  status: Enum(queued, processing, completed, failed)
  progress: int (0-100)
  progress_message: str (nullable)
  error_message: str (nullable)
  created_at: datetime
  started_at: datetime (nullable)
  completed_at: datetime (nullable)
  ```

**Modify existing models:**

- [ ] `presentation.py` — extend `PresentationModel`:
  ```
  + owner_id: UUID (FK → UserModel)
  + client_id: UUID (FK → ClientModel)
  + master_deck_id: UUID (FK → MasterDeckModel, nullable)
  + status: Enum(draft, in_review, approved) (default draft)
  + review_comment: text (nullable)
  + source_type: Enum(brief, url, manual)
  + is_saved: bool (default False — "Save to library" flag)
  + deleted_at: datetime (nullable — soft delete for GDPR)
  ```

- [ ] `slide.py` — extend `SlideModel`:
  ```
  + deleted_at: datetime (nullable — soft delete)
  ```

**Migration:**
- [ ] Generate initial Alembic migration: `alembic revision --autogenerate -m "initial_schema"`
- [ ] Create seed script: auto-create "Oliver Team" (is_default=True) on first run
- [ ] Test: `alembic upgrade head` creates all tables in PostgreSQL

**Verification:**
- `alembic upgrade head` runs without errors
- All tables visible in PostgreSQL
- Seed script creates Oliver Team
- Existing Presenton tables (PresentationModel, SlideModel) coexist with new tables

---

### 3. Auth Module (Azure AD SSO)

Implement Microsoft Azure AD OAuth 2.0 login, JWT session management, and auth middleware.

**Dependencies:** `pip install msal>=1.31 python-jose[cryptography]>=3.3 httpx>=0.28`

**Backend:**

- [ ] Create `backend/services/auth_service.py`:
  - `AuthService` class with:
    - `get_authorization_url()` → returns Azure AD OAuth URL with redirect_uri
    - `exchange_code_for_token(code: str)` → exchanges auth code for access/id token via MSAL
    - `validate_token(token: str)` → validates JWT, returns claims (oid, email, name)
    - `get_or_create_user(claims: dict)` → find by `azure_oid` or create new `UserModel` (role=user by default)
    - `create_session_jwt(user: UserModel)` → sign app-level JWT with user_id, role, expiry (24h)
    - `refresh_session(token: str)` → refresh if near expiry
  - Auto-add new users to Oliver Team (default team)

- [ ] Create `backend/api/v1/auth/router.py`:
  - `GET /api/v1/auth/login` → redirect to Azure AD authorize URL
  - `GET /api/v1/auth/callback` → exchange code, create/update user, set JWT cookie, redirect to dashboard
  - `GET /api/v1/auth/me` → return current user (id, email, name, role, teams, clients)
  - `POST /api/v1/auth/logout` → clear JWT cookie
  - Register router in `api/v1/ppt/router.py` (or new `api/v1/router.py`)

- [ ] Create `backend/api/middlewares/auth_middleware.py`:
  - `AuthMiddleware`: extract JWT from cookie/header → validate → attach `request.state.user` (UserModel)
  - Skip auth for: `/api/v1/auth/*`, `/api/docs`, `/api/openapi.json`, `/api/health`
  - Return 401 for invalid/missing token

- [ ] Create `backend/utils/auth_dependencies.py`:
  - `get_current_user(request) → UserModel` (FastAPI Depends)
  - `require_role(role: str)` → dependency that checks `request.state.user.role`
  - `require_super_admin` → shortcut dependency
  - `require_client_admin` → shortcut dependency

- [ ] Add env vars to `.env.example`:
  ```
  AZURE_AD_TENANT_ID=
  AZURE_AD_CLIENT_ID=
  AZURE_AD_CLIENT_SECRET=
  AZURE_AD_REDIRECT_URI=http://localhost/api/v1/auth/callback
  JWT_SECRET_KEY=<random-256-bit-key>
  ```

**Frontend:**

- [ ] Create `frontend/store/slices/authSlice.ts`:
  - State: `{user: User | null, isLoading: bool, isAuthenticated: bool}`
  - Actions: `fetchCurrentUser()` (calls `/api/v1/auth/me`), `logout()`
  - Types: `User {id, email, displayName, role, teams: Team[], clients: Client[]}`

- [ ] Create `frontend/components/AuthGuard.tsx`:
  - Wraps app; redirects to `/api/v1/auth/login` if not authenticated
  - Shows loading state while checking auth

- [ ] Create `frontend/app/login/page.tsx`:
  - Simple login page with "Sign in with Microsoft" button
  - Button redirects to `/api/v1/auth/login`

- [ ] Modify `frontend/app/layout.tsx`:
  - Wrap app with `AuthGuard` and Redux Provider
  - Call `fetchCurrentUser()` on mount

**Verification:**
- Click "Sign in with Microsoft" → Azure AD login → redirect back → user created in DB
- `/api/v1/auth/me` returns user with role and teams
- Unauthenticated requests to `/api/v1/ppt/*` return 401
- New user auto-added to Oliver Team

---

### 4. RBAC & Team Management

Role-based access control middleware and team/client access enforcement.

**Backend:**

- [ ] Create `backend/api/middlewares/rbac_middleware.py`:
  - `check_client_access(user: UserModel, client_id: UUID)` → verify user belongs to a team for this client (or is super_admin)
  - `check_team_admin(user: UserModel, client_id: UUID)` → verify user is client_admin for this client or super_admin
  - Both raise `HTTPException(403)` on failure

- [ ] Create `backend/api/v1/admin/users_router.py` (Super Admin only):
  - `GET /api/v1/admin/users` → list all users with their roles and team memberships
  - `GET /api/v1/admin/users/{id}` → user detail
  - `PUT /api/v1/admin/users/{id}/role` → change role (super_admin, client_admin, user)
  - `DELETE /api/v1/admin/users/{id}` → deactivate user (soft: is_active=False)
  - All endpoints protected with `require_super_admin` dependency

- [ ] Create `backend/api/v1/admin/teams_router.py`:
  - `POST /api/v1/admin/teams` → create team for a client (Client Admin for that client, or Super Admin)
  - `GET /api/v1/admin/teams` → list teams (filtered by user's accessible clients)
  - `GET /api/v1/admin/teams/{id}` → team detail with members
  - `POST /api/v1/admin/teams/{id}/members` → add user to team (body: `{user_id}`)
  - `DELETE /api/v1/admin/teams/{id}/members/{user_id}` → remove user from team
    - Enforce: transfer ownership of user's presentations in this client before removal
  - `DELETE /api/v1/admin/teams/{id}` → delete team (Super Admin only, cannot delete Oliver Team)

- [ ] Create `backend/api/v1/admin/clients_router.py`:
  - `POST /api/v1/admin/clients` → create client (Super Admin only)
  - `GET /api/v1/admin/clients` → list clients (filtered: Super Admin sees all, others see assigned)
  - `GET /api/v1/admin/clients/{id}` → client detail
  - `PUT /api/v1/admin/clients/{id}` → update client (Client Admin or Super Admin)
  - `DELETE /api/v1/admin/clients/{id}` → deactivate client (Super Admin only, soft delete)
  - When creating client: auto-create a team with same name

- [ ] Create `backend/services/access_service.py`:
  - `get_accessible_client_ids(user: UserModel) → list[UUID]` — returns client IDs user can access via team membership
  - `get_accessible_clients(user: UserModel) → list[ClientModel]` — returns full client objects
  - Used throughout the app for filtering queries

- [ ] Modify all existing presentation endpoints (`backend/api/v1/ppt/endpoints/presentation.py`):
  - Add `user: UserModel = Depends(get_current_user)` to all handlers
  - Filter `get_all_presentations` by user's accessible clients
  - Check `check_client_access(user, client_id)` on create/generate
  - Check ownership or client access on get/update/delete

**Verification:**
- Super Admin can see all clients, users, teams
- Client Admin can manage only assigned clients and their teams
- User can only see presentations for their assigned clients
- Removing user from team requires ownership transfer of their decks
- Oliver Team cannot be deleted
- Users auto-join Oliver Team on creation

---

### 5. Audit Logging

Full audit trail for GDPR compliance with CSV/JSON export.

**Backend:**

- [ ] Create `backend/services/audit_service.py`:
  - `AuditService` class:
    - `log(user_id, action, resource_type, resource_id, client_id, details, ip_address)` → insert `AuditLogModel`
    - Use background task (not blocking request) via `asyncio.create_task()`
    - `query(filters: AuditLogFilter) → list[AuditLogModel]` — paginated, filterable by date range, user, action, client
    - `export_csv(filters) → StreamingResponse` — CSV export
    - `export_json(filters) → StreamingResponse` — JSON export

- [ ] Create `backend/api/middlewares/audit_middleware.py`:
  - Auto-log middleware for mutating requests (POST, PUT, DELETE)
  - Captures: user_id (from auth), action (from endpoint name), IP (from request)
  - Non-blocking: fires and forgets the log insert

- [ ] Create `backend/api/v1/admin/audit_router.py`:
  - `GET /api/v1/admin/audit-log` → paginated audit log (Super Admin: all, Client Admin: own clients)
  - `GET /api/v1/admin/audit-log/export?format=csv|json` → download filtered audit log
  - Filters: `date_from`, `date_to`, `user_id`, `action`, `client_id`, `resource_type`

- [ ] Add audit calls to critical actions:
  - Auth: login, logout, role change
  - Clients: create, update, delete
  - Teams: create, add/remove member
  - Master decks: upload, parse, delete
  - Presentations: create, generate, export, status change, delete
  - Brand config: create, update

**Verification:**
- Generate a presentation → audit log shows entries for each step
- Export audit log as CSV → opens in Excel with correct columns
- Client Admin can only see logs for their clients
- Super Admin sees all logs

---

### 6. i18n Setup

Configure react-i18next for English MVP with structure ready for future languages.

**Frontend:**

- [ ] Install: `npm install react-i18next i18next i18next-http-backend i18next-browser-languagedetector`
- [ ] Create `frontend/i18n/` directory:
  - `i18n.ts` — i18next initialization config
  - `locales/en/common.json` — common strings (buttons, labels, errors)
  - `locales/en/auth.json` — auth-related strings
  - `locales/en/admin.json` — admin panel strings
  - `locales/en/wizard.json` — wizard step strings
  - `locales/en/editor.json` — slide editor strings
- [ ] Create `frontend/i18n/I18nProvider.tsx` — wraps app with I18nextProvider
- [ ] Update `frontend/app/layout.tsx` — add I18nProvider
- [ ] Replace hardcoded strings in existing Presenton components with `t()` calls (incremental — do this as we touch each component)

**Verification:**
- App loads in English
- All new UI strings use `t('key')` pattern
- Adding a new locale JSON file makes it available

---

## Phase 2: Admin Panel & Client Management

### 7. Admin Panel Frontend Shell

Create the admin section layout, navigation, and page structure.

**Frontend:**

- [ ] Create `frontend/app/admin/layout.tsx`:
  - Sidebar navigation with sections:
    - Users & Roles (Super Admin only)
    - Clients (filtered by role)
    - Teams (filtered by role)
    - Master Decks (filtered by role)
    - Brand Config (filtered by role)
    - Audit Log (Super Admin: full, Client Admin: own clients)
    - Analytics (Super Admin: full, Client Admin: own clients)
    - System Settings (Super Admin only: LLM config, image provider config)
  - Role-based sidebar item visibility
  - Breadcrumb navigation
  - Responsive layout (desktop only, 1280px+)

- [ ] Create admin pages (skeleton — data integration in subsequent steps):
  - `frontend/app/admin/page.tsx` → dashboard/overview redirect
  - `frontend/app/admin/users/page.tsx` → user list + role management
  - `frontend/app/admin/users/[id]/page.tsx` → user detail + team assignments
  - `frontend/app/admin/clients/page.tsx` → client list
  - `frontend/app/admin/clients/[id]/page.tsx` → client detail (tabs: info, teams, master decks, brand)
  - `frontend/app/admin/clients/[id]/teams/page.tsx` → team management for client
  - `frontend/app/admin/clients/[id]/master-decks/page.tsx` → master deck list for client
  - `frontend/app/admin/clients/[id]/brand/page.tsx` → brand config editor
  - `frontend/app/admin/audit/page.tsx` → audit log viewer with filters + export
  - `frontend/app/admin/analytics/page.tsx` → analytics dashboard
  - `frontend/app/admin/settings/page.tsx` → system settings (LLM, image providers)

- [ ] Create `frontend/store/slices/adminSlice.ts`:
  - State for users list, clients list, teams list
  - CRUD async thunks calling admin API endpoints

- [ ] Create admin-specific components:
  - `frontend/app/admin/components/AdminSidebar.tsx`
  - `frontend/app/admin/components/UserTable.tsx`
  - `frontend/app/admin/components/ClientCard.tsx`
  - `frontend/app/admin/components/RoleBadge.tsx`
  - `frontend/app/admin/components/AuditLogTable.tsx`
  - `frontend/app/admin/components/DataExportButton.tsx` (CSV/JSON)

**Verification:**
- `/admin` accessible only to Super Admin and Client Admin roles
- User role sees no admin link in navigation
- Sidebar items filtered by role
- All pages render skeleton/empty states

---

### 8. Client & Brand Management

Full CRUD for clients, brand configuration, and frontend integration.

**Backend:**
- [ ] Implement `clients_router.py` endpoints with full validation (from step 4)
- [ ] Create `backend/api/v1/admin/brand_config_router.py`:
  - `GET /api/v1/admin/clients/{client_id}/brand` → get brand config
  - `PUT /api/v1/admin/clients/{client_id}/brand` → update brand config (colors, fonts, voice rules, voice examples)
  - `POST /api/v1/admin/clients/{client_id}/brand/logo` → upload logo file
  - `POST /api/v1/admin/clients/{client_id}/brand/guideline` → upload brand guide document (PDF/DOCX)
  - `DELETE /api/v1/admin/clients/{client_id}/brand/logo/{index}` → remove logo
  - All protected with `check_team_admin(user, client_id)`

**Frontend:**
- [ ] Wire `frontend/app/admin/clients/page.tsx`:
  - Client list as cards/table with name, logo, team count, deck count, status
  - "New Client" button (Super Admin only)
  - Client create/edit modal with name, slug, retention policy, review policy

- [ ] Wire `frontend/app/admin/clients/[id]/page.tsx`:
  - Tabs: Overview | Teams | Master Decks | Brand Config
  - Overview: client info, quick stats

- [ ] Wire `frontend/app/admin/clients/[id]/brand/page.tsx`:
  - Color pickers for primary/secondary palettes
  - Font selectors (heading, body, accent)
  - Logo upload area (drag & drop, multiple logos)
  - Voice rules textarea
  - Voice examples table: good example ↔ bad example pairs
  - Brand guideline document upload (PDF/DOCX)
  - Preview panel showing how brand config looks applied to a sample slide

**Verification:**
- Create client "Nike" → team auto-created → appears in client list
- Upload Nike logo → appears in brand config
- Set brand colors/fonts → saved to DB
- Upload brand guideline PDF → file stored, path in DB
- Client Admin for Nike can edit Nike brand, cannot see Adidas

---

### 9. Master Deck Upload & Parser

Upload client PPTX master decks, auto-detect layout types, admin review.

**Backend:**

- [ ] Create `backend/api/v1/admin/master_decks_router.py`:
  - `POST /api/v1/admin/clients/{client_id}/master-decks` → upload PPTX file
    - Save file to `data/clients/{client_id}/master_decks/{deck_id}/original.pptx`
    - Create `MasterDeckModel` with `parse_status=pending`
    - Enqueue parse job to Redis
    - Return deck_id + status
  - `GET /api/v1/admin/clients/{client_id}/master-decks` → list master decks
  - `GET /api/v1/admin/master-decks/{id}` → deck detail with parsed layouts
  - `PUT /api/v1/admin/master-decks/{id}` → update name, description, active status
  - `PUT /api/v1/admin/master-decks/{id}/layouts/{layout_index}` → edit parsed layout (admin review: rename, recategorize type, edit react code)
  - `DELETE /api/v1/admin/master-decks/{id}` → soft delete

- [ ] Create `backend/services/master_deck_parser_service.py`:
  - `MasterDeckParserService` class:
    - `parse(deck_id: UUID)` → main parse pipeline:
      1. Open PPTX with `python-pptx`
      2. Enumerate `prs.slide_masters[0].slide_layouts` → extract each layout
      3. For each layout:
         - Extract placeholder types and positions
         - Render screenshot via Puppeteer/LibreOffice (reuse existing `pptx_slides` logic)
         - Extract OXML
      4. Extract theme: `a:clrScheme` → color palette, `a:fontScheme` → font config
      5. Extract logos/images from master slide backgrounds
      6. For each layout screenshot + OXML:
         - Call existing `slide_to_html` pipeline (vision LLM converts screenshot + OXML → HTML)
         - Call existing `html_to_react` pipeline (LLM converts HTML → React + Zod schema)
         - Auto-classify layout type (title, content, chart, comparison, quote, table, metrics, timeline, image) based on placeholder analysis + LLM classification
      7. Store results in `MasterDeckModel.parsed_config` and `layouts` JSON
      8. Update `parse_status=completed`
    - `get_layout_for_content_type(deck_id, content_type) → layout` — find best matching layout

  - Reuse from Presenton:
    - `backend/api/v1/ppt/endpoints/pptx_slides.py` — PPTX parsing logic
    - `backend/api/v1/ppt/endpoints/slide_to_html.py` — vision LLM slide-to-HTML
    - `backend/api/v1/ppt/endpoints/html_to_react.py` — HTML-to-React conversion

**Frontend:**

- [ ] Wire `frontend/app/admin/clients/[id]/master-decks/page.tsx`:
  - Upload button (drag & drop PPTX)
  - List of uploaded master decks with parse status badge (pending/processing/completed/failed)
  - Click deck → expand to show parsed layouts as thumbnail grid
  - Each layout card shows: thumbnail, auto-detected type, name
  - Click layout → edit modal: rename, change type category, edit/regenerate React code
  - "Reparse" button to re-run parser

**Verification:**
- Upload a client PPTX → parse job queued → status changes to processing → completed
- Parsed layouts visible as thumbnails with auto-detected types
- Admin can rename layouts and recategorize types
- Bad PPTX (no slide masters) → system still extracts patterns from content slides
- Layout React code renders correctly in preview

---

## Phase 3: Content Pipeline

### 10. File Upload & Document Parsing

Enhance document upload for briefs + attachments with parsing.

**Backend:**

- [ ] Modify `backend/api/v1/ppt/endpoints/files.py`:
  - Extend upload endpoint to accept multiple files
  - Classify uploads: brief (DOCX primary), attachments (Excel/CSV/images/PDF), URL reference
  - Store files at `data/presentations/{presentation_id}/uploads/`
  - Return parsed file info for each uploaded file

- [ ] Create `backend/services/attachment_parser_service.py`:
  - `parse_excel(file_path) → list[TableData]` — extract sheets → structured table data (via `openpyxl`)
  - `parse_csv(file_path) → TableData` — parse CSV into structured table data
  - `extract_images(file_path) → list[ImageInfo]` — extract images from any doc format
  - `parse_pdf_text(file_path) → str` — extract text from PDF (via `pdfplumber`, already a dep)
  - Output model: `TableData {headers: list[str], rows: list[list[Any]], title: str | None}`

- [ ] Add deps: `pip install openpyxl>=3.1`

- [ ] Modify `backend/services/documents_loader.py`:
  - Add `load_url(url: str) → str` (secondary input):
    - Fetch URL via `aiohttp` (already a dep)
    - Extract article via `trafilatura` or `readability-lxml`
    - Return markdown
  - Add deps: `pip install trafilatura>=2.0`

**Verification:**
- Upload DOCX brief → parsed to markdown, text extracted
- Upload Excel → sheets extracted as structured table data
- Upload CSV → rows/columns extracted
- Upload images (PNG/JPG) → stored, metadata extracted
- Upload PDF → text extracted
- Paste URL → article content extracted as markdown

---

### 11. Content Intelligence Service

NLP-powered classification and extraction of content blocks.

**Backend:**

- [ ] Create `backend/models/content_models.py`:
  ```python
  class ContentBlockType(str, Enum):
      narrative = "narrative"
      quote = "quote"
      metric = "metric"
      table = "table"
      timeline = "timeline"
      comparison = "comparison"
      list_items = "list_items"
      image_reference = "image_reference"
      call_to_action = "call_to_action"

  class ContentBlock(BaseModel):
      type: ContentBlockType
      raw_text: str
      extracted_data: dict | None  # e.g., {value: "2M", label: "Impressions"} for metrics
      source_section: str | None  # heading from brief
      priority: int  # 1-10

  class ClassifiedContent(BaseModel):
      title: str | None
      blocks: list[ContentBlock]
      tables: list[TableData]
      images: list[ImageInfo]
      summary: str  # brief overall summary
  ```

- [ ] Create `backend/services/content_intelligence_service.py`:
  - `ContentIntelligenceService` class:
    - `classify(markdown: str, attachments: list) → ClassifiedContent`:
      1. Split markdown into chunks using `ScoreBasedChunker` (existing)
      2. For each chunk, classify content type:
         - Rule-based pass first:
           - Regex for numbers/percentages/currency → `metric`
           - Regex for quoted text with attribution → `quote`
           - Markdown table patterns → `table`
           - Year sequences or date patterns → `timeline`
           - "vs", "compared to", "versus" → `comparison`
           - Bullet lists → `list_items`
           - Image references (URLs, "see figure") → `image_reference`
         - LLM-based classification for ambiguous blocks (batch multiple chunks in one call)
      3. Merge attachment data: Excel tables → `table` blocks, images → `image_reference` blocks
      4. Extract numeric data from metric blocks: `{value, label, unit, context}`
      5. Rank blocks by priority (title > metrics > quotes > narrative)
      6. Return `ClassifiedContent`

    - `ask_followup_questions(content: ClassifiedContent) → list[str] | None`:
      - If total content is too short (< 200 words, < 3 blocks): generate clarifying questions
      - LLM call: "Given this brief content, what additional information would be needed for a presentation?"
      - Return list of questions or None if content is sufficient

**Verification:**
- Brief with "Revenue grew 45% to $2.3M" → classified as `metric` with `{value: "45%", label: "Revenue growth"}`
- Table in markdown → classified as `table` with extracted data
- Quoted text with speaker → classified as `quote`
- Short 2-sentence brief → returns follow-up questions
- Excel attachment → tables merged into content blocks

---

### 12. Slide Mapping Engine

Rules engine that maps classified content to master deck layout types.

**Backend:**

- [ ] Create `backend/services/slide_mapping_engine.py`:
  - `SlideMappingEngine` class:
    - `map(classified_content: ClassifiedContent, master_deck: MasterDeckModel, n_slides: int, instructions: str | None) → list[SlideMapping]`:
      1. Determine required slide types based on content:
         - Always: Title slide (1st)
         - If >5 content blocks: Agenda/TOC slide (2nd)
         - Per section heading in brief: Section divider slide
         - Per content block: map type to layout:
           - `metric` → metrics/KPI layout
           - `quote` → quote layout
           - `table` → table layout or chart layout (user will choose later)
           - `comparison` → dual-column/comparison layout
           - `timeline` → timeline layout
           - `list_items` → bullet list layout
           - `narrative` → content/description layout
           - `image_reference` → image-focused layout
      2. Match content types to available layouts in master deck (by layout `type` field)
      3. If no matching layout: fall back to generic content layout
      4. Respect `n_slides` constraint: merge/drop low-priority blocks if too many, split high-content blocks if too few
      5. Use LLM for ambiguous mappings (existing `generate_presentation_structure` pattern)
      6. Return ordered list of `SlideMapping`:
         ```python
         class SlideMapping(BaseModel):
             content_block_index: int | None
             layout_id: str
             layout_name: str
             slide_type: str
             content_summary: str
             attachment_ids: list[str]  # mapped attachments
         ```

**Verification:**
- Brief with 3 metrics, 1 quote, 2 narrative sections, 1 table → correct layout assignments
- Requesting 8 slides from 15 content blocks → low-priority blocks merged/dropped
- Missing layout type in master deck → falls back to generic layout
- Attachments (Excel) assigned to correct table/chart slides

---

### 13. Chart Data Extraction & Native PPTX Charts

Extract numeric data → chart-ready structures. Render as native editable PPTX charts.

**Backend:**

- [ ] Create `backend/services/chart_data_extractor.py`:
  - `ChartDataExtractor` class:
    - `extract(content_block: ContentBlock, table_data: TableData | None) → ChartData | None`:
      1. If `table_data` provided: convert directly to `ChartData`
      2. If `content_block.type == metric`: try to extract numeric series from text
      3. Use LLM for complex extraction: "Convert this data to chart format"
      4. Recommend chart type based on data shape:
         - Single category with values → bar chart
         - Time series → line chart
         - Parts of whole (percentages summing to ~100%) → pie/donut chart
         - Two series comparison → grouped bar
         - Project phases with dates → Gantt (shape-based)
         - Sequential increases/decreases → waterfall (shape-based)
      5. Return `ChartData`:
         ```python
         class ChartData(BaseModel):
             chart_type: Enum(bar, column, line, pie, doughnut, area, scatter, bubble, gantt, waterfall)
             title: str
             categories: list[str]  # X-axis labels
             series: list[ChartSeries]  # {name, values: list[float]}
             unit: str | None  # e.g., "$", "%", "units"
         ```

- [ ] Create `backend/services/native_chart_service.py`:
  - `NativeChartService` class (extends `PptxPresentationCreator`):
    - `add_native_chart(slide, chart_data: ChartData, position, size)`:
      - For bar/column/line/pie/doughnut/area/scatter/bubble:
        - Use `python-pptx` chart API directly: `slide.shapes.add_chart()`
        - Set chart data from `ChartData.series`
        - Apply brand colors from client config
        - Set fonts from brand config
      - For Gantt:
        - Build from shapes: one `add_shape()` per task bar (rectangle)
        - Add text labels, date markers, connectors
        - Apply brand colors
      - For Waterfall:
        - Build from shapes: stacked rectangles with invisible bases
        - Add value labels, category labels, connector lines
        - Color-code: green for increases, red for decreases, blue for totals

- [ ] Modify `backend/services/pptx_presentation_creator.py`:
  - Import and use `NativeChartService` for chart shapes
  - Replace any image-based chart rendering with native chart calls

**Frontend:**

- [ ] Create `frontend/app/(presentation-generator)/components/ChartDataEditor.tsx`:
  - Spreadsheet-like grid (using simple HTML table with contentEditable cells, or a lightweight lib)
  - Columns: categories + series names
  - Rows: data values
  - Add/remove rows and series
  - Chart type selector dropdown
  - Live Recharts preview next to the data editor
  - "Apply" button saves changes to Redux store and triggers slide re-render

**Verification:**
- Table with sales data → extracted as bar chart → native editable chart in PPTX
- Open PPTX in PowerPoint → right-click chart → "Edit Data" works
- Gantt chart renders as editable shapes with task bars
- Waterfall chart shows increases (green) and decreases (red) as shapes
- Chart data editor: change a value → preview updates live → export reflects change
- Brand colors applied to all chart elements

---

## Phase 4: Generation Pipeline

### 14. Brand Enforcement Service

Apply client brand rules to generated content and PPTX output.

**Backend:**

- [ ] Create `backend/services/brand_enforcement_service.py`:
  - `BrandEnforcementService` class:
    - `get_brand_context_for_llm(client_id: UUID) → str`:
      - Load `BrandConfigModel` for client
      - Build a prompt snippet with voice rules, examples, tone guidelines
      - Used to inject into LLM system prompts during content generation
    - `enforce_on_pptx_model(model: PptxPresentationModel, brand: BrandConfigModel) → PptxPresentationModel`:
      - Walk all slides → all shapes → replace:
        - Font families → brand fonts (heading font for titles, body font for content)
        - Colors → brand palette (map template colors to brand primary/secondary)
        - Add logo shape to designated position on each slide (configurable: top-left, bottom-right, etc.)
      - Contrast check: ensure text color has sufficient contrast against background
    - `enforce_on_native_charts(slide, brand: BrandConfigModel)`:
      - Set chart colors to brand palette
      - Set chart fonts to brand fonts

**Verification:**
- Generate deck for Nike → all fonts are Nike brand fonts
- All chart bars use Nike brand colors
- Nike logo appears on every slide in the configured position
- Text contrast passes WCAG AA check

---

### 15. Outline Generation (Modified)

Enhance existing outline generation with brand context and content intelligence.

**Backend:**

- [ ] Modify `backend/utils/llm_calls/generate_presentation_outlines.py`:
  - Extend `get_system_prompt()` to include:
    - Brand voice guidelines (from `BrandEnforcementService.get_brand_context_for_llm()`)
    - Available layout types from master deck (names + descriptions)
    - Content classification summary (types and counts of content blocks)
    - **Critical instruction**: "You are restructuring and condensing existing content from a brief. Do NOT invent facts, statistics, or claims. Every data point must originate from the source material."
  - Extend user prompt to include:
    - User's custom instructions (free-text from Step 2)
    - Classified content blocks with their types
    - Attachment summaries (what Excel/CSV data is available)

- [ ] Modify `backend/utils/llm_calls/generate_slide_content.py`:
  - Same brand context injection
  - Same "strictly extract, don't invent" instruction
  - For chart-type slides: include chart data schema so LLM populates data correctly
  - For slides with mapped attachments: include attachment data as context

- [ ] Implement LLM auto-fallback in `backend/services/llm_client.py`:
  - Wrap all LLM calls in try/except
  - On provider failure (timeout, rate limit, API error):
    - Log the failure
    - Switch to fallback provider (Claude → OpenAI → Gemini)
    - Retry the call
    - Log which provider was used

**Verification:**
- Generate outline from brief → outline reflects source content accurately
- No hallucinated facts in outline
- Brand voice guidelines influence tone of generated text
- Disable primary LLM → auto-fallback to secondary provider → generation succeeds
- User instructions ("focus on ROI") reflected in outline emphasis

---

### 16. Job Queue & Async Generation

Redis-based job queue with ARQ for reliable async presentation generation.

**Backend:**

- [ ] Add dep: `pip install arq>=0.26`

- [ ] Create `backend/services/job_queue_service.py`:
  - `JobQueueService` class:
    - `enqueue(job_type: str, payload: dict) → UUID` — push job to ARQ Redis queue, create `JobModel` in DB
    - `get_status(job_id: UUID) → JobModel` — poll job status
    - `cancel(job_id: UUID)` — cancel queued job

- [ ] Create `backend/workers/main.py`:
  - ARQ worker entry point
  - Register task functions:
    - `generate_presentation_task(ctx, job_id, ...)`
    - `parse_master_deck_task(ctx, job_id, ...)`
  - Configure: max_jobs=5, job_timeout=600s, retry=3 with exponential backoff

- [ ] Create `backend/workers/presentation_worker.py`:
  - `generate_presentation_task(ctx, job_id: UUID)`:
    1. Load job from DB, update status → `processing`
    2. Load brief content + attachments
    3. Run `ContentIntelligenceService.classify()`
    4. Run `SlideMappingEngine.map()`
    5. Run outline generation (LLM)
    6. Run slide content generation (LLM, batched 10 at a time)
    7. Run asset generation (images, icons — parallel)
    8. Run chart data extraction + native chart prep
    9. Run brand enforcement
    10. Save slides to DB
    11. Update job status → `completed`, progress → 100
    - At each step: update `JobModel.progress` and `progress_message`
    - On failure: update status → `failed`, save error_message
    - SSE: push progress events via Redis pub/sub (existing SSE pattern)

- [ ] Create `backend/workers/master_deck_worker.py`:
  - `parse_master_deck_task(ctx, job_id: UUID)`:
    - Load master deck from DB
    - Run `MasterDeckParserService.parse()`
    - Update parse_status on completion/failure

- [ ] Modify `docker-compose.yml`:
  - Add `worker` service: same image as `api`, entrypoint `python -m arq backend.workers.main.WorkerSettings`

- [ ] Create `backend/api/v1/ppt/endpoints/jobs.py`:
  - `GET /api/v1/ppt/jobs/{id}` → job status + progress
  - `GET /api/v1/ppt/jobs/{id}/stream` → SSE stream of job progress events
  - `DELETE /api/v1/ppt/jobs/{id}` → cancel job

**Verification:**
- Start generation → job queued → worker picks up → progress updates visible
- Kill worker mid-job → job stays in `processing` → worker restart picks up retry
- 3 concurrent users generate → jobs processed via queue without conflicts
- Master deck parse runs as background job → status updates in admin UI

---

## Phase 5: Frontend Wizard & Editor

### 17. Wizard Flow — Steps 1-2 (Upload & Configure)

**Frontend:**

- [ ] Create `frontend/app/(presentation-generator)/generate/layout.tsx`:
  - Wizard layout with step indicator bar (5 steps)
  - Step labels: Upload → Configure → Outline → Generate → Edit
  - Current step highlighted, completed steps checkmarked
  - Each step is a nested route

- [ ] Create `frontend/app/(presentation-generator)/generate/upload/page.tsx` (Step 1):
  - Large drag & drop zone for primary brief (DOCX)
  - "Or browse files" button fallback
  - Section for additional attachments (Excel, CSV, images, PDF)
  - Each uploaded file shows: name, size, type badge, remove button
  - Optional: URL input field with "Add reference URL" button
  - Parsed content preview (shows extracted text/tables for each file)
  - "Next" button → navigates to Step 2
  - Auto-save: uploads saved to `JobModel` / session state

- [ ] Create `frontend/app/(presentation-generator)/generate/configure/page.tsx` (Step 2):
  - Client selector dropdown (only assigned clients from `authSlice`)
  - Master deck selector (filtered by selected client, shows thumbnails)
  - Slide count slider (range: 5-40, default: smart suggestion based on content length)
  - Free-text instructions textarea ("Focus on...", "Skip...", "Emphasize...")
  - Tone selector (if not locked by brand config): professional / casual / educational
  - "Generate Outline" button → triggers outline generation → navigates to Step 3
  - "Back" button → returns to Step 1

- [ ] Create `frontend/store/slices/wizardSlice.ts`:
  - State: `{currentStep, uploadedFiles, selectedClientId, selectedDeckId, slideCount, instructions, tone, outlines, jobId, presentationId}`
  - Persist to localStorage for auto-save (user can close browser and return)
  - Actions: setStep, setFiles, setClient, setDeck, etc.

**Verification:**
- Drag DOCX → file appears with metadata → proceed to Step 2
- Select client → master decks filter → select deck → slide count slider works
- Close browser on Step 2 → reopen → state restored
- Back button works between steps

---

### 18. Wizard Flow — Step 3 (Outline Review)

**Frontend:**

- [ ] Create `frontend/app/(presentation-generator)/generate/outline/page.tsx` (Step 3):
  - **Split view (side-by-side)**:
    - LEFT panel: parsed source content (brief markdown rendered as formatted text)
      - Collapsible sections per heading
      - Highlight which content block maps to which outline item (linked highlighting)
    - RIGHT panel: generated outline
      - Each outline item as a card: slide number, title, description, layout type badge
      - Drag & drop reordering (using existing `@dnd-kit` dependency)
      - Inline edit: click title/description to edit
      - "Add Slide" button → insert new outline item
      - "Delete" button per slide → remove with confirmation
      - Layout type dropdown per slide (from master deck available layouts)
  - **Attachment mapping panel** (bottom or collapsible):
    - List of uploaded attachments (Excel tables, images, etc.)
    - Drag attachment onto a specific outline item → maps it to that slide
    - Show which attachments are assigned and which are unassigned
  - **Mandatory approval**: "Approve Outline & Generate" button (disabled until user has reviewed)
  - "Back" button → return to Step 2

**Verification:**
- Outline shows on right, source on left
- Drag slide 3 above slide 2 → order changes
- Edit title inline → saved
- Add new slide → appears at bottom
- Delete slide → removed from outline
- Map Excel attachment to slide 5 → indicator shows
- Click "Approve & Generate" → generation starts → navigate to Step 4

---

### 19. Wizard Flow — Step 4 (Generation Progress)

**Frontend:**

- [ ] Create `frontend/app/(presentation-generator)/generate/progress/page.tsx` (Step 4):
  - Progress bar showing overall completion (0-100%)
  - Status message text (e.g., "Generating slide 5 of 15...")
  - Live slide preview grid:
    - As each slide completes, its thumbnail appears in the grid
    - Slides appear one by one with a fade-in animation
    - Click thumbnail → shows larger preview in a modal
  - SSE connection to `/api/v1/ppt/jobs/{id}/stream` for real-time updates
  - On completion → auto-navigate to Step 5 (`/presentation/{id}`)
  - On failure → show error message with "Retry" button
  - "Cancel" button → cancels job, returns to Step 3

**Verification:**
- Generation starts → progress bar moves → slides appear one by one
- Close browser → reopen → progress page shows current state (polling fallback)
- Generation fails at slide 8 → error shown → "Retry" re-queues from slide 8
- Cancel → job cancelled → return to outline

---

### 20. Wizard Flow — Step 5 / Slide Editor

Enhance Presenton's existing slide editor with new capabilities.

**Frontend:**

- [ ] Modify `frontend/app/(presentation-generator)/presentation/page.tsx`:
  - Integrate with wizard flow (accessible as Step 5 and as standalone for saved presentations)
  - Add toolbar with:
    - Review status indicator (Draft / In Review / Approved)
    - "Save to Library" button
    - Export dropdown: ".pptx" and "PDF"
    - "Download" button

- [ ] Add per-slide regeneration:
  - Right-click slide or "..." menu → "Regenerate this slide"
  - Opens modal with:
    - Current slide content preview
    - Text input: "Instructions for regeneration" (e.g., "Make more concise", "Add a chart")
    - "Regenerate" button → calls backend → replaces slide content
    - "Cancel" button

- [ ] Add per-image choice:
  - Click image on slide → popover shows:
    - Current image (from source or AI-generated)
    - "Source image" tab (if brief had an image for this section)
    - "Stock images" tab (search Pexels/Pixabay)
    - "AI generate" tab (enter prompt, generate new image)
    - "Upload" tab (upload custom image)
  - Select image → replaces in slide

- [ ] Add per-table/chart choice:
  - Click table → context menu:
    - "Keep as table" (render as formatted table)
    - "Convert to chart" → opens chart type picker + chart data editor
  - Click chart → context menu:
    - "Edit data" → opens `ChartDataEditor`
    - "Change chart type" → type picker dropdown
    - "Convert to table" → renders data as table instead

- [ ] Integrate `ChartDataEditor.tsx` (from step 13):
  - Opens as modal/panel when editing chart data
  - Changes reflect live in slide preview

**Verification:**
- Regenerate slide 5 with "make it shorter" → new content generated, slide updates
- Click image → swap with stock photo from Pexels → slide updates
- Click table → "Convert to chart" → bar chart appears → edit data → chart updates
- Save to library → presentation persisted → appears on dashboard

---

### 21. Review Workflow

Status-based review: Draft → In Review → Approved.

**Backend:**

- [ ] Create `backend/api/v1/ppt/endpoints/review.py`:
  - `PUT /api/v1/ppt/presentation/{id}/status` → change status
    - Body: `{status: "in_review" | "approved" | "draft", comment: str | None}`
    - Validation: only owner or team member can change status
    - If client review_policy == "require_reviewer": cannot self-approve (owner != approver)
    - Audit log entry on every status change
  - `GET /api/v1/ppt/presentation/{id}/comments` → list comments/status changes
  - `POST /api/v1/ppt/presentation/{id}/comment` → add comment (any team member)

**Frontend:**

- [ ] Create `frontend/app/(presentation-generator)/components/ReviewWorkflow.tsx`:
  - Status badge (color-coded: yellow=Draft, blue=In Review, green=Approved)
  - "Submit for Review" button (Draft → In Review)
  - "Approve" button (In Review → Approved, only if reviewer is different user or self-approve allowed)
  - "Request Changes" button (In Review → Draft, with required comment)
  - Comments section: threaded comments with user avatar, timestamp
  - "Add Comment" text area

- [ ] Add to presentation page header:
  - Review status badge
  - Action buttons based on current status and user role

**Verification:**
- User creates deck → status = Draft
- User clicks "Submit for Review" → status = In Review
- Different user opens same deck → can Approve or Request Changes
- Client with require_reviewer policy → owner cannot self-approve
- Comment added → visible to all team members
- Approved deck → export enabled

---

## Phase 6: Export & Polish

### 22. Export Pipeline (PPTX + PDF)

Enhance export with native charts, brand enforcement, and PDF support.

**Backend:**

- [ ] Modify `backend/utils/export_utils.py`:
  - `export_presentation(presentation_id, format, user)`:
    1. Load presentation + slides from DB
    2. Load master deck config + brand config
    3. Convert slide data → `PptxPresentationModel` (existing pipeline via Next.js API)
    4. For chart slides: use `NativeChartService.add_native_chart()` instead of image
    5. Run `BrandEnforcementService.enforce_on_pptx_model()` — final brand pass
    6. Render PPTX via `PptxPresentationCreator`
    7. If format == "pdf": also render via Puppeteer (existing pipeline)
    8. Audit log: record export action
    9. Return file path for download

- [ ] Create `backend/api/v1/ppt/endpoints/export.py`:
  - `POST /api/v1/ppt/presentation/{id}/export` → body: `{format: "pptx" | "pdf"}`
  - Returns: file download response
  - Access check: user must have access to presentation's client

**Frontend:**

- [ ] Add export dropdown to slide editor:
  - "Export as .pptx" → calls export endpoint → browser downloads file
  - "Export as PDF" → calls export endpoint → browser downloads file
  - Show loading spinner during export
  - Toast on success/failure

**Verification:**
- Export PPTX → open in PowerPoint → all text editable, charts editable, brand correct
- Export PDF → opens in viewer, all slides rendered correctly
- Charts in PPTX: right-click → Edit Data → data table appears
- Gantt/Waterfall: shapes are individually selectable and editable
- Brand colors/fonts/logo present on every slide

---

### 23. Client Library & Dashboard

Two-level navigation: clients → templates/decks. Dashboard for saved presentations.

**Frontend:**

- [ ] Redesign `frontend/app/(presentation-generator)/dashboard/page.tsx`:
  - **Two-level navigation:**
    - First view: grid of client cards (only assigned clients + Oliver Team)
    - Each card: client logo, name, deck count, "New Presentation" button
    - Click client → second view: client's master decks + saved presentations
  - **Client detail view:**
    - Tabs: "Templates" (master decks) | "My Presentations" | "Team Presentations"
    - Templates tab: master deck cards with thumbnails, "Use this template" button
    - My Presentations: list of user's saved decks for this client (with status badges)
    - Team Presentations: decks by other team members (read-only unless reviewer)
  - **"New Presentation" button** → starts wizard (Step 1) with client pre-selected
  - Search/filter bar for presentations

- [ ] Create `frontend/store/slices/clientSlice.ts`:
  - State: `{clients: Client[], selectedClientId, masterDecks: MasterDeck[], presentations: Presentation[]}`
  - Thunks: `fetchClients()`, `fetchMasterDecks(clientId)`, `fetchPresentations(clientId)`

**Verification:**
- User in Nike + Adidas teams → sees Nike, Adidas, Oliver Team cards
- Click Nike → sees Nike master decks + Nike presentations
- User not in Adidas team → Adidas card not visible
- Super Admin sees all clients
- "New Presentation" → wizard starts with Nike pre-selected

---

### 24. Data Retention Service

Auto-purge expired data per client retention policy.

**Backend:**

- [ ] Create `backend/services/retention_service.py`:
  - `RetentionService` class:
    - `run_cleanup()` — scheduled task (daily via ARQ cron):
      1. Query all clients with `retention_days IS NOT NULL`
      2. For each client: find presentations where `created_at < now - retention_days` and `deleted_at IS NULL`
      3. Soft-delete expired presentations (set `deleted_at`)
      4. Delete associated files from filesystem
      5. Audit log each deletion
    - `purge_soft_deleted(days_after_soft_delete=30)` — permanently delete records 30 days after soft delete

- [ ] Add ARQ cron job in `backend/workers/main.py`:
  - Schedule `run_cleanup` daily at 2:00 AM
  - Schedule `purge_soft_deleted` weekly

**Verification:**
- Set client retention to 1 day → create presentation → wait → presentation soft-deleted
- Soft-deleted presentations not visible in UI but still in DB
- After 30 days → permanently deleted from DB and filesystem
- Audit log shows retention cleanup entries

---

### 25. Brand-Adaptive UI Theme

UI colors adapt to the selected client's brand colors.

**Frontend:**

- [ ] Modify `frontend/app/globals.css`:
  - Define CSS custom properties for brand-adaptive colors:
    ```css
    :root {
      --brand-primary: #...;
      --brand-secondary: #...;
      --brand-accent: #...;
    }
    ```

- [ ] Create `frontend/hooks/useBrandTheme.ts`:
  - Reads selected client's brand config from Redux store
  - Sets CSS custom properties on `document.documentElement`
  - Fallback: Oliver brand colors when no client selected

- [ ] Update key UI elements to use brand CSS variables:
  - Wizard step indicator active color
  - Primary buttons
  - Header accent color
  - Slide editor toolbar highlights
  - Client card borders

**Verification:**
- Select Nike (red brand) → wizard step bar turns red, buttons turn red
- Select Adidas (blue brand) → UI shifts to blue accents
- No client selected → Oliver default theme
- Theme changes are instant (no page reload)

---

### 26. Analytics Dashboard

Admin dashboard with usage, quality, and performance metrics.

**Backend:**

- [ ] Create `backend/api/v1/admin/analytics_router.py`:
  - `GET /api/v1/admin/analytics/overview` → aggregated stats:
    - Total presentations generated (all time, this month, this week)
    - Active users count
    - Average generation time
    - Most popular master decks
  - `GET /api/v1/admin/analytics/usage` → usage metrics:
    - Decks per user (top 10)
    - Decks per client (top 10)
    - Decks per day (time series, last 30 days)
  - `GET /api/v1/admin/analytics/quality` → quality metrics:
    - Average slides accepted without edit (% per deck)
    - Average regenerations per deck
    - Average comments per deck
    - Approval rate
  - `GET /api/v1/admin/analytics/performance` → performance metrics:
    - Average generation time (by slide count)
    - LLM provider usage breakdown
    - Error/failure rates
    - API cost estimates (based on token usage)
  - Client Admin: filtered to their clients. Super Admin: all.

**Frontend:**

- [ ] Wire `frontend/app/admin/analytics/page.tsx`:
  - Overview cards: total decks, active users, avg generation time
  - Usage chart: line chart of decks per day (Recharts)
  - Quality metrics: bar chart of acceptance rates
  - Performance: generation time distribution
  - Client filter dropdown (Super Admin sees all)
  - Date range picker

**Verification:**
- Generate 5 decks → analytics show 5 decks, correct generation times
- Client Admin sees only their client's metrics
- Charts render with real data

---

## Phase 7: Testing & Verification

### 27. Testing Suite

**Backend tests:**

- [ ] Auth tests: login flow, JWT validation, role checks, token expiry
- [ ] RBAC tests: client access, team membership, cross-client isolation
- [ ] Audit tests: actions logged correctly, export format correct
- [ ] Content intelligence tests: classification accuracy for each content type
- [ ] Chart data extraction tests: table → chart data conversion accuracy
- [ ] Slide mapping tests: content type → layout mapping correctness
- [ ] Brand enforcement tests: color/font/logo replacement
- [ ] Native chart tests: each chart type renders correctly in PPTX
- [ ] Job queue tests: enqueue, process, retry, failure handling
- [ ] Retention tests: soft delete, purge timing
- [ ] Export tests: PPTX validity, PDF generation
- [ ] Integration test: full pipeline brief → classified content → outline → slides → export

**Frontend tests (Cypress E2E):**

- [ ] Login flow: Azure AD redirect → callback → dashboard
- [ ] Wizard flow: upload → configure → outline → generate → edit → export
- [ ] Admin: create client → upload master deck → configure brand → assign users
- [ ] RBAC: user cannot access unauthorized clients
- [ ] Review workflow: Draft → In Review → Approved
- [ ] Chart editor: edit data → preview updates → export correct
- [ ] Auto-save: close browser mid-wizard → return → state preserved

**Verification:**
- `make test` runs all backend tests
- `make test-e2e` runs Cypress suite
- All tests pass in CI (Docker-based)

---

## Dependency Graph (Build Order)

```
Phase 1: Foundation
  1. Project Setup ──────────────────────────────────────┐
  2. Database Schema ─── depends on 1                    │
  3. Auth Module ──────── depends on 2                   │
  4. RBAC & Teams ─────── depends on 3                   │
  5. Audit Logging ────── depends on 4                   │
  6. i18n Setup ───────── depends on 1 (parallel with 2) │

Phase 2: Admin & Clients                                 │
  7. Admin Panel Shell ── depends on 4, 6                │
  8. Client & Brand ───── depends on 7                   │
  9. Master Deck Parser ─ depends on 8                   │

Phase 3: Content Pipeline                                │
  10. File Upload ─────── depends on 2, 4                │
  11. Content Intelligence depends on 10                 │
  12. Slide Mapping ───── depends on 9, 11               │
  13. Charts (extract + native PPTX) ── depends on 11    │

Phase 4: Generation                                      │
  14. Brand Enforcement ─ depends on 8                   │
  15. Outline Generation ─ depends on 11, 14             │
  16. Job Queue ────────── depends on 15, 12, 13         │

Phase 5: Frontend Wizard & Editor                        │
  17. Wizard Steps 1-2 ── depends on 7, 10               │
  18. Wizard Step 3 ───── depends on 15, 17              │
  19. Wizard Step 4 ───── depends on 16, 18              │
  20. Wizard Step 5 / Editor ── depends on 13, 19        │
  21. Review Workflow ──── depends on 4, 20              │

Phase 6: Export & Polish                                 │
  22. Export Pipeline ──── depends on 13, 14, 20         │
  23. Client Library ───── depends on 8, 20              │
  24. Data Retention ───── depends on 5, 8               │
  25. Brand-Adaptive Theme depends on 8                  │
  26. Analytics Dashboard ─ depends on 5, 7              │

Phase 7: Testing                                         │
  27. Testing Suite ────── depends on everything         ─┘
```

---

## Files Quick Reference

**New files to create (backend):**
```
backend/
├── api/
│   ├── middlewares/
│   │   ├── auth_middleware.py          (Step 3)
│   │   ├── rbac_middleware.py          (Step 4)
│   │   └── audit_middleware.py         (Step 5)
│   └── v1/
│       ├── auth/
│       │   └── router.py              (Step 3)
│       └── admin/
│           ├── users_router.py        (Step 4)
│           ├── teams_router.py        (Step 4)
│           ├── clients_router.py      (Step 4)
│           ├── brand_config_router.py (Step 8)
│           ├── master_decks_router.py (Step 9)
│           ├── audit_router.py        (Step 5)
│           └── analytics_router.py    (Step 26)
├── models/
│   ├── sql/
│   │   ├── user.py                    (Step 2)
│   │   ├── client.py                  (Step 2)
│   │   ├── team.py                    (Step 2)
│   │   ├── team_membership.py         (Step 2)
│   │   ├── brand_config.py            (Step 2)
│   │   ├── master_deck.py             (Step 2)
│   │   ├── audit_log.py               (Step 2)
│   │   └── job.py                     (Step 2)
│   └── content_models.py             (Step 11)
├── services/
│   ├── auth_service.py                (Step 3)
│   ├── access_service.py              (Step 4)
│   ├── audit_service.py               (Step 5)
│   ├── attachment_parser_service.py   (Step 10)
│   ├── content_intelligence_service.py (Step 11)
│   ├── slide_mapping_engine.py        (Step 12)
│   ├── chart_data_extractor.py        (Step 13)
│   ├── native_chart_service.py        (Step 13)
│   ├── brand_enforcement_service.py   (Step 14)
│   ├── master_deck_parser_service.py  (Step 9)
│   ├── job_queue_service.py           (Step 16)
│   └── retention_service.py           (Step 24)
├── workers/
│   ├── main.py                        (Step 16)
│   ├── presentation_worker.py         (Step 16)
│   └── master_deck_worker.py          (Step 16)
├── utils/
│   └── auth_dependencies.py           (Step 3)
└── migrations/                        (Step 2, Alembic)
```

**New files to create (frontend):**
```
frontend/
├── app/
│   ├── login/
│   │   └── page.tsx                   (Step 3)
│   ├── admin/
│   │   ├── layout.tsx                 (Step 7)
│   │   ├── page.tsx                   (Step 7)
│   │   ├── users/                     (Step 7)
│   │   ├── clients/                   (Step 7, 8, 9)
│   │   ├── audit/                     (Step 7)
│   │   ├── analytics/                 (Step 26)
│   │   ├── settings/                  (Step 7)
│   │   └── components/               (Step 7)
│   └── (presentation-generator)/
│       ├── generate/
│       │   ├── layout.tsx             (Step 17)
│       │   ├── upload/page.tsx        (Step 17)
│       │   ├── configure/page.tsx     (Step 17)
│       │   ├── outline/page.tsx       (Step 18)
│       │   └── progress/page.tsx      (Step 19)
│       └── components/
│           ├── ChartDataEditor.tsx    (Step 13)
│           ├── ReviewWorkflow.tsx     (Step 21)
│           └── AttachmentMapper.tsx   (Step 18)
├── components/
│   └── AuthGuard.tsx                  (Step 3)
├── store/slices/
│   ├── authSlice.ts                   (Step 3)
│   ├── wizardSlice.ts                 (Step 17)
│   ├── clientSlice.ts                 (Step 23)
│   └── adminSlice.ts                  (Step 7)
├── hooks/
│   └── useBrandTheme.ts              (Step 25)
└── i18n/
    ├── i18n.ts                        (Step 6)
    ├── I18nProvider.tsx               (Step 6)
    └── locales/en/                    (Step 6)
```
