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.
✨ 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.
🔐 Add OpenRouter api_key and model to credentials
♻️ 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.
✨ 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.
💄 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.