David flagged 2026-06-09 night: 'when i click the settings button i
go to settings but we have no back link to radio'. Real UX gap —
Options was a one-way trip. The popup auto-closes on launch and the
NewTab is in a different tab, so there was no obvious way back.
Fix: the existing brand header (helmet + 'RangerHQ Tuner — Options'
title) is now a single anchor pointing at newtab.html. Adds a small ←
glyph to the left of the helmet that hover-shifts left + colour-shifts
to accent green for a clear back-affordance.
Same-tab navigation (just an href, no chrome.tabs.create) so the user
doesn't accumulate Tuner tabs. They came IN via Options, they go OUT
into the player UI in the same tab.
Aria: the ← is hidden from screen readers (decorative); the title
attribute on the anchor gives the accessible label.
2 files, +24 lines, 4 lines modified in existing .opt-brand block.
Branch: v0.4.1-prep — not for immediate ship since v0.4.0 is still
in Web Store re-review. Bundle this with light-mode + other polish
into v0.5.0 OR ship as quick v0.4.1 patch after v0.4.0 clears.
manifest.json version 0.3.0 → 0.4.0. CHANGELOG.md entry for v0.4.0
documents the first-run UX hint feature (merged in b82f14e via
v0.4.0-prep branch). Zero new permissions, zero new dependencies,
zero behaviour change for existing users.
Shipped the same day v0.3.0 went LIVE on Chrome Web Store
(~15.5h after submission). Same-account update review window is
~24-48h per the reference_chrome_web_store_rules memory.
The 'pick a station to begin' state was too subtle on first install
(David's own 30-second panic moment when he uninstalled the dev build
and reinstalled from the Web Store, 2026-06-09 evening).
Two layered cues, both pure CSS driven by a body.is-first-run class:
1. Subtle accent-green glow pulses around:
- popup: the station list section
- newtab: the Quick Stations chip row
(rgba alpha 0.18-0.25, 2.4s ease-in-out infinite — visible but not noisy)
2. Bouncing ↓ arrow appended to the 'Pick a station to begin' text in
both surfaces (after-pseudo with translateY animation).
The is-first-run class is toggled in popup.js + newtab.js via a new
reflectFirstRunHint() function called from:
- init() once stations + currentStation are resolved
- onPickStation() the moment a user picks
- the chrome.storage.onChanged listener when another surface picks
(so the hint disappears on both surfaces simultaneously)
Existing users with stored currentStationId never see either cue —
the class only attaches when currentStation is null.
Working on branch v0.4.0-prep so the live main (= what shipped to
Web Store v0.3.0) is unchanged. Merge to main when ready to bump the
manifest version + readme.txt + CHANGELOG.md for v0.4.0 release.
Approved + published 2026-06-09 ~17:08 Dublin, ~15.5 hours after
submission. Extension ID bmdmepddehnpbdffkblbjofgkllmpkgp. Install URL:
https://chromewebstore.google.com/detail/bmdmepddehnpbdffkblbjofgkllmpkgp
- Replaced static 'in review' badge with three LIVE auto-updating
Chrome Web Store badges (version, users count, rating)
- Added install-link callout above the description
Internal submission notes — paste copy for the Chrome Web Store
Dashboard plus reviewer-targeted permission justifications. No
reason for random repo visitors to read it.
Local file stays on disk for quick-reference during future
updates; .gitignore prevents accidental re-add.
David's first-pass through the Dashboard revealed that the
Single Purpose field, and each permission justification field,
have a 1000-char cap (not the much smaller cap I had assumed
from looking at older Web Store docs). Rewrote each to use
roughly 900 chars of the budget — gives reviewers the technical
detail they need to verify the code matches the claims.
- Single Purpose (970 chars) — 3 numbered things the extension does,
what it explicitly does not do (no SDK, no analytics, no remote).
- offscreen (976 chars) — why MV3 needs offscreen for audio, with
the developer.chrome.com pointer for the reviewer to verify.
- storage (867 chars) — per-key inventory + Options-page wipe path
+ privacy policy URL for cross-reference.
- somafm.com host (910 chars) — exact endpoints listed, what we
do NOT send with the requests, SomaFM's community-friendly stance.
- Added: 'I am not using remote code' note for the cert checkbox.
Dedicated Web Store listing icon, separate from the manifest icons
in the .zip. Same source helmet (src/assets/img/ranger.png) but
resized tighter to 116x116 with 6px breathing room on a #1a221c
square — reads better at listing thumbnail size than the toolbar
icon's heavier padding.
The Chrome Web Store enforces a 132-character limit on the manifest
description field. The v0.3.0 description was 195 chars and got
rejected on upload. Trimmed to the same wording as the listing short
description in WEB_STORE_SUBMISSION.md §2.
The longer 1500-char marketing description still lives in §3 of the
submission notes and goes in the Dashboard's 'Detailed description'
field at listing time.
Both generated from the RangerHQ helmet logo + Arial typography on the
extension's own #0f1411 dark palette so the Dashboard preview blends
naturally with the screenshots.
- promo-tile-440x280.png: helmet anchor left, "RangerHQ Tuner" cream
bold + "Indie radio in your toolbar" accent green + "SomaFM · No
telemetry · GPL v2+" muted footer. Web Store requires this size for
search/browse listing tile.
- marquee-1400x560.png: same idea scaled up. Optional asset; Google
promotes extensions with marquees more often.
ImageMagick recipe documented in store/screenshots/README.md.
Web Store-spec screenshots for Dashboard upload, generated from
David's screen grabs taken on the live v0.3.0 build.
Screenshots (1280×800 PNG, the Chrome Web Store required size):
1. Stations tab — DEF CON Radio + quick chips + browse list
2. History tab — the headline 4-button-search feature with 4
tracks already logged (Spotify / YouTube / Apple / Bandcamp)
3. Favourites tab — starred-track persistence demo
4. Toolbar popup — composed on a dark canvas, helmet icon
visible in the Chrome address bar above
ImageMagick recipe:
- NTP shots: crop bottom Chrome chrome (1378x950+0+0), resize
to width 1280, north-anchored extent to 1280x800
- Popup: resize to height 740, center on 1280x800 canvas
with palette-matching #0f1411 background
Source originals archived under store/source/ for regeneration.
No code changes — extension behaviour unchanged.
Prep for Chrome Web Store first submission. No code changes — v0.3.0
behaviour unchanged.
- LICENSE — full GPL v2 text, matches RangerHQ WP family.
- PRIVACY.md — canonical privacy policy. "Collects nothing, stores
everything locally" using Google's data-category vocabulary so the
Dashboard Privacy Practices tab can be ticked uniformly "does not
collect" across all 9 categories.
- WEB_STORE_SUBMISSION.md — paste-ready Dashboard copy: single-purpose
statement, 132-char short + 1500-char detailed description, permission
justifications for offscreen + storage + somafm.com host, build ZIP
command, screenshot brief, post-approval update workflow.
The public version of the privacy policy will live at
davidtkeane.com/rangerhq-tuner/privacy (HTML mirror of PRIVACY.md).
Web Store submission target. Mirrors rangerhq-radio's track-history pattern
(inc/history.php) so the family stays coherent across surfaces.
Highlights
- New Tab Page override (Tier 2.5) — Chrome's default new tab replaced with
a RangerHQ-branded landing showing the player, current track, quick chips,
searchable browse list, and now history + favourites tabs.
- Track history + favourites — capped FIFO 500, dedup against last entry,
skip "(unknown)" artist (SomaFM dead-air). Stored in chrome.storage.local
under tuner.history + tuner.favourites.
- 4-button search per entry — Spotify / YouTube / Apple Music / Bandcamp.
Pure public-search-URL link-outs in a new tab, NO auth, NO API keys, NO
quota, NO third-party SDK embedded.
- Options page (chrome://extensions → details → options) — live stats,
history cap slider (50-500), Clear history / Clear favourites / Clear
EVERYTHING buttons, About panel with Gitea + davidtkeane.com links.
- Popup nav row — Open in tab / History (#hash deep link) / Settings,
using chrome.tabs.create + chrome.runtime.openOptionsPage. No new perms.
- Cross-surface sync — popup ↔ newtab listen on chrome.storage.onChanged
for tuner.currentStationId / tuner.isPlaying / history / favourites.
- Storage gateway — offscreen doc can't reliably reach chrome.storage in
some Chrome versions, so it sends LOG_TRACK_REQUEST to the SW which
does the write. history.js also defensively guards every storage call.
- Metadata latency fix — polling now starts immediately on PLAY, in
parallel with audio buffer fill. First track display drops from
~10-15s to ~1-2s.
Permissions unchanged
- Still ["offscreen", "storage"] + somafm.com host only.
- chrome.tabs.create works on our own extension URLs without "tabs" perm.
- No webRequest, no <all_urls>, no third-party SDK.
Bumped from 0.1.0 (last tag on Gitea) directly to 0.3.0.
v0.2.0 (newtab + clock) was a working local build but never tagged;
its features ship together with v0.3.0 in this single commit.
Chrome MV3 extension, browser-resident sibling to rangerhq-radio
(WP plugin). Plays SomaFM via the chrome.offscreen API + a source-
adapter pattern at src/sources/.
Architecture highlights:
- Audio runs in offscreen document — SW would get killed.
- Source-adapter pattern locks Tier 1 contract (RadioSource interface
in src/sources/base-source.js). Adding a network = drop a file +
register one line in src/sources/index.js.
- Vanilla JS, no build step. Pure ES modules.
- No telemetry, no third-party JS. Outbound only to somafm.com.
- Narrow permissions: offscreen + storage + somafm.com host_perms.
No tabs, no <all_urls>, no webRequest.
22 files, ~30 min build following the saved plan at
~/.ranger-memory/projects/rangerhq-tuner-plan.md.
Tier 2 + Tier 3 (Web Store submission) not started.