# app.theclientspeaks.com — Status

## What it is

The Client Speaks — multi-tenant SaaS for collecting customer feedback
(quick ratings) and marketing testimonials (with consent), via embedded
widget, hosted page, or API. Feedback Receivers (tenants) sign in with
Google/MS OAuth; Feedback Providers (end clients) never authenticate.

## Stack

- Follows the RestartWorks `/public` docroot standard: web files in `public/`; `.env`, `logs/`, user content live above it.
- Auth: `public/api/auth/google.php` / `ms.php` (Google/Microsoft OAuth), sharing one `users` table (`auth_provider` column tells them apart).
- Login page: `public/login.php`. Terms of Service: `public/terms.php`.
- Deploy: two-branch (`dev`→`dev-app.theclientspeaks.com`, `main`→`app.theclientspeaks.com`) via a gitignored `public/deploy.php` webhook receiver.

## Progress

- [x] SpinUp scaffold (auth, login, session, db, deploy pipeline)
- [x] `db/migrations/0001_tenants_feedback_testimonials.sql` written — **needs to be run against the dev DB via phpMyAdmin before this will work**
- [x] Tenant auto-provisioning: `includes/tenant.php` creates a `tenants` row for a user on first dashboard visit (business name from OAuth profile, unique subdomain slug, API key)
- [x] `index.php` is now the real Feedback Receiver dashboard: business name/subdomain, feedback/testimonial counts, friendly notice if migration 0001 hasn't been run yet
- [x] Branding pass on `login.php` + `index.php`: "The Client Speaks" wordmark, teal accent, real Google/Microsoft provider icons on OAuth buttons, dev-environment badge
- [x] Widget API endpoint (`public/api/feedback.php`) — POST tenant_key + rating (up/down/1-5) + optional free_text/context_ref/source_type, CORS-open, IP hashed not stored raw. Dashboard shows the tenant key + a curl example to test it.
- [x] Hosted page (`public/hosted.php`) — public, no-auth, reached via `/hosted.php?t={subdomain}` (kept as a fallback/test path). Fork screen: "Leave quick feedback" (working, submits via `tenant_slug` on `/api/feedback.php`, honeypot field) vs. "Write a testimonial" (coming-soon placeholder). Dashboard links to each tenant's hosted page.
- [x] Real wildcard subdomain routing (`includes/subdomain.php` + `includes/hosted_page.php`) — `*.theclientspeaks.com` has a live Let's Encrypt cert (confirmed in cPanel; `*.app.` / `*.dev-app.` variants shown in the SSL tool are NOT real, no cert). `index.php` now checks the request Host before any dashboard/auth logic: a tenant subdomain renders the public hosted page (same rendering `hosted.php` uses), anything else falls through to the dashboard.
- [x] `db/migrations/0002_tenant_subdomain_provisioning.sql` written — adds `subdomain_provisioned`/`subdomain_provisioned_at`/`subdomain_provision_error` to `tenants`. **Needs to be run against the dev DB via phpMyAdmin.**
- [x] cPanel subdomain auto-provisioning (`includes/cpanel.php`) — calls UAPI `SubDomain::addsubdomain` with `CPANEL_API_TOKEN` from `.env` to actually create `{tenant}.theclientspeaks.com` in cPanel, pointed at whichever docroot ran the request (self-targeting: dev when triggered from dev-app, prod once this runs there). Triggered from `ensure_tenant_for_user` on every dashboard load until it succeeds once. Dashboard shows Live / Not live yet (with the cPanel error) / Status unknown (migration 0002 not run). **Confirmed working end-to-end 2026-07-06**: cPanel's `addsubdomain` auto-creates the DNS A record too (no manual wildcard DNS record needed — don't add one, it'd be redundant). **Known gap: a subdomain provisioned while testing on dev keeps pointing at the dev docroot until re-provisioned from prod — not automated yet.**
- [x] Tenant-chosen custom subdomain (`public/api/subdomain-check.php` + `subdomain-update.php`, dashboard editor) — live availability check (format/reserved/DB-uniqueness, debounced) as you type, Save re-provisions in cPanel via the same `maybe_provision_subdomain()` used at login. Label minimum lowered to 2 chars (was an arbitrary 3, not a DNS requirement).
- [x] Old-subdomain cleanup — `cpanel_delete_subdomain()` (`includes/cpanel.php`) removes the previous cPanel subdomain once the new one is confirmed provisioned (never deletes the old one first, so a tenant is never left with neither working). Surfaced on the dashboard save confirmation.
- [ ] Feedback Receiver dashboard: submissions list (view/filter/export)
- [ ] Testimonial submission path — AI-polish step (Anthropic API), side-by-side original vs. AI version, approval + consent-to-publish checkbox, optional photo upload, `/api/testimonials.php`
- [ ] Turnstile (Cloudflare) + IP rate limit on the hosted page — currently only has the honeypot field; TURNSTILE_SITE_KEY/SECRET aren't in `.env` yet
- [ ] Receiver dashboard: QR/link generator, embed code, API key management (rotate/copy UI)
- [ ] Third-party direct API (`/api/testimonials`, tenant-key auth) — deferred until standalone product works well

## Open items

- Pricing/tiering — not yet decided
- Exact API contract — pending
- Domain: theclientspeaks.com registered; wildcard subdomain per tenant planned but not yet configured
- Turnstile keys not yet in `.env` — hosted page only has honeypot protection until those are added
