- Migration: url_format_override on bio_pages, bio_url_format on workspaces,
needs_migration + migration_required_at on bio_pages (model casts existed, columns did not)
- PublicUrl tests updated for effectiveFormat() logic:
- Pro default = SystemPath (no subdomain without explicit bio_url_format=Subdomain)
- url_format_override on bio overrides workspace format
- needs_migration=true → publicUrl() returns ''
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Migration: add UNIQUE(workspace_id, slug) to bio_pages
Old (subdomain_host, slug) unique allowed MySQL NULL bypass —
two free-plan bios with NULL subdomain_host + same slug were not caught
- Edit::save(): validate slug uniqueness before update, dispatch toast on conflict
- Edit::save(): validate slug format (alphanumeric + dash/underscore, max 64)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- QR Code: split-pane wizard with type tabs, live preview canvas, style/frame/advanced options
- UTM Builder modal: generate URL with UTM params, copy + create short link
- A/B Test: LinkVariants modal with auto-balance weights (min 2, max 5 variants)
- AI: SlugSuggester service (3 suggestions via claude-haiku), AI-slug button in CreateLink
- AI pages: /ai/insights, /ai/anomalies, /ai/ab-generator with sidebar entry
- Analytics: #[Url] attributes for range/country/device/linkId, 24h/7d/30d/90d buttons
- QR enum migration (MySQL-only guard) to add email/phone/sms/location types
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>