document.addEventListener('DOMContentLoaded', () => {
const grid = document.getElementById('inventory-grid');
const slotA = document.getElementById('slot-a');
const slotB = document.getElementById('slot-b');
const btnLoad = document.getElementById('btn-load-starters');
const btnClear = document.getElementById('btn-clear');
const btnBreed = document.getElementById('btn-breed');
const btnClone = document.getElementById('btn-clone');
let selectedSeeds = [];
let inventory = [];
// Toast feedback. fetch() does NOT reject on HTTP 4xx/5xx, so every call
// must be routed through apiCall() to actually surface backend errors.
function showToast(message, kind = 'info') {
let host = document.getElementById('toast-host');
if (!host) {
host = document.createElement('div');
host.id = 'toast-host';
document.body.appendChild(host);
}
const t = document.createElement('div');
t.className = `toast toast-${kind}`;
t.textContent = message;
host.appendChild(t);
// force reflow so the entry transition runs
void t.offsetWidth;
t.classList.add('show');
setTimeout(() => {
t.classList.remove('show');
setTimeout(() => t.remove(), 300);
}, 3200);
}
async function apiCall(url, options) {
try {
const res = await fetch(url, options);
let data = {};
try { data = await res.json(); } catch (_) { /* non-JSON response */ }
if (!res.ok) {
throw new Error(data.detail || `Request failed (${res.status})`);
}
return data;
} catch (e) {
showToast(e.message || 'Network error', 'error');
throw e;
}
}
// Initialize
fetchInventory();
// Event Listeners
btnLoad.addEventListener('click', async () => {
try {
await apiCall('/api/inventory/starter', { method: 'POST' });
showToast('Baseline specimens initialized', 'success');
fetchInventory();
} catch (_) {}
});
btnClear.addEventListener('click', async () => {
try {
await apiCall('/api/inventory/clear', { method: 'POST' });
selectedSeeds = [];
updateSelection();
showToast('Registry purged', 'info');
fetchInventory();
} catch (_) {}
});
btnBreed.addEventListener('click', async () => {
if (selectedSeeds.length !== 2) return;
try {
await apiCall('/api/breed', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ parent1_id: selectedSeeds[0].id, parent2_id: selectedSeeds[1].id })
});
selectedSeeds = [];
updateSelection();
showToast('Hybrid synthesized', 'success');
fetchInventory();
} catch (_) { /* error already shown; keep selection so user can retry */ }
});
btnClone.addEventListener('click', async () => {
if (selectedSeeds.length !== 1) return;
try {
await apiCall('/api/clone', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ seed_id: selectedSeeds[0].id })
});
showToast('Specimen replicated', 'success');
fetchInventory();
} catch (_) {}
});
async function fetchInventory() {
try {
const res = await fetch('/api/inventory?t=' + Date.now());
const data = await res.json();
inventory = data.seeds || [];
renderGrid();
} catch (e) {
console.error('Error fetching inventory', e);
}
}
function renderGrid() {
if (inventory.length === 0) {
grid.innerHTML = '
Registry empty. Initialize baseline specimens to begin observation.
';
return;
}
grid.innerHTML = '';
inventory.forEach(seed => {
const isSelected = selectedSeeds.some(s => s.id === seed.id);
const card = document.createElement('div');
card.className = `seed-card ${isSelected ? 'selected' : ''}`;
// Build tooltip text
const num = [['THC', seed.thc], ['CBD', seed.cbd], ['Yield', seed.yield], ['GrowTime', seed.grow_time]];
let tooltip = `${seed.name}\nType: ${seed.type}\n`;
num.forEach(([k, v]) => { tooltip += `${k}: ${v} (Stab: ${seed.stabilities[k]})\n`; });
tooltip += `Bud: rgb(${seed.bud_color.join(',')})\nLeaf: rgb(${seed.leaf_color.join(',')})\n`;
card.title = tooltip;
card.innerHTML = `
${seed.name}
${seed.type}
${seed.stage}
`;
if (seed.stage !== 'MATURE') {
const growBtn = document.createElement('button');
growBtn.className = 'grow-btn';
growBtn.textContent = 'ADVANCE STAGE';
growBtn.onclick = async (e) => {
e.stopPropagation();
try {
await apiCall(`/api/seed/${seed.id}/grow`, { method: 'POST' });
fetchInventory();
} catch (_) {}
};
card.appendChild(growBtn);
}
card.onclick = () => toggleSelection(seed);
grid.appendChild(card);
});
}
function toggleSelection(seed) {
const idx = selectedSeeds.findIndex(s => s.id === seed.id);
if (idx >= 0) {
selectedSeeds.splice(idx, 1);
} else {
if (selectedSeeds.length >= 2) selectedSeeds.shift();
selectedSeeds.push(seed);
}
updateSelection();
renderGrid();
}
function updateSelection() {
renderSlot(slotA, selectedSeeds[0], 'Primary Specimen');
renderSlot(slotB, selectedSeeds[1], 'Secondary Specimen');
const canBreed = selectedSeeds.length === 2 && selectedSeeds.every(s => s.can_attempt);
const canClone = selectedSeeds.length === 1 && selectedSeeds[0].can_attempt;
btnBreed.disabled = !canBreed;
btnClone.disabled = !canClone;
}
function renderSlot(slotElement, seed, placeholderText) {
if (!seed) {
slotElement.innerHTML = `Select ${placeholderText}
`;
return;
}
const swatch = (rgb) => ``;
const num = [['THC', seed.thc], ['CBD', seed.cbd], ['Yield', seed.yield], ['GrowTime', seed.grow_time]];
let statsHtml = '';
num.forEach(([k, v]) => {
statsHtml += `
${k}
${v} (${seed.stabilities[k]})
`;
});
statsHtml += `
Bud${swatch(seed.bud_color)} ${seed.bud_color.join(',')}
Leaf${swatch(seed.leaf_color)} ${seed.leaf_color.join(',')}
`;
let allelesHtml = '';
['THC', 'CBD', 'Yield', 'GrowTime'].forEach(t => {
if (seed.alleles[t]) {
allelesHtml += `
${t}
${seed.alleles[t].join(' / ')}
`;
}
});
['BudColor', 'LeafColor'].forEach(t => {
const a = seed.alleles[t];
if (a && a.Hue) {
allelesHtml += `
${t} Hue
${a.Hue.map(x => Math.round(x)).join(' / ')}
`;
}
});
slotElement.innerHTML = `
${placeholderText}
${seed.name}
${statsHtml}
ALLELES:
${allelesHtml}
Lineage Tree
`;
const canvas = slotElement.querySelector('.lineage-canvas');
drawLineageTree(canvas, seed);
}
function drawLineageTree(canvas, seed) {
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
const parentCoords = [{x: 60, y: 30}, {x: 180, y: 30}];
const gpCoords = [{x: 20, y: 110}, {x: 100, y: 110}, {x: 160, y: 110}, {x: 240, y: 110}];
const parents = (seed.lineage || []).map(id => inventory.find(s => s.id === id));
let grandparents = [];
parents.forEach(p => {
if (p && p.lineage) {
grandparents.push(inventory.find(s => s.id === p.lineage[0]));
grandparents.push(inventory.find(s => s.id === p.lineage[1]));
} else {
grandparents.push(null, null);
}
});
// Draw Lines
ctx.strokeStyle = '#39FF14';
ctx.lineWidth = 2;
ctx.beginPath();
if (grandparents[0]) { ctx.moveTo(parentCoords[0].x + 24, parentCoords[0].y + 48); ctx.lineTo(gpCoords[0].x + 16, gpCoords[0].y); }
if (grandparents[1]) { ctx.moveTo(parentCoords[0].x + 24, parentCoords[0].y + 48); ctx.lineTo(gpCoords[1].x + 16, gpCoords[1].y); }
if (grandparents[2]) { ctx.moveTo(parentCoords[1].x + 24, parentCoords[1].y + 48); ctx.lineTo(gpCoords[2].x + 16, gpCoords[2].y); }
if (grandparents[3]) { ctx.moveTo(parentCoords[1].x + 24, parentCoords[1].y + 48); ctx.lineTo(gpCoords[3].x + 16, gpCoords[3].y); }
ctx.stroke();
// Draw Images
const drawImg = (s, coords, size) => {
if (!s) return;
const img = new Image();
img.src = `/api/image_mature/${s.id}/${size}?sid=${window.WEEDSIM_SID}`;
img.onload = () => ctx.drawImage(img, coords.x, coords.y, size, size);
};
parents.forEach((p, i) => drawImg(p, parentCoords[i], 48));
grandparents.forEach((gp, i) => drawImg(gp, gpCoords[i], 32));
}
});