# SAS bonus watch A Tampermonkey / Violentmonkey userscript that watches SAS EuroBonus award availability and notifies you when seats you care about appear. It runs inside your own browser, so requests to sas.se are ordinary same-origin browser requests and it keeps working behind sas.se's Cloudflare protection. - Any number of **watches**: origins, destinations, one travel window, cabins (Economy / Premium / Business), seats, max stops - Finds **outbound and return** flights inside the window; optionally only alerts when both legs exist, with min/max nights - **Gentle on sas.se**: about one request a minute, a full sweep at most every 15 minutes, backs off when refused - Alerts via **ntfy** (phone push), **Discord**, **Slack**, any **webhook**, and desktop notifications - Dashboard shows what is bookable now **and what you missed**: seat drops and sold-out flights ## Install 1. Install [Tampermonkey](https://www.tampermonkey.net/) (Firefox, Chrome, Edge, Safari) or [Violentmonkey](https://violentmonkey.github.io/). 2. Open the raw script and let the extension install it: **** (or: Tampermonkey → *Create a new script* → paste the file → save). 3. Open **** and keep that tab open. It turns into the dashboard. Pin the tab so the browser doesn't unload it. > **Chrome / Edge:** userscripts need *Developer mode* enabled on `chrome://extensions` (or "Allow user scripts" > in the Tampermonkey extension details) since Manifest V3. ## Set up In the **Config** section of the dashboard: 1. **Watches** – one row per search: | Column | Meaning | |---|---| | On | Pause a watch without deleting it | | From / To | Airport codes, comma-separated (`ARN, CPH`). Every From × To pair is checked | | Earliest / Latest | Travel window. Outbound **and** return flights departing in it are found | | Cabins | Economy, Premium (premium economy), Business | | Seats | Seats needed **on the same flight** | | Stops | Any, direct only, or at most 1 / 2 stops | | Both legs | Only alert when an outbound has a later return in the window (default on). Off = alert for either direction on its own | | Nights | Optional min / max stay, measured between the two departure dates (turns on Both legs) | 2. **Notifications** – add one or more targets: | Type | URL | |---|---| | `ntfy` | `https://ntfy.sh/`. Install the ntfy app and subscribe to the same topic. Priority sets how loud it is | | `discord` | A channel webhook URL | | `slack` | An incoming-webhook URL | | `webhook` | Anything; receives `POST` JSON `{title, body, watch, matches: [...]}` | The first time the script posts to a host, Tampermonkey asks you to allow it — choose *Always allow domain*. 3. Press **Save & restart**, then **Test notification**. 4. Optional: **Baseline (no alerts)** records what is available right now, so you are only told about new seats. ### JSON export / import / reset Under the tables, **JSON – export / import / reset** holds the same config as JSON: *Download .json* for a backup, *Load file…* or paste + *Apply JSON to tables* to restore, *Reset to example* to start over. Nothing takes effect until you press **Save & restart**. Unsaved edits are colour-coded until you save or discard: **yellow** = changed field, **green** = new row, **red** = row that will be removed on save (click ↺ to keep it). ## Dashboard - **Current matches** per watch, sorted *By departure* or *Newest first* (most recently appeared seats on top). Click a watch's header or summary row to collapse it; this is remembered across browser restarts: - summary: number of flights, bookable seats per cabin with how many were lost in red (`Economy ×30 −6`), and when the most recent flight appeared - `Economy ×8 −2` – 8 seats left now; the red number is how many were taken since the highest count seen (10) - red, struck through, *gone 12 okt 13:42* – sold out (or below your seat count); kept for two weeks or until departure - grey, *no matching return* – still has seats, but its return disappeared (with Both legs) - **Recent changes**: `+` new / back, `▲` more seats, `▼` fewer seats, `−` gone, `○` lost its return - **Log**: every request and sweep You are notified only when seats are **new or come back**. Drops and sold-out flights are shown, not pushed. ## How it polls 1. **Calendar** – `/bff/award-finder/destinations/v1?origin=ARN&destinations=HND&passengers=N`: one request per route pair returns every date with seats **in both directions**. With `passengers=N` it only counts flights that have N free seats. 2. **Details** – `/bff/award-finder/routes/v1?origin=…&destination=…&departureDate=…`: only for dates in your window that have seats in your cabins, and only when that date's calendar entry changed (or every 6 h). Requests are at least 40 s apart (+0–20 s random) and a sweep starts at most every 15 minutes; both are adjustable under *Request pacing* (minimum 20 s). If sas.se refuses requests, the script backs off exponentially (up to an hour), the badge turns red (*blocked – reload this tab*) and you get a notification. Reload the tab and complete the check if sas.se shows one. Only one tab polls at a time, so opening the dashboard twice does not double the rate. ## Limitations - Runs only while the browser is open and the computer awake. - Firefox / Chrome may unload background tabs under memory pressure; pinning the tab usually prevents it. - Config, matches and history live in the userscript manager's storage on that browser only. Use *Download .json* to move the config elsewhere. ## Auto-update The script header points Tampermonkey at this repo: ``` // @version 1.3.0 // @updateURL https://gitea.mscloud.site/mans/sas-bonus-watch/raw/branch/main/sas-bonus-watch.user.js // @downloadURL https://gitea.mscloud.site/mans/sas-bonus-watch/raw/branch/main/sas-bonus-watch.user.js ``` Tampermonkey periodically downloads `@updateURL` (default once a day; *Dashboard → Settings → Script Update*, or *Check for userscript updates* in the extension menu to force it) and installs the new file **only if its `@version` is higher**. Settings and history are kept across updates. Requirements: - **The raw URL must be readable without logging in.** Tampermonkey cannot send Gitea credentials, so the repo (or at least this file) must be public. Check by opening the raw URL in a private window. If the repo has to stay private, publish just the `.user.js` somewhere public (a public mirror repo, or a public Gitea release asset) and point both URLs there. - The script must be installed from that URL (or have the header above) for updates to be picked up. - `@name` and `@namespace` must never change, or Tampermonkey treats it as a different script and the new copy starts with an empty config. ### Releasing a change 1. Edit `sas-bonus-watch.user.js`. 2. Run the tests: `node --test` (Node 20+). 3. **Bump `@version`** (e.g. 1.3.0 → 1.3.1). Without this, nobody gets the update. 4. Commit and push to `main`. ## Development - `sas-bonus-watch.user.js` – the script. The pure logic (config validation, parsing, matching, state diffing) is exported to Node when loaded with `require`, the browser part only runs in the userscript manager. - `sas-bonus-watch.test.cjs` – tests for the logic using response shapes captured from sas.se. Run `node --test`. Nothing is sent anywhere except sas.se and the notification targets you configure.