Changelog
Format roughly follows Keep a Changelog; versions are semver in the script's @version header. History below was reconstructed from the build session on 2026-06-14 (it predates this changelog), then maintained going forward.
[Unreleased]
- Connecting itineraries — itinerary-level parsing + "Nonstop only" toggle. Paused, awaiting vetting + a captured connecting-route fixture (see
docs/ROADMAP.md#1).
Extension [0.27.18] / [1.41.36]2026-06-25
Added — keep-alive defers while you're active on SQ (no redundant pings)
- The keep-alive used to ping on its timer even while you were actively searching on SQ — pointless, since your own requests already nudge the session. Now the panel reports SQ activity to the worker (throttled), the alert poll marks activity too, and a scheduled ping is skipped ("deferred") if SQ was touched within the last interval. A manual Ping now never defers.
- Safety cap: at most
MAX_KA_SKIPS(3) consecutive skips, then a real login-verifying ping runs — so a tab firing redirected requests after a dead session can't keep keep-alive "alive" forever. Skips keep the session-longevity tracking continuous (activity = alive). - The Options ping log shows deferred pings as skipped · deferred (with a tooltip). (
lastActivity,kaSkipStreak, purekaShouldDefer()+ unit test.)
Extension [0.27.17] / [1.41.35]2026-06-24
Added — sort results by Best or Date; icon meanings clarified; keep-alive Method column
- Sort toggle on the Search List view: Best (most award seats → cheapest miles → earliest) or Date (chronological). The header now spells out what "Best" means inline (it was unexplained).
- Fixed the ⚡ icon overload. ⚡ was used both for booking automation *and* "free/instant" — confusing, since it mostly means automation. ⚡ is now booking automation only; the two "free" spots use ✨ free (green); ⟳ stays "costs ≈1 SQ request" (amber). Added an Icon legend section to the Help tab.
- Keep-alive log (⚙ Options) now has a Method column showing Light vs ⟳ Deep per ping, so you can see which pings did the extra search-page touch (Deep shows only when signed in).
Extension [0.27.16] / [1.41.34]2026-06-24
Fixed — Booking shortcuts card no longer flush against "Watch this route"
- The nested Booking-shortcuts card had no top margin, so it sat flush against the "★ Watch this route" row above it and looked like one section. Added
margin-topto.card.bookingfor clear separation.
Extension [0.27.15] / [1.41.33]2026-06-24
Changed — fare tier also shown in Search List view & Round-trips; Deep keep-alive disclosed
- Extended the tier chip from watchlist results to the Search → List view ("Best dates" rows) and Round-trips (shows outbound/return tiers, e.g.
SAVERorSAVER/ADVANTAGE). Consistent with the watchlist rows. (.rtierstyle; reusessummariseFlights().minTier.) - Store + privacy docs: disclosed the optional Deep keep-alive method — the
host_permissions/alarmsjustifications and the published privacy notice now note that in Deep mode each keep-alive ping also GETs SQ's award search page (default Light mode is unchanged: a single lightweight login-status GET).
Extension [0.27.14] / [1.41.32]2026-06-24
Added — fare tier (Saver / Advantage / …) shown in watchlist results
- Each Watchlist-results row now shows the cheapest available fare tier next to its miles (e.g.
209k SAVER), so you can tell Saver from Advantage at a glance. The data was always parsed (SQ's sellingClass description) — it just wasn't surfaced. (summariseFlightsnow returnscheapest/minTier;tierLabel()formats it;.ttierchip.)
Extension [0.27.13]2026-06-24 (extension-only; userscript stays 1.41.31)
Added — keep-alive ping Method (Light / Deep), switchable in Options
- New Method selector in ⚙ Options → Session keep-alive: - Light (default, unchanged): pings
/home/dwLoggedInUserData.formonly — Akamai-safe, but may only refresh the login layer, not the redemption *search* session. - Deep: when signed in, each ping *also* GETs/redemption/loadFlightSearchPage.form, so it exercises the search session too — more likely to keep searches working longer, at one extra request per ping (a bit more Akamai exposure). Login state still comes from the authoritativeuserDatafetch. - Reversible anytime; default stays Light. Intended as an experiment — flip to Deep for a day or two and compare the session-alive figures on the Options page. (
st.keepAliveMode;keepAlivePingdeep-touch.)
Extension [0.27.12] / [1.41.31]2026-06-24
Fixed — "08:00 SGT" no longer claimed as the universal release moment
- The schedule tooltip said 08:00 SGT was "the exact moment SQ loads new T-355 dates" and the Watch recipe said it "fires the moment SQ loads new dates" — but the release hour varies by route & departure region (a user flagged it's not 08:00 SGT for some routes). Reworded both: 08:00 SGT is a fixed daily check at the T-355 rollover that mainly suits SIN/Asia departures; if a route releases at another hour, ~hourly is the reliable choice (it catches any new date within ~an hour). No behavior change — the trigger still fires at 00:00 GMT / 08:00 SGT; only the wording is honest now.
Extension [0.27.11] / [1.41.30]2026-06-24
Changed — ⟳ cost marker on the other per-date request mentions; aircraft-filter hint
- Carried the amber ⟳ cost marker into the remaining "≈1 request per date" spots: the Help steps (visible amber badge), and the Load all button / "more dates" header / per-date row tooltips (plain ⟳). So ⚡ (free) and ⟳ (costs requests) read consistently everywhere.
- The watch "Aircraft has" filter now has a tooltip + clearer placeholder (
Airbus · A380, 777) explaining it substring-matches the aircraft NAME — soAirbuscatches all Airbus (A350/A380/A330),Boeingall Boeing, or list models comma-separated. (It already worked; it just wasn't discoverable.)
Extension [0.27.10] / [1.41.29]2026-06-24
Changed — "1 request per found date" badge uses the amber cost marker
- The Auto-load badge now reads
⟳ ≈1 request per found datein amber, matching the app's existing cost convention (⟳ deeper — drills dates) instead of staying unmarked. Deliberately not ⚡ — that icon is reserved for *free / instant* (e.g.⚡ freeon the Cheapest-fare filter,⚡ All instant), so the two markers stay opposites: ⚡ = no cost, ⟳ = costs requests. (.costtagstyle.)
Extension [0.27.9]2026-06-24 (extension-only; userscript stays 1.41.28)
Fixed — "sign in again" notification was truncated
- The session-expired nudge ran past what macOS shows, cutting off mid-word (
…need a signed-in sessi…). Tightened the title (SQ Award Finder — sign in again) and body (SQ session expired — alerts + keep-alive paused. Sign in at singaporeair.com to resume (toggles stay on).) so the whole message is visible.
Extension [0.27.8] / [1.41.28]2026-06-24
Added — "Catch the daily T‑355 release" recipe in Watch help
- Documented, in the UI itself, how to set up the common goal of snagging newly-released award space the moment it drops: route+cabin → ★ Watch → Newest date rule (N days at the booking edge) → Cheapest fare ≤ Saver price (Saver/promo only) → 🔔 Auto-check at 08:00 SGT. Shows in the "How Watch works" guide (and the userscript variant points to the extension for hands-off 8am checks).
Extension [0.27.7] / [1.41.27]2026-06-24
Changed — Booking shortcuts card: collapsible, and says when it triggers
- The card's three checkboxes are settings, not buttons, but only the (hidden) tooltips said so — a user reasonably asked whether they auto-run or only fire from a notification. Added a visible one-liner under the title: *these are settings that take effect whenever you tap Open in SQ on a date (or click an alert notification); tick once, nothing runs until you open a flight.*
- Made the whole card collapsible (collapsed by default), matching the Watch-tab settings pattern. The header summarizes what's on (e.g.
⚡ to passenger · 👤 travellers) so you can see the state without expanding. (S.showBooking+#t-bookingtoggle.)
Extension [0.27.6] / [1.41.26]2026-06-24
Changed — manual watch checks reuse recently-loaded dates (don't re-pull < 6h)
- A foreground watch check (Check all now / a route's 🔍) used to re-fetch every found date's flight details every time, even ones loaded minutes ago. It now reuses any date whose details were loaded < 6h ago (the same
DRILL_STALE_MSfreshness the Search board already uses) — the calendar is still re-scanned (cheap, catches newly-released dates), but only new or stale dates are re-pulled. - When recent dates exist, you're asked first (like the watchlist re-check confirm): OK re-pulls everything fresh for the latest seat counts; Cancel does the quick reuse-only check. The progress line now shows e.g.
found 9 date(s) with space · 6 still fresh (reused, <6h) — loading 3 flight detail(s)…. - (
scanRoutegains{force, prior};watchReusableCount()drives the prompt. Background auto-checks are unchanged — they already rotate dates rather than re-pull all.)
Extension [0.27.5] / [1.41.25]2026-06-24
Fixed — "view" now lands you on the flight (with a highlight pulse)
- Clicking view on a watchlist result switched to the Search board but rendered scrolled to the top (the empty search form), so the loaded flights sat below the fold and it looked like nothing was searched. It now scrolls the opened date's row into view and gives it a brief highlight pulse (a fading blue glow) so the eye lands on what you opened — useful when the board is dense with dates and results. (
scrollToExpanded()+sqaf-flashkeyframe.)
Extension [0.27.4] / [1.41.24]2026-06-24
Changed — watch scan shows what it's actually doing
- The status used to sit on "scanning SIN→PVG…" with no detail during the award-calendar phase (one lookup per 7-day window). It now narrates each phase:
connecting…→scanning award calendar · week 3/8→N date(s) with space — loading flights…→loading flights · 2/5 (Jul 04). (scanRoute's progress callback now passes a descriptive message instead of a bare d/n.)
Extension [0.27.3] / [1.41.23]2026-06-24
Fixed — "view" on a watchlist result did nothing
viewWatchRouteloaded the route/date into the Search state but never switched to the Search tab (its siblingviewBgDatedoes) — so clicking view silently set things up while you stayed on Watch, looking like nothing happened. Now it switches to the Search board and shows the loaded flights.
Changed — watch card no longer searches on a stray click
- The redesigned (bigger) watch card used the whole body as the "check this route" trigger, so an accidental click fired a search. Checking is now a deliberate 🔍 icon on the card; the card body is inert.
Extension [0.27.2]2026-06-24 (extension-only; userscript stays 1.41.22)
Changed — alerts and keep-alive now share login state
- They used to check login independently: keep-alive *authoritatively* (it fetches the login endpoint each ping), the alert poll via the cookie (which lives ~10 days and outlives a dead session) — so the poll ran and failed even when keep-alive already knew the session was gone. The poll now reuses keep-alive's recent authoritative result (
loginFromKeepAlive): a recent signed-in ping is trusted (no redundant check), a recent signed-out one skips the scan, and when keep-alive is paused the poll checks the login endpoint directly (the cookie lies then) — which also lets it spot a re-sign-in and resume keep-alive. The redundant "alerts paused" notification is suppressed when keep-alive already nudged. + unit tests for the shared-state decision.
Extension [0.27.1]2026-06-24 (extension-only; userscript stays 1.41.22)
Fixed — phantom "−N dates removed" when the session lapsed mid-poll
- If the background scan failed partway (session idle-timed-out while the login cookie was still present, HTTP error, or access-blocked),
calendarChunkreturned empty results, and the diff misread "got 0 dates" as "all N removed" — producing a false−N(then+Nagain on the next good poll). Now a failed/incomplete calendar scan is treated as couldn't-check:calendarChunkflags HTTP/parse errors as failures (not "no space"), andscanRouteDatesthrows if any window couldn't be read, so the poll logs "⚠ check failed" and keeps the previous snapshot (no false removal) and retries next cycle.
Extension [0.27.0] / [1.41.22]2026-06-24
Added — manual checks now recorded in the route's history
- The history logged only background change-events, so a manual check that found no change (or the panel's foreground "Check all now") left no trace. Now every manual check is recorded as a 🔍 manual check · N dates with space entry: Options → Check now always logs one (even with no change), and the panel's Check-all / per-route checks ask the worker to append one (the SW stays the single writer of history). Panel renamed "background history" → check history.
Added — Options "Check now" shows progress
- The button now disables + reads "⏳ Checking…" while the background poll runs (and "checking now…" next to it), restoring when it finishes — instead of looking dead until results suddenly appear.
Changed — session-ended notification mentions alerts too
- When the session expires and keep-alive pauses, the notification now also says background award-space checks are paused (when alerts are on) and both resume on sign-in — previously it only mentioned keep-alive.
Extension [0.26.0] / [1.41.21]2026-06-24
Changed — Watch tab reorganized (watchlist is now the focus)
- The Watch tab crammed the watchlist, action buttons and alert settings into one flat list, so the saved routes (the point of the tab) rendered as tiny pills that looked like a footnote. Now: - Watched routes are full-width cards — route + cabin on top, date rules / criteria / last-check below, controls (🔔/🔕 ✎ 📜 ✕) on the right. They're the visual focus. - Alert & background settings collapse into a single section (
▸ Alert & background settings, collapsed by default) that shows the auto-check state at a glance and notes "⚙ also in Options" — since every one of those settings is also editable on the Options page. - The keep-alive paused notice stays visible even when settings are collapsed.
Extension [0.25.4]2026-06-24 (extension-only; userscript stays 1.41.20)
Fixed — "no space now" was misleading
- A watched route's status read "no space now", a present-tense claim from a snapshot up to an hour old — so a live panel search could find seats while options still said "no space now". Reworded to the last check's finding: "no space found · last checked Xm ago" (and "N dates with space" when found). Also stops showing a time when the route hasn't actually been polled (was borrowing the global poll time even when there was no per-route snapshot).
Extension [0.25.3]2026-06-24 (extension-only; userscript stays 1.41.20)
Fixed — watched-route "checked X ago" also looked frozen
- Extends the previous fix to each watched route's sub-line: its "checked X ago" now ticks live every second (text only — the row/checkbox isn't rebuilt). The route's open-count + status still reflect the last background poll (hourly), so a panel "live check" can find space the hourly snapshot hasn't yet — use Check now to force a fresh background poll.
Extension [0.25.2]2026-06-24 (extension-only; userscript stays 1.41.20)
Fixed — "last check X ago" looked frozen
- Only the Session-status block ticked every second, so the background-alerts "last check X ago" appeared stuck while the session figures moved — making the alert timer look like it wasn't running (it is — on the chosen Schedule, e.g. hourly). The "last check" text now ticks live too (text only — it does NOT re-render the checkboxes, so it can't fight your input).
Extension [0.25.1]2026-06-24 (extension-only; userscript stays 1.41.20)
Fixed — "Signed in" showed stale "signed out" right after signing in
- With keep-alive off, the only login signal was the last keep-alive ping, which could be hours old — so a fresh sign-in still read
signed out · stale. The "Signed in" line now falls back to the live login cookie when there's no recent ping (shown assigned in · per cookie), and only uses the stale ping if the cookie is unreadable. The stale banner now talks about the session-alive *durations* being old, not your login state.
Changed — options page layout
- Watched routes moved up to sit directly under Background alerts (they're two halves of one feature — the alert checker + the list it checks); the two session cards (keep-alive, status) now group together below. Added a one-line hint tying the routes list to the alerts.
Extension [0.25.0] / [1.41.20]2026-06-24
Changed — keep-alive now PAUSES (and auto-resumes) instead of switching itself off
- When the session ends, keep-alive no longer turns the toggle off (forcing you to re-tick it). The toggle stays on (your intent); it goes to a
⏸ paused — not signed instate, stops pinging the dead session, and resumes automatically when you sign back in at singaporeair.com (the on-page panel detects the sign-in and re-arms; a scheduled alert poll resumes it too). The options page and the panel both show the paused state clearly while the toggle stays on. (FieldkeepAliveStopped→keepAlivePaused.) - The session-expiry notification now says the toggle stays on and it resumes on sign-in.
- Documented the previously-unstated behavior in the keep-alive notes: it confirms a sign-out over two consecutive pings before pausing (anti-flap, so a pause can lag the real sign-out by up to one interval), and it only runs while Chrome is open.
Extension [0.24.5]2026-06-23 (extension-only; userscript stays 1.41.19)
Fixed — "Longest kept alive" showed stale inflated data; "alive for" went blank when signed out
- Both figures are now derived from the ping log (self-correcting), not the stored counter that the old gap-spanning bug inflated to ~5 days (and
Math.maxnever walked back). "Session alive for" now shows the last session's span when you're signed out (e.g.3h 48m · last session), and "Longest kept alive" is the longest signed-in run in the log.+means the run reaches the oldest log entry (could be longer).
Changed — session-expiry nudge is louder
- When keep-alive auto-stops because the session ended, the desktop notification is now high-priority + sticky (
requireInteraction) and chimes (respecting the master sound toggle), so the "sign in again" nudge isn't missed. (The notification itself already existed.)
Extension [0.24.4] / [1.41.19]2026-06-23
Fixed — panel "checked" status disagreed with the options page
- The panel chip's "checked X ago / not checked" used only the foreground (manual-search) timestamp, while the options page shows the background poll time — so a route the background had just checked still read "not checked" on the panel. The chip now reflects the background poll (matching options), falling back to your manual search, and only shows "not checked" when neither has run. (Open-date detail stays on the 🛰 badge.)
Extension [0.24.3] / [1.41.18]2026-06-23
Fixed — per-route sound looked muted / panel vs options seemed to disagree
- The per-route chime used the
🔊emoji whose sound-waves read as a *muted* speaker at chip size, so a signed-on route looked off — and next to the options checkbox (clearly "on") the two seemed to contradict. Both UIs now use 🔔 (on) / 🔕 (off) — the slashed bell is unambiguous. (Verified live: the route was sound-ON in both; only the glyph was misleading.)
Fixed — options-page route removals could be re-added by the panel
- The panel owns the watchlist and mirrored its own copy on sync, so removing a route (or muting it) from the options page while an SQ tab was open could be reverted. The panel now reconciles against the shared store (
reconcileWatchlist) — the store is authoritative for membership + per-route sound, while the panel keeps its richer per-route fields — on load and on every store change.
Extension [0.24.2] / [1.41.17]2026-06-23
Fixed — settings reverted because the panel clobbered the options page
- The on-page panel's
extSync()mirrorsenabled/keepAlive/autoExtend/soundOnAlert/schedulefrom its ownSinto the shared store on load and on every panel change. The store-change listener only adoptedkeepAlive/autoExtendback, so a change made in the options page to Enable alerts / sound / schedule was overwritten by the panel's stale value (and the checkbox un-checked). Now the panel adopts all shared settings from the store first (adoptExtSettings()on load) and the listener reflects all of them, so the two UIs stay in sync instead of fighting. Pairs with the worker-side merge-write fix in 0.24.1.
Extension [0.24.1]2026-06-23 (extension-only; userscript stays 1.41.16)
Fixed — settings checkboxes wouldn't stay checked (storage write-race)
- The keep-alive ping and watch poll read the whole store, did multi-second fetches, then wrote the whole stale store back — reverting any setting (Enable alerts / Keep-alive / Auto-extend / sound) toggled meanwhile, after which the options storage-listener redrew the boxes unchecked. The worker now merge-writes only its own fields (
commit()), and the options page serializes its saves so two quick clicks can't clobber each other.
Fixed — "Session alive for" counted across monitoring gaps
- A ping days after the previous one (keep-alive was off in between) was shown as one continuous session ("alive for 5d 20h / still going"). It's now measured from the current uninterrupted run in the ping log (a gap > ~2.5× the interval breaks it);
trackSessionresets on the gap too, so the longevity stat isn't inflated. A trailing "+" means "at least this long" (the run fills the log window).
Changed
- Per-route sound control now shows 🔊 / 🔇 reflecting state (was a static 🔊 that read as muted).
Extension [0.24.0] / [1.41.16]2026-06-23
Fixed — login detection used a cookie that never exists (verified live on SQ)
- The login cookie name
_kfvstoken(borrowed from the HeyMax extension) does not exist on singaporeair.com. SQ's real login cookies areSQCLOGIN_COOKIE/LOGIN_COOKIE/RU_LOGIN_COOKIE(~10-day expiry). Consequences fixed: - WorkerisLoggedIn()always returned "signed out", so scheduled background alerts silently paused with "not signed in" even while you were logged in. Now checks the real cookies, and falls back to the authoritative login-endpoint fetch when none are found (so a cookie rename can't pause alerts). New unit tests cover the gate. - Options "Login cookie valid until" showed "no login cookie (signed out?)" even when signed in; now shows the real expiry (soonest-expiring login cookie = the binding upper bound). - Userscript panel flashed "signed out" on every load (the cheap cookie hint always missed); now the hint reads the real cookies, so no flicker and no false-negative on a transient network error.
Fixed — Session status panel showed stale data as if live
- When the last keep-alive ping is old (keep-alive off, or no recent ping), the panel now flags it as stale instead of presenting a frozen "signed in / Session alive for 5d / (still going)" as current. "Session alive for" is frozen at the last confirming ping (it no longer grows forever once pings stop), "(still going)" only shows while monitoring is live, and the Recent pings table now shows the full date + time (a bare clock time hid that old pings were days ago).
Added — richer Watched routes in options
- Each watched route now shows its date rules (When), filter criteria (aircraft / seats / miles / connections), currently-open date count, and last-checked time — previously only
origin→dest · cabinwas shown.
Extension [0.23.15] / [1.41.15]2026-06-17
Fixed — consistency audit (parallel UI/code/docs audit)
- Bug: per-route alert
criteriawas never synced to the worker —extSync()omitted it from the watchlist mapping, so background alerts ran withcriteria === undefinedand ignored each route's aircraft / min-seats / max-miles / nonstop filters (and re-check cadence). Now included. - UI consistency: "🗑 Forget saved travellers" is now a danger-red link (was the same blue as benign links); the muted bot-check badge's "pending" state is now a faint amber (the three state classes were all identical grey); the keep-alive auto-stop notice uses the panel's muted red (was a harsh off-palette
#b3261e); added a.bksubCSS rule; unified the sound-toggle label with the options page. - Removed dead state fields
showWatch/showHelp(unused). - Docs refresh: README status/version (was 1.36.0/0.18.0 + "connections paused" — both stale) and test count (→ 66); CWS doc manifest version (→ 0.23.14→0.23.15); ROADMAP #8 marked done; passenger-select proposal notes the shipped multi-pax stepper + "use same contact".
Extension [0.23.14] / [1.41.14]2026-06-17
Changed — clearer payment-page notice + funnel reference doc
- The payment-page notice now states plainly that the extension STOPS here — it will not enter your card or place the booking (do that yourself) — and reminds you of SQ's ~8-minute payment timer, so nobody assumes the extension completes the purchase.
- Added
docs/BOOKING-FUNNEL.md— the verified end-to-end funnel map + every DOM gotcha we hit (React Continue / native click / stale ref, the custom passenger combobox, the multi-pax "NEXT PASSENGER" stepper, multiplesubmitbuttons, the contact-details / "use same contact" blocker, theLightBoxconfirm popup,_abck/Akamai behaviour, the firm payment-page stop).
Extension [0.23.13] / [1.41.13]2026-06-17
Fixed/Added — reached the payment page end-to-end (2-pax, verified live)
First full run to the payment page (/payment/bookingPayment.form) on a 2-pax award booking. Two fixes from that run:
- Confirm-passengers popup matcher was case-sensitive — the real popup is a
LightBoxContentwith abutton.nextbutton("Next");findDialogForwardButtonmissed both, so the to-payment step fail-safed instead of clicking Next. Now case-insensitive class selectors + recognises thenextbuttonclass. - "Use the same contact details as Passenger 1" — the real blocker on multi-pax was the per-passenger contact email/phone (required, not part of a saved-traveller profile). New
useSameContact()ticks that checkbox after the stepper, so the other passengers reuse the main contact — selecting an existing option, no PII typed — which clears the requirement and lets "Straight to payment" proceed. - Confirmed live: search → fare → ×2 Continue → both passengers (stepper) → same-contact → Straight to payment → Confirm-passengers popup → Next → payment page (Akamai auto-resolved, no CAPTCHA). The driver still hard-stops at the payment page — never enters card, never clicks the final pay.
Extension [0.23.12] / [1.41.12]2026-06-17
Added — multi-passenger auto-select (the "NEXT PASSENGER" stepper)
- Passenger auto-select now steps through every passenger: fill the current passenger's dropdown → click "NEXT PASSENGER" → fill the next → … until the last passenger (where "Straight to payment" appears), then the ⏩ to-payment step proceeds. Skips already-filled slots (e.g. the account holder), fails safe if a step doesn't advance. Verified live on a 2-pax booking: select P1 → NEXT PASSENGER → P2 combo (SQ excludes the already-used traveller) → native-click select → "Straight to payment" appears. Supersedes the single-pax-only limitation noted in 1.41.11. (
nextPassengerBtn(); stepper loop inonPassengerPage.)
Extension [0.23.11] / [1.41.11]2026-06-17
Fixed — stale Continue ref left auto-advance stranded on Select/Review
- The driver grabbed the
proceedButton, waited ~450ms, then clicked it — but React re-renders the button in that window, so the stored ref was detached and the click did nothing (auto-advance sat on "fare selected — continuing…" forever). A *fresh* query click sailed straight through. NewclickProceed()helper waits → settles → re-queries a fresh node right before clicking; used byadvanceSelect/advanceReview. Verified live: now drives Select → Review → Passenger via the extension on its own.
Known limitation — multi-passenger is a "NEXT PASSENGER" stepper
- Live finding (2-pax): the passenger page is a stepper — Passenger 1's CTA is "NEXT PASSENGER", and "Straight to payment" only appears after the last passenger. So the ⏩ to-payment step (and passenger auto-select) currently handle single-passenger only; for multi-pax they fail safe ("couldn't find Straight to payment — continue manually") rather than driving the stepper. Full multi-pax support (fill → Next passenger → … → pay) is future work. See
docs/proposals/passenger-auto-select.md.
Extension [0.23.10] / [1.41.10]2026-06-17
Fixed — "Straight to payment" matched the wrong button
- The passenger page has several
button.submitbutton(e.g. "Seat selection" *and* "Straight to payment").proceedToPayment/highlightPaygrabbed the first viadocument.querySelector('button.submitbutton')— the wrong one — so the ⏩ to-payment step appeared to do nothing. Now matched by text (payButton(), visible-only). Found live driving a 2-pax booking. (The to-payment step still — correctly — can't proceed while passengers are incomplete; SQ validation gates it, which is the intended fail-safe.) - Live notes from the 2-pax run: the page stacks both passenger sections on one page (no "next passenger" button); for multi-pax the slots aren't pre-filled, so 👤 auto-select (when on) is what fills them.
Extension [0.23.9] / [1.41.9]2026-06-17
Fixed — auto-advance stuck on Select/Review (Continue is React; needed a native click)
- The classic
/redemptionSelect & ReviewproceedButton("Continue") is now a React component (__reactProps$).reactClick*preferred* invoking React'sonClickwith a synthetic mock event — which selects a fare fine but does not navigate — so auto-advance got stuck on "fare selected — continuing…". FlippedreactClickto do a native.click()first (it flows through React's real event pipeline and navigates), falling back to the direct onClick invoke only if needed. Verified live: it now drives Select → Review → Passenger. (The custom passenger combobox still uses the direct invoke — it ignores native clicks — viaopenCombo.) selectTravellerhardened: native click, then fall back to the direct onClick invoke if the slot didn't fill. Roster-capture verified live (reads the savedbutton.Suggestiontravellers).
Extension [0.23.8] / [1.41.8]2026-06-17
Changed — CAPTCHA-aware auto-advance (replaces the over-cautious _abck pre-gate)
- The old
_abck-based pre-gate held auto-advance whenever the flag read-1— but live use showed-1lingers even in a perfectly fine, interacted session, so it over-blocked (and on timeout dropped auto-advance). Removed it. ⚡ now just opens; if SQ throws a CAPTCHA ("Access Blocked"), the driver detects it (detectChallenge, unit-tested), pauses, tells you to complete it, keeps the intent alive while you do, and resumes automatically the moment you're through (5-min cap). Honest and hands-off. - De-emphasised the bot-check badge — it no longer gates anything, so it's now a tiny, muted, neutral hint (was a prominent amber "not validated" pill that read like a warning). Removed the matching ⚡-card "will wait until validated" note.
Extension [0.23.7] / [1.41.7]2026-06-17
Fixed — flow detection is path-only (new flow is CASH-only; award stays classic)
- Corrected a bug from 0.23.5:
detectNewFlowkeyed off thedisableCIBVScookie, but live testing showed that cookie is a cash-booking opt-in that stays set across both flows — award redemption ignores it and stays on classic/redemption/*(verified: classic Select page, 15ButtonResetfares, withdisableCIBVSset). The cookie would have wrongly disabled auto-advance on working classic award pages. Detection is now path-only (/book-flight/⇒ new flow). "Open in SQ" always targets the classic award funnel, so auto-advance needs no new-flow gate — removed it and the misleading ⚡-card notice. Auto-extend stays new-flow-gated (path-based). Seedocs/NEW-BOOKING-FLOW.md(KEY CORRECTION).
Extension [0.23.6] / [1.41.6]2026-06-17
Added — ↻ auto-extend session (new booking flow only, opt-in)
- The new
/book-flightflow pops a "Session is expiring — Extend session" dialog mid-booking. A new opt-in toggle (panel + options page, default OFF) auto-clicks Extend session so a booking doesn't time out. Tightly scoped: only fires on the new flow (onNewBookingFlow) and only ever clicks an element whose text is "extend session"/"need more time" (React-handler invoke or normal click). Benign session-keepalive — never a login or payment. Setting syncs between the panel and options page.
Extension [0.23.5] / [1.41.5]2026-06-17
Added — old/new booking-flow awareness (SQ's "new booking experience" BETA)
SQ is A/B-rolling a new React/Next booking app under /book-flight/*, opted into via the disableCIBVS cookie (vs the classic /redemption/* flow we support). Findings documented in docs/NEW-BOOKING-FLOW.md. Two fixes:
- Panel resilience — the new app re-renders
<body>on hydration and wiped our injected panel. We now re-attach#sqaf(and our<style>) if it's removed (a 1.2s guard + a<body>-swap MutationObserver), so the panel survives on the new flow. - ⚡ auto-advance flow-guard —
detectNewFlow(cookiedisableCIBVStruthy, or path/book-flight/, unit-tested) gates the auto-advance: on the new flow it doesn't arm ⚡ (its React DOM differs from the classic selectors), opens normally, and shows a clear "new booking experience active" notice in the ⚡ card + a toast. - Note: full auto-advance *support* on the new flow is deferred — it's BETA and its DOM will churn; we ship detect-and-behave-sanely now and revisit when it's default. See
docs/NEW-BOOKING-FLOW.mdopen questions.
Extension [0.23.4] / [1.41.4]2026-06-17
Added — always-visible session bot-check badge
- A compact "bot-check: ready / ⏳ not validated" badge now sits in the signed-in bar (right side), visible on every tab regardless of the ⚡ toggle, and updates live as you interact (a light badge-only refresh every 2s — no full re-render, so it won't disturb typing). Reads
_abckonly.
Extension [0.23.3] / [1.41.3]2026-06-17
Added — bot-check pre-flight for ⚡ auto-advance (well-behaved, not evasive)
- Reads Akamai Bot Manager's
_abckvalidation flag (the~-segment that's-1until the session is validated as human). When ⚡ auto-advance is armed and the flag is-1, the script does not POST to SQ's protected booking endpoint — it shows a notice, asks you to interact with the page, polls the cookie, and resumes automatically the moment your genuine interaction validates it (25s timeout → opens the normal search without auto-advance). This prevents the "Access Blocked" CAPTCHA that a cold, un-validated submit triggers. A live indicator in the ⚡ card shows the current state. - It never synthesizes interaction to flip the flag — that would be defeating bot detection. The human does the real interacting; the script only detects when the session is in good standing. (
parseAbck, unit-tested.) See ADR-023.
Extension [0.23.2] / [1.41.2]2026-06-17
Added — sound on alerts (global + per-route, both default ON)
- A short chime plays when an award-availability alert fires, on top of the desktop notification. Controlled by a global master switch plus a per-route 🔊/🔇 on each watch chip (and in the options page) — both default ON; global off mutes everything (
shouldPlaySound, unit-tested). The panel + options page each get a Test sound button. MV3 workers can't use the Audio API, so the chime is played from an offscreen document (offscreen.html/js, Web-Audio-generated two-note tone — no sound file shipped). Adds the"offscreen"permission.
Added — login-cookie expiry readout
- The options-page Session status now shows "Login cookie valid until X" (read live via
chrome.cookiesfrom_kfvstoken), captioned as an upper bound — the real session can end sooner (idle timeout / absolute cap), so the keep-alive ping remains the authoritative liveness check.
Extension [0.23.1] / [1.41.1]2026-06-17
Fixed / Added — session keep-alive robustness
- Keep-alive now auto-stops when the session is truly gone. Previously it pinged forever even after you'd signed out / hit SQ's session cap (pinging a dead session can't revive it). Now: two *definitive* sign-outs in a row (a real HTTP response with no member fields — a network blip doesn't count) → turns the ↻ Session keep-alive toggle OFF, records the reason, fires a desktop notice, and the panel + options page both reflect it. Re-enable after signing back in. (
keepAliveDecision, unit-tested.) - Keep-alive ping now bypasses the HTTP cache (
cache:'no-store'). A cached response (a) never reached SQ's server, so it wasn't actually keeping the session warm, and (b) reported a *stale* signed-in/out state — which is why the status could show "signed out" right after you'd signed in. - ±20% interval jitter. The ping was a fixed clock (every N min exactly); it's now a self-rearming one-shot at N ± 20% so the cadence isn't perfectly regular (gentler against Akamai). (
kaDelay, tested.) - Session-ping history retention is unchanged: the last 50 pings (
KEEPALIVE_LOG_MAX) — ~12.5h at the default 15-min interval — kept locally for the options-page readout; the longevity summary (longest session) is a single value kept indefinitely.
Extension [0.23.0] / [1.41.0]2026-06-17
Added — ⚡ Booking shortcuts (opt-in funnel automation; all default OFF)
DOM automation that drives SQ's real booking pages in your own signed-in tab. Everything is opt-in, default OFF, and fails safe (any hiccup → you're left where you are with an on-page notice). See ADR-023 and docs/proposals/{auto-advance-to-passenger,passenger-auto-select}.md.
- ⚡ Open straight to the passenger page (one-way): "Open in SQ" (and alert deep-links) auto-pick the cheapest available fare (
pickCheapestFare, pure + unit-tested), click through Select → Review, and stop on the passenger page. Shows a notice if the cheapest *bookable* tier isn't the cheapest offered (e.g. "Saver was waitlist — picked Advantage"). Round-trips/connections open normally. - 👤 Auto-select my saved travellers: on the passenger page, fills empty passenger slots from your KrisFlyer saved-traveller list. Selects existing saved people only (never types new details). The captured names are stored on your device only and never transmitted; a 🗑 "Forget" button clears them. Works via direct React-handler invocation (SQ's custom dropdown ignores synthetic clicks).
- ⏩ …then continue to the payment page (second opt-in, nested under auto-advance): also clicks "Straight to payment" + the confirm-passengers popup, submitting your passenger details, and lands you on the payment page. Hard stop there — it never enters card details and never clicks the final pay/confirm. (SQ validates missing fields and shows a confirm popup first.)
Notes
- The live DOM driving can't be unit-tested (manual verify on the real site); the fare parser is covered by 6 new tests (52 total). The funnel-page driver is content-script only — the background worker is unchanged; alert deep-links auto-advance through the existing
applyOpenpath when the toggle is on.
[1.40.1]2026-06-16 (userscript only)
Added — userscript auto-update
- Added
@updateURL/@downloadURL(→ the GitHub rawmainsource) +@supportURL. Once this version is installed, Tampermonkey/Violentmonkey auto-update future versions — no more re-pasting the source. (The extension updates separately via the Chrome Web Store / reload.)
Extension [0.22.0] / [1.40.0]2026-06-16
Fixed — deep-criteria alerts no longer fire on connecting-leg matches; QA pass
- After the itinerary rewrite,
parseDetailreturns connections too, so a deep watch (e.g. "A380") could alert on an A380 that's only on a connecting leg — which the panel then hides (Nonstop-only ON). Closed the gap: a per-watch "Nonstop only" alert preference (default ON) folded intocriteriaMatch, so the worker alerts, the manual Check all results, and the ✎ editor all agree. Turn it off per watch to allow connecting matches (chip shows+conn). Pre-existing/old records withoutstopsare treated as nonstop (backward compatible). Userscript + worker parity tested. - QA/polish audit of the session's changes: confirmed no orphaned
isLoggedIn, no leftoverselectedDate/scrollToDetail, correctacKeysclosure ordering inbuildDetail, and right persistence (nonstopOnlysaved;expanded/loadingDatetransient). 46 tests.
Extension [0.21.0] / [1.39.0]2026-06-16
Added — extension options page + session keep-alive longevity
- New options page (
options.html/options.js,options_uiin the manifest, opens in a tab) — a real settings surface for the background worker: alerts enable + schedule (hourly / 08:00-SGT release), test-alert / check-now, session keep-alive, session-status readout, and watched-route removal. - Keep-alive now runs independently of alerts (its own toggle) at a configurable interval (default 15 min, 5–25), scheduled via
chrome.alarms(clears when off). Each ping is logged ({ts, status, loggedIn}) and folded into longevity stats by a pure, testedtrackSession: the page shows "session alive for X", "longest kept alive" (recorded the moment a ping first sees the session dead), the last ping result, and a recent-ping table — so you can *measure* how long the SQ session actually survives. Honest caveat in the UI: idle-timeout (pings reset) vs absolute cap (they can't beat it), Chrome-open only, keep it gentle. - Panel: the ↻ keep-alive toggle is no longer gated on alerts, with a ⚙ Options & status link (worker
openOptionsaction).package:extnow bundles the options files. 46 tests. Spec:docs/proposals/options-page.md.
Extension [0.20.0] / [1.38.0]2026-06-16
Added — connecting itineraries (correct, itinerary-level parsing) [ADR-009]
flightsParsenow parses at the itinerary level: asegmentis one bookable journey, itslegskept together. A fare is bookable only if every leg has it (AND across legs), priced at the whole-journeydata.miles, with seats = min over legs. (Previously each leg was a bogus standalone "flight" with the whole-journey price stamped on it.) Nonstop = a 1-leg itinerary, unchanged.- Nonstop-only filter, default ON — connections are opt-in. When it hides options, a "N connecting option(s) hidden · show connections" hint appears.
- Connection display: the inline detail shows a
N stop · via SINheader, each leg with its flight no / route / times / aircraft and the layover between, plus a ✓/· mark on each leg showing which one matches your aircraft filter (Q1). Calendar cells get a⇆mark, list rows a1 stopbadge. CSV gainsstops+viacolumns. - Filter semantics (vetted): aircraft = match if any leg is a selected type; depart-time = first leg; seats = itinerary min. Worker
parseDetailported to mirror (deep alerts use itinerary seats / any-leg aircraft), kept honest by a fixture parity test. - Built against the captured real fixture; 45 tests (pure parse + jsdom render + worker parity), including hardening edge cases: multi-stop (3-leg) AND-availability, mixed nonstop+connection dates, the nonstop-only gate, and backward-compat with pre-1.38 cached records (no
legs→ treated as nonstop). Round-trip verified to pair connections correctly viadateSummary. Spec:docs/proposals/connecting-itineraries.md.
Extension [0.19.1] / [1.37.1]2026-06-16
Fixed — Search no longer silently uses a stale airport when you type a name
- Before: typing a city name in From/To (e.g. "Tokyo") and hitting Search without picking a suggestion silently fell back to the *previously selected* airport and searched that — no warning. Now Search resolves each field (3-letter code / "City (XXX)" / exact city or airport name / unchanged selection) and blocks with a clear message + red field if it can't, instead of guessing. Exact-name typing now also auto-resolves, and a bare 3 letters must be a real airport in the loaded list (so "Tok" no longer masquerades as code "TOK"). Same-origin/destination is caught too. (38 tests.)
Extension [0.19.0] / [1.37.0]2026-06-16
Added — deep alerts now re-check already-known dates (per-watch re-check cadence)
- Previously the background worker drilled deep criteria (aircraft / min-seats) only on newly-opened dates, so a plane swap onto a date you already saw (e.g. a 777 becoming an A380 on 14 Feb) never alerted. Now the worker also re-checks known-but-unmatched dates and alerts when one starts matching — exactly the *"I'm waiting for an A380 slot to open"* case.
- New per-watch "Re-check" control in the ✎ criteria editor (only shown with deep criteria), with a plain-language note so the behaviour is visible in the UI: - Balanced (default) — drill all new dates each poll, then sweep known dates in rotation (≤6/ poll) so all get re-checked over a few hours. - Aggressive — re-check (almost) everything each poll (cap 24) — catches a swap within the hour, most requests. - Gentle — new dates each poll; known dates only every 5th poll.
- Worker snapshot gains
matched(dates already confirmed — never re-drilled or re-alerted),cursor(rotation position) andtick(poll counter); old snapshots upgrade gracefully. New logic is a pure, testedselectDeepDrill(mode, …). 37 tests.
Extension [0.18.0] / [1.36.0]2026-06-16
Changed — flights expand inline, per date (replaces the bottom detail panel)
- List view: every result row now expands/collapses inline with a ▸/▾ caret. The whole row is the click target (the old decorative ▷ "play" icon is gone — it read like a required button). The first open on an unloaded date loads its planes & seats (one request) showing an in-row spinner; re-opening an already-loaded date is instant and costs no request. Multiple rows stay open at once so you can compare dates side by side.
- Calendar view: clicking a date drops a full-width detail strip beneath that week (cells are too small to expand in place); several weeks can hold open strips at once.
- Each expanded detail carries its own Open in SQ / ↗ booking deep-links (wired per-node, no id clashes). The single detail panel that used to render at the foot of the panel is removed, along with
selectedDate(state is now anexpandedset + aloadingDatemarker for the spinner). - New render tests assert the inline List detail and the Calendar week-strip;
prunePastnow prunes theexpandedset. 36 tests. *(Supersedes the 1.35.2 auto-scroll, which is no longer needed.)*
Extension [0.17.1] / [1.35.2]2026-06-16
Changed — open a date → auto-scroll to its flight detail *(superseded by 1.36.0's inline expand)*
- Opening a date smooth-scrolled the panel to the flight detail at the foot, where the fares and Open in SQ / ↗ booking deep-links live, instead of it rendering below the fold. Replaced by inline expand.
Extension [0.17.0] / [1.35.1]2026-06-16
Added — per-watch criteria enforced by the worker (Stage 3b)
- The background worker now enforces each route's criteria. Free max-miles filters at the calendar pass (
calendarChunknow returns the cheapest miles per date). Deep aircraft / min-seats: when a watch has them, the worker drills only the newly-opened dates (capped at 6/route/poll) via a compactparseDetail+fetchFlightDetail, and notifies only on matching dates. So *"alert me when SIN→PVG A380 Suites opens"* works without constant drilling. PortedcriteriaMatch/criteriaHasDeepinto the worker, guarded by parity tests (incl. the detail parser). 34 tests. - Clearer label: the criterion reads "Cheapest fare ≤ N k miles" (a price cap) instead of the terse "Max miles".
Extension [0.16.0] / [1.35.0]2026-06-16
Added — per-watch match criteria (Stage 3a: model + editor + on-demand)
- Each watched route can now carry alert/match criteria: Max miles (⚡ *free* — read from the calendar) and, behind a tagged ⟳ "deeper — drills new dates" box, Aircraft has (keyword, e.g.
A380, 777) + Min seats (*deep* — need flight detail). Set in the ✎ editor; the chip summarises them (✈A380 ≥2 ≤130k). - Check all now results now narrow by each route's own criteria (
criteriaMatch), on top of the global view filters. Pure tests forcriteriaMatch/criteriaHasDeep. (33 tests.) - *Next (Stage 3b): the background worker enforces these — free max-miles on the calendar pass, and deep aircraft/seats by drilling only newly-opened dates.*
Extension [0.15.0] / [1.34.0]2026-06-16
Added — the background's findings are now reusable in the panel
- The panel mirrors the worker's per-route snapshots (the dates it currently sees). Each Watch chip gets a 🛰 N badge; opening it lists those dates, and clicking one loads that route + date into Search (single-day range) ready to pull aircraft & seats. So you can act on what the background already found without re-scanning. (Extension only — the worker provides the data.)
Extension [0.14.0] / [1.33.1]2026-06-16
Added — per-watch date rules (Stage 2: background worker)
- The background worker now honours each route's own rules.
extSyncmirrorswhento the worker;scanRouteDatesscans the route's rule ranges (watchScanRanges) and keeps only qualifying dates (watchMatch— window + day-of-week), replacing the old fixed ~8-week window. The "When" rule logic is ported intobackground.jsand guarded by a parity test (test/worker.test.js, chrome stubbed) so it can't silently drift from the userscript.npm testnow runs pure + render + worker = 32. - *Next (Stage 3): opt-in deep alert criteria (aircraft/seats/time) drilling only newly-opened dates.*
Extension [0.13.0] / [1.33.0]2026-06-16
Added — per-watch date rules (Stage 1: on-demand)
- Each watched route now owns its own dates — no more borrowing the Search-form span. Tap ✎ on a chip to set rules: Next N weeks (rolling) · Dates (a specific From→To) · Newest (the dates opening at the ~355-day edge), each with optional day-of-week. Stack multiple rules per route (union) — e.g. *"01 Jun–30 Jun wkends + 01 Dec–31 Dec wkdys"*. The chip summarises them.
- Check all now and per-route checks scan each route's own rules (
watchScanRanges), keeping only qualifying dates (watchMatch— in a rule's window + its day-of-week). Existing watch items migrate tonext 8 weeks. Pure invariant tests added (next/range/newest, day-of-week, union, horizon clamp). - *Next (Stage 2): the background worker uses these per-route rules; (Stage 3) opt-in deep alert criteria (aircraft/seats/time) that drill only newly-opened dates.*
Extension [0.12.0] / [1.32.0]2026-06-16
Added — seats.aero-style Search filters
- Day-of-week filter — in "More filters":
All / Wkdys / Wkends+ per-day chips (Mon–Sun). Keeps only dates on the chosen days. (Shared logic with the upcoming per-watch date rules.) - Max-miles filter — cap results at N k miles per flight.
- Both are instant view-filters — they recompute from already-loaded flights, no extra search. Added a "⚡ All instant — no extra search" note on the filters card to make that explicit (deeper filters that would need extra calls will be tagged + opt-in, not silent). Invariant tests added.
Extension [0.11.2] / [1.31.2]2026-06-16
Fixed (round-trip audit)
- Round-trips no longer pair across stale routes. If you changed From/To in round-trip mode without re-scanning both legs,
tripPairswould pair the new-route outbound against the old-route return. Now a leg only pairs when its cached data'scacheKeymatches the *current* route. Tested.
Added (testing)
- More filter edge cases (
matchingFlights): all-aircraft-off → none; unknown seats pass min-seats (summarise to 0); noon counts as afternoon not morning; missing depTime passes the time filter. - Round-trip: stale-route leg ignored.
npm test= pure (22) + render (7) = 29.
Extension [0.11.1] / [1.31.0]2026-06-16
Added (testing)
- Audit-pass invariant tests across more areas (no new bugs found — the logic was sound; now locked):
matchingFlightsfilter rules (aircraft-off / min-seats / depart-time),tripPairsround-trip pairing (stay-window inclusive, negative-nights excluded, seats=min / miles=sum),scanWindowbounds (always within[today, +355], ≤120 days, end≥start), and signed-out gating (Search disabled + login bar; none on Help).npm test= pure (21) + render (7) = 28.
Extension [0.11.0] / [1.31.0]2026-06-16
Changed — result-consistency audit + a render test harness
- One canonical result set. The List, Calendar, "best value" and counts now all derive from
S.calendarwith the invariantS.cache ⊆ S.calendar ⊆ [today, today+355]. Fixes two latent divergences found in audit: an orphan cached date (loaded under a different date span) could appear in the Calendar / be crowned cheapest but not show in the List; the grid now spans the found set, a full scan drops orphan cache, andsweetInfo()ignores non-found dates. (ADR-022) - Watch tab states its date window — a note now explains a check scans the date span set on the Search tab (not the full 355 days), and that background auto-check looks ~8 weeks ahead.
Added (testing)
test/render.test.js— a jsdom harness that mounts the real panel and asserts cross-view invariants unit tests can't reach: no past dates in results, List dates === Calendar dates, best value ignores orphan cache, empty state, tabs.npm testnow runs pure (18) + render (6) = 24. jsdom is a devDependency; the render suite skips gracefully if it isn't installed.
Extension [0.10.0] / [1.30.0]2026-06-16
Fixed (edge cases)
- Stale past dates no longer leak into results. A scan saved on a previous day left now-past (unbookable) dates in the calendar/cache. The List iterated that saved data and showed them, while the Calendar grid only renders today-onward — so they disagreed, and "best value" was computed off a past date. New
prunePast()drops any date before today from calendar/cache/snapshot/selection (and the round-trip legs + watched routes), called on every render. Regression test added. - List and Calendar now always agree. The Calendar grid spans the dates actually found (not the live form's scan window, which diverges if you change the dates without re-searching).
- Date picker opens again. The custom
showPicker()click handler double-triggered the native indicator and toggled the picker shut; removed it — the now-visible calendar icon opens it natively.
Added
- Sticky header + tabs. The title and the Search/Watch/Help tabs stay pinned at the top while the body scrolls, so you can switch tabs without scrolling back up.
Extension [0.9.1] / [1.29.1]2026-06-16
Fixed
- Best-value ★ is now an inline SVG — the previous character + variation-selector fix worked in most setups but could still get macOS *emoji* presentation on some Chrome builds (rendering it large / grey). The marker is now a small drawn SVG star (gold, 11px) — never a font glyph, so it's bulletproof across systems. Calendar-cell star forced to text presentation too.
- Range note moved to its own line — the "ⓘ scanning from today…" note was crammed onto the same line as the date range and hard to read; it's now a separate line below, divided off.
Extension [0.9.0] / [1.29.0]2026-06-15
Changed — Watch & Help scrutiny pass (+ two reported nits)
Watch tab
- Explainer collapses once you have routes — the long "What is this?/How to use it" block now shows in full only when empty; with routes it folds behind a small ▸ How Watch works toggle.
- Route chips show state — each chip now shows seats (when >1) and checked Xh ago / not checked, and clicking a chip checks just that route (new per-route
runWatchRoute). - Title shortened to "★ Watched routes (N/8)"; a "what next" line appears when you have routes but no results yet.
Help tab
- Split into collapsible sections (Basics · Dates/round-trip/freshness · Where the data comes from · How SQ releases space) — Basics open by default; no more wall of text.
- Synced stale labels — "Load aircraft & seats" → Load all, ".ics" → 📅 Calendar, "🗄 line" → "Details cached line"; the privacy section now correctly notes the extension's optional background check (instead of a blanket "no background"). Login bar hidden on Help; Troubleshooting label added above the debug tools.
Reported nits
- Oversized ★ in the results list — it was getting macOS *emoji* presentation (large, two-tone). Forced text presentation (
★︎+font-variant-emoji) and a fixed size → small gold star. - "🗄" cabinet icon replaced everywhere with plain wording ("Details cached: …").
Extension [0.8.3] / [1.28.3]2026-06-15
Changed — more Search-flow polish (review items 1–4)
- "Load aircraft & seats" moved out of the form into the results header as "Load all (N)" — it only appears when there are un-loaded dates (shows "all loaded" otherwise). The form is now a single primary action: 🔍 Search. (Results header wraps on narrow panels.)
- Purpose line on first run — the "Start here" card now opens with what the tool is: *finds KrisFlyer award (miles) seats — Suites/First/Business/Economy — across a date range, on your own singaporeair.com session.*
- ".ics" → "📅 Calendar" — the export is labelled by intent, not file extension.
- Round-trip hint — selecting Round-trip now shows a one-line note that it scans each leg separately (≈2× requests): Search Outbound, switch to Return, Search again → date pairs appear.
Extension [0.8.2] / [1.28.2]2026-06-15
Changed — Search-flow review polish
- Clearer (not alarming) range note. "⚠ start moved to today (past dates skipped)" → a neutral ⓘ "scanning from today — your range reaches before today, and past dates can't be booked." The amber warning is now reserved for the real booking limit (355-day horizon) and the 120-day cap.
- List no longer leaks system state. "Not loaded — click to load aircraft & seats" → "More dates with space — open one to see planes & seats" (rows: "open to see planes & seats"); "Matches" → "Best dates"; "No match" → "Has space, but no flight matches your filters".
- Cleaner empty state. On first run the redundant status line is hidden so the "Start here" card carries the message; the restore line is demoted to a small "↩ restored your last search · reset" (timestamp moved to its tooltip).
- "Pax" → "Seats" (award seats you need on a flight).
Extension [0.8.1] / [1.28.1]2026-06-15
Changed
- Watch tab: "+ Add current route" → "✈ Go to Search". Now that routes are added via ★ Watch this route on the Search tab, the old add button was redundant and re-introduced the cross-tab confusion. It's now a Go to Search button that takes you to the Search tab — one clear way to add a route.
Extension [0.8.0] / [1.28.0]2026-06-15
Changed — date controls + watch-from-search
- Removed the confusing "Weeks" number and "Weeks fwd" preset. The date span is now just the ± presets (±3d / ±1wk / ±2wk / ±4wk / ±8wk) around your date, plus a Custom… option that reveals a From → To date picker for an exact range. One control instead of two overlapping ones. (Old persisted
flexDays:0migrates to ±2wk.) Background-alert scan depth decoupled (fixed ~8 weeks). - Calendar icon now visible — the native date-picker indicator was a dark glyph lost on the dark theme; it's brightened, and clicking a date field opens the picker.
- ★ Watch this route — from the Search tab. You no longer have to set a route on Search *then* switch to Watch to add it (an odd cross-tab dance). A one-click ★ Watch this route button on Search saves it straight to your Watch list; the button shows "★ Watching this route" once saved. Watch-tab copy updated to match.
- Long Custom ranges: the 120-day-per-scan cap now reflects in the shown end date with a clear note.
Extension [0.7.1] / [1.27.1]2026-06-15
Changed
- Watch tab now explains itself — a new user landing on Watch found it confusing because the copy jumped into *how* before *what*. It now leads with "What is this?" (SQ releases seats in waves and they sell out, so save routes and re-check them in one click / get alerts), then a numbered "How to use it", then the empty-state nudge. Title is now "★ Watch — keep an eye on routes".
Extension [0.7.0] / [1.27.0]2026-06-15
Changed — UX overhaul, Stage 2 (Search flow)
- Details on demand — the confusing two-step ① Scan / ② Load is gone. 🔍 Search finds dates; aircraft & seats then load when you click a date (
loadDatedrills just that one), or all at once with Load aircraft & seats, or automatically via the Auto-load after Search toggle (off by default, and it states "≈1 request per found date" so the cost is explicit). (ADR-021) - List ⇄ Calendar toggle — results default to a clean List (matches first → click-to-load dates → no-match), with a one-click switch to the month Calendar grid. (
S.resultView) - Click-to-load everywhere — un-loaded dates are now clickable in both views (calendar cells and the List's "tap to load" rows) to fetch just that date.
- Plain language — no more circled ①/②; buttons, tooltips, status and Help describe the flow in words. Added a first-run "Start here" empty state.
- Verified live in Chrome via a new
test/preview.htmlharness (List + Calendar with sample data).
Extension [0.6.0] / [1.26.0]2026-06-15
Changed — UX overhaul, Stage 1 (tabs)
- Three tabs: Search · Watch · Help. The panel was one long, overwhelming column; it's now split so a new user sees a clean Search by default, with everything else one tab away. (ADR-020, spec
docs/proposals/ux-overhaul.md.) - Watchlist + Alerts unified into "Watch" — they were confusingly separate, but alerts just watch the watchlist. The Watch tab now reads as one idea: tracked routes → Check all now (on-demand) → optional 🔔 Auto-check in the background (the old alerts: schedule, Test, 📜 history).
- Advanced hidden by default — aircraft/product/seat/time filters collapse behind ▸ More filters; debug + dump-state moved to the Help tab.
- No features or data removed — sections were tab-guarded, not rewritten; all element ids/handlers intact. Stage 2 (next): Search-flow polish — list/calendar toggle, details on-demand + auto-load toggle, plain-language button renames, first-run empty state.
Extension [0.5.3] / [1.25.0]2026-06-15
Added
- Fare cost vs your balance — when signed in (and miles known), each fare pill on the date board shows how it sits against your KrisFlyer balance for your pax count: green "Nk left" if you can cover it, red "short Nk" if not. Tooltip has the full breakdown (total for N pax, remaining, % of balance). Pure
affordCalc()helper, unit-tested. (HeyMax parity:milesBalanceDelta.)
Extension [0.5.2] / [1.24.1]2026-06-15
Fixed
- Miles balance now shows — the field is
profile.ffpMiles(confirmed against HeyMax:Number(d.profile.ffpMiles)), which my defensive guesses had missed. Added it as the primary key (other spellings stay as fallbacks). Documented thedwLoggedInUserData.formresponse indocs/SQ-API.md §6; test now uses the real shape.
Extension [0.5.1] / [1.24.0]2026-06-15
Added
- Open in SQ — new-tab option — the booking link now defaults to opening in this tab (board is saved/restored), with a separate ↗ button to open in a new tab (keeps the panel). Applies to the date board and both legs of round-trip rows. (The ↗ icon now actually means "new tab".)
- Signed-in line shows KrisFlyer number + miles — "✓ Signed in · Name" now also shows KF \<number\> and your miles balance when the session exposes it (read from your own session, never sent anywhere). Miles field name is matched defensively across common spellings; if it's not found, just name + KF number show, and
LOGIN keys:is logged in debug to help identify it.
Changed
- Instant tooltips — replaced the native
titlehover (browser-fixed ~1s delay) with a custom bubble shown immediately. One delegated handler reuses the existingtitle=""text (still restored for accessibility when not hovering), so all ~150 tips are now instant without markup churn.
Extension [0.5.0] / [1.23.0]2026-06-15
Added
- Click an alert → open the actual flight — a notification now deep-links to *its own* route + earliest new date (encoded in the notification id, so it survives the ephemeral service worker). Clicking opens a fresh SQ tab and the content script runs the existing "Open in SQ" (submits SQ's redemption search), landing you on the real bookable results for that flight instead of a generic page. Multiple alerts each open independently to the correct flight (HeyMax-style). Worker hands off to the tab via
chrome.tabs.sendMessage(retried until the content script is ready); asessionStoragerid-guard stops a re-fire across the openInSQ navigation. Test/sign-out notifications still open the generic redemption page. (ADR-018)
Extension [0.4.1] / [1.22.1]2026-06-15
Added
- Errors now show in the 📜 history — a route whose background check fails (timeout, network, SQ returning HTML, …) used to be silently skipped, so the timeline couldn't tell "no change" apart from "check errored." It now logs a red ⚠ check failed: <reason> event, and the global status line reflects route failures too (was falsely showing "✓"). Consecutive identical errors collapse into one row with a
×Ncount so an hourly-failing route doesn't flood the log. (ADR-019)
Changed
- History cap raised 40 → 200 events/route so longer-running watches keep more of their timeline.
Extension [0.4.0] / [1.22.0]2026-06-15
Added
- Per-route alert history (📜) — the background worker now logs every change it sees on a watched route, not just new dates: each poll appends
{ts, added[], removed[]}(first check is a silentbaseline), capped at 40 events/route. Open the 📜 on a watchlist chip to see the timeline — +green dates that opened, −red dates that vanished — so you can revisit *what happened* to a search. History mirrors live from the worker (storage.onChanged); clear wipes one route's log; removing a watch item prunes its history. Notifications still fire on new dates only (removals are logged, not pinged). Extension-only — the userscript has no background poller. (ADR-019) - Version in the panel header — a
v1.22.0chip next to the title (was only in the footer), so it's obvious which build you're running when reporting issues.
Extension [0.3.0] / [1.21.0]2026-06-15
Added
- Alert schedule — a "Check" toggle:
~hourly(catches mid-day waves) or08:00 SGT, which fires at 00:00 & 00:01 GMT (= 08:00 & 08:01 SGT) daily — the exact moment SQ loads new T-355 award dates. (schedulemirrored to the worker;sqaf-poll-rel0/rel1daily alarms.) - Delete recents — each recent/pinned chip now has a ✕ to remove it (was only pin/unpin; the tooltip explains unpinned age out after the cap).
Changed
- Notifications now "punch through" —
requireInteraction: true+ max priority, so they stay on screen until dismissed instead of dropping silently into Notification Center. (For macOS, also set System Settings → Notifications → Google Chrome → style Alerts.) The Test-alert hint says so.
Extension [0.2.2] / [1.20.0]2026-06-15
Added
- Test alert button (in the alerts row) — fires a sample desktop notification on demand, so you can confirm OS notifications are allowed and the worker is reachable.
- Background status line — "✓ Background last checked Xh ago · next within ~1h" (or "first check within ~an hour", or a "signed out" warning), updated live via
chrome.storage.onChanged. So you can see the poll is actually running, not just that notifications work.
Extension [0.2.1] / [1.19.0]2026-06-15
Changed
- Reverted the same-day floor — SQ award is bookable until ~2h before departure, so today is a valid start again (the earlier same-day FAILURE was actually the signed-out case, now handled).
- Proper airplane icon (was a flat blue placeholder).
Added
- Help: "How SQ releases award space" — T-355, daily ~08:00 SGT, and that space loads in waves ("gone" can reappear; First/Suites trickle in) — which is *why* the watchlist + alerts pay off.
- Optional
↻ Session keep-alivesub-toggle (extension, off by default): while alerts are on, a lightdwLoggedInUserData.formping every ~15 min refreshes the idle timer so the session is less likely to lapse (only while the browser stays open; won't survive a full close or SQ's hard cap).
Extension [0.2.0] / [1.18.0]2026-06-15
Added
- Phase 2 — background availability alerts (ADR-018). A service worker +
chrome.alarmspolls your watchlist routes about once an hour, even with every tab closed (it carries your SQ cookies viahost_permissions), and fires a desktop notification when a new award date opens on a watched route. Calendar-only (light on SQ); aircraft/seat detail stays on-demand in the panel. First observation sets a silent baseline (no spam); pauses + notifies once if you're signed out. Throttled, capped (≤8 routes, ≤12 weeks). Click a notification → opens SQ's redemption page. - The panel gains a 🔔 Background alerts toggle (in the Watchlist card, extension only); it mirrors the watchlist + setting to
chrome.storagefor the worker. Plain userscript shows an "install the extension for alerts" hint. New permissions:alarms,notifications,storage,cookies.
Extension [0.1.0]2026-06-15
Added
- Chrome MV3 extension (Phase 1) in
extension/— repackages the userscript as a content script on singaporeair.com (same features, same own-session model, no userscript manager).content.jsis generated fromsrc/vianpm run build:ext. No background alerts yet — that's Phase 2 (docs/proposals/extension.md, ADR-018). Load unpacked viachrome://extensions.
[1.17.1]2026-06-14
Added
- Transparency section in the help panel — "Where the data comes from & what it does": lists the exact SQ endpoints used, explains the auth model (your existing logged-in session cookies; no password, no credentials stored, no server), throttling, local-only caching, and how to verify via 🐛 debug.
[1.17.0]2026-06-14
Added
- KrisFlyer login detection + gating (ADR-017). On load (and on any all-FAILURE scan) the tool checks
/home/dwLoggedInUserData.form(signed in ⟺ akfNumberis present) plus the_kfvstokencookie. When signed out it shows a red "Not signed in to KrisFlyer" bar with a Re-check link and disables Scan / Load / Refresh / Watchlist-scan; the FAILURE message now names login as the likely cause. A green "Signed in" bar shows when authenticated. - New pure
parseLoginData()(+ unit tests). This fixes the earlier mis-diagnosis where a signed-out{status:FAILURE}looked like a same-day/date-range problem.
[1.16.0]2026-06-14
Added
- Surface SQ's aircraft config sub-code.
aircraftCodeis<equip>_<config>_<flightNo>(e.g.77W_7W2_828); the date detail now showsequip·config(77W·7W2) with the full code in the tooltip, and CSV gainsconfig+aircraftCodecolumns — so seat-product patterns can be eyeballed/analysed even though the sub-code's meaning isn't decoded yet.
Fixed
- Same-day search returned
{status:FAILURE}. SQ has no same-day award search, so the scan now floors to tomorrow (date-picker min +scanWindow), with a clearer FAILURE message.
[1.15.1]2026-06-14
Changed
- In-UI guidance. The watchlist card now has a numbered how-to (with a "pull-only, no notifications" note), and the ? How it works panel was expanded to cover Span, round-trip, watchlist, freshness/refresh, and the no-background-checks caveat — so usage is explained in the app, not just in docs/chat.
[1.15.0]2026-06-14
Added
- Multi-route watchlist (N4 / ADR-016). A ★ header toggle opens a Watchlist card: add the current route (up to 8), then Scan all loads every route one at a time (throttled, capped at 31 dates/route) into its own cache — without disturbing the active board. A Watchlist results panel combines matching dates across all routes (min-seats + departure-time filters apply), sorted by seats; each row shows route/date/aircraft/seats/miles with a view button to open that route+date in the board for full per-aircraft filtering. Watchlist + scanned data persist. ~N× the requests of a single route — opt-in, with per-route freshness shown.
[1.14.1]2026-06-14
Added
- Cache-age surfaced in more places: ranked rows show "loaded Xh ago" in their tooltip, and the Round-trips panel shows a per-leg freshness line (
🗄 Outbound: … · Return: …) with each pair's per-leg load age in its tooltip.cacheInfo()now accepts a board's cache.
[1.14.0]2026-06-14
Added
- Cache-freshness made explicit. A
🗄 N date(s) loaded · newest … · oldest … · M stale (>6h)summary line, an ↻ Refresh all button (force re-fetch every loaded date now, ignoring the 6h check), and per-date "details loaded Xh ago" in each cell's tooltip.
Changed
- Moved the session / personal-use disclaimer to the top of the panel (with a "how it works" link); the footer now keeps only debug/dump + the version.
- Clearer initial status text ("Ready. Pick route, cabin & dates above, then ① Scan calendar.").
- Footer shows the version (
VERSIONconstant).
[1.13.1]2026-06-14
Changed
- ± presets snap to whole 7-day weeks. Since each calendar call returns a fixed 7-day window, the ± spans are now exact multiples of 7 (±3d→7, ±1wk→14, ±2wk→28, ±4wk→56 days) instead of
2N+1— so every scan is an exact number of calls with no tail over-fetch / no stray extra call. (1-day asymmetry: one more day before the target than after.)
[1.13.0]2026-06-14
Added
- ± date-range presets (ROADMAP #5). A Span toggle:
Weeks fwd(the existing "N weeks after Start") or±3d / ±1wk / ±2wk / ±4wk, which treat the Start date as the target/centre and scan that many days either side (no drift; Weeks is disabled in ± mode). Composes with round-trip (the return leg still shifts by the stay window). Unit-tested.
[1.12.0]2026-06-14
Added
- Round-trip search (N3 / ADR-015, option A — pairing view). A Trip toggle (One-way / Round-trip); in round-trip mode an Outbound / Return leg switcher and a stay length (min–max nights). Each leg is its own board (scan + load independently; the return leg auto-scans the outbound window shifted by the stay range). A Round-trips panel pairs (outbound, return) dates that both pass your filters and fall in the stay window — combined miles, min seats across the pair, per-leg aircraft, and out↗ / in↗ Open-in-SQ buttons. One-way mode is unchanged.
Changed
- Persistence schema →
sqaf_state_v2(multi-board). Per "fresh start", the old one-way snapshot (v1) is not migrated and is removed on first load.
Tooling
- Added a
tripPairsunit test (now 11 tests).
[1.11.0]2026-06-14
Added
- Explanatory tooltips throughout (ADR-014). Native
titlehovers on every control and indicator, written for power users — what each does and *how to read what's happening*: - Per-date cells get a full interpretation (cheapest miles, best aircraft, max seats, matching-flight count, sweet/★, and any change-since-last-load), plus why a cell is dimmed (“award space exists but no flight matches your filters”) or not yet loaded. - The two-step flow, incremental load, 7-day window / 355-day clamp, filters-don't-re-query, product/maker/time/min-seats, aircraft chips (type · product · equipment), tier pills (bookable vs waitlist), change markers (✦/▲/▼), recents/pinning, CSV/.ics, debug/dump.
[1.10.0]2026-06-14
Added
- Open in SQ ↗ (ROADMAP #2 / ADR-013): button on a selected date that submits SQ's own redemption search form for that date, navigating the tab to the real results page to book — the same mechanism the site uses. State is persisted, so the board restores when you return.
[1.9.0]2026-06-14
Added
- Availability-change diff (N2 / ADR-012): each drill compares the fresh seats against the previous load's snapshot (same route) and shows what changed — new dates, seat deltas (
4→7), new/gone flights — as cell markers (✦/▲/▼) and a "Changes since …" panel. Re-scan now prunes cache entries for dates that lost all space, so the board stays accurate. Persisted. - .ics export (N5): export each matching date as an all-day calendar event, next to CSV.
Tooling
- Parser unit tests (
npm test, no deps): loads the script in a stubbed env and asserts over the pure functions via awindow.__sqafTesthook. 10 tests coveringaircraftCategory,calendarParse,flightsParse(tiers/dedupe/empty), andcomputeDiff. Addedpackage.jsonwithcheck/testscripts. (ROADMAP #6.)
[1.8.0]2026-06-14
Added
- Retry/backoff on transient request failures (network error / timeout / 429 / 5xx): bounded
httpRetry(3 tries, 0.8s→1.6s backoff, never retries on abort) wraps the calendar, prime, and flight-detail calls. A persistently failing date now records an error and the drill continues instead of aborting the whole run. (ROADMAP #3 / ADR-011.) - Airport list cached 24h in
localStorage— skips the lookup on subsequent sessions. (ROADMAP #4.)
[1.7.0]2026-06-14
Added
- Aircraft product filter (ADR-010, option A): a "Product" quick-toggle row (Long-haul / Regional / Narrowbody / All) derived only from explicit signals in SQ's aircraft name. Chips carry a product tooltip; categories logged under 🐛 debug. Does not infer seat generation. Pure classifier + UI — no contract/data-model change.
[1.6.0]2026-06-14
Added
- Incremental cache: flight detail is cached per date with a timestamp and keyed by route+cabin; re-scans only fetch missing/stale (>6h) dates.
- Recent + pinned searches (chips; up to 6 recent + unlimited pinned).
- All fare tiers + waitlist shown per flight in the date detail (Saver/Advantage/… pills, greyed waitlist).
- Sweet-day highlights: dates at the cheapest miles in range get a ★ + badge.
[1.5.0]2026-06-14
Added
- UI redesign: card sections, draggable + resizable panel, weekday-aligned calendar with month labels, departure-time presets, inline help, footer for debug.
Fixed
- Clamp the scan to SQ's 355-day booking window (start→today, end→today+355) with a notice; surface
status:"FAILURE"clearly instead of silent "0 dates". (Matches HeyMaxau=355.)
[1.4.0]2026-06-14
Added
- CSV export of currently-matching flights.
- Persistence: last search + scanned data saved to
localStorageand restored on reload.
[1.3.0]2026-06-14
Added
- Departure-time filter.
- Per-date best aircraft + seat count in each cell, green intensity by seats.
- "Best dates by seats" ranked list.
[1.2.0]2026-06-14
Added
- City/airport autocomplete (SQ
getCIBAirportListJson). - Explicit start date + weeks with a live "dates being scanned" caption.
[1.1.0]2026-06-14
Fixed
- Prime the redemption session before scanning — the calendar was returning homepage HTML (302) instead of JSON. Re-prime + retry on lapsed session.
[1.0.0]2026-06-14
Added
- Initial userscript: prime → scan award calendar (7-day windows) → drill each date for flight detail → filter the calendar by aircraft type / seats. Debug toggle +
window.__sqaf.
--- *Context:* this began as a security review of the HeyMax Chrome extension (kgdlkkikggjhckdodghipinhceacbfhf), which was found safe; the SQ award endpoints were reverse-engineered from it to build this companion tool.