Files
SoulStream/handoff/rounds/patch-round26.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

137 lines
9.1 KiB
JavaScript

// Round twenty-six: the odds and ends. A cached stream copied out into a
// folder picked on the phone (the desktop has the command and no UI for it),
// the Soulseek listening port in You → Sharing (the desktop never exposed it
// at all), and the desktop's debug overlay as a screen behind a new dev-build
// gate. community_pick_file is deliberately not here: it is the avatar /
// recovery-artifact dialog, and the backend has no chat file-send at all.
const fs = require('fs');
const root = process.env.SS_ROOT || 'F:/Coding/SoulStream/';
const scratch = 'C:/Users/Kyle/AppData/Local/Temp/claude/F--Coding-SoulStream/5488514a-11e8-482a-b0a0-20cb511ee110/scratchpad/';
const ins = (name) => fs.readFileSync(scratch + name, 'utf8').replace(/\s+$/, '');
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); };
const NL = String.fromCharCode(10);
// ── the dev gate: Vite's own DEV flag, plus the mock walk saying so ──
load('src/mobile/util.js');
// an append has no anchor to be ambiguous, so it guards itself: a re-run
// after a throw further down must not export devBuild twice
if (s.includes('export const devBuild')) throw new Error('src/mobile/util.js already exports devBuild — round 26 has already run against this tree');
s = s.replace(/\s*$/, NL) + `
/** The dev flavour, and nothing else. That build is the one that loads from
* the Vite dev server over the cable (ANDROID.md, "Iterating without
* cargo"), so Vite's own DEV flag IS the flavour; the embedded build the
* phone keeps is compiled and reads false. The mock walk builds for
* production too, so it says who it is through the key the harness sets. */
export const devBuild = (() => {
if (import.meta.env.DEV) return true;
try { return localStorage.getItem("mockLibrary") === "1"; } catch { return false; }
})();
`;
save();
// ── the sheet: a cached stream copied into a folder on this phone ──
load('src/mobile/HalfSheet.svelte');
rep(` /* a peer's sheet (a raw result's folder, or the browse screen's user) */`,
ins('ins-r26-halfsheet-copy.txt') + NL + ` /* a peer's sheet (a raw result's folder, or the browse screen's user) */`);
rep(` {#if streamMates(track, album).length > 1}
{@render row("download", "Save this record", streamMates(track, album).length + " tracks streaming from the same record", act(() => saveStreams(streamMates(track, album))))}
{/if}`,
` {#if streamMates(track, album).length > 1}
{@render row("download", "Save this record", streamMates(track, album).length + " tracks streaming from the same record", act(() => saveStreams(streamMates(track, album))))}
{/if}
{@render row("copy", "Copy to a folder…", "the streamed file as it is, into a folder on this phone", act(() => copyToFolder([{ t: track, album }])))}`);
// a prompt that asks for a number gets the numeric keypad; Svelte drops an
// attribute whose value is undefined, so every other prompt is unchanged
rep(` <input class="field" bind:value={promptVal} placeholder={a.placeholder || ""} autocomplete="off" spellcheck="false" enterkeyhint="done" onkeydown={(e) => { if (e.key === "Enter") submitPrompt(); }} use:focusLater />`,
` <input class="field" bind:value={promptVal} placeholder={a.placeholder || ""} autocomplete="off" spellcheck="false" inputmode={a.numeric ? "numeric" : undefined} enterkeyhint="done" onkeydown={(e) => { if (e.key === "Enter") submitPrompt(); }} use:focusLater />`);
save();
// ── You: the listening port, and the dev flavour's door to Diagnostics ──
load('src/mobile/screens/You.svelte');
rep(` import { refreshSourcePrefs } from "../../lib/sources.js";`,
` import { refreshSourcePrefs } from "../../lib/sources.js";
import { devBuild } from "../util.js";`);
rep(` async function refetchCovers() {`, ins('ins-r26-you-port.txt') + NL + ` async function refetchCovers() {`);
rep(`<div class="sheet-sec">Sharing</div>
<div class="group">`,
`<div class="sheet-sec">Sharing</div>
<div class="sec-note">What other people can take from you, and how they reach you. Your library folder is always shared; the listening port is what peers dial when they download from you — the app asks the router to forward it over UPnP and falls back to the server relaying the connection when the router will not. On mobile data the carrier's NAT blocks incoming connections whatever the port is, and a change applies on the next connect.</div>
<div class="group">`);
rep(` {#each cfg?.shared_folders ?? [] as folder (folder)}`,
` <button class="row" onclick={portPrompt}>
<span class="lead"><Icon name="hash" size={20} /></span>
<span class="b"><span class="t">Listening port</span><span class="m">{cfg?.listen_port ?? DEFAULT_LISTEN_PORT}{cfg?.listen_port == null ? " (the default)" : ""} · what peers dial to download from you</span></span>
<span class="r"><Icon name="chevron-right" size={17} /></span>
</button>
{#each cfg?.shared_folders ?? [] as folder (folder)}`);
rep(` <div class="kv"><span class="k">Soulseek</span><span class="v dim">{lib.username || "not signed in"}</span></div>`,
` <div class="kv"><span class="k">Soulseek</span><span class="v dim">{lib.username || "not signed in"}</span></div>
{#if devBuild}
<button class="row" onclick={() => push("diag", {})}>
<span class="lead"><Icon name="devices" size={20} /></span>
<span class="b"><span class="t">Diagnostics</span><span class="m">the desktop's debug overlay — this process, the network, the stream and what is pre-buffering</span></span>
<span class="r"><Icon name="chevron-right" size={17} /></span>
</button>
{/if}`);
save();
// ── the screen itself ──
fs.writeFileSync(root + 'src/mobile/screens/Diagnostics.svelte', ins('ins-r26-diag.txt') + NL);
// ── the route ──
load('src/mobile/Mobile.svelte');
rep(` import Transfers from "./screens/Transfers.svelte";`,
` import Transfers from "./screens/Transfers.svelte";
import Diagnostics from "./screens/Diagnostics.svelte";`);
rep(` case "transfers": return { title: "Transfers", kicker: "Downloads" };`,
` case "transfers": return { title: "Transfers", kicker: "Downloads" };
case "diag": return { title: "Diagnostics", kicker: "Dev build" };`);
rep(` <Transfers />`,
` <Transfers />
{:else if entry.n === "diag"}
<Diagnostics />`);
save();
// ── the mocks ──
load('src/lib/mockLibrary.js');
rep(` if (cmd === "excluded_search_phrases") return Promise.resolve([]);`,
` if (cmd === "excluded_search_phrases") return Promise.resolve([]);` + NL + ins('ins-r26-mock.txt'));
rep(` upload_slots: 2,`, ` upload_slots: 2,
listen_port: null,`);
save();
// ── the walks ──
load('tests/mobile/smoke.mjs');
rep(` cardPhs: document.querySelectorAll('.rail .acard svg').length,`,
` cardPhs: document.querySelectorAll('.rail .acard svg').length,` + NL + ins('ins-r26-probe.txt'));
rep(` { do: \`__click('.halfsheet .row', 'Save to library')\`, name: "np-saved", wait: 900 },`, ins('ins-r26-np-steps.txt'));
rep(` "np-saved": (i) => !i.half.includes("up") && /Saved to the library/.test(i.notice),`,
` "np-copied": (i) => !i.half.includes("up") && /1 file copied/.test(i.notice),
"np-saved": (i) => !i.half.includes("up") && /Saved to the library/.test(i.notice),`);
rep(` { do: \`__click('.screen .row', 'Wi-Fi only')\`, name: "wifi-off", wait: 400 },`,
` { do: \`__click('.screen .row', 'Wi-Fi only')\`, name: "wifi-off", wait: 400 },` + NL + ins('ins-r26-mock-steps.txt'));
rep(` "wifi-off": (i) => i.wifiOnly === "off" && !i.toast,`,
` "wifi-off": (i) => i.wifiOnly === "off" && !i.toast,` + NL + ins('ins-r26-mock-expect.txt'));
rep(` { do: \`__click('.prefs[data-mode="albumart"] .chip', '1500 px')\`, name: "you-artedge-back", wait: 900 },`,
` { do: \`__click('.prefs[data-mode="albumart"] .chip', '1500 px')\`, name: "you-artedge-back", wait: 900 },` + NL + ins('ins-r26-phone-steps.txt'));
rep(` "you-artedge-back": (i) => i.artEdge === "1500 px" && !i.toast,`,
` "you-artedge-back": (i) => i.artEdge === "1500 px" && !i.toast,` + NL + ins('ins-r26-phone-expect.txt'));
save();
// ── the doc ──
load('ANDROID.md');
rep(`record the queue holds. A moment after boot the phone warms the library's thumbnails the`, ins('ins-r26-doc-copy.txt'));
rep(`eight: Enter, or the screen left with a query in it), kept on the device.`,
`eight: Enter, or the screen left with a query in it), kept on the device.` + NL + NL + ins('ins-r26-doc.txt'));
save();
console.log('round 26 wired');