The STEM Lab

Equipment Tracker

Connect to Google Sheets for live sync, or use demo mode to explore the app.

THE STEM LAB
FUTURE READY
โ€“

Equipment Tracker

Summer 2026 ยท 0 out
0
Available
0
Checked Out
0
Missing
0
Benched
0
Still Out
0
Returned
0
Missing
0
Benched
Filter
Records
Who & Where
Equipment & Asset IDs

Tap equipment to expand. Select IDs from the list or type custom ones.

Add Unlisted Equipment
Notes
๐Ÿ“ก Google Sheets sync โ€” free, no backend needed. Takes ~10 minutes to set up.
Step 1 โ€” Create the Sheet
1

New Google Sheet

Go to sheets.google.com, create a sheet named "STEM Lab Tracker 2026".

2

Rename tab to "Records"

Right-click the tab โ†’ Rename โ†’ type: Records

3

Paste headers in Row 1

id instructor location checkoutDate equipment missingItems benchedItems status notes checkedOutAt returnedAt
Step 2 โ€” Apps Script
1

Open Apps Script

Extensions โ†’ Apps Script. Delete existing code.

2

Paste this script

// STEM Lab Equipment Tracker โ€” Apps Script // Paste this entire block into Extensions > Apps Script, then Deploy > New deployment > Web App // Execute as: Me | Access: Anyone const SHEET_NAME = 'Records'; function doGet(e) { try { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME); const data = sheet.getDataRange().getValues(); if (data.length < 2) return respond({ records: [] }); const headers = data[0].map(h => String(h).trim()); const records = data.slice(1).map(function(row) { var obj = {}; headers.forEach(function(h, i) { obj[h] = row[i]; }); // Parse JSON fields โ€” send as raw strings, let client parse // so we don't lose data if parsing fails here return obj; }); return respond({ records: records }); } catch(err) { return respond({ error: err.toString(), records: [] }); } } function doPost(e) { try { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME); const d = JSON.parse(e.postData.contents); if (d.action === 'checkout') { var customNote = ''; if (d.customEquipment && d.customEquipment.length > 0) { customNote = ' [CUSTOM:' + JSON.stringify(d.customEquipment) + ']'; } sheet.appendRow([ String(d.id), String(d.instructor || ''), String(d.location || ''), String(d.checkoutDate || ''), JSON.stringify(d.equipment || {}), JSON.stringify([]), JSON.stringify([]), 'out', String(d.notes || '') + customNote, String(d.checkedOutAt || new Date().toISOString()), '' ]); return respond({ ok: true, action: 'checkout' }); } if (d.action === 'return') { var rows = sheet.getDataRange().getValues(); for (var i = 1; i < rows.length; i++) { if (String(rows[i][0]) === String(d.id)) { sheet.getRange(i + 1, 6).setValue(JSON.stringify(d.missingItems || [])); sheet.getRange(i + 1, 7).setValue(JSON.stringify(d.benchedItems || [])); sheet.getRange(i + 1, 8).setValue(String(d.status || 'returned')); sheet.getRange(i + 1, 9).setValue(String(d.returnNotes || rows[i][8] || '')); sheet.getRange(i + 1, 11).setValue(String(d.returnedAt || new Date().toISOString())); return respond({ ok: true, action: 'return' }); } } return respond({ ok: false, error: 'ID not found' }); } return respond({ ok: false, error: 'Unknown action' }); } catch(err) { return respond({ ok: false, error: err.toString() }); } } function respond(obj) { return ContentService .createTextOutput(JSON.stringify(obj)) .setMimeType(ContentService.MimeType.JSON); }
3

Deploy as Web App

Deploy โ†’ New Deployment โ†’ Web App โ†’ Execute as: Me โ†’ Access: Anyone โ†’ Deploy. Copy the URL.

Step 3 โ€” Connect
Current URL: None
Deploy to Netlify
1

Free account at netlify.com

Sign up free โ€” no credit card.

2

Sites โ†’ Add new โ†’ Deploy manually

Drag this HTML file in โ†’ Rename to index.html โ†’ Deploy. Done.

3

Add to phone home screen

iPhone: Safari โ†’ Share โ†’ Add to Home Screen. Android: Chrome โ†’ menu โ†’ Add to Home Screen.