AI-Created Software

The following AI-generated aka vibe-coded software. The software is specific to my needs and my system and may not work for you or your system. Support is limited, but if you report a bug I'll certainly look into it. All code is provided in a zip file for you to review should you choose.

Page Monitor

A Chrome extension that watches a chosen region of a page and tells you when it actually changes — not when an ad rotates or a view counter ticks over.

The icon is a page with one region singled out — the amber band overhangs the page edges the way the picker’s highlight overhangs the element it has selected, and amber is already the interface’s colour for a change. icons/build-icons.py regenerates the set; the teal rim is there because a navy tile disappears against Chrome’s dark toolbar, taking the silhouette with it.

Install

  1. Open chrome://extensions
  2. Turn on Developer mode (top right)
  3. Click Load unpacked and choose this folder
  4. Pin Page Monitor to the toolbar so the button is reachable

Requires Chrome 116 or later, for chrome.runtime.getContexts.

Use

Open a page you care about, click the Page Monitor button, then Watch a region of this page. Chrome asks for permission to read that site; the picker overlay then takes over the page.

  • Hover to highlight, click to lock a region in
  • widen to the parent, narrow into a child, move between siblings
  • Backspace undoes the last move, Home returns to where you clicked
  • Enter to confirm, Esc to cancel

A click almost always lands on a deeply nested <span> when you meant the card three levels up, so the arrow keys are the part that matters.

They also have to be reversible, which arrow navigation is not on its own: pressing left then up lands on a different parent, and pressing down from there descends into whichever child holds the most text rather than the one you were on. Step past a promising element and there is no way back to it.

So the picker keeps a trail. Every move can be undone with Backspace, in exact reverse order however far you have wandered, and Home jumps straight back to the element you clicked — itself an undoable move. The original click is outlined faintly while you are away from it, and the bar counts how many steps you have taken from it.

Descending also prefers the child you last came up from, so then is a true reversal rather than a jump to a sibling.

A click almost always lands on a deeply nested <span> when you meant the card three levels up, so the arrow keys are the part that matters. Reopen the popup afterwards to name the watch and set how often it runs.

To watch a whole page without picking a region, use Add by URL on the management page.

How it works

Scheduling

One repeating alarm ticks every few minutes and checks which monitors are due, rather than one alarm per monitor. Missed windows are handled for free — if the browser was closed over a due time, the check runs at the next tick, and at startup. Changing an interval needs no alarm bookkeeping, and checks are spaced out so five pages aren’t hit at the same instant.

chrome.alarms is required rather than setInterval because Manifest V3 tears the service worker down after roughly 30 seconds of inactivity.

Nothing runs while Chrome is closed. A daily check on a browser you open twice a week is really a “whenever you next open Chrome” check. If that matters, a server-side watcher is the better tool — see Alternatives below.

Retrieval

Two strategies, chosen per monitor:

Each watch has one setting for how hard Page Monitor tries to load the page, ordered cheapest to most intrusive:

Mode What it does Cost
Automatically Background request, escalating to a hidden tab if the region isn’t in the raw HTML. Remembers which worked. None
Background request, no page Fetches the HTML directly. Can’t see anything drawn by JavaScript. None
Hidden tab A background tab, loaded plainly. A tab briefly in the strip
Hidden tab, told it’s on screen As above, plus the shim below. Same
Foreground tab A real tab in your window, switched to and back. Takes focus for the check
Separate window An unfocused window. Genuinely visible, never takes focus. A window appears

One ordered list rather than two settings, because “how to load the page” and “help it render in the background” were never independent: the second only ever applied when the first chose a tab, and it mixed up how much help a hidden tab gets with where the page opens.

Automatic only ever picks between the two invisible rungs. Escalating on its own to a foreground tab or a window would put a page on screen without being asked, which isn’t a decision to make for someone. When one of those is needed, the failure message says so and the choice stays yours.

Recorded steps and hash-routed URLs both need a real page, so they use a rendering tab whatever is chosen — the note under the dropdown says so rather than letting the setting silently not apply.

Requests send If-None-Match and If-Modified-Since. A 304 skips the parse and the diff entirely, and is polite to the site.

Parsing

MV3 service workers have no DOM, so DOMParser doesn’t exist there. Rather than bundle a JavaScript HTML parser — slower, and subtly different from Chrome’s own parsing — Page Monitor borrows the real one from an offscreen document created with the DOM_PARSER reason.

src/lib/extractor.js is deliberately a classic script hanging off globalThis rather than an ES module, because the identical code has to run in three places: the picker (live DOM), the offscreen document (parsed DOM), and an injected background tab (live DOM). This is not tidiness. If the fetch path and the tab path extracted text even slightly differently, switching between them would look like a content change and fire a false alert.

Selectors

Storing one CSS path breaks the first time the site ships a layout tweak. Page Monitor stores up to six ranked candidates and tries them in order:

  1. A stable id
  2. A test hook — data-testid, data-qa, data-cy, itemprop, and similar
  3. aria-label, optionally with role
  4. A stable class combination
  5. A path anchored to the nearest ancestor that has a real hook
  6. A full positional path, as a last resort

“Stable” excludes anything that looks machine-generated: embedded hex runs, CSS-module suffixes (Button_root__a1b2), emotion classes (css-1x9df2), styled-components (sc-bdVaJa), and runtime state classes (is-active, selected). Every candidate is verified to resolve uniquely before it’s stored.

Drift detection

A selector can still match while pointing at something completely unrelated. Page Monitor stores a token fingerprint of the region at pick time and compares it on each check. If the overlap collapses, it reports the region may have moved and asks you to reselect, rather than reporting a bogus change.

URLs

Three distinct ideas that are easy to conflate:

  • Watch URL — what you gave it, untouched. Shown in the UI, opened on click.
  • Canonical URL — tracking params dropped, the rest sorted. This is the identity of a monitor, so ?utm_source=x doesn’t duplicate a page you already watch, and ?b=2&a=1 matches ?a=1&b=2. Meaningful params are kept, because ?id=1 and ?id=2 really are different pages.
  • Request URL — the canonical URL with the fragment removed. This is what goes over the wire.

Fragments never reach a server, so a fragment can’t be part of the request. It isn’t thrown away, though: #reviews becomes a scoping root, and the region’s selector is resolved inside that section first. That’s more robust than it sounds — the watched region stays correct even when content above it shifts.

Hash routes are the exception. In example.com/#/orders/5 the fragment is the route, and the server returns the same shell HTML whatever it says. These are detected (#/, #!, or a fragment containing ?) and always use a tab.

What counts as a change

Each watch chooses what to compare:

Visible text only (the default). Markup is ignored entirely, so a redeploy that changes nothing but class names stays invisible. This is right for prices, availability wording, headlines, job listings — anything where the words are the information.

Visible text and control state. Adds the attributes that encode the state of a control — disabled, aria-disabled, checked, aria-expanded, aria-selected, open, hidden and similar — plus class names that carry state rather than styling (is-*, has-*, btn--sold-out, out-of-stock).

This is the answer to an item going out of stock where the only difference is the Buy button being disabled. It shows up in the change history as:

- Add to cart [button]
+ Add to cart [button disabled]

Ordinary styling classes and build hashes are still ignored, so this costs almost nothing in false positives. There’s a pleasing symmetry here: the class names rejected as selectors because they toggle at runtime are exactly the ones worth watching as state.

The full HTML of the region. Compares the markup itself. Catches anything, including a great deal you won’t care about.
Three normalisations make it survivable: one tag per line (raw outerHTML is a single enormous line, so any edit would diff as “everything changed”), no indentation (indenting means adding one wrapper shifts every descendant), and alphabetically sorted attributes (servers reorder them freely). Class changes are ignored by default, and a per-watch list of ignored attributes covers framework internals, nonces and CSRF tokens.

Reach for this only when state mode isn’t enough. Expect to add ignore rules.

Reading it. Markup is hard to read, so anywhere an HTML watch’s content is shown — the verification panes, and each recorded change — there is a Markup / Rendered switch.
A change can be viewed as a markup diff, or as the before and after rendered side by side.

Rendering someone else’s HTML inside an extension page is not something to do casually, so it happens in an iframe with two independent protections. The sandbox grants nothing at all, which means no scripts and an opaque origin, so the markup cannot reach the extension page or its APIs. Inside it, a policy of default-src 'none' blocks every network request — without which a remote image would tell the watched site, from your address and on a schedule, that you are monitoring it.

Images therefore show as their alt text rather than loading, and the result never looks like the site: stylesheets were dropped at extraction and class attributes are usually stripped. It only has to be more readable than the source, which for tables and lists it comfortably is.

Note that HTML mode deliberately does not skip CSS-hidden elements, unlike text mode. A parsed document has no computed style, so skipping them would make the background-request path and the tab path disagree — and in HTML mode display:none is often the very state being watched.

On top of the mode, per watch:

  • Ignore patterns — regular expressions stripped before comparison. Good for view counters, “last updated” timestamps, and relative dates.
  • Minimum change size — ignore changes below N characters added or removed. New watches start at 0, reporting every difference, so nothing is missed before you know what a given page is like. Raise it once you see what noise it makes, though ignore patterns are usually the better fix.
  • Link and image addresses — off by default in text and state modes; turn on to catch a button that starts pointing somewhere new. Implied by HTML mode.

Below the threshold, the stored text is still updated, so the same trivial edit isn’t rediscovered on every check.

Changing any of these settings re-baselines the watch. The stored snapshot was produced under different rules, so comparing against it would report a change caused by the method rather than the page. Each snapshot carries a signature of the settings that produced it — including the retrieval mode, since text mode skips CSS-hidden elements and a parsed document can’t detect those — and a mismatch triggers a silent re-baseline instead of a false alert.

Logged-in and personalised pages

Chrome treats a request from an extension to a third-party as same-site when the extension has host permissions for it, so ordinary cookie sessions work in both modes — even SameSite=Strict cookies. Two caveats: this applies to network requests only, and it does not apply if you have third-party cookies blocked, in which case background requests may lose the session while tab mode keeps it.

Cookies are only one way a site remembers you, though, and background requests get none of the others:

How the site remembers you Background request Background tab
Session cookie Yes Yes
Token in localStorage or sessionStorage No Yes
Authorization header set by page JavaScript No Yes
Location or locale stored in a cookie Yes Yes
Location stored in localStorage No Yes
IP-based geolocation Yes, same IP Yes
Browser Geolocation API No Yes
Language and locale From your browser settings Yes

The localStorage rows aren’t a limitation that can be engineered around. A token in localStorage is read by the page’s own JavaScript and attached to requests by that JavaScript; a raw request for the HTML has no way to know it exists. Sites using token-in-localStorage auth can only be watched in tab mode.

This mostly self-corrects: when a background request hits a login wall, the page that comes back doesn’t contain your region, the selector fails, and Automatic mode retries in a tab where your session works — then remembers. Setting the monitor to tab mode deliberately is still better, because it skips a wasted request and can’t be fooled by a selector that happens to match something on the login page.

Set anything signed-in, geo-specific or locale-specific to tab mode.

Background requests send an Accept-Language header built from your browser’s own preferences, so a locale-sensitive site returns the same page it would in a normal tab. (Hardcoding this is a quiet disaster: every check would compare against a page you never see.)

Steps before reading

Some regions don’t exist until a choice is made — a stock notice that appears only once a colour and size are picked, a panel that has to be expanded, a tab that has to be opened.

There are two ways in:

While picking a region, press Need to click first? in the overlay. The page stays where it is and the picker hands over to the recorder. This is the route to use when setting up a new watch.

On an existing watch, press Record steps, which reopens the page and starts recording.

Either way you then use the page normally and Page Monitor records what you did. When you press Done, the same tab hands back to the region picker, because the region can only be selected once the steps have revealed it.

Note that the picker deliberately swallows clicks — otherwise choosing a region would navigate you away mid-selection. If a click seems to do nothing while the picker is up, that’s why, and “Need to click first?” is the way through. Both overlays can also be moved to the top of the window with the ⇅ button, since a bar pinned to the bottom can sit right on top of the control you need.

Recorded steps reuse the same ranked, verified selector builder as watched regions, so a step is exactly as durable as a region and degrades the same way. Between steps the replay polls for the next element rather than sleeping a fixed amount, since a page usually re-renders after a click and the next control may not exist for several hundred milliseconds.

Three things worth knowing before using this:

It forces tab mode. A background request has nothing to click. Any watch with steps loads a real tab on every check, and the mode selector is locked.

Every step replays on every check. Record only what reveals the region. Recording “Add to cart” means adding to cart on a schedule. Nothing in the extension can tell an innocuous click from a destructive one, so this is on you.

Replay is the most fragile part of the extension.
Any redesign can break a step. Failures are reported as themselves — Step 2 of 4 failed: Click Blue — rather than being allowed to surface later as a region that mysteriously stopped matching, and a failed replay never overwrites the last good snapshot.

Watching the steps run

When a watch with steps starts failing, Watch the steps run on its page opens the site visibly and rehearses the recording in front of you. Each step is outlined on the page before it’s acted on, the panel tracks which steps have passed, and it stops where the recording breaks.

It runs the real replay code with observation hooks attached, not a reimplementation — a rehearsal that ran different code would prove nothing.

The panel distinguishes the failures that look identical in a scheduled check:

  • A step’s selectors matched nothing. The saved selectors are listed so you can see what it was hunting for.
  • A dropdown option is gone. It lists the options currently offered, which usually explains it outright — the size you recorded is out of stock, or the labels were reworded.
  • A step matched by a fallback selector. Noted inline. Worth attention: the first choice has already broken, and the next redesign will likely finish the job.
  • Every step ran but the region wasn’t found. The steps are fine and it’s the region that needs reselecting.
  • Everything worked. The region is highlighted and its text shown, compared against what was stored at the last check.

Step through advances one step at a time so you can inspect the page in between, and the panel can be moved to the other side with the ⇄ button.

Typing

Text you type into a box is captured, but only the final value, not the keystrokes. The recorder listens for the browser’s change event, which fires when you leave the field rather than as you type — so a value lands when you click elsewhere, tab away, or press Enter. Pressing Enter is recorded as its own step, so a search box replays as type the query then press Enter, and the synthetic Enter submits the form the way a real one would.

Because only the final value is replayed, a type-ahead that reacts to individual keystrokes may not respond. Replay sets the value through the native property setter and fires input and change, which is what React and Vue watch for, so most autocompletes do work — but if yours doesn’t, click the suggestion during recording instead and that click gets recorded as a step.

Passwords, card numbers and similar are never recorded. Fields are skipped by type and by name, and the recorder says so when it skips one.
Values you type into ordinary fields are stored in plain settings and replayed on a schedule, so treat them accordingly.

Selecting a variant often reloads the page. The recorder saves each step as it happens and re-attaches itself after a navigation, so a reload mid-recording loses nothing.

The watch list

Each row states, in order: how the last run ended, when it ran, how the page is loaded, the schedule, and the next run.

Sorting and filtering

Above the list: a search box, a sort selector, and a button that reverses the order, all sharing one height token so the row lines up rather than each control being padded into rough agreement. Search matches the name and the address, and every whitespace-separated term must appear somewhere, so bank rates narrows rather than widening the way a plain substring match would.

Sorting is by unread changes (the default), name, date created, or last run. Watches missing the value being sorted on stay last in both directions — a column of never-run watches at the top of “last run, oldest first” is technically correct and useless. Ties break by name so equal rows hold a stable order.

Sort and direction are remembered between visits; filter values are not, because arriving at a list that is silently hiding most of itself is disorienting.

Both are registries in src/lib/list-view.js. A sort supplies a value for a watch and the comparator handles direction, missing values and tie-breaking; a filter supplies a test and declares the control it needs. The toolbar builds itself from those definitions, so adding either is an entry in one file and no UI work. Tests assert that every registered entry declares what the toolbar requires, so an incomplete one fails loudly rather than rendering blank.

Reading the current text

Each row also shows the region’s current text: one line always, and a scrollable box on demand. Show text in the header opens the box on every row and remembers the choice; individual rows can be opened on their own.

It isn’t open by default because a fixed box on every row roughly triples the row height, and the list’s main job is spotting which watch needs attention. The single line covers the common case — a row saying “No change” without showing what didn’t change tells you very little — and the box is there when you want to read it.

Rows carry the first 4,000 characters. A watched region can run to megabytes, and sending all of it for every row would make opening the list expensive; longer regions say so and point at the watch’s own page.

The outcome is explicit — Last run OK: No change, or Last run failed: Refused by the site — rather than being left to the colour of the signal bar. Drift counts as a success, because the check itself worked; what it found is reported through the status.

For a watch set to Automatically, the row names what it settled on: Automatic, using background request, or Automatic, using hidden tab. The word “Automatic” alone would leave a watch on the cheap path reading identically to one paying for a tab on every check.

Only a mode that has actually produced a successful read is reported.
The escalation memory can hold a mode that was tried and failed, and presenting that as how the watch runs would point at the wrong thing when something needs diagnosing.

Run history

Every check is recorded, not only the ones that find something. Each watch’s page lists its runs newest first, with the time, the outcome, how long it took, and how the page was actually loaded. Runs that found a change carry a Show change button that opens the diff inline.

This matters because a watch checking cleanly for a week and a watch that silently stopped running look identical when only changes are recorded. The run log is the difference between the two.

For a watch set to Automatically, each row names the rung that actually did the work — Automatic chose background request, or Automatic chose hidden tab with an escalated badge. Without that, there’s no way to tell a watch that settled on the cheap path from one paying for a tab on every single check.

Runs that never reached the page — a missing permission, say — say so rather than claiming a mode. Runs started by hand are marked, so a schedule that has quietly stopped firing can’t be mistaken for a working one on the strength of your own manual checks.

The log is capped per watch (50 by default, in Settings) and is deleted with the watch.

The toolbar popup opens straight to it: a Changes entry sits in the header, carrying a count of anything unread, next to Manage. Navigation is on the first row and the run actions on the second, because four controls beside the title overflow a 384px popup the moment the manual button appears.

Each entry in the Changes list shows the watched address and an Open page link that opens it in a new tab, so a change can be acted on without first finding the watch it came from. The watch’s own page carries the same link on its address.

Links are built through a guard that accepts only http and https. Watch addresses are validated when a watch is created, but they are then stored, exported and imported, so an href is built from data that has been outside the extension — and a javascript: link rendered into a privileged page would run there.

Verifying a watch

Once a watch is running, two things on its page answer “is this actually watching what I think it is?”

Read the page now performs a live read exactly the way a scheduled check would — same mode, same headers, same extraction — but writes nothing. No snapshot, no history entry, no schedule change, no notification. It shows which selector matched and whether it was the first choice or a fallback, whether the page was loaded by background request or tab, the full extracted text beside the text stored at the last check, and a diff if the two differ. Validators are deliberately left off the request, so a 304 can’t turn the preview into a blank answer.

Show me the region on the page opens the page and draws a box around exactly what the monitor resolves to, scrolled into view and labelled with the selector and character count. The text preview tells you what was extracted; only the box on the real page tells you whether that’s the thing you meant. It’s read-only — looking at a watch never changes it.

Between them these cover the failure modes that otherwise stay invisible: a selector that quietly started matching a different element, a background request returning a login wall or the wrong locale, and a fallback selector doing the work because the first one broke.

Permissions

Page Monitor asks for host access one site at a time, when you add a watch. Requesting <all_urls> up front would trigger a frightening install prompt and heavy Web Store review for no benefit.

The picker itself runs under activeTab, granted by your click on the toolbar button. Persistent access, needed for background checks, is requested separately at that same click, because chrome.permissions.request() needs a user gesture and the popup closes the moment the picker takes over the page.

On some platforms the permission prompt dismisses the popup. If that happens, open the watch from the management page and use the Allow reading… button there instead.

Revoking a site’s permission later marks its watches as needing permission rather than failing mysteriously.

Storage

Where What Why
chrome.storage.sync Watch configuration, one item each Roams between your machines. Hard limits: 8 KB per item, 512 items, 100 KB total.
chrome.storage.local Page snapshots and change history Too large for sync, and worthless on another device.

Export and import are on the settings page. Exports are named page-monitor-2026-09-06-143052.json — local date and time to the second, largest unit first so the files sort chronologically by name, and no punctuation in the time part because a colon is not a legal filename character on Windows. An export carries every global setting and every watch-level setting — schedule, load mode, window sizing, recorded steps, compare mode, ignore rules, the region’s selectors and fingerprint — and restores them into a fresh installation.

Three things it deliberately does not carry:

Run history and status. The importing machine has no snapshots, so inherited status would describe a check that never happened there — a watch claiming “Last run OK: No change, 2 hours ago” with no stored text behind it, and an unread badge for a change nobody can open. Imported watches start as not run yet.

Snapshots, change history and run logs. Large, machine-specific, and meaningless without the page state they were taken from.

Site permissions. Chrome grants those to an installation; they cannot travel in a file. Every imported watch needs access allowed again from its own page, and the import message says so.

Imported scheduled watches are queued straight away so they can take a baseline; manual ones wait to be run. Watches already present are skipped rather than duplicated, unknown keys in a file are discarded rather than stored, and a file from another tool is refused by name.

Two tests guard completeness by deriving the field list from the code, so adding a setting without covering it fails loudly rather than leaving it silently outside the export.

Things that will bite you

Bot detection. Background requests carry a header and TLS fingerprint that some CDNs treat as a bot. A 403 or 429 is reported as the site refused the background request, with the fix: switch that monitor to tab mode.

Logged-in pages. Cookies ride along on background requests where host permission exists. Sites using short-lived, JavaScript-set tokens need tab mode.

Tab mode is visible. A pinned background tab appears briefly in the tab strip. There’s no way around this — it’s the only way to see rendered content.

Very heavy pages. Tab mode waits for load plus a settle delay (2.5s by default, adjustable per monitor). Slow single-page apps may need more.

Pages that refuse to render in the background. A hidden tab differs from a visible one in ways no settle delay fixes: visibilityState reads 'hidden', requestAnimationFrame does not fire at all, and setTimeout is clamped to about a second. If a page’s variant logic runs through rAF, it never completes, however long you wait. The classic symptom is a rehearsal that works perfectly while every scheduled check fails.

Hidden tab, told it’s on screen installs a shim at document_start in the page’s own world that reports visibilityState as visible and keeps animation frames flowing by pairing each requestAnimationFrame with a timer, whichever fires first. Harmless when it wasn’t needed. Automatic uses this rung, so most sites of this kind are handled without intervention.

Foreground tab opens a normal tab in your current window and switches to it, then puts your previous tab back when the check finishes. The page is genuinely on screen and inherits your window’s size, so nothing has to be guessed and no windows are rearranged. It does take over the screen for the length of the check, which is disruptive if it lands mid-task — best for watches that run a few times a day rather than every few minutes.

Focus is restored before the check tab is closed, not after: closing an active tab lets Chrome pick whichever neighbour it likes, which would leave you somewhere you never asked to be.

Separate window uses an unfocused browser window instead of a hidden tab. A tab in an on-screen window is genuinely visible and gets real animation frames, so the page behaves exactly as it does when you watch it. Less interrupting than a foreground tab, since it never takes focus, at the cost of a window appearing and disappearing.

The window is maximized by default. This isn’t cosmetic: window width drives responsive breakpoints, and a small window can render a tablet layout in which the watched region doesn’t exist at all. Same as your current window copies whatever size you’re browsing at, and a fixed size pins it when a region only appears above a particular width. Changing any of these re-baselines the watch, because a different layout legitimately produces different text.
Hidden tab loads plainly, for the rare case where the shim interferes with a site.

When a tab check can’t find its region, the failure now reports what the page looked like: whether it still considered itself hidden, and whether it loaded essentially empty. Those two point at rendering rather than at a broken selector.

Lazy-loaded content. A background tab never scrolls, so content behind an IntersectionObserver may not load. Page Monitor scrolls each action target and the watched region into view before acting on them.

Browser Geolocation. A site calling the Geolocation API gets a real answer only in tab mode, and only where you have already granted that site location permission. Results in a hidden tab may be delayed — worth verifying per site.

Tests

cd test
npm install jsdom
npm test                  # all three, in order

node run-lint.mjs         # static check for undeclared references
node run-tests.mjs        # 168 unit tests
node run-integration.mjs  # 125 integration tests

run-lint.mjs exists because node --check only validates syntax. A rename that half-applies — the old constant left under its old name, the new name referenced but never declared — parses perfectly and then throws in the browser. That happened during the load-mode merge and reached a user, so there is now a check for it: every name called as a function and every SHOUTING_CASE constant must be declared or imported in its file.

Getting this right needed a proper scanner rather than regex stripping. Comments, strings, regex literals and nested template literals all have to be tracked, because a pattern like /[&<>"']/g or a template whose ${...} hole contains another template will desynchronise a naive strip — and a desynchronised strip hides real declarations, which is a worse failure than the one it was meant to catch.

The unit suite runs the real extractor inside jsdom against realistic markup: selector generation and survival across a simulated redeploy, fragment scoping, whitespace normalisation, drift detection, and diffing. One test asserts that the fetch path and the tab path produce byte-identical text for the same page.

The integration suite drives the real checkMonitor against a mocked Chrome API, a mocked network, and a real DOMParser: baseline capture, unchanged detection, noise outside the region, 304 short-circuiting, ignore patterns, thresholds, lost selectors, blocked requests, backoff, permission gating, hash-route routing, and that the verification preview reads live without mutating any stored state.

Two genuine bugs were found this way, both of which would have caused exactly the false alerts the extension exists to avoid:

  1. Newlines inside a text node became line breaks, so a site reformatting its HTML source looked like a content change on an identical page.
  2. Adjacent inline elements fused — <span>£249</span><span>1,284 views</span> extracted as £2491,284 views, so an ignore pattern for the counter matched from the 2 in 249 and silently ate the price too.

Alternatives

If you’d rather not run this in a browser, changedetection.io is open-source and self-hostable, with the significant advantage of running on a server instead of depending on Chrome being open. Distill.io and Visualping are the hosted commercial options. None of them offer quite the same select-this-region-on-the-page-I’m-looking-at flow.

Layout

manifest.json
src/
  lib/
    extractor.js     selector generation, resolution, text extraction (shared)
    url-utils.js     canonicalisation, fragments, hash-route detection
    storage.js       sync configs, local snapshots and history
    actions.js       replaying recorded steps in a tab
    wake.js          makes a hidden tab render like a visible one
    diff.js          line diff, change sizing, hashing
    list-view.js     sort and filter registries for the watches list
    modes.js         load-mode and outcome labels shared by both interfaces
    render.js        the sandboxed document used to display watched markup
    schedule.js      clock times, next-run arithmetic, descriptions
    base.css         design tokens
  background/
    service-worker.js  lifecycle and message router
    scheduler.js       tick alarm and due-monitor scanning
    checker.js         retrieval, comparison, notification
  offscreen/           DOMParser host
  content/             picker, highlighter, step recorder and rehearsal overlays
  popup/               toolbar UI
  options/             management, diff viewer, settings
test/
Download Release:
⬇️ Page Monitor.zip
(1 votes, average: 5.00 out of 5)

Leave a Reply

Your email address will not be published. Required fields are marked *

Notify me of followup comments via e-mail.