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.