Commit 4f83790a by PLN (Algolia)

feat(armada): set builder — pick and order the Thu-24 arc against validated state

Fourteen candidates: the nine from the overnight sweep plus the five the driver journal
shows PLN actually played this week, each with its silent-eval verdict. Per-track minutes
against the 120-minute target, reorder, copy the paths out. One static file, no build,
renders offline, LAN-reachable for phone picking.
parent 0cd6300a
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Set Builder — Thu 2026-09-24</title>
<style>
:root{
--bg:#0b0d10; --panel:#13171c; --panel2:#181d24; --line:#252c35;
--ink:#e6edf3; --dim:#8b98a5; --ok:#3fb950; --warn:#d29922;
--accent:#4ea3ff; --bad:#f85149;
--mono: "Geist Mono","GeistMono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
--sans: "Geist",system-ui,-apple-system,"Segoe UI",sans-serif;
}
*{box-sizing:border-box}
html,body{margin:0}
body{background:var(--bg);color:var(--ink);font-family:var(--sans);
font-size:16px;line-height:1.45;padding:16px 16px 64px;-webkit-text-size-adjust:100%}
h1{font-size:1.15rem;margin:0 0 2px;letter-spacing:.02em}
h2{font-size:.8rem;text-transform:uppercase;letter-spacing:.12em;color:var(--dim);
margin:0 0 10px;font-weight:600}
.sub{color:var(--dim);font-size:.82rem;font-family:var(--mono);margin-bottom:18px}
.cols{display:grid;grid-template-columns:1fr;gap:18px;max-width:1500px;margin:0 auto}
@media(min-width:1000px){.cols{grid-template-columns:1fr 1fr}}
.panel{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px}
.card{background:var(--panel2);border:1px solid var(--line);border-left:3px solid transparent;
border-radius:8px;padding:12px 14px;margin-bottom:10px;cursor:pointer;
min-height:56px;transition:border-color .12s,background .12s}
.card:hover{border-color:#3a4552}
.card.on{border-left-color:var(--accent);background:#17222e}
.card .top{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.name{font-family:var(--mono);font-size:1rem;font-weight:600}
.genre{font-size:.72rem;color:var(--dim);border:1px solid var(--line);
border-radius:99px;padding:1px 8px}
.pill{font-size:.68rem;font-weight:700;letter-spacing:.08em;border-radius:99px;
padding:2px 9px;text-transform:uppercase}
.pill.ok{background:rgba(63,185,80,.14);color:var(--ok)}
.pill.warn{background:rgba(210,153,34,.16);color:var(--warn)}
.pill.played{background:rgba(78,163,255,.16);color:var(--accent)}
.note{color:#c2cdd9;font-size:.85rem;margin-top:6px}
.card.warnrow .note{color:#e8c46a}
.path{font-family:var(--mono);font-size:.74rem;color:var(--dim);margin-top:6px;
word-break:break-all}
.added{float:right;font-size:.7rem;color:var(--accent);font-family:var(--mono)}
.row{display:flex;align-items:center;gap:8px;background:var(--panel2);
border:1px solid var(--line);border-radius:8px;padding:10px;margin-bottom:8px}
.row .idx{font-family:var(--mono);color:var(--dim);font-size:.8rem;width:1.6em;text-align:right}
.row .rname{font-family:var(--mono);font-size:.95rem;flex:1;min-width:0;
overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.row.warnrow .rname{color:var(--warn)}
button{font-family:var(--sans);font-size:.95rem;color:var(--ink);background:#222a33;
border:1px solid var(--line);border-radius:7px;min-width:40px;min-height:40px;
cursor:pointer;padding:0 10px}
button:hover{background:#2c3642}
button:disabled{opacity:.3;cursor:default}
button.x{color:var(--bad)}
input[type=number]{width:62px;min-height:40px;background:#0e1216;color:var(--ink);
border:1px solid var(--line);border-radius:7px;font-family:var(--mono);
font-size:.95rem;text-align:center;padding:0 4px}
.mins-u{font-size:.7rem;color:var(--dim);font-family:var(--mono)}
.total{margin-top:14px;padding-top:12px;border-top:1px solid var(--line)}
.tnum{font-family:var(--mono);font-size:1.35rem;font-weight:700}
.tmsg{font-family:var(--mono);font-size:.85rem;margin-top:2px}
.under{color:var(--warn)} .over{color:var(--bad)} .good{color:var(--ok)}
.bar{height:14px;background:#0e1216;border:1px solid var(--line);border-radius:99px;
margin-top:10px;overflow:hidden;position:relative}
.fill{height:100%;width:0;background:var(--accent);transition:width .18s}
.fill.good{background:var(--ok)} .fill.over{background:var(--bad)}
.target{position:absolute;top:0;bottom:0;width:2px;background:#5b6672}
.acts{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}
pre{background:#0e1216;border:1px solid var(--line);border-radius:8px;padding:12px;
font-family:var(--mono);font-size:.78rem;color:#c2cdd9;overflow-x:auto;
white-space:pre;margin:12px 0 0;min-height:2em}
.empty{color:var(--dim);font-family:var(--mono);font-size:.85rem;padding:8px 0}
.flash{font-family:var(--mono);font-size:.8rem;color:var(--ok);margin-left:4px}
</style>
</head>
<body>
<h1>SET BUILDER</h1>
<div class="sub">Thu 2026-09-24 &middot; target 120 min &middot; tap a track to add / remove</div>
<div class="cols">
<section class="panel">
<h2>Candidates <span id="libcount" class="mins-u"></span></h2>
<div id="lib"></div>
</section>
<section class="panel">
<h2>The set</h2>
<div id="set"></div>
<div class="total">
<div class="tnum" id="tnum">0 / 120 min</div>
<div class="tmsg" id="tmsg"></div>
<div class="bar"><div class="fill" id="fill"></div><div class="target" id="tick"></div></div>
</div>
<div class="acts">
<button id="copy">Copy setlist</button>
<button id="clear">Clear set</button>
<span id="flash" class="flash"></span>
</div>
<pre id="out">(empty)</pre>
</section>
</div>
<script>
"use strict";
var TARGET = 120;
var TRACKS = [
{id:"sept1", path:"live/collab/baba/sept1.tidal", status:"OK", genre:"",
note:"d1 d4 d5 d11 need the #55 boot seed"},
{id:"jeudrill", path:"live/collab/raph/jeudrill.tidal", status:"OK", genre:"",
note:"d3 d4 d7 need seed"},
{id:"piment_bresilien", path:"live/collab/raph/piment_bresilien.tidal", status:"OK", genre:"",
note:"d3 d4 d5 d7 d9 need seed"},
{id:"you_my_sunshine", path:"live/midi/nova/dnb/liquid/you_my_sunshine.tidal", status:"OK",
genre:"dnb/liquid", note:"d4 needs seed"},
{id:"techno_orage", path:"live/midi/nova/techno/techno_orage.tidal", status:"WARN",
genre:"techno", note:"d2 (line 158) emits NOTHING even with seed — real silence, fix or cut"},
{id:"siberie_samuel", path:"live/collab/siberie_samuel.tidal", status:"OK", genre:"",
note:"cleanest: every orbit emits with no seed"},
{id:"cyber_hump", path:"live/midi/nova/techno/cyber_hump.tidal", status:"OK", genre:"techno",
note:"was dead (vi typo), FIXED 2026-09-22; bank 'humps' NOT in preload — lazy-loads"},
{id:"cafe_glace", path:"live/midi/nova/nujazz/cafe_glace.tidal", status:"OK", genre:"nujazz",
note:"d4 d5 need seed"},
{id:"ete_a_mauerpark", path:"live/midi/nova/techno/ete_a_mauerpark.tidal", status:"OK",
genre:"techno", note:"d1-d5 d7-d9 need seed; bank 'bass2' NOT in preload — lazy-loads"},
/* Play-history tracks: what PLN's hands actually reached for in the last 5 days
(lcxl3-driver journal). Validated 2026-09-22 with tools/silent-eval.py, cold
and with the #55 boot seed; all 28 sample banks resolve on disk and all 28 are
already inside the 124-bank preload plan. */
{id:"rose_rouge", path:"live/midi/nova/nujazz/rose_rouge.tidal", status:"OK",
genre:"nujazz", played:true,
note:"played 2x in the last 5 days; d4 d10 d11 d12 need the #55 seed"},
{id:"sunny_side_up", path:"live/midi/nova/lounge/sunny_side_up.tidal", status:"OK",
genre:"lounge", played:true, note:"d4 d7 need seed"},
{id:"quand_on_decolle", path:"live/midi/nova/ambient/quand_on_decolle.tidal", status:"OK",
genre:"ambient", played:true, note:"d4 d5 need seed"},
{id:"something_about_drums", path:"live/midi/nova/dnb/something_about_drums.tidal",
status:"OK", genre:"dnb", played:true, note:"d4 d5 need seed"},
{id:"bombe_dj", path:"live/midi/nova/techno/bombe_dj.tidal", status:"OK",
genre:"techno", played:true, note:"d3 d4 d5 d9 need seed"}
];
var BY_ID = {};
TRACKS.forEach(function(t){ BY_ID[t.id] = t; });
var KEY = "pv.setbuilder.thu24";
var set = load();
function load(){
try {
var raw = localStorage.getItem(KEY);
if(!raw) return [];
var v = JSON.parse(raw);
if(!Array.isArray(v)) return [];
return v.filter(function(e){ return e && BY_ID[e.id]; })
.map(function(e){ return {id:e.id, mins: clampMins(e.mins)}; });
} catch(e){ return []; }
}
function save(){
try { localStorage.setItem(KEY, JSON.stringify(set)); } catch(e){}
}
function clampMins(m){
m = parseFloat(m);
if(!isFinite(m) || m < 0) m = 12;
return Math.min(m, 600);
}
function indexOf(id){
for(var i=0;i<set.length;i++) if(set[i].id === id) return i;
return -1;
}
function el(tag, cls, text){
var n = document.createElement(tag);
if(cls) n.className = cls;
if(text !== undefined && text !== null) n.textContent = text;
return n;
}
/* ── library ───────────────────────────────────────────────────────────── */
function renderLib(){
var box = document.getElementById("lib");
box.textContent = "";
TRACKS.forEach(function(t){
var i = indexOf(t.id);
var card = el("div", "card" + (i >= 0 ? " on" : "") +
(t.status === "WARN" ? " warnrow" : ""));
card.setAttribute("role", "button");
card.tabIndex = 0;
var top = el("div", "top");
top.appendChild(el("span", "name", t.id));
if(t.genre) top.appendChild(el("span", "genre", t.genre));
top.appendChild(el("span", "pill " + (t.status === "WARN" ? "warn" : "ok"), t.status));
if(t.played) top.appendChild(el("span", "pill played", "played recently"));
if(i >= 0) top.appendChild(el("span", "added", "in set #" + (i+1) + " — tap to remove"));
card.appendChild(top);
card.appendChild(el("div", "note", t.note));
card.appendChild(el("div", "path", t.path));
function toggle(){
var j = indexOf(t.id);
if(j >= 0) set.splice(j, 1);
else set.push({id:t.id, mins:12});
commit();
}
card.addEventListener("click", toggle);
card.addEventListener("keydown", function(ev){
if(ev.key === "Enter" || ev.key === " "){ ev.preventDefault(); toggle(); }
});
box.appendChild(card);
});
document.getElementById("libcount").textContent = set.length + "/" + TRACKS.length + " picked";
}
/* ── the set ───────────────────────────────────────────────────────────── */
function renderSet(){
var box = document.getElementById("set");
box.textContent = "";
if(!set.length){
box.appendChild(el("div", "empty", "nothing picked yet — tap a candidate"));
}
set.forEach(function(e, i){
var t = BY_ID[e.id];
var row = el("div", "row" + (t.status === "WARN" ? " warnrow" : ""));
row.appendChild(el("span", "idx", (i+1) + "."));
row.appendChild(el("span", "rname", t.id + (t.status === "WARN" ? " ⚠" : "")));
var inp = document.createElement("input");
inp.type = "number"; inp.min = "0"; inp.step = "1"; inp.value = e.mins;
inp.setAttribute("aria-label", "minutes for " + t.id);
inp.addEventListener("change", function(){
e.mins = clampMins(inp.value); inp.value = e.mins; commit();
});
row.appendChild(inp);
row.appendChild(el("span", "mins-u", "min"));
var up = el("button", "", "↑");
up.disabled = (i === 0);
up.title = "move up";
up.addEventListener("click", function(){
var tmp = set[i-1]; set[i-1] = set[i]; set[i] = tmp; commit();
});
var dn = el("button", "", "↓");
dn.disabled = (i === set.length - 1);
dn.title = "move down";
dn.addEventListener("click", function(){
var tmp = set[i+1]; set[i+1] = set[i]; set[i] = tmp; commit();
});
var rm = el("button", "x", "×");
rm.title = "remove";
rm.addEventListener("click", function(){ set.splice(i, 1); commit(); });
row.appendChild(up); row.appendChild(dn); row.appendChild(rm);
box.appendChild(row);
});
}
/* ── total + bar ───────────────────────────────────────────────────────── */
function renderTotal(){
var tot = set.reduce(function(a, e){ return a + clampMins(e.mins); }, 0);
var diff = tot - TARGET;
var kind = Math.abs(diff) <= 5 ? "good" : (diff < 0 ? "under" : "over");
var msg = Math.abs(diff) <= 5
? (diff === 0 ? "bang on" : "within 5 min — good enough")
: (diff < 0 ? Math.abs(diff) + " min short" : "+" + diff + " min over");
document.getElementById("tnum").textContent = tot + " / " + TARGET + " min";
var m = document.getElementById("tmsg");
m.textContent = msg + " · " + set.length + " track" + (set.length === 1 ? "" : "s");
m.className = "tmsg " + kind;
/* Bar is scaled so the 120-min target always sits at 80% of the width:
an over-run stays visible instead of pinning the bar at full. */
var span = TARGET / 0.8;
var fill = document.getElementById("fill");
fill.style.width = Math.min(100, (tot / span) * 100) + "%";
fill.className = "fill " + kind;
document.getElementById("tick").style.left = "80%";
}
/* ── output text ───────────────────────────────────────────────────────── */
function setText(){
return set.map(function(e){ return BY_ID[e.id].path; }).join("\n");
}
function renderOut(){
var txt = setText();
document.getElementById("out").textContent = txt || "(empty)";
}
function flash(msg, bad){
var f = document.getElementById("flash");
f.textContent = msg;
f.style.color = bad ? "var(--warn)" : "var(--ok)";
setTimeout(function(){ if(f.textContent === msg) f.textContent = ""; }, 2500);
}
function commit(){
save(); renderLib(); renderSet(); renderTotal(); renderOut();
}
document.getElementById("copy").addEventListener("click", function(){
var txt = setText();
if(!txt){ flash("nothing to copy", true); return; }
var ok = function(){ flash("copied " + set.length + " paths"); };
var fail = function(){ flash("clipboard blocked — select the text below", true); };
try {
if(navigator.clipboard && navigator.clipboard.writeText){
navigator.clipboard.writeText(txt).then(ok, fallback);
} else { fallback(); }
} catch(e){ fallback(); }
function fallback(){
try {
var ta = document.createElement("textarea");
ta.value = txt;
ta.style.position = "fixed"; ta.style.top = "-1000px";
document.body.appendChild(ta);
ta.select();
var done = document.execCommand("copy");
document.body.removeChild(ta);
done ? ok() : fail();
} catch(e){ fail(); }
}
});
document.getElementById("clear").addEventListener("click", function(){
if(!set.length) return;
if(!window.confirm("Clear the whole set?")) return;
set = [];
commit();
});
commit();
</script>
</body>
</html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment