<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: SignalWatch</title>
    <description>The latest articles on DEV Community by SignalWatch (@signalwatch).</description>
    <link>https://dev.to/signalwatch</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4140352%2F12d40a22-db08-430d-b5f3-c61592458d52.png</url>
      <title>DEV Community: SignalWatch</title>
      <link>https://dev.to/signalwatch</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/signalwatch"/>
    <language>en</language>
    <item>
      <title>An invoice PDF that never leaves the browser</title>
      <dc:creator>SignalWatch</dc:creator>
      <pubDate>Sun, 04 Oct 2026 02:40:43 +0000</pubDate>
      <link>https://dev.to/signalwatch/an-invoice-pdf-that-never-leaves-the-browser-5e5a</link>
      <guid>https://dev.to/signalwatch/an-invoice-pdf-that-never-leaves-the-browser-5e5a</guid>
      <description>&lt;p&gt;I wanted a one-page invoice I could send without opening a billing app, and without pasting a client's name into someone else's database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://clearquire.litework.me" rel="noopener noreferrer"&gt;Clearquire&lt;/a&gt; is a free page that does that. You type the seller, the client, the lines, an optional tax rate, notes, and payment instructions. The browser writes an A4 PDF. You download it and email it yourself.&lt;/p&gt;

&lt;p&gt;Nothing in the form is uploaded. There is no account. An optional draft can sit in this browser's localStorage, and that switch starts off. A logo (PNG, JPEG, or WebP, up to 2 MB) is resized in the browser and drawn on the PDF.&lt;/p&gt;

&lt;p&gt;There are two templates. Minimal keeps the page quiet. Modern puts an accent colour in a header band. GBP is the default currency. USD, EUR, and a longer list are there too. Symbols and decimal separators follow the browser language.&lt;/p&gt;

&lt;p&gt;It is a layout tool. It does not choose a VAT rate, file a return, take a payment, or connect to a bank. The page says that plainly.&lt;/p&gt;

&lt;p&gt;The useful part, if you build similar tools, is the boundary. The server never receives the invoice. The PDF is generated in the page. That is the whole product.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Fail a GitHub Actions job when a vendor page changes</title>
      <dc:creator>SignalWatch</dc:creator>
      <pubDate>Tue, 29 Sep 2026 00:43:53 +0000</pubDate>
      <link>https://dev.to/signalwatch/fail-a-github-actions-job-when-a-vendor-page-changes-1c0g</link>
      <guid>https://dev.to/signalwatch/fail-a-github-actions-job-when-a-vendor-page-changes-1c0g</guid>
      <description>&lt;p&gt;A scheduled workflow can fail when a vendor pricing page, docs page, or changelog changes its text. The job below reads the latest event from a SignalWatch monitor and exits 1 if that event is a content change from the last six hours.&lt;/p&gt;

&lt;p&gt;This is for public, server-rendered pages. It does not render JavaScript, log in, or take a screenshot. If the price only exists after a client-side render, the monitor will not see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;.github/workflows/upstream-drift.yml&lt;/code&gt;. It uses GNU &lt;code&gt;date&lt;/code&gt;, which &lt;code&gt;ubuntu-latest&lt;/code&gt; provides.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;upstream-drift&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*/6&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Fail if the vendor page changed in the last 6h&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SIGNALWATCH_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;CHECK_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;123"&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;latest=$(curl -fsS "https://signalwatch.litework.me/api/v1/checks/$CHECK_ID/events?limit=1" -H "X-API-Key: $KEY")&lt;/span&gt;
          &lt;span class="s"&gt;event=$(echo "$latest" | jq -r '.[0].event // "none"')&lt;/span&gt;
          &lt;span class="s"&gt;when=$(echo "$latest" | jq -r '.[0].created_at // "1970-01-01T00:00:00Z"')&lt;/span&gt;
          &lt;span class="s"&gt;age=$(( $(date +%s) - $(date -d "$when" +%s) ))&lt;/span&gt;
          &lt;span class="s"&gt;if [ "$event" = "changed" ] &amp;amp;&amp;amp; [ "$age" -lt 21600 ]; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "::error::Upstream page changed"&lt;/span&gt;
            &lt;span class="s"&gt;echo "$latest" | jq -r '.[0].diff'&lt;/span&gt;
            &lt;span class="s"&gt;exit 1&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;
          &lt;span class="s"&gt;echo "no recent change ($event, ${age}s ago)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the API key as a repository secret named &lt;code&gt;SIGNALWATCH_KEY&lt;/code&gt;. Replace &lt;code&gt;123&lt;/code&gt; with the monitor id from the dashboard.&lt;/p&gt;

&lt;p&gt;The events endpoint returns the newest row first. On that API a content change is &lt;code&gt;event: "changed"&lt;/code&gt;. Webhook deliveries for the same moment use &lt;code&gt;content.changed&lt;/code&gt;. &lt;code&gt;diff&lt;/code&gt; is a unified text diff. &lt;code&gt;change_ratio&lt;/code&gt; is changed characters divided by the longer of the two texts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the monitor
&lt;/h2&gt;

&lt;p&gt;Free covers 3 monitors at a 15 minute interval, no card: &lt;a href="https://signalwatch.litework.me/" rel="noopener noreferrer"&gt;https://signalwatch.litework.me/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two settings matter more than the schedule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A CSS selector, so you hash the price table or the docs section instead of the whole document.&lt;/li&gt;
&lt;li&gt;An ignore regex, so a line like "updated 3 minutes ago" does not fail the build.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is a public monitor on the Hacker News front page if you want to read real diffs before creating an account: &lt;a href="https://signalwatch.litework.me/live.html" rel="noopener noreferrer"&gt;https://signalwatch.litework.me/live.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Curl, Python, and signature checks, including this workflow, are on the examples page: &lt;a href="https://signalwatch.litework.me/examples.html" rel="noopener noreferrer"&gt;https://signalwatch.litework.me/examples.html&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What a failure looks like
&lt;/h2&gt;

&lt;p&gt;The Actions log prints the diff and the job goes red:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;--- before
&lt;/span&gt;&lt;span class="gi"&gt;+++ after
&lt;/span&gt;&lt;span class="p"&gt;@@ -3 +3 @@&lt;/span&gt;
&lt;span class="gd"&gt;-Starter $10/mo
&lt;/span&gt;&lt;span class="gi"&gt;+Starter $14/mo
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Downtime is a different event (&lt;code&gt;down&lt;/code&gt;, then &lt;code&gt;up&lt;/code&gt; when the fetch recovers). Those do not fail this job. Extend the &lt;code&gt;if&lt;/code&gt; if a failed fetch should fail CI as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;p&gt;Public http(s) URLs only. Fetches are capped at 2 MB and follow up to 3 redirects. JavaScript is not executed.&lt;/p&gt;

&lt;p&gt;Email, Slack, Discord, and HMAC-signed JSON are the push path. This workflow is the pull path, for a repo that already lives in Actions and wants upstream drift to turn the badge red.&lt;/p&gt;

&lt;p&gt;The same idea as a push into n8n: &lt;a href="https://dev.to/signalwatch/run-an-n8n-workflow-when-a-pages-text-changes-38m0"&gt;https://dev.to/signalwatch/run-an-n8n-workflow-when-a-pages-text-changes-38m0&lt;/a&gt;&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>devops</category>
      <category>github</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Run an n8n workflow when a page's text changes</title>
      <dc:creator>SignalWatch</dc:creator>
      <pubDate>Thu, 24 Sep 2026 04:12:02 +0000</pubDate>
      <link>https://dev.to/signalwatch/run-an-n8n-workflow-when-a-pages-text-changes-38m0</link>
      <guid>https://dev.to/signalwatch/run-an-n8n-workflow-when-a-pages-text-changes-38m0</guid>
      <description>&lt;p&gt;The last post was the product. This one is the wiring: an n8n Webhook trigger that runs when a public page's text changes, with the unified diff in the payload.&lt;/p&gt;

&lt;p&gt;Screenshot monitors are built for an inbox. If you already live in n8n, you want a POST you can branch on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An n8n instance (cloud or self-hosted) with a Webhook node&lt;/li&gt;
&lt;li&gt;A public URL whose text you care about: a pricing page, a changelog, a status page, a terms page&lt;/li&gt;
&lt;li&gt;A SignalWatch monitor. Free is 3 monitors at 15 minutes, no card: &lt;a href="https://signalwatch.litework.me/" rel="noopener noreferrer"&gt;https://signalwatch.litework.me/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does not render JavaScript and it does not log in. If the price only exists after a client-side render, this will not see it. Server-rendered HTML is the fit.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Catch the webhook in n8n
&lt;/h2&gt;

&lt;p&gt;Add a Webhook node. Method POST. Copy the &lt;strong&gt;production&lt;/strong&gt; URL (the test URL is only for a trial run).&lt;/p&gt;

&lt;p&gt;n8n's default Webhook output looks like &lt;code&gt;{ headers, params, query, body }&lt;/code&gt;. The event fields are under &lt;code&gt;body&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Point a monitor at that URL
&lt;/h2&gt;

&lt;p&gt;Create a monitor for the page. Two noise controls matter more than the schedule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CSS selector&lt;/strong&gt;, so you hash one region (the price table) instead of the whole document&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignore regex&lt;/strong&gt;, so "updated 3 minutes ago" does not page you&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Paste the n8n production URL into the monitor's webhook URL. Press &lt;strong&gt;Send test alert&lt;/strong&gt;. The Webhook node receives an event with &lt;code&gt;"event": "test"&lt;/code&gt;. Map your later nodes from that payload so you are not waiting for a real change.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Branch on the event
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="err"&gt;$json.body.event&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your Webhook node is set to put the JSON at the top level, drop the &lt;code&gt;.body&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;content.changed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{{$json.body.diff}}&lt;/code&gt; is a unified diff. &lt;code&gt;change_ratio&lt;/code&gt; is how much of the text moved. Post the diff to Slack, append a row, or hand it to an LLM node for one sentence.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;check.down&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The fetch failed. &lt;code&gt;error&lt;/code&gt; says why.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;check.up&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;It recovered.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The button. Use it to pin the schema.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A real change looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"check_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Competitor pricing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/pricing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"content.changed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"change_ratio"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.031&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"diff"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--- before&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;+++ after&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;@@ -3 +3 @@&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;-Starter $10/mo&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;+Starter $14/mo"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Slack and Discord webhook URLs are special-cased: paste &lt;code&gt;https://hooks.slack.com/...&lt;/code&gt; or a Discord webhook and you get a formatted message with a fenced diff, not this raw JSON. Everything else, including n8n, gets the JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Check the signature if the workflow does something expensive
&lt;/h2&gt;

&lt;p&gt;Every delivery sends:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;X-SignalWatch-Event&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;X-SignalWatch-Delivery&lt;/code&gt; (new id per attempt)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;X-SignalWatch-Timestamp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;X-SignalWatch-Signature: sha256=...&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The signature is HMAC-SHA256 of &lt;code&gt;timestamp + "." + raw body&lt;/code&gt;, using the webhook signing secret on the dashboard. Reject timestamps older than five minutes. An n8n Crypto node can do the compare before you open a ticket or spend an LLM call.&lt;/p&gt;

&lt;p&gt;Deliveries are queued. If n8n is down, they retry with backoff for hours instead of dying inside the poll.&lt;/p&gt;

&lt;h2&gt;
  
  
  See a real diff first
&lt;/h2&gt;

&lt;p&gt;There is a public monitor on the Hacker News front page, scoped to story titles, so you can read actual diffs with no account: &lt;a href="https://signalwatch.litework.me/live.html" rel="noopener noreferrer"&gt;https://signalwatch.litework.me/live.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Curl, Python, and Node signature checks are on the examples page: &lt;a href="https://signalwatch.litework.me/examples.html#n8n" rel="noopener noreferrer"&gt;https://signalwatch.litework.me/examples.html#n8n&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The product post, if you want the constraints in one place: &lt;a href="https://dev.to/signalwatch/i-wanted-the-diff-not-a-screenshot-a-small-url-change-api-1aoh"&gt;https://dev.to/signalwatch/i-wanted-the-diff-not-a-screenshot-a-small-url-change-api-1aoh&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you wire this up, I want to know which page you pointed it at and which noise filter you actually needed.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>automation</category>
    </item>
    <item>
      <title>I wanted the diff, not a screenshot: a small URL-change API</title>
      <dc:creator>SignalWatch</dc:creator>
      <pubDate>Thu, 24 Sep 2026 04:05:26 +0000</pubDate>
      <link>https://dev.to/signalwatch/i-wanted-the-diff-not-a-screenshot-a-small-url-change-api-1aoh</link>
      <guid>https://dev.to/signalwatch/i-wanted-the-diff-not-a-screenshot-a-small-url-change-api-1aoh</guid>
      <description>&lt;p&gt;I kept wanting one thing: paste a public URL, and get a unified diff when the text changes — in email, Slack, or a JSON webhook I can point at n8n.&lt;/p&gt;

&lt;p&gt;Screenshot tools are built for that inbox moment ("something changed, go look"). Self-hosting changedetection.io is the right answer if you want a container. I wanted the hosted, boring version: visible text, a CSS selector, an ignore regex, and a signed webhook.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://signalwatch.litework.me/" rel="noopener noreferrer"&gt;SignalWatch&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Polls a public URL (15 min on the free plan)&lt;/li&gt;
&lt;li&gt;Hashes the visible text, optionally scoped to a CSS selector&lt;/li&gt;
&lt;li&gt;Strips noise with an ignore regex, ignore-line-order, and a minimum change ratio&lt;/li&gt;
&lt;li&gt;Sends email, a Slack/Discord message with a diff block, or HMAC-signed JSON&lt;/li&gt;
&lt;li&gt;Keeps the event history so the dashboard can show what actually moved&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it does not do
&lt;/h2&gt;

&lt;p&gt;It does not render JavaScript, log in, or take screenshots. If the price only exists after client-side rendering, it will not see it.&lt;/p&gt;

&lt;p&gt;There is a public monitor on the Hacker News front page so you can see real diffs without an account: &lt;a href="https://signalwatch.litework.me/live.html" rel="noopener noreferrer"&gt;live sample&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Free tier is 3 monitors, no card.&lt;/p&gt;

&lt;p&gt;I would like to know what you would watch with it, and which noise filter you actually need.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>python</category>
    </item>
  </channel>
</rss>
