Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,15 @@ fix flows to every page automatically; two hand-written HTML pages never will.
`Studio._urlSuppress`. URL writing is NOT a protocol mutation — never route
it through `pushUndo`. Any NEW shareable state must flow through
`encodeApp()` + `Studio.updateUrl` (never hand-build `location.search`).
**`?repo=` alone is a tab-scoped repo override** (LAB-227 Phase 4): `Studio.sessionRepo`
is parsed in the classic shell and `courseSettings()` returns it ahead of the stored
`studio_gh_repo` and the default (`Studio.DEFAULT_COURSE_REPO` = the private
`reiserlab/arena-experiments`; the Pattern Designer's `DEFAULT_DATA_REPO` and the
dashboard's `DEFAULT_REPO` carry the same string, pinned by `tests/test-studio-shell.js`).
Every repo consumer reads `courseSettings().repo` — never `localStorage` directly — so the
override wins for previews, Open from Repo, vocab, replay AND saves; it is never written to
storage, `encodeApp({sessionRepo})` keeps it across the canonical rewrite, and typing a
repo in ⚙ Settings ends it.
- **Top bar: Protocol ▾ vs ⚙ Settings.** `Protocol ▾` (`#fileMenu`, renamed from File)
holds protocol ACTIONS only (new/open/replay/save/promote/copy/reset/soak). `⚙ Settings`
(`#settingsMenu`) holds the Studio/bench settings kept in the browser, not the YAML: the
Expand Down
156 changes: 112 additions & 44 deletions arena_studio.html

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions dashboard/data-browser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,11 @@ relative-heading pages instead of failing import.

## Public page, private data

The dashboard itself is public. Runlogs live in `reiserlab/cshl-2026-course`,
which was private during the course and has been public since 2026-08 (reads work
signed-out; writes still need a token).
The dashboard itself is public. Runlogs live in the lab's private data repo
`reiserlab/arena-experiments` (the default in the repo field since LAB-227 Phase 4;
every read needs a token). The 2026 course's logs stay in `reiserlab/cshl-2026-course`,
public since 2026-08 (reads work signed-out) — type that repo into the field to browse
them.

The sign-in flow uses:

Expand Down
4 changes: 2 additions & 2 deletions dashboard/data-browser/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ function updateGithubUi(user) {
const repo = G.parseRepo(els.githubRepoInput.value);
els.courseRepoLink.href = `https://github.com/${repo.full}`;
} catch (_) {
els.courseRepoLink.href = 'https://github.com/reiserlab/cshl-2026-course';
els.courseRepoLink.href = `https://github.com/${G.DEFAULT_REPO}`;
}
}

Expand Down Expand Up @@ -951,7 +951,7 @@ async function browseGithub() {

function normalizeDirectoryUrl(value) {
const url = new URL(
A.safeText(value).trim() || '/cshl-2026-course/runlogs/',
A.safeText(value).trim() || '/arena-experiments/runlogs/',
window.location.href
);
if (!url.pathname.endsWith('/')) url.pathname += '/';
Expand Down
5 changes: 4 additions & 1 deletion dashboard/data-browser/github-repo.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
const REPO_KEY = 'studio_gh_repo';
const BENCH_KEY = 'studio_bench_id';
const FOLDERS_KEY_PREFIX = 'dashboard_runlog_folders:';
const DEFAULT_REPO = 'reiserlab/cshl-2026-course';
// The lab's private data repo (LAB-227) — same default as Arena Studio and the
// Pattern Designer; the 2026 course's logs stay in reiserlab/cshl-2026-course
// (type it into the repo field). tests/test-studio-shell.js pins the three equal.
const DEFAULT_REPO = 'reiserlab/arena-experiments';
// Run logs are committed as `.jsonl.gz` since Studio v0.72; fetchRaw reads
// BYTES and lets the shared format module inflate (magic-detected, so a raw
// `.jsonl` still works). Vendored byte-identical copy of js/runlog-format.js.
Expand Down
6 changes: 3 additions & 3 deletions dashboard/data-browser/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<h1>Course Data Dashboard</h1>
</div>
<div class="header-actions">
<a id="courseRepoLink" class="quiet-link" href="https://github.com/reiserlab/cshl-2026-course" target="_blank" rel="noreferrer">Course repo</a>
<a id="courseRepoLink" class="quiet-link" href="https://github.com/reiserlab/arena-experiments" target="_blank" rel="noreferrer">Data repo</a>
<div class="status-strip" id="statusStrip" aria-live="polite">
<span class="status-dot" aria-hidden="true"></span>
<span id="statusText">No data loaded</span>
Expand Down Expand Up @@ -50,7 +50,7 @@ <h2>Data</h2>
</div>

<div class="github-controls">
<input id="githubRepoInput" type="text" value="reiserlab/cshl-2026-course" aria-label="GitHub repository">
<input id="githubRepoInput" type="text" value="reiserlab/arena-experiments" aria-label="GitHub repository" title="GitHub data repo (owner/name) — the lab's reiserlab/arena-experiments by default; type reiserlab/cshl-2026-course for the 2026 course logs">
<button id="githubSignInButton" type="button">Sign in</button>
<button id="githubSignOutButton" type="button" disabled>Sign out</button>
<button id="chooseRigsButton" type="button" title="Choose which rig folders are included in the data catalog" disabled>Select rigs</button>
Expand All @@ -60,7 +60,7 @@ <h2>Data</h2>
<details class="local-source">
<summary>Local server repository</summary>
<div class="inline-control">
<input id="repoBaseInput" type="text" value="/cshl-2026-course/runlogs/" aria-label="Local runlog repository path">
<input id="repoBaseInput" type="text" value="/arena-experiments/runlogs/" aria-label="Local runlog repository path">
<button id="scanLocalButton" type="button">Scan local</button>
</div>
</details>
Expand Down
23 changes: 15 additions & 8 deletions docs/development/data-repo-token-runbook.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# Data-repo tokens — renewal and setup runbook

Arena Studio, the Pattern Designer and the data-browser dashboard write protocols,
patterns and run logs to a GitHub **data repo** (the CSHL course repo today; the lab's
`reiserlab/arena-experiments` next) using a personal access token pasted once per
browser (⚙ Settings → GitHub → **Sign in…**). The token is stored in that browser only
patterns and run logs to a GitHub **data repo** — the lab's private
`reiserlab/arena-experiments` by default (LAB-227 Phase 4, 2026-11); the CSHL course
benches and `rig03-sr` type `reiserlab/cshl-2026-course` into the Repo field instead —
using a personal access token pasted once per browser (⚙ Settings → GitHub →
**Sign in…**). A `?repo=owner/name` link overrides the stored repo for that tab only.
The rig's folder name is its **Rig id** (⚙ Settings; the course benches use
`bench01`…`bench07`). The token is stored in that browser only
(`studio_gh_pat`, sessionStorage or localStorage) and sent only as an
`Authorization` header. This page is the operational how-to; design background is in
`studio-github-save-proposal.md`, the course-week checklist in
Expand Down Expand Up @@ -38,9 +42,10 @@ the stored token; a save or run-log commit made with a dead token falls back to
`ghp_…` value once; store it in the lab password manager.
4. On each bench browser (served from `http://127.0.0.1:8000/`, not `file://`):
⚙ Settings → **Sign in…** → paste → **YES** to "Remember this token" → 🛡 advanced
(instructor password) → 🔓 in the GitHub block → **Repo** `reiserlab/cshl-2026-course`,
**Bench id** `bench01`…`bench07` (must match `roster.yaml`), tick **Commit
directly to default branch** → 🔒. Connect the arena; the roster pre-fills the
(instructor password) → 🔓 in the GitHub block → **Repo** `reiserlab/cshl-2026-course`
(the field is pre-filled with the lab repo — overwrite it), **Rig id**
`bench01`…`bench07` (must match `roster.yaml`), tick **Commit directly to default
branch** → 🔒. Connect the arena; the roster pre-fills the
experimenter.
5. Check: the run log shows `ages list from the course repo (… 5 entries)` and no
"unreadable" lines; a test save lands in `protocols/<bench-id>/`.
Expand All @@ -63,8 +68,10 @@ the stored token; a save or run-log commit made with a dead token falls back to
6. In the Studio: ⚙ Settings → **Sign in…** → paste. Answer **YES** to "Remember" only on a
rig computer you control or your own laptop; answer **NO** (session-only) on a
shared or borrowed machine. **Sign out** clears the token everywhere.
7. Set **Repo** and your **Rig id** once (🔓 needed); saves go to `protocols/<rig-id>/`,
run logs to `runlogs/<rig-id>/`, and your commits carry your GitHub identity.
7. Set your **Rig id** once (🔓 needed; **Repo** is already `reiserlab/arena-experiments`);
saves go to `protocols/<rig-id>/`, run logs to `runlogs/<rig-id>/`, and your commits
carry your GitHub identity. A rig may keep its own genotype / age / sex / fly-number
lists at `protocols/<rig-id>/<file>.yaml`; the Studio reads those before the root files.

Sharing across the lab needs no extra permission: everyone with a token sees every
rig's folder, **Protocol ▾ → Promote to shared…** copies a protocol and its patterns into
Expand Down
19 changes: 17 additions & 2 deletions js/studio-replay.js
Original file line number Diff line number Diff line change
Expand Up @@ -1246,7 +1246,18 @@
setProtoLine('Downloading ' + name + ' from ' + repo.full + '…');
const bytes = await readRepoBytes(repo, path);
if (token !== R.loadToken) return false;
if (!bytes) throw new Error(path + ' was not found in ' + repo.full + '.');
// Signed out, GitHub answers 404 for a private repo exactly as for a
// missing file — say so (the lab's default data repo is private).
if (!bytes) {
throw new Error(
path +
' was not found in ' +
repo.full +
(ghToken()
? '.'
: ' (HTTP 404) — sign in if this repository is private.')
);
}
const text = Fmt
? await Fmt.readRunlogText(bytes)
: new TextDecoder().decode(bytes);
Expand Down Expand Up @@ -2111,7 +2122,11 @@
if (!hit) {
if (Studio.showBanner)
Studio.showBanner(
'Replay: run ' + s.runId + ' was not found in ' + repo.full + '.'
'Replay: run ' +
s.runId +
' was not found in ' +
repo.full +
(ghToken() ? '.' : ' — sign in if this repository is private.')
);
return false;
}
Expand Down
16 changes: 13 additions & 3 deletions js/studio-url-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,9 @@
* is bench identity, NOT doc state — emitted whenever present (the caller
* only passes an explicit selection), regardless of doc locality. `repo`
* is emitted ONLY alongside a valid repo-path `p` (a repo ref without a
* document to point at is bench localStorage config, not URL state).
* @param {object} state {mode, p, lib, set, rig, repo, source?: 'local'|'committed'}
* document to point at is bench localStorage config, not URL state) —
* except `sessionRepo`, the tab-scoped override a bare ?repo= link set.
* @param {object} state {mode, p, lib, set, rig, repo, sessionRepo, source?: 'local'|'committed'}
*/
function encode(state) {
const s = state || {};
Expand All @@ -261,6 +262,11 @@
} else if (!local && isSafeKey(s.p)) {
params.set('p', s.p);
}
// sessionRepo: the tab-scoped repo override (a bare ?repo=, Studio.sessionRepo) —
// kept so a refresh keeps the tab on the requested repo; only when no repo-bound
// doc or replay already names the repo above.
if (!replayMode && !repoMode && isSafeRepo(s.sessionRepo))
params.set('repo', s.sessionRepo);
if (!replayMode && isSafeKey(s.lib)) params.set('lib', s.lib);
if (!replayMode && !local && isSafeKey(s.set)) params.set('set', s.set);
if (isSafeKey(s.rig)) params.set('rig', s.rig);
Expand Down Expand Up @@ -293,7 +299,10 @@
* `advanced` must be true ONLY when advanced mode is active AND was
* URL-requested (the caller enforces this; a remembered-unlocked browser
* passes false to keep a clean URL, mirroring the derived-rig rule).
* @param {object} app {mode, protocolKey, rigKey, repo, repoPath, advanced}
* `sessionRepo` is the tab-scoped repo override (Studio.sessionRepo, from a
* bare ?repo= link): emitted as `repo` only when no repo-bound doc or replay
* owns the link.
* @param {object} app {mode, protocolKey, rigKey, repo, repoPath, sessionRepo, advanced}
* @returns {string} query string (leading '?', or '' when all defaults)
*/
function encodeApp(app) {
Expand Down Expand Up @@ -323,6 +332,7 @@
return encode({
mode: a.mode,
p: a.protocolKey || undefined,
sessionRepo: a.sessionRepo || undefined,
rig: a.rigKey || undefined,
advanced: a.advanced || undefined,
soak: a.soak || undefined,
Expand Down
12 changes: 8 additions & 4 deletions pattern_editor.html
Original file line number Diff line number Diff line change
Expand Up @@ -2551,7 +2551,7 @@ <h3 class="modal-title">Save to Repo</h3>
<input type="radio" name="repoDest" id="destProtocol" value="protocol">
<span class="dest-text">
<b>A protocol's pattern folder</b>
<span>protocols/&lt;bench-id&gt;/&lt;protocol&gt;_patterns/ — travels with that protocol (needs a bench id)</span>
<span>protocols/&lt;rig-id&gt;/&lt;protocol&gt;_patterns/ — travels with that protocol (needs a rig id)</span>
<input type="text" class="dest-proto-input" id="destProtoName" placeholder="protocol name, e.g. my_protocol" disabled>
</span>
</label>
Expand Down Expand Up @@ -3582,9 +3582,13 @@ <h3 class="modal-title">Save to Repo</h3>
// ============================================================
let sessionRepo = null; // ?repo= override for this tab
let _urlArenaLock = false; // ?arena= handover locks the config after initUI
// Same default as the Studio's DEFAULT_COURSE_REPO (LAB-227): the lab's private
// data repo, so a fresh browser lists it in Open from Repo… / Save to Repo…
// without a Studio visit; a stored repo or a ?repo= link overrides it.
const DEFAULT_DATA_REPO = 'reiserlab/arena-experiments';

function repoSettings() {
const raw = (sessionRepo || localStorage.getItem('studio_gh_repo') || '').trim();
const raw = (sessionRepo || localStorage.getItem('studio_gh_repo') || DEFAULT_DATA_REPO).trim();
const rm = /^([A-Za-z0-9-]{1,39})\/([A-Za-z0-9._-]{1,100})$/.exec(raw);
const ok = rm && !raw.includes('..');
return {
Expand Down Expand Up @@ -3791,13 +3795,13 @@ <h3 class="modal-title">Save to Repo</h3>
if (!built) return;
_pendingRepoSave = built;
document.getElementById('repoSaveRepoLine').textContent =
'Repo: ' + cs.repoRaw + (cs.benchId ? ' · bench ' + cs.benchId : '');
'Repo: ' + cs.repoRaw + (cs.benchId ? ' · rig ' + cs.benchId : '');
const protoRadio = document.getElementById('destProtocol');
const protoInput = document.getElementById('destProtoName');
protoRadio.disabled = !cs.benchId;
protoRadio.closest('.dest-row').title = cs.benchId
? 'Commits next to a protocol so they travel together'
: 'Needs a bench id (set in Arena Studio → ⚙ Settings) — course benches only';
: 'Needs a rig id (set in Arena Studio → ⚙ Settings → Rig id)';
if (!cs.benchId) document.getElementById('destLibrary').checked = true;
protoInput.disabled = !protoRadio.checked;
updateRepoSavePath();
Expand Down
73 changes: 73 additions & 0 deletions tests/test-studio-shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const path = require('path');
const root = path.join(__dirname, '..');
const studio = fs.readFileSync(path.join(root, 'arena_studio.html'), 'utf8');
const replay = fs.readFileSync(path.join(root, 'js/studio-replay.js'), 'utf8');
const designer = fs.readFileSync(path.join(root, 'pattern_editor.html'), 'utf8');
const dashRepo = fs.readFileSync(path.join(root, 'dashboard/data-browser/github-repo.js'), 'utf8');
const dashIndex = fs.readFileSync(path.join(root, 'dashboard/data-browser/index.html'), 'utf8');

let totalChecks = 0;
let failures = 0;
Expand Down Expand Up @@ -407,6 +410,76 @@ check(
!/:root\[data-ui-theme="night"\] (img|canvas)\b/.test(studio) &&
studio.includes('filter:url(#uiNightRed)')
);
console.log('=== data repo defaults + rig id (LAB-227 Phase 4) ===');
// ONE default string across the three tools: the lab's private data repo. The course
// repo stays reachable by typing it (course benches + rig03-sr still write there).
const DATA_REPO = 'reiserlab/arena-experiments';
check(
'Studio, Pattern Designer and dashboard default to the lab data repo',
studio.includes("Studio.DEFAULT_COURSE_REPO = '" + DATA_REPO + "';") &&
designer.includes("const DEFAULT_DATA_REPO = '" + DATA_REPO + "';") &&
dashRepo.includes("const DEFAULT_REPO = '" + DATA_REPO + "';")
);
check(
'Studio Repo field + dashboard repo field show the data repo, not the course repo',
/id="ghRepoInput" placeholder="reiserlab\/arena-experiments"/.test(studio) &&
/id="githubRepoInput" type="text" value="reiserlab\/arena-experiments"/.test(dashIndex) &&
dashIndex.includes('href="https://github.com/reiserlab/arena-experiments"')
);
check(
'the Pattern Designer falls back to the same default when no repo is stored',
designer.includes(
"(sessionRepo || localStorage.getItem('studio_gh_repo') || DEFAULT_DATA_REPO).trim()"
)
);
check(
'?repo= is a tab-scoped override applied in courseSettings (wins signed in or out)',
studio.includes('Studio.sessionRepo = (function () {') &&
studio.includes(
"Studio.sessionRepo ||\n localStorage.getItem('studio_gh_repo') ||\n Studio.DEFAULT_COURSE_REPO"
) &&
studio.includes('sessionRepo: Studio.sessionRepo || null,') &&
!studio.includes("localStorage.setItem('studio_gh_repo', Studio.sessionRepo")
);
check(
'typing a repo in ⚙ Settings ends the ?repo= override',
studio.includes('Studio.sessionRepo = null;')
);
check(
'the shared-pattern listing names the repo it came from',
studio.includes(
"'shared pattern library: ' + n + ' preview(s) wired from ' + repo.full + '/patterns/'"
) && !studio.includes("'course repo: ' + n + ' shared pattern preview(s)")
);
check(
'Patterns ↗ passes the effective repo (default, stored, or ?repo=)',
studio.includes(
'var cs=window.Studio && Studio.courseSettings ? Studio.courseSettings() : null;'
) && studio.includes("var repo=cs && cs.repo ? cs.repo.full : '';")
);
check(
'UI says "Rig id" (the storage key studio_bench_id keeps its name)',
/<label for="ghBenchId">Rig id<\/label>/.test(studio) &&
!/Settings → Bench id/.test(studio) &&
!/[Bb]ench id not set/.test(studio) &&
!/set bench id/.test(studio) &&
studio.includes("localStorage.getItem('studio_bench_id')") &&
!/needs a bench id|bench id \(set in/.test(designer)
);
check(
'per-rig vocabularies: protocols/<rig-id>/<file> is tried before the repo root',
studio.includes('async function readCourseVocabFile(cs, token, file) {') &&
studio.includes("paths.push('protocols/' + cs.benchId + '/' + file);") &&
studio.includes("await readCourseVocabFile(cs, token, 'genotypes.yaml')") &&
studio.includes('await readCourseVocabFile(cs, token, file);')
);
check(
'replay: a signed-out 404 says "sign in if this repository is private"',
/ghToken\(\)\s*\?\s*'\.'\s*:\s*' \(HTTP 404\) — sign in if this repository is private\.'/.test(
replay
) && /ghToken\(\)\s*\?\s*'\.'\s*:\s*' — sign in if this repository is private\.'/.test(replay)
);

console.log('=== bridge-loss policy wiring (LAB-303 S1 review) ===');
check(
'bridgeRequired only for a recorded run the bridge is logging',
Expand Down
Loading
Loading