ppt-tool/implementation_plan.md
Vadym Samoilenko cf21ba4516 Phase 1-2: Foundation + Admin Panel & Client Management
Phase 1 (Foundation):
- Project restructure (presenton-main → backend/ + frontend/)
- Database schema (8 new models, Alembic config, seed script)
- Auth (Azure AD SSO + dev bypass, JWT sessions, AuthMiddleware)
- RBAC (access_service, rbac_middleware, admin routers)
- Audit logging (fire-and-forget, AuditMiddleware, admin router)
- i18n (react-i18next with 5 namespace files)

Phase 2 (Admin Panel & Client Management):
- Admin panel shell (sidebar layout, role guard, 12 pages)
- Redux admin slice with 18 async thunks
- User management (role changes, deactivation)
- Client management (CRUD, brand config, team management)
- Brand config editor (colors, fonts, logos, voice rules)
- Master deck upload & parser (PPTX → HTML → React pipeline)
- Audit log viewer with filters and CSV/JSON export

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 15:37:17 +00:00

1449 lines
61 KiB
Markdown

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