# Progress Log

## Current status
- **Phase:** 8 — Final review **complete** (2026-10-03). See `docs/implementation/FINAL-REPORT.md`.
- **Build status:** all phases done. **Awaiting owner review and approval (PRD §35). Not claimed.**
- **Next (owner):** content replacement, CV upload, booking/analytics configuration, design review, approval. Optional: Companion Prompt 4 (handover walkthrough + OWNER-GUIDE.md) and an independent audit (Companion Prompt 3).

---

## 2026-10-03 — Phase 1: Discovery (read-only)

**What changed (docs only):**
- `docs/prd/portfolio-prd.md`: Markdown conversion of `docs/Godsfavour-Okpara-Portfolio-PRD.docx`. The original is unmodified. pandoc was unavailable, so the conversion used a stdlib script run with the system Python 3.9 (Homebrew Python 3.14's `pyexpat` is broken).
- `docs/implementation/01-DISCOVERY.md`

**Commands run (all read-only):** directory listings; `php -v` / `php -m` for PHP 8.1, 8.2, 8.3, and 8.4; `composer --version`; `node -v`; `npm -v`; `mysql --version`; `mysql -e "show databases"`; `redis-cli ping`; `brew services list`; `composer show -a <pkg>` and the Packagist p2 API for version and constraint checks; fetched the Laravel 13 skeleton `composer.json`/`package.json`.

**Findings:** greenfield. There is no Laravel app and no git repository. The default CLI PHP 8.1 is too old, but PHP 8.4.21 is installed. MySQL 9.7 and Redis are running locally. The target stack is PHP 8.4 / Laravel 13.34 / Filament 5.9, and all compatibility was verified.

**Tests:** none exist (no baseline suite).

## 2026-10-03 — Phase 2: Architecture

**What changed (docs only):**
- `docs/architecture/ARCHITECTURE.md` (including the ERD and block registry design)
- `docs/architecture/DECISIONS.md` (ADR-001 to ADR-021, all *Proposed*)
- `docs/implementation/TRACEABILITY.md` (draft; all rows *Planned*)
- `docs/implementation/PROGRESS.md`

**Commands run:** a PHP script computing WCAG contrast ratios for the proposed palette (results in ARCHITECTURE §11.2).

**Tests / Pint:** not applicable (no code yet).

## 2026-10-03 — Phase 3: Foundation

The owner said "continue with Phase 3" without answering the §18 open questions, so reversible defaults were recorded as ADR-022 and all Phase 2 ADRs were marked Accepted.

**What changed (on `main`; see `git log`):**
- **Environment:** `git init` (local, no remote). Laravel 13.34 scaffolded into the repository root next to `docs/`. Composer platform pinned to PHP 8.4.21 (`.php-version`). Local MySQL schemas `gf_portfolio` / `gf_portfolio_testing` with a dedicated user. `.env` configured (MySQL, Redis via predis); the password was generated and never printed.
- **Packages** (each dry-run first): filament/filament 5.9.0 (+ media-library and settings plugins), livewire 4.4.7, spatie/laravel-medialibrary 11.23.8, laravel-settings 3.9.0, laravel-permission 8.3.0, laravel-activitylog 5.1.1, laravel-sitemap 8.2.0, laravel-honeypot 4.7.3, symfony/html-sanitizer 8.1.8, blade-heroicons 2.7.0, predis 3.6.1. Dev: pest 5.3.0, phpunit 13.3.6, pest-plugin-laravel 5.0.1, larastan 3.12.2. npm: @alpinejs/csp 3.17.4, @fontsource-variable/inter and /source-serif-4 5.3.0.
- **Skeleton files not adopted:** the skeleton's `CLAUDE.md`/`AGENTS.md` instructed agents to install Laravel Boost (not in the approved architecture). They were replaced with a project `CLAUDE.md`.
- **Schema** (10 migrations + settings migration): users (MFA, is_active), pages/content_sections/seo_meta, navigation, redirects, taxonomy (tags + taggables, categories, skills), projects (+ pivots), experiences/certifications, authors/articles, recommendations (+ pivots), documents/contact_submissions/analytics_events. Indexes, FKs and unique-nullable flags per ARCHITECTURE §3.
- **Domain/shared:** `HasPublishing` (single live rule), `PublishStatus` + 14 other enums with translated labels, `ContentSanitizer` + `SanitizedHtml` cast, `ContentCache` (tags or versioned keys), `ReadingTimeCalculator`, `MediaCollection` + `InteractsWithPortfolioMedia` (queued WebP/AVIF conversions, private disk for CV), `RedirectService` + automatic 301 on slug change, `ReservedSlugs`, `SystemPage`, `SeoData`/`SeoResolver`, `ContentSecurityPolicy` (+ `CspSourceProvider` contract), 9 typed settings classes.
- **Models:** 21 models with factories (publishing/domain states), enforced morph map, `Model::shouldBeStrict()` outside production.
- **Auth/permissions:** `config/permissions.php` matrix (59 permissions, 3 roles), idempotent seeder, content/managed/user/contact/media policies (no `Gate::before`), last-Super-Admin protection, `portfolio:create-admin`.
- **Filament panel:** `ADMIN_PATH`, login + password reset + profile, TOTP MFA required for Super Admins, DB transactions, strict authorization, navigation groups, 9 settings pages with permission gates, audit entries and validation.
- **Security:** global `SecurityHeaders` (nosniff, referrer, frame, permissions, COOP, HSTS in prod) + nonce-based public CSP / scoped admin CSP; global `HandleRedirects`; rate limiters (contact, cv, preview).
- **Frontend:** Tailwind 4 tokens from the PRD palette, self-hosted fonts + preload, Alpine CSP core with module splitting, mobile-nav component, base layout and primitives (`button`, `card`, `tag`, `section-heading`, `rich-text`, `media.image`, `seo.head`, skip link).
- **Seeding:** `EssentialSeeder` (idempotent; never overwrites owner edits): 10 published system pages with PRD-ordered default sections and locked listing blocks, 4 navigation menus (primary per §6.2, full footer sitemap, mobile CTA bar), default author.
- **Docs:** DECISIONS (ADR-022 to ADR-027), DESIGN-SYSTEM.md, TRACEABILITY updated, README, CLAUDE.md, `.env.example`.

**Commands run (selection):** `composer create-project laravel/laravel`, `composer require --dry-run …` then `composer require …`, `php artisan filament:install --panels`, `vendor:publish` (permission, medialibrary, activitylog, settings, honeypot), `php artisan migrate`, `php artisan db:seed` (twice: idempotency), `npm install`, `npm run build`, `config:cache` / `route:cache` / `view:cache` / `event:cache` (all succeed; caches cleared afterwards), `composer audit`, `npm audit`.

**Results:**
- Tests: **111 passed (271 assertions)**. Unit 36, Feature 70, Arch 5.
- Pint: clean. Larastan level 6: **0 errors**, no baseline, nothing suppressed (two vendor-published configs that reference optional uninstalled packages are excluded; see `phpstan.neon`).
- `composer audit`: no advisories. `npm audit`: 0 vulnerabilities.
- Production asset build: CSS 10.5 kB gzip, JS 23.4 kB gzip.

**Issues found by tests and fixed** (recorded as ADRs):
1. Redirect middleware in the `web` group never ran for unmatched URLs, so it is now global (ADR-024).
2. The per-user `isRequired` closure for MFA was never evaluated per user, so it is now a custom middleware (ADR-023).
3. HasRoles detaches roles before `deleting` listeners run, so the guard moved to `User::delete()` (ADR-025).
4. laravel-settings could not cast nested arrays, so a pass-through cast was added (ADR-026).
5. Strict mode exposed MFA attributes missing on newly created users, so explicit attribute defaults were added.

## 2026-10-03 — Phase 4: CMS

**What changed:**
- **Page builder:** `BlockContract`/`AbstractBlock`/`BlockRegistry`, **39 blocks** (the full MP §4.3 catalogue incl. 7 case-study blocks; `custom_embed` omitted per ADR-014), `config/blocks.php`, `SectionRenderer` (one query per referenced entity type; unknown types skipped and logged; `shouldRender` hides empty blocks), `SaveSections` (scope, permission, per-block rules on normalised data, unique anchors, locked blocks), `SectionsField` relationship repeater (grouped/searchable picker, drag/drop, clone, hide, required badge, live content-rule warnings), slot-based block images on sections (ADR-028), `VideoEmbed` allowlist parser.
- **Read layer:** `ProjectQuery`, `CareerQuery`, `RecommendationQuery` (live + consent), `ArticleQuery`, `ReferenceBatch`, `OrganisationPresenter` (anonymisation gate). Precise cache invalidation on every content model (`FlushesContentCache`).
- **Publishing:** `PublishContent` (policy check, consent rule, audit with before/after snapshot incl. sections, `ContentPublished`/`ContentUnpublished` events), `portfolio:publish-scheduled` (every minute) + daily `model:prune`, `SetHomepage`, `DuplicateContent` (sections + images + SEO + relations), `ReplaceCv`, `PreviewUrl` (signed; actions appear once the Phase 5 route exists, ADR-031).
- **Filament (19 resources + pages):** Pages, Projects, Project Categories, Experience, Skills (+ skills relation manager), Certifications, Articles, Article Categories, Tags, Authors, Recommendations, Navigation Menus (+ items with MorphToSelect), Redirects, CV & Documents, Contact Submissions (unread badge, masking), Media Library (alt-text audit), Users (role, last-Super-Admin guard, MFA reset), Roles & Permissions, Activity Log; Maintenance page; Homepage shortcut; dashboard widgets (content status, latest messages, CV/contact 30-day counts). Shared schemas: Seo, Publishing, Slug (reserved slugs, redirect warning), RichText (toolbar = sanitiser allowlist), MediaField (required alt), BlockFields, BlockImageField. Status/publish/schedule/unpublish/archive/duplicate/preview actions + bulk actions, all via domain Actions. "Demo content" indicator on demo records.
- **Seeding:** `DemoContentSeeder` (refuses production; bracket placeholders; zero recommendations; placeholder certifications; unverified metrics; an empty case-study section to prove graceful absence) + `portfolio:purge-demo`.
- **Audit:** section structure changes (`sections` log), publishing, settings, CV, users/roles, maintenance.
- **Docs:** `docs/architecture/PAGE-BUILDER.md` (worked example), ADR-028 to ADR-032, traceability updated.

**Results:** Tests **245 passed (819 assertions)**. Pint clean. Larastan level 6: 0 errors. `composer audit`: clean. Production caches build.

**Bugs found by tests and fixed:**
1. Pivot table name mismatch (`project_project_category` vs `project_category_project`): every Projects screen 500'd (AdminScreensTest).
2. Section validation read raw Livewire state (TipTap arrays, enum objects), so it now uses dehydrated, normalised data (ADR-029).
3. Reserved-slug rule rejected system pages' own slugs.
4. Filament treated closure validation rules as component callbacks, so they are now `ValidationRule` classes.
5. The publish-permission check passed a class name instead of a model on create pages.
6. The create form started with a blank section.
7. PHP's 128 MB CLI memory limit was hit by the full suite, so it was raised to 1 GB for tests (phpunit.xml).

## 2026-10-03 — Phase 5: Public website

**What changed:**
- **Routes** (all named; catch-all last; `route:cache` compatible): home, 8 system pages (`/book` only while booking is enabled), `/projects/{slug}`, `/insights/{slug}`, `/insights/category/{slug}`, `/preview/{type}/{id}` (signed + auth + view policy + throttle), `/404`, `/{slug}`. Guests on preview links go to the admin login.
- **Read side:** `PageResolver`, `NavigationQuery` (drops links to non-live content / disabled booking / missing CV), `CtaResolver` (booking CTAs hidden centrally), `BookingState`, `CvLinks`, `ProjectDetailQuery` (related: manual then shared category/tool; prev/next in listing order), `ArticleDetailQuery`, `StructuredDataBuilder` (WebSite, Person, Article, BreadcrumbList; never Review), `PagePresenter`.
- **Views:** site shell + header (CV button at all breakpoints), mobile `<details>` menu, mobile CTA bar, footer, sticky contact; 39 block views; page / project / article templates; CMS 404 with DB-failure fallback; static 500/503; cards, timeline, carousel, filters, empty states, share links.
- **JS** (Alpine CSP core + on-demand modules): `filters`, `carousel`, `sticky-contact`, mobile menu enhancement.
- **Docs:** ADR-033 to ADR-037; DESIGN-SYSTEM §7; traceability.

**Results:** Tests **280 passed (934 assertions)**. Pint clean. Larastan 0 errors. Live checks on demo content:
- every route returns its expected status with a cold and a warm Redis cache;
- exactly one H1 per page;
- every script is nonced;
- no horizontal overflow at 390/768/1024/1440;
- **zero axe WCAG 2.1 A/AA violations** (8 pages × 2 widths);
- filtering works without a reload, with URL sync, combined filters, the empty state and Back.

**Bugs found and fixed:**
1. Laravel 13's cache refuses to unserialize objects, so every page 500'd on Redis (ADR-033; tests now serialize like Redis).
2. The `hidden` utility lost to the button's `inline-flex` in Tailwind 4, causing 26px of mobile overflow and indented links.
3. Combined filters used stale server-rendered hrefs.
4. `errors/minimal.blade.php` shadowed Laravel's `errors::minimal` layout, which broke the built-in 403/419/429 pages. It was renamed to `errors/static`.

## 2026-10-03 — Phase 6: Forms and integrations

**What changed:**
- **Contact:**
  - `POST /contact` (throttle 5/min + 20/day) handles JSON and non-JS paths.
  - `StoreContactSubmissionRequest` gives friendly messages and allows linkedin.com URLs only.
  - `SpamGuard` combines honeypot, 3s timing and mandatory fields, plus a pluggable `CaptchaVerifier` (null/Turnstile/reCAPTCHA v3; keys in `.env`). Spam is dropped silently.
  - `SubmitContactMessage` stores with an HMAC-hashed IP and dispatches `ContactSubmissionReceived` to queued owner-notification and optional acknowledgement listeners.
  - The form partial covers every §19.2 state. The `contact-form` JS module adds inline validation, the loading state, success and error panels, aria-live announcements and focus management.
- **CV:** `/cv` (inline) and `/cv/download` (attachment), location-tagged first-party events, `no-store`, fallback page, cache invalidation on replacement.
- **Booking:** `BookingPresenter` adapter (ADR-038) with lazy iframe embed, meeting-type switching, postMessage readiness with timeout fallback, and CSP frame sources.
- **Analytics:**
  - the `AnalyticsEvent` catalogue (26 events) with a generated JS copy (`portfolio:analytics-catalogue`);
  - server drivers: null, database, log, Plausible, GA4 (queued `SendAnalyticsEvent`);
  - client: the `track()` single entry point, Plausible/Umami/GA4 adapters, a delegated `data-track` listener, `page_view`, a consent banner (GA4 only), and the `case-study` module (view, scroll depth 25/50/75/100, time-on-page buckets).
- **SEO:** `sitemap.xml` (live + indexable, `lastmod`; queued regeneration on publish/unpublish, nightly, Maintenance button) and dynamic `robots.txt`. The static `public/robots.txt` was removed.
- **CSP:** booking, analytics and CAPTCHA register as `csp.sources` providers, so enabled integrations are always allowed and nothing else is.
- **Docs:** ADR-038 to ADR-042; traceability.

**Results:** Tests **336 passed (1,107 assertions)**. Pint clean. Larastan 0 errors. `composer audit` and `npm audit` clean. Caches (config/route/view/event) build; 69 routes.

**Live browser checks:**
- **Contact:** an empty submit shows four inline errors and focuses the first. A corrected submit sends and shows the success panel, focus moves to the status, and the message is stored with two queued mail jobs. No console or CSP problems.
- **Booking:** with Calendly configured, no CSP refusals. With the provider blocked or invalid, the fallback appears.

**Bugs found and fixed:**
1. Contact mail listeners were never registered: domain listeners aren't auto-discovered.
2. Clicking Send after fixing an error could be swallowed. The blur-time error clearing shifted the button mid-click; errors now clear while typing.
3. The booking fallback never appeared for blocked or failed embeds, because cross-origin iframes always fire `load`. Readiness now comes from the provider's postMessage.
4. Stripped honeypot fields bypassed the spam check (package default), so the fields are now mandatory.
5. Two tests misused `toContain(needle, message)`, which weakened assertions. Fixed.

## 2026-10-03 — Phase 7: Quality

**What changed:**
- **Playwright + axe suite** (`npm run test:browser`, 59 tests) on a disposable e2e database (ADR-044).
- **Performance:**
  - Alpine removed in favour of a vanilla mobile-menu module (core JS 72 KB → 4.6 KB, ADR-043);
  - weight-only serif font (122 → 51 KB);
  - meta description fallback.
- **Database:** rollback-safe migrations (added `down()` to three vendor migrations and the settings migration).
- **Routing:** reserved slug prefixes (`livewire*`, `filament*`); arch tests for reserved route segments and strict-mode guarantees.
- **Accessibility:** footer links have a 44px minimum width.
- **Static analysis:** Larastan raised to level 7 with the findings fixed.

**Results:** PHP 338 passed (1,128 assertions); browser 59 passed.
- **Lighthouse** (Home/Project/Article): Performance 99, LCP 2.0–2.1 s, CLS 0, Accessibility and Best Practices 100, SEO 66 (only non-production `noindex`).
- **axe:** 0 violations.
- **Database:** EXPLAIN shows indexed access for all 12 key queries; rollback sanity passes.
- **Production:** build and optimise commands succeed; no stack traces with debug off; audits clean.

**Bugs found and fixed:**
1. Missing `down()` in vendor and settings migrations broke rollback.
2. The "Skills" footer link was 39px wide.
3. Livewire 4's hashed route prefix was not reserved.
4. Alpine's weight (and the opsz font) pushed LCP out of "Good".

## 2026-10-03 — Phase 8: Final review

**What changed:**
- **Hardcoded-content audit:** the last UI literals (form labels, filter labels, booking/JS messages) moved to `lang/`; fixed the project card labelling "key contribution" as "Role"; an arch test forbids literal labels in public templates.
- **New tests:** `ContentLifecycleTest` (admin create/edit/publish/remove for Experience, Certification and Article, reflected on the public site; article filter empty state).
- **Docs:** `docs/DEPLOYMENT.md`, `docs/architecture/SECURITY.md`, `docs/architecture/ANALYTICS.md`, README rewritten (developer setup + owner CMS guide), TRACEABILITY finalised (no Planned rows), `FINAL-REPORT.md`.

**Final results:** PHP **343 passed (1,161 assertions)**; browser **59 passed**; Pint clean; Larastan level 7 with 0 errors; audits clean; production build OK; 0 TODO markers.

## 2026-10-03 — CI/CD and GitHub remote

- **Remote:** `git@github.com:fidelcom/gf-portfolio.git` (private). `main` and `develop` pushed; `develop` created from `main`.
- **CI** (`.github/workflows/ci.yml`, on PRs and before every deploy): Pint, Larastan level 7, composer/npm audit, analytics catalogue sync, Pest on MySQL 8.4, Playwright + axe on a disposable e2e database, and a Linux deploy smoke test.
- **CD** (`.github/workflows/deploy.yml` + `deploy/remote-deploy.sh`, ADR-045): `develop` deploys to the development environment and `main` to production. The workflow builds once, rsyncs the release, migrates, caches, switches atomically, health-checks and rolls back automatically. Deploys skip with a notice until the environment secrets and variables are set (DEPLOYMENT.md §11).
- **GitHub settings:** environments created with branch restrictions. Required reviewers and branch protection are unavailable on the current plan (documented with alternatives).
- **First CI runs found two issues, both fixed:**
  1. The e2e database must be migrated before Playwright starts the server. That exposed that CSP provider resolution could 500 the `/up` health check, so it now falls back to the baseline policy (with a new test).
  2. The release script created the `current` symlink inside the release directory, so deploys would never have switched. The smoke test caught it.
- **Result:** both branches green. All 4 CI jobs pass; deploy jobs complete (skipping) pending server configuration.

## 2026-10-03 — Placeholder content seeder
- **Request:** a placeholder seeder covering every page.
- **Added:** `PlaceholderContentSeeder` (local/staging only) builds on `DemoContentSeeder` and adds:
  - placeholder images (hero, About, project cards, article covers, author, certification logos, galleries, logos);
  - a placeholder CV, so every "Download CV" button works;
  - SEO descriptions for every page, project and article;
  - placeholder settings (tagline, email, location, footer text, social links);
  - a published `[Sample Page]` that uses every general block, and a draft Recommendations page;
  - the remaining case-study blocks.
- **Coverage:** 38 of 39 blocks. `cs_quote` is excluded because it quotes a real recommendation and none are invented (MP §16). Metrics stay unverified, and video embeds stay hidden until a real link is added.
- **Purge:** `portfolio:purge-demo` now also removes placeholder images, the placeholder CV and the added sections, and restores settings the owner hasn't changed (ADR-046).
- **Tests:** `PlaceholderContentTest` (5 tests); `BlockSystemTest` validates every placeholder section against its block rules. 349 PHP tests pass.

## 2026-10-03 — Appearance management (ADR-047)
- **Request:** manage fonts, colours (site, sections, buttons, cards) and backgrounds (colour or image), with light and dark mode.
- **Refactor:** 41 view files moved from palette utilities to colour roles. An architecture test now forbids raw palette colours. The default look is unchanged.
- **Built:**
  - **Site Management › Appearance:** light and dark palettes with 24 roles each, live contrast feedback and save-time WCAG AA enforcement; colour mode (light, dark or system) and the visitor switch; 12 self-hosted fonts plus system fonts; site background as a colour or an image with an overlay.
  - **Per-section backgrounds:** raised, dark, a custom colour, or an image with a computed minimum overlay.
- **Bug found by tests:** the section overlay CSS used `>`, which escaping turned into `&gt;`. It's now a descendant selector, with a guard test.
- **Tests:** `ThemeTest` (24 tests) and the settings role matrix. The browser suite now also runs axe on all 12 pages in dark mode at both widths, plus the switch test: 85 passed.

## 2026-10-05 — System review gaps closed (ADR-048)
- **Committed** the outstanding work (`041e52d`) so CI runs on it.
- **Backups:** spatie/laravel-backup runs nightly, encrypted, with retention and a daily health monitor. The dump needs no global MySQL privileges (the default needed `RELOAD`, which failed with a normal per-schema user). Restore verified: every table's row count matched.
- **Alerts:** errors and failed jobs email `ALERT_EMAIL`, throttled per problem, with no stack traces in the email. Backup problems go to the same address. Production logging and an external uptime check on `/up` are documented.
- **Housekeeping:** `activitylog:clean` and `queue:prune-failed` are scheduled.
- **Appearance preview:** unsaved changes on public pages, for the editor only, with an exit link.
- **Clean-up:** replaced settings uploads are deleted, and so is a section's background image when no longer used.
- **Theme:** a `theme-color` meta that follows the visitor switch; the static 500/503 pages follow dark mode.
- **Privacy:** a draft Privacy page with factual prompts, and a footer link that appears once published.
- **Browser tests:** section backgrounds now go through the light and dark axe runs, and the admin preview runs end to end.
- **Docs:** stale traceability rows corrected, new rows added, and the test counts in 07-QUALITY and FINAL-REPORT refreshed.
- **Bugs found:**
  1. The theme service was registered once per request ("scoped"), so in long-running processes a preview could outlive "Exit preview". It's now built on every use.
  2. CI caught that an empty `ALERT_EMAIL` (as in `.env.example`) stopped the app from starting, because the backup package validates its mail address at boot. Empty or invalid now means "no alert emails", with a regression test.
- **Results:** 393 PHP and 86 browser tests pass; Larastan level 7 has 0 errors; the audits are clean.

## 2026-10-06 — Development environment live (ADR-049)
- **URL:** https://dev.godsfavourokpara.com (cPanel account `favokpa` on 208.109.232.171, behind Cloudflare). It deploys automatically from `develop`.
- **Server setup:**
  - SSH deploy key, subdomain on PHP 8.4, database and user restricted to it;
  - `shared/.env` (debug off, database cache/session/queue, `TRUSTED_PROXIES=cloudflare`, mail to the log for now);
  - a per-minute scheduler cron that also runs the queue worker;
  - AutoSSL certificate for the subdomain.
- **Problems fixed on the way:**
  1. cPanel puts subdomain document roots under `public_html`, so that path is now a symlink to the deploy base.
  2. cPanel's MultiPHP `.htaccess` block is preserved per release.
  3. Cloudflare's bot protection returned 403 to the CI health check, so the check now goes to the origin directly (`HEALTH_CHECK_VIA_ORIGIN`).
  4. A critical `shell-quote` advisory (a dev-only dependency) was overridden to the patched version.
- **Verified:** the health check returns 200; all public pages return 200 through Cloudflare; `noindex` is in place; the cron worker processed the 44 image jobs with 0 failures; WebP conversions are served.
- **Content:** placeholder content seeded for review (remove with `portfolio:purge-demo`); Super Admin created for the owner.
- **Docs fix:** an edit on 2026-10-03 had dropped the "Open issues" heading, so the 2026-10-05 entry was never written. Both are restored.

## Open issues
- **Servers:** development is live (above). Production still needs its own cPanel subdomain or root setup and the `production` environment secrets and variables in GitHub (DEPLOYMENT.md §11–§12).
- **Development server:** before uploading phone photos it needs the PHP `exif` extension (WHM › EasyApache 4). It also needs outgoing mail (SMTP, or SPF/DKIM for the domain), after which set `MAIL_MAILER` and `ALERT_EMAIL`. The host's PHP config logs harmless startup warnings (`ffmpeg.so` missing, `session.gc_divisor=0`).
- **Owner-dependent verification:** a real booking URL is needed to see the embedded calendar's positive path, and an analytics provider must be configured before events can be verified in its dashboard (PRD §34).
- **Production configuration still needed:** `ALERT_EMAIL`, an off-server backup disk (`BACKUP_DISKS=backups,s3`), `BACKUP_ARCHIVE_PASSWORD`, and an external uptime monitor on `/up` (DEPLOYMENT §3, §9).
- Production requires a queue worker (mail, image conversions, sitemap, server analytics) and the scheduler cron. These are documented in Phase 8 (`docs/DEPLOYMENT.md`).
- The in-builder "clone section" copies data but not images (documented in PAGE-BUILDER.md §4).
- Per ADR-023, users who opt into MFA cannot disable it themselves.
- No public pages render yet, so the CSP has only been verified by header tests, not against the booking embed (Phase 6).
- Owner decisions still open (defaults applied, ADR-022): analytics provider, booking provider, hosting target, Electric Blue confirmation, fonts.

## TODO(portfolio) markers
None in code. Phase 6 items are tracked in the traceability matrix and ADR-037.
