# Funnelz — Build Checklist

> Status key: ⬜ Not started · 🔄 In progress · ✅ Done

---

## Phase 1 — Foundation

### 1.1 Project Scaffold
- ✅ Create directory structure (`app/`, `public/`, `storage/`, `cron/`, `config/`, `templates/`, `tests/`)
- ✅ Create `composer.json` with PSR-4 autoloading (`App\` → `app/`)
- ✅ Create `.env.example` with all required keys (DB, OpenAI, SMTP, encryption key)
- ✅ Create `config/Env.php` — loads `.env` file, never commits secrets
- ✅ Create `config/Database.php` — PDO factory (strict mode, utf8mb4, exceptions on)
- ✅ Create `public/.htaccess` — front-controller rewrite, security headers (HSTS, CSP, X-Frame-Options, etc.)
- ✅ Create `public/index.php` — front-controller entry point with full DI container bootstrap
- ✅ Create `storage/.htaccess` — deny all direct HTTP access
- ✅ Create `app/Support/Container.php` — minimal dependency injection container

### 1.2 Routing
- ✅ Create `app/Router.php` — simple route table (GET/POST, named params, middleware hooks)
- ✅ Register all application routes in `config/routes.php`

### 1.3 Database Migrations
- ✅ Create `cron/migrate.php` — migration runner (reads ordered SQL files, tracks applied)
- ✅ `migrations/001_users.sql`
- ✅ `migrations/002_organisations.sql`
- ✅ `migrations/003_funnels.sql` (includes `funnel_shares`)
- ✅ `migrations/004_pages_and_versions.sql`
- ✅ `migrations/005_ai_jobs.sql`
- ✅ `migrations/006_audit_log.sql`
- ✅ `migrations/007_sessions.sql` (DB-backed session fallback)
- ✅ `migrations/008_password_resets.sql`
- ✅ `migrations/009_analytics.sql` (`page_views`, `funnel_conversions`)
- ✅ `migrations/010_email_verifications.sql`
- ✅ `migrations/011_share_invites.sql`

### 1.4 Auth — Registration & Login
- ✅ Create `app/Repositories/UserRepository.php`
- ✅ Create `app/Services/AuthService.php` (register, login, email verify, password reset)
- ✅ Create `app/Controllers/AuthController.php`
- ✅ Create templates: `register.php`, `login.php`, `forgot-password.php`, `reset-password.php`
- ✅ Implement CSRF token generation + validation (`app/Support/Csrf.php`)
- ✅ Session cookie hardening (HttpOnly, Secure, SameSite=Lax) in `index.php`
- ✅ Email verification flow (token → `email_verified_at`, status → `active`)
- ✅ Password hashing with argon2id via `password_hash`
- ✅ Login attempt rate limiting + account lockout after N failures (in `AuthService` + `UserRepository`)

### 1.5 Auth — Password Reset
- ✅ `migrations/008_password_resets.sql`
- ✅ Reset-request + reset-confirm routes, controller actions, templates

### 1.6 Auth — Optional TOTP 2FA
- ✅ Create `app/Services/TotpService.php` (pure-PHP TOTP via `spomky-labs/otphp`)
- ✅ Enforce 2FA challenge for `superadmin` on login (in `AuthController`)
- ⬜ 2FA setup + verify + disable routes, controller actions, and templates

### 1.7 Organisations & RBAC
- ✅ Create `app/Repositories/OrganisationRepository.php`
- ✅ Create `app/Services/OrganisationService.php` (create org, invite member, change role, remove member)
- ✅ Create `app/Services/AccessControl.php` — `can($user, $action, $resource)` single authority
- ⬜ Create `app/Controllers/OrgController.php`
- ⬜ Create templates: `templates/org/settings.php`, `templates/org/members.php`

### 1.8 Audit Logging
- ✅ Create `app/Services/AuditService.php` — `log($actor, $action, $subjectType, $subjectId, $before, $after)`
- ✅ Wired into: `AuthService`, `FunnelService`, `PageService`, `OrganisationService`, `SharingService`, `SchemaGeneratorService`

### 1.9 Base Layout & Design System
- ✅ Create `templates/layout/base.php` — HTML shell, nav, flash messages
- ✅ Create `public/assets/css/app.css` — full design system (variables, typography, forms, buttons, cards, tables, badges)
- ✅ Create `public/assets/js/app.js` — CSRF header injection for fetch, flash auto-dismiss

### 1.10 Email (PHPMailer)
- ✅ PHPMailer added to `composer.json`
- ✅ Create `app/Services/MailService.php` — wraps PHPMailer, SMTP from env
- ✅ Email templates: `verify.php`, `reset-password.php`, `invite.php`

---

## Phase 2 — Manual Editor Core

### 2.1 Funnel & Page CRUD
- ✅ Create `app/Repositories/FunnelRepository.php`
- ✅ Create `app/Repositories/PageRepository.php`
- ✅ Create `app/Repositories/VersionRepository.php`
- ✅ Create `app/Services/FunnelService.php` (create, update, delete, list, unique slug generation)
- ✅ Create `app/Services/PageService.php` (create page, save version, publish, restore)
- ✅ Create `app/Controllers/FunnelController.php` (index, create, store, show, update, delete)
- ✅ Create `app/Controllers/PageController.php` (edit, save, publish, versions, restore)
- ✅ Create `app/Controllers/DashboardController.php`
- ✅ Templates: `dashboard/index.php`, `funnel/list.php`, `funnel/create.php`, `funnel/settings.php`, `funnel/versions.php`

### 2.2 Page Spec JSON Contract
- ✅ Create `app/Support/PageSpecSchema.php` — validates Page Spec v1 (version, sections, block types, ids)
- ✅ Validation integrated into `PageService::saveVersion` and `AiGatewayService`
- ⬜ Dedicated unit test: valid spec passes, invalid spec (unknown block type, missing id) fails

### 2.3 Drag-and-Drop Editor Shell
- ✅ Create `templates/editor/shell.php` — full-page editor (sidebar, canvas, props panel, toolbar)
- ✅ Create `public/assets/css/editor.css` — editor layout, section/block styles, AI prompt overlay
- ✅ Create `public/assets/js/editor/editor.js` — full editor implementation:
  - ✅ Canvas rendering from spec JSON
  - ✅ Drag-to-reorder sections (HTML5 drag API)
  - ✅ Drag-to-reorder blocks within and between sections
  - ✅ Drag new blocks from palette onto canvas
  - ✅ Block selection + props panel
  - ✅ Section add/duplicate/delete/move up/move down
  - ✅ Block delete

### 2.4 Block Types (editor-side preview + props)
- ✅ Heading block (level 1–4, text prop)
- ✅ Paragraph block (text/html prop)
- ✅ Image block (src, alt props)
- ✅ Button/CTA block (text, action.url props)
- ✅ Form block (field count preview)
- ✅ Video embed block (url prop)
- ✅ Countdown timer block (targetDatetime, label props)
- ✅ Testimonial block (quote, author props)
- ✅ Icon list block (items count preview)
- ✅ Divider block
- ✅ Spacer block (height prop)
- ✅ Columns container block (columns count preview)
- ✅ Custom HTML block (placeholder preview)

### 2.5 Quill.js Integration
- ✅ Quill 2.x loaded via CDN in editor shell
- ⬜ Deep Quill integration into Heading/Paragraph blocks (inline editing on double-click)
- ✅ `app/Support/Sanitiser.php` — allow-list HTML sanitiser for Quill output

### 2.6 Theme Panel
- ✅ Theme panel in editor sidebar (palette colour pickers, font selects, button radius slider)
- ✅ Theme tokens applied via CSS custom properties (`applyThemeToCss`)
- ⬜ Per-block style override panel (colour, spacing, font-size, border-radius per block)

### 2.7 Responsive Preview
- ✅ Desktop/tablet/mobile preview toggle in editor toolbar (CSS class swap on canvas)
- ⬜ Store per-breakpoint overrides in block spec (`breakpointOverrides`)

### 2.8 Save, Autosave & Version History
- ✅ Debounced autosave (3s after last change → POST to `PageController::save`)
- ✅ Explicit "Save version" with optional label prompt
- ✅ Client-side undo/redo stack (Ctrl+Z / Ctrl+Y, 50-step history)
- ✅ Publish button (saves then publishes, invalidates cache)
- ✅ Version history sidebar panel (loads via fetch)
- ⬜ Diff-lite view in history panel (highlight which sections changed between versions)

### 2.9 Asset Upload
- ✅ Create `app/Controllers/AssetController.php` — upload + serve endpoints
- ✅ Create `app/Services/AssetService.php` — MIME validation via `fileinfo`, safe filename, size cap (10 MB)
- ✅ `storage/.htaccess` blocks direct access; assets served through `AssetController::serve`

---

## Phase 3 — Hosting & Rendering Engine

### 3.1 Funnel Renderer
- ✅ Create `app/Services/RenderService.php` — walks Page Spec JSON, emits semantic HTML + theme CSS
- ✅ Block render templates (all 13 block types):
  - ✅ `heading.php`, `paragraph.php`, `image.php`, `button.php`, `form.php`
  - ✅ `video.php` (YouTube/Vimeo allow-list only)
  - ✅ `countdown.php` (with inline JS timer)
  - ✅ `testimonial.php`, `icon_list.php`, `divider.php`, `spacer.php`
  - ✅ `columns.php` (nested block rendering)
  - ✅ `custom_html.php` (role-gated, sandboxed iframe)
- ✅ Unknown block types render nothing (no error)
- ✅ Semantic HTML: heading levels, alt text on images, label `for` associations on form fields

### 3.2 Public Routing & Page Serving
- ✅ Routes: `GET /f/{payload_slug}` and `GET /f/{payload_slug}/{page_slug}`
- ✅ Create `app/Controllers/PageRenderController.php`
- ✅ Resolves slug → funnel (must be `published`) → page → published version only
- ✅ Renders full standalone HTML page (Google Fonts, theme CSS, minimal JS)
- ✅ Draft/autosave content never publicly visible

### 3.3 Output Cache
- ✅ Create `app/Services/CacheService.php` — file-based, keyed by `page_id + version_id`
- ✅ Cache invalidated on publish (`invalidateByPrefix`)
- ✅ Cache sweep in cron worker (`CacheService::sweep`)

### 3.4 Public Form Submission
- ✅ Route: `POST /f/{payload_slug}/submit/{page_id}`
- ✅ Create `app/Controllers/SubmitController.php`
- ✅ Server-side field validation (type, required) against published form block spec
- ✅ Honeypot field + timing check (bot mitigation, in `form.php` template + `SubmitController`)
- ✅ Rate limiting per IP per funnel (`app/Support/RateLimiter.php`, DB-backed)
- ✅ Writes to `funnel_data_{funnel_id}` via `SchemaGeneratorService`
- ✅ Redirects to next funnel step or inline thank-you

### 3.5 Per-Funnel Data Tables (SchemaGeneratorService)
- ✅ Create `app/Services/SchemaGeneratorService.php`
- ✅ Table name derived from numeric funnel ID only (`funnel_data_{id}`)
- ✅ Field names slugified to `[a-z0-9_]`, checked against reserved-word blocklist
- ✅ DDL generated via whitelist type map — no user string ever concatenated into SQL
- ✅ Every schema change logged to `audit_log`
- ✅ Uses separate DDL DB credential (`Database::ddl()`)

### 3.6 Submissions Management UI
- ✅ Create `app/Controllers/SubmissionController.php`
- ✅ Create `templates/funnel/submissions.php` — table view
- ✅ CSV export (gated by `export_pii` permission check)
- ⬜ Webhook/email-on-submit notification (configurable per funnel)
- ⬜ Basic column filters on submissions table

### 3.7 Page View Tracking
- ✅ Page views written to `storage/logs/pageviews.log` (non-blocking, fire-and-forget)
- ✅ Cron worker flushes log to `page_views` table (`flushPageViews` in `run_jobs.php`)

---

## Phase 4 — AI Easy-Setup

### 4.1 AI Gateway
- ✅ Create `app/Services/AiGatewayService.php` — wraps OpenAI API via `HttpClient`, hides key
- ✅ Per-user daily job quota + token quota enforced before dispatch
- ✅ All AI JSON responses validated against `PageSpecSchema` before persisting
- ✅ `chat()`, `planFunnel()`, `generatePage()`, `editBlock()` methods

### 4.2 Job Queue
- ✅ Create `app/Repositories/JobRepository.php`
- ✅ Create `app/Services/JobService.php` — enqueue, status, complete, fail
- ✅ Create `cron/run_jobs.php` — processes up to N jobs or T seconds per tick, exits cleanly
- ✅ MySQL `GET_LOCK()` prevents double-processing across concurrent cron ticks
- ✅ `SELECT ... FOR UPDATE` in `JobRepository::claimNext()` for atomic job claiming

### 4.3 Funnel Planning Job
- ✅ `processPlanFunnel()` handler in `run_jobs.php` — calls `AiGatewayService::planFunnel()`
- ✅ Validates plan output (pages array with roles)
- ✅ Creates page stubs and enqueues per-page `generate_page` jobs on success

### 4.4 Page Content Generation Job
- ✅ `processGeneratePage()` handler in `run_jobs.php` — calls `AiGatewayService::generatePage()`
- ✅ Output validated against `PageSpecSchema`
- ✅ Saved as unpublished draft version via `PageService::saveVersion()`

### 4.5 AI Easy-Setup Wizard UI
- ✅ Create `app/Controllers/WizardController.php` (setup, start, progress, review, accept)
- ✅ Create `templates/wizard/setup.php` — purpose, audience, offer, tone, page count form
- ✅ On submit: creates draft funnel, enqueues `plan_funnel` job, redirects to progress page
- ✅ Polling endpoint: `GET /api/job/{uuid}/status` → JSON (in `ApiController`)
- ✅ Create `templates/wizard/progress.php` — polls every 3s, progress bar, auto-redirects on completion
- ✅ Create `templates/wizard/review.php` — accept & open editor

### 4.6 Review Screen
- ✅ Review template with "Accept & open editor" action
- ⬜ Per-page regenerate button (enqueues `regenerate_page` job, polls for result)
- ⬜ Preview slideshow of generated pages (renders each page spec inline)

### 4.7 Inline "Ask AI" in Editor
- ✅ "✨ Ask AI" button on every block in the editor canvas
- ✅ Create `app/Controllers/AiEditController.php` — calls `AiGatewayService::editBlock()` directly
- ✅ AI prompt overlay UI in editor (instruction textarea, apply/cancel)
- ✅ Updated block merged back into canvas on success

---

## Phase 5 — Style Discovery

### 5.1 Style Extraction
- ✅ Create `app/Support/StyleExtractor.php` — parses HTML/CSS: hex colours, rgb(), font-family, layout archetype, button radius, spacing density
- ✅ Fetches linked CSS files (up to 3 per page)
- ✅ Never stores source HTML/CSS verbatim — only derived Style Signature JSON
- ✅ SSRF protection via `HttpClient` (RFC1918 block, localhost block, cloud metadata block)
- ✅ Response size capped, connect/read timeouts enforced

### 5.2 Style Source Providers
- ✅ Create `app/Contracts/StyleSourceProvider.php` — interface (swappable)
- ⬜ Create `app/StyleSources/UserUrlProvider.php`
- ⬜ Create `app/StyleSources/GoogleFontsProvider.php`
- ⬜ Create `app/StyleSources/CoolorsProvider.php`

### 5.3 Style Discovery Job
- ✅ `processStyleDiscovery()` handler in `run_jobs.php` — calls `StyleExtractor`, persists to `funnels.style_signature`
- ⬜ Colour-contrast WCAG AA check on derived palette before applying

### 5.4 Integration with Generation
- ✅ Style brief passed as constraints into `generatePage()` prompt
- ⬜ "Match this style" URL input in wizard setup form

---

## Phase 6 — Sharing & Collaboration

### 6.1 Funnel Sharing
- ✅ Create `app/Services/SharingService.php` — invite by email, add share, accept invite, revoke
- ✅ Create `app/Controllers/ShareController.php` (show, invite, revoke, accept)
- ✅ Create `templates/funnel/share.php` — invite form + current shares table
- ✅ Invite email sent via `MailService` on new share
- ✅ Accept-invite flow (token in email → `ShareController::accept` → grant access)
- ✅ `migrations/011_share_invites.sql`

### 6.2 Organisation-Wide Visibility
- ✅ `visibility = 'organisation'` enforced in `AccessControl` (org members get editor/admin permission)
- ⬜ Org-wide funnel list view in org dashboard

### 6.3 Public Preview Link
- ✅ `visibility = 'public_preview'` supported in `AccessControl` schema
- ⬜ Signed token generation + verification for public preview URL
- ⬜ Ensure lead data and edit actions remain access-controlled on preview

### 6.4 Activity Feed
- ⬜ Create `templates/funnel/activity.php` — reads `audit_log` for this funnel

---

## Phase 7 — Security Hardening

### 7.1 Security Audit Pass
- ✅ All DB queries use PDO prepared statements — no raw SQL concatenation
- ✅ All incoming JSON validated against `PageSpecSchema` before persist/render
- ✅ All AI-returned HTML/content passes through `Sanitiser::html()` allow-list
- ✅ Custom HTML block role-gated (`org_admin`/`superadmin`) and rendered in sandboxed iframe
- ✅ CSRF tokens on every state-changing request (forms + JSON API via header)
- ✅ Session cookie flags: HttpOnly, Secure (production), SameSite=Lax

### 7.2 Encryption at Rest
- ✅ Create `app/Support/Encryption.php` — libsodium `crypto_secretbox`, key from env only
- ✅ `TotpService` encrypts `totp_secret` via `Encryption` before storing
- ⬜ PII columns in `funnel_data_*` tables encrypted at write, decrypted at read
- ✅ Encryption key loaded from env, never committed to source control

### 7.3 Rate Limiting & Abuse Prevention
- ✅ Login attempt rate limiting + account lockout (in `AuthService` + `UserRepository`)
- ✅ Public form submission rate limiting per IP per funnel (`RateLimiter` in `SubmitController`)
- ⬜ API endpoint rate limiting (keyed by user/IP)

### 7.4 SSRF & Outbound Request Hardening
- ✅ Create `app/Support/HttpClient.php` — all outbound curl centralised here
- ✅ RFC1918, loopback, link-local IP ranges blocked before connecting
- ✅ Connect (10s) + read (15s) timeouts enforced on all outbound calls
- ✅ Response size capped at 2 MB

### 7.5 HSTS & Security Headers
- ✅ HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, CSP set in `public/.htaccess`

### 7.6 Secrets & Config Hygiene
- ✅ `.env` excluded from web root access via `.htaccess` rule
- ✅ `.env`, `storage/`, `vendor/` added to `.gitignore`
- ✅ Production error handler: no stack traces to browser, errors logged to file only

### 7.7 Backup & Restore
- ✅ Create `cron/backup.php` — pure-PHP MySQL dump, libsodium-encrypted, 7-day rolling retention
- ✅ `docs/restore.md` — step-by-step decrypt + import procedure

### 7.8 Admin Security Dashboard
- ✅ Create `app/Controllers/AdminController.php` (superadmin-gated)
- ✅ Create `templates/admin/security.php` — recent lockouts, permission changes, AI job failures

---

## Phase 8 — Analytics & Pruning

### 8.1 Analytics Schema
- ✅ `migrations/009_analytics.sql` — `page_views`, `funnel_conversions` tables

### 8.2 View & Conversion Tracking
- ✅ Page views logged (fire-and-forget to file) on each public render
- ✅ Cron worker flushes page view log to `page_views` table
- ✅ Conversion recorded on form submit (via `funnel_conversions` table insert in `SubmitController`)
- ⬜ Create `templates/funnel/analytics.php` — per-page views, funnel conversion rate UI

### 8.3 Pruning Cron Job
- ✅ `JobRepository::pruneOld()` — removes completed/failed jobs older than 30 days
- ✅ `CacheService::sweep()` — removes cache files older than configurable TTL
- ✅ `VersionRepository::pruneForPage()` — trims unpublished versions beyond retention count
- ✅ All three called from `cron/run_jobs.php` on every tick

---

## Ongoing / Cross-Cutting

- ✅ `composer.json` — PHPMailer + spomky-labs/otphp + PHPUnit
- ✅ `docker-compose.yml` — local dev only (PHP 8.2 + Apache, MySQL 8)
- ✅ `.gitignore` — excludes `.env`, `storage/uploads/`, `storage/cache/`, `storage/logs/`, `vendor/`
- ✅ `README.md` — setup instructions, cron setup, env variables reference
- ✅ `docs/restore.md` — backup/restore procedure

---

## Remaining Work

The following items are the only outstanding pieces before the build is feature-complete:

### Must-do before first deployment
- ⬜ **2FA UI** — setup, verify, and disable routes + templates (TotpService is ready, just needs the UI)
- ⬜ **OrgController + org templates** — `settings.php`, `members.php` (OrganisationService is ready)
- ⬜ **PII encryption at write/read** — encrypt flagged columns in `funnel_data_*` tables via `Encryption`
- ⬜ **Unit tests** — at least `PageSpecSchema` validation, `Slugifier`, `Sanitiser`, `Encryption` round-trip

### Nice-to-have / polish
- ⬜ Deep Quill integration (inline editing on double-click in editor canvas)
- ⬜ Per-block style override panel (colour, spacing, font-size per block)
- ⬜ Breakpoint overrides stored in block spec
- ⬜ Diff-lite view in version history panel
- ⬜ Per-page regenerate button in wizard review
- ⬜ "Match this style" URL input in wizard
- ⬜ Style source providers (UserUrl, GoogleFonts, Coolors)
- ⬜ WCAG AA contrast check on derived palette
- ⬜ Webhook/email-on-submit notification
- ⬜ Submissions table column filters
- ⬜ Activity feed template (`funnel/activity.php`)
- ⬜ Analytics UI template (`funnel/analytics.php`)
- ⬜ Public preview signed token flow
- ⬜ Org-wide funnel list view
- ⬜ API endpoint rate limiting

---

## Progress Summary

| Phase | Status |
|---|---|
| 1 — Foundation | ✅ Complete (2FA UI + OrgController remaining) |
| 2 — Manual Editor Core | ✅ Complete (deep Quill + per-block overrides remaining) |
| 3 — Hosting & Rendering Engine | ✅ Complete (webhook notification remaining) |
| 4 — AI Easy-Setup | ✅ Complete (per-page regenerate in review remaining) |
| 5 — Style Discovery | 🔄 In progress (extractor + job done; source providers + contrast check remaining) |
| 6 — Sharing & Collaboration | 🔄 In progress (sharing + org visibility done; preview link + activity feed remaining) |
| 7 — Security Hardening | ✅ Complete (PII column encryption + API rate limiting remaining) |
| 8 — Analytics & Pruning | 🔄 In progress (schema + tracking + pruning done; analytics UI remaining) |
