File size: 5,978 Bytes
ae853c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// The Deck — shared-state equilibrium dashboard bridging Play and Lab.
let summary = null, selection = [];

function rgb2hsv(r, g, b) {
    r /= 255; g /= 255; b /= 255;
    const mx = Math.max(r, g, b), mn = Math.min(r, g, b), d = mx - mn;
    let h = 0;
    if (d) { if (mx === r) h = ((g - b) / d) % 6; else if (mx === g) h = (b - r) / d + 2; else h = (r - g) / d + 4; h *= 60; if (h < 0) h += 360; }
    return [h, mx === 0 ? 0 : d / mx, mx];
}
const css = (rgb) => `rgb(${rgb.join(',')})`;
const el = (id) => document.getElementById(id);

async function load() {
    const [s, sel, sig] = await Promise.all([
        fetch('/api/deck/summary?t=' + Date.now()).then(r => r.json()),
        fetch('/api/selection').then(r => r.json()),
        fetch('/api/deck/signals').then(r => r.json()),
    ]);
    summary = s; selection = sel.ids || [];
    renderGauges(s); renderPortals(s); renderBreakdowns(s); renderGamut(s);
    renderRecent(s); renderTray(); renderSignals(sig);
}

function renderGauges(s) {
    const a = s.traitAverages;
    const cells = [
        { val: s.accessions, lbl: 'Accessions', sub: `F0 → F${s.maxGeneration}` },
        { val: s.breedableCount, lbl: 'Breedable', sub: `${s.growingCount} growing` },
        { val: a.THC, lbl: 'Avg THC', sub: `CBD ${a.CBD}%` },
        { val: a.Yield, lbl: 'Avg Yield', sub: `${a.GrowTime}d grow` },
        { val: a.Stability, lbl: 'Avg Stability', sub: `${Object.keys(s.byBudFamily).length} color families` },
    ];
    el('gauges').innerHTML = cells.map(c =>
        `<div class="gauge"><div class="val">${c.val}</div><div class="lbl">${c.lbl}</div><div class="sub">${c.sub}</div></div>`).join('');
}

function renderPortals(s) {
    el('play-sub').textContent = `${s.breedableCount} breedable · ${s.growingCount} growing`;
    el('lab-sub').textContent = `${s.accessions} accessions · ${Object.keys(s.byGeneration).length} generations`;
}

function bars(title, obj, swatch) {
    const max = Math.max(1, ...Object.values(obj));
    const rows = Object.entries(obj).map(([k, v]) => {
        const sw = swatch ? `<span style="display:inline-block;width:9px;height:9px;border:1px solid #555;background:${k};margin-right:5px"></span>` : '';
        return `<div class="bar-row"><div class="k"><span>${sw}${k}</span><span>${v}</span></div><div class="bar"><span style="width:${100 * v / max}%"></span></div></div>`;
    }).join('');
    return `<div class="bar-title">${title}</div>${rows}`;
}
function renderBreakdowns(s) {
    el('breakdowns').innerHTML =
        bars('Type', s.byType) + bars('Generation', s.byGeneration) + bars('Bud Family', s.byBudFamily, true);
}

function renderGamut(s) {
    const S = 240, R = S / 2 - 14, cx = S / 2, cy = S / 2;
    const NS = 'http://www.w3.org/2000/svg';
    let svg = `<svg class="gamut-svg" width="${S}" height="${S}" viewBox="0 0 ${S} ${S}">`;
    for (let an = 0; an < 360; an += 8) {
        const a0 = (an - 90) * Math.PI / 180, a1 = (an + 8 - 90) * Math.PI / 180;
        const p = (r, a) => `${cx + r * Math.cos(a)},${cy + r * Math.sin(a)}`;
        svg += `<path d="M${p(R - 6, a0)} L${p(R, a0)} A${R} ${R} 0 0 1 ${p(R, a1)} L${p(R - 6, a1)} A${R - 6} ${R - 6} 0 0 0 ${p(R - 6, a0)} Z" fill="hsl(${an},85%,55%)" opacity="0.45"/>`;
    }
    (s.gamut || []).forEach(rgb => {
        const [h, sat] = rgb2hsv(...rgb), rad = sat * (R - 10), ang = (h - 90) * Math.PI / 180;
        svg += `<circle cx="${cx + rad * Math.cos(ang)}" cy="${cy + rad * Math.sin(ang)}" r="5" fill="${css(rgb)}" stroke="#fff" stroke-width="0.6"/>`;
    });
    svg += `</svg>`;
    el('gamut').innerHTML = svg;
}

function renderRecent(s) {
    if (!s.recentCrosses.length) { el('recent').innerHTML = `<div class="empty-note">No crosses yet. Breed two specimens in Play Mode.</div>`; return; }
    el('recent').innerHTML = s.recentCrosses.map(r => `
        <div class="cross">
            <span class="sw" style="background:${css(r.budColor)}"></span>
            <div class="meta"><b>${r.name}</b> · F${r.generation} · THC ${r.THC}<div class="ped">${r.pedigree}</div></div>
        </div>`).join('');
}

function renderTray() {
    const host = el('tray');
    if (!summary) return;
    const byId = {};
    summary.recentCrosses.forEach(r => byId[r.id] = r);
    // fall back: we only have colors for recent; show ids/names where known
    if (!selection.length) { host.innerHTML = `<div class="empty-note">No specimens selected. Pick in Play or Lab; they ride along here.</div>`; return; }
    host.innerHTML = selection.map(id => {
        const r = byId[id];
        const sw = r ? `<span class="sw" style="background:${css(r.budColor)}"></span>` : '';
        return `<span class="tray-chip">${sw}${r ? r.name : id.slice(0, 8)}</span>`;
    }).join('');
}

function renderSignals(sig) {
    const chip = el('kos-chip');
    if (sig.available) { chip.textContent = `Observer Bus: online (${sig.count || 0})`; chip.classList.add('online'); }
    else { chip.textContent = 'Observer Bus: offline'; chip.classList.remove('online'); }
    const host = el('signals');
    if (!sig.available) { host.innerHTML = `<div class="empty-note">K-os not connected. Set KEY_OS_URL to stream signals here.</div>`; return; }
    if (!sig.signals.length) { host.innerHTML = `<div class="empty-note">Bus online · no active signals.</div>`; return; }
    host.innerHTML = sig.signals.map(g => `
        <div class="sig ${g.severity || 'info'}"><span class="lane">${g.lane}</span> ${g.title}<div class="ped">${g.message || ''}</div></div>`).join('');
}

async function saveSelection(ids) {
    selection = ids;
    await fetch('/api/selection', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ids }) });
    renderTray();
}

el('btn-refresh').onclick = load;
el('btn-clear-sel').onclick = () => saveSelection([]);
el('btn-to-play').onclick = () => { location.href = '/play'; };
el('btn-to-lab').onclick = () => { location.href = '/lab'; };
load();