Compare commits

...
Author SHA1 Message Date
christian e670e1ee72 Merge pull request 'Band Lineup 1.11.1: Sync replaces last week's hand-placed people' (#50) from claude/band-lineup-sync-replaces into main 2026-10-13 11:05:08 -04:00
christianandClaude Opus 5.5 4b0f161c93 Band Lineup 1.11.1: Sync replaces last week's hand-placed people
Sync only filled empty spots and spots it had filled itself, so anyone
dragged in by hand stayed forever — pressing Sync for a new week swapped
some photos and left others, depending on how each spot got filled.

Hand placements are now stamped with the plan they were made for. Sync
keeps the ones made for the plan it is loading (a sub dragged in this
week survives a re-sync) and replaces or clears the rest. The last
synced plan is saved so placements made after a restart stamp right.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:03:31 -04:00
christian dafb48d9ed Merge pull request 'Vista Cues 1.0.1: its own heading in the + picker instead of sitting under Slack' (#49) from claude/vista-cues-picker into main 2026-10-11 11:40:50 -04:00
2 changed files with 14 additions and 8 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "band-lineup", "id": "band-lineup",
"name": "Band Lineup", "name": "Band Lineup",
"version": "1.11.0", "version": "1.11.1",
"proddash": ">=1.4.0", "proddash": ">=1.4.0",
"description": "Stage lineup photos with mic/pack labels — a drag-in photo library plus optional Planning Center auto-fill", "description": "Stage lineup photos with mic/pack labels — a drag-in photo library plus optional Planning Center auto-fill",
"client": "client.js", "client": "client.js",
+13 -7
View File
@@ -247,7 +247,7 @@ module.exports = {
return { return {
configured: pco.configured, serviceTypeSet: Boolean(serviceTypeId), configured: pco.configured, serviceTypeSet: Boolean(serviceTypeId),
reachable: false, lastError: '', lastFetched: 0, columns, reachable: false, lastError: '', lastFetched: 0, columns,
plan: null, plan: settings.lastPlan || null, // the last synced plan, so hand placements after a restart stamp right
slots: effectiveSlots().map((label, index) => ({ index, label, person: null })), slots: effectiveSlots().map((label, index) => ({ index, label, person: null })),
library: [], choir: choirOn, library: [], choir: choirOn,
}; };
@@ -493,7 +493,8 @@ module.exports = {
await syncPhotos(); // import the photo source (Drive folder or local path) on every sync await syncPhotos(); // import the photo source (Drive folder or local path) on every sync
if (pco.configured && serviceTypeId) { if (pco.configured && serviceTypeId) {
const plan = pickPlan(await listPlans(serviceTypeId)); const plan = pickPlan(await listPlans(serviceTypeId));
latest.plan = plan ? { title: plan.title, dates: plan.dates } : null; latest.plan = plan ? { id: String(plan.id), title: plan.title, dates: plan.dates } : null;
if (plan) { settings.lastPlan = latest.plan; saveSettings(); }
if (!plan) { latest.reachable = true; latest.lastError = 'No plan found for that service type.'; } if (!plan) { latest.reachable = true; latest.lastError = 'No plan found for that service type.'; }
else { else {
@@ -521,12 +522,15 @@ module.exports = {
const libEntries = Object.entries(library).map(([id, v]) => ({ id, name: v.name, file: v.file })); const libEntries = Object.entries(library).map(([id, v]) => ({ id, name: v.name, file: v.file }));
// Slots a person is sitting in by hand — don't fill them, and don't place that same person twice. // Slots a person was placed in by hand FOR THIS PLAN — don't fill them, and don't place that
// same person twice. A hand placement made for an earlier plan (last week's lineup, or one
// from before placements were stamped) is stale: Sync replaces it like any synced slot.
const eff = effectiveSlots(); const eff = effectiveSlots();
const planId = String(plan.id);
const openIdx = []; const manualNames = new Set(); const openIdx = []; const manualNames = new Set();
for (let i = 0; i < eff.length; i += 1) { for (let i = 0; i < eff.length; i += 1) {
const e = board[String(i)]; const e = board[String(i)];
if (e && (e.personId || e.name)) { if (e && (e.personId || e.name) && e.plan === planId) {
const nm = e.personId ? (library[e.personId] && library[e.personId].name) : e.name; const nm = e.personId ? (library[e.personId] && library[e.personId].name) : e.name;
if (nm) manualNames.add(normName(nm)); if (nm) manualNames.add(normName(nm));
} else openIdx.push(i); } else openIdx.push(i);
@@ -540,8 +544,8 @@ module.exports = {
const lib = bestLibraryMatch(m.name, libEntries); // folder photo wins over the PCO headshot const lib = bestLibraryMatch(m.name, libEntries); // folder photo wins over the PCO headshot
const file = lib && lib.file ? lib.file : await cachePcoPhoto(m.personId, m.photoUrl); const file = lib && lib.file ? lib.file : await cachePcoPhoto(m.personId, m.photoUrl);
board[String(i)] = { pco: { name: m.name, file, birthday: m.birthday || '', personId: m.personId } }; board[String(i)] = { pco: { name: m.name, file, birthday: m.birthday || '', personId: m.personId } };
} else if (board[String(i)] && board[String(i)].pco) { } else if (board[String(i)]) {
delete board[String(i)]; // was synced, no longer on the plan delete board[String(i)]; // synced or stale, and no longer on the plan
} }
} }
saveBoard(); saveBoard();
@@ -580,6 +584,7 @@ module.exports = {
current = { current = {
streams, getLatest: () => latest, pco, listPlans, photoDir, streams, getLatest: () => latest, pco, listPlans, photoDir,
library, board, saveLibrary, saveBoard, rebuildLocal, refresh: () => syncNow(), slots, library, board, saveLibrary, saveBoard, rebuildLocal, refresh: () => syncNow(), slots,
planId: () => (latest.plan && latest.plan.id) || '',
photoUrlFor, libraryList, photoUrlFor, libraryList,
setChoir, choirOn: () => choirOn, setChoir, choirOn: () => choirOn,
}; };
@@ -684,7 +689,7 @@ module.exports = {
const personId = String(q.get('personId') || '').trim(); const personId = String(q.get('personId') || '').trim();
if (!/^\d+$/.test(slot)) return json(res, 400, { error: 'Bad slot.' }); if (!/^\d+$/.test(slot)) return json(res, 400, { error: 'Bad slot.' });
if (personId && !current.library[personId]) return json(res, 404, { error: 'No such library photo.' }); if (personId && !current.library[personId]) return json(res, 404, { error: 'No such library photo.' });
if (personId) current.board[slot] = { personId, name: '' }; if (personId) current.board[slot] = { personId, name: '', plan: current.planId() };
else delete current.board[slot]; else delete current.board[slot];
current.saveBoard(); current.rebuildLocal(); current.saveBoard(); current.rebuildLocal();
json(res, 200, { ok: true }); json(res, 200, { ok: true });
@@ -702,6 +707,7 @@ module.exports = {
const name = String(q.get('name') || '').trim(); const name = String(q.get('name') || '').trim();
const existing = current.board[index] || { personId: '', name: '' }; const existing = current.board[index] || { personId: '', name: '' };
existing.name = name; existing.name = name;
existing.plan = current.planId();
if (!existing.personId && !name) delete current.board[index]; if (!existing.personId && !name) delete current.board[index];
else current.board[index] = existing; else current.board[index] = existing;
current.saveBoard(); current.rebuildLocal(); current.saveBoard(); current.rebuildLocal();