Commit Graph

228 Commits

Author SHA1 Message Date
Tommy D. Rossi 9722518c5d Close wildcard CORS on recorder start/stop.
The toolbar Record button already starts recording from the extension
service worker (chrome-extension:// origin). Allowing any website origin
on POST /recorder/start and /recorder/stop was leftover from when the
page fetched the relay directly.

A page can no longer pass the CORS preflight, so it cannot start or
stop a recording. The worker fetch still skips the Sec-Fetch-Site block
because chrome-extension:// to 127.0.0.1 is cross-site.

Also switch the idle-icon tutorial to npx playwriter examples, bump
the extension to 0.0.118, and tighten the changeset/extension rules
in AGENTS.md.

Session: ses_fd2023624ffeK812PCDMCsUNVJ
2026-08-23 11:43:25 +02:00
Tommy D. Rossi c38866dea9 Serve the Playwriter skill from playwriter.dev instead of GitHub.
`npx skills add remorses/playwriter` showed the GitHub owner in every
install command. Akarso already avoids that by hosting the skills.sh
discovery document on its own domain. Do the same on playwriter.dev.

The website now answers:

  GET /.well-known/agent-skills/index.json
  GET /.well-known/agent-skills/playwriter/SKILL.md

The SKILL.md bytes are imported from skills/playwriter/SKILL.md so the
served file cannot drift from git. The index includes a sha256 digest
because skills.sh checks it.

All public install commands now use:

  npx -y skills add https://playwriter.dev

This covers the README, website docs, welcome page, and tutorial page.
The extension version is 0.0.113 because those HTML pages changed.

The tutorial page is also shorter than the previous daemon walkthrough.
That rewrite was already in the working tree with the skill URL change.

Session: ses_fd657bfa3ffe8lJ3G5L134SgMB
2026-08-22 15:39:20 +02:00
Tommy D. Rossi a6cb87a74e Show Stop recording with a red square on the toolbar
While a skill records, the toolbar button used to show only
Recording skill. That text looks like status, not an action.

The button now shows a red stop square and Stop recording
with a unicode ellipsis. Click still stops the recording.

Also bump the extension to 0.0.109 and update the security
docs to match the new label.

Session: ses_fd70431a9ffejIwMLovDAQ4TLr
2026-08-22 12:24:34 +02:00
Tommy D. Rossi 86ce45b1a5 Teach the recorder prompt to write portable in-page SDKs, not machine-local utils.js
Recorded skills were getting /Users/... imports and a generic utils.js.
WebSockets never appear in the event log, so agents stopped at quota POSTs.

The recorder prompt and skill-recorder docs now:

- Import helpers relative to the playwriter cwd, or via homedir(). Never hardcode absolute paths.
- Name the script for the flow (submit.js, sdk.js). Do not default to utils.js.
- Prefer an in-page class SDK when the recording has a JSON API. fetch runs inside page.evaluate so cookies and captchas stay in the tab. Methods take one object. Failures throw method, path, status, and body.
- If submit has no matching POST, tap WebSocket with addInitScript, reload, replay the action, then dump construct/send/message. An empty tap after reload alone is expected.

Verified the tap on grok.com/imagine: wrap survives reload, Invia yields wss://grok.com/ws/imagine/listen frames.

Session: ses_fd72f6af5ffezwQjT8IVIfbZOH
2026-08-22 12:04:57 +02:00
Tommy D. Rossi c06dd1e7e3 Stop failing toolbar Record when many sessions are open.
The in-page Record button posted /recorder/start with no sessionId. The
relay treated many sessions as an error, so Record failed whenever more
than one playwriter session existed.

Session pick on start is not useful for this flow. Extension sessions
share the same Chrome tabs. The agent identifies the recording later,
at stop time.

Start without a sessionId now:
- uses an explicit sessionId when the CLI sent one
- otherwise reuses a free extension session
- otherwise creates an extension session
- never picks a headless or cloud session (those have no user tabs)

Stop without a recording id still stops the only active recording. If
more than one recording is active, stop throws and lists each id,
session, and current or last page URL:

    Multiple active recordings. Pass a recording id.

      3  session 1  https://app.example.com/settings
      5  session 2  https://github.com/remorses/playwriter

Then: playwriter recorder stop 3

The toolbar Stop button now sends the recording id it started, so it is
never ambiguous. Failed start/stop calls toast the error.

Also document the recorder goal, mechanics, commands, multiple-recording
case, and troubleshooting in skill-recorder, CLI, and troubleshooting
docs. recorder status prints page URLs.

Extension 0.0.108.

Session: ses_fd76982e8ffejVfvd9Qqui5xcE
2026-08-22 10:48:08 +02:00
Tommy D. Rossi 578ebaa2bb Keep utils.js in recorder skill output and fix Oracle review nits.
utils.js is still wanted. Stop output, skill.md, and the skill-recorder
docs now match recorder-prompt.md: write SKILL.md plus an importable
utils script, and replay with playwriter commands.

Also:

- jq upload filter is setInputFiles, not a fake file-upload event type
- fill examples define name/url so they run
- recorder stop prints events -r <id> so concurrent recordings do not mix
- e2e test again asserts click x is undefined

The toolbar events -r line stays unstaged with the other toolbar
session work.

Session: ses_fe411266affeR59e58UaOBxQm4
2026-08-22 10:47:02 +02:00
Tommy D. Rossi abd6709e9d Point recorder skill generation at playwriter commands and persist action fields in tests.
After a recording stops, agents were told to write SKILL.md plus a utils.js
script and to ask a long list of questions first. The toolbar clipboard
prompt also skipped how Playwriter works, so agents often drove raw
Playwright.

recorder stop and the toolbar copy now start with playwriter skill /
https://playwriter.dev/SKILL.md. The next step is recorder events, then a
SKILL.md of markdown steps with example playwriter -e commands built from
the events.

Docs and skill.md match that flow. Frames are described as
recordings/<id>/frames/<ms>.jpg: read the jpeg closest to an event ms.
CLI help no longer mentions snapshot-diff events that the recorder does
not emit.

Tests assert the structured fields already stored on actions (fill.text,
click.button, setInputFiles.files) and that thin page-error events keep
.message. Extension version is 0.0.106.

Session: ses_fe411266affeR59e58UaOBxQm4
2026-08-22 10:27:58 +02:00
Tommy D. Rossi b49b23e560 feat(relay): allow cross-origin recorder start/stop
The toolbar Record Skill button POSTed to the relay straight from the page
origin, so every site failed the preflight:

    Access to fetch at 'http://127.0.0.1:19988/recorder/start' from origin
    'https://antidetection.com' has been blocked by CORS policy

Two middlewares blocked it:

- the CORS `origin` callback only echoed `chrome-extension://<our ids>`
- `privilegedRouteMiddleware` rejected any request whose `Sec-Fetch-Site`
  was not `same-origin` or `none`

Both now carve out exactly `/recorder/start` and `/recorder/stop`. The
CORS callback reads `c.req.path`, so the exception cannot leak to other
routes.

Why these two routes are safe to open:

- responses carry only `{ recordingId, sessionId, file }`, no page
  content, credentials, or user data
- recorded events are served by `/recorder/events/:id`, which keeps the
  extension-only origin check
- `Content-Type: application/json` is still required, so a plain form
  POST cannot reach them and a preflight is still forced
- token auth in remote mode still applies on every request
- the toolbar shows `Recording skill…` while recording, so a silent
  background recording is not possible

Tests assert `/recorder/start` and `/recorder/stop` are not 403 with
`Sec-Fetch-Site: cross-site`, that `/recorder/status` still is, and that
a `text/plain` body still returns 415.

The security docs get a "Recorder route CORS" section explaining the
tradeoff.

Session: ses_fe43304dbffeT4MJIAE09Oo2s4
2026-08-20 16:23:41 +02:00
Tommy D. Rossi f996f2ae3e docs: update recorder instructions for ms-based frames, auto-stop, and -s flag
- recorder-prompt.md: frame naming now uses <ms>.jpg with ms field on
  events. Updated awk example for finding the frame before a click.
  Note that recording auto-stops after 20 minutes.
- skill.md: document -s <id> flag for recording existing sessions and
  mention auto-stop after 20 minutes.
- skill-recorder.mdx: add -s <id> usage, note that recording attaches
  to current Playwriter tabs, mention auto-stop.

Session: ses_fe518993fffeLkk8nc00YYO4l2
2026-08-19 18:46:42 +02:00
Tommy D. Rossi 16cb17f166 Simplify website architecture diagrams and fix alignment
The homepage relay diagram used nested boxes that were hard to
scan. Flatten it to Browser, localhost relay, and Client, with
the uncontrolled-tab note under the browser box.

In remote-access and security, replace filled Unicode arrows
(◄ ► ▲ ▼) with ASCII < > ^ v so the diagram renderer keeps
edges aligned. Also re-align the sandbox path table and the
host.docker.internal platform table.

index.mdx and pages/index.mdx stay in sync. They both render
the same homepage architecture diagram.

Session: ses_fe627d9dbffeHNaljibGg42LXw
2026-08-19 13:47:56 +02:00
Tommy D. Rossi 30ff868257 Tighten website hero layout and dim the video shader
Hero headings were 40/56/72 with py-18/24/28. That pushed the
first content too far down. Use 32/42/52 and py-8/10/12 instead.

VideoBackgroundShader writes inline opacity 1 on the canvas once
it is ready, so canvasClassName opacity utilities never apply.
Dim the shader with an outer wrapper at opacity-70.

Remove the Learn more arrow links. The unused ArrowDownIcon
goes with them.

Session: ses_fe627d9dbffeHNaljibGg42LXw
2026-08-19 13:47:51 +02:00
Tommy D. Rossi 09955133c2 Upgrade website Holocron and better-auth drizzle adapter
Bump @holocron.so/vite from 0.21.0 to 0.29.0 and
better-auth-drizzle-adapter from 1.0.5 to 1.1.0.

docs.json sets decorativeLines to none so the new Holocron
default does not draw extra page chrome.

The lockfile follows those two direct bumps and the Holocron
subtree (safe-mdx, mermaid, motion, and related peers).

Session: ses_fe627d9dbffeHNaljibGg42LXw
2026-08-19 13:47:46 +02:00
Tommy D. Rossi 7be1eca586 Save CDP screencast frames during action recording
TabCapture needed an extension-icon click and produced a video
agents had to slice with ffmpeg. Page.startScreencast needs no
grant and already skips static frames.

Each compositor change is written to
~/.playwriter/recordings/<id>/frames/<t>-<seq>.jpg. A pink
ripple still marks user clicks. recording-stopped carries
framesDir and frameCount.

Session: ses_fe69b2cbbffeXWH0kfihFS54b1
2026-08-19 13:42:50 +02:00
Tommy D. Rossi 832305110f Simplify action recorder to Playwright actions plus POST network
Drop late click screenshots, custom coordinates, the 200ms stall,
scroll polling, and file-input scanning. Those either froze the
page, lied about the click frame, or duplicated Playwright.

Clicks record immediately. Typing reuses the focused-element
locator. File picks are setInputFiles actions. Network stays
POST/PUT/PATCH/DELETE xhr and fetch only.

Session: ses_fe69b2cbbffeXWH0kfihFS54b1
2026-08-19 13:22:01 +02:00
Tommy D. Rossi 5c2ec1379c Speed up recorder and add click coords plus screenshots
Drop post-action aria snapshots, cookies, storage, and focus.
Those CDP walks froze the page on every key. Persist Playwright
actions only, plus mutating xhr/fetch (POST/PUT/PATCH/DELETE).

Clicks store page x/y from the injected recorder and a tmp PNG
with a red rectangle on the locator box. Typing reuses the
cached selector. Double clicks stall 200ms so they are one
action. Persist is debounced 50ms.

The playwright submodule points at the JsonRecordActionTool
selector cache and click stall.

Session: ses_fe69b2cbbffeXWH0kfihFS54b1
2026-08-19 13:07:46 +02:00
Tommy D. Rossi 9a9fdab3e1 fix: store recorder events as an editable JSON array
Fill keystrokes from Playwright actionUpdated now mutate the last
action in place. The 800ms coalesce timer is gone.

Files are ~/.playwriter/recordings/<id>.json. recorder events still
prints one JSON object per line for jq. Old jsonl files still parse.

Session: ses_ff0d0feb1ffeF63vbdQE7Icb8V
2026-08-17 15:01:04 +02:00
Tommy D. Rossi dfc593ec0a fix: move recording id to -r flag in recorder events
`recorder events <recordingId> [...eventIds]` mixed two kinds of ids in
positionals: `recorder events 1 4 7` read as one recording id followed
by event ids, which is impossible to parse at a glance. Positionals are
now only event ids and the recording is selected with an optional
`-r, --recording <id>` flag defaulting to the latest recording:

    playwriter recorder events              # latest recording, thin view
    playwriter recorder events 4 7          # full details of events 4, 7
    playwriter recorder events -r 2 4 7     # same, for recording 2

Updated help examples, `recorder stop` next-steps output,
recorder-prompt.md and the website Skill Recorder page accordingly.

A matching design rule (never mix different id kinds in positionals) was
added to the goke CLI framework docs.

Session: ses_ff54ec81cffeYS7SzMYkGaS5tf
2026-08-16 21:25:33 +02:00
Tommy D. Rossi d0870fe43d fix: address recorder review findings, default recorder events to latest
Review fixes (from a post-merge agent review of the recorder commits):

- recording id collision: files are now created with flag 'wx' so a
  concurrent relay process sharing ~/.playwriter/recordings can no longer
  truncate an active recording; the manager retries with the next id on
  EEXIST (up to 20 attempts)
- test isolation: recordings dir is resolved at call time via
  getRecordingsDir() honoring PLAYWRITER_RECORDINGS_DIR; recorder.test.ts
  now records into a temp dir and cleans it up, instead of polluting the
  user's real recordings dir and bumping their ids
- capture chain hangs: each queued capture races a 15s timeout so one
  hung page.evaluate can't stall later captures or block stop() forever;
  page polling skips ticks while captures are backed up
- cached rejected import: the recording manager import promise resets on
  failure instead of caching the rejection forever
- `recorder events` rejects non-numeric event ids with a clear message
  instead of silently printing nothing
- changeset cleanup: correct command name (`recorder`, not `record`),
  bullet list no longer interrupted by stray paragraphs
- stale `playwriter record` naming fixed in comments; documented the
  known coalescing tradeoff (long typing pause splits one fill into two
  action events)
- website CLI reference now lists the recorder commands

Also includes: `recorder events` (CLI + /recorder/events/latest route)
defaults to the most recent recording via latestRecordingId() when no
recording id is passed.

Session: ses_ff500aa18ffeiSe873v4YFxyfA
2026-08-16 20:35:07 +02:00
Tommy D. Rossi 4d54e60449 docs(website): add Skill Recorder tab, move Changelog to footer
New "Skill Recorder" navigation tab with a landing page explaining the
record-then-generate-skill workflow: value proposition, quick start with
thin timeline + per-id drill-down examples, event reference table, and
ten use-case sections (typed API client reverse engineering, CRM data
entry, message-driven bookings, invoice harvesting, internal admin ops,
repeat submissions, legacy apps, QA-pass E2E tests, bug repro reports,
fixing broken skills) sourced from a research pass on real browser
automation pain points.

The Changelog tab is removed from the top navigation and replaced by a
"Changelog" link in the footer platform links pointing to
/changelog/intro.

Session: ses_ff500aa18ffeiSe873v4YFxyfA
2026-08-16 20:25:56 +02:00
Tommy D. Rossi 6677d20449 fix remote relay authentication for Docker clients
Send the configured bearer token during the MCP remote relay health check so token-protected Docker and devcontainer connections can start successfully.

Add an end-to-end MCP startup regression test against a token-protected relay. Update CLI help and remote access guides to require the host and token configuration with a 0.0.0.0 relay binding.

Add a playwriter patch changeset that references issue #108.

Session: ses_00e1a04d8ffew22iE5MbYEiZXb
2026-08-11 21:30:46 +02:00
Tommy D. Rossi b47a2e7dc0 docs: document dialog auto-dismiss behavior in troubleshooting page
Explains why JavaScript dialogs (alert, confirm, prompt) disappear
instantly when using Playwriter. This is core Playwright behavior:
when no page.on("dialog") handler is registered, Playwright auto-
dismisses the dialog to prevent the page from freezing (an open JS
dialog blocks all CDP/JS execution).

Documents the default behavior for each dialog type:
- alert: dismissed (OK)
- confirm: dismissed (Cancel)
- prompt: dismissed (Cancel)
- beforeunload: accepted (navigation proceeds)

Shows how agents can handle dialogs by registering a handler before
the triggering action.

Session: ses_019076847ffeYQGXqk77ZLixZ4
2026-08-09 18:48:48 +02:00
Tommy D. Rossi 2d93ef2e05 website: redesign hero sections with IvarText font, larger text, no logos
- Add IvarText serif font file and @font-face declaration in globals.css
- Remove playwriter logo from all three hero sections (landing, changelog, cloud)
- Increase hero heading font sizes from 28/36/44px to 40/56/72px
- Add leading-tight for more line height
- Add font-ready fade-in to prevent FOUT (fallback serif flash)
- Match akarso hero layout: more vertical padding, consistent button styling
- Simplify changelog hero by removing unnecessary wrapper divs

Session: ses_07a1be50affeNIY6WWST7gW5bD
2026-07-21 11:25:22 -07:00
Tommy D. Rossi e282b614c2 document streaming limitations: per-tab permission click, multi-tab untested
- Chrome activeTab requires a manual extension icon click per tab
  (managed browser via `playwriter browser start` auto-accepts)
- one capture per tab: stream or record, never both
- concurrent multi-tab streaming is architecturally supported but
  untested; recommend the tee muxer (repeat --rtmp) for streaming the
  same tab to several platforms with a single capture and encode
- note the ~2-4s latency expectation

Session: ses_0a8205a5cffeId4FJXly9O4aOr
2026-07-12 14:20:23 -07:00
Tommy D. Rossi 310756bb38 add playwriter stream: live RTMP streaming of a tab via ffmpeg
New `playwriter stream start|stop|status` CLI commands and `stream.*`
executor API for streaming a browser tab live to RTMP endpoints
(X Live, Twitch, YouTube). Built for 24/7 auto-generated content:
render a page, stream it forever.

Architecture:

  extension MediaRecorder (fMP4 chunks, tabCapture)
        │ same WS recordingData messages as recording (no protocol
        │ or extension changes, old extensions keep working)
        ▼
  relay: StreamRelay pipes chunks to ffmpeg stdin
        │ re-encode H.264 CBR + AAC, forced GOP (MediaRecorder
        │ output is VFR with uncontrolled keyframes)
        ▼
  -f flv rtmp://... (single) or tee muxer with per-output FIFO
  threads + onfail=ignore (multi-destination, one encode)

ffmpeg is a child of the relay process, so streams survive CLI exit
and run indefinitely (no 15-min recording cap).

Details:

- defaults match X Live recommended encoder settings: 1920x1080,
  9000 kbps, 30 fps, 3s keyframes, AAC 128k 44100Hz stereo; use
  --video-bitrate 6000 --keyframe-interval 2 for Twitch
- binary/recordingData WS routing offers chunks to StreamRelay first,
  falls back to RecordingRelay (tabId sets are disjoint because the
  extension refuses a second capture of the same tab)
- /stream/start|stop|status endpoints under privileged auth
- stream keys never leak: destinations redacted in logs/status and
  raw URLs scrubbed from ffmpeg stderr before storage
- backpressure: 32MB stdin cap kills a stalled stream instead of
  buffering unbounded
- stop flow hardened after oracle review: shared stop promise for
  concurrent stops, extension stop failures and forced/nonzero
  ffmpeg exits report failure instead of false success
- --no-audio injects silent anullsrc track (X Live requires audio)
- tests: ffmpeg arg snapshots + real fMP4 pipe integration tests
  (generate with ffmpeg testsrc, stream to local flv, ffprobe verify)
- docs: website/docs/streaming.mdx, short skill.md reference, README

Session: ses_0a8205a5cffeId4FJXly9O4aOr
2026-07-12 13:45:11 -07:00
Tommy D. Rossi 99a53c9ccc add CPU profiling, Web Vitals, and React performance docs page
New docs page covering four profiling workflows through Playwriter:

1. CPU profiling via CDP Profiler.* commands
   Start/stop V8 profiler, save .cpuprofile, analyze with profano CLI.
   Covers sampling interval, session cwd gotchas, extension overhead.

2. Web Vitals collection (TTFB, FCP, LCP, CLS)
   PerformanceObserver-based collection with page reload for fresh metrics.

3. Long tasks and interaction latency
   Detect >50ms tasks and slow event handlers blocking interactivity.

4. React component render profiling (React 19.2+)
   Capture React Performance Track measures, convert to .cpuprofile format,
   analyze with profano --sort self. Shows component names, mount times,
   scheduler events (Cascading Update, Update Blocked).

   The conversion creates a flat profile where each component/event is a
   node under root with samples proportional to measure duration.

Session: ses_10139b5c5ffeU14dA4oc9TeCUi
2026-06-25 15:10:47 +02:00
Tommy D. Rossi 9ea20a4d4a add 10 feature and reference docs pages with new navigation structure
Add dedicated pages for each major Playwriter feature, breaking out
content that was previously only covered on the homepage. Each page
has SEO-optimized titles and descriptions with searchable keywords.

New pages in Features group:
- Accessibility Snapshots, Search, and Page Diffing
  snapshot(), getCleanHTML(), getPageMarkdown(), search, showDiffSinceLastCall
- Screenshot with Element Labels for AI Agents
  screenshotWithAccessibilityLabels, color coding, refToLocator
- Multi-Agent Browser Sessions with Isolated State
  state persistence, shared pages, popup handling, context variables
- JavaScript Debugger, Live Code Editor, and CSS Inspector
  createDebugger, createEditor, breakpoints, getStylesForLocator, React inspection
- Network Interception, API Capture, and Cookie Management
  response capture, API replay, authenticated fetches, cookie management via CDP
- Record Browser Automation as MP4 Video
  recording.start/stop, ghost cursor, createDemoVideo, tabCapture
- Headless Chrome, Direct CDP, and CI Setup
  browser install/start, --browser headless, --direct, CI/Docker
- Security Model and Sandboxed Execution
  local-only architecture, origin validation, sandboxed fs, cookie safety

New pages in Getting Started:
- Quick Start: Automate Chrome in 2 Minutes
  core workflow, 4-step setup, MCP config, links to feature pages

New pages in Reference:
- Configuration, Environment Variables, and MCP Setup Options
  all env vars, CLI flags, MCP config examples for Claude/Cursor/OpenCode

Navigation restructure:
- Added Features group between Getting Started and Guides
- Moved Use Cases from its own group into Guides
- Added Configuration to Reference group
- Replaced Changelog anchor with proper Holocron changelog tab
- Removed Use Cases group (merged into Guides)

Session: ses_10139b5c5ffeU14dA4oc9TeCUi
2026-06-25 15:10:36 +02:00
Tommy D. Rossi e56abed889 add changelog tab with hero section and GitHub releases link
Add a ChangelogHeroSection component to hero-section.tsx and a
changelog/intro.mdx page that links to GitHub releases.

This replaces the old Changelog anchor link in the global nav
with a proper Holocron changelog tab that pulls releases from
GitHub automatically.

Session: ses_10139b5c5ffeU14dA4oc9TeCUi
2026-06-25 15:10:16 +02:00
Tommy D. Rossi 01d7b1393e update homepage title to emphasize agent control
"Control Your Real Chrome Browser with Playwright"
→ "Let Agents Control Your Real Chrome Browser"

Shifts the framing from Playwright (implementation detail)
to the core value prop: AI agents controlling your browser.

Session: ses_1015d49fcffebpkTVLdZmlqk9s
2026-06-25 13:58:57 +02:00
Tommy D. Rossi 9f92d04b0d unhide Cloud Browsers tab in website sidebar
The Cloud Browsers docs page was hidden behind `"hidden": true`.
Removing the flag makes it visible in the navigation sidebar
so users can discover cloud browser features.

Session: ses_1015d49fcffebpkTVLdZmlqk9s
2026-06-25 13:58:52 +02:00
Tommy D. Rossi 5919864808 shift primary color to orange-red, use red video bg for cloud browsers page
- primary color: #ff9500 → #ff6a00 (more orange-red)
- hero HeroBackground now accepts dotColor prop
- cloud browsers page uses red dotColor rgba(255, 50, 50, 0.7)
- main landing page uses the new primary-matching orange-red

Session: ses_1015d49fcffebpkTVLdZmlqk9s
2026-06-25 13:58:22 +02:00
Tommy D. Rossi b9fcf12cef add favicon.ico to website from gray extension icon
Multi-size ICO (48, 32, 16) with transparency, converted from
extension/icons/icon-gray-128.png.

Session: ses_10182a70cffeH4htO0PMjxRbki
2026-06-25 13:18:16 +02:00
Tommy D. Rossi 68c65ca2c6 improve index page title for SEO, add Home sidebar title
- title: "Control Your Real Chrome Browser with Playwright" (shows as
  "Control Your Real Chrome Browser with Playwright — Playwriter" in
  browser tab and Google results)
- sidebarTitle: "Home" (clean sidebar label instead of repeating the title)

Session: ses_1019526e6ffenre3Zso9o0s2dB
2026-06-25 13:00:57 +02:00
Tommy D. Rossi 4f3d827958 batch lastCloudCreateAt into existing placeholder update write
Removes the separate D1 write for the rate limit timestamp by
piggybacking it onto the placeholder→real-ID update that already
happens. Both statements run in one db.batch() call, so connect
has the same number of D1 round trips as before the rate limit
was added.

Session: ses_107161964ffefCyMno6mi5LtMr
2026-06-24 20:47:44 +02:00
Tommy D. Rossi 7e9b93eaad fix Promise.all VM orphan risk in cloud connect
The lastCloudCreateAt update and bu.createBrowser() were running in
Promise.all. If createBrowser succeeded but the D1 update failed,
Promise.all rejected and vmResult was lost, orphaning a paid VM.

Now the D1 update runs first (sequentially), then createBrowser.
If the DB write fails, no VM is created. If VM creation fails,
the catch block has no vm.id to worry about and just cleans up
the placeholder row.

Session: ses_107161964ffefCyMno6mi5LtMr
2026-06-24 20:33:12 +02:00
Tommy D. Rossi 68a153400c fix slot index gaps and downgrade edge case in sessions table
- use row.slotIndex from DB instead of sequential counter in /api/cloud/status
  so slots keep their real index after gaps (e.g. stopping slot 2 of 3)
- dashboard renders max(totalSlots, maxSessionIndex) rows so sessions
  above subscription quantity are still visible after downgrades

Session: ses_10bc2a749ffeInm3HXNY1r5v6D
2026-06-24 18:30:39 +02:00
Tommy D. Rossi 6a4056cfc7 hide visible title on login page by passing empty title
Empty title skips the h1 render; browser tab falls back to "Playwriter".
No new props needed: AuthPage conditionally renders h1 only when title is truthy.

Session: ses_105c733a9ffekXw867fixfqgKX
2026-06-24 18:28:54 +02:00
Tommy D. Rossi 252e06d317 add cloud live CLI command, dashboard sessions table, replace Browser Use live URLs
**CLI: `playwriter cloud live [key]`**
- prints and opens playwriter.dev/live?wss= URL for an active cloud session
- accepts cloud-N key, cloudSessionId, or browserUseSessionId
- auto-selects when only one session is active

**Dashboard: browser slots table**
- new CloudSessionsPanel shows all subscription slots (not just active)
- active sessions: green dot, duration, expiry countdown, "Watch live" link
- empty slots: gray "Available" status with dashes
- header shows active/total counter (e.g. "1/4 active")
- auto-refreshes every 10s, clears transient errors on success

**Replace Browser Use live URLs**
- all liveUrl fields now point to our /live?wss= route instead of live.browser-use.com
- uses exact CDP endpoint URL (not reconstructed from UUID) to handle BU sharding
- cloud-api.ts, cli.ts, cloud-client.ts all updated
- new buildLiveUrl() helper in cloud-client.ts (CLI) and cloud-api.ts (website)

Session: ses_10bc2a749ffeInm3HXNY1r5v6D
2026-06-24 18:24:53 +02:00
Tommy D. Rossi 1beaf0d695 fix duplicate logo on login page
The /login route passed visualTitle={<PlaywriterLogo>} to AuthPage,
but AuthPage already renders the logo at the top. This caused the logo
to appear twice on the login screen.

- Remove the visualTitle prop from the /login route, use "Sign in" text title
- Remove unused visualTitle and headTitle props from AuthPage component
- No other pages had this issue (device flow and dashboard use text titles)

Session: ses_105c733a9ffekXw867fixfqgKX
2026-06-24 18:21:15 +02:00
Tommy D. Rossi c95cb21189 separate proxy and browser spend into independent budgets
Instead of combining browserCost + proxyCost into a single cloudSpend
counter, track them separately with independent budgets. This lets
operators set different limits for proxy bandwidth vs VM runtime.

Schema (org table):
- Restore proxySpendCents / proxyBudgetCents (proxy bandwidth)
- Add browserSpendCents / browserBudgetCents (VM runtime)
- Rename proxySpendPeriodStart → spendPeriodStart (shared period marker)
- Keep lastCloudCreateAt for rate limiting

Schema (cloudSession table):
- Restore lastProxyCostCents (proxy cost baseline)
- Add lastBrowserCostCents (browser cost baseline)

cloud-api.ts connect handler:
- Check both budgets independently before allowing new sessions
- Reset both counters on billing period rollover

scheduled.ts cron:
- Compute proxy and browser deltas separately per session
- Atomic increment both spend counters independently
- Kill sessions if either budget is exceeded
- Conditional baseline update uses WHERE on both previous values

Migration adds new columns alongside existing ones (no renames needed
since proxy columns were never dropped from D1).

Session: ses_107161964ffefCyMno6mi5LtMr
2026-06-24 16:54:43 +02:00
Tommy D. Rossi 212277689c harden cloud browser cost safety: track total cost, rate limit, cap timeout
**Problem:** Several cost vulnerabilities in cloud browser management:
1. Budget only tracked proxyCost, not browserCost ($0.02/hr VM runtime).
   Users with proxyRegion=null bypassed the budget entirely.
2. No rate limiting on /connect: rapid connect/disconnect loops waste
   VM costs (Browser Use charges minimum 1 minute per VM).
3. Timeout allowed up to 240 minutes (4 hours) with no server cap.
4. customProxy accepted from users without validation.
5. Sessions with cancelled subscriptions kept running until timeout.
6. Concurrent flood: VM created before slot claim meant N concurrent
   requests could create N paid VMs when only 1 slot was available.

**Fixes:**

Schema changes (db/src/schema.ts + migration):
- Rename proxy-only columns to cloud-wide:
  proxySpendCents → cloudSpendCents
  proxyBudgetCents → cloudBudgetCents
  proxySpendPeriodStart → cloudSpendPeriodStart
  lastProxyCostCents → lastTotalCostCents
- Add org.lastCloudCreateAt for per-org creation rate limiting
- Migration copies existing data to new columns

cloud-api.ts:
- Restore slot-claim-before-VM-creation pattern to prevent
  concurrent flood (slot claim is cheap D1 write; VM creation
  is expensive BU API call that costs money)
- Track browserCost + proxyCost as total cost in budget checks
- Add 10-second per-org creation cooldown (MIN_CREATE_INTERVAL_MS)
  to prevent rapid connect/disconnect loops
- Cap timeout at 60 minutes (MAX_TIMEOUT_MINUTES) instead of 240
- Remove customProxy from connect schema (silently ignored if sent)
- Parallelize rate limit timestamp update with VM creation

scheduled.ts:
- Track total cost (browserCost + proxyCost) instead of proxy only
- Kill sessions for orgs without active subscription (cancelled/expired)
- Rename all proxy* references to cloud* for consistency

Session: ses_107161964ffefCyMno6mi5LtMr
2026-06-24 13:35:15 +02:00
Tommy D. Rossi e6ae866ac2 remove placeholder/pending pattern from cloud browser connect
**Before:** connect did 2 D1 writes per session: insert a placeholder row
with a pending-xxx ID, create the BU VM, then update the placeholder
with the real session ID. Required stale placeholder cleanup, pending
row filtering in status/cron, and complex rollback paths.

**After:** connect creates the BU VM first, then does 1 D1 insert with
the real session ID. If the insert fails (unique constraint = slot taken,
or any DB error), stops the VM. Simpler code, fewer D1 writes.

Changes:
- Remove PENDING_PREFIX, PENDING_STALE_MS, isPendingRow(),
  checkSlotOccupied(), claimCloudSessionSlot()
- Add insertCloudSession() that inserts with real BU session ID
- Wrap post-createBrowser code in try/catch to stop VM on any failure
  (prevents VM leaks on DB errors)
- Status endpoint no longer filters out pending rows
- Cron no longer has WHERE NOT LIKE pending-% filter
- resolveActiveSession and cron error handler now treat HTTP 400
  (malformed ID) as dead alongside 404, cleaning up any legacy
  pending-* rows on first check after deploy

Trade-off: in a rare race (two concurrent connects for the last slot),
the loser creates a VM then immediately stops it (fractions of a cent).

Session: ses_107161964ffefCyMno6mi5LtMr
2026-06-24 13:16:45 +02:00
Tommy D. Rossi 3e37ca1132 remove device flow workarounds, use better-auth API directly
better-auth-drizzle-adapter@1.0.5 now implements consumeOne and
incrementOne, so the device authorization plugin endpoints work
natively. Removed:

- Custom handleDeviceToken function and middleware intercept
- Direct DB queries for device verify, approve, deny
- orm/schema imports that were only used for device flow workarounds

The device page now uses auth.api.deviceVerify with request.headers,
and approve/deny actions use auth.api.deviceApprove/deviceDeny.
The built-in /api/auth/device/token endpoint handles token exchange.

Session: ses_10aba81e2ffeUWR93aNz4avLK2
2026-06-23 20:56:56 +02:00
Tommy D. Rossi 59500c77f8 fix cloud login: use better-auth client SDK, custom device/token endpoint
**CLI (`playwriter/src/cli.ts`):**
- Use `createAuthClient` with `deviceAuthorizationClient` plugin instead of
  hardcoded endpoint URLs. Hardcoded URLs broke when better-auth changed paths
  between versions. Now calls `client.device.code()` and `client.device.token()`.
- Added `better-auth` as a dependency of the playwriter package.

**Website server (`website/src/server.tsx`):**
- Added custom `/api/auth/device/token` handler in the auth middleware, intercepted
  before better-auth processes it. The drizzle adapter does not implement `consumeOne`
  which the device authorization plugin requires for the token exchange. Our handler
  does: find approved device code → delete it → create session → return access_token.
- Device verify page queries the deviceCode table directly via Drizzle instead of
  `auth.api.deviceVerify` (which depends on `incrementOne`, also missing from adapter).
- Session check moved before device code lookup so we redirect to login first.

**Website actions (`website/src/actions.tsx`):**
- Approve/deny actions update the DB directly via Drizzle instead of
  `auth.api.deviceApprove`/`deviceDeny` (which depend on adapter methods not
  available in better-auth-drizzle-adapter: `consumeOne`, `incrementOne`).

**Other:**
- Hidden Cloud Browsers tab in docs.json.
- Cloud browsers docs copy improvements (unrelated).
- Changeset for the playwriter package.

Session: ses_10aba81e2ffeUWR93aNz4avLK2
2026-06-23 20:24:19 +02:00
Tommy D. Rossi bb7b938f16 add centered logo linking to /dashboard on all AuthPage screens
The PlaywriterLogo component is now rendered above the title in the
shared AuthPage layout, wrapped in a spiceflow Link to /dashboard.
This affects all message screens: CLI Approved, CLI Denied, Invalid
Device Code, Device Code Claimed, and login pages.

Session: ses_10ab972f7ffehKgUpczAXRiPCm
2026-06-23 18:21:04 +02:00
Tommy D. Rossi 1e61680a3c Clean up dashboard: rename concurrent sessions to browsers, reorder panels, remove clutter
- Rename "concurrent sessions" to "browsers" in billing panel subtitle
- Show subscription card before Quick Start code snippets
- Remove "Full documentation" link from Quick Start panel
- Remove descriptive paragraphs from both CLI and MCP tabs
- Remove subtitle paragraph under Quick Start heading
- Remove "Organization: ..." line from dashboard header

Session: ses_10ac8dddaffe3Oth3P4r7dJukF
2026-06-23 18:04:48 +02:00
Tommy D. Rossi 946a0657e2 fix website primary color consistency
Use the same orange brand color across Holocron docs and custom website routes.

- Set Holocron light and dark primary colors to #FF9500 so docs do not derive a lighter dark-mode accent.
- Override the dashboard CSS primary token and ring token to the same orange.
- Leave unrelated docs navigation changes unstaged.

Session: ses_10af1cdd3ffeTJr7Qg6c3hHUA7
2026-06-23 17:42:39 +02:00
Tommy D. Rossi eff7a6ffc4 add cloud browser live preview page
Add a /live page in the website for viewing and controlling a remote CDP browser session from the browser.

This includes:
- a reusable CDP screencast component that streams Page.screencastFrame frames to a canvas
- mouse, wheel, keyboard, navigation, reload, and viewport forwarding over CDP
- a small /live entry page that accepts either a Browser Use session id or a full WebSocket URL
- stable scrollbar gutter styling to avoid layout shifts around the full-screen viewer

Session: ses_10bc2a749ffeInm3HXNY1r5v6D
2026-06-23 16:56:52 +02:00
Tommy D. Rossi b0729babc9 Add Quick Start panel with code snippets to dashboard
New QuickStartPanel component at top of dashboard showing how to use
cloud browsers. Two tabs: CLI and MCP.

CLI tab shows two usage cases in a single code block split by a comment:
- Interactive login flow: cloud login, session new, execute
- API key flow: export PLAYWRITER_API_KEY, session new --proxy, execute

MCP tab shows the MCP client JSON config with API key env var.

Uses holocron CodeBlock for Prism syntax highlighting with copy buttons.
Matches existing dashboard card styling (rounded-xl, border-border).

Session: ses_10bb4920bffeAdYhx4RlRNuzEp
2026-06-23 15:11:57 +02:00
Tommy D. Rossi 1b0cc4be6b enable Cloudflare smart placement for website worker
Adds placement.mode = "smart" so the worker runs closer to the D1 database
rather than always at the nearest edge, reducing latency for DB-heavy requests.
2026-06-23 13:24:46 +02:00
Tommy D. Rossi 2f0335d5c7 refine button and outline styles: consistent border-foreground/15, dark mode fixes
- outline button variant: replace generic border+shadow with border-foreground/15 and hover border-foreground/25, explicit bg-white with dark:bg-background
- ghost variant: remove bg-accent hover (just text color change now)
- hero GitHub link: match the new outline border style
- sign out button: same border treatment, switch from double to single quotes for consistency
2026-06-23 13:24:41 +02:00