From 4b0f161c9370528238d3bf7aace75ecaebfd3271 Mon Sep 17 00:00:00 2001 From: Christian Date: Tue, 13 Oct 2026 11:03:31 -0400 Subject: [PATCH] Band Lineup 1.11.1: Sync replaces last week's hand-placed people MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- modules/band-lineup/module.json | 2 +- modules/band-lineup/server.js | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/modules/band-lineup/module.json b/modules/band-lineup/module.json index c144f92..df2cc60 100644 --- a/modules/band-lineup/module.json +++ b/modules/band-lineup/module.json @@ -1,7 +1,7 @@ { "id": "band-lineup", "name": "Band Lineup", - "version": "1.11.0", + "version": "1.11.1", "proddash": ">=1.4.0", "description": "Stage lineup photos with mic/pack labels — a drag-in photo library plus optional Planning Center auto-fill", "client": "client.js", diff --git a/modules/band-lineup/server.js b/modules/band-lineup/server.js index 4141122..92bcf63 100644 --- a/modules/band-lineup/server.js +++ b/modules/band-lineup/server.js @@ -247,7 +247,7 @@ module.exports = { return { configured: pco.configured, serviceTypeSet: Boolean(serviceTypeId), 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 })), library: [], choir: choirOn, }; @@ -493,7 +493,8 @@ module.exports = { await syncPhotos(); // import the photo source (Drive folder or local path) on every sync if (pco.configured && 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.'; } else { @@ -521,12 +522,15 @@ module.exports = { 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 planId = String(plan.id); const openIdx = []; const manualNames = new Set(); for (let i = 0; i < eff.length; i += 1) { 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; if (nm) manualNames.add(normName(nm)); } else openIdx.push(i); @@ -540,8 +544,8 @@ module.exports = { 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); board[String(i)] = { pco: { name: m.name, file, birthday: m.birthday || '', personId: m.personId } }; - } else if (board[String(i)] && board[String(i)].pco) { - delete board[String(i)]; // was synced, no longer on the plan + } else if (board[String(i)]) { + delete board[String(i)]; // synced or stale, and no longer on the plan } } saveBoard(); @@ -580,6 +584,7 @@ module.exports = { current = { streams, getLatest: () => latest, pco, listPlans, photoDir, library, board, saveLibrary, saveBoard, rebuildLocal, refresh: () => syncNow(), slots, + planId: () => (latest.plan && latest.plan.id) || '', photoUrlFor, libraryList, setChoir, choirOn: () => choirOn, }; @@ -684,7 +689,7 @@ module.exports = { const personId = String(q.get('personId') || '').trim(); 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.board[slot] = { personId, name: '' }; + if (personId) current.board[slot] = { personId, name: '', plan: current.planId() }; else delete current.board[slot]; current.saveBoard(); current.rebuildLocal(); json(res, 200, { ok: true }); @@ -702,6 +707,7 @@ module.exports = { const name = String(q.get('name') || '').trim(); const existing = current.board[index] || { personId: '', name: '' }; existing.name = name; + existing.plan = current.planId(); if (!existing.personId && !name) delete current.board[index]; else current.board[index] = existing; current.saveBoard(); current.rebuildLocal();