# Wayback Downloader - Project TODO

## Backend Infrastructure
- [x] Update drizzle/schema.ts with download_jobs and job_assets tables
- [x] Generate and apply DB migration SQL
- [x] Add DB query helpers in server/db.ts
- [x] Add tRPC procedures: jobs.create, jobs.list, jobs.get, jobs.updateStatus
- [x] Write server/wayback.ts (CDX API integration — free, no signup)
- [x] Write server/downloader.ts (file download utilities using Node.js built-ins)
- [x] Write server/liveCrawler.ts (live website crawler using Node.js built-ins)
- [x] Write server/zipGenerator.ts (ZIP archive creation using archiver)
- [x] Write server/jobProcessor.ts (core job processing logic)
- [x] Write server/scheduler.ts (5-second background job scheduler)
- [x] Use built-in storage.ts (S3 storage helpers — Manus built-in, no signup)
- [x] Wire scheduler startup in server/_core/index.ts

## Frontend
- [x] Update index.css with dark slate/blue theme
- [x] Update App.tsx with routes and dark theme
- [x] Build polished landing page (Home.tsx) with animated hero, feature grid, how-it-works timeline, CTA
- [x] Build Dashboard.tsx with source selector toggle, rich job cards, skeletons, toasts, URL pre-fill

## Additional Features
- [x] Install archiver and axios packages
- [x] Add source type toggle (Wayback Archive / Live Crawl) to job creation
- [x] URL pre-fill from ?url= query param on dashboard
- [x] Toast notifications on job create success/failure (Sonner)
- [x] Loading skeletons for jobs list
- [x] Responsive mobile-first layout with sticky navbar
- [x] Animated hero section on landing page
- [x] Feature grid with icons on landing page
- [x] How It Works step timeline on landing page
- [x] Clear CTA for unauthenticated users

## Testing
- [x] Write vitest tests for job procedures and scheduler (19 tests, 2 files)
- [x] Run pnpm test and verify all pass

## New Features (Round 2)
- [x] Backend: jobs.delete tRPC procedure (protected, owner-only)
- [x] Backend: jobs.create accepts optional targetDate for Wayback Archive mode
- [x] Backend: jobs.exportCsv tRPC procedure returning CSV string
- [x] DB: update createDownloadJob to accept targetDate, pass to job processor
- [x] Job processor: use targetDate when querying CDX snapshots
- [x] Dashboard: Delete button on each job card with confirmation
- [x] Dashboard: Archive date picker input shown when Wayback Archive mode is selected
- [x] Dashboard: Export CSV button in the jobs list header
- [x] Tests: cover delete, exportCsv, and targetDate in jobs.test.ts

## URL Rewriting Feature (Round 3)
- [x] DB schema: add rewriteUrl column to download_jobs table
- [x] DB migration: generate and apply migration SQL
- [x] DB helper: update createDownloadJob to accept rewriteUrl
- [x] Backend: write urlRewriter.ts utility (HTML href/src, CSS url(), JS string replacement)
- [x] Backend: integrate urlRewriter into zipGenerator.ts (rewrite files before adding to ZIP)
- [x] Backend: jobs.create tRPC procedure accepts optional rewriteUrl
- [x] Backend: pass rewriteUrl through jobProcessor to zipGenerator
- [x] Frontend: add "Rewrite URLs to new domain" optional input field in Dashboard job creation form
- [x] Frontend: show rewrite URL field with clear label and placeholder
- [x] Frontend: pass rewriteUrl to jobs.create mutation
- [x] Frontend: show rewrite URL badge on job cards when set
- [x] Tests: cover urlRewriter utility and jobs.create with rewriteUrl

## Self-Contained ZIP Fix (Round 4)
- [x] Rewrite liveCrawler.ts: discover ALL assets from HTML (link[href], script[src], img[src], source[src/srcset], video[src], audio[src], link[rel=icon], meta[content]) including root-relative paths
- [x] Rewrite liveCrawler.ts: recursively extract assets from downloaded CSS files (@import, url())
- [x] Rewrite liveCrawler.ts: download external fonts (Google Fonts, Typekit) and inline them as base64 data URIs in CSS
- [x] Rewrite liveCrawler.ts: rewrite all asset references in HTML to relative local paths (not absolute, not root-relative)
- [x] Rewrite liveCrawler.ts: rewrite all asset references in CSS to relative local paths
- [x] Rewrite urlRewriter.ts: handle root-relative paths (/path/to/asset) correctly — convert to relative paths based on file depth
- [x] Rewrite urlRewriter.ts: handle relative paths (../img/logo.png) correctly
- [x] Rewrite urlRewriter.ts: handle protocol-relative URLs (//cse.google.com/...)
- [x] Rewrite wayback downloader: same root-relative and relative path fixes for Wayback mode
- [x] Tests: add tests for root-relative and relative path rewriting

## Domain SEO Update (onlinewebsitedownloader.com)
- [x] Update index.html canonical URL, og:url, Twitter card, JSON-LD to https://onlinewebsitedownloader.com
- [x] Update sitemap.xml with correct domain URLs
- [x] Update robots.txt sitemap reference
- [x] Update Terms, Privacy, About, FAQ pages with correct domain
- [x] Update Home.tsx footer links with correct domain

## OAuth Fix (Round 5)
- [x] Fix OAuth callback: use manus.space domain as redirectUri for token exchange
- [x] Encode custom domain origin in state for post-auth redirect
- [x] Server: parse state to extract returnTo origin, redirect after successful auth
- [x] Deploy fix to production via checkpoint

## Major Improvements (Round 6) - Better Than Any Wayback Downloader
- [x] Batch download: allow multiple URLs in one job (batch paste mode, up to 20 URLs)
- [x] Download progress: real-time progress bar with percentage (polling every 2s)
- [x] Download history with search and filter (search by URL, filter by status/source type)
- [x] Better error messages: expandable asset panel showing exactly which assets failed and why
- [x] Quick re-download button for previously downloaded sites
- [x] Retry button for failed jobs
- [x] Site preview: thum.io screenshot using actual archiveUrl for Wayback jobs (matches downloaded snapshot)
- [x] Smart asset detection: categorize assets by type with counts (implemented in Round 2)
- [x] Download size estimation before starting (implemented in Round 2)
- [x] Comparison view: show what changed between two snapshots (implemented in Round 2)
- [x] Mobile swipe actions (implemented in Round 2)

## Rebranding & Pricing Changes (May 9)
- [x] Remove all "Wayback Machine" / "Wayback" references from UI
- [x] Rebrand to generic "Website Downloader" terminology  
- [x] Change pricing model: per-URL (not per-domain)
- [x] Unlimited re-downloads of the same URL for free
- [x] Each NEW/different URL requires payment after free quota
- [x] Update Dashboard, Home, and all pages with new branding
- [x] Update pricing modal text
- [x] Fix OAuth: add missing stripeCustomerId column to DB
- [x] Add direct $5 purchase button in download form
- [x] Update Terms, FAQ, SEO structured data with per-URL language

## Pricing Revert to Per-Domain (May 9 - Round 2)
- [x] Revert db.ts: normalizeUrl back to normalizeDomain (extract hostname only)
- [x] Revert db.ts: hasUserDownloadedUrl back to domain-based check
- [x] Revert routers.ts: jobs.create to use domain-based quota (not URL-based)
- [x] Remove 3-download cap: unlimited re-downloads of same domain
- [x] Update Dashboard: per-domain language (not per-URL)
- [x] Update Home, Terms, FAQ, SEO: per-domain language
- [x] Update Admin: back to "Domain Purchases" with unlimited re-downloads

## 5 Major UX Improvements
- [x] Batch download: multi-URL paste, each unique domain counts against quota/requires payment
- [x] Search & filter: search by URL/domain, filter by status/source type in download history
- [x] Quick re-download: one-click re-download and retry buttons on all job cards
- [x] Better error details: expandable asset panel showing failed/downloaded assets per job
- [x] Domain count info shown in batch summary (unique domains = quota cost)

## No Refunds Policy
- [x] Update Terms of Service: all sales final, no refunds
- [x] Update FAQ payment section to mention no refunds

## Download Engine Fixes (May 9 - Incomplete Downloads)
- [x] Fix Wayback mode: when CDX API is down/empty, fall back to multi-page live crawl of archived version
- [x] Fix live crawl mode: BFS multi-page crawl following internal links (up to 150 pages)
- [x] Increase MAX_ASSETS limit for large sites (from 300 to 1000)
- [x] Add CDX retry logic with exponential backoff (3 attempts total: initial + 2 retries at 2s/4s delays)
- [x] Show user how many pages were found vs downloaded in job details (implemented in Round 2)

## UI Fix - Hide Upgrade Button for Admins
- [x] Hide "Buy $5 Unlimited" button when user is admin or already has unlimited tier for that domain

## Contact Info Replacement Feature (May 9) - COMPLETE
- [x] Add contactReplacements to jobs table (JSON field)
- [x] Create contactReplacer.ts utility with text replacement logic
- [x] Integrate contactReplacer into zipGenerator.ts
- [x] Update jobProcessor.ts to pass contactReplacements through pipeline
- [x] Add input validation to jobs.create and jobs.batchCreate
- [x] Update retry procedure to preserve contactReplacements
- [x] Write 7 new tests for contactReplacer utility + 1 router test (all 58 tests passing)
- [x] Frontend: add contact replacement UI with add/remove rows
- [x] Support multiple replacements per download in UI (type selector + find/replace inputs)
- [x] Expandable section with Add/Remove replacement buttons

## Bug Fixes (May 9)
- [x] Fix asset count display: show counts for completed jobs or when downloadedAssets > 0

## 6 New Features + Speed Optimizations (May 9 - Round 2) - COMPLETE
- [x] Speed: increase MAX_CONCURRENT from 5 to 20 for parallel asset downloads (4x faster)
- [x] Speed: add keep-alive HTTP agents to reuse TCP connections
- [x] Speed: reduce FETCH_TIMEOUT from 15s to 8s (fail fast on slow assets)
- [x] Speed: live crawl - fetch pages in parallel batches (BATCH_SIZE=20) instead of BFS sequential
- [x] Speed: deduplicate assets before downloading to skip redundant requests
- [x] Pages found vs downloaded: track pagesFound/pagesDownloaded in DB and show in job cards
- [x] Smart asset detection: show asset breakdown by type (HTML, CSS, JS, images, fonts) in job cards
- [x] Download size estimation: show estimated ZIP size based on downloaded bytes in job cards
- [x] Site preview thumbnail: thum.io screenshot generated on job completion, displayed in job cards
- [x] Mobile swipe actions: swipe left to delete, swipe right to download on job cards
- [x] Comparison view: /compare page with same-domain validation and full diff
- [x] Compare page: Snapshot B selector auto-filtered to same domain as Snapshot A
- [x] Compare button added to Dashboard header for easy navigation
- [x] All 71 tests passing (13 new: seoAnalyzer x4, seoOptimizer x6, replacementsPaid gate x2, router x1)
- [x] replacementsPaid gate: contact replacements only applied when replacementsPaid > 0 (verified in tests)
- [x] seoOptimize gate: SEO optimization only applied when seoOptimize=true in zipGenerator

## Parallel Chunked Downloading + Per-Replacement Pricing + SEO Optimization (May 9) - COMPLETE
- [x] Split asset list into 4 parallel worker chunks (Promise.all over chunks)
- [x] Merge all chunk results into a single ZIP after all workers complete
- [x] Show per-chunk progress aggregated into single progressPercent
- [x] DB: add seoOptimize and replacementsPaid columns to download_jobs
- [x] Backend: calculate price = $1 × number of contact replacement rules
- [x] Backend: create Stripe checkout session (dynamic price for replacements + $2 for SEO)
- [x] Backend: gate replacement application in zipGenerator — only apply if replacementsPaid
- [x] Backend: gate SEO optimization in zipGenerator — only apply if seoOptimize
- [x] Backend: seoAnalyzer.ts — analyze HTML files for SEO issues, return score 0-100 + improvement %
- [x] Backend: seoOptimizer.ts — apply fixes to HTML files (meta tags, titles, alt text, canonical, OG)
- [x] Backend: jobs.estimateSeo tRPC procedure — free SEO score + estimated improvement %
- [x] Backend: jobs.createAddonCheckout tRPC procedure — Stripe checkout for add-ons
- [x] Backend: stripeWebhook.ts — handle addon payment completion, re-queue job to apply add-ons
- [x] Frontend: "Enhance this download" accordion on each completed job card
- [x] Frontend: "Free estimate" link shows SEO score, improvement %, and top issues
- [x] Frontend: "Apply Add-ons" button with total price ($X.XX) triggers Stripe checkout
- [x] Frontend: shows "add-ons applied" badge when seoOptimize or replacementsPaid is true
- [x] All 71 tests passing (13 new: seoAnalyzer x4, seoOptimizer x6, replacementsPaid gate x2, router x1)
- [x] replacementsPaid gate: contact replacements only applied when replacementsPaid > 0 (verified in tests)
- [x] seoOptimize gate: SEO optimization only applied when seoOptimize=true in zipGenerator

## Bug Fixes + SEO Optimization (May 10)
- [x] Fix "Enhance this download0" — stray "0" in the accordion title (Boolean() cast)
- [x] Fix stray "0" rendering under SEO Optimization section (Boolean() cast)
- [x] Admin/unlimited users bypass add-on panel (hide entire Enhance section for admins)
- [x] Fix tier enforcement: jobProcessor reads user tier and applies correct asset limit
- [x] Fix "Get More" / "Upgrade" button: hides for tier_7_50 and admins
- [x] Verify unlimited tier is stored in DB and checked before capping assets
- [x] Landing page SEO: proper title, meta description, canonical, OG tags, Twitter cards (already done)
- [x] Landing page SEO: JSON-LD structured data (WebApplication + Organization schema) (already done)
- [x] Landing page SEO: sitemap.xml and robots.txt (already done)
- [x] Landing page SEO: FAQ section with keyword-rich questions (already done)
- [x] Landing page SEO: features/benefits section (already done)
- [x] Landing page SEO: update structured data pricing to match new 4-tier model
## Pricing Tier Update (May 10)
- [x] Update schema: add tier_7_50 to paymentTiers enum
- [x] Update stripeProducts.ts: tier_2_50=$2.50/150 assets, tier_5=$5/500 assets, tier_7_50=$7.50/unlimited
- [x] Update DB migration: alter domainPurchases.tier enum to include tier_7_50
- [x] Update routers.ts: createCheckout accepts tier_7_50
- [x] Update Dashboard.tsx UpgradeModal: show 3 paid tiers with correct prices/limits
- [x] Update Admin.tsx: already shows tier_7_50 stats
- [x] Update jobProcessor.ts: free=50, tier_2_50=150, tier_5=500, tier_7_50=9999 (unlimited)

## URL Rewriter Subdomain Fix (May 11)
- [x] Fix urlRewriter.ts: subdomain URLs (jury.domain.com, countyclerk.domain.com) now rewritten to target domain
- [x] Add 2 new tests: subdomain rewrite + mailto preservation (73 tests total, all passing)

## Timestamp Bug Fix (May 11)
- [x] Fix job card timestamp: "Started" date always shows May 9th regardless of actual createdAt
- [x] Fix "downloading for X days" duration calculation showing stale/wrong elapsed time

## Zombie Job Fix (May 11)
- [x] Add zombie job auto-recovery: jobs stuck "in progress" for 2+ hours auto-reset to "queued"
- [x] Old stuck jobs (180007, 180003) will be auto-re-queued by the zombie recovery on next scheduler tick

## Live Download Ticker (May 11)
- [x] Add currentUrl column to download_jobs schema
- [x] Update jobProcessor to write currentUrl as each page is fetched (throttled to 1 DB write per 2s)
- [x] Surface currentUrl in jobs.list API response (auto-included via SELECT *)
- [x] Render live URL ticker on in-progress job cards in Dashboard.tsx
- [x] Show "Restarted" date on job cards when a job was re-queued after being stuck

## Live Ticker Follow-up
- [x] Add dedicated restartedAt column so Restarted badge is based on explicit signal not updatedAt heuristic
- [x] Extend currentUrl ticker to Wayback asset downloads (currently only live crawl pages)

## Job Management Controls (May 11)
- [x] Add jobs.stop tRPC procedure (set status=failed, errorMessage="Stopped by user")
- [x] Add jobs.restart tRPC procedure (reset status=queued, clear error, clear currentUrl)
- [x] Add jobs.delete tRPC procedure (delete job + job_assets rows, any status allowed)
- [x] Add Stop button to in-progress/queued job cards (amber, with confirmation dialog)
- [x] Add Restart button to failed/done job cards (cyan, with confirmation dialog)
- [x] Add Delete button to all job cards (with confirmation dialog, works even for in-progress)

## Job Control Gaps (May 11)
- [x] Add cooperative cancellation to jobProcessor so Stop/Delete immediately halts active work
- [x] Clear job_assets before restart so stale records don't mix with new run (deleteJobAssets helper added)

## Live Ticker + SEO Optimization (May 11)
- [x] Fix live URL ticker for Wayback downloads (write currentUrl per asset batch)
- [x] SEO: Improve page titles and meta descriptions for all public pages (usePageSeo hook on all pages)
- [x] SEO: Add FAQ JSON-LD structured data to FAQ page
- [x] SEO: Add HowTo JSON-LD structured data to Home page (index.html)
- [x] SEO: Improve heading hierarchy (H1/H2/H3) across all pages (Home.tsx rewritten with proper hierarchy)
- [x] SEO: Add alt text to all images (aria-label and aria-hidden on all icons)
- [x] SEO: Add internal linking between pages (navbar links, footer nav, FAQ cross-links)
- [x] SEO: Preconnect to fonts, defer non-critical scripts, lazy-load images (preconnect already in index.html)
- [x] SEO: Add comparison table (vs HTTrack, vs wget, vs SiteSucker) on Home page
- [x] SEO: Add social proof / testimonials section to Home page
- [x] SEO: Add use cases section with keyword-rich content (replaces blog section)

## Restart Button Bug (May 11)
- [x] Fix Restart button: replaced 3 nested AlertDialogs with single shared dialog (avoids event bubbling bug)
- [x] Re-queue lost job 300001 (user will need to start a new download)

## progressPercent Column Bug (May 11)
- [x] Fix column name mismatch: was missing pagesFound/seoOptimize/replacementsPaid/currentUrl columns — applied via SQL ALTER TABLE

## Live Stats Row on Job Cards (May 11)
- [x] Show pages found, assets downloaded, total assets, and download size on in-progress job cards
- [x] Also show these stats on completed job cards as a summary

## Independent Email/Password Auth (May 11)
- [x] DB schema: add passwordHash column to users table
- [x] DB migration: ALTER TABLE users ADD COLUMN passwordHash applied
- [x] Backend: bcrypt password hashing via bcryptjs
- [x] Backend: auth.register tRPC procedure (email + password, create user, set session cookie)
- [x] Backend: auth.emailLogin tRPC procedure (email + password, verify, set session cookie)
- [x] Backend: auth.me works for email-auth users (uses same session cookie mechanism)
- [x] Backend: auth.logout works for both auth methods
- [x] Frontend: /signup page with email + name + password form + Manus OAuth fallback
- [x] Frontend: /login page with email + password form + Manus OAuth fallback
- [x] Frontend: Update navbar Sign In / Get Started to use /login and /signup
- [x] Frontend: Update Home.tsx hero CTA to use /signup instead of Manus OAuth
- [x] Frontend: Keep Manus OAuth as alternative "Sign in with Manus" option on both pages
- [x] Frontend: Toast on successful registration, redirect to /dashboard
- [x] Frontend: Error message on login failure (invalid credentials)
- [x] Dashboard: redirect unauthenticated users to /login instead of Manus OAuth

## Search Engine Submission (May 11)
- [x] Add Google Search Console verification meta tag placeholder to index.html
- [x] Add Bing Webmaster Tools verification meta tag placeholder to index.html
- [x] Add /compare and /signup to sitemap.xml
- [x] Update all sitemap lastmod dates to 2026-05-11
- [x] Note: Google/Bing deprecated sitemap ping endpoints — submission must be done manually via Search Console after publishing

## Forgot Password / Password Reset (May 11)
- [x] Replaced with support contact flow: "Forgot password?" on /login opens support form that notifies owner
- [x] Owner checks /myadmin to find user's account details and assists manually

## Name + Password Auth Simplification + Admin Panel (May 11)
- [x] DB: openId made nullable, synthetic openId used for name-only users
- [x] DB: unique index on displayName for name-based login lookup
- [x] Backend: auth.register accepts fullName + password (no email required)
- [x] Backend: auth.emailLogin accepts fullName + password (name-based login)
- [x] Backend: auth.contactSupport procedure sends owner notification with user's name and message
- [x] Frontend: /signup page — fullName + password fields (no email field)
- [x] Frontend: /login page — fullName + password fields (no email field)
- [x] Frontend: "Forgot password?" on /login shows support contact form with notification button
- [x] Admin: /myadmin user list shows Auth Type (Name+Password vs Manus OAuth), fullName, role

## Phone Replacement + Domain Rewrite Improvements (May 11)
- [x] contactReplacer: normalize phone numbers before matching (strip dashes, dots, spaces, parens)
- [x] contactReplacer: replace tel: href links (e.g. href="tel:9726450500") with new number
- [x] contactReplacer: support replacing multiple source numbers to one target (7000, 7001, 7009 → 645-0500 — use 3 separate rules)
- [x] urlRewriter: replace email addresses containing the original domain
- [x] urlRewriter: replace bare text occurrences of original domain in HTML body
- [x] Dashboard: "Apply Replacements" button on completed jobs opens reprocess modal with pre-populated rules
- [x] Backend: jobs.reprocess tRPC procedure re-ZIPs existing completed jobs with new replacement rules

## Support Email Integration (May 13)
- [x] Add help@onlinewebsitedownloader.com to site footer (Home.tsx Support nav column)
- [x] Add help@onlinewebsitedownloader.com to Home page footer Support section
- [x] Add help@onlinewebsitedownloader.com to Login page forgot-password form (both intro text and success message)
- [x] Add help@onlinewebsitedownloader.com to About page Contact & Support section and footer

## Email Address Rename (May 13)
- [x] Replace all help@onlinewebsitedownloader.com → support@onlinewebsitedownloader.com site-wide (Home.tsx, About.tsx, Login.tsx)

## Pricing & Payment Update (May 17)
- [x] Update stripeProducts.ts: Free/25MB, $5/150MB, $7.50/250MB, $10/500MB, $15/Unlimited, PWYW min $20
- [x] Update add-ons: $2.50 domain+email replacement, $1.50/phone number replacement
- [x] Update DB schema enum to add new tiers (tier_10, tier_15, tier_pwyw)
- [x] Update jobProcessor.ts tier asset limits for new tiers (tier_15 = unlimited for admin)
- [x] Update routers.ts createCheckout to handle new tiers including PWYW (custom_amount mode)
- [x] Update Dashboard UpgradeModal to show all 5 paid tiers + PWYW
- [x] Enable all Stripe payment methods (Link, CashApp, etc.) in checkout sessions
- [x] Expanded payment methods to full list: card, cashapp, link, affirm, afterpay_clearpay, klarna, amazon_pay, us_bank_account, crypto (all 3 checkout flows)
- [x] Run DB migration for updated tier enum

## Pre-Download Tier Selection Flow (May 17)
- [x] Frontend: Show tier selection modal BEFORE download starts (not after)
- [x] Frontend: Free tier → start download immediately; paid tier → Stripe checkout first
- [x] Frontend: After Stripe payment success, auto-start the download job (via webhook)
- [x] Frontend: Move add-ons (domain/email/phone replacement) to post-download only
- [x] Frontend: Add-ons panel only appears on completed jobs, not before/during download
- [x] Backend: createPaidDownloadCheckout procedure creates pending job + Stripe session
- [x] Backend: Stripe webhook on checkout.session.completed triggers job start for paid downloads
- [x] Backend: pending_jobs table stores job params until payment confirmed

## Permanent PWYW Payment Link & Pricing Page (May 17)
- [x] Remove buy now pay later (Affirm, Afterpay, Klarna) from all checkout flows
- [x] Create permanent Stripe Payment Link for PWYW tier (buy.stripe.com/test_4gM9AV9RH66O7ln84XbfO00)
- [x] Add public /pricing page with PWYW hero card and all tier cards
- [x] Wire /pricing route in App.tsx

## Phone Number Multi-Format Replacement (May 17)
- [x] Phone number replacement: entering a number once (e.g. 9726450500) should find/replace ALL format variants: (972) 645-0500, 972-645-0500, 972.645.0500, 9726450500, +19726450500, +1 972 645 0500, +1-972-645-0500, +1 (972) 645-0500, etc.
- [x] Normalize input phone number to digits only, then generate regex matching all separator/prefix variants
- [x] Apply multi-format regex in contactReplacer.ts for phone type replacements
- [x] Update UI hint text to say "Enter digits only — all formats replaced automatically"

## Domain Multi-Format Replacement + Phone Input Flexibility (May 18)
- [x] Domain replacement: entering oldsite.com once should match all URL variants: http://oldsite.com, https://oldsite.com, http://www.oldsite.com, https://www.oldsite.com, www.oldsite.com, plain oldsite.com
- [x] Phone find field: accept any format input (972-645-0500, (972) 645-0500, +1 972 645 0500, etc.) — strip to digits before building match regex
- [x] Update UI hint for domain field to explain all URL variants are replaced automatically

## Download Button Bug (May 18)
- [ ] Homepage Download button leads to "Not found" page — admin download works fine. Investigate route mismatch.
- [x] Download button on completed jobs starts a new scrape instead of downloading the ZIP file directly — FIXED: added e.stopPropagation() and target="_blank" to prevent event bubbling
