From c1152b4ab3ac101cfd7b18eeb7c7518c973bbd0b Mon Sep 17 00:00:00 2001 From: Christian Date: Tue, 20 Oct 2026 13:11:56 -0400 Subject: [PATCH] Band Lineup 1.12.0: Sync keeps your moves and puts each person in one spot Two Sync problems found by checking the real October 25 plan against the board: - Someone on the plan twice (Mike Powell is Leader and Guitar [Acoustic]) could fill two spots, pushing a real player off the board. Which spot the duplicate took depended on the order Planning Center returned people, so it only happened some weeks. Assignment now uses each person once. - Moving a synced person to another spot was undone by the next Sync, which re-placed everyone it had filled. A move is now a hand placement for the current plan and Sync leaves it alone. Co-Authored-By: Claude Opus 5.5 --- modules/band-lineup/matching.js | 6 +++++- modules/band-lineup/matching.test.js | 9 +++++++++ modules/band-lineup/server.js | 8 ++++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/modules/band-lineup/matching.js b/modules/band-lineup/matching.js index cfea022..a12701d 100644 --- a/modules/band-lineup/matching.js +++ b/modules/band-lineup/matching.js @@ -128,7 +128,11 @@ function assignMembersToSlots(slots, members) { if (score > bestScore) { bestScore = score; bestIndex = i; } } if (bestIndex < 0) return null; - return pool.splice(bestIndex, 1)[0]; + const chosen = pool.splice(bestIndex, 1)[0]; + // One spot per person: someone scheduled twice (Leader + Acoustic) must not fill a second spot. + const key = chosen.personId || ''; + if (key) for (let i = pool.length - 1; i >= 0; i -= 1) if (pool[i].personId === key) pool.splice(i, 1); + return chosen; }); } diff --git a/modules/band-lineup/matching.test.js b/modules/band-lineup/matching.test.js index 516574c..90f30c8 100644 --- a/modules/band-lineup/matching.test.js +++ b/modules/band-lineup/matching.test.js @@ -65,3 +65,12 @@ console.log('assignment:', JSON.stringify(out)); assert.strictEqual(bestLibraryMatch('Chris Smith', [{ id: '1', name: 'Chris' }, { id: '2', name: 'Chris' }]), null, 'ambiguous tie → null'); console.log('bestLibraryMatch: all assertions passed ✓'); } + +// --- one spot per person: someone scheduled twice (Leader + Acoustic) fills one spot only --- +{ + const P = (label, name, personId) => ({ label, name, personId }); + const slots = ['[LEADER, VOX 1, ACOUSTIC] MIX 1', '[GUITAR, 2] MIX 7']; + const got = assignMembersToSlots(slots, [P('[Leader]', 'Mike', 'mp'), P('Guitar [Acoustic]', 'Mike', 'mp'), P('Guitar [Rhythm]', 'Craig', 'cs')]).map((m) => (m ? m.name : null)); + assert.deepStrictEqual(got, ['Mike', 'Craig'], 'Mike once, Craig gets the guitar spot'); + console.log('one spot per person: all assertions passed ✓'); +} diff --git a/modules/band-lineup/server.js b/modules/band-lineup/server.js index 92bcf63..abc1057 100644 --- a/modules/band-lineup/server.js +++ b/modules/band-lineup/server.js @@ -530,8 +530,8 @@ module.exports = { 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) && e.plan === planId) { - const nm = e.personId ? (library[e.personId] && library[e.personId].name) : e.name; + if (e && (e.personId || e.name || e.pco) && e.plan === planId) { + const nm = e.pco ? e.pco.name : e.personId ? (library[e.personId] && library[e.personId].name) : e.name; if (nm) manualNames.add(normName(nm)); } else openIdx.push(i); } @@ -675,6 +675,10 @@ module.exports = { const from = String(p.get('from') || ''); const to = String(p.get('to') || ''); if (!/^\d+$/.test(from) || !/^\d+$/.test(to) || from === to) return json(res, 400, { error: 'Bad move.' }); const a = current.board[from]; const b = current.board[to]; + // Moving a spot is a hand placement for this plan: Sync must not move it back. + const plan = current.planId(); + if (a) a.plan = plan; + if (b) b.plan = plan; if (a === undefined) delete current.board[to]; else current.board[to] = a; if (b === undefined) delete current.board[from]; else current.board[from] = b; current.saveBoard(); current.rebuildLocal();