# 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= ``` **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) ```