Hotfix for v0.5.1's BAGeL/PopTron slug mistake. Same deferred-from- Web-Store pattern as v0.5.1 — bundle ships only after v0.5.0 clears.
24 KiB
Changelog
All notable changes to RangerHQ Tuner are documented here. Format: Keep a Changelog 1.1.0 — versioning: SemVer.
[Unreleased]
Planned — Future polish
.m3uparser alongside.plsto widen future-adapter compatibility- Station artwork lazy-load + fallback to family helmet
- Better error UI for failed streams ("Stream unavailable, try another")
.crxpackaging instructions in README for sideload users- Second source adapter stub to exercise the multi-source UI
[0.5.2] — 2026-06-10 — Quick Stations fix: replace retired BAGeL with PopTron
Hotfix for v0.5.1's Quick Stations row. David flagged: "the stations still look a bit off" — turned out only 13 chips were rendering instead of the intended 14. Investigation showed BAGeL Radio has been retired from SomaFM's current channels.json catalogue — the render loop's "drop missing slugs silently" behaviour (working as designed) made the chip just disappear.
Changed
src/newtab/newtab.jsQUICK_IDSarray:'bagel'→'poptron'. PopTron (alternative / alt-rock) fills the same indie/alt-rock genre slot BAGeL covered, and is currently a live SomaFM channel.
Result
14 chips visible again. Layout reads cleaner — 6+6+2 instead of 6+6+1 (ThistleRadio no longer alone on its own row).
No other change
- No CSS, no permissions, no dependencies, no behaviour change
- Single one-line edit in
QUICK_IDS - Still held from Chrome Web Store upload until v0.5.0 clears re-review
Lesson — SomaFM channel catalogue is mutable
Channels can be retired at any time. The render loop's "drop missing slugs silently" pattern protects against this, but the developer-side responsibility is to verify slugs against the live channels.json before adding to QUICK_IDS:
curl -s https://somafm.com/channels.json | python3 -c "import json,sys; print([c['id'] for c in json.load(sys.stdin)['channels']])"
[0.5.1] — 2026-06-10 — Quick Stations expansion (8 → 14)
Third update of the day. Tagged on Gitea but held from Chrome Web Store upload until v0.5.0 clears re-review — we want the v0.5.0 features (light mode + theme toggle + back-link + first-run UX hint) landing in users' browsers before this stations expansion piles on top.
Added — 6 more channels in the NewTab Quick Stations chip row
Was reading sparse with only 8 chips. The row had space for more. Added six channels for broader genre coverage + two on-brand picks:
| Slug | Channel | Why |
|---|---|---|
defcon |
DEF CON Radio | Electronic / hacker culture — David's personal favourite, on-brand for a cybersecurity-adjacent extension |
beatblender |
Beat Blender | Electronic / breakbeat — popular channel |
bootliquor |
Boot Liquor | Americana / outlaw country — adds country/twang coverage |
u80s |
Underground 80s | 80s indie / synthwave — adds decade coverage |
bagel |
BAGeL Radio | Indie / alt-rock — guitar-driven indie |
thistle |
ThistleRadio | Celtic / folk — 🇮🇪 Dublin-built nod |
Total chip count: 8 → 14. No new files, no new permissions, no new dependencies, no code change other than the QUICK_IDS array in src/newtab/newtab.js.
The render loop already drops any slug missing from SomaFM's channels.json catalogue, so if a channel is retired upstream the chip just disappears silently — no error, no broken state.
Files touched
src/newtab/newtab.js—QUICK_IDSarray updated from 8 to 14 entries, with per-slug genre comments. +19 / -8 lines.
Total: 1 file, +19/-8 lines.
Not changed
- No new permissions
- No new dependencies
- No CSS / HTML changes
- No data migration
Shipping note
v0.5.1 is tagged on Gitea on 2026-06-10 but deliberately held from the Chrome Web Store until v0.5.0 clears re-review (currently pending). David's reasoning: "we will ship to web store after the last one gets accepted, that one is important so a user knows what to do" — the v0.5.0 first-run hint + light mode features are higher-value for new users; this stations expansion is a smaller polish that should land after.
[0.5.0] — 2026-06-10 — Light mode + theme toggle + Options back-link
Second post-launch update, ~36 hours after v0.4.0 cleared the Chrome Web Store. Three bundled improvements, all UX polish:
Added — Light mode support across all three surfaces
Phase 1 — Auto-follow OS theme. Each stylesheet (popup, NewTab, Options) gets a @media (prefers-color-scheme: light) block that flips the :root CSS variables to a light palette while preserving brand identity. Every existing rule using var(--*) tokens carries over unchanged.
Light palette:
--bg#f6f4edcream-leaning off-white--bg-soft#ece5d2slightly darker for header bars--bg-row#ddd6c0hover backgrounds--bg-row-hi#c8c0a8active rows--fg#2a2f28dark forest-green-leaning text--accent#2a7d3edarker brand green (preserves "RangerHQ green" identity)--cream#b8861adarker amber (readable on light)--danger#b53a2bdarker red (readable on light)
NewTab helmet watermark opacity bumped 0.025 → 0.04 in light mode (helmet contrasts differently on light bg). Wired via a new --watermark-opacity CSS variable.
State pills, primary play button, toasts, and danger button hover all get text-colour overrides for both modes so they stay readable when their background becomes solid accent / cream / danger.
Added — Manual Auto / Dark / Light theme toggle (Phase 2)
Three-radio group in a new "Appearance" card on the Options page. Stored in chrome.storage.local under tuner.theme. Defaults to "auto" (follow OS).
- Auto — follows OS via
@media (prefers-color-scheme: light)(covers ~95% of users) - Dark — forces dark palette regardless of OS
- Light — forces light palette regardless of OS
This exists because there are three independent theme layers (OS, browser-chrome, page CSS) that can disagree. @media (prefers-color-scheme: ...) reads the OS layer only, NOT the browser-chrome theme. So users with mismatched layers (e.g. macOS light + Chrome dark theme installed) can override the auto-follow.
Implementation: data-theme attribute on <html> ( "light" / "dark" / removed for auto). html[data-theme="..."] CSS selectors beat both the default :root and the @media :root on specificity.
New file: src/lib/theme.js — shared getTheme/setTheme/applyTheme/initTheme helpers used by all three entry-point scripts.
The choice syncs across surfaces via chrome.storage.onChanged: pick Light in Options → popup + NewTab flip instantly without reload. Theme applies BEFORE first paint (each init() calls initTheme() first) so there's no dark flash on light-mode-preferring browsers.
Added — Options page → Tuner back-link
The Options page header (helmet + "RangerHQ Tuner — Options") is now a single clickable anchor pointing at newtab.html. Subtle ← glyph to the left of the helmet; hover shifts the arrow left and tints it accent green. Same-tab navigation — user came IN via Options, goes OUT into the player UI in the same tab. No accumulating Tuner tabs.
Fixes UX gap David flagged on 2026-06-09 night: "when i click the settings button i go to settings but we have no back link to radio."
Files touched
src/lib/theme.js(NEW, ~55 lines)src/popup/popup.css(+50 lines — light + dark override blocks)src/popup/popup.js(+8 lines — initTheme call + storage.onChanged hook)src/newtab/newtab.css(+50 lines — light + dark override blocks)src/newtab/newtab.js(+8 lines — same wiring)src/options/options.html(+24 lines — Appearance card)src/options/options.css(+90 lines — light + dark blocks + radio styling)src/options/options.js(+32 lines — radio handlers + cross-surface sync)
Total: 8 files, ~340 lines added.
Not changed
- No new permissions
- No new host_permissions
- No new external libraries
- No data migration required
- Existing user state (current station / volume / history / favourites) survives intact
Reviewer expectations
Same-account update with no permission change. Same-day review expected (~hours to 24h), similar to v0.4.0's 24h-ish re-review window per reference_chrome_web_store_rules.
[0.4.0] — 2026-06-09 — First-run UX hint
First post-launch update, shipped the same day v0.3.0 went LIVE on the Chrome Web Store. Pure UX polish — zero new permissions, zero new code dependencies, zero behaviour change for existing users.
Added — Discoverable "pick a station to begin" affordance
Triggered by David's own 30-second panic on first Web Store install (uninstalled the dev build, installed from the Web Store fresh, hit Play, got nothing, then realised "ah, I need to pick a station first"). The product worked correctly — the dev build had a seeded station from hours of testing, the fresh install does not — but the 30-second panic exposed a real first-run-UX gap.
Two layered cues, both pure CSS driven by a body.is-first-run class:
- Subtle accent-green glow pulses around the station list (popup) and the Quick Stations chip row (NewTab). Uses a 2.4-second
box-shadowkeyframe at low alpha (0.18-0.25) — visible but not noisy. - Bouncing ↓ arrow appended to the "Pick a station to begin" text in both surfaces. Uses an
::afterpseudo-element with a 1.8-secondtranslateYkeyframe.
The is-first-run class is toggled by a tiny reflectFirstRunHint() function called from:
init()once stations +currentStationare resolvedonPickStation()the moment a user picks- the
chrome.storage.onChangedlistener when another surface picks (so the hint disappears on both surfaces simultaneously via cross-surface sync)
Existing users with a stored tuner.currentStationId never see either cue — the class only attaches when currentStation is null.
Files touched
src/popup/popup.css(+35 lines — keyframes +.is-first-runrules)src/popup/popup.js(+11 lines —reflectFirstRunHint()+ 3 call sites)src/newtab/newtab.css(+36 lines — same idea, NewTab-namespaced)src/newtab/newtab.js(+10 lines — same pattern)
Total: 4 files, +92 lines, 0 deletions.
Not changed
- No new permissions
- No new host_permissions
- No new external libraries
- No change to
manifest.jsonbeyond the version bump and CHANGELOG-referenced URL - No data migration required (no storage shape change)
Same-day context
This update ships the same day:
- v0.3.0 went LIVE on the Chrome Web Store (~17:08 Dublin, ~15.5h after submission)
- RangerHQ Radio v1.0.0 stability milestone went LIVE on WordPress.org (~21:51 Dublin)
- David received a PhD-prep signal from his Research in Computing lecturer at NCI Dublin
A solid day.
[0.3.0] — 2026-06-08
Added — Track history + 4-button search + Options page (Web Store target)
This is the version that goes to the Chrome Web Store. Mirrors the rangerhq-radio WordPress plugin's history feature so the family stays coherent across surfaces.
Track history
src/lib/history.js— helpers for history/favourites/search-URL/dedup/cap/clear, mirroringinc/history.phpin the WP plugin.- Storage:
tuner.history(FIFO, capped at 500 by default) andtuner.favourites(uncapped) inchrome.storage.local. Cap configurable 50–500 on the Options page. - Entry shape (mirror WP plugin exactly):
{ artist, title, station, stationId, at }whereat = Date.now(). - Dedup against the LAST entry only so consecutive identical now-playing reads don't pile up.
- Skip
(unknown)artist — SomaFM's dead-air/promo placeholder doesn't pollute the log.
Metadata polling
src/offscreen/offscreen.js— the offscreen document (where audio lives) now polls SomaFM's per-channel song endpoint every 25 s while playing. OnPLAY, polling starts (immediate first fetch + interval). OnPAUSEorERROR, polling stops. Within-session dedup prevents redundant logs when the same song stays current across polls.- Why the offscreen doc, not the SW: the SW gets killed when idle; the offscreen doc is alive while audio plays. This is the right home for the loop.
- New message types in
src/lib/messages.js:TRACK_LOGGED,FAVOURITE_TOGGLED,STORAGE_WIPED.
4-button search (the headline feature)
-
Every track row in History and Favourites gets four search-link buttons that open the major services' public search pages in a new tab:
Service URL pattern Spotify https://open.spotify.com/search/{enc}YouTube https://www.youtube.com/results?search_query={enc}Apple Music https://music.apple.com/search?term={enc}Bandcamp https://bandcamp.com/search?q={enc}Where
enc = encodeURIComponent(artist + " " + title). Zero auth, zero API keys, zero quota, zero ToS gray area. RangerHQ Tuner doesn't play these services — it opens the search results page and the user picks. Same approach as the rangerhq-radio WP plugin. -
Each link is brand-coloured (muted on the dark palette) — Spotify green, YouTube red, Apple pink, Bandcamp cyan.
target="_blank"+rel="noopener noreferrer".
Tabbed New Tab Page
- The New Tab Page's bottom section is now a 3-tab control: Stations · History · Favourites, plus a ⚙ icon that opens the Options page.
- History tab: newest first, with track title, artist (accent green),
station · 2m agometa line, star toggle, and the 4 search links. - Favourites tab: same shape, all your starred tracks.
- Each tab has its own "Clear …" button in the toolbar (confirm dialog).
Options page (NEW)
- Registered via
"options_page": "src/options/options.html"in the manifest. Opens viachrome://extensions→ details → Extension options OR via the ⚙ on the New Tab Page. - Local data card: live stats (History size, Favourites size, Stations cached, Total
tuner.*bytes), history-cap slider (50–500 step 50), and three buttons:Clear history— dropstuner.historyonly.Clear favourites— dropstuner.favouritesonly.Clear EVERYTHING(danger styling) — wipes everytuner.*key inchrome.storage.local.
- Playback card: current volume %, last-played station name (read-only).
- About card: version, Gitea repo link, davidtkeane.com link, SomaFM link, licence reminder.
- A small toast message confirms each destructive action.
- The page listens for
chrome.storage.onChangedso stats stay live if you wipe from somewhere else.
Manifest
- Version:
0.2.0→0.3.0. - Description updated to mention the 4-button search affordance (helps Web Store reviewers see the single-purpose statement matches the listing).
- Added
"options_page": "src/options/options.html". - No new permissions, no new host permissions. Still
["offscreen", "storage"]+somafm.comonly. Search-link clicks open in a new browser tab — that's the user's navigation, not an API call from the extension.
Cross-surface state sync
The popup and the New Tab Page now stay in lockstep without manual reloads. Both surfaces subscribe to chrome.storage.onChanged and re-render when:
tuner.currentStationIdchanges (someone in the other surface picked a station)tuner.isPlayingchanges (play/pause from anywhere)tuner.historyortuner.favouritesmutates (re-render the relevant tab)
Single source of truth = chrome.storage.local. No more drift between popup and New Tab.
Storage gateway architecture (MV3 safety net)
Offscreen documents don't reliably have access to chrome.storage across Chrome versions, so all storage writes from the offscreen audio host are routed through the service worker via a new LOG_TRACK_REQUEST message type. The SW (which always has chrome.storage) does the actual tuner.history write and broadcasts TRACK_LOGGED back to the UI surfaces.
As belt-and-braces, every function in src/lib/history.js now checks for chrome.storage availability before using it and returns sensible defaults if absent — so the module is safe to import from any extension context.
Metadata latency fix (parallelised audio + polling)
When PLAY arrives at the offscreen doc, metadata polling now starts immediately in parallel with the audio HTTP buffer fill, rather than waiting for audio.play() to resolve. First-track display time drops from ~10-15 seconds (audio buffer + then poll) to ~1-2 seconds (poll racing in parallel with buffer fill).
Popup quick-link nav
The toolbar popup now has a three-button nav row above the footer:
- ⎘ Open in tab —
chrome.tabs.create({url: chrome.runtime.getURL('src/newtab/newtab.html')})opens the full Tuner UI as a regular pinnable tab. - ♪ History — same as Open in tab but with
#historyhash so the History pane is pre-selected.newtab.jsreadslocation.hashon init and respects it. - ⚙ Settings —
chrome.runtime.openOptionsPage()opens the Options page directly.
No new permissions: chrome.tabs.create() works on our own extension URLs without the tabs permission. The Web Store permission profile stays ["offscreen", "storage"] + somafm.com host only.
Why this is the Web Store version
- Clear single-purpose statement now obvious: "Plays SomaFM internet radio, logs heard tracks, and provides search-link shortcuts to Spotify/YouTube/Apple Music/Bandcamp."
- Privacy story stays clean: "collects nothing, stores everything locally, no remote storage" — every Options-page checkbox stays "does not collect" on the Dashboard.
- The four service buttons are pure
<a href>link-outs — Web Store reviewers can verify they don't embed third-party SDKs. - All inline styles are gone (lessons from
rangerhq-buddy v0.1.5). - Accessibility: every button has an
aria-label, every interactive element is reachable by keyboard, focus rings visible, contrast ≥ 4.5:1. - Storage operations are SW-gated, so even Chrome versions with the offscreen-doc storage quirk run cleanly.
[0.2.0] — 2026-06-08
Added — New Tab Page override (Tier 2.5)
Replace Chrome's default New Tab Page with a RangerHQ-branded landing that surfaces the player, current track, a quick-station chip row, and a searchable browse list. Same chrome.offscreen audio pipeline as the popup — the New Tab Page is just a second view onto the same playback state. Live ticking clock (HH:MM in bold + small dim seconds) updates every second; date renders in the user's locale.
Note: v0.2.0 was a working local build but was never tagged on Gitea — its features ship to the world together with v0.3.0 in a single commit.
manifest.json: addedDescription string updated to mention the optional New Tab Page replacement (Web Store reviewers prefer overrides to be disclosed in the description)."chrome_url_overrides": { "newtab": "src/newtab/newtab.html" }src/newtab/newtab.html— full-viewport landing with header (helmet + clock), large now-playing block, generous Play/Volume controls, quick-station chips, searchable browse list, footer linking todavidtkeane.com+ Gitea.src/newtab/newtab.css— same earthy palette as the popup but laid out for a full screen. Subtle helmet watermark (2.5% opacity) in the background. Responsive shrink-to-fit atmax-height: 700px.src/newtab/newtab.js— reusessrc/lib/messages.js,src/sources/index.js, and the samechrome.storage.localkeys as the popup. State stays consistent: pick a station in the popup, refresh new tab → same station selected, same play state. Adds a clock that updates every 30 s.
Quick-pick stations: the chip row surfaces the 8 most-loved SomaFM channels (Groove Salad, Drone Zone, Indie Pop Rocks!, Secret Agent, Space Station Soma, Lush, Deep Space One, Fluid). One click → playing.
Why this feature exists: validates user_browser_resident_tools — David has the browser open all day, every new tab is a touchpoint. Every Cmd+T now opens a RangerHQ-branded landing that's one click from playing radio. The Web Store reviewer is also more likely to grant a clear single-purpose override than a vague one — having a useful New Tab Page is good signal alongside the toolbar player.
Permissions unchanged: no new permissions or hosts added. The override is a pure UI choice; no expanded API surface.
[0.1.0] — 2026-06-08
Added — Tier 1 MVP (Buddy is alive — er, Tuner is alive)
Buddy's browser cousin lives. First release of RangerHQ Tuner — a Chrome Manifest V3 extension that plays SomaFM internet radio from your toolbar. Sibling to rangerhq-radio (the WordPress version live on wp.org since 2026-06-04). Same brand idea, different surface: WP version lives in admin pages, Chrome version lives one toolbar-click away no matter what you're doing.
Architecture
- Manifest V3 — uses the
chrome.offscreenAPI to host the<audio>element in a hidden document. Service workers can't host audio in MV3 (they get killed when idle), so a one-off offscreen document is the only supported pattern. Working code insrc/offscreen/offscreen.js+src/background/service-worker.js. - Source-adapter pattern at
src/sources/— every radio network is a single file conforming to theRadioSourceinterface inbase-source.js. Adding a new network = drop a new file + register one line insrc/sources/index.js. Popup, SW, and offscreen never know which network is which. - Vanilla JS, no build step — pure ES modules loaded directly. No webpack, no Vite, no
npm install. Same ethos as the RangerHQ WP family (hand-rolled PHP). - No telemetry, no third-party JS — everything is local. Only outbound network calls are to SomaFM's public endpoints (catalogue + stream metadata).
Features
- Toolbar popup showing a searchable SomaFM station list (~150 channels) with artwork, name, and genre.
- Play / Pause / Volume controls. Volume persists across browser restarts.
- Last station picked is remembered — Tuner remembers where you left off.
- Now-playing metadata displayed from SomaFM's per-channel song history endpoint.
- Catalogue cached in
chrome.storage.localfor 6 hours to make popup re-opens instant. - Audio continues after popup closes — the offscreen document owns the playback, popup is just a view.
- RangerHQ helmet icon in the Chrome toolbar (resized from
src/assets/img/ranger.pngto 16/32/48/128 PNGs with a dark#1a221cpadded background that matches the popup palette).
Files (22 created)
rangerhq-tuner/
├── manifest.json
├── README.md
├── CHANGELOG.md
├── .gitignore
└── src/
├── assets/
│ ├── icons/icon-{16,32,48,128}.png (toolbar icons)
│ └── img/ranger.png (master helmet logo, 257×275)
├── background/service-worker.js (message router only)
├── offscreen/offscreen.{html,js} (audio host — the MV3 gotcha solved)
├── popup/popup.{html,css,js} (the toolbar UI)
├── sources/
│ ├── base-source.js (RadioSource interface contract)
│ ├── somafm.js (first concrete adapter)
│ └── index.js (registry)
└── lib/
├── messages.js (type + target constants)
└── playlist-parser.js (.pls parser)
Manifest permissions (narrow on purpose)
"permissions": ["offscreen", "storage"],
"host_permissions": ["https://somafm.com/*", "https://*.somafm.com/*"]
No tabs, no <all_urls>, no webRequest. Smallest permission ask possible = easiest Web Store review.
Architecture / status
- Tier 1 MVP — shipped this commit.
- Tier 2 (planned) — full UX polish, favourites via
chrome.storage.sync, now-playing polling loop,.m3uparser,.crxpackaging, second source adapter stub. - Tier 3 (planned) — Chrome Web Store submission. See
~/.ranger-memory/docs/for the per-family submission checklist; Chrome Web Store specifics tracked in the user's memory underreference_chrome_web_store_rules.
Why this exists
David has Chrome open essentially all day. The WP version of RangerHQ Radio requires going to admin pages to use; the toolbar version is one click away regardless of context. Same code-level effort, dramatically higher daily-use payoff. The "best thing we done so far" verdict on first sound (2026-06-08 evening) confirmed the bet.