Smyths scraper — hourly run

A Linux container wakes every hour, scrapes 40 Smyths Toys product pages, diffs against the previous run, and commits the results to this repo. This page is rendered from those commits.

Loading status…
Last 24h success rate
 
Runs in last 24h
 
Changes detected (24h)
 

What this is

A single-purpose product watcher. Every hour it pulls the same 40 Smyths Toys product pages, parses each into structured fields (name, price, availability, image, category), and compares the result against the previous run.

Anything that moved — a price change, a stockout, a rename — shows up in the changes feed. Pages that throw an hCaptcha are solved by a free vision model and the puzzle is saved with its verdict. A run that fails becomes a row in the timeline with the error tail attached.

No database, no server: the scraper commits JSON files to this repo, GitHub Pages serves them, and the browser renders them.

How it works

Live timeline

Reverse-chronological. Newest run at top. Click any row to see captcha counts, Tor rotations, duration, and (on failures) the stderr tail.

Loading timeline…

Last detected changes

Field-level diff against the previous run's snapshot (data/runs/<prev>.ndjson), matched by SKU. The first run has nothing to compare to, so everything is new; after that, only what actually moved — price, stock, name, image, category — appears here. Price moves are coloured down / up.

Loading changes…

Captchas the solver saw

A challenged Tor exit gets an hCaptcha “select in order” doodle puzzle. The vision model reads the legend, locates each target, and clicks them in order. The coloured rings below mark where it clicked; the verdict says whether it cleared.

Loading captchas…

Issues we hit + how we solved them

Tor exit rotation was a silent no-op in the container.The Dockerfile baked a control-port password hash that didn't match the plaintext the app sends (tor --hash-password salts randomly), so every SIGNAL NEWNYM failed auth (515) and a flagged exit could never be left. Fixed three ways: the hash is generated at build time so it always matches, rotateExit() now checks for a real 250 OK instead of assuming success, and the container verifies rotation auth at boot and refuses to start otherwise.
Free vision providers returned 503/429 under load.The solver chains Gemini, OpenRouter Qwen3-VL, Groq and Nvidia and rotates on rate-limit or 5xx. If every free tier is saturated, the lane rotates the Tor exit instead — a fresh exit is often not challenged at all.
Vanilla Playwright was blocked instantly by Imperva.patchright (a Chromium fork) strips the CDP automation signals Imperva fingerprints.
Headless Chrome was detected even with stealth.Run real headed Chrome inside Xvfb, a virtual display. Nothing reaches a physical screen, which is what lets it run in a container with no GUI.
A single IP was rate-limited within a few requests.A local Tor instance rotates the exit circuit with SIGNAL NEWNYM on consecutive failures.
Retina screenshots came back at 2× page coordinates, so clicks landed at half-position.page.screenshot({ scale: 'css' }) puts the solver and the page on one coordinate system.
A Docker build failed on a transient apt-get mirror error.The Dockerfile is idempotent; rerunning cleared it.

Reliability

Two distinct things. Resilience is how a single run keeps itself going; it runs inside the container. Self-healing is what happens around the runs — recovery and code repair — and lives on the host, not in the container.

Resilience — inside each run (in the container)

  • Tor exit rotation: SIGNAL NEWNYM on consecutive failures or a flagged exit.
  • Vision-provider rotation across Gemini, OpenRouter, Groq and Nvidia on rate-limit or 5xx.
  • Randomised pacing with the odd longer pause, and a retry on a refreshed session.
  • A boot check that aborts the run if Tor control auth is misconfigured.

This is ordinary fault tolerance, not healing — it just gets one scrape finished.

Self-healing — around the runs (on the host)

  • The 15-minute tick kills a container that overruns, lets the next tick retry, and records a failed run on the timeline with its error tail.
  • A Claude Code agent reviews health and, when a run fails for a new reason (markup drift, a provider change), diagnoses it from the logs, patches the scraper code, rebuilds the image, and pushes to the smyths-scraper repo. The next run picks up the fix.
  • Every code change lands as a normal commit; see the Agent activity feed below.

What runs where: the container is the scrape plus its resilience. The host holds the schedule, the diff/timeline, the dashboard, and the repair agent — none of which ships to a deployment of the container.

Recent failure / heal events

No failures recorded yet.

Agent activity

Fixes, improvements and reviews the Claude Code agent has made, newest first. Each maps to a commit in the repos.

Loading agent activity…

Where this plugs in

This is a demo, so the integration points still being agreed use sensible placeholders. None of them touch the scrape engine — they are where it reads input and writes output.

Integration pointWhat the demo uses
Output schemaa factory JSON shape in transform.js — swap the mapper for the real field contract
URL list sourcea fixed file of 40 URLs; the job already takes a list, so this is just where the list is read from
Triggera local 15-minute cron; in production a worker, queue or scheduler calls the job instead
Runtimea Docker linux/amd64 image — the same image runs anywhere that env runs
Output sinkNDJSON committed to this repo; point it at a bucket, a database or a callback

Most of the moving parts are optional and switch off with a single env var — no code change:

Once the schema, list source, trigger, runtime and output sink are confirmed, only that input/output layer changes.

Repos & lineage