Changelog
All notable changes to Wisprs are documented here. Format based on Keep a Changelog.
# Changelog
All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
[Unreleased]
### Fixed
- **Home proxy Squid:** Template [`scripts/home-proxy/squid.conf.wisprs`](scripts/home-proxy/squid.conf.wisprs) adds **`connect_timeout 30 seconds`** after `http_port` (Squid 7: **`dns_v4_first` is obsolete** — documented IPv6/DNS mitigations in [`HOME_PROXY_SQUID_TAILSCALE.md`](HOME_PROXY_SQUID_TAILSCALE.md) §2).
### Added
- **Free-tools yt-dlp observability.** [`HOME_PROXY_SQUID_TAILSCALE.md`](HOME_PROXY_SQUID_TAILSCALE.md) §8 documents `journalctl`, Squid `access.log`, `free_tool_jobs`, Tailscale debug, and optional **`FREE_TOOLS_YT_DLP_DEBUG`** ([`src/lib/env.ts`](src/lib/env.ts), [`exec.ts`](src/lib/free-tools/providers/exec.ts): `-v` + redacted argv + stderr on success). [`.env.example`](.env.example) updated.
### Changed
- **YouTube free-tool yt-dlp** now includes **`ytdlpProxyArgs()`** so **`FREE_TOOLS_HTTP_PROXY`** applies to MP3/MP4 jobs and title extraction ([`youtube-args.ts`](src/lib/free-tools/providers/youtube-args.ts)), same pattern as TikTok ([`tiktok-args.ts`](src/lib/free-tools/providers/tiktok-args.ts)).
- **Home proxy doc:** Smoke tests note that **TikTok often times out** through Squid; validate with **example.com** first; remote curl examples include example before TikTok ([`HOME_PROXY_SQUID_TAILSCALE.md`](HOME_PROXY_SQUID_TAILSCALE.md)).
- **Home proxy doc:** Clarify that **§§1–3 run only on the home Mac**; production env **§4** runs on the Wisprs server ([`HOME_PROXY_SQUID_TAILSCALE.md`](HOME_PROXY_SQUID_TAILSCALE.md)).
### Fixed
- **Home proxy: use `tailscale serve --tls-terminated-tcp=3128` with `https://` clients.** Raw `--tcp=3128` forwards **plain** TCP; **`curl -x https://`** then fails (**wrong version number**). **`--tls-terminated-tcp`** terminates TLS on the tailnet listener and forwards plain HTTP proxy bytes to Squid—matches **`FREE_TOOLS_HTTP_PROXY=https://…ts.net:3128`**. Updated [`HOME_PROXY_SQUID_TAILSCALE.md`](HOME_PROXY_SQUID_TAILSCALE.md), [`scripts/home-proxy/start-tailscale-serve.sh`](scripts/home-proxy/start-tailscale-serve.sh), and [`scripts/home-proxy/com.wisprs.tailscale-serve-squid.plist`](scripts/home-proxy/com.wisprs.tailscale-serve-squid.plist). Mac must **`tailscale serve reset`** then re-run the script (or reload LaunchDaemon).
### Added
- **Home egress proxy (Squid + Tailscale) for `FREE_TOOLS_HTTP_PROXY`.** Runbook [`HOME_PROXY_SQUID_TAILSCALE.md`](HOME_PROXY_SQUID_TAILSCALE.md); Mac helpers in [`scripts/home-proxy/`](scripts/home-proxy/) (Squid install script, Tailscale serve wrapper, optional LaunchDaemon plist, `squid.conf.wisprs`, production env snippet). Shared [`ytdlpProxyArgs()`](src/lib/free-tools-proxy.ts) centralizes yt-dlp `--proxy`; TikTok provider args use it ([`tiktok-args.ts`](src/lib/free-tools/providers/tiktok-args.ts)).
- **wisprs-free-tool-ship: `seo-geo-stack.md`.** Centralizes Wisprs SEO sources ([`docs/SEO_STRATEGY.md`](docs/SEO_STRATEGY.md), [`project_docs/KEYWORD_CLUSTERS.md`](project_docs/KEYWORD_CLUSTERS.md), [`docs/seo/`](docs/seo/)) and when to apply **seo-geo**, **ai-seo**, and **programmatic-seo** during free-tool ships; wired from [`SKILL.md`](.cursor/skills/wisprs-free-tool-ship/SKILL.md), [`implementation.md`](.cursor/skills/wisprs-free-tool-ship/implementation.md), [`reference-keywords.md`](.cursor/skills/wisprs-free-tool-ship/reference-keywords.md), [`free-tool-strategy/SKILL.md`](.cursor/skills/free-tool-strategy/SKILL.md), and [`AGENTS.md`](AGENTS.md).
- **Free TikTok Downloader (`/tools/tiktok-downloader`).** Public tool page with async `tiktok-mp4` / `tiktok-mp3` jobs via yt-dlp, shared install health in [`src/lib/free-tools/providers/yt-dlp-health.ts`](src/lib/free-tools/providers/yt-dlp-health.ts), and optional datacenter IP unblock via [`FREE_TOOLS_HTTP_PROXY`](src/lib/env.ts) ([`.env.example`](.env.example)). Providers [`tiktok.ts`](src/lib/free-tools/providers/tiktok.ts) + [`tiktok-args.ts`](src/lib/free-tools/providers/tiktok-args.ts); URL validation in [`validation.ts`](src/lib/free-tools/validation.ts). Client shows thumbnail, output size, and duration when metadata allows; transcribe handoff + upgrade CTAs ([`tiktok-downloader-tool-client.tsx`](src/app/tools/tiktok-downloader/tiktok-downloader-tool-client.tsx)). SEO/schema on [`page.tsx`](src/app/tools/tiktok-downloader/page.tsx). Registered in [`public-urls.ts`](src/lib/seo/public-urls.ts), **Free TikTok Downloader** in [`navigation.tsx`](src/components/navigation.tsx) / [`mobile-nav.tsx`](src/components/mobile-nav.tsx), and conversion allowlist in [`events/conversion/route.ts`](src/app/api/events/conversion/route.ts). Ops notes in [`docs/growth/TOOLS.md`](docs/growth/TOOLS.md).
- **Completed free-tool job API: optional media fields.** [`GET /api/tools/jobs/[id]`](src/app/api/tools/jobs/[id]/route.ts) returns `outputSizeBytes`, `durationSeconds`, and `thumbnailUrl` when stored on the job (used by the TikTok tool UI).
- **Cursor skills split for free tools.** Wisprs implementation playbook at [`.cursor/skills/wisprs-free-tool-ship/`](.cursor/skills/wisprs-free-tool-ship/); generic strategy stays in [`.cursor/skills/free-tool-strategy/SKILL.md`](.cursor/skills/free-tool-strategy/SKILL.md). [`AGENTS.md`](AGENTS.md) links both.
### Changed
- **YouTube free-tool provider** delegates yt-dlp presence checks to shared [`yt-dlp-health.ts`](src/lib/free-tools/providers/yt-dlp-health.ts) ([`youtube.ts`](src/lib/free-tools/providers/youtube.ts)).
- **Free-tool job typing** widened for stored tool kinds / imports ([`types.ts`](src/lib/free-tools/types.ts), [`db-queries.ts`](src/lib/free-tools/db-queries.ts)).
- **IndexNow scheduled runs now write `indexnow_runs` when there is nothing to submit.** [`src/lib/queue/indexnow-sitemap-job.ts`](src/lib/queue/indexnow-sitemap-job.ts) inserts an audit row with `url_count: 0` and the job `mode` (`delta` or `full`) when the delta window yields no URLs, so production DB checks reflect daily delta execution. Missing `NEXT_PUBLIC_APP_URL` is also logged as a failed run with a clear `error` message.
- **Merchant-level shipping/return policy schema is now centralized and referenced by Product offers.** [`src/app/layout.tsx`](src/app/layout.tsx) now emits stable `@id` JSON-LD nodes for `OfferShippingDetails` (`/#shipping-policy`, US-first, digital/SaaS no physical shipping) and `MerchantReturnPolicy` (`/#return-policy`, `MerchantReturnNotPermitted`). [`src/lib/content/seo.ts`](src/lib/content/seo.ts) now references those IDs from Product `offers.shippingDetails` and `offers.hasMerchantReturnPolicy`, aligning with Google’s recommended merchant-level policy pattern and reducing recurring Product-snippet warning noise without changing runtime behavior.
- **FAQ schema emission is now validity-safe for rich results.** [`src/lib/content/seo.ts`](src/lib/content/seo.ts) now emits `FAQPage` only when it can construct a non-empty `mainEntity` from actual question/answer content; otherwise FAQ-typed pages fall back to `WebPage`. This prevents invalid FAQ rich-result items such as “Missing field `mainEntity`” on pages without structured FAQ pairs.
- **Product schema now includes required `image` for rich result eligibility.** [`src/lib/content/seo.ts`](src/lib/content/seo.ts) now injects a resolved absolute image URL into `Product` JSON-LD (`document.heroImage` when available, otherwise the default Open Graph image), fixing Search Console invalid-item errors like “Missing field `image`” on pages such as `/ai-transcription-software`.
- **Free Audio-to-Text failure UX and transcription queue guards were hardened to prevent misleading completion states and malformed-job crashes.** [`src/app/tools/free-audio-to-text/free-audio-to-text-client.tsx`](src/app/tools/free-audio-to-text/free-audio-to-text-client.tsx) now hides the progress bar in failed state (instead of showing `100%`) and adds clearer failed guidance that explains upload can complete while free transcription limits still block start. [`src/lib/queue/transcription-worker.ts`](src/lib/queue/transcription-worker.ts) now validates required job payload fields (`transcriptionId`, `filePath`, `fileSize`) both before enqueue and at worker intake, rejecting/dropping malformed jobs early instead of crashing with `ERR_INVALID_ARG_TYPE` on undefined file paths.
- **Free Audio-to-Text landing SEO/GEO was upgraded with canonical cleanup and richer discovery signals.** [`src/app/tools/free-audio-to-text/page.tsx`](src/app/tools/free-audio-to-text/page.tsx) now has stronger keyword-targeted metadata, explicit OG/Twitter images, improved `SoftwareApplication`/`FAQPage` fields, and cleaner breadcrumb structure. [`src/app/tools/free-audio-to-text/free-audio-to-text-client.tsx`](src/app/tools/free-audio-to-text/free-audio-to-text-client.tsx) adds answer-first explanatory sections, expanded FAQ copy, and contextual internal links for crawl depth and conversion paths. Canonical drift was reduced by pointing footer links to [`/tools/free-audio-to-text`](src/components/footer.tsx), redirecting legacy [`/tools/free-audio-to-text-converter`](src/app/tools/[slug]/page.tsx), and excluding that legacy path from public URL + llms listings in [`src/lib/seo/public-urls.ts`](src/lib/seo/public-urls.ts) and [`src/app/llms.txt/route.ts`](src/app/llms.txt/route.ts); sitemap priority/frequency for the canonical tool page was increased in [`src/app/sitemap.ts`](src/app/sitemap.ts).
- **Free Audio-to-Text now hard-stops at transcript success until the user explicitly imports into the dashboard.** [`src/lib/queue/transcription-worker.ts`](src/lib/queue/transcription-worker.ts) and [`src/lib/queue/bridge-completion-poller.ts`](src/lib/queue/bridge-completion-poller.ts) now skip locale detection and transcript-artifact prewarm for the free/self-hosted completion path itself (`userTier === 'free'` or `routingMetadata.queueClass === 'free-self-hosted'`), so anonymous free completions no longer run Clerk-adjacent or AI-enrichment follow-up work even if row metadata is imperfect. The public result UI in [`src/app/tools/free-audio-to-text/free-audio-to-text-client.tsx`](src/app/tools/free-audio-to-text/free-audio-to-text-client.tsx) now frames the dashboard path as an explicit import/upgrade step while keeping the main CTA intact.
- **Free Audio dashboard import now works for linked transcription jobs without output files.** [`src/app/actions/free-tools/import-to-transcription.ts`](src/app/actions/free-tools/import-to-transcription.ts) now handles `free-audio-to-text` jobs via `free_tool_jobs.transcriptionId` before the generic `outputPath` guard, preserving the explicit import CTA flow for users who choose to continue into Wisprs after receiving the anonymous transcript.
- **Free Audio-to-Text handoff tokening and CTA flow were hardened for transcribe redirects.** Free Audio submit and chunk-complete APIs now return a dedicated `handoffToken` (separate from polling `publicToken`) in [`src/app/api/tools/free-audio-to-text/jobs/route.ts`](src/app/api/tools/free-audio-to-text/jobs/route.ts) and [`src/app/api/tools/free-audio-to-text/upload/complete/route.ts`](src/app/api/tools/free-audio-to-text/upload/complete/route.ts), and the client uses that token for handoff links/CTA tracking in [`src/app/tools/free-audio-to-text/free-audio-to-text-client.tsx`](src/app/tools/free-audio-to-text/free-audio-to-text-client.tsx). Handoff error rendering is now unified through [`src/app/dashboard/tools/handoff/handoff-error-alert.tsx`](src/app/dashboard/tools/handoff/handoff-error-alert.tsx) from [`page.tsx`](src/app/dashboard/tools/handoff/page.tsx), and conversion event validation now explicitly accepts `free-audio-to-text` in [`src/app/api/events/conversion/route.ts`](src/app/api/events/conversion/route.ts).
- **Free Audio completed-state UX now replaces uploader with transcript-first view, with one-click reset.** In [`src/app/tools/free-audio-to-text/free-audio-to-text-client.tsx`](src/app/tools/free-audio-to-text/free-audio-to-text-client.tsx), once transcription reaches completed with transcript text, the uploader card is hidden and the transcript card becomes the primary surface. The transcript textarea now defaults taller (`min-h-72`) while remaining expandable (`resize-y`), and a new **Transcribe again** action resets the entire flow back to the initial uploader state (file/link input, status, progress, transcript output, and file input DOM state).
- **Free-tool transcriptions no longer prewarm AI artifacts before conversion.** [`src/lib/transcript-artifacts-prewarm.ts`](src/lib/transcript-artifacts-prewarm.ts) now skips prewarm for free-tool-origin rows (`metadata.source` = `free-audio-to-text` / `free_tool`, or `userId` null), preventing background summary/action-point generation and related noisy logs until users enter the converted dashboard flow.
- **Free Audio-to-Text now links orchestration UUID jobs to transcription rows for explicit funnel lifecycle tracking.** Added `free_tool_jobs.transcription_id` linkage in schema ([`src/lib/db/schema.ts`](src/lib/db/schema.ts)) plus migration script update ([`scripts/apply-migrations.sql`](scripts/apply-migrations.sql)). Free Audio creation paths now create a `free_tool_jobs` row first and link it to the created `transcriptions` row in both legacy submit ([`/api/tools/free-audio-to-text/jobs`](src/app/api/tools/free-audio-to-text/jobs/route.ts)) and chunked complete ([`/api/tools/free-audio-to-text/upload/complete`](src/app/api/tools/free-audio-to-text/upload/complete/route.ts)). Polling endpoint [`/api/tools/free-audio-to-text/jobs/[id]`](src/app/api/tools/free-audio-to-text/jobs/[id]/route.ts) now resolves by free-tool UUID first (with temporary numeric fallback via linked transcription id), derives status from the linked transcription row, mirrors lifecycle status back to `free_tool_jobs`, and returns safe failed-state errors.
- **Free-tool external IDs now use public UUIDs for audio-to-text jobs, and bridge auto-start is supported when unreachable.** Free Audio-to-Text creation paths now generate/store `metadata.publicJobId` and return that as API `jobId` in [`/api/tools/free-audio-to-text/jobs`](src/app/api/tools/free-audio-to-text/jobs/route.ts) and [`/api/tools/free-audio-to-text/upload/complete`](src/app/api/tools/free-audio-to-text/upload/complete/route.ts), while polling supports public ID first with temporary numeric fallback in [`/api/tools/free-audio-to-text/jobs/[id]`](src/app/api/tools/free-audio-to-text/jobs/[id]/route.ts). Added bridge supervisor auto-start helper [`src/lib/ai/bridge-supervisor.ts`](src/lib/ai/bridge-supervisor.ts), connection-error detection in [`bridge-stt-client.ts`](src/lib/ai/bridge-stt-client.ts), worker retry-on-autostart wiring in [`transcription-worker.ts`](src/lib/queue/transcription-worker.ts), and env controls in [`src/lib/env.ts`](src/lib/env.ts) / [`.env.example`](.env.example) (`STT_BRIDGE_START_COMMAND`, `STT_BRIDGE_AUTOSTART_TIMEOUT_MS`).
- **Free Audio-to-Text now uses chunked public uploads for file reliability, with legacy submit fallback retained.** Added public chunked endpoints [`/api/tools/free-audio-to-text/upload/init`](src/app/api/tools/free-audio-to-text/upload/init/route.ts), [`/upload/chunk`](src/app/api/tools/free-audio-to-text/upload/chunk/route.ts), and [`/upload/complete`](src/app/api/tools/free-audio-to-text/upload/complete/route.ts), backed by tokenized upload session helpers in [`src/lib/free-tools/public-upload-session.ts`](src/lib/free-tools/public-upload-session.ts) and client uploader logic in [`src/lib/free-tools/public-chunked-uploader.ts`](src/lib/free-tools/public-chunked-uploader.ts). Updated [`free-audio-to-text-client.tsx`](src/app/tools/free-audio-to-text/free-audio-to-text-client.tsx) to upload files through init→chunk→complete (with retries and progress-aware pending state) while preserving direct-link submit via legacy [`/api/tools/free-audio-to-text/jobs`](src/app/api/tools/free-audio-to-text/jobs/route.ts). Middleware now allows the new public upload routes in [`src/middleware.ts`](src/middleware.ts).
- **Free Audio-to-Text tool shipped with public intake API, stronger uploader UX, and navigation integration.** Added the new route shell at [`/tools/free-audio-to-text`](src/app/tools/free-audio-to-text/page.tsx) and client flow in [`free-audio-to-text-client.tsx`](src/app/tools/free-audio-to-text/free-audio-to-text-client.tsx): drag/drop uploader with selected-file controls, file/link mutual exclusivity, status badge animation parity with YouTube free tools, gradient-tinted card treatment, and clearer FAQ separation. Added public submit/poll endpoints under [`/api/tools/free-audio-to-text/jobs`](src/app/api/tools/free-audio-to-text/jobs/route.ts) and [`/api/tools/free-audio-to-text/jobs/[id]`](src/app/api/tools/free-audio-to-text/jobs/[id]/route.ts) with server-side gating (allowed extensions, 500MB cap, 10-minute limit) plus anonymous token polling. Registered the tool in discoverability/public routing via [`src/lib/seo/public-urls.ts`](src/lib/seo/public-urls.ts), middleware public route allowlist in [`src/middleware.ts`](src/middleware.ts), and both desktop/mobile Free Tools navigation entries in [`src/components/navigation.tsx`](src/components/navigation.tsx) and [`src/components/mobile-nav.tsx`](src/components/mobile-nav.tsx).
- **YouTube free-tool pages now have distinct SEO/GEO intent, schema, and support-cluster coverage.** Updated route-level metadata/canonicals for [`/tools/youtube-video-downloader`](src/app/tools/youtube-video-downloader/page.tsx) and [`/tools/youtube-audio-downloader`](src/app/tools/youtube-audio-downloader/page.tsx), expanded answer-first/FAQ copy in the shared shell [`youtube-mp3-tool-client.tsx`](src/app/tools/youtube-mp3/youtube-mp3-tool-client.tsx), and added JSON-LD (`SoftwareApplication`, `BreadcrumbList`, `FAQPage`) per page. Added supporting pages [`/tools/youtube-video-to-text`](src/app/tools/youtube-video-to-text/page.tsx) and [`/tools/how-to-transcribe-youtube-video-to-text`](src/app/tools/how-to-transcribe-youtube-video-to-text/page.tsx), plus crawl/discoverability registration in [`src/lib/seo/public-urls.ts`](src/lib/seo/public-urls.ts). Growth keyword mapping and post-rollout backlog documented in [`docs/growth/YOUTUBE_TOOL_SEO_KEYWORD_MAP.md`](docs/growth/YOUTUBE_TOOL_SEO_KEYWORD_MAP.md) and [`docs/growth/TOOLS.md`](docs/growth/TOOLS.md).
- **Free tools → transcribe handoff flow (auth-safe, reusable, and conversion-tracked).** Added shared handoff URL helpers in [`src/lib/free-tools/transcribe-handoff.ts`](src/lib/free-tools/transcribe-handoff.ts), a reusable CTA in [`src/components/free-tools/free-tool-transcribe-cta.tsx`](src/components/free-tools/free-tool-transcribe-cta.tsx), and a new handoff route [`src/app/dashboard/tools/handoff/page.tsx`](src/app/dashboard/tools/handoff/page.tsx). Handoff import now runs through [`src/app/actions/free-tools/import-to-transcription.ts`](src/app/actions/free-tools/import-to-transcription.ts), preserving the normal “ready to transcribe” confirmation experience and idempotency by `freeToolJobId`.
- **Public downloader success UX and transcribe journey refined.** The completed state in [`src/app/tools/youtube-mp3/youtube-mp3-tool-client.tsx`](src/app/tools/youtube-mp3/youtube-mp3-tool-client.tsx) now hides the form, uses nested left/right cards, keeps Download/Download again in the left panel, and routes both the big transcribe CTA and inline Log in link through the same handoff flow in a new tab.
- **Display filename resolution improved for completed jobs/imports.** Job status API [`src/app/api/tools/jobs/[id]/route.ts`](src/app/api/tools/jobs/[id]/route.ts) now returns `displayFileName` using [`resolveImportedFreeToolFileName`](src/lib/free-tools/job-display-filename.ts). Worker/provider metadata paths were hardened: embedded tag title extraction in [`src/lib/upload/metadata-extractor.ts`](src/lib/upload/metadata-extractor.ts), worker fallback in [`src/lib/queue/free-tools-worker.ts`](src/lib/queue/free-tools-worker.ts), and YouTube provider metadata embedding via `yt-dlp --add-metadata` in [`src/lib/free-tools/providers/youtube.ts`](src/lib/free-tools/providers/youtube.ts).
- **Conversion analytics pipeline extended for free-tool funnel events.** Added allowlisted client event endpoint [`src/app/api/events/conversion/route.ts`](src/app/api/events/conversion/route.ts), new event names in [`src/lib/events/types.ts`](src/lib/events/types.ts), and event-worker compatibility handling in [`src/lib/queue/event-worker.ts`](src/lib/queue/event-worker.ts) so new funnel events are processed instead of dropped.
- **Local storage provider now persists uploaded bytes in development.** [`src/lib/storage.ts`](src/lib/storage.ts) local provider now writes/deletes files under `uploads/`, fixing `/api/files/...` 404s for imported free-tool transcriptions.
- **Free-tools submit defaults raised to 50/hour.** [`src/lib/env.ts`](src/lib/env.ts) defaults for `FREE_TOOLS_SUBMITS_PER_IP_PER_HOUR` and `FREE_TOOLS_SUBMITS_PER_DEVICE_PER_HOUR` increased from `10` to `50`.
- **Free tools are now publicly accessible without login and protected by backend abuse controls.** [`src/middleware.ts`](src/middleware.ts) now allows `/api/tools/jobs(.*)` without Clerk session checks. [`src/app/api/tools/jobs/route.ts`](src/app/api/tools/jobs/route.ts) enforces per-hour limits by hashed IP and anonymous device cookie (`wisprs_ft`), with Redis-backed counters and in-memory fallback via [`src/lib/free-tools/rate-limit.ts`](src/lib/free-tools/rate-limit.ts). New env controls: [`FREE_TOOLS_SUBMITS_PER_IP_PER_HOUR`](src/lib/env.ts) and [`FREE_TOOLS_SUBMITS_PER_DEVICE_PER_HOUR`](src/lib/env.ts). Client requests include credentials for cookie-based limit continuity in [`src/app/tools/youtube-mp3/youtube-mp3-tool-client.tsx`](src/app/tools/youtube-mp3/youtube-mp3-tool-client.tsx).
- **YouTube free tools UX/navigation and routes were expanded for pSEO discoverability.** Added dedicated routes for [`/tools/youtube-video-downloader`](src/app/tools/youtube-video-downloader/page.tsx) and [`/tools/youtube-audio-downloader`](src/app/tools/youtube-audio-downloader/page.tsx), with legacy `/tools/youtube-mp3` redirecting to video downloader in [`src/app/tools/youtube-mp3/page.tsx`](src/app/tools/youtube-mp3/page.tsx). Desktop and mobile navigation now expose separate video/audio tool links in [`src/components/navigation.tsx`](src/components/navigation.tsx) and [`src/components/mobile-nav.tsx`](src/components/mobile-nav.tsx). The downloader page now includes dynamic server status badges, gradient CTA styling, and queue/progress UX refinements in [`youtube-mp3-tool-client.tsx`](src/app/tools/youtube-mp3/youtube-mp3-tool-client.tsx).
- **Homepage directory badges: Fazier; badges removed from footer.** [`src/components/directory-badges.tsx`](src/components/directory-badges.tsx) adds the **Fazier** featured launch badge (`fazier.com/launches/wisprs.co`, official SVG). [`src/components/footer.tsx`](src/components/footer.tsx) no longer renders [`DirectoryBadgesRow`](src/components/directory-badges.tsx); directory badges appear only in the trust strip below the homepage hero ([`src/app/page.tsx`](src/app/page.tsx)).
- **Blog hero overlay WebP sibling** — [`scripts/overlay-blog-hero.ts`](scripts/overlay-blog-hero.ts) writes `{slug}-featured.webp` next to `.png` (Sharp WebP q92/effort 6; **`--no-webp`** to skip). Docs/skills cross-link **wisprs-marketing-images**. Example post [`heroImage`](content/blog/getting-started-with-audio-transcription.md) → `.webp`.
- **Blog featured image layout** — Post hero and [`BlogPostCard`](src/components/blog-post-card.tsx) use **1200×630 aspect** ([`src/lib/content/blog-featured-image.ts`](src/lib/content/blog-featured-image.ts)) instead of fixed `h-64`/`h-48` + `object-cover`, so OG-style artwork is not cropped at the bottom.
- **Deploy: `content:sync` opt-in** — [`scripts/deploy-wisprs.sh`](scripts/deploy-wisprs.sh) **skips** `npm run content:sync` by default so deploys no longer upsert `content/blog` + pSEO JSON into prod. Set **`DEPLOY_RUN_CONTENT_SYNC=1`** to run it (still uses `keep-db-if-any` for blog bodies). Manual sync on server documented in [`devops/WISPRS_DEPLOY.md`](devops/WISPRS_DEPLOY.md). Skill [`wisprs-deploy`](.cursor/skills/wisprs-deploy/SKILL.md) updated.
- **Production `content:sync` blog bodies** — [`src/lib/content/sync.ts`](src/lib/content/sync.ts): when `NODE_ENV=production`, default **`keep-db-if-any`** — if the blog row already has a body, **do not replace it** from `content/blog/*.md`. When sync runs: override with `from-file`. Documented in [`.env.example`](.env.example), [`devops/WISPRS_DEPLOY.md`](devops/WISPRS_DEPLOY.md), [`docs/guides/CONTENT_PUBLISHING_GUIDE.md`](docs/guides/CONTENT_PUBLISHING_GUIDE.md).
### Added
- **Blog hero image pipeline (docs).** [`docs/guides/publishing-assets/blog-hero-image-pipeline.md`](docs/guides/publishing-assets/blog-hero-image-pipeline.md) — runbook for featured/OG art: plate → Satori + Resvg + Sharp, `heroImage` wiring, troubleshooting; reference asset `public/images/blog/getting-started-with-audio-transcription-featured.png`. Linked from [`docs/guides/publishing-assets/README.md`](docs/guides/publishing-assets/README.md), [`docs/README.md`](docs/README.md), [`docs/guides/CONTENT_PUBLISHING_GUIDE.md`](docs/guides/CONTENT_PUBLISHING_GUIDE.md).
- **Homepage directory trust strip.** [`src/app/page.tsx`](src/app/page.tsx) — “Listed & featured on” + [`DirectoryBadgesRow`](src/components/directory-badges.tsx) between hero and How it works; label [`HOME_DIRECTORY_BADGES_LABEL`](src/lib/marketing-copy.ts).
- **Phase 2 + ramp content calendar.** [`src/lib/content/calendar/`](src/lib/content/calendar/) defines Phase 2 job specs, KEYWORD_CLUSTERS backlog, and a 5→7→10→12 weekly manifest; [`content/calendar/manifest.json`](content/calendar/manifest.json) is generated via `pnpm content:calendar:manifest`. pg-boss queue `content-calendar` ([`src/lib/queue/content-calendar-job.ts`](src/lib/queue/content-calendar-job.ts)) enqueues daily jobs; `pnpm content:calendar:enqueue` and admin routes `POST /api/content/jobs/calendar-today` / `seed-phase2`. Env and ops: [`devops/CONTENT_CALENDAR.md`](devops/CONTENT_CALENDAR.md), [`content/calendar/README.md`](content/calendar/README.md), [`project_docs/90DAY_PSEO_STATUS.md`](project_docs/90DAY_PSEO_STATUS.md).
### Changed
- **Content queue: Approve failed jobs with salvageable output.** Draft-stage article-shape exhaustion used to set `failed` with only `draft`, so **Approve** never appeared. [`src/lib/content/agents/orchestrator.ts`](src/lib/content/agents/orchestrator.ts) now sets **`pending-review`** with `finalContent` + Fix list (same idea as humanizer QA warnings). [`src/lib/content/job-salvage.ts`](src/lib/content/job-salvage.ts) + [`src/app/api/content/jobs/[id]/approve/route.ts`](src/app/api/content/jobs/[id]/approve/route.ts) allow **Approve** for existing **`failed`** rows when `draft` / `seo_optimized` / `humanized` / `final_content` is present (best stage wins). [`content-queue-client.tsx`](src/app/dashboard/content-queue/content-queue-client.tsx) and review actions show Approve/Reject accordingly; [`reject`](src/app/api/content/jobs/[id]/reject/route.ts) accepts `failed`. Review page **Open page** uses correct path for `alternatives` / `podcast` / `use-cases` / `tools`.
- **Content edit: pipeline body preview for pSEO + full slug match.** [`src/app/actions/content-cms.ts`](src/app/actions/content-cms.ts) matches `content_jobs` when the editor URL uses a full slug (`alternatives/wisprs-vs-descript`) while jobs store the entity slug (`wisprs-vs-descript`); includes `final_content` in the lookup. [`src/app/dashboard/content/edit/page.tsx`](src/app/dashboard/content/edit/page.tsx) applies `bodyOverride` for **pSEO** as well as blog so humanized output shows before **Approve**. Publishing still runs only via `POST /api/content/jobs/[id]/approve` (writes program JSON + `syncContentToDb`).
- **Homepage directory trust strip background.** [`src/app/page.tsx`](src/app/page.tsx) — same violet/pink radial overlay as “How it works” plus a short top fade from hero black so the strip reads as one continuous page instead of a flat band.
- **Marketing PNGs losslessly optimized** — [`marketing/images/`](marketing/images/) PNGs recompressed with `oxipng -o max --strip all -a` (same filenames; ~2.9 MiB total savings).
### Added
- **MarketingDB footer badge.** [`src/components/footer.tsx`](src/components/footer.tsx) — “Listed on MarketingDB” badge (official embed) in the footer bar for directory verification and backlinks.
- **FoundrList footer badge.** [`src/components/footer.tsx`](src/components/footer.tsx) — official “Live on FoundrList” embed (`/product/wisprs`, `rel="nofollow noopener noreferrer"`) next to MarketingDB for badge verification.
- **ShowMeBestAI footer badge.** [`src/components/footer.tsx`](src/components/footer.tsx) — “Featured on ShowMeBestAI” embed (`feature-badge-dark.webp`) with directory badges.
- **ToolFame footer badge.** [`src/components/footer.tsx`](src/components/footer.tsx) — “Featured on toolfame.com” embed (`/item/wisprs`, `badge-dark.svg`, 54px height).
- **Turbo0 footer badge.** [`src/components/footer.tsx`](src/components/footer.tsx) — “Listed on Turbo0” embed (`turbo0.com/item/wisprs`, `badge-listed-dark.svg`, 54px height) for backlink verification.
- **Public demo page for promo video.** [`src/app/demo/page.tsx`](src/app/demo/page.tsx) — `/demo` serves the same hero promo (WebM + MP4 + poster) with native controls, OG/Twitter metadata, and direct file links for launches (Product Hunt, social). Public in [`src/middleware.ts`](src/middleware.ts); included in sitemap via [`src/lib/seo/public-urls.ts`](src/lib/seo/public-urls.ts). [`docs/PROMO_VIDEO.md`](docs/PROMO_VIDEO.md) documents primary vs direct URLs. Middleware matcher skips `.mp4` / `.webm` so static video files do not run Clerk.
- **STT bridge model prewarm.** [`stt-bridge/prewarm_models.py`](stt-bridge/prewarm_models.py) downloads/loads faster-whisper checkpoints for `BRIDGE_STT_WHISPER_MODEL_FAST` and `BRIDGE_STT_WHISPER_MODEL_ACCURATE` using production env (run on server after deploy so “Best quality” does not block on first HF fetch).
- **Free-tier STT routing tests.** [`src/lib/stt-quality.test.ts`](src/lib/stt-quality.test.ts) — `npx tsx --test src/lib/stt-quality.test.ts`.
- **Contabo vmi3157334 systemd preset.** [devops/configs/systemd/wisprs-stt-bridge.service.contabo-vmi3157334](devops/configs/systemd/wisprs-stt-bridge.service.contabo-vmi3157334) (12 vCPU / 47 GiB, **MemoryMax=24G**, **CPUQuota=1000%**, Whisper env). [devops/RECON_RESULTS.md](devops/RECON_RESULTS.md) contabo-prod table filled from live `ssh contabo-prod`.
- **STT bridge: env-tunable faster-whisper.** `BRIDGE_STT_WHISPER_DEVICE` (default `cpu`), `BRIDGE_STT_WHISPER_COMPUTE_TYPE` (default `int8`), `BRIDGE_STT_WHISPER_MODEL_FAST` / `BRIDGE_STT_WHISPER_MODEL_ACCURATE` (defaults `small` / `large-v3`). Documented for **CPU-only Contabo** sizing in [devops/CONTABO_MIGRATION.md](devops/CONTABO_MIGRATION.md) §6, [devops/WISPRS_STT_BRIDGE.md](devops/WISPRS_STT_BRIDGE.md), [stt-bridge/README.md](stt-bridge/README.md).
- **Contabo migration runbook and large-host STT unit.** New [devops/CONTABO_MIGRATION.md](devops/CONTABO_MIGRATION.md) (SSH e.g. `contabo-prod`, recon, provision Postgres/nginx/systemd, `pg_dump`/restore, `.env.production`, deploy overrides, DNS, STT tuning). [devops/RECON_RESULTS.md](devops/RECON_RESULTS.md) gains **Server hardware (contabo-prod)** table (fill after recon). [devops/configs/systemd/wisprs-stt-bridge.service.large-host.example](devops/configs/systemd/wisprs-stt-bridge.service.large-host.example) for higher MemoryMax/CPUQuota; [devops/configs/systemd/README.md](devops/configs/systemd/README.md) indexes units. [devops/WISPRS_DEPLOY.md](devops/WISPRS_DEPLOY.md), [devops/app/WISPRS_DEPLOYMENT.md](devops/app/WISPRS_DEPLOYMENT.md), [devops/WISPRS_STT_BRIDGE.md](devops/WISPRS_STT_BRIDGE.md), [AGENTS.md](AGENTS.md), [scripts/deploy-wisprs.sh](scripts/deploy-wisprs.sh), and wisprs-deploy skill reference Contabo deploy.
- **GSC “Discovered – not indexed” runbook and internal links.** New [`docs/seo/GSC_COVERAGE_RUNBOOK.md`](docs/seo/GSC_COVERAGE_RUNBOOK.md) documents live verification, triage vs robots/4xx, and manual GSC steps. Footer adds links to use cases, comparisons (`/alternatives/best-otter-ai-alternatives`), free audio-to-text tool, and podcast transcription; desktop and mobile nav add **Compare** to the same alternatives hub. Root `metadata.robots` is explicitly `index, follow` for clarity (dashboard remains disallowed in `robots.txt`).
- **Manual IndexNow sitemap submit script.** `scripts/indexnow-submit-sitemap.ts` (and `pnpm indexnow:submit-sitemap` / `pnpm indexnow:submit-sitemap:dry`) submits the same URL list as `/sitemap.xml` and the nightly pg-boss job; supports `--dry-run`; optional `indexnow_runs` row when `DATABASE_URL` is set. Documented in `docs/events/ENV_SETUP.md` and `devops/WISPRS_DEPLOY.md`.
- **IndexNow verification file (openssl key).** Added `public/{INDEXNOW_KEY}.txt` with a key generated via `openssl rand -hex 16`, matching [Trevor Lasn’s IndexNow setup](https://www.trevorlasn.com/blog/how-to-setup-indexnow-for-instant-search-engine-indexing). Production must set **`INDEXNOW_KEY`** in `.env.production` to the same value. Docs: `docs/events/ENV_SETUP.md` and `.env.example` updated (Bing IndexNow vs Webmaster API key).
- **STT accuracy reference and benchmark harness.** New [`docs/reference/STT_ACCURACY_AND_BENCHMARKS.md`](docs/reference/STT_ACCURACY_AND_BENCHMARKS.md) (model inventory, vendor sourcing, Wisprs-measured WER template). [`scripts/stt-benchmark/`](scripts/stt-benchmark/) with word-level WER (`wer.ts`), OpenAI + ElevenLabs runners, `--verify-wer` sanity check; `pnpm stt-benchmark:verify`. Outputs go to `benchmark-results/` (gitignored). Docs index, pitch deck appendix, and STT bridge doc link to the reference.
- **Content queue: status badge and live polling.** Reusable `ContentJobStatusBadge` (backed by `getStatusDisplay()` in `src/lib/content/status-display.ts`) used on the queue list and review page for consistent status styling. Review page mounts `ContentQueueReviewPoller` to poll `GET /api/content/jobs/[id]/status` every 12s while the job is in an active pipeline stage and calls `router.refresh()` when status changes so the badge and content stay in sync without leaving the page.
- **Blog edit: editable slug.** Content edit form (blog only) now has a Slug field so the article URL can be changed. Saving with a new slug writes the file under the new name, deletes the old markdown file, and revalidates both `/blog/[old-slug]` and `/blog/[new-slug]`. Server action `saveBlogContentEdit` accepts optional `previousSlug` and removes the previous file when the slug changes.
- **Next.js image config and FileCard thumbnails.** `next.config.ts` includes `images.remotePatterns` for localhost (dev) and a comment for adding production storage hostnames. `FileCard` uses `next/image` for thumbnail URLs; blob and data URLs use a plain `<img>` with an eslint-disable (next/image does not support them).
- **Dependency scripts and security overrides.** `package.json` scripts `deps:audit` and `deps:outdated` for quick checks. pnpm overrides force patched versions of transitive deps (hono, @hono/node-server, flatted, esbuild, lodash, @tootallnate/once) so `pnpm audit` reports no known vulnerabilities.
- **Content edit: pipeline output and Fix list for human review.** When a content job exists for a blog slug (e.g. after a pipeline run or retry), the content edit page loads the latest job’s draft or humanized body as the initial editor content and shows the **Fix list** (editorSummary action points) for reference. New `getContentJobEditorContext(slug)` in `content-cms.ts` fetches the latest job by `targetSlug`/`slug` with `editorSummary` or body; the edit page merges `bodyOverride` into the document and passes `editorSummary` to the form.
- **GPT-5.3 Chat in LLM seed for draft/humanizer.** `gpt-5.3-chat-latest` (GPT-5.3 Instant) added to `scripts/seed-llm-models.ts` with `prioritySummary: 1` so draft and humanizer use it when OpenAI is preferred. Run `npm run db:seed-models` to apply.
- **Blog featured section and backend tagging.** Blog page shows a "Featured" section at the top (up to 6 posts) for any post whose tags include `featured`. Content edit form (blog only) has a "Feature on blog" checkbox that adds or removes the `featured` tag. Three articles (Getting Started with Audio Transcription, New Feature: AI-Powered Summaries, Transcription accuracy tips) were labeled as featured in frontmatter and synced to the DB via `pnpm content:sync`.
- **Content edit: full emoji picker and Option A markdown improvements.** (1) **Emoji:** Replaced the fixed row of 10 emojis with a single “Emoji” button that opens a full dropdown picker (`emoji-picker-react`), rendered in a portal so it isn’t clipped by the card; dark theme, click-outside to close, viewport clamping so the picker stays on screen. (2) **Image upload:** New `POST /api/content/upload-image` (admin-only) accepts an image file, stores under `public/images/content/<yyyy-mm>/`, returns `{ url }`. Editor has “Upload image” (file picker), and paste/drop of images uploads and inserts `` into the body. (3) **Columns:** Markdown renderer allows safe block HTML (sections that start with `<div` and end with `</div>`); sanitized with `isomorphic-dompurify` (allow-list tags/attrs). “Insert columns” button in the editor inserts a two-column grid block (Tailwind `grid grid-cols-1 md:grid-cols-2 gap-4`). No storage or pipeline changes; body remains Markdown.
- **Editor summary for failed content jobs.** When draft or humanizer exhaust retries (article-shape or word-count), the pipeline now stores the last content (draft or humanized) and an `editorSummary` (jsonb) with 5–6 short, actionable items for the human editor. New `content_jobs.editor_summary` column; `src/lib/content/agents/editor-summary.ts` maps shape failures to plain-English fixes (e.g. "Keep list runs short…", "Add a short paragraph before each list section…"). Orchestrator persists draft on draft terminal failure and sets `editorSummary` on both draft failure and humanizer shape/word-count exhaustion. Run `npm run db:push` to add the column. UI can show `editorSummary.actionPoints` and edit content from `draft` or `humanized` per `contentForEditor`.
- **Content list pagination and dashboard counts.** Dashboard content list (`/dashboard/content`) shows 10 items per page with Previous/Next and “Showing X–Y of Z.” New `getDashboardContentCounts()` returns total/blog/pseo counts for stat cards. When the DB returns 0 items, the list falls back to file-based content from `content/blog` so the page is usable before `npm run content:sync`.
### Changed
- **Free-tier transcription mode → bridge model.** [`resolveFreeTierDesiredAccuracyMode`](src/lib/stt-quality.ts): **Speed** → `fast` (small), **Best quality** → `accurate` (large-v3), **Auto** → `accurate` only when duration and file size are within the same caps as worker force-fast (else `fast`). [`processTranscription`](src/app/actions/transcriptions.ts) always sets `accuracyMode` for free tier; [`retranscribeTranscription`](src/app/actions/transcriptions.ts) copies `sttQuality` from the source row. [`submitBridgeJob`](src/lib/ai/bridge-stt-client.ts) sets `routing.preferredMode` to match `accuracyMode`.
- **Deploy: optional STT bridge unit sync.** `DEPLOY_SYNC_STT_BRIDGE_UNIT=1` in [`scripts/deploy-wisprs.sh`](scripts/deploy-wisprs.sh) copies [`devops/configs/systemd/wisprs-stt-bridge.service.contabo-vmi3157334`](devops/configs/systemd/wisprs-stt-bridge.service.contabo-vmi3157334) to `/etc/systemd/system/wisprs-stt-bridge.service` and runs `daemon-reload`. Documented in [`devops/WISPRS_DEPLOY.md`](devops/WISPRS_DEPLOY.md) and [`.cursor/skills/wisprs-deploy/`](.cursor/skills/wisprs-deploy/).
- **Contabo STT bridge preset: `int8` not `int8_float16`.** [devops/configs/systemd/wisprs-stt-bridge.service.contabo-vmi3157334](devops/configs/systemd/wisprs-stt-bridge.service.contabo-vmi3157334) sets `BRIDGE_STT_WHISPER_COMPUTE_TYPE=int8`; on this CPU-only host (faster-whisper 1.2.1 / ctranslate2 4.7.x), `int8_float16` and `float16` fail at runtime. [devops/RECON_RESULTS.md](devops/RECON_RESULTS.md) updated. **On the server:** copy the unit (or edit `Environment=` to `int8`), then `daemon-reload` + `restart wisprs-stt-bridge`.
- **Accuracy copy (fact-check alignment).** `COPY_POLICY_ACCURACY` and site surfaces (marketing copy, FAQ, product FAQs, features data, home schema, footer, how-it-works, creators, llms.txt, default mode) use qualified wording (excellent accuracy on clear audio; varies by language/conditions) instead of fixed “99%” claims. [`project_docs/FEATURES_TRACKING.md`](project_docs/FEATURES_TRACKING.md) updated for the new policy.
- **Fix list shows exact lines/paragraphs that failed QA.** The content edit Fix list (from pipeline) now shows concrete excerpts for failures: long sentences, long/short paragraphs display the offending text (truncated to 120 chars) under each instruction. Article-shape validator returns `paragraphBlocks` and `sentences`; `buildShapeFindings()` maps failures to editor-facing findings with optional excerpts. Editor summary accepts `findings` and stores `actionPoints` as `{ message, excerpt? }[]`; the edit form renders each point with an optional quoted excerpt below. Legacy jobs with string-only action points still render correctly.
- **Content edit layout: editor full row, then Fix list and meta.** The content edit page now uses a two-row layout: the body (markdown) editor uses the full first row; the Fix list (from pipeline) and metadata cards (Title, Description, Primary keyword, etc.) are on the second row, with Fix list and meta side-by-side on large screens when editor summary is present.
- **Content edit: sidebar, layout, and action bar.** When opening the content edit page, the dashboard sidebar is collapsed by default to give the editor more horizontal space (new `setCollapsed` in sidebar context and `SidebarCollapseOnEdit` in the edit layout). Edit page container widened to `max-w-[1600px]`. Action bar: “← Content” stays left; “Open page,” “Revalidate + submit,” and “AI context” are grouped on the right; “Open page” uses the same bordered button style as the others.
- **Content list: Revalidate + submit and AI context moved to edit page.** The content list Actions column now only has Edit and Open page. “Revalidate + submit” and “AI context” live on the content edit page (`/dashboard/content/edit`) so they sit with the piece being edited.
- **Homepage pricing: Most Popular badge and Enterprise card.** “Most Popular” badge is rendered outside the Studio card so it is not clipped by `overflow-hidden`, uses a background-matching border, and the popular card has top padding so the badge sits clear. Enterprise card layout and copy match the pricing page (title, Custom + “Starting at $300/month,” tagline, Contact Sales, features); Enterprise spans the full bottom row (`md:col-span-3` on the grid item).
### Fixed
- **`sttQuality` missing from `transcriptions.metadata` after completion.** The worker’s first `processing` update and bridge/sync completion paths replaced the entire `metadata` JSON instead of merging, dropping `sttQuality`, `queuedAt`, and other keys set at upload/start. [`transcription-worker.ts`](src/lib/queue/transcription-worker.ts) now merges on processing start, synchronous completion, and generic failure; [`bridge-completion-poller.ts`](src/lib/queue/bridge-completion-poller.ts) merges existing metadata (passed from the poller) when applying bridge results. Chunked [`upload-worker.ts`](src/lib/queue/upload-worker.ts) persists `sttQuality` whenever the session includes it (including `auto`). **Post-deploy check:** `SELECT id, metadata->>'sttQuality', metadata->>'bridgeMode' FROM transcriptions WHERE status = 'completed' ORDER BY id DESC LIMIT 5;` — expect `sttQuality` alongside bridge fields for new jobs.
- **Transcriptions dashboard pagination.** `src/app/dashboard/transcriptions/page.tsx` now defaults to `10` rows per page and keeps the pagination controls visible (with “Showing X–Y of Z”) while clamping `currentPage` when filters/sorts change.
- **`sttQuality` missing on uploads created via `POST /api/upload`.** `src/app/api/upload/route.ts` now always persists `{ sttQuality }` in `transcriptions.metadata` (including `auto`), instead of only persisting when the preference was not `auto`, so `metadata->>'sttQuality'` is reliably present for analytics/debug and can be preserved through transcription completion.
- **Blog featured images: hero image and center icon.** Featured cards and the single post page now use `post.heroImage` when set (including `/images/blog/...`) instead of always showing the fallback logo. The gradient overlay with the center “failed loading” SVG icon was removed from both `BlogPostCard` and the blog `[slug]` page so the hero image displays without a placeholder icon.
- **Lint: 8 errors and 7 warnings.** Replaced internal `<a href="...">` with `<Link>` on the transcriptions page (no-html-link-for-pages). Escaped apostrophes with `'` in forgot-password, sales, sign-in, and checkout-success-celebration (no-unescaped-entities). Resolved exhaustive-deps in text-to-speech, wisprs success, multi-file-upload, sales-call-kit, transcription-status-tracker, and use-transcription-status; switched file-card thumbnail to next/image with blob/data fallback; `pnpm run lint` passes with no warnings or errors.
- **Build: jsdom default-stylesheet for server bundle.** Added `browser/default-stylesheet.css` (copy from jsdom) so the Next server bundle can resolve the path when isomorphic-dompurify (and thus jsdom) runs during page-data collection, fixing “Failed to collect page data for /podcast/[slug]”.
- **IndexNow 422 (key verification and URL scope).** Nightly IndexNow submissions were failing with HTTP 422 because (1) the key URL (`/api/indexnow-key`) was protected by auth, so Bing/Yandex could not read it, and (2) with the key under `/api/`, IndexNow Option 2 only accepts URLs under that path; we submit `/`, `/blog/...`, etc. Fix: key endpoint is now public; middleware rewrites `/{INDEXNOW_KEY}.txt` to the key handler (Option 1 root key) so all site URLs are valid; provider uses `keyLocation = https://host/{key}.txt` by default so no env change is required. Failed runs now store the API response body in `indexnow_runs.error` for easier debugging.
- **Health endpoint 401.** `GET /api/health` was behind auth and returned 401 for unauthenticated requests. It is now in the middleware public route list so load balancers and monitoring can hit it without auth.
- **Powered-by strip: remove border around Eleven Labs logo.** The Eleven Labs logo in `PoweredByStt` no longer has `ring-1 ring-white/15` so it matches the borderless OpenAI logo.
### Added
- **Admin-only Content CMS and content queue.** Content list (`/dashboard/content`), content edit (`/dashboard/content/edit`), and content queue (`/dashboard/content-queue`) are restricted to users with `users.role === 'admin'`. Single gate: `requireAdmin()` in `src/lib/auth/clerk.ts` (auth + role check). Optional local bootstrap via `ADMIN_EMAILS` (comma-separated emails) in `.env.local`; `getCurrentUser()` upgrades matching users to admin in the DB on next request. Guarded: content list/edit/queue pages (redirect to `/dashboard` when not admin), Server Actions in `content-cms.ts` and `content-admin.ts`, and content job APIs (`GET/POST /api/content/jobs`, approve/reject/retry, refresh-blogs, `GET /api/content/automation/[slug]`). API routes return 403 JSON when not admin. **Admin docs:** `docs/admin/` with README (overview, TOC), DESIGN.md (scope, request flow, failure behavior), STRUCTURE_AND_RBAC.md (role model, guarded surfaces, code map), and DEV_SETUP.md (ADMIN_EMAILS steps, security, optional DB admin). Cross-links from `docs/auth/README.md`, `docs/auth/QUICK_REFERENCE.md`, and `docs/guides/CONTENT_PUBLISHING_GUIDE.md`. No Clerk org roles; admin is app-DB only.
- **GPT 5.4 prompt guidance and content-agent audit.** New `docs/guides/llm-prompt-guidance.md` (condensed output contract, verbosity, tool use, verification, grounding, personality, completeness). `docs/PROMPTS_IN_USE.md` and `docs/guides/content-agent-context.md` link to it. New `docs/guides/content-agent-prompt-audit.md` audits brief, draft, seo-aeo, and humanizer against that guidance with aligns, gaps, and recommendations.
- **Article-shape validator and pipeline gates.** `src/lib/content/agents/article-shape.ts`: list-to-prose ratio, sentence/paragraph length, flow, and topic-coherence checks with configurable thresholds. Orchestrator runs article-shape validation after draft and humanizer; failed runs get retry with injected feedback (`articleShapeRetryFeedback`). Content dry-run reports article-shape metrics and pass/fail in trace.
- **Wisprs content audit skill.** Reusable skill at `.cursor/skills/wisprs-content-audit/` (SKILL.md + AUDIT_CHECKLIST.md) to audit blog and pSEO content before publish: combined pSEO, copy/GTM, SEO-GEO, and AI SEO checks with a structured pass/fail report. Use when auditing any content asset for quality, keywords, CTAs, and GEO readiness.
- **Content dry run and word-count enforcement.** `scripts/content-dry-run.ts` runs the full content pipeline (brief → draft → seo-aeo → humanizer) for a blog input; shared `src/lib/content/agents/word-count.ts` enforces minimum words per template (reference-guide, core-software, etc.) in draft, humanizer, and orchestrator. Post-humanizer checks for word count and truncated bullets. Agent tools: `our-docs` and `web-search` in `src/lib/content/agents/tools/`. Docs: `docs/PROMPTS_IN_USE.md`, `docs/guides/content-agent-prompts.md`; `scripts/list-llm-model-priorities.ts` for inspecting seeded LLM priorities.
- **Content agent LLM usage persisted to DB.** All AI SDK usage from the content pipeline (brief, draft, seo-aeo, humanizer) is now stored in `content_agent_llm_calls`: provider, model, input/output/cache tokens, cost estimate, latency, finish reason, step count, tool calls/results summary, and optional job/stage/attempt context. Telemetry layer normalizes usage (prompt/completion vs input/output), and persist errors are logged without failing the pipeline. Run `npm run db:push` (or apply migrations) to create the table.
- **Content agents v6 telemetry, structured outputs, and context docs.** Full input/output telemetry for all content pipeline agents (brief, draft, seo-aeo, humanizer): shared `src/lib/content/agents/telemetry.ts` with `estimateCost`, `getTokenCount`, and `trackContentAgentLLMCall` (feature: `content-agent`, agent name, optional input/output char counts). All four agents instrumented with success/failure logging and metrics. Orchestrator: brief validation with `contentBriefSchema.safeParse` before store (invalid brief → job error); optional draft structure check (heading + min word count); SEO scores clamped 0–100. Docs: `docs/guides/content-agent-context.md` (context injection per agent, file paths, provider/model selection). Optional: `keywordMetadata` on brief agent input; `logLLMCall`/`trackLLMUsage` accept optional context for feature tagging; transcript summary calls use `feature: 'transcript-summary'` so transcript vs content-agent usage can be filtered in logs/metrics.
- **Hybrid content platform for blog and pSEO pages.** Added a shared content system that loads markdown blog content from `content/blog` and programmatic SEO programs from `content/pseo-programs`, with template-aware generation in `src/lib/content/`, a content ops dashboard at `/dashboard/content`, and an automation payload endpoint for AI workflows. The first live pSEO template families now include core software, podcast workflows, direct comparisons, alternatives lists, use cases, and free-tool pages.
- **Wisprs publishing system docs.** Added a long-form internal publishing SOP in `docs/guides/CONTENT_PUBLISHING_GUIDE.md`, template playbooks under `docs/guides/templates/`, and reusable working assets under `docs/guides/publishing-assets/` including the new-page kickoff template, brief template, prompt templates, and publish/refresh checklists.
### Fixed
- **Content job retry 500 (queue not initialized).** The content job retry API (`POST /api/content/jobs/[id]/retry`) now calls `initializeQueue().catch(() => {})` before `getQueue()`, so retry no longer returns 500 when pg-boss has not been initialized (e.g. first hit after `next dev`). Matches the pattern used by the events track route.
- **Retry button no visible response.** The content queue review page Retry button now shows a loading state ("Retrying…") and disables while the request is in flight; on failure it shows an alert with the API error or status code; network errors are caught and surfaced so the user always gets feedback.
- **Content Ops dashboard shows 0 when content_items empty.** `getAllContentDocuments()` in `src/lib/content/queries.ts` now falls back to `loadPublishedBlogPostsFromFiles()` when the DB returns no items or throws, so the dashboard reflects file-based blog count (e.g. 15 articles) when the DB is empty or unsynced.
- **Retry button on queued jobs.** Retry is now shown only for in-progress or failed states (briefing, drafting, seo-check, humanizing, failed), not for queued jobs.
- **Blog related posts null crash.** When content DB queries failed and fallback returned null, `relatedPosts.length` threw. Blog post page now uses `(relatedPosts ?? []).length` and `(relatedPosts ?? []).map(...)` so the related-posts section renders safely when DB is unavailable or returns null.
- **Blog FAQ Q/A display.** Markdown renderer now treats FAQ blocks as both (1) `### Question?` with the answer in the next section and (2) `### Question?\nAnswer` in a single section (single newline), rendering both as distinct **Q:** / **A:** pairs with clear styling so FAQs no longer appear as plain H3 titles.
- **Blog hero image 404 crash.** Hero images under `/images/blog/` now use a safe fallback in `BlogPostCard` and blog `[slug]` page so missing assets never trigger 404s or malloc/crash; 14 placeholder images added in `public/images/blog/`.
- **Content queries when DB schema is missing or fails.** Blog list, post by slug, filters, related documents, and adjacent posts now have file-based fallbacks in `src/lib/content/queries.ts`, `db-queries.ts`, and `sync.ts` so the blog and related-docs sections work when `content_items` is missing or lacks expected columns (e.g. after a fresh clone). `getRelatedDocumentsFromDb` returns `null` on error instead of throwing; `console.error` in catch blocks was removed so failed DB lookups do not trigger the Next.js error overlay in development.
### Changed
- **Content list page visual pass and row separators.** `/dashboard/content` now uses a table layout with a clear "Content" header strip, semantic columns (Title & path, Keyword, Status, Source, Actions), row hover state, and explicit horizontal borders between rows (`border-b border-white/20`) for clearer separation. Intro block tightened (section label, "Content" title, short subtitle); stat cards and action buttons use consistent card and button styles.
- **Content edit: editor center stage, metadata in sidebar.** Content edit page layout widened to `max-w-7xl`. Edit form uses a two-column layout: main area is the body (markdown) editor only with larger height (480px) and optional "Editing: {title}" context; right sidebar (sticky) holds Title, Description/Summary, Primary keyword, Category, Tags, Hero image URL (blog), secondary keywords/audience (pseo), and Save/Cancel. On narrow viewports the sidebar stacks below the editor.
- **Content queue review on dedicated page.** List at `/dashboard/content-queue` shows jobs only; review (rich markdown preview, Approve/Reject/Retry) moved to `/dashboard/content-queue/[id]`. Preview uses the same markdown renderer as the blog for WYSIWYG-style display. Back link returns to queue (optionally with same status filter).
- **Content queue list UX.** Jobs list uses two cards per row (`md:grid-cols-2`); each job card is compact (vertical stack, `p-3`, `gap-2`) with score chips and pipe separators for SEO/AEO/Voice, "View →" link affordance, and hover/focus on the link area. Stats cards and panel boundaries use stronger borders/backgrounds for separation.
- **Refresh-blogs API and script support skip.** `POST /api/content/jobs/refresh-blogs` accepts optional `?skip=N`; `scripts/refresh-blogs.ts` accepts a second argument `[skip]` (e.g. `npx tsx scripts/refresh-blogs.ts 10 5` for the next 10 after the first 5).
- **Content agents: writing-style rules and GPT 5.4 alignment.** Draft: added `<writing_style>` block (sentence/paragraph/word-level and copywriting principles) and article-shape retry feedback; explicit list budget (15–20 items total, max 25, <20% list lines). Humanizer: writing-style subsection in `voice-rubric.ts` plus article-shape retry and list discipline (preserve or reduce list count, never add). Brief: one line that the resulting draft will follow Wisprs writing standards. SEO-AEO: one line to preserve or improve writing style when optimizing. Wisprs content audit skill: AUDIT_CHECKLIST.md includes article-shape and structure checks.
- **Article-shape thresholds tightened.** `article-shape.ts`: listItemRatioMax 0.20, maxConsecutiveListItemsMax 4, maxListDominatedSections 0, minParagraphCount 10; articleShapeFeedback message strengthened with explicit list-count cap and prose-conversion guidance for retries.
- **Content agents: template key threading and GEO freshness.** Brief step derives `effectiveTemplateKey` from `brief.pageIdentity.templateKey` and threads it through SEO-AEO, humanizer, and min-words logic in the dry run. SEO-AEO system prompt includes “Updated [Month] [Year]” GEO instruction. Orchestrator and humanizer use shared word-count enforcement; post-humanizer truncated-bullet scan and trace warning in content-dry-run.
- **Blog post and ops.** `content/blog/5-tips-for-better-transcription-accuracy.md` updated (body and frontmatter). `.gitignore` excludes `.content-dry-run/`. Deploy and apply-migrations scripts updated; `scripts/seed-llm-models.ts` expanded; `docs/guides/content-agent-context.md` and devops deploy doc tweaks.
- **SEO docs now match the live content architecture.** Refreshed `project_docs/SYSTEM_OVERVIEW.md`, `project_docs/90DAY_PSEO_MASTER_PLAN.md`, `project_docs/KEYWORD_CLUSTERS.md`, `docs/SEO_STRATEGY.md`, and `docs/seo/KEYWORD_MASTER_CATALOG.md` so the source-of-truth docs reflect the actual route families, template keys, structured-data model, and hybrid blog+pSEO workflow. Older planning docs were marked as historical where appropriate.
- **Public discovery surfaces follow the new route families.** `src/lib/content/seo.ts`, `src/lib/seo/public-urls.ts`, `src/app/sitemap.ts`, and `src/app/llms.txt/route.ts` now understand the expanded route map and emit metadata, breadcrumbs, and public URLs consistently across blog, core, podcast, alternatives, use-case, and tools content.
### Performance
- **Homepage mobile performance and SEO/GEO improvements.** Reduced the homepage hero’s mobile LCP cost by replacing the oversized PNG poster with a lightweight WebP poster (`public/videos/promo-hero-poster.webp`), tightening responsive image sizing, and preventing the promo video from loading on mobile, reduced-data, or reduced-motion contexts. Deferred GTM/GA4 bootstrapping until after the page load event so analytics no longer compete with first paint. Added a homepage canonical URL plus `SoftwareApplication` and `FAQPage` schema, and reused the shared FAQ source so on-page content and structured data stay aligned.
### Performance
- **Mobile LCP and critical-path improvements.** Four targeted changes to improve mobile PageSpeed (baseline LCP 21.3s): (1) **Hero image-first LCP:** `product-mockup.tsx` now renders a `next/image` poster with `priority` and responsive `sizes` as the LCP element; the promo video gets `preload="none"` and is loaded + played only after the `window load` event fires, so mobile paints the poster immediately without waiting for video metadata. A crossfade makes the transition seamless. (2) **Font preload trim:** `src/lib/fonts.ts` — only Satoshi (body font) and Fredoka (hero heading + nav wordmark) are preloaded; the other 6 families (Quicksand, Doppio One, Mohave, Andika, Cal Sans, Space Grotesk) are now `preload: false` to eliminate unnecessary `<link rel="preload">` requests before first paint. (3) **Hero inline style removed:** `hero-section.tsx` inline `<style>` block replaced with equivalent responsive Tailwind classes (`sm:grid sm:grid-cols-[42fr_58fr]`, `text-center sm:text-left`) eliminating a render-blocking style injection. (4) **Analytics deferred:** `AnalyticsInit` and `TrackPageView` both gate on `window load` before bootstrapping Mixpanel/PostHog or sending page-view events, so analytics no longer compete with the LCP element on the critical render path.
- **Homepage recovery follow-up: marketing shell, logo visibility, and fresh audits.** Follow-up pass after the root-layout/Clerk crash: marketing pages now stay server-first while Clerk is scoped back to authenticated and pricing/auth layouts, the hero uses the real `public/videos/image.png` poster with the deferred `ProductMockupVideo` enhancer, and the powered-by strip uses the exact `public/logos/elevenlabs.webp` asset with corrected sizing so the logo is visible instead of collapsing into a blank chip. Fresh Lighthouse reruns showed the live site materially better than local dev: production scored about 75 mobile / 87 desktop, with the main remaining homepage bottlenecks now concentrated in mobile LCP and unused JavaScript rather than render-blocking third-party work or layout shift.
### Changed
- **Landing hero: promo video and two-column layout.** (1) **Video:** Replaced static dashboard mockup with optimized promo video: transcoded source (`promo-clean.mp4`) to 1080p 30fps MP4, WebM, and poster via `scripts/transcode-hero-video.sh`; hero uses `<video>` with poster, preload=metadata, muted, autoPlay, playsInline, loop and WebM + MP4 sources in `product-mockup.tsx`. (2) **Layout:** Hero is two columns on desktop (CSS grid 42fr / 58fr at 640px+); text left, video card right; 16:9 aspect-video so landscape video fits the frame. (3) **UI:** Removed FeatureHighlight chat bubbles and Privacy First badge; reduced frame border and padding between columns; heading sizes tuned (lg/xl) so "AI transcription" stays on one line. Source video and script documented in SETUP.md; `promo-clean.mp4` / `promo.mp4` gitignored.
### Added
- **Verify transcript artifacts on prod:** Runbook in `devops/WISPRS_DEPLOY.md`: run `scripts/verify-transcript-artifacts.ts [transcriptionId]` on the server (or with prod `DATABASE_URL`) to confirm artifact rows and `created_at`/`updated_at`; grep `journalctl -u wisprs` for `[artifacts-timing]` for processing-time metrics.
- **ELEVENLABS_API_KEY in prod env sync:** `scripts/sync-wisprs-prod-env.sh` now includes `ELEVENLABS_API_KEY` in the list of vars copied from `.env.local` to server `.env.production`; run the script and restart wisprs to fix 401 Invalid API key on prod.
- **AI artifacts: storage verification, timing, pre-warm, and parallel preload.** (1) **Verify script:** `scripts/verify-transcript-artifacts.ts` — run with `DATABASE_URL=... npx tsx scripts/verify-transcript-artifacts.ts [transcriptionId]` to confirm `transcript_artifacts` rows on prod (or list recent transcriptions by artifact count). (2) **Timing logs:** Extraction server actions (topics, chapters, action-points, minutes, summary, speakers, sales-call-kit) now log `[artifacts-timing]` with `cacheMs`, `chunksMs`, `llmMs`, `persistMs`, `totalMs` (or `cacheHit=true`) so latency can be attributed to cache, chunking, LLM, or persist. (3) **Pre-warm:** After a transcription completes (transcription worker, bridge completion poller, ElevenLabs webhook), we fire-and-forget generate summary, topics, and action points in parallel so the first tab open often hits cache. (4) **Parallel preload:** Transcript detail page preloads summary, topics, and action points on mount when the transcript is completed so switching to those tabs is often instant. See plan: AI-Extracted Data Storage and Latency.
- **Mobile responsiveness overhaul.** Marketing and dashboard are fully usable across devices. (1) **Marketing:** Mobile hamburger menu and slide-in drawer (Features, Use Cases, Pricing, FAQs, Blog, Sign in, CTA); hero and CTAs with 44px touch targets and stacked layout on small screens; features, pricing, FAQ, how-it-works, and footer with responsive grids and tap-friendly links. (2) **Dashboard:** Transcript detail layout stacks main content and sidebar on mobile (`flex-col lg:flex-row`); search/replace and tab pills wrap; dashboard pages use consistent `px-4 sm:px-6 lg:px-8` and `min-w-0` to prevent overflow. (3) **Design skills:** Installed and applied guidelines from web-design-guidelines, frontend-design, frontend-design-system, tailwind-design-system, and responsive-design. Public share viewer and settings/billing/translate/TTS pages audited for mobile.
- **Branding: logos, favicon, and Open Graph image.** (1) **Logo:** Wisprs W-ribbon + waveform lockup at `public/logos/wisprs-logo.png`; used in nav, footer, dashboard sidebar, share page, and email layout. (2) **Favicon:** `wisprs-favicon.png` (W-ribbon on dark) used for `public/favicon.ico`, `favicon-16x16.png`, and `favicon-32x32.png`; layout icons point to these. (3) **OG image:** `src/app/opengraph-image.png` (logo + wordmark + tagline “Transcribe Audio at the speed of AI.”); root layout `openGraph` and `twitter` metadata set with absolute image URL, title, description, and site URL. (4) **Logo/wordmark alignment:** Logo given a small downward nudge (`mt-1`) and gap between logo and wordmark reduced (`gap-1` / `space-x-1`) in nav, footer, sidebar, and share page for consistent lockup. Design concept and image-gen prompts in `project_docs/LOGO_CONCEPT.md`.
### Fixed
- **Hero video never playing on desktop.** The promo video only loaded when `(prefers-reduced-data: no-preference)` matched; that media query has poor support, so most desktops never got the video. `ProductMockupVideo` now only skips when `(prefers-reduced-motion: reduce)` or `(prefers-reduced-data: reduce)` matches, so the video plays on desktop by default and still respects accessibility preferences.
- **Homepage client-side crash and missing ElevenLabs logo:** Removed the broken global Clerk-provider pattern from the root marketing shell, restored route-scoped Clerk layouts (`dashboard`, `pricing`, `features`, auth flows), added server-rendered `PublicPricingCards` to keep marketing pages free of Clerk dependencies, and swapped the powered-by mark to the exact `public/logos/elevenlabs.webp` asset with a larger chip so the logo is visible on the dark homepage.
- **Action Points "response did not match schema":** Relaxed Zod schema in `src/lib/ai/action-points.ts`: `assignee`, `dueDate`, and `priority` use `.nullish()` so LLM responses with `null` pass; `completed` allows nullish; mapping normalizes nulls to `undefined` before persist. Added `[action-points] schema validation failed` logging (and `cause`/`issues`) in catch for prod debugging.
- **Minutes tab "response did not match schema":** Relaxed schema in `src/app/actions/transcript/minutes.ts`: `ActionItemSchema` owner/dueDate and `MinutesSchema` date/duration/arrays use `.nullish()` or `.nullish().default([])`; result is normalized before persist (nulls → defaults). Added `[minutes] schema validation failed` logging in catch.
- **Paid users seeing "free tier" during transcription:** Stream route `getStageMessage` now uses current user role: for bridge stages (`queued_free_tier`, `bridge_queued`, `bridge_processing`, `bridge_finalizing`), users with paid role (pro, studio, agency, enterprise, admin) see "Queued…" / "Transcribing with pro models…" / "Finalizing transcript…" instead of "free tier" copy. When the job actually uses ElevenLabs, existing "Transcribing with ElevenLabs..." is unchanged.
- **Build: Mixpanel and webpack.** (1) **Mixpanel:** Added `mixpanel` to `serverExternalPackages` and server webpack externals so the server-side events provider resolves at runtime instead of being bundled; eliminates "Can't resolve 'mixpanel'" during build. (2) **retry-handler:** Removed extra closing brace in `retry-handler.ts` that caused "Return statement is not allowed here" and broke compilation. (3) Clean `.next` before build avoids stale webpack chunk (e.g. missing `1073.js`) errors. `pnpm run build` now completes successfully.
- **Queue not initialized on local:** Event tracking (`POST /api/events/track`, `trackSignIn`) no longer throws "Queue not initialized. Call initializeQueue() first." when running only `next dev`. The events track route and the trackSignIn server action now call `initializeQueue()` before `trackEvent()` so the pg-boss queue is lazily initialized on first use when the DB is available; init errors are caught so requests still return 200 if the queue cannot start.
- **Settings (and dashboard) Server Component error:** `app/actions/events.ts` is a `'use server'` file and was exporting the string constant `SIGN_IN_TRACKED_KEY`; Next.js allows only async function exports from such files. Constant moved to `track-sign-in.tsx`, so the server action file only exports `trackSignIn`.
- **Client-side crash on dashboard/transcribe (and all pages):** Monitoring module no longer imports server `env`; it reads `process.env.NEXT_PUBLIC_MIXPANEL_TOKEN` (and optional PostHog vars) directly in the browser so the full Zod env schema is never run client-side (it requires server-only vars and would throw).
### Added
- **Slack on production:** Docs for getting event and IndexNow Slack notifications in prod: add `SLACK_WEBHOOK_URL` to `.env.local`, run `./scripts/sync-wisprs-prod-env.sh`, restart wisprs. New "Slack on production" subsection in `docs/events/ENV_SETUP.md`; deploy runbook and production env template updated with sync-script reference.
- **Mixpanel page views and sign_in events:** (1) **Page views/autocapture:** Root layout now mounts `AnalyticsInit` client component so `@/lib/monitoring/unified` loads on every page; Mixpanel init (and session replay) runs site-wide instead of only when transcript/AI code is used. (2) **Sign-in tracking:** Server action `trackSignIn()` and dashboard-only client `TrackSignIn` send one `sign_in` event per session to the pipeline (Mixpanel, Slack, etc.); previously only `sign_up` (Clerk webhook) was sent.
- **Slack: all events and rich messages:** All 10 event types are sent to Slack (allowlist removed). Per-event formatting: emoji, bold title, ordered key props, and sales/debug footer (e.g. "Sales: new paid customer – Pro", "Debug: dashboard link + error"). Optional `reason` and `debug: { message?, error?, code? }` on events for Slack and logging only; user-facing UI and Resend emails stay generic. `notifyTranscriptionComplete(..., options?: { reason?, debug? })`; stuck cleanup, transcription worker, bridge poller, and `markTranscriptionFailed` pass failure reason/error so devs see it in Slack. Docs: `docs/events/TRACKING_PLAN.md` Slack section and "errors in Slack only, never in prod UI".
- **Production env sync script:** `scripts/sync-wisprs-prod-env.sh` reads `.env.local` with grep/sed and appends/updates event and analytics vars on the server `.env.production` (MIXPANEL_TOKEN, NEXT_PUBLIC_MIXPANEL_TOKEN, INDEXNOW_*, NEXT_PUBLIC_APP_URL, NEXT_PUBLIC_GA4_MEASUREMENT_ID, etc.). Client Mixpanel fix: `NEXT_PUBLIC_MIXPANEL_TOKEN` added so browser autocapture/session replay works (server still uses `MIXPANEL_TOKEN`).
- **Unified event pipeline:** Single `trackEvent()` entry point fans out to Resend, Slack, Mixpanel, GA4, IndexNow, and GSC via pg-boss `event` queue and `event-worker`. Event types: `transcription_started`, `transcription_ended`, `transcription_failed`, `sign_up`, `sign_in`, `checkout_started`, `checkout_completed`, `subscription_started`, `subscription_ended`. Notifications (queued/complete/failure) use `trackEvent()` with `emailRecipient` for Resend; call sites added in checkout create/success, Polar webhook, Clerk webhook. Docs: `docs/events/TRACKING_PLAN.md`, `docs/events/ENV_SETUP.md` (incl. testing Mixpanel and IndexNow); `.env.example` and ENV_SETUP updated for all provider vars.
- **IndexNow:** Optional `INDEXNOW_KEY` and `INDEXNOW_KEY_LOCATION`; key served at `GET /api/indexnow-key`. Events with `urls` submit to api.indexnow.org. Dev-only `GET /api/dev/indexnow-test?url=...` to test submission locally.
- **Mixpanel:** Server-side events via pipeline; client `mixpanel.init()` with autocapture and session replay (100%) when `MIXPANEL_TOKEN` set in env.
- **GA4 client-side gtag:** Optional `NEXT_PUBLIC_GA4_MEASUREMENT_ID`; when set, root layout injects gtag.js for page views. Server-side Measurement Protocol unchanged (`GA4_MEASUREMENT_ID` + `GA4_API_SECRET`).
- **Bing Webmaster verification:** `public/BingSiteAuth.xml` served at `/BingSiteAuth.xml` for site ownership verification in Bing Webmaster Tools.
- **SEO and IndexNow:** Dynamic sitemap, robots.txt, JSON-LD, llms.txt; shared public URL list; nightly IndexNow job (3 AM) with dry run and dev endpoint. **Slack:** Each run posts a summary to Slack when `SLACK_WEBHOOK_URL` is set. **Logging:** IndexNow runs use the unified logger (`feature: 'indexnow'`) and are persisted in the `indexnow_runs` table for audit and tracking.
- **Stuck transcription kill and monitoring:** (1) **Automated cleanup:** Scheduled job `stuck-transcription-cleanup` runs every 30 minutes and marks transcriptions in `processing` longer than `STUCK_TRANSCRIPTION_THRESHOLD_HOURS` (default 4) as failed, with a clear error so users can use Transcribe Afresh. (2) **Manual cancel:** `markTranscriptionFailed` server action (owner-only, pending/processing only) and Cancel button on transcription detail (linked-processing and new-processing) and on transcribe page queue. (3) **Idempotency:** Bridge completion poller and transcription worker only apply terminal state (completed/failed) when row is still `processing`, so user cancel or stuck cleanup is not overwritten by a late bridge response.
- **ElevenLabs webhook secret verification:** Optional `ELEVENLABS_WEBHOOK_SECRET` (wsec_...) for verifying incoming async STT webhooks. When set, the handler validates the `elevenlabs-signature` header (HMAC-SHA256 of `timestamp.rawBody`), rejects missing or invalid signatures, and enforces a 30-minute replay window. Production template and `docs/guides/WEBHOOK_SETUP.md` document the secret; production URL set to `https://wisprs.co/api/webhooks/elevenlabs`.
- **ElevenLabs API compliance:** Optional `ELEVENLABS_STT_MODEL_ID` (scribe_v1 or scribe_v2); STT uses it with fallback to scribe_v1. Breaking-changes policy comments in STT/TTS response parsing (ignore unknown fields). Security subsection in WEBHOOK_SETUP (service accounts, key server-side only, voice permissions); `docs/integrations/ELEVENLABS.md` for STT model, enable_logging reference, and webhook/security links. TTS type fix: removed duplicate `permission_on_resource` on `ElevenLabsVoice`.
- **Troubleshooting: Transcription email not received:** `docs/guides/TROUBLESHOOTING.md` section "Transcription Email Not Received (Queued or Complete)" with flow, causes (Resend not configured, no user email, Resend error, worker not running), and verification steps; `docs/features/NOTIFICATIONS.md` links to it.
- **Unified notification service (pg-boss + Resend):** Notifications (email, in-app, webhook) now use **pg-boss** for durable delivery and retries instead of the in-memory queue. (1) **`sendNotification()`** enqueues to `QUEUE_NAMES.NOTIFICATION`; a **notification worker** (`src/lib/queue/notification-worker.ts`) processes each job and calls `sendNotificationsToProviders()`. (2) **Job queued:** When a transcription is enqueued (`processTranscription`), users receive an email and in-app event ("Transcription queued"); completion and failure continue to trigger "Transcription Complete" / "Transcription Failed" emails. (3) **Resend idempotency:** All Resend sends use a deterministic `idempotencyKey` (`wisprs-notif/${eventKind}/${transcriptionId}/...`) so pg-boss retries do not send duplicate emails. (4) **Event kinds:** `eventKind: 'job_queued'` and `eventKind: 'job_completed'` in notification payload for templates and future in-app UI. (5) **Docs:** Production env template and `docs/payments/ENV_SETUP.md` note verified domain for Resend; queue docs updated with `notification` queue. Plan: unified notification service (pg-boss, job queued, Resend compliance).
- **Free-Tier STT Balance Plan (async-first model):** Scalable free-tier STT operating model to protect server resources while preserving PLG. (1) **Stream polling:** SSE status poll interval increased from 500ms to 3s; stream session can stay open up to 60 minutes (no 10-minute cutoff); timeout message tells users they can leave and will be notified. (2) **Async completion channel:** Bridge completion poller queue (`bridge-completion-poller`) runs every minute; free-tier worker submits jobs to the bridge and returns immediately; poller applies completion/failure to DB and sends email/in-app notifications so completion is independent of browser session. (3) **Free-tier hard caps:** Per-file max duration (`FREE_TIER_MAX_FILE_DURATION_MINUTES`, default 30) with clear rejection copy; admission uses audio-minutes backlog and `FREE_TIER_AUDIO_MINUTES_PER_MINUTE` for estimated wait. (4) **Notification-first UX:** Stage messages and default copy updated so users know they can leave the page and will be notified when ready; completion email copy reinforces this. (5) **Docs:** Queue README links fixed (MANAGEMENT/ARCHITECTURE/SETUP → QUEUE_MANAGEMENT/QUEUE_ARCHITECTURE/QUEUE_SETUP); free-tier async model and `bridge-completion-poller` documented in `docs/queue/README.md`, `docs/queue/QUEUE_MANAGEMENT.md`, and `docs/setup/STT_PROVIDERS_AND_SETUP.md`. Plan reference: `.cursor/plans/free-tier_stt_balance_plan_bd15dfd8.plan.md`.
- **Speed vs Quality upload option**: At upload time users can choose **Auto** (recommended), **Speed**, or **Best quality** for transcription. Applies to free-tier self-hosted STT only: preference is stored in `transcriptions.metadata.sttQuality` / upload session metadata and passed to the bridge as `accuracyMode` (fast vs accurate). Simple upload (`POST /api/upload`) and chunked upload (init + upload-worker) both support the control; no backend or model names shown in the UI. See `docs/setup/STT_PROVIDERS_AND_SETUP.md`.
- **Dual-backend STT bridge (spike)**: Bridge supports an engine abstraction with **faster-whisper** as default and optional **NVIDIA ParaKeet TDT 0.6B v3**. Optional request hint (`request.engine` / `routing.preferredEngine`); env-driven policy (`BRIDGE_STT_DEFAULT_ENGINE`, `BRIDGE_STT_ENABLE_PARAKEET`, `BRIDGE_STT_ENGINE_POLICY`, etc.). Result metadata includes `bridgeEngine`, `bridgeModel`, `bridgeMode`, `bridgeDevice`, `bridgeRoutingReason`. Doc: Faster Whisper vs ParaKeet comparison in `docs/features/STT_BRIDGE_SELF_HOSTED.md`; setup and env in `docs/setup/STT_PROVIDERS_AND_SETUP.md` and `stt-bridge/README.md`.
- **LLM pipeline (OpenAI default mode)**: Seed and config aligned with docs pipeline: gpt-5-mini (chat, translation, extraction cost), gpt-5 (summary, Sales Call Kit, extraction quality), gpt-5-nano (locale detection). Seeded gpt-5-mini, gpt-5, gpt-5-nano; OpenAI preferred when no provider preference; translation and extraction no longer force Anthropic. See `docs/LLM_PRICING_AND_RECOMMENDATIONS_2026.md` and `docs/LLM_MODELS_AUDIT.md`.
- **Transcription processing time**: `transcriptions.processing_time_ms` column and index for query/sort/aggregate; worker sets it from bridge/provider metadata on completion (no backfill).
- **Live status/duration without reload**: Status stream sends `duration` and `processingTimeMs`; `TranscriptionDetailLive` client wrapper holds live transcription state and passes `onTranscriptionChange` from the tracker so File Information (Status, Duration) updates from the stream without page reload.
- **Troubleshooting**: "Transcription Failed: [Errno 111] Connection refused" in `docs/guides/TROUBLESHOOTING.md` with cause (bridge fetching audio URL) and fix (production `NEXT_PUBLIC_APP_URL=https://wisprs.co`).
- **Deploy runbook**: Nginx proxy buffers (post-deploy) and Wisprs rate-limit zones in `devops/WISPRS_DEPLOY.md`; production env template includes `BRIDGE_STT_URL`, `BRIDGE_STT_FILE_SECRET`, and note for `NEXT_PUBLIC_APP_URL`.
- **Transcript artifacts**: Added durable `transcript_artifacts` storage for transcript-derived AI outputs (`summary`, `chapters`, `action-points`, `minutes`, `topics`, `speakers`, `sales-call-kit`) with unique `(transcription_id, artifact_type)` upserts and DB relations.
- **Pricing and subscription behavior (Polar):** (1) **Annual checkout** — Pricing page toggle sends `interval: 'annual'`; checkout API uses annual product IDs (`POLAR_PRODUCT_ID_*_ANNUAL`); sync and limits treat annual subscriptions the same as monthly by plan. (2) **Trials** — Polar trials (e.g. 3d monthly, 7d annual) honored; we persist `polarStatus` and `trialEnd` in `subscriptions.metadata` and show "Your trial ends on …" on dashboard pricing when trialing. (3) **Duplicate-purchase prevention** — `POST /api/checkout/create` returns 409 `ALREADY_SUBSCRIBED` when the user has an active subscription, with optional `manageUrl` from `NEXT_PUBLIC_POLAR_CUSTOMER_PORTAL_URL`; pricing UI shows a dismissible banner and "Manage subscription" link. (4) **Success-page pending UX** — When Polar has not yet attached a subscription ID, verify-checkout returns `code: SUBSCRIPTION_PENDING`; success page shows "Setting up your subscription…" with Refresh and Go to Dashboard and one automatic retry. See `docs/payments/POLAR_COMPLIANCE_AND_SUBSCRIPTION_AUDIT.md` and `docs/payments/POLAR_SETUP_PRODUCTS_AND_WEBHOOKS.md`.
- **Legal and info pages:** Terms, Privacy, Cookie Policy, Data Protection, About, Changelog, Stats, and Status pages now have real content. Legal content in `src/content/legal/` (draft, aligned with founding docs and product: Clerk, Polar, Resend, ElevenLabs; no training on user content; Enterprise SOC2/GDPR/HIPAA); About from founding bible; Changelog reads repo `CHANGELOG.md`; Stats and Status with honest copy (no unsubstantiated metrics).
### Security
- **Redacted committed Redis password:** Removed hardcoded Redis password from devops docs (OPERATIONS.md, CONFIGURATION.md, DEPLOYMENT.md, env.production.template); replaced with `YOUR_REDIS_PASSWORD` / “set in .env.production”. **If that password was in use on winter-exodus, rotate it** (redis-cli `CONFIG SET requirepass newpassword` and update .env.production).
### Changed
- **Homepage hero copy:** Bullets reordered and updated in `marketing-copy.ts`: (1) Transcribe audio and video in 100+ languages; (2) Extract AI summaries, chapters, topics, and action items; (3) Export to TXT, SRT, VTT, MD, DOCX, or JSON; (4) Start for free. No credit card required. Tagline unchanged (99% accuracy and speaker recognition). Removed redundant "on most content" from hero and accuracy claims site-wide earlier in session.
- **Codebase deslop (whole-codebase cleanup):** Removed AI-generated noise: (1) **Comments** — Dropped ~120+ narrating comments and JSX section labels (e.g. `// Create`, `// Handle`, `{/* Step Circle */}`) across components (how-it-works-steps, hero-section, features-section, product-faqs, layout, navigation) and lib (external-chat, middleware, health route, languages-countries, clerk webhook, context-manager, validate-share, realtime-transcription, translation-interface, dashboard-sidebar-provider, share-transcript-modal). (2) **Types** — Replaced `as any` with proper typing: `getErrorStatus()` in error-handler/retry-handler; metadata segments typed via `Record<string, unknown>` + narrow in transcript-tabs and public-share-viewer; ShareSettingsBody and `Synthesis` cast in share-transcript-modal and text-to-speech page. (3) **Try/catch** — Removed defensive try/catch in storage upload (non-throwing); simplified user/workspace lookup in tts actions; documented fail-open in action-points/chat/summary routes; replaced console-only catches with short comments in auth middleware, events/index, queue-health, health/queues. Behavior unchanged; edits comments and types only.
- **Nav and footer CTAs:** Nav link "FAQ" renamed to "FAQs" (nav + footer). Nav primary CTA uses `NAV_CTAS` from `marketing-copy.ts`: "Log in" and "Transcribe For Free →" (matches hero); consistent button size (h-10, primary min-w-[180px]).
- **Features page card footers:** Feature cards on `/features` use flex layout so "Learn more →" is always pinned to the bottom of each card; grid item wrapper has `h-full flex flex-col`, card and CardHeader use `flex-1 min-h-0`, footer link has `mt-auto` for symmetrical footers across rows.
- **Homepage "Powered by" strip:** Centered below hero; shows OpenAI and ElevenLabs logos only (no "OpenAI Whisper" / "ElevenLabs Scribe" text); slash separator; no gray background on logo containers; ElevenLabs logo sized to compensate for asset inset; uses `public/logos/openai.svg` and `public/logos/elevenlabs.webp`.
- **Homepage hero and copy:** H1 "AI transcription software that gets it right."; short tagline "Speech to text with 99% accuracy on most content and speaker recognition."; pain/outcome-led bullets (99% accuracy, transcribe audio/video 100+ languages, start for free) from marketing-copy. Hero layout tightened so CTAs sit above the fold; trust line "Trusted by [avatars] creators and teams"; risk line "No credit card required. No commitment. Cancel anytime."; partner logos removed from hero. Blurred gradient separator added between hero and How it works.
- **Features tracking and STT copy (Whisper + ElevenLabs Scribe):** Added `project_docs/FEATURES_TRACKING.md` as single source for implemented features (STT engines, plan ladder, copy/SEO guidelines). Copy no longer implies only OpenAI Whisper: accuracy feature and FAQ answers now mention self-hosted Whisper-based models (free) and ElevenLabs Scribe (paid, scribe_v1/v2). Updated `src/lib/features-data.ts` (accuracy short/long/technicalDetails), `src/components/faq-section.tsx`, and `src/lib/faqs.ts`. Linked FEATURES_TRACKING from `docs/SOURCE_OF_TRUTH.md`.
- **Features, use-cases, pricing, FAQ copy upgrade:** Applied same copy policy and GTM keyword-led copy to /features, /use-cases, /pricing, /faq. marketing-copy.ts: features hero/meta use "99% accuracy on most content"; useCases hero keyword-led ("Speech to text for creators, teams, sales & enterprise"), subhead and new useCaseCards (Sales, Creators, Teams, Enterprise) with keyword-led descriptions; pricing hero subhead "Speech to text plans from free to team. 60 minutes free; then Pro, Studio, or Agency"; faq hero subhead "Transcription, pricing, security, and formats." Use-cases page now consumes copy.useCaseCards from module. features-data.ts: accuracy feature uses qualified accuracy wording. Pricing page: FAQ section H2/subhead keyword-led; exceed-limits answer tightened. faqs.ts: accuracy and exceed-limits answers aligned with policy. **Wisprs copywriter skill:** Added `.cursor/skills/wisprs-copywriter/SKILL.md` documenting the full iron-clad flow (brief, Seven Sweeps, GTM anti-slop, copy policy, implement in marketing-copy, quality check) for future copy work.
- **Homepage copy audit (ruthless pass):** Centralized trust and accuracy policy in `src/lib/marketing-copy.ts` (COPY_POLICY_TRUST_LINE, COPY_POLICY_FINAL_CTA, COPY_POLICY_ACCURACY). Hero: removed unsubstantiated "100k+"/"99,995" users; trust line "Trusted by creators and teams"; qualified "99% accuracy on most content"; added risk reducer "Free tier, no commitment. Cancel anytime." Features: H2 "Speech to text built for creators and teams"; sharper pain point and qualified accuracy in first card. Final CTA: "Join creators and teams who use Wisprs for accurate, fast transcriptions." How it works: benefit for Upload step; qualified accuracy in Transcribe; specific formats (TXT, SRT, VTT, DOCX) and outcome in Transform. Product mockup: "55+ pieces of content" → "Turn one recording into multiple assets." Footer, FAQ, and pricing: 99% qualifier site-wide; FAQ subhead and exceed-limits answer tightened; pricing subhead with keyword and plan names.
- **SEO (sitemap and robots):** robots.txt now disallows `/forgot-password`, `/reset-password`, and `/wisprs/` so flow and auth pages are not indexed; sitemap continues to list only public marketing/content. Comments in `app/robots.ts` and `lib/seo/public-urls.ts` document that sitemap and robots share the same indexing intent.
- **Stuck transcription cleanup:** Cleanup now uses `createdAt` (not `updatedAt`) for the age threshold so transcriptions that receive progress or poller updates are still marked failed after `STUCK_TRANSCRIPTION_THRESHOLD_HOURS` (default 4). Fixes jobs stuck in processing overnight that were previously skipped because `updatedAt` was refreshed.
- **Pricing CTA when logged in:** Plan cards (Pro, Studio, Agency) now show "Upgrade" instead of "Get Started" when the user is signed in (Clerk); Enterprise keeps "Contact Sales", current plan shows "Current Plan". Implemented in `PricingCards` via computed `ctaLabel` from `isSignedIn`.
- **Hide Free plan on marketing/features:** Pricing cards on the homepage (`PricingSection`) and on feature pages (`FeaturePricing`, e.g. /features/accuracy) now show only paid plans (Pro, Studio, Agency, Enterprise); Free plan card is hidden on those pages.
- **Email templates (Koala-style balance and spacing):** (1) **Line-height:** `lineHeight: 22` → `'22px'` in paragraph/signOff so unitless value no longer renders as 22× font size. (2) **Layout:** Container padding `20px 40px 48px`, logoSection marginBottom 20px; full-width block CTA button (`display: 'block'`, `width: '100%'`). (3) **Vertical spacing:** Paragraph marginBottom 12px (and CSS override in wisprs-email-layout); buttonSection margin 20px; signOff marginTop 24 then 40px; greeting uses `greetingParagraph` (marginBottom 20) for salutation-to-body gap. (4) **Design-skills-setup skill:** `.cursor/skills/design-skills-setup/SKILL.md` documents install/use of frontend-design, web-design-guidelines, find-skills and Wisprs theme reference. (5) **Preview and test:** Dev-only `/dev/email-preview` page and `GET /api/dev/email-preview`; `scripts/send-test-email.ts` and `email:send-test` script for sending sample emails via Resend.
- **Pricing (Studio & Agency):** Studio $79/month (3,000 min STT, 90K TTS, 150K translation); Agency $149/month (5,000 min STT, 150K TTS, 250K translation). Limits and display copy updated in `limits.ts`, `pricing-data.ts`; founding and margin docs updated. Set Studio/Agency prices in Polar dashboard to match.
- **.gitignore:** Added `.env.production`, `.env.production.local`, `/audios/`, `*-dump*.sql`, `Untitled` so they are not tracked.
- **Repo-wide (devops, scripts, app, lib):** Devops runbooks and configs (DEPLOY, ITERATIVE_DEPLOY, PROD_ENV_AND_DB_CHECK, RECON_CHECKLIST, app docs, nginx/systemd configs, env templates); deploy script and check scripts; cancel-pgboss script; upload page, pricing, success page, multi-file-upload, stt-constants, db index, chunked-uploader, utils; Cursor wisprs-deploy skill; stt-bridge .gitignore and parakeet requirements; docs/plans and testing guide.
- **STT bridge docs (server specs + performance):** `devops/WISPRS_STT_BRIDGE.md` now has a "Performance and capacity" section tying server specs (winter-exodus: 2 CPU, ~4G RAM, 78G disk from RECON), exact bridge config (faster-whisper, small, fast, MemoryMax 1536M, CPUQuota 30%), and observed transcript runs (IDs 9 and 10) with a 4.5× processing/audio rule of thumb. `devops/RECON_RESULTS.md` Server hardware table filled via deploy SSH; `docs/features/STT_BRIDGE_SELF_HOSTED.md` links to the runbook for specs/config/performance.
- **Language/Auto-detect icon:** Replaced grid-style globe (🌐) with earth globe emoji (🌍) for Auto-detect and unknown language across transcriptions table, transcript tabs, stats cards, and language-display utils.
- **Code cleanup (deslop):** Removed redundant defensive code and noise from branch: upload-worker uses single `eq(storageKey)` after early throw (dropped dead `isNull` branch and import); ai-summary-panel uses inferred types in `.map()` callbacks; polar webhook logs `eventType` from one cast; button variant alias via lookup map; transcription-detail-live uses shared `cn` from `@/lib/utils`.
- **STT timeouts for long files**: App now uses a 20-minute minimum wait for bridge/self-hosted jobs (`STT_MAX_WAIT_SECONDS` in `src/lib/ai/stt-constants.ts`), and the stt-bridge source download timeout is configurable via `BRIDGE_DOWNLOAD_TIMEOUT_SECONDS` (default 600s / 10 min) so large files no longer fail with "timed out" during download.
- **Upload then confirm transcription**: After upload (simple or chunked), transcription is no longer started automatically. The user is redirected to the transcription detail page and sees "Ready to transcribe" with a mode selector (Auto / Speed / Quality) and a "Start transcription" button. The job is enqueued only when the user clicks Start; `metadata.queuedAt` prevents double-enqueue. Retranscribe / Transcribe Afresh still start immediately.
- **Pricing & plans**: `/pricing` aligned with founding docs; all five plans (Free, Pro, Studio, Agency, Enterprise) shown; FAQ copy updated for Polar and overage behavior.
- **Features & product copy**: Single source of truth documented in `docs/SOURCE_OF_TRUTH.md`; `features-data.ts` and `product-faqs.ts` aligned with per-plan entitlements (speaker ID on Pro+, export formats by plan, API copy for Agency/Enterprise); share page wording ("Full transcription features").
- **Modes**: Comments in `use-cases` and `modes` clarify only `default` and `sales` are app modes; use-case pages remain marketing.
- **Accuracy claim**: All "95% accuracy" references updated to "99%" across the app (features, hero, FAQ, pricing, footer, blog, product-faqs, modes, creators, layout) and `docs/features/STT_BRIDGE_SELF_HOSTED.md`.
- **Features & Use Cases pages**: Reduced hero and grid padding so content is visible above the fold (`py-24` → `py-10`, `mb-16` → `mb-8` on `/features`; same compact spacing on `/use-cases` and CTA section).
- **Nginx proxy buffers**: `devops/configs/nginx/wisprs.co` uses 256k / 8×256k / 512k for ~10K users and large Clerk session headers (avoids 502 after SSO).
- **STT setup docs**: Production must use public URL for `NEXT_PUBLIC_APP_URL` so bridge can fetch audio; link to Connection refused troubleshooting.
- **AI artifact retrieval path**: Transcript-derived AI outputs now use a shared `Redis -> Postgres -> generate` helper layer, with Redis backfill on DB hits and `forceRegenerate` overwriting both cache and DB.
### Fixed
- **Notification email subject:** Strip any "[Wisprs test]" prefix from notification email subjects before sending so production emails never show it (only the test script adds that prefix; safeguard in `sendNotificationsToProviders`).
- **Email CTA links:** Notification emails now use transcription-specific URLs for the "View transcription" / "View dashboard" button when `transcription_id` is present (e.g. `https://wisprs.co/dashboard/transcriptions/13`). Redirect-after-sign-in already works (middleware + sign-in page `redirect_url`); no tokens in links. Doc: `docs/features/NOTIFICATIONS.md` "Email CTA links and auth" and optional Clerk magic-link note.
- **Notification emails not sent (enqueue failed):** pg-boss rejects job expiration >= 24 hours. Notification jobs in `src/lib/notifications/unified.ts` now use `expireInSeconds: 82800` (23h) so "Transcription queued" and completion emails are enqueued and delivered.
- **Upload/transcribe "Authentication required" while logged in:** (1) Middleware now allows GET `/api/transcriptions/:id/stream` through; auth is enforced in the stream route handler (Node runtime) via `getServerUser()`. (2) Upload routes (init, chunk, complete) return a clearer 401 message: "Session may have expired. Try signing out and signing in again." and log cookie presence when returning 401. (3) `getCurrentUser()` in `src/lib/auth/clerk.ts` logs non-PII diagnostics when returning null (no session, Clerk fetch failed, or DB sync failed). Plan: fix_upload_auth_401.
- **TypeScript (tts, usage, export, webhooks, dashboard, components, queue/lib):** (1) **tts.ts:** Use `Uint8Array(result.audioBuffer)` for `File` constructor (BlobPart). (2) **usage.ts:** Type `billingPeriod` from subscription metadata with explicit cast. (3) **Export route:** Use `BodyInit` (string or `Uint8Array(content)`) for `NextResponse` body. (4) **Webhooks:** Clerk — typed deleted user payload for `email_addresses`/`primary_email_address_id`; ElevenLabs — guard `currentSegment`, handle undefined `rawLangCode`; Polar — added `SubscriptionRenewedEvent` to `polar-types.ts` and union, fixed default/catch event type logging. (5) **verify-checkout:** Correct `syncSubscriptionFromPolarId(user.id, subscriptionId, productId)` and require `productId`. (6) **Dashboard:** TTS page — `Synthesis` allows `voice` null and `processingCost` string/number, display uses `Number()`; TTS [id] — `formatCost` accepts number/string/null; transcribe — pass `transcriptText ?? ''` to InlineTranscript; transcriptions [id] — typed `metadata`/`speakers` for TranscriptTabs; share — `shareData` guard, `transcriptText ?? ''`, discriminated `PublicShareViewerInitialData`, speaker list array guard. (7) **Components:** Button variants `outline`/`destructive`/`default`; Badge `outline`; MediaPlayer WaveSurfer `seeking` (not `seek`) and `currentTime`; removed duplicate local `cn` in action-points-panel, transcript-actions, transcript-sidebar, ai-summary-panel; dashboard-sidebar `minutesRemaining` as `typeof Infinity`; transcription-detail-live `fileUrl` on transcription type; transcription-status-tracker TranscriptTabs prop assertion; transcript-tabs `salesCallKitData ?? undefined`; sales-call-kit `copied` as `Record<string, boolean>`. (8) **Queue/lib:** transcription-worker — metadata via cast `meta`, bridgeMeta spread objects only, `queue.work` `batchSize`; upload-worker — `session.storageKey` guard, `eq`/`isNull` for storageKey, `batchSize`; bridge-completion-poller `batchSize`; transcription-deduplication status cast; chunk-manager `chunkData` cast for storeChunk; detect-locale `maxOutputTokens`; language-display and use-language-display string/null handling.
- **TypeScript (pg-boss, scripts, upload, health):** (1) **getQueueCounts:** pg-boss types do not declare `getQueueCounts`; added `getQueueCountsSafe()` in `src/lib/queue/index.ts` that uses runtime `getQueueCounts` or `countStates()` (with `created` → `pending` mapping) and returns empty counts on failure. Admission, queue-health, transcription-worker, and health/queues route now use `getQueueCountsSafe`. (2) **Health routes:** `pgbossStarted` explicitly typed as boolean. (3) **Scripts:** `fetch-real-models.ts` and `seed-llm-models.ts` — null guard in loop, cost fields converted to string for `llm_models` insert. (4) **Upload routes:** Added `src/types/busboy.d.ts` for busboy module; upload route uses `Writable` from `stream` for write stream and typed busboy/stream callbacks; chunk route callback types and `error: unknown` for busboy error handler.
- **Transcription usage not decrementing**: Pro/free users’ remaining minutes now decrease correctly after transcriptions. ElevenLabs webhook handler calls `incrementUsage` on completion (fire-and-forget); transcription worker uses `effectiveDurationSeconds` (result or job file-metadata fallback) so usage is incremented even when the provider omits `durationSeconds`. Fixes sidebar/billing showing full allowance (e.g. 1000/1000 min) despite completed jobs.
- **Transcription detail UI**: Status and Duration in File Information now update live from the status stream (via `TranscriptionDetailLive` + `onTranscriptionChange`); `router.refresh()` still runs on completed/failed.
- **502 after SSO**: Resolved by increasing Nginx proxy buffer size on server (and in repo config) so large Clerk response headers are accepted.
- **Database**: `DATABASE_URL` in `.env.local` set to correct local PostgreSQL user so `users` and `transcriptions` queries succeed.
- **Docs**: `docs/founding/COMPLETION_STATUS.md`, `docs/founding/wisprs_founding_bible.md` pricing updated; `docs/README.md` links to SOURCE_OF_TRUTH and COMPLETION_STATUS.
### Removed
- **Dashboard sidebar**: "Use Cases" link removed from dashboard nav (Use Cases remain on marketing site only).
- **Better Auth**: Removed from project; authentication is Clerk-only (no `BETTER_AUTH_*` env vars).
### Added
#### STT: Self-Hosted Bridge + Commercial (OSS and 11labs)
- **Free-tier STT bridge:** Self-hosted FastAPI + faster-whisper service in `stt-bridge/`; free users routed to queue `transcription-free-self-hosted` and transcribed via bridge (no spill to ElevenLabs). Bridge supports fast (small) and accurate (large-v3) modes.
- **Bridge client:** `src/lib/ai/bridge-stt-client.ts` — submit job, poll status, fetch result; app passes `BRIDGE_STT_FILE_SECRET` so bridge can download audio from `/api/files/...` without Clerk.
- **Routing source of truth:** User tier from DB (`users.role`); upload-worker and transcription-worker use it for queue selection and provider choice. Paid users use ElevenLabs (transcription-priority).
- **Docs:** `docs/setup/STT_PROVIDERS_AND_SETUP.md` — single source of truth for OSS vs commercial STT, env vars, and how to confirm which provider processed a transcription (e.g. `node scripts/check-transcription-data.js <id>`).
- **Script:** `scripts/check-transcription-data.js` now prints `provider`, `provider_model`, and bridge metadata (`bridgeJobId`, `bridgeModel`, `bridgeMode`) for self-hosted jobs.
#### Sales Call Kit and error handling
- **Sales Call Kit fetcher:** Implemented missing `fetchSalesCallKit` in `src/components/transcript-tabs.tsx` (was referenced in useEffect and onRegenerate, causing ReferenceError).
- **Sales Call Kit schema:** LLM often returns flat `subject`/`body` at root; added `FlatSalesCallKitSchema` in `src/lib/ai/sales-call-kit.ts` and map result to nested `followUpEmail` so validation passes.
- **Error UX:** Sales Call Kit failures (e.g. invalid API key) now trigger the app error boundary (Something went wrong / Try again / Go home) instead of inline error in the tab.
#### Iron-Clad Usage Tracking System (Latest)
- **Fixed TTS Usage Tracking**: User ID now properly passed from TTS page to server actions
- Added `getCurrentUserId()` server action for client components
- TTS page now fetches and passes user ID to synthesis function
- Usage tracking now works correctly for all TTS operations
- **Hardened STT Usage Tracking**: Enhanced with transactions and validation
- Wrapped `incrementUsage()` in database transactions for atomicity
- Added input validation (userId, minutes)
- Added user existence checks before tracking
- Atomic SQL updates to prevent race conditions
- Comprehensive error logging
- **Implemented Translation Usage Tracking**: Complete translation character tracking
- Added `translation_characters_used` and `translation_count` columns to `usage_tracking` table
- Implemented `incrementTranslationUsage()` with transaction safety
- Added `calculateUsageFromTranslations()` function
- Integrated usage tracking in both standalone and transcription translation routes
- Added usage limit checks before translations (`checkTranslationUsageLimit()`)
- Updated `getBillingOverview()` to include translation usage
- Added translation limits to plan configuration (Free: 10K, Pro: 50K, Studio: 250K, Agency: 500K, Enterprise: unlimited)
- **Universal Hardening**: All usage tracking functions now use:
- Database transactions for atomicity
- Input validation (userId > 0, values > 0)
- User existence validation
- Atomic SQL updates (`sql` template) to prevent race conditions
- Error logging without breaking main operations
- Source-of-truth calculation from source tables when needed
- **Build Fix**: Extracted `TRANSLATION_LIMITS` to separate constants file
- Created `src/lib/constants/translation.ts` for client-safe constants
- Prevents Node.js module imports in client components
- Resolves "Module not found: Can't resolve 'net'" build error
#### Translation Service Enhancements
- **Text Length Validation**: Comprehensive validation for translation text length
- API validation with clear error messages
- Maximum length: 10M characters (database limit)
- Empty text validation
- Character count display in UI
- **Automatic Text Chunking**: Intelligent chunking for long texts
- Chunks at sentence boundaries (preferred)
- Falls back to paragraph breaks, then word boundaries
- 500-character overlap for context preservation
- Automatic chunking for texts > 100K characters (Google) or > 50K characters (LLM)
- Chunked translations merged seamlessly
- **UI Feedback & Warnings**: Visual feedback for character limits
- Color-coded character counter (white/orange/yellow/red)
- Warning banners for large texts (> 100K chars)
- Error banners for texts exceeding database limit (> 10M chars)
- Translate button disabled when text exceeds limits
- Real-time character count updates
- **Database Optimization**: Length checks and safety measures
- Automatic truncation with `...[truncated]` marker for texts > 10M chars
- Length validation before database insertion
- Applied to both source and translated text
- **Translation Limits Constants**: Centralized limits configuration
- `GOOGLE_MAX_CHARACTERS`: 100,000 (Google Translate API limit)
- `LLM_MAX_CHARACTERS`: 50,000 (Conservative LLM limit)
- `DB_MAX_CHARACTERS`: 10,000,000 (Database practical limit)
- `CHUNK_OVERLAP_CHARACTERS`: 500 (Context preservation)
- **Enhanced Copy/Download Functionality**: Improved user experience
- Visual feedback for copy/download actions ("Copied!", "Downloaded!")
- Checkmark icons on success
- Proper error handling with user-friendly messages
- Disabled state when no translated text available
- **Translation Documentation**: Comprehensive documentation
- Complete translation service documentation (`docs/features/TRANSLATION_SERVICE.md`)
- Architecture diagrams and flow charts
- API endpoint documentation
- Usage examples and best practices
- Troubleshooting guide
#### Pricing Pages & Billing Settings
- **Public Pricing Page** (`/pricing`): Complete pricing page with all plans
- Free, Pro ($25/month), Studio ($49/month), Agency ($99/month), Enterprise (Custom)
- Updated pricing to match final pricing docs (60 min STT free, 1,000 min Pro, 5,000 min Studio, 10,000 min Agency)
- TTS character limits displayed (5K free, 25K Pro, 150K Studio, 300K Agency)
- Premium voice caps displayed (10% Pro, 25% Studio, 35% Agency)
- Seamless authentication flow: logged-out users redirected to sign-in with plan preserved
- After authentication, redirects to dashboard pricing page
- **Dashboard Pricing Page** (`/dashboard/pricing`): Authenticated pricing page
- Shows current plan highlighted with usage progress
- Displays STT minutes and TTS characters used/remaining
- Plan cards with CTA buttons for checkout initiation
- Plan comparison information
- **Billing Settings Page** (`/dashboard/settings/billing`): Complete billing management
- Current plan overview with usage progress bars
- STT minutes and TTS characters usage breakdown
- Billing history table with order details
- Links to pricing page for plan changes
- **Settings Index Page** (`/dashboard/settings`): Settings hub
- Billing & Usage section linking to billing settings
- Account information display
- **Updated Pricing Data Structure** (`src/lib/pricing-data.ts`):
- Added Free plan (60 min STT, 5K chars TTS)
- Updated Pro plan: $25/month, 1,000 min STT (was 600), 25K chars TTS, premium voices ≤ 10%
- Updated Studio plan: $49/month, 5,000 min STT (was 2,000), 150K chars TTS, premium voices ≤ 25%
- Updated Agency plan: $99/month, 10,000 min STT (was 5,000), 300K chars TTS, premium voices ≤ 35%
- Added Enterprise plan: Custom $300+/month
- **Updated Billing Limits** (`src/lib/billing/limits.ts`):
- Updated plan limits to match pricing docs exactly
- Added TTS character limits for all plans
- Added premium voice caps (10%, 25%, 35%)
- **Enhanced Pricing Cards Component** (`src/components/pricing-cards.tsx`):
- Authentication-aware checkout flow
- Redirects logged-out users to sign-in with plan preserved
- For authenticated users: starts checkout directly from dashboard pricing page
- Shows current plan badge
- Displays STT/TTS limits and premium voice caps
- Handles Free and Enterprise plans appropriately
- **Updated Dashboard Sidebar** (`src/components/dashboard-sidebar.tsx`):
- Upgrade button now redirects to `/dashboard/pricing` instead of direct checkout
- Users can choose upgrade plan on pricing page
- **Updated Checkout API** (`src/app/api/checkout/create/route.ts`):
- Includes plan in success URL: `?checkout=success&plan={plan}`
- Adds plan to metadata for webhook processing
- **Billing Actions** (`src/app/actions/billing.ts`):
- `getBillingOverview()`: Fetches current plan, subscription, and usage (STT + TTS)
- `getBillingHistory()`: Fetches order history from `polar_orders` table
#### Collapsible and Responsive Dashboard Sidebar
- **Collapsible Sidebar**: Desktop sidebar can be collapsed/expanded with smooth transitions
- Collapse button in header (chevron icon) toggles sidebar width
- Sidebar width: 224px (expanded) → 64px (collapsed)
- Logo and wordmark scale down to 75% when collapsed
- Navigation icons remain visible, text labels hide when collapsed
- Plan usage card hides when collapsed
- Collapsed state persists to localStorage
- **Responsive Mobile Design**: Mobile-optimized sidebar with overlay
- Hamburger menu button in main content area (top-left) when sidebar is closed
- Sidebar slides in from left with dark overlay backdrop
- Close button in sidebar header when open
- Touch-friendly tap targets and smooth animations
- **Header Layout**: Logo, wordmark, and collapse button in single row
- Logo and wordmark on the left
- Collapse/hamburger button on the right
- All elements vertically aligned in header row
- Smooth scaling animations for collapsed state
- **Navigation Enhancements**:
- Gradient divider lines between menu items for visual separation
- Settings menu item moved under Folders in navigation list
- User avatar and logout button in bottom section
- Avatar on left, logout button with text on right
- Layout adapts for collapsed state (vertical stack when collapsed)
- **State Management**: React Context provider for sidebar state
- `DashboardSidebarProvider` manages collapsed/expanded and mobile open/closed states
- LocalStorage persistence for collapsed state
- Context hooks for easy access throughout dashboard
- **Visual Polish**: Apple-level design consistency
- Smooth transitions (300ms duration) for all state changes
- Proper z-index layering for mobile overlay
- Responsive padding and spacing adjustments
- Consistent hover states and active indicators
### Added
#### Polar.sh Billing Integration (Latest)
- **Complete Subscription Billing System**: Full integration with Polar.sh for subscription management
- Checkout session creation via Polar SDK (`@polar-sh/sdk`)
- Subscription lifecycle management (create, update, cancel, renew)
- Usage tracking and gating based on transcription minutes per month
- Feature-based access control based on subscription tier
- **Database Schema**: New tables for billing infrastructure
- `subscriptions` table: Tracks Polar subscriptions with status, periods, and metadata
- `usage_tracking` table: Tracks transcription minutes used per billing period
- Foreign key relationships with `users` table
- Optimized indexes for fast lookups
- **Plan Limits Configuration**: Centralized plan limits and features (`src/lib/billing/limits.ts`)
- Free: 30 minutes/month, basic features
- Pro: 600 minutes/month, $25/month, AI summaries, speaker diarization
- Studio: 2000 minutes/month, batch upload
- Agency: 5000 minutes/month, API access, team collaboration
- Enterprise: Unlimited minutes, all features
- **Usage Tracking Service**: Comprehensive usage tracking system
- Automatic increment after successful transcriptions
- Usage limit checks before allowing new transcriptions
- Period-based tracking with automatic reset at billing boundaries
- Real-time usage display in dashboard sidebar
- **Feature Gating Middleware**: Access control for transcription features
- `checkTranscriptionAllowance()` checks usage limits before uploads
- Integrated into upload worker to prevent over-limit transcriptions
- User-friendly error messages with upgrade CTAs
- **Checkout Flow**: Seamless checkout experience
- API endpoint: `POST /api/checkout/create` for creating checkout sessions
- Pricing cards component with checkout integration
- Success celebration with confetti and feature unlock display
- Resilient sync strategy: checks database first (webhook may have synced), then falls back to checkout session lookup
- **Webhook Handler**: Comprehensive webhook processing (`src/app/api/webhooks/polar/route.ts`)
- Signature verification using HMAC SHA256
- Idempotent event processing
- Handles all Polar events: `subscription.*`, `checkout.*`, `customer.*`, `order.*`
- Informational logging for non-critical events
- Error handling with graceful degradation
- **Subscription Sync Logic**: Shared sync function for webhooks and checkout
- `syncSubscriptionFromPolar()`: Core sync logic used by both webhooks and checkout
- Maps Polar product IDs to Wisprs plan names
- Updates user role in database
- Initializes/resets usage tracking for new billing periods
- Handles subscription updates, cancellations, and renewals
- **Checkout Success Flow**: Post-checkout verification and celebration
- `CheckoutSuccessCelebration` component detects success params in URL
- Checks database first (optimistic: webhook may have synced)
- Falls back to checkout session lookup if not found
- Shows "Processing Subscription" instead of "Failed" when webhook sync is expected
- 10-second confetti celebration with unlocked features display
- Auto-refresh of dashboard sidebar plan/usage info
- **Dashboard Integration**: Real-time plan and usage display
- Dashboard sidebar shows current plan, minutes used/remaining, usage percentage
- Upgrade CTA for free users or when usage >= 80%
- "Unlimited" display for enterprise plans (no "∞ / ∞ min" display)
- Refresh context for manual updates after checkout
- **Environment Configuration**: Polar environment variables
- `POLAR_ACCESS_TOKEN`: Polar API access token
- `POLAR_WEBHOOK_SECRET`: Webhook signature verification secret
- `POLAR_SERVER`: Environment selection (sandbox/production)
- `POLAR_PRODUCT_ID_PRO`, `POLAR_PRODUCT_ID_STUDIO`, `POLAR_PRODUCT_ID_AGENCY`: Product IDs
- `NEXT_PUBLIC_APP_URL`: Public app URL for redirects
- **Worker Integration**: Usage tracking in background workers
- Upload worker checks usage limits before allowing transcriptions
- Transcription worker increments usage after successful transcriptions
- Proper error handling and user feedback
- **Error Handling**: Comprehensive error handling throughout
- Expected 404 errors flagged (webhook may have processed checkout)
- User-friendly error messages
- Graceful fallbacks when webhook sync is delayed
- Comprehensive logging for debugging
### Added
#### Secure Transcript Sharing with Enterprise RBAC (Latest)
- **Cryptographically Secure Share Links**: Enterprise-grade sharing system with secure token generation
- 64-character hex tokens generated using `crypto.randomBytes(32)` for maximum security
- Unique, non-sequential tokens prevent link guessing and unauthorized access
- Share links format: `https://wisprs.com/share/{token}`
- **Configurable Permission Levels**: Granular access control for shared transcripts
- **View Only**: Read-only access to transcript text
- **View + Download**: Access to transcript and export functionality
- **View + Edit**: Full access including editing capabilities
- Permission-based UI rendering (hides features based on access level)
- **Password Protection**: Optional password protection for sensitive transcripts
- Bcrypt hashing (12 rounds) for secure password storage
- Rate limiting on password verification (5 attempts per 15 minutes)
- In-memory rate limiting to prevent brute force attacks
- **Expiration Dates**: Optional expiration dates for time-limited sharing
- Automatic link invalidation after expiration
- Clear error messages for expired links
- **PLG Branding**: Product-led growth features on public share pages
- Always-on branding (cannot be disabled per requirements)
- Animated waveform logo and wordmark matching app branding
- "Join Wisprs" CTA button with gradient styling
- "Powered by Wisprs" footer badge
- Feature highlights (multilingual, fast, accurate)
- **Access Logging & Audit Trail**: Comprehensive access tracking
- Logs all access attempts (successful and failed)
- Stores IP address and user agent for security auditing
- Access count tracking and last accessed timestamp
- Separate `shared_transcript_access_logs` table for detailed audit trails
- **RBAC Integration**: Role-based access control for sharing
- `canCreateShareLink`: Basic users can create view-only links for their own transcripts
- `canManageShareLinks`: Pro+ users can manage all share link settings
- `canShareWorkspaceTranscripts`: Studio+ users can share workspace transcripts
- Workspace-level permission checks for team collaboration
- **Share Modal UI**: Beautiful modal interface for managing share links
- Permission level selector using custom `Select` component (consistent with app design)
- Password toggle and input field
- Expiration date picker (optional)
- Link preview with copy button
- Access statistics (view count, last accessed timestamp)
- Revoke button to deactivate share links
- Real-time validation and error handling
- **Public Share Page**: Dedicated public viewing page for shared transcripts
- Server Component with direct database queries (no API round-trip)
- Custom error page for invalid/revoked/expired links (no Next.js 404 errors)
- Password prompt modal for protected links
- Full transcript display with all formatting options
- Timestamps view (default), Full Text view, and Speakers view
- Copy transcript button with gradient animation
- Export buttons (if permission allows)
- Responsive design matching app theme
- Dark mode enforced for consistent branding
- **Database Schema**: New tables for sharing infrastructure
- `shared_transcripts` table with indexes for performance
- `shared_transcript_access_logs` table for audit trails
- Foreign key constraints with cascade deletes
- Optimized indexes for fast token lookups
- **API Endpoints**: Comprehensive API for share management
- `POST/PATCH /api/transcriptions/[id]/share` - Create/update share links
- `GET /api/transcriptions/[id]/share` - Get share settings
- `DELETE /api/transcriptions/[id]/share` - Revoke share links
- `GET /api/public/share/[token]` - Public access endpoint with logging
- `POST /api/public/share/[token]/verify` - Password verification with rate limiting
- **Security Measures**: Enterprise-grade security features
- Secure token generation (cryptographically random)
- Password hashing with bcrypt (12 rounds minimum)
- Rate limiting on password verification
- Access logging for compliance and auditing
- Token uniqueness validation
- Expiration date enforcement
- Active/inactive status management
#### Full Text Transcript Formatting Enhancement
- **Intelligent Paragraph Segmentation**: Enhanced Full Text mode with smart paragraph detection
- Uses segments data (when available) to create natural paragraph breaks
- Breaks paragraphs on speaker changes (always), pauses > 2 seconds, and sentence boundaries
- Limits paragraphs to maximum 5 sentences to prevent walls of text
- Falls back to speaker data or sentence-based detection when segments unavailable
- **Improved Visual Formatting**: Better typography and spacing for readability
- Increased paragraph spacing from `mb-6` to `mb-8` for better visual separation
- Improved line height from `leading-7` to `leading-8` for easier reading
- Added max-width constraint (`max-w-4xl mx-auto`) for optimal reading width
- Centered layout with constrained width instead of full-width text dump
- **Optimized for Long Transcripts**: Enhanced paragraph grouping for 3+ hour transcripts
- More intelligent segment grouping based on timing data
- Better sentence detection and grouping (2-3 sentences per paragraph in fallback mode)
- Maintains performance with long transcripts
- **Clean Text Display**: Full Text mode now shows clean, readable paragraphs
- No timestamps or speaker labels (unless those modes are enabled)
- Proper paragraph breaks for easy reading and digestion
- Works seamlessly with search highlighting functionality
#### Language Display Enhancement (Latest)
- **Database-Driven Language Display**: Replaced hardcoded language code mappings with database-driven language names and country flag icons
- Created centralized utility module (`src/lib/utils/language-display.ts`) for server-side language display
- Created client-side hook (`src/hooks/use-language-display.ts`) for React components
- All language codes (ENG, EN, etc.) now display as full language names with flag emojis (e.g., "🇺🇸 English" instead of "ENG")
- Uses existing languages and countries cache infrastructure for fast lookups
- Graceful fallbacks for null/undefined/unknown language codes
- **Component Updates**: Updated all transcription module components to use new language display system
- `TranscriptionsTable`: Language column shows flag + name
- `TranscriptTabs`: Language badge shows flag + name
- `TranscriptionStatsCards`: Top language card shows flag + name
- `TranscriptionDetailPage`: Language display shows flag + name
- `TranscriptionFilters`: Language filter dropdown shows flag + name for each option
- **Non-Breaking Changes**: All function signatures maintained, same fallback behavior, pure UX improvement
#### Timestamp Color Consistency (Latest)
- **Unified Timestamp Styling**: All timestamps across transcript tabs now use consistent pink color (`#EC4899`)
- Transcript view timestamps (when Timestamps toggle enabled)
- AI Summary timeline highlights
- Chapters panel time ranges
- Topics panel duration displays
- All timestamps use `font-medium` for visual consistency
#### Action Points Initial State (Latest)
- **Unchecked by Default**: Action points now always start unchecked regardless of LLM output
- Updated LLM prompt to always set `completed: false`
- Added explicit mapping to force `completed: false` after extraction
- Cache retrieval also resets completed status to false
- Users can mark action points as completed using existing checkbox toggle
- Better UX: action points start as actionable items, not pre-completed
#### Transcript View Page - AI Tab Caching & Performance Optimization
- **Client-Side Caching**: Implemented intelligent client-side state management for all AI-generated content
- All AI data (Summary, Chapters, Minutes, Topics, Speakers, Action Points) cached in parent component
- Data persists across tab switches for instant display (no re-fetching)
- Eliminates unnecessary LLM API calls when switching between tabs
- Expected 80%+ reduction in API costs for tab navigation
- **Smart Tab Switching**: Intelligent data fetching based on cache state
- Only fetches data when tab becomes active AND data doesn't exist
- Instant display when data is already cached
- Loading overlay only shows when actually fetching
- **Regenerate Functionality**: Manual refresh option for all static tabs
- Regenerate buttons added to Summary, Chapters, Minutes, Topics, Speakers, Action Points tabs
- Positioned in top-right corner of each panel
- Bypasses both client-side and server-side cache
- Shows loading state during regeneration
- Chat tab remains dynamic (no regenerate needed)
- **Loading Overlay Enhancement**: Improved loading UX with full dark background
- Changed from semi-transparent (`bg-black/40`) to full dark (`bg-black/90`)
- Increased z-index to `z-50` for proper layering
- Centered preloader with tab-specific messages (e.g., "wisprn' the summary...")
- Covers entire content area for better visual feedback
- **Server-Side Cache Bypass**: Added `forceRegenerate` parameter to all Server Actions
- `generateChapters()`, `generateMinutes()`, `extractTopics()`, `generateSpeakerSummary()`
- `generateSummary()`, `extractActionPoints()`
- When `forceRegenerate` is true: skips Redis cache, deletes existing cache, generates fresh data
- **Performance Optimizations**: Render optimization with React hooks
- All fetch functions use `useCallback` to prevent unnecessary re-renders
- Proper dependency arrays for memoization
- Optimized state management to reduce component re-renders
#### Speaker Context Integration (Latest)
- **Speaker Information in Summaries**: Enhanced summary generation with speaker context
- Summary generation now receives speaker data from ElevenLabs diarization
- Speaker count correctly displayed in AI Summary panel
- Speaker names/IDs included in chunked context for better LLM analysis
- Context manager includes speaker information when available
- Improved summary quality with speaker attribution
#### Transcript View Module Enhancements
- **Custom Select Component Integration**: Replaced native `<select>` dropdown in media player with custom `Select` component
- Consistent styling with transcript filters dropdowns
- Gradient highlight for selected playback speed option
- Improved keyboard navigation and accessibility
- Matches design language across all dropdowns in the application
- **Visual Balance Improvements**: Enhanced transcript view height matching with sidebar
- Implemented flexbox-based layout using `items-stretch` for natural height matching
- Added max-height constraint (`calc(100vh-150px)`) for optimal scrollbar behavior
- Transcript text area now fills available space while respecting max-height
- Eliminated bottom gap between transcript card and sidebar
- Both containers now align perfectly for Apple-level visual balance
#### Transcript Tab Enhancements
- **Search & Replace**: Split search input into two side-by-side fields
- Left input: "Search in transcript..."
- Right input: "Replace with..."
- "Replace" button appears when both fields have content
- Enter key support in replace field
- **Shortcut Icons**: Added quick action buttons above transcript text
- Reordered buttons: All, Full Text, Timestamps, Speakers, Translate
- Translate button moved to end of button row
- Timestamps toggle (show/hide timestamps with active gradient state)
- Full Text toggle (show/hide full text view with active gradient state)
- Speakers toggle (show/hide speaker labels with active gradient state)
- All buttons use rounded-full styling to match tab pills
- Active states use gradient background (purple → pink → orange)
- **Timestamp State Synchronization**: Sidebar checkbox syncs with main tab pill
- "Show Timestamps" checkbox in sidebar reflects main tab state
- Toggling either control updates both UI elements
- Consistent state management across transcript view
#### Transcriptions Table
- **Language Display**: Enhanced language column to show full language name and flag icon
- Displays flag emoji (🇺🇸, 🇪🇸, etc.) alongside full language name
- Example: "🇺🇸 English" instead of "EN"
- Added `getLanguageFlag()` and `getLanguageName()` utility functions
#### Custom Select Component
- Created reusable `Select` component to replace native `<select>` elements
- Ensures Cal Sans font is applied to all dropdown options
- Used in transcription filters (Status, Date Range, Language, File Type)
### Added
#### Enhanced Transcriptions Page
- **Statistics Cards**: Added 4 stat cards above the table
- Transcription Volume (total count, last 30 days)
- Status Distribution (total transcriptions by status)
- Top Language (most transcribed language with emoji flag)
- Avg Processing Time (average processing time in minutes)
- **Advanced Filtering**: Comprehensive filter bar with multiple options
- Search input for file name search
- Status filter (All, Completed, Processing, Pending, Failed)
- Date range filter (All time, Last 7/30/90 days)
- Language filter (dynamically populated from transcriptions)
- File type filter (All, Audio, Video)
- **Sortable Table View**: Full-featured table with column sorting
- Sortable columns: File Name, Status, Date, Duration, File Size, Language
- Visual sort indicators (arrows) showing current sort field and direction
- Sorting works on full filtered dataset before pagination
- Language display shows full name with emoji flag (e.g., "🇺🇸 English")
- **View Toggle**: Switch between table and grid card views
- Table view (default): Sortable, paginated table
- Grid view: Card-based layout using existing TranscriptionCard component
- **Pagination**: User-selectable pagination with controls
- Items per page: 10, 20, 50, 100
- Previous/Next navigation buttons
- Page counter display
- Automatic reset to page 1 when filters or sorting change
- **Custom Select Component**: Replaced native `<select>` elements with custom styled dropdowns
- Full dark theme support for dropdown options
- Keyboard navigation (Arrow keys, Enter, Escape)
- Click outside to close
- Selected state with checkmark and gradient highlight
- Smooth animations and transitions
- Proper ARIA attributes for accessibility
- **Loading State**: Added waveform logo animation to loading state
#### Footer Navigation
- Added "Changelog" link to Product column
- Added "Blog", "About Us", "Stats", and "Status Page" links to Company column
- Added "Data Protection" and "Cookie Policy" links to Legal column
- Moved "FAQ" from Company to Product column
- Removed "Dashboard" link from Product column
#### Transcription Flow
- **Empty State Card**: Redesigned upload interface with centered icon, text, and button
- Icon positioned near top with proper spacing
- Centered text and call-to-action button
- "Supported formats" section at bottom center
- Full-page drag-and-drop functionality
- **New Transcription Button**: Added button in top right of transcribe page that navigates to `/dashboard/upload`
- **Upload Page**: Redesigned to match transcribe page empty state design
#### Real-Time Processing
- **Server-Sent Events (SSE)**: Implemented real-time transcription status updates
- Endpoint: `/api/transcriptions/[id]/stream`
- Auto-reconnection with exponential backoff
- Progress tracking and status updates
- **Transcription Queue Component**: Displays active (pending/processing) transcriptions
- Real-time status updates
- Progress indicators with gradient colors
- Cancel and view options
- **Transcription Error Component**: User-friendly error messages with retry functionality
#### Transcript Display & Actions
- **Transcript Tabs Component**: Pill-style tabs for different views
- Transcript tab with search functionality
- AI Summary tab
- Wisprs Chat tab (renamed from "Chat with Audio")
- Action Points tab
- Chapters tab
- Minutes tab
- Speaker Summary tab
- Topic Summary tab
- Vertical separators between tab pills
- Two-row layout with horizontal divider
- **AI Summary Panel**: Comprehensive AI-generated summary
- Stats section (Word Count, Characters, Speakers, Duration, Sentiment with emoji)
- Timeline Highlights with pink timestamps
- Topics section with gradient badges
- Key Points with light gray background cards
- All sections properly vertically aligned
- **Wisprs Chat Panel**: Interactive chat interface for transcript Q&A
- Message history
- Suggested questions
- Mock AI responses (structured for API integration)
- **Action Points Panel**: Extractable action items
- Checkbox list with completion tracking
- Assignee and due date support
- Export functionality
- **Chapters Panel**: Automatic chapter detection
- Chapter titles and summaries
- Time range badges with pink timestamps
- **Minutes Panel**: Meeting minutes extraction
- Date and duration
- Attendees list
- Agenda items
- Key decisions
- Action items with owners and due dates
- **Speaker Summary Panel**: Speaker analysis
- Speaker names, roles, and speaking times (pink timestamps)
- Speaking time percentages with progress bars
- Key points per speaker
- **Topic Summary Panel**: Topic analysis
- Topic names with mention counts and durations (pink timestamps)
- Percentage of total with progress bars
- Key insights per topic
#### Media Player
- **Waveform Player**: Integrated `@wavesurfer/react` for audio/video playback
- Waveform visualization with gradient colors
- Playback controls (play/pause, seek, volume, mute, speed)
- Current time and duration display
- Video element support for video files
- Compact, minimal Apple design
- Horizontal divider above player
- **Custom Select for Playback Speed**: Replaced native dropdown with custom `Select` component
- Consistent with transcript filter dropdowns
- Gradient highlight for selected option
- Improved accessibility and keyboard navigation
#### Sidebar
- **Transcript Sidebar**: Utility actions in collapsible sections
- **Export Section** (collapsible):
- Copy Transcript (enhanced with gradient animation and toast feedback)
- Download TXT
- Download SRT
- Download DOCX
- Download MD (Markdown export)
- Download VTT
- Advanced Export (highlighted with gradient)
- **More Section** (collapsible):
- Show Timestamps (toggle, syncs with main tab pill)
- ChatGPT integration
- Claude integration
- Translate
- Share Transcript (opens share modal with RBAC controls)
- Download Audio (downloads original audio/video file)
- Rename File
- Delete Transcription (with confirmation)
- **Copy Transcript Enhancement**: Improved UX with visual feedback
- Gradient background animation on copy (purple → pink → orange)
- Animated checkmark icon with draw animation
- Temporary toast notification: "Copied to clipboard!"
- Smooth transitions and state management
- Uses app gradient colors instead of generic green
- **Download Audio Functionality**: Direct download of original files
- Handles various file URL formats (relative, absolute, API routes)
- Proper blob handling and download trigger
- File size display in button
- Error handling for missing files
- **Markdown Export**: New export format for documentation
- Supports timestamps and speaker labels (optional)
- Clean paragraph formatting
- Compatible with Markdown viewers and documentation tools
#### File Information Card
- Added Status column to File Information grid
- Moved status badge from header to File Information card
- Added vertical divider between "Created" and "Completed" timestamps
- Applied gradient colors to timestamp labels ("Created" and "Completed")
- Removed timestamp from transcript header
- Added language flag emoji to language badge
- Full language name display (e.g., "English" instead of "en")
#### Styling & Design
- **Gradient Colors**: Applied throughout UI
- Progress bars use gradient: `from-[#8B5CF6] via-[#EC4899] to-[#F59E0B]`
- Processing spinners use gradient colors
- Active tab pills use gradient background
- Waveform visualization uses gradient
- **Typography**: Harmonized all body text to use `calSans` font
- **Color Consistency**: All timestamps use pink color (`#EC4899`)
- **Vertical Alignment**: Fixed all list items to be vertically centered
- Timeline Highlights
- Key Points
- Agenda items
- Speaker key points
- Topic insights
- Action points checkboxes
#### Layout Improvements
- Reduced padding on transcription detail page to match upload page
- Increased max-width to `max-w-7xl` to accommodate sidebar
- Proper spacing between tab rows
- Compact media player design
- **Transcript View Height Optimization**:
- Flexbox-based height matching between transcript card and sidebar using `items-stretch`
- Max-height constraint (`calc(100vh-150px)`) for optimal scrollbar behavior
- Natural height matching without forced viewport calculations
- Eliminated visual gaps and improved visual balance for Apple-level design
### Changed
#### Branding
- Updated app name capitalization from "WISPRS" to "Wisprs" across entire application
- Enforced full dark mode theme across public share pages
- Consistent animated waveform logo and wordmark across all pages
- Gradient color scheme applied to metadata labels and timestamps on share pages
#### Error Handling
- Custom error pages for invalid/revoked/expired share links
- Replaced Next.js 404 errors with user-friendly error messages
- Consistent branding and design on error pages
- Clear action buttons (Go Home, Get Started Free) on error pages
#### Transcriptions Page
- Converted to Client Component for interactivity
- Replaced native `<select>` elements with custom `Select` component
- Updated grid cards to use Cal Sans font for all text elements
- Fixed sorting to work on full dataset before pagination
- Improved language display with full names and emoji flags
- Fixed processing time calculation to prevent negative values
- Enhanced dropdown styling consistency across all filters
#### Transcription Detail Page
- Changed "Back to Dashboard" link to "Back to Transcripts" (navigates to `/dashboard/transcriptions`)
- Updated both "Created" and "Completed" timestamps to show full date and time
- Format: "November 23, 2025 at 07:28 AM" for both timestamps
- Replaced inline transcript component with tabbed interface
- Moved utility buttons from header to sidebar
- Removed download button from header (now in sidebar)
- Removed edit button (as per user request)
- Status badge moved from header to File Information card
- Language display shows full name with flag emoji
#### Error Handling & Modals
- Updated error page (`src/app/error.tsx`) to use Cal Sans font
- Updated not-found page (`src/app/not-found.tsx`) to use Cal Sans font
- Updated transcription error component to use Cal Sans font
- Updated delete button to use Cal Sans font
- Updated folders modal to use Cal Sans font
- All error messages now properly wrap text with `break-words` and `whitespace-pre-wrap`
#### Tab Organization
- Renamed "Chat with Audio" to "Wisprs Chat"
- Moved "Copy Transcript" from tab row to sidebar Export section
- Replaced "Copy" button with "Chapters" button in tab row
- Added "Minutes" tab after "Chapters"
- Added "Speaker Summary" and "Topic Summary" tabs after "Minutes"
- All tabs display in two rows with horizontal divider
#### AI Summary Panel Structure
- Moved "Stats" section above "Timeline Highlights"
- Moved "Timeline Highlights" and "Topics" above "Key Points"
- Added emoji beside sentiment value
- Converted "Key Points" to light gray background cards (matching Timeline Highlights)
- Added "Characters Count" and "Speaker Count" to Stats section
- Changed Stats grid from 3 columns to 5 columns to prevent wrapping
### Removed
- Removed "Dashboard" link from footer Product column
- Removed "New Transcription" button from transcribe page header (later re-added with navigation)
- Removed "We support MP3, MP4, WAV, M4A, and more." line from empty state
- Removed `MultiFileUpload` component from transcribe page (user requested single upload interface)
- Removed download button from transcription detail page header
- Removed edit button from sidebar
- Removed timestamp line from transcript header
- Removed inline graphs from statistics cards (per user request for cleaner design)
### Changed
#### Public Share Page
- Server Component with direct database queries (no API round-trip)
- Custom error handling for invalid/revoked/expired links
- Password protection modal for secure links
- Default view set to "Timestamps" for better initial experience
- Paragraph formatting matching Full Text tab in dashboard
- Gradient colors for metadata labels and timestamps
- Permission badge with dynamic colors (yellow/orange/green)
- Waveform logo integrated into CTA buttons
- Full dark mode theme enforcement
#### Transcript View Page - Caching Architecture
- **State Management**: Refactored all AI panel components to accept props instead of fetching internally
- `ChaptersPanel`, `MinutesPanel`, `SpeakerSummaryPanel`, `TopicSummaryPanel` now receive data as props
- `AISummaryPanel`, `ActionPointsPanel` updated to accept cached data
- Removed local `useState` and `useEffect` for data fetching from panel components
- Centralized state management in parent `TranscriptTabs` component
- **Loading States**: Improved loading state management
- Separate loading states for each tab (prevents cross-tab loading interference)
- Regenerating flags to distinguish between initial load and regeneration
- Error states properly isolated per tab
- **Cache Strategy**: Two-layer caching implementation
- **Client-Side**: Component state in parent (persists for session, instant display)
- **Server-Side**: Redis cache with 24h TTL (fast subsequent loads)
- Cache check order: Client state → Redis cache → LLM generation
#### AI Summary Panel
- **Speaker Count Display**: Fixed to show actual speaker count from ElevenLabs diarization
- Uses `summary.speakerCount` or falls back to `transcription.speakerCount`
- No longer shows "0" when speakers are detected
- **Context Passing**: Enhanced to pass speaker information to LLM
- Speaker data included in chunked context
- Speaker names/IDs included in prompt for better analysis
### Fixed
#### Hydration Errors
- Fixed React hydration mismatch in `WaveformBackground` component
- Client-only rendering with `useState` and `useEffect`
- Rounded floating-point values to prevent precision differences
- SSR placeholder to avoid server/client HTML mismatch
- Eliminated hydration warnings in browser console
### Technical
#### New Components
- `src/components/empty-state-card.tsx` - Full-page upload interface
- `src/components/transcription-queue.tsx` - Active transcriptions display
- `src/components/transcription-error.tsx` - Error handling component
- `src/components/inline-transcript.tsx` - Inline transcript display (replaced by tabs)
- `src/components/transcript-tabs.tsx` - Tabbed transcript interface with search/replace and shortcut icons
- `src/components/transcript-sidebar.tsx` - Utility actions sidebar
- `src/components/media-player.tsx` - Waveform audio/video player
- `src/components/ai-summary-panel.tsx` - AI summary display
- `src/components/chat-with-audio.tsx` - Chat interface (renamed to WisprsChatPanel)
- `src/components/action-points-panel.tsx` - Action points extraction
- `src/components/multi-file-upload.tsx` - Multi-file upload (removed from transcribe page)
- `src/components/select.tsx` - Custom select dropdown component with Cal Sans font support
- `src/components/share-transcript-modal.tsx` - Share link management modal with RBAC
- `src/components/public-share-viewer.tsx` - Public transcript viewer for shared links
- `src/components/waveform-background.tsx` - Animated waveform background watermark
- `src/components/toast.tsx` - Toast notification component
#### New API Routes
- `src/app/api/upload/route.ts` - File upload endpoint
- `src/app/api/transcriptions/[id]/stream/route.ts` - SSE status updates
- `src/app/api/transcriptions/[id]/export/route.ts` - Export transcript in various formats (TXT, SRT, VTT, DOCX, JSON, MD)
- `src/app/api/transcriptions/[id]/share/route.ts` - Share link management (create, get, update, delete)
- `src/app/api/public/share/[token]/route.ts` - Public access endpoint with access logging
- `src/app/api/public/share/[token]/verify/route.ts` - Password verification with rate limiting
#### Enhanced API Routes
- `src/app/api/transcriptions/[id]/route.ts` - Added DELETE handler for transcription deletion
- `src/app/api/transcriptions/[id]/export/route.ts` - Added Markdown (MD) export format
#### New Hooks
- `src/hooks/use-transcription-status.ts` - SSE connection hook
#### New Utilities
- `src/lib/storage.ts` - Storage abstraction layer (S3/R2 compatible)
- `src/lib/utils.ts` - Added `formatTimestamp()` utility function for consistent date/time formatting
- Supports multiple formats: `full`, `short`, `compact`
- Optional time inclusion
- Used across transcription detail pages and components
- `src/lib/sharing/generate-token.ts` - Cryptographically secure token generation
- `src/lib/sharing/validate-share.ts` - Share token validation and permission checking
- `src/lib/sharing/permissions.ts` - RBAC permission checking for sharing features
#### Enhanced Server Actions
- `uploadAndQueueTranscription()` - Combined file upload and transcription creation
- `batchCreateTranscriptions()` - Handle multiple file uploads
- `getTranscriptSummary(transcriptionId, forceRegenerate?)` - AI summary with caching and regeneration support
- `chatWithTranscript()` - Interactive chat with transcript (remains dynamic, no caching)
- `extractActionPoints(transcriptionId, forceRegenerate?)` - Action points extraction with caching
- `getTranscriptionById()` - Enhanced to join with `transcriptionFiles` for `fileUrl`
- `generateChapters(transcriptionId, forceRegenerate?)` - Chapters generation with cache bypass
- `generateMinutes(transcriptionId, forceRegenerate?)` - Meeting minutes with cache bypass
- `extractTopics(transcriptionId, forceRegenerate?)` - Topics extraction with cache bypass
- `generateSpeakerSummary(transcriptionId, forceRegenerate?)` - Speaker summaries with cache bypass
#### Enhanced AI Functions
- `generateSummary(options)` - Added `forceRegenerate` option, enhanced with speaker context
- `extractActionPoints(options)` - Added `forceRegenerate` option for cache bypass
- All AI functions now support cache invalidation and regeneration
#### Enhanced Export Functions
- `formatMD()` - New Markdown formatter in `src/lib/export/formatters.ts`
- Supports optional timestamps and speaker labels
- Clean paragraph formatting for documentation
- Compatible with Markdown viewers and documentation tools
#### Database Schema
- Enhanced transcription queries to include file URLs and storage keys
- Added support for metadata storage (duration, language, etc.)
- **New Tables for Sharing**:
- `shared_transcripts` - Share link configuration and settings
- `shared_transcript_access_logs` - Access audit trail with IP and user agent
- Optimized indexes for fast token lookups and access queries
### Design Principles Applied
- **KISS (Keep It Simple)**: Zero-config defaults, minimal user input
- **DRY (Don't Repeat Yourself)**: Reusable components and patterns
- **SOLID**: Single responsibility components, clean interfaces
- **FAANG UX Patterns**:
- Progressive disclosure
- Immediate feedback
- Error recovery
- Value density (100x value per scroll/click)
- Reduced friction (auto-upload, smart defaults)
- Aha moment in <3 clicks
- **Apple Design**: Clean, minimal, spacious, purposeful
### Performance
- Server Components for data fetching (reduced client bundle)
- Streaming with Suspense boundaries
- Real-time updates via SSE (no polling overhead)
- Optimistic UI updates
- Lazy loading for heavy components
- **Client-Side Caching**: Instant tab switching with cached data (no API calls)
- **Server-Side Caching**: Redis cache with 24h TTL for fast subsequent loads
- **Smart Fetching**: Only calls LLM when data doesn't exist (80%+ cost reduction)
- **Render Optimization**: `useCallback` for all fetch functions to prevent unnecessary re-renders
- **Cache Invalidation**: Manual regeneration bypasses all caches for fresh data
### Accessibility
- Semantic HTML structure
- Proper ARIA labels where needed
- Keyboard navigation support
- Focus management
---Notes
- **AI Features**: All AI features (Summary, Chat, Action Points, Chapters, Minutes, Speaker/Topic Summary) now use real LLM integration with intelligent caching - **Caching Strategy**: Two-layer caching (client-side + Redis) for optimal performance and cost efficiency - **Cost Optimization**: Smart fetching eliminates 80%+ of unnecessary LLM API calls - **User Experience**: Instant tab switching with cached data, manual regeneration option available - Media player supports both audio and video files with waveform visualization - Export functionality supports TXT, SRT, VTT, DOCX, and JSON formats - Real-time status updates use Server-Sent Events for efficient one-way communication - Storage abstraction allows switching between S3 and R2 providers - All timestamps consistently use pink color (`#EC4899`) for visual consistency - All list items are vertically centered for improved readability - Speaker information from ElevenLabs diarization is now properly integrated into AI summaries