Merge branch 'launch-page-product-insights'
:whale: Ship headless Chromium in the production image for JavaScript-rendered landing pages PageRendererService (and the Playwright directory automation) need a Playwright CLI and Chromium at runtime. The final stage now copies Node from node:22-trixie-slim (same Debian release as the Ruby base), installs the playwright package pinned to the version the playwright-ruby-client gem speaks, runs playwright install --with-deps chromium into /ms-playwright, and sets PLAYWRIGHT_CLI so the app uses the global CLI instead of npx. Browsers are world-readable so the non-root rails user can launch them. Verified by building the image locally and rendering a JavaScript page in it as the rails user. :sparkles: Give the product analysis real material: render JS shells, read linked pages, search the web Single-page apps such as q.cv ship an empty <div id="root"> so the scrape had nothing to give the model. Three changes: - PageRendererService renders the page in headless Chromium (Playwright, optional and self-disabling when the CLI is missing) whenever the static HTML has fewer than 40 readable words; the scraper re-extracts insights, description and social image from the rendered DOM. Page requests to private networks are blocked at the route level. - ProductAnalysisService fetches up to two linked pricing/about/docs pages (static, trimmed) and passes them alongside the landing page, and turns on OpenRouter's web-search plugin so the model can learn how the product is used, compared and received. Toggle with credentials openrouter.web_search or OPENROUTER_WEB_SEARCH. The model is now asked even when the landing page had no text, told explicitly that it is a JavaScript app. - Output budget raised to 8000 tokens because reasoning models count their thinking against it and returned empty content; empty replies now log the finish reason and usage. The result carries a sources block, shown as the panel subtitle. Tests stub the credential lookups so they no longer depend on whether the master key is present. :closed_lock_with_key: Add OpenRouter api_key and model to credentials :recycle: Route the product analysis through OpenRouter so the model is a config choice ProductAnalysisService now talks to OpenRouter's OpenAI-compatible chat endpoint via the ruby-openai gem already in the bundle. The model comes from OPENROUTER_MODEL / credentials openrouter.model (default anthropic/claude-sonnet-5), the key from OPENROUTER_API_KEY / credentials openrouter.api_key. Strict json_schema output is requested first and the call is retried on a plain JSON-only prompt when a model rejects it; fenced or prefixed JSON is tolerated. Result cache is keyed by model + URL. :sparkles: Show what we learned about the product on /launch, with an AI read of the landing page The preview step now aims for "they really understood my product": - ProductInsightsService extracts deterministic facts from the page the scraper already fetched: detected stack (Next.js, Stripe, Plausible…), linked pages (pricing, docs, changelog…), platform and business signals (iOS app, free trial, SOC 2…), headings, calls to action, contact emails, structured data and word count. Rendered in a new "At a glance" card. - ProductAnalysisService asks Claude (claude-opus-5, structured JSON output, low effort) to explain the product back: summary, audience, key features, differentiators, comparable products, pricing model, a directory-ready pitch, launch angles and suggested tags. Served by GET /projects/analysis as a second step after /metadata, reusing the cached page text; results cached per URL for a day; rate limited; hidden entirely when no Anthropic key is configured. - /launch rearranged at package-page width: product card + glance card side by side, the AI panel below with a loading skeleton, then the CTA. Signed-in makers get the same panels next to the editor, and suggested tags, comparable products and the pitch flow into the package fields. - All scraped/AI text is rendered with DOM APIs instead of innerHTML. :lipstick: Widen /launch to match the package pages and skip the URL input flash on prefilled URLs - Wrap the page in max-w-6xl (same as packages index/show/edit) so the editor's three-column grid has room; keep the hero, URL step and guest preview card centred at max-w-3xl. - When arriving with ?url= the URL card is rendered hidden and the "Fetching project details" status is rendered server-side, and the controller autofills on connect without the 500ms delay, so the input no longer flashes before the details replace it.