What is the Page Change Detector?
The Page Change Detector is a built-in LumaBrowser extension that monitors web pages for content changes. You point it at a URL, optionally click a few elements you care about, and it checks the page on a schedule. When something changes, you get a webhook POST and an optional desktop notification, with a human-readable diff of what was added and removed.
It is the open, local-first alternative to cloud services like Distill.io, Visualping, ChangeTower, Wachete, and Hexowatch — with one important advantage: checks run inside a real browser, so JavaScript-rendered pages, single-page apps, and authenticated dashboards work without contortions.
- Price & stock alerts — pick the price element on a product page and get a webhook the moment it drops or comes back in stock.
- Competitor pricing pages — watch the pricing tier rows. Forward to a Slack channel for go-to-market visibility.
- Documentation & release notes — know the second a vendor updates their changelog, deprecates an API, or ships a new version.
- Job boards & listings — monitor a search results page for new postings, properties, tickets, or auction items.
- Status pages & incident banners — pick the status badge selector and pipe state changes to your alerting stack.
- Internal dashboards — with no-refresh mode, watch a metric tile on a dashboard you already keep open without ever opening another tab.
- Government & regulatory pages — legal filings, permit lists, public notices, tariff schedules.
- Tickets, releases, drops — concert seats, sneaker restocks, beta signups, registration windows opening.
| Concern | Cloud monitors (Visualping, Distill, etc.) | LumaBrowser Page Change Detector |
|---|---|---|
| JavaScript-rendered pages | Often costs extra; sometimes unsupported entirely | Built in — every check runs in a real Chromium tab |
| Authenticated pages & dashboards | Requires sharing credentials or upgrading to enterprise | Use the same logged-in browser session you already have |
| Pricing model | Per-check / per-monitor / per-frequency tiers | Free with LumaBrowser. Run as many monitors as your machine handles |
| Where the data lives | On a vendor's servers | In a local SQLite database on your machine |
| Element picker | Yes (varies by tier) | Yes — in-page overlay, multi-select, persisted per monitor |
| Webhook on change | Often a paid feature | Built in — structured JSON to any URL |
| Programmability | Web UI only | Full REST API: create, update, history, manual check, run picker |
| Scheduling jitter | Fixed intervals | Optional ±jitter % so monitors don't fire in lockstep |
- Open the page. A silent (hidden) tab is created for the URL so it never clutters your tab bar. If you've enabled no-refresh mode, the existing open tab is reused instead.
- Extract the text. Either the full page text, or the concatenated text of the specific elements you picked.
- Hash it. A SHA-256 checksum is computed and compared to the last snapshot stored in SQLite.
- If it changed, a line-level diff is computed (added vs. removed lines, with snippet previews) and a record is appended to the monitor's history.
- Notify. Optionally fire a desktop notification, and POST a structured JSON payload to your webhook URL.
- Sleep, then repeat. The next interval is scheduled with optional ±jitter to avoid every monitor firing on the same second.
Watching the whole page is easy — and noisy. A page header re-render, a tracking pixel, a footer copyright year tick — any of them will trip a full-page checksum. The fix is to watch only the elements that matter.
Open a monitor's settings, click Pick elements, and the page opens in a real tab with a live overlay. Hover to highlight, click to select, click again to deselect. The picker captures stable CSS selectors (preferring IDs, data attributes, and structural paths) and persists them on the monitor.
From that point forward, the checksum is computed against just those elements — so a price tile that flips from "$24.99" to "$19.99" trips the alert, and an unrelated banner change in the header does not.
Default: silent background tab
Each cycle, a hidden tab navigates to the URL, lets the page load, runs the extraction, then sticks around for next time. It never appears in your tab bar. This is the right default for any page that doesn't update itself.
No-refresh mode: piggy-back on an open tab
Some pages update themselves — status dashboards over WebSockets, live tickers, internal SPAs polling their own backends. For those, no-refresh mode tells the monitor: don't ever open or refresh a tab; just look at whatever's already there. If the page has no open tab, the cycle is skipped quietly.
This is the mode that makes monitoring a dashboard you keep open all day genuinely free — no extra page loads, no extra requests to the backend, no risk of tripping rate limits.
Each monitor has its own check interval — default 300000 ms (5 minutes), configurable from seconds to days. The next-fire time is computed per cycle, so a slow check doesn't push every following check off the clock.
If you set intervalJitterPercent, every cycle picks a random delay within ±N% of the base interval. With twenty monitors all on a 5-minute schedule, jitter prevents them from firing in lockstep on the same second — useful when each fire spawns a hidden tab and a webhook POST.
// Base 5 min, ±20% jitter → next fire somewhere in [240s, 360s]
{
"checkIntervalMs": 300000,
"intervalJitterPercent": 20
}
Quick start
1. Create a monitor
curl -X POST http://localhost:3000/api/page-change-detector \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Pricing Page",
"url": "https://acme.com/pricing",
"checkIntervalMs": 300000,
"intervalJitterPercent": 10,
"webhookUrl": "https://hooks.slack.com/services/XXX/YYY/ZZZ",
"desktopNotifications": true,
"enabled": true
}'
2. (Recommended) Pick the elements to watch
Open the Monitors side panel in LumaBrowser, expand your monitor, and click Pick elements — then click the price tiles, the headline, the in-stock badge, whatever you actually care about. Or trigger it from the API:
curl -X POST http://localhost:3000/api/page-change-detector/mon_xxx/pick
This opens the URL in a foreground tab, overlays the picker, and waits for you to click Done. The chosen selectors are saved on the monitor.
3. Force the first check
curl -X POST http://localhost:3000/api/page-change-detector/mon_xxx/check
The first check is always recorded as initial snapshot — subsequent checks compare against it. From here on, you'll get webhooks and notifications whenever the chosen text changes.
4. Inspect history
# Most recent 20 entries
curl http://localhost:3000/api/page-change-detector/mon_xxx/history
# Paginated, change events only
curl "http://localhost:3000/api/page-change-detector/mon_xxx/history?page=0&pageSize=25&changedOnly=1"
When a change is detected and webhookUrl is set, this JSON is POSTed with Content-Type: application/json:
{
"monitorId": "mon_1714742312000_a4b1k2",
"monitorName": "Acme Pricing Page",
"url": "https://acme.com/pricing",
"timestamp": "2026-05-03T17:22:08.412Z",
"checksum": "9f3c1b2e... (sha256 of the new extracted text)",
"prevChecksum": "1a8d04bf... (or null on the first detected change)",
"diffSummary": "+2 / -1\n+ $19.99 — In stock, ships in 1–2 days\n- $24.99 — Out of stock",
"textPreview": "First 2000 characters of the new extracted text…",
"textLength": 4821,
"changeCount": 17
}
Two notes:
diffSummaryis a compact, multi-line string. Line 0 is the stats header (+N / -M, orwhitespace / formatting only); subsequent lines are up to four added (+) and four removed (-) snippets, each truncated to 140 characters.- For the first detected change on a fresh monitor,
prevChecksummay benull— treat the first webhook as a baseline notification.
All endpoints are mounted at /api/page-change-detector on the local LumaBrowser HTTP server (default http://localhost:3000). Responses follow the standard envelope: {"success": true, ...} on the happy path; {"success": false, "error": "..."} on failure.
| Endpoint | Description |
|---|---|
GET /api/page-change-detector | List all monitors. Returns { success, monitors, count }. |
POST /api/page-change-detector | Create a monitor. Body fields below. Returns { success, monitor }. |
GET /api/page-change-detector/:id | Fetch a single monitor by ID. |
PATCH /api/page-change-detector/:id | Partial update. Any field accepted by create can be patched, plus selectors (an array of CSS selectors, or null to clear). |
DELETE /api/page-change-detector/:id | Delete a monitor and its history. |
GET /api/page-change-detector/:id/history | Change history. Default returns the most recent ?limit=20. Pass ?page, ?pageSize, or ?changedOnly=1 to switch into paginated mode (response then includes total, page, pageSize, changedOnly). |
POST /api/page-change-detector/:id/check | Force an immediate check. Useful for verifying a webhook URL or seeding the first snapshot. |
POST /api/page-change-detector/:id/pick | Open the URL in a foreground tab and launch the in-page element picker. Resolves when the user clicks Done or Cancel. Response is { success, cancelled, selectors?, monitor? }. |
Update example: switch to a tighter schedule
curl -X PATCH http://localhost:3000/api/page-change-detector/mon_xxx \
-H "Content-Type: application/json" \
-d '{
"checkIntervalMs": 60000,
"intervalJitterPercent": 25
}'
Update example: clear element selectors (fall back to full page)
curl -X PATCH http://localhost:3000/api/page-change-detector/mon_xxx \
-H "Content-Type: application/json" \
-d '{ "selectors": null }'
| Field | Type | Default | Description |
|---|---|---|---|
name | string | — | Required. Human-readable label for the monitor. |
url | string | — | Required. The URL to monitor. Bare hostnames are upgraded to https://. |
checkIntervalMs | integer (ms) | 300000 (5 min) | How often the monitor checks. Floor of 1000 ms. |
intervalJitterPercent | integer (0–100) | 0 | Random ±% offset applied per cycle. Prevents lockstep across many monitors. |
webhookUrl | string | empty | Where to POST the change payload. Empty = no webhook. |
desktopNotifications | boolean | true | Show a desktop notification on detected changes. |
enabled | boolean | true | Run the schedule. Set false to pause without deleting. |
noRefreshRequired | boolean | false | Piggy-back on an existing open tab; never create a new one. Cycles where the page isn't open are silently skipped. |
selectors | array of strings, or null | null | CSS selectors to extract text from. null or empty means watch the whole page text. Set via the picker, or PATCH with an array directly. |
Frequently asked questions
How is this different from setting up a cron job that curls a page?
A cron curl only sees server-rendered HTML. Modern sites render most of their content in the browser with JavaScript, so the price you see and the price curl sees are often completely different. LumaBrowser checks inside a real Chromium tab, so what the monitor sees is what your users see — including content rendered by React, Vue, Svelte, or any other framework. You also get the element picker, structured webhooks, history, and a UI without writing any of it.
Does the monitor actually open visible tabs every five minutes?
No. By default the monitor uses a silent tab — created and reused in the background so it never appears in your tab bar. In no-refresh mode, no tab is created at all; the monitor simply reads whatever tab you already have open at that URL.
Can I monitor a page that requires login?
Yes. The monitor runs in your normal LumaBrowser session, so it inherits cookies and authentication state. Log in once, leave the session active, and the monitor sees the page exactly as you do.
What happens if the page is briefly down or returns an error?
If text extraction fails, the cycle is logged and skipped — no false-positive change is recorded. The next cycle tries again. Brief outages don't pollute your history with phantom diffs.
How long is change history kept?
Indefinitely — every check is appended to a SQLite table on your machine, and the dedicated history view supports paginated queries with a "changes only" filter. Delete the monitor to drop its history; or run a SQL DELETE if you want to prune by age.
Can I trigger anything more sophisticated than a webhook on change?
Yes. The webhook payload includes the diff summary, change count, checksum, and the first 2 KB of the new content — pipe it into n8n, Zapier, an HTTP-triggered Lambda, or your own service. From there you can rerun a fuller LumaBrowser automation (template extraction, AI Chat workflow), open a ticket, send an SMS, anything.
Is there a hard limit on the number of monitors?
None imposed by the extension. Practically, the limit is what your machine can handle — each silent check briefly opens a tab, so hundreds of monitors on tight schedules will compete for memory. Use jitter to smooth out the load and longer intervals where seconds don't matter.