๐Ÿงช Sample Tracking Registry

Track bioink batches, tissue samples, and printed constructs through their lifecycle

๐Ÿ”

Select a sample from the Registry to view details

Export & Import

`n`); win.document.close(); } function importData() { const file = document.getElementById('import-file').files[0]; if (!file) return alert('Select a JSON file first'); const reader = new FileReader(); reader.onload = (e) => { try { const imported = JSON.parse(e.target.result); if (imported.samples && Array.isArray(imported.samples)) { const count = imported.samples.length; imported.samples.forEach(s => { if (!state.samples.find(x=>x.id===s.id)) state.samples.push(s); }); state.nextId = Math.max(state.nextId, imported.nextId || 0); saveState(); renderAll(); alert(`Imported ${count} samples (duplicates skipped)`); } else { alert('Invalid file format'); } } catch(err) { alert('Failed to parse: ' + err.message); } }; reader.readAsText(file); } function clearAllData() { if (!confirm('Delete ALL samples? This cannot be undone.')) return; state = { samples: [], nextId: 1 }; saveState(); renderAll(); } /* โ”€โ”€ Demo Data โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ function loadDemoData() { if (state.samples.length && !confirm('This will add demo samples. Continue?')) return; const now = Date.now(); const h = 3600000; const demo = [ { id:'BIO-0001', type:'bioink', name:'GelMA 5% Batch 12', material:'GelMA 5% w/v', source:'Lab A Synthesis', volume:'25 mL', storage:'4ยฐC dark', operator:'Dr. Chen', parentId:'', tags:['gelma','validated'], notes:'Photo-crosslinkable, LAP initiator 0.05%', stage:'loaded', events:[ { type:'stage-change', stage:'prepared', description:'Batch synthesized and filtered', operator:'Dr. Chen', timestamp: new Date(now-48*h).toISOString() }, { type:'measurement', description:'Viscosity: 0.45 Paยทs at 25ยฐC, pH 7.2', operator:'Dr. Chen', timestamp: new Date(now-47*h).toISOString() }, { type:'stage-change', stage:'loaded', previousStage:'prepared', description:'Loaded into cartridge #3', operator:'Tech Kim', timestamp: new Date(now-24*h).toISOString() }, ]}, { id:'BIO-0002', type:'bioink', name:'Alginate-CaClโ‚‚ 2%', material:'Sodium alginate 2% w/v', source:'Sigma-Aldrich lot #A2033', volume:'50 mL', storage:'Room temp', operator:'Dr. Patel', parentId:'', tags:['alginate','stock'], notes:'Crosslinker: 100mM CaClโ‚‚', stage:'prepared', events:[ { type:'stage-change', stage:'prepared', description:'Stock solution prepared', operator:'Dr. Patel', timestamp: new Date(now-72*h).toISOString() }, ]}, { id:'TIS-0001', type:'tissue', name:'hMSC Passage 4', material:'Human mesenchymal stem cells', source:'ATCC PCS-500-012', volume:'2ร—10โถ cells', storage:'37ยฐC 5% COโ‚‚', operator:'Dr. Chen', parentId:'', tags:['hmsc','p4','expansion'], notes:'Doubling time ~36h, viability >95%', stage:'maturation', events:[ { type:'stage-change', stage:'prepared', description:'Thawed from cryostock', operator:'Dr. Chen', timestamp: new Date(now-120*h).toISOString() }, { type:'stage-change', stage:'maturation', previousStage:'prepared', description:'Seeded in T-75 flask for expansion', operator:'Dr. Chen', timestamp: new Date(now-96*h).toISOString() }, { type:'observation', description:'Confluency ~60%, healthy morphology', operator:'Tech Kim', timestamp: new Date(now-48*h).toISOString() }, { type:'measurement', description:'Cell count: 1.8ร—10โถ, viability 97%', operator:'Dr. Chen', timestamp: new Date(now-12*h).toISOString() }, ]}, { id:'CON-0001', type:'construct', name:'Cartilage Disc v3', material:'GelMA 5% + hMSC', source:'Print Run #47', volume:'ร˜8mm ร— 2mm', storage:'37ยฐC incubator', operator:'Dr. Patel', parentId:'BIO-0001', tags:['cartilage','study-7'], notes:'3-layer construct, 200ยตm nozzle', stage:'postprocess', events:[ { type:'stage-change', stage:'prepared', description:'Print file loaded, parameters set', operator:'Dr. Patel', timestamp: new Date(now-20*h).toISOString() }, { type:'stage-change', stage:'printing', previousStage:'prepared', description:'Printing started, pressure 25 kPa, speed 10 mm/s', operator:'Dr. Patel', timestamp: new Date(now-19*h).toISOString() }, { type:'observation', description:'Layer adhesion good, no clogging', operator:'Dr. Patel', timestamp: new Date(now-18.5*h).toISOString() }, { type:'stage-change', stage:'postprocess', previousStage:'printing', description:'UV crosslink 30s, transferred to media', operator:'Dr. Patel', timestamp: new Date(now-18*h).toISOString() }, ]}, { id:'SCA-0001', type:'scaffold', name:'PCL Mesh Template', material:'Polycaprolactone', source:'3D Systems FDM', volume:'20ร—20ร—5 mm', storage:'Room temp, sealed', operator:'Tech Kim', parentId:'', tags:['pcl','template'], notes:'400ยตm pore size, 0/90ยฐ lay pattern', stage:'archived', events:[ { type:'stage-change', stage:'prepared', description:'FDM printed at 65ยฐC', operator:'Tech Kim', timestamp: new Date(now-168*h).toISOString() }, { type:'stage-change', stage:'analysis', previousStage:'prepared', description:'SEM imaging and mechanical testing', operator:'Dr. Chen', timestamp: new Date(now-144*h).toISOString() }, { type:'measurement', description:'Compressive modulus: 12.3 MPa, porosity 62%', operator:'Dr. Chen', timestamp: new Date(now-140*h).toISOString() }, { type:'stage-change', stage:'archived', previousStage:'analysis', description:'Template validated, stored for future use', operator:'Tech Kim', timestamp: new Date(now-120*h).toISOString() }, ]}, ]; demo.forEach(s => { s.createdAt = s.events[0].timestamp; s.updatedAt = s.events[s.events.length-1].timestamp; if (!state.samples.find(x=>x.id===s.id)) state.samples.push(s); }); state.nextId = Math.max(state.nextId, 6); saveState(); renderAll(); } /* โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ function timeAgo(iso) { const diff = Date.now() - new Date(iso).getTime(); const mins = Math.floor(diff/60000); if (mins < 1) return 'just now'; if (mins < 60) return mins + 'm ago'; const hrs = Math.floor(mins/60); if (hrs < 24) return hrs + 'h ago'; const days = Math.floor(hrs/24); return days + 'd ago'; } function formatDuration(ms) { const mins = Math.floor(ms/60000); if (mins < 60) return mins + ' min'; const hrs = Math.floor(mins/60); if (hrs < 24) return hrs + 'h ' + (mins%60) + 'm'; const days = Math.floor(hrs/24); return days + 'd ' + (hrs%24) + 'h'; } function dateStr() { return new Date().toISOString().slice(0,10); } function downloadBlob(blob, name) { const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = name; a.click(); URL.revokeObjectURL(a.href); } function renderAll() { renderStats(); renderRegistry(); } /* โ”€โ”€ Init โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ renderAll(); `n