- Export downloads one file with the saved config, all match data and history, the flight cache and view settings - Import restores such a file after a confirmation; older config-only .json files are accepted and keep existing match data - Reset (with a warning) deletes everything and starts over with new random example watches; replaces "Reset state" - The JSON config section is gone - Old ntfy targets no longer stop the script: they are ignored at runtime and on import, shown as "no longer supported" in the editor Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
150 lines
8.3 KiB
Markdown
150 lines
8.3 KiB
Markdown
# 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 **Zulip**, **Slack**, **Discord**, 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:
|
||
**<https://gitea.mscloud.site/mans/sas-bonus-watch/raw/branch/main/sas-bonus-watch.user.js>**
|
||
(or: Tampermonkey → *Create a new script* → paste the file → save).
|
||
3. Open **<https://www.sas.se/robots.txt>** 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
|
||
|
||
On first start the script creates two example watches from ARN with a random theme, a destination that suits it
|
||
and dates at its next occurrence – e.g. *Christmas in Rovaniemi* (20–30 Dec), *Getaway weekend in Prague* (a Friday
|
||
→ Monday a few weeks out), *Ski week in Innsbruck* (sportlov), *Valentine's escape to Paris*. Keep, edit or delete them.
|
||
|
||
In the **Config** section of the dashboard:
|
||
|
||
1. **Watches** – each watch is two lines of fields:
|
||
|
||
| 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 |
|
||
|---|---|
|
||
| `zulip` | Zulip's Slack-compatible webhook: `https://<org>.zulipchat.com/api/v1/external/slack_incoming?api_key=<bot key>&stream=<channel>`. Each watch posts to a topic named after it, unless you add `&topic=…` |
|
||
| `slack` | An incoming-webhook URL |
|
||
| `discord` | A channel 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.
|
||
|
||
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).
|
||
|
||
### Export, Import, Reset
|
||
|
||
The buttons at the top right:
|
||
|
||
- **Export** downloads one `.json` file with everything: the saved config, all tracked matches (including drops and
|
||
sold-out flights), recent changes, the flight cache, and view settings (collapsed watches, sort order).
|
||
Unsaved edits in the config tables are not included.
|
||
- **Import** restores such a file after a confirmation and replaces the current config and data. Config-only `.json`
|
||
files from older versions are accepted too; they replace just the config and keep your matches and history.
|
||
- **Reset** deletes everything, after a warning, and starts over with new random example watches. Export first if you
|
||
want a backup.
|
||
|
||
## 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 **Export** /
|
||
**Import** to back up or move everything to another browser.
|
||
|
||
## Auto-update
|
||
|
||
The script header points Tampermonkey at this repo:
|
||
|
||
```
|
||
// @version 1.5.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.5.0 → 1.5.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.
|