// Round thirty-four: the first run. Every walk and every deploy started // from Kyle's phone — signed in, folder set, a config full of his // settings — and nobody had seen what this app looks like on a phone that // has just installed it. On Android the library root is never missing (it // falls back to the app's own invisible sandbox), `setup_done` has never // been written by the phone so EVERY phone reads as unstamped, and signed // out is answered honestly everywhere and helpfully nowhere. So: one store, // a two-card sheet the boot gate opens only on a phone that is genuinely // fresh (three clauses, all required), the note with the row that fixes it // on the four screens that are empty because of the same two things, a // preview door in You that writes nothing, and a fresh-install mode in the // mock so the whole thing is walkable end to end. No password is handled // anywhere in it — the Soulseek step navigates to the card and stops. 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 shell.css block is an APPEND with no anchor and several inserts below re-emit their own anchors, so a second run would write twice where `once` cannot see it coming and only throw further down, leaving half a tree. Refuse one here, before anything is touched: the store is the file this round creates and nothing else in the repo makes it. */ if (fs.existsSync(root + 'src/mobile/setup.svelte.js')) { throw new Error('src/mobile/setup.svelte.js already exists — round 34 has already run against ' + root); } // ── the store, and the note that shares it ──────────────────────────── fs.writeFileSync(root + 'src/mobile/setup.svelte.js', ins('ins-r34-setup-store.txt') + NL); fs.writeFileSync(root + 'src/mobile/SetupNote.svelte', ins('ins-r34-setupnote.txt') + NL); // ── signed out has to be a FACT, not an absence ─────────────────────── load('src/mobile/slsk.svelte.js'); rep(` /** the saved username, for the sign-in card */ savedUser: "", hasPassword: false,`, ` /** the saved username, for the sign-in card */ savedUser: "", hasPassword: false, /** the credentials have been READ — see loadCredentials */ credsRead: false,`); rep(` slsk.hasPassword = !!c?.has_password; } catch { /* fine */ } }`, ` slsk.hasPassword = !!c?.has_password; } catch { /* fine */ } /* read, whatever it said. "Signed out" is a fact the first run has to be able to state, and before this resolves every phone in the world merely LOOKS signed out — a note that says so then is a lie with good timing. */ slsk.credsRead = true; }`); save(); // ── the shell: the config at boot, the gate after the two answers, and // the stamp whichever door closed the sheet ────────────────────────── load('src/mobile/Mobile.svelte'); rep(` import { loadPrefs } from "./prefs.svelte.js";`, ` import { loadPrefs } from "./prefs.svelte.js"; import { setup, loadSetup, maybeOpenFirstRun, openSetup, finishSetup, setupState } from "./setup.svelte.js";`); rep(` $effect(() => { syncShade(); });`, ` $effect(() => { syncShade(); });` + NL + NL + ins('ins-r34-mobile-effect.txt')); rep(` hooks.recentsDirty = refreshRecents; bindMedia(); loadLibrary(); /* the network: subscribe first, then the saved account signs in on its own */ bindSlsk().then(() => connectStored()).catch(() => {});`, ins('ins-r34-mobile-boot.txt')); rep(`wifi: { downloadsAllowed, cellular }, next, prev, toggle`, `setup, openSetup, setupState, wifi: { downloadsAllowed, cellular }, next, prev, toggle`); save(); // ── the sheet ───────────────────────────────────────────────────────── load('src/mobile/HalfSheet.svelte'); rep(` import { downloadsAllowed } from "./network.svelte.js";`, ` import { downloadsAllowed } from "./network.svelte.js"; import { setup, needsFolder, needsAccount, chooseFolder, requestAccess, finishSetup } from "./setup.svelte.js";`); rep(` {:else if kind === "sleep"}`, ins('ins-r34-halfsheet.txt') + NL + ` {:else if kind === "sleep"}`); save(); // ── Home: the stale empty state (its copy predates the picker in You) ── load('src/mobile/screens/Home.svelte'); rep(` import Release from "../Release.svelte";`, ` import Release from "../Release.svelte"; import SetupNote from "../SetupNote.svelte";`); rep(`{:else if lib.loaded}
Nothing in the library on this phone yet. Music the phone can play lives in the app's own Library folder for now; follow artists and Save records from the catalog, and this page fills up.
{/if}`, `{:else if lib.loaded}
Nothing in the library on this phone yet. Save records from the catalog and follow the artists you collect, and this page fills up with what they release.
{/if}`); save(); // ── Library: the albums stub, and the Artists tab, which had no empty // state at all — a count of zero over an empty each block ──────────── load('src/mobile/screens/Library.svelte'); rep(` import Release from "../Release.svelte";`, ` import Release from "../Release.svelte"; import SetupNote from "../SetupNote.svelte";`); rep(`
Nothing here yetThe library on this phone is empty.
`, `
Nothing here yetThe library on this phone is empty.
`); rep(`{:else if tab === "artists"}
{arts.length} artists · {lib.follows.length} followed
`, `{:else if tab === "artists"}
{arts.length} artists · {lib.follows.length} followed
{#if !arts.length}
No artists yetEvery artist with a record in the library on this phone is listed here.
{/if}`); save(); // ── Search: the flat sentence becomes the row that does it ──────────── load('src/mobile/screens/Search.svelte'); rep(` import Release from "../Release.svelte";`, ` import Release from "../Release.svelte"; import SetupNote from "../SetupNote.svelte";`); rep(` {#if !online}Sign in on Community to play what the library does not hold.{/if} `, ` `); save(); // ── You: one test for the private folder, and the preview door ──────── load('src/mobile/screens/You.svelte'); rep(` import { settings, setSetting } from "../prefs.svelte.js";`, ` import { settings, setSetting } from "../prefs.svelte.js"; import { setup, openSetup, loadSetup, isPrivateFolder } from "../setup.svelte.js";`); /* one wrong test is better than two: the regex lives in the store now */ rep(` const privateFolder = $derived(!cfg?.download_dir || /\\/data\\/(user\\/\\d+\\/|data\\/)com\\.soulstream/.test(cfg.download_dir));`, ` const privateFolder = $derived(isPrivateFolder(cfg?.download_dir));`); /* and the two doors onto the same folder are told about each other: this screen re-reads when the tour it opens moves the folder, and the tour's store is told when this screen's own picker does — otherwise the note on Home goes on offering a folder that has already been picked here. */ rep(` $effect(() => { api.getSettings().then((s) => { cfg = s; }, () => {}); });`, ` $effect(() => { void setup.folder; api.getSettings().then((s) => { cfg = s; }, () => {}); });`); rep(` await api.setSettings({ downloadDir: r.path }); cfg = await api.getSettings(); await rescan();`, ` await api.setSettings({ downloadDir: r.path }); cfg = await api.getSettings(); void loadSetup(); await rescan();`); rep(`
Soulseek{lib.username || "not signed in"}
`, `
Soulseek{lib.username || "not signed in"}
` + NL + ins('ins-r34-you-row.txt')); save(); // ── the look: almost nothing, because both surfaces are the app's own // parts. Appended, so the guard above is what refuses a second run. load('src/mobile/shell.css'); s = s.replace(/\s*$/, NL) + NL + ins('ins-r34-css.txt') + NL; save(); // ── the mock answers as a fresh install ─────────────────────────────── load('src/lib/mockLibrary.js'); rep(`const enabled = (() => {`, ins('ins-r34-mock-fresh.txt') + NL + NL + `const enabled = (() => {`); /* The fresh gate sits BELOW round 31's weather gate, not above it. The fresh block short-circuits nine commands, so anchoring on `if (!enabled)` would put it first and quietly make those nine unarmable — a fresh phone whose database will not open is exactly the case a later test will want. */ rep(` const forecast = weather(cmd); if (forecast) return forecast;`, ` const forecast = weather(cmd); if (forecast) return forecast;` + NL + ins('ins-r34-mock-empty.txt')); rep(` if (cmd === "get_credentials") return Promise.resolve({ username: MOCK_USER, has_password: true });`, ` if (cmd === "get_credentials") return Promise.resolve(fresh ? { username: "", has_password: false } : { username: MOCK_USER, has_password: true });`); rep(` if (cmd === "plugin:storage|all_files_access") return Promise.resolve({ granted: true });`, ` if (cmd === "plugin:storage|all_files_access") return Promise.resolve({ granted: !fresh });`); /* a provisioned mock is a phone that HAS been set up — say so, rather than leaving the field absent and the flag false by omission */ rep(` volume_normalization: true,`, ` volume_normalization: true,` + NL + ` setup_done: true,`); rep(`const cacheReady = new Set(); // paths a preload or a finished stream has fully fetched`, ins('ins-r34-mock-settings.txt') + NL + `const cacheReady = new Set(); // paths a preload or a finished stream has fully fetched`); rep(` if (cmd === "player_state") return Promise.resolve({ status: "idle", position: 0 });`, ` if (cmd === "player_state") return Promise.resolve({ status: "idle", position: 0 });` + NL + ins('ins-r34-mock-transport.txt')); save(); // ── the walks. The two list anchors are the ARRAY ENDS, not a step: a // sibling round appending its own block must not push mine into the // middle of the walk, because mine reloads the page. load('tests/mobile/smoke.mjs'); rep(` contactsLoaded: (function () { try { return __ss.contacts.loaded ? 'yes' : (__ss.contacts.error ? 'err' : 'no'); } catch (e) { return ''; } })(),`, ` contactsLoaded: (function () { try { return __ss.contacts.loaded ? 'yes' : (__ss.contacts.error ? 'err' : 'no'); } catch (e) { return ''; } })(),` + NL + ins('ins-r34-probe.txt')); rep(`]; const MOCK_EXPECT = {`, ins('ins-r34-mock-steps.txt') + NL + `]; const MOCK_EXPECT = {`); rep(`}; /* the phone walk: a record by name, played for real */`, ins('ins-r34-mock-expect.txt') + NL + `}; /* the phone walk: a record by name, played for real */`); rep(` { do: \`document.querySelectorAll('.nav button')[0].click()\`, name: "end", wait: 800 },`, ins('ins-r34-phone-steps.txt') + NL + ` { do: \`document.querySelectorAll('.nav button')[0].click()\`, name: "end", wait: 800 },`); /* The mock leg reloads for the first time in this round, and the viewport emulation lives on the DevTools SESSION rather than on the page: the re-attach after a reload drops it, and every still from there on would be the Chrome window's own size at 1× with the phone's touch emulation gone. Keep what was set and put it back. */ rep(` await cdp.send("Emulation.setDeviceMetricsOverride", { width: 411, height: 914, deviceScaleFactor: 2, mobile: true });`, ` await cdp.send("Emulation.setDeviceMetricsOverride", metrics);`); rep(` if (!PHONE) { await cdp.send("Page.addScriptToEvaluateOnNewDocument"`, ` /* the emulated phone, kept: a re-attach has to be handed it again */ let metrics = { width: 411, height: 914, deviceScaleFactor: 2, mobile: true }; if (!PHONE) { await cdp.send("Page.addScriptToEvaluateOnNewDocument"`); rep(` const r = await cdp.send("Emulation.setDeviceMetricsOverride", { width: s.view[0], height: s.view[1], deviceScaleFactor: 2, mobile: true });`, ` metrics = { width: s.view[0], height: s.view[1], deviceScaleFactor: 2, mobile: true }; const r = await cdp.send("Emulation.setDeviceMetricsOverride", metrics);`); rep(` try { await reattach(); } catch (e) { problems.push("STEP " + (s.name || "reload") + ": could not re-attach after the reload: " + e.message); }`, ` try { await reattach(); if (!PHONE) await cdp.send("Emulation.setDeviceMetricsOverride", metrics); } catch (e) { problems.push("STEP " + (s.name || "reload") + ": could not re-attach after the reload: " + e.message); }`); rep(` end: (i) => /^(Good |Still)/.test(i.title),`, ins('ins-r34-phone-expect.txt') + NL + ` end: (i) => /^(Good |Still)/.test(i.title),`); save(); // ── the doc ─────────────────────────────────────────────────────────── load('ANDROID.md'); rep(`## Settings that reach the config`, ins('ins-r34-doc.txt') + NL + NL + `## Settings that reach the config`); /* round thirty-one called its weather block the last one in the mock walk, which was true until this round put the fresh install after it */ rep(`any exception or console error. The block at the end of the mock walk is the weather:`, `any exception or console error. Near the end of the mock walk is the weather:`); /* the clause after this one ("asserts what each step should show") is round thirty-two's now — it hangs the launch budget off it — so the anchor stops at the step list's own last item, which nothing else rewrites. */ rep(`all at 915×411) and back,`, `all at 915×411) and back → and last, on a reload of its own, a phone that has just been installed (the first-run sheet and its two cards, its Soulseek door, the notes on Home, on both Library tabs and on Search, the preview from You, a folder picked from the tour and the note losing the row it answered), then back to the provisioned device,`); rep(`- **Per library folder.** Follows and the play history live in the library folder, so the phone's own folder starts empty of both; they arrive with a shared library.`, `- **Per library folder.** Follows and the play history live in the library folder, so the phone's own folder starts empty of both; they arrive with a shared library. ` + ins('ins-r34-doc-notdone.txt')); save(); console.log('round 34 wired');