Files
SoulStream/handoff/rounds/patch-round25.cjs
T
KeelrLeek 9f0601027c android: the handoff — the pending rounds, the pipeline, and what is unverified
Everything written this session that has not been through the device: rounds 43
to 48 (each patch adversarially verified, 43's transient-notice expectation
fixed), a partial round 49, the pipeline scripts, the Android Auto design and
the icon comparison mock. Also the two harness changes that were never A/B'd —
adaptive waits, which may only shorten a declared wait and never lengthen it,
and a ship that runs the mock walk and the deploy at the same time. The A/B and
its 255-step baseline are in handoff/tools; main stays at what the phone has
actually walked.
2026-09-14 07:57:41 -05:00

322 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Round twenty-five: the catalog's numbers — Popular asked on its own as
// well as with the page, a listener count for the rows a record's own
// artist anchor could not reach, and an artist's hero photo chosen on this
// phone (set_artist_hero is pick_artist_hero's install step split out and
// shared, the way round fifteen did it for a playlist's cover).
const fs = require('fs');
const root = process.env.SS_ROOT || 'F:/Coding/SoulStream/';
const DIR = 'C:/Users/Kyle/AppData/Local/Temp/claude/F--Coding-SoulStream/5488514a-11e8-482a-b0a0-20cb511ee110/scratchpad/';
let p, s;
const load = (file) => { p = file; s = fs.readFileSync(root + p, 'utf8'); };
const save = () => fs.writeFileSync(root + p, s);
const once = (a) => {
const i = s.indexOf(a);
if (i < 0) throw new Error('missing in ' + p + ': ' + a.slice(0, 90));
if (s.indexOf(a, i + 1) >= 0) throw new Error('ambiguous in ' + p + ': ' + a.slice(0, 90));
return i;
};
const rep = (a, b) => { once(a); s = s.replace(a, () => b); };
/* the long insertions live beside this script; the file's own last newline
is not part of the text */
const ins = (name) => fs.readFileSync(DIR + 'ins-r25-' + name + '.txt', 'utf8').replace(/\r?\n$/, '');
/* commands.rs is CRLF (and carries a BOM) — anchor and insert in ITS line
endings, or the file comes back mixed */
const CRLF = String.fromCharCode(13) + String.fromCharCode(10);
const crlf = (t) => t.replace(/\r?\n/g, CRLF);
const repRs = (a, b) => rep(crlf(a), crlf(b));
// ── the backend: the desktop's install step, split out and shared ──
load('src-tauri/src/commands.rs');
repRs(` let Some(source) = rx.await.unwrap_or(None) else {
return Ok(None);
};
let source = std::path::PathBuf::from(source);`,
` let Some(source) = rx.await.unwrap_or(None) else {
return Ok(None);
};
install_artist_hero(&app, artist_id, source).map(Some)
}
/// A picked image — the desktop's dialog, or a photo the phone's storage
/// plugin already copied into the app's cache — validated and written as
/// this artist's chosen hero.
fn install_artist_hero(
app: &tauri::AppHandle,
artist_id: u64,
source: String,
) -> Result<ArtistHeroOverride, String> {
let source = std::path::PathBuf::from(source);`);
repRs(` let (ext, _) = artist_image_format(&source)?;
let dir = artist_hero_dir(&app)?;`,
` let (ext, _) = artist_image_format(&source)?;
let dir = artist_hero_dir(app)?;`);
repRs(` let mut hero = load_artist_hero_override(&app, artist_id)?
.ok_or_else(|| "The imported hero could not be reopened".to_string())?;
hero.width = width;
hero.height = height;
Ok(Some(hero))
}`,
` let mut hero = load_artist_hero_override(app, artist_id)?
.ok_or_else(|| "The imported hero could not be reopened".to_string())?;
hero.width = width;
hero.height = height;
Ok(hero)
}
/// The phone's door onto the same import: a photo already on disk (the
/// storage plugin's picker copied it into the app's cache) becomes this
/// artist's chosen hero. The dialog above stays the desktop's.
#[tauri::command]
pub async fn set_artist_hero(
app: tauri::AppHandle,
artist_id: u64,
path: String,
) -> Result<ArtistHeroOverride, String> {
install_artist_hero(&app, artist_id, path)
}`);
save();
load('src-tauri/src/lib.rs');
rep(` commands::pick_artist_hero,`, ` commands::pick_artist_hero,
commands::set_artist_hero,`);
save();
// ── the wrapper ──
load('src/lib/api.js');
rep(`export const clearArtistHero = (artistId) => invoke("clear_artist_hero", { artistId });`,
`export const clearArtistHero = (artistId) => invoke("clear_artist_hero", { artistId });
// The phone's hero door: a photo the storage plugin already copied into the
// app's cache, installed the way the desktop's dialog pick installs one.
export const setArtistHero = (artistId, path) => invoke("set_artist_hero", { artistId, path });`);
save();
// ── a data: URL is already the image ──
// A custom hero comes back inside catalog_artist as a data: URL, and the
// art gate sent everything that is not https to the thumbnail cache, which
// cannot open one: the banner, the ground and the accent all went blank.
load('src/mobile/art.svelte.js');
rep(` if (art[path] !== undefined) return Promise.resolve(art[path]);`,
` if (art[path] !== undefined) return Promise.resolve(art[path]);
/* a data: URL is already the image — an artist's chosen hero comes back
from Rust as one, and the thumbnail fetch would throw it away */
if (path.startsWith("data:")) { art[path] = path; return Promise.resolve(path); }`);
save();
// ── the store ──
load('src/mobile/catalog.svelte.js');
rep(`import * as api from "../lib/api.js";`, `import * as api from "../lib/api.js";
import { note } from "./notice.svelte.js";`);
rep(` /** "artist\\u0000title" → { status, info: { description, genre, label, source } } */
albumInfo: {},
});`,
` /** "artist\\u0000title" → { status, info: { description, genre, label, source } } */
albumInfo: {},
/** artist id → { status, tracks } — Popular on its own, ahead of the page */
popular: {},
/** catalog album id → { status, counts: { trackId: listeners } } — the
* deferred Last.fm pass for the rows the record's page had no count for */
listeners: {},
});`);
rep(`/** A portrait for a byline — the same identity rule, search only. */`, ins('popular'));
rep(`export function lookupAlbum(id) {`, ins('store'));
save();
// ── the artist page: Popular's fast lane ──
load('src/mobile/screens/Artist.svelte');
rep(` import { catalog, lookupArtist, artistKey, fans, yearOf, looseTitle, loadExtras, picture } from "../catalog.svelte.js";`,
` import { catalog, lookupArtist, artistKey, fans, yearOf, looseTitle, loadExtras, loadPopular, picture } from "../catalog.svelte.js";`);
rep(` $effect(() => {
const id = page?.artist?.id;
const nm = page?.artist?.name || name;
if (id != null) untrack(() => loadExtras(id, nm));
});`,
` $effect(() => {
const id = page?.artist?.id;
const nm = page?.artist?.name || name;
if (id != null) untrack(() => loadExtras(id, nm));
});
/* Popular's own lane, asked the moment an id is known — from the
search's own row, or from the identity search — so the section can
paint before the full page is built */
$effect(() => { const id = artistId; if (id != null) untrack(() => loadPopular(id)); });`);
rep(` const popularAll = $derived(page?.popular_tracks ?? []);`,
` /* the page's own list once it lands (the same Last.fm order, from the
same inputs); the fast lane's until then, and for an artist whose
page came back with no Popular at all */
const fastPopular = $derived(artistId != null ? catalog.popular[artistId]?.tracks ?? [] : []);
const popularAll = $derived(page?.popular_tracks?.length ? page.popular_tracks : fastPopular);`);
save();
// ── the catalog record: a count for the rows the anchor could not reach ──
load('src/mobile/screens/CatalogAlbum.svelte');
rep(` import { catalog, lookupAlbum, lookupArtist, artistKey, seedFor, picture, yearOf, looseTitle, loadAlbumInfo, infoKey } from "../catalog.svelte.js";`,
` import { catalog, lookupAlbum, lookupArtist, artistKey, seedFor, picture, yearOf, looseTitle, loadAlbumInfo, infoKey, loadTrackListeners } from "../catalog.svelte.js";`);
rep(` const peak = $derived(rows.reduce((m, t) => Math.max(m, t.listeners || 0), 0));`,
` /* the rows the record's own artist anchor found no count for — a
feature's guest, a compilation's stranger — get the deferred pass by
their OWN artist id; a record's stats are anchored on its artist
alone and cannot reach them */
$effect(() => { const rs = rows, k = id; if (rs.length) untrack(() => loadTrackListeners(k, rs)); });
const extraCounts = $derived(catalog.listeners[id]?.counts ?? {});
const listenersOf = (t) => t.listeners ?? extraCounts[t.id] ?? null;
const peak = $derived(rows.reduce((m, t) => Math.max(m, listenersOf(t) || 0), 0));`);
rep(` {#if showCount}<span class="lbar"><i style="transform: scaleX({(peak ? Math.max(0.04, (t.listeners || 0) / peak) : 0).toFixed(3)})"></i><em>{fmtCount(t.listeners)}</em></span>{/if}`,
` {#if showCount}<span class="lbar"><i style="transform: scaleX({(peak ? Math.max(0.04, (listenersOf(t) || 0) / peak) : 0).toFixed(3)})"></i><em>{fmtCount(listenersOf(t))}</em></span>{/if}`);
save();
// ── the artist page's own overflow sheet (the bar's ⋯ fell through to home) ──
load('src/mobile/HalfSheet.svelte');
rep(` import { catalog, yearOf } from "./catalog.svelte.js";`,
` import { catalog, yearOf, artistKey, picture, heroFromPhoto, heroClear } from "./catalog.svelte.js";`);
rep(` if (t.n === "catalogAlbum") return "catalogAlbum-more";`,
` if (t.n === "catalogAlbum") return "catalogAlbum-more";
if (t.n === "artist") return "artist-more";`);
rep(` {:else if kind === "home-more"}`,
` {:else if kind === "artist-more"}
{@const nm = top().c.name}
{@const ent = catalog.artists[artistKey(nm)] ?? null}
{@const pg = ent?.page ?? null}
{@const aid = pg?.artist?.id ?? ent?.id ?? null}
<div class="sheet-head">
<span class="art">{#if picture(pg?.artist?.picture)}<img src={picture(pg.artist.picture)} alt="" />{:else}<Icon name="users" size={22} />{/if}</span>
<span class="b"><span class="t">{nm}</span><span class="m">{pg?.albums?.length ? pg.albums.length + " releases on the catalog" : "Artist"}</span></span>
</div>
<div class="sheet-rows">
{#if aid != null}
{@render row("image", "Choose hero photo…", "a wide picture from this phone, 1920×800 or larger", act(() => heroFromPhoto(aid, nm).catch(oops)))}
{#if pg?.banner_custom}
{@render row("x", "Remove custom hero", "the catalog's own photos come back", act(() => heroClear(aid, nm).catch(oops)))}
{/if}
<div class="sheet-hair"></div>
{/if}
{@render row("settings", "Artwork settings", "covers and portraits, in You", act(() => go("you")))}
</div>
{:else if kind === "home-more"}`);
save();
// ── You → Album artwork: the desktop's portrait refresh ──
load('src/mobile/screens/You.svelte');
rep(` let refetching = $state(false);`, ` let refetching = $state(false);
let refreshingArtists = $state(false);`);
rep(` const ROWS = [
["ambience", "image", "Ambience", "the artwork in play, blurred, under every screen"],`, ins('you'));
rep(` <span class="b"><span class="t">{refetching ? "Upgrading covers…" : "Re-fetch all album covers"}</span><span class="m">every record to the canonical high-resolution cover — about one a second</span></span>
</button>
</div>`,
` <span class="b"><span class="t">{refetching ? "Upgrading covers…" : "Re-fetch all album covers"}</span><span class="m">every record to the canonical high-resolution cover — about one a second</span></span>
</button>
<button class="row" onclick={refreshPortraits} disabled={refreshingArtists}>
<span class="lead"><Icon name="users" size={20} /></span>
<span class="b"><span class="t">{refreshingArtists ? "Refreshing portraits…" : "Refresh artist portraits"}</span><span class="m">a photo for every artist already in your library</span></span>
</button>
</div>`);
save();
// ── the mocks ──
load('src/lib/mockLibrary.js');
rep(`let follows = [];`, `/* the heroes chosen this session — pick_image → set_artist_hero */
const heroes = {};
let follows = [];`);
rep(` artist: a, banner: null, banners: [], banner_custom: false, logos: [],`,
` artist: a, banner: heroes[a.id] ?? null, banners: heroes[a.id] ? [heroes[a.id]] : [], banner_custom: !!heroes[a.id], logos: [],`);
rep(` if (cmd === "catalog_related") return Promise.resolve(RELATED);`,
` if (cmd === "catalog_related") return Promise.resolve(RELATED);
/* Popular's own lane: ranked, with counts — and for the artists whose
page gives none (the related ones), the only Popular there is */
if (cmd === "catalog_artist_popular") {
const id = Number(args?.artistId);
const a = id === CAT_ARTIST.id ? CAT_ARTIST : RELATED.find((r) => r.id === id);
if (!a) return Promise.resolve([]);
const rows = id === CAT_ARTIST.id
? CAT_TRACKS.slice(0, 7)
: TITLES.slice(0, 4).map((t, i) => ({
id: 4200000 + id * 10 + i, title: t, artist: a.name, artist_id: id, rank: 700000 - i * 1000,
album: a.name + " Live", album_id: 900 + id, cover: null, duration_secs: 210 + i * 13, preview: null, track_no: i + 1,
}));
return new Promise((r) => setTimeout(() => r(rows.map((t, i) => ({ ...t, listeners: 1_900_000 - i * 240_000 }))), 260));
}
/* the deferred pass: a count per row, by that row's own artist id */
if (cmd === "catalog_track_listeners") {
const rows = args?.tracks || [];
return new Promise((r) => setTimeout(() => r(rows.map((t, i) => (t.artistId ? 640_000 - i * 20_000 : null))), 260));
}
if (cmd === "set_artist_hero") { const id = Number(args?.artistId); heroes[id] = svgArt(180, "H"); return Promise.resolve({ data_url: heroes[id], width: 3840, height: 2160 }); }
if (cmd === "clear_artist_hero") { const id = Number(args?.artistId); const had = id in heroes; delete heroes[id]; return Promise.resolve(had); }`);
rep(` return Promise.resolve({ album: a, tracks: a.id === 302127 ? CAT_TRACKS.map((t, i) => ({ ...t, listeners: 2_400_000 - i * 150_000 })) : [], album_plays: a.id === 302127 ? 310_000_000 : null, album_listeners: a.id === 302127 ? 4_100_000 : null, totals_summed: false });`,
` /* the last two rows are a guest's: the album's own artist anchor
has no count for them, which is what the deferred pass is for */
return Promise.resolve({ album: a, tracks: a.id === 302127 ? CAT_TRACKS.map((t, i) => (i >= CAT_TRACKS.length - 2 ? { ...t, artist: "Daft Punk feat. Todd Edwards", artist_id: 411 } : { ...t, listeners: 2_400_000 - i * 150_000 })) : [], album_plays: a.id === 302127 ? 310_000_000 : null, album_listeners: a.id === 302127 ? 4_100_000 : null, totals_summed: false });`);
rep(` if (cmd === "refresh_all_album_art") return new Promise((r) => setTimeout(() => r(140), 300));`,
` if (cmd === "refresh_all_album_art") return new Promise((r) => setTimeout(() => r(140), 300));
if (cmd === "refresh_artist_art") return new Promise((r) => setTimeout(() => r(26), 300));`);
/* the thumbnail cache READS A FILE (std::fs::read on the path it is given),
so a data: URL comes back as an error there and the gate remembers "".
The mock answered every path with a picture, which quietly made the art
gate's own data: answer untestable — the hero looked fine either way. */
rep(` if (cmd === "album_art") {
const m = /(\\d+)$/.exec(args.path || "");`,
` if (cmd === "album_art") {
/* not a path: the real one fails to open it, which is why the art
gate has to answer a data: URL itself */
if (String(args.path || "").startsWith("data:")) return Promise.reject(new Error("album_art: not a path"));
const m = /(\\d+)$/.exec(args.path || "");`);
save();
// ── the walks ──
load('tests/mobile/smoke.mjs');
/* every row renders an .lbar whether or not it has a number, so counting
the bars proves nothing — count the <em>s that say something */
rep(` cardPhs: document.querySelectorAll('.rail .acard svg').length,`,
` cardPhs: document.querySelectorAll('.rail .acard svg').length,
popCounts: [].filter.call(document.querySelectorAll('.row.pop .lbar em'), function (e) { return e.textContent.trim(); }).length,
barCounts: [].filter.call(document.querySelectorAll('.trk .lbar em'), function (e) { return e.textContent.trim(); }).length,
heroPix: !!document.querySelector('.hero .hero-img.dissolve'),`);
rep(` { do: \`__click('.disco-tabs button', 'Singles')\`, name: "cat-artist-tab", wait: 700 },`, ins('steps-hero'));
rep(` { do: \`__click('.rail .release', 'Discovery')\`, name: "cat-album", wait: 1500 },`,
` { do: \`__click('.rail .release', 'Discovery')\`, name: "cat-album", wait: 1500 },
/* the two guest rows the record's own anchor had no count for */
{ name: "cat-album-listeners", wait: 900 },`);
rep(` { do: \`__click('.prefs[data-mode="albumart"] .chip', '1500 px')\`, wait: 400 },`, ins('steps-you'));
rep(` "cat-artist-related": (i) => i.title.startsWith("Justice") && i.screen.includes("has-hero"),`,
` "cat-artist-related": (i) => i.title.startsWith("Justice") && i.screen.includes("has-hero") && i.heroPix && i.pop >= 4 && i.popCounts >= 4,
"artist-more": (i) => i.half.includes("up") && /Choose hero photo/.test(i.halfText) && !/Remove custom hero/.test(i.halfText),
"artist-hero-set": (i) => !i.half.includes("up") && /3840×2160/.test(i.notice),
"artist-more-custom": (i) => i.half.includes("up") && /Remove custom hero/.test(i.halfText) && i.heroPix,
"artist-hero-cleared": (i) => !i.half.includes("up") && /photos are back/.test(i.notice),`);
rep(` "cat-album": (i) => i.screen.includes("tr-push") && i.title.startsWith("Discovery") && i.eds >= 2 && i.lede && i.bars >= 10,`,
` "cat-album": (i) => i.screen.includes("tr-push") && i.title.startsWith("Discovery") && i.eds >= 2 && i.lede && i.bars >= 10,
"cat-album-listeners": (i) => i.bars === 14 && i.barCounts === 14,`);
rep(` "you-artedge": (i) => i.artEdge === "1000 px",`,
` "you-artedge": (i) => i.artEdge === "1000 px",
"you-portraits": (i) => /portraits refreshed/.test(i.notice),`);
rep(` { name: "artist", wait: 4500 },`, ins('steps-phone'));
rep(` artist: (i) => i.screen.includes("has-hero") && i.amb.includes("on:img") && i.tabs >= 1,`,
` artist: (i) => i.screen.includes("has-hero") && i.amb.includes("on:img") && i.tabs >= 1,
"artist-more": (i) => i.half.includes("up") && /Choose hero photo/.test(i.halfText),`);
save();
// ── the doc ──
load('ANDROID.md');
rep(`played track under "On this phone"). Search corrects an obvious`,
`played track under "On this phone"). Popular is asked on its own as well as
with the page (\`catalog_artist_popular\`: the Last.fm ranking and its counts,
waiting on nothing but the linked MBID, so the section can paint before the
full page is built — and an artist whose page came back with no Popular at
all still gets one), and a catalog record's rows that the album's own artist
anchor found no count for — a feature's guest, a compilation's stranger —
get the desktop's deferred pass by their OWN Deezer artist id
(\`catalog_track_listeners\`). An artist's photo is Kyle's to choose: the
artist page's ⋯ offers **Choose hero photo** (the phone's image picker, then
\`set_artist_hero\` — \`pick_artist_hero\`'s install step split out and shared,
so the desktop's dialog keeps its own door; 1920×800 and landscape or it is
refused) and **Remove custom hero**, which puts the catalog's own photos
back; the page is asked again either way, so the banner, the ground and the
accent all follow. The desktop's own chooser has had no home since its pill
left the glass bar — the phone has one first. You → Album artwork carries
**Refresh artist portraits** (\`refresh_artist_art\`), the desktop Settings
row. A \`data:\` URL is the image itself in \`art.svelte.js\` now — a chosen
hero comes back from Rust as one, and the thumbnail fetch used to throw it
away. Search corrects an obvious`);
save();
console.log('round 25 wired');