SIMON Master Console Rebuild
True command center for SIMON + WEB360
This rebuild gives you a cleaner production spine: interactive tree, live viewer, guarded editor, dynamic graphics, architecture map, and note storage. It is designed to sit beside your existing
console.php
and become the stronger replacement path.
Dashboard
Tree
Viewer
System Map
File Registry
Notes
Files
15,279
Folders
408
Scanned Size
3.84 GB
PHP Files
891
Editable Text Files
12,540
File viewer
guarded to /htdocs
/infinity/marketplace.php
<?php declare(strict_types=1); /** * /the_hall/webpage.php — THE HALL (Interactive Webpage / Front-End) * * What this is: * - A single-file, interactive front-end page for THE HALL concept * - Demo registry runs in-browser (localStorage) so it works on any host * - Includes: search, add record, verification workflow, “unaccounted” queue, * tombstone image upload (stored as data URLs in localStorage), map pinning, JSON export/import * * What this is NOT: * - Not the private/admin SQLite registry (your /the_hall/index.php file does that) * - This is a front door / prototype / UI layer you can later connect to your PHP API. * * Install: * - Put as: /the_hall/webpage.php * - Visit in browser */ ?><!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" /> <title>THE HALL — Central Disposition Registry</title> <meta name="theme-color" content="#070b16" /> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> <style> :root{ --bg:#070b16; --panel:rgba(255,255,255,.06); --edge:rgba(255,255,255,.12); --txt:#eaf2ff; --mut:#a9b6d9; --acc:#6ae0ff; --acc2:#ff4fd6; --ok:#44d18a; --warn:#ffd36a; --bad:#ff6b6b; --r:18px; } *{box-sizing:border-box} html,body{height:100%} body{ margin:0; font:14px/1.55 system-ui,-apple-system,Segoe UI,Roboto,Arial; color:var(--txt); background: radial-gradient(1200px 700px at 70% 10%, rgba(106,224,255,.10), transparent 60%), radial-gradient(900px 650px at 15% 90%, rgba(255,79,214,.08), transparent 62%), linear-gradient(180deg, #04060f, var(--bg)); overflow-x:hidden; } a{color:rgba(106,224,255,.95); text-decoration:none} a:hover{text-decoration:underline} .wrap{max-width:1200px; margin:0 auto; padding:18px 14px 60px} .top{ display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; margin-bottom:12px; } .brand{display:flex; align-items:center; gap:12px} .mark{ width:44px;height:44px;border-radius:16px;display:grid;place-items:center; background: radial-gradient(circle at 30% 30%, rgba(255,255,255,.18), rgba(255,255,255,.06) 55%, rgba(255,255,255,.02) 80%); border:1px solid rgba(255,255,255,.14); box-shadow: 0 18px 50px rgba(0,0,0,.45), 0 0 26px rgba(106,224,255,.12); position:relative; } .mark:before{content:"H"; font-weight:1000; letter-spacing:.02em} h1{margin:0; font-size:18px; letter-spacing:.22em; text-transform:uppercase} .mut{color:var(--mut)} .nav{display:flex; gap:8px; flex-wrap:wrap} .btn{ display:inline-flex; align-items:center; justify-content:center; gap:8px; padding:9px 11px; border-radius:13px; border:1px solid rgba(255,255,255,.14); background: rgba(255,255,255,.05); color:var(--txt); font-weight:900; font-size:12px; cursor:pointer; user-select:none; } .btn:hover{border-color:rgba(106,224,255,.38)} .btn.primary{background:rgba(106,224,255,.12); border-color:rgba(106,224,255,.35)} .btn.danger{background:rgba(255,107,107,.10); border-color:rgba(255,107,107,.28)} .btn.ghost{background:transparent} .grid{display:grid; gap:12px; grid-template-columns: 1.05fr .95fr} @media (max-width: 980px){.grid{grid-template-columns:1fr}} .card{ border:1px solid rgba(255,255,255,.10); background: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.03)); border-radius:var(--r); padding:14px; box-shadow:0 18px 60px rgba(0,0,0,.35); backdrop-filter: blur(10px); } .row{display:flex; gap:10px; flex-wrap:wrap; align-items:center; justify-content:space-between} .pill{ display:inline-flex; gap:8px; align-items:center; padding:6px 10px; border-radius:999px; border:1px solid rgba(255,255,255,.12); background:rgba(0,0,0,.20); color:var(--mut); font-size:12px; } .pill.ok{border-color:rgba(68,209,138,.22); color:rgba(196,255,224,.92)} .pill.warn{border-color:rgba(255,211,106,.22); color:rgba(255,238,200,.92)} .pill.bad{border-color:rgba(255,107,107,.22); color:rgba(255,210,210,.92)} .two{display:grid; grid-template-columns:1fr 1fr; gap:10px} @media (max-width: 760px){.two{grid-template-columns:1fr}} input,select,textarea{ width:100%; padding:10px 11px; border-radius:12px; border:1px solid rgba(255,255,255,.12); background:rgba(0,0,0,.25); color:var(--txt); outline:none; } textarea{min-height:92px; resize:vertical} label{display:block; font-size:12px; color:rgba(169,182,217,.92); margin:8px 0 6px} table{width:100%; border-collapse:collapse} th,td{padding:9px 8px; border-bottom:1px solid rgba(255,255,255,.08); vertical-align:top} th{color:rgba(169,182,217,.92); font-size:12px; text-align:left; letter-spacing:.08em; text-transform:uppercase} .k{color:rgba(169,182,217,.85); font-size:12px} .small{font-size:12px} .sectionTitle{ margin:12px 0 6px; color:rgba(234,242,255,.72); font-size:12px; letter-spacing:.18em; text-transform:uppercase; } .hint{color:rgba(169,182,217,.92); font-size:12px} .mono{font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;} .split{ display:grid; grid-template-columns: 1fr; gap:10px; } .tabs{display:flex; gap:8px; flex-wrap:wrap; margin-top:10px} .tab{padding:8px 10px; border-radius:999px; border:1px solid rgba(255,255,255,.12); background:rgba(0,0,0,.18); cursor:pointer; font-weight:900; font-size:12px; color:var(--mut)} .tab.active{border-color:rgba(106,224,255,.38); color:var(--txt); background:rgba(106,224,255,.10)} .list{display:grid; gap:10px; margin-top:10px} .item{ border:1px solid rgba(255,255,255,.10); border-radius:16px; background:rgba(0,0,0,.18); padding:12px; } .item h3{margin:0 0 6px; font-size:14px} .thumb{width:100%; max-height:220px; object-fit:cover; border-radius:14px; border:1px solid rgba(255,255,255,.12); margin-top:10px} .notice{ border:1px solid rgba(255,255,255,.12); background:rgba(0,0,0,.18); border-radius:16px; padding:12px; } .hr{height:1px; background:rgba(255,255,255,.08); margin:12px 0} </style> </head> <body> <div class="wrap"> <div class="top"> <div class="brand"> <div class="mark" aria-hidden="true"></div> <div> <h1>THE HALL</h1> <div class="mut small">Central Disposition + Verification Registry (Burial / Cremation / Unknown)</div> </div> </div> <div class="nav"> <button class="btn" data-go="dashboard">Dashboard</button> <button class="btn" data-go="add">Add Record</button> <button class="btn" data-go="unaccounted">Unaccounted</button> <button class="btn" data-go="export">Export/Import</button> <button class="btn danger" id="resetBtn">Reset Demo Data</button> </div> </div> <div class="grid"> <div class="card" id="left"> <div class="row"> <div> <div class="pill">Mode: <strong>Interactive Demo UI</strong></div> <div class="mut small">Data stays in your browser (localStorage). Later this UI can be wired to your private PHP/SQLite registry.</div> </div> <div class="row" style="justify-content:flex-end"> <span class="pill ok" id="countPeople">People: 0</span> <span class="pill" id="countRecords">Records: 0</span> <span class="pill ok" id="countVerified">Verified: 0</span> <span class="pill bad" id="countFlagged">Flagged: 0</span> </div> </div> <div class="sectionTitle">Search</div> <div class="two"> <input id="q" placeholder="Search name / alias / cemetery / crematory / external ref..." /> <div style="display:flex; gap:8px; justify-content:flex-end"> <button class="btn primary" id="searchBtn">Search</button> <button class="btn ghost" id="clearBtn">Clear</button> </div> </div> <div class="tabs"> <div class="tab active" data-tab="dashboard">Dashboard</div> <div class="tab" data-tab="add">Add Record</div> <div class="tab" data-tab="unaccounted">Unaccounted</div> <div class="tab" data-tab="export">Export/Import</div> </div> <!-- DASHBOARD --> <div id="tab-dashboard" class="split"> <div class="sectionTitle">Recent updates</div> <div id="recent" class="list"></div> </div> <!-- ADD --> <div id="tab-add" class="split" style="display:none"> <div class="sectionTitle">Add person + disposition record</div> <div class="notice"> <div class="mut small"> Evidence-first fields. “DNA” is a reference ID/hash (case # / lab ref). Coordinates are private. Tombstone image is optional. </div> </div> <div class="two"> <div> <label>Full name *</label> <input id="full_name" required placeholder="e.g., Louie Sinclair" /> </div> <div> <label>Alias (optional)</label> <input id="alias" placeholder="Nickname / alternate name" /> </div> </div> <div class="two"> <div> <label>Date of birth</label> <input id="dob" placeholder="YYYY-MM-DD" /> </div> <div> <label>Date of death</label> <input id="dod" placeholder="YYYY-MM-DD" /> </div> </div> <div class="two"> <div> <label>DNA Ref (ID / hash only)</label> <input id="dna_ref" placeholder="LabCase# / SHA256:..." /> </div> <div> <label>Life Network Ref (internal)</label> <input id="life_ref" placeholder="Internal network ID" /> </div> </div> <div class="two"> <div> <label>Disposition type</label> <select id="dtype"> <option value="burial">Burial</option> <option value="cremation">Cremation</option> <option value="unknown">Unknown / Unaccounted</option> </select> </div> <div> <label>Verification status</label> <select id="status"> <option value="unverified">Unverified</option> <option value="verified">Verified</option> <option value="disputed">Disputed</option> </select> </div> </div> <div class="sectionTitle">Burial fields</div> <div class="two"> <div><label>Cemetery name</label><input id="cem_name" /></div> <div><label>City / State</label><input id="cem_city_state" placeholder="Dallas, TX" /></div> </div> <div class="two"> <div><label>Section / Lot / Grave</label><input id="cem_plot" placeholder="Section B / Lot 214 / Grave 3" /></div> <div><label>Plot label (human ID)</label><input id="plot_label" placeholder="DAL-0007 / B / 214 / 3" /></div> </div> <div class="sectionTitle">Cremation fields</div> <div class="two"> <div><label>Crematory name</label><input id="crem_name" /></div> <div><label>City / State</label><input id="crem_city_state" placeholder="Dallas, TX" /></div> </div> <label>Urn location</label> <input id="urn_location" placeholder="Vault / family custody / niche / etc." /> <div class="sectionTitle">Coordinates (private)</div> <div class="two"> <div><label>Latitude</label><input id="lat" placeholder="Click map to fill" /></div> <div><label>Longitude</label><input id="lon" placeholder="Click map to fill" /></div> </div> <div class="sectionTitle">Evidence + references</div> <label>Source summary</label> <input id="source_summary" placeholder="Cemetery ledger, permit #, obituary, family record, etc." /> <label>External reference</label> <input id="external_ref" placeholder="Certificate/permit/ledger ID" /> <label>Notes</label> <textarea id="notes" placeholder="Anything important for later verification…"></textarea> <div class="sectionTitle">Tombstone image (optional)</div> <input id="tombstone" type="file" accept="image/*" /> <div class="row" style="margin-top:10px"> <div class="hint">Click “Save Record” to create a person + a disposition entry.</div> <div style="display:flex; gap:8px"> <button class="btn primary" id="saveBtn">Save Record</button> <button class="btn" id="demoFillBtn">Fill Demo Example</button> </div> </div> </div> <!-- UNACCOUNTED --> <div id="tab-unaccounted" class="split" style="display:none"> <div class="sectionTitle">Unaccounted / Needs verification</div> <div class="notice"> <div class="mut small"> Flag rules: unknown type, disputed, missing plot info for burials, missing urn info for cremations, or missing linkage fields. </div> </div> <div id="unacc" class="list"></div> </div> <!-- EXPORT --> <div id="tab-export" class="split" style="display:none"> <div class="sectionTitle">Export / Import JSON</div> <div class="notice"> <div class="mut small">Use this to move data between browsers or prepare for server import.</div> </div> <div class="two"> <button class="btn primary" id="exportBtn">Generate Export JSON</button> <button class="btn" id="downloadBtn">Download JSON</button> </div> <label>Export JSON</label> <textarea id="jsonBox" class="mono" placeholder="Click Generate Export JSON"></textarea> <div class="two"> <button class="btn primary" id="importBtn">Import JSON</button> <button class="btn danger" id="wipeBtn">Wipe Local Data</button> </div> </div> </div> <!-- RIGHT PANEL --> <div class="card" id="right"> <div class="row"> <div> <h2 style="margin:0 0 6px; font-size:16px">Pinpoint Map (Private)</h2> <div class="mut small">Click map to set coordinates. This is for recordkeeping & verification.</div> </div> <div class="right" style="display:flex; gap:8px"> <button class="btn" id="centerBtn">Center</button> <button class="btn" id="clearPinBtn">Clear Pin</button> </div> </div> <div id="map" style="height:430px; border-radius:14px; border:1px solid rgba(255,255,255,.10); overflow:hidden; margin-top:10px"></div> <div class="hr"></div> <div class="sectionTitle">Selected record</div> <div id="selected" class="mut small">None selected.</div> <div class="hr"></div> <div class="sectionTitle">Reality guardrails</div> <div class="notice"> <div class="mut small"> THE HALL is an evidence ledger: identity references, official record pointers, and verified location metadata. Keep it private and authorized. </div> </div> </div> </div> </div> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script> (() => { const LS_KEY = "the_hall_demo_v1"; const el = (id) => document.getElementById(id); const state = { data: { people: [], records: [] }, selectedId: null, map: null, marker: null }; // ---------- Utilities ---------- const uid = () => Math.random().toString(16).slice(2) + Date.now().toString(16); const nowUTC = () => new Date().toISOString(); function pillClass(status){ if(status === "verified") return "ok"; if(status === "disputed") return "bad"; return "warn"; } function save(){ localStorage.setItem(LS_KEY, JSON.stringify(state.data)); } function load(){ const raw = localStorage.getItem(LS_KEY); if(raw){ try { state.data = JSON.parse(raw) || {people:[],records:[]}; } catch { state.data = {people:[],records:[]}; } } else { seed(); } } function seed(){ const p1 = { id: uid(), full_name:"Louie Sinclair", alias:"Louie", dob:"2014-06-01", dod:"2024-02-14", dna_ref:"LabCase#LS-10491", life_ref:"LN-LOUIE-0001", created_utc:nowUTC(), updated_utc:nowUTC() }; const r1 = { id: uid(), person_id:p1.id, type:"burial", status:"unverified", cemetery_name:"Evergreen Memorial", cemetery_city_state:"Dallas, TX", plot_label:"DAL-0007 / B / 214 / 3", cemetery_plot:"Section B / Lot 214 / Grave 3", crematory_name:"", crem_city_state:"", urn_location:"", lat: 32.7767, lon: -96.7970, source_summary:"Family record + cemetery map photo", external_ref:"Permit#TX-88421", notes:"Needs cemetery ledger confirmation.", tombstone_dataurl:"", created_utc:nowUTC(), updated_utc:nowUTC() }; const p2 = { id: uid(), full_name:"Unknown (John Doe)", alias:"JD-041", dob:"", dod:"", dna_ref:"", life_ref:"", created_utc:nowUTC(), updated_utc:nowUTC() }; const r2 = { id: uid(), person_id:p2.id, type:"unknown", status:"disputed", cemetery_name:"", cemetery_city_state:"", plot_label:"", cemetery_plot:"", crematory_name:"", crem_city_state:"", urn_location:"", lat: null, lon: null, source_summary:"Unmatched cremation log entry", external_ref:"CremLog#2219", notes:"Needs identity linkage + location resolution.", tombstone_dataurl:"", created_utc:nowUTC(), updated_utc:nowUTC() }; state.data.people = [p1,p2]; state.data.records = [r1,r2]; save(); } function counts(){ const people = state.data.people.length; const records = state.data.records.length; const verified = state.data.records.filter(r => r.status === "verified").length; const flagged = computeUnaccounted().length; el("countPeople").textContent = `People: ${people}`; el("countRecords").textContent = `Records: ${records}`; el("countVerified").textContent = `Verified: ${verified}`; el("countFlagged").textContent = `Flagged: ${flagged}`; el("countPeople").className = "pill ok"; el("countRecords").className = "pill"; el("countVerified").className = "pill ok"; el("countFlagged").className = "pill bad"; } function findPerson(pid){ return state.data.people.find(p => p.id === pid) || null; } function computeUnaccounted(){ return state.data.records.filter(r => { if(r.type === "unknown") return true; if(r.status === "disputed") return true; const person = findPerson(r.person_id); if(!person) return true; if(r.type === "burial"){ const missingCem = !String(r.cemetery_name||"").trim(); const missingPlot = !String(r.plot_label||"").trim() && !String(r.cemetery_plot||"").trim(); return missingCem || missingPlot; } if(r.type === "cremation"){ const missingCrem = !String(r.crematory_name||"").trim(); const missingUrn = !String(r.urn_location||"").trim(); return missingCrem || missingUrn; } return false; }).sort((a,b)=> (b.updated_utc||"").localeCompare(a.updated_utc||"")); } function renderRecent(list){ const box = el(list === "unacc" ? "unacc" : "recent"); box.innerHTML = ""; const q = el("q").value.trim().toLowerCase(); let rows = (list === "unacc") ? computeUnaccounted() : [...state.data.records].sort((a,b)=> (b.updated_utc||"").localeCompare(a.updated_utc||"")); if(q){ rows = rows.filter(r => { const p = findPerson(r.person_id); const blob = [ p?.full_name, p?.alias, r.type, r.status, r.cemetery_name, r.cemetery_city_state, r.plot_label, r.cemetery_plot, r.crematory_name, r.crem_city_state, r.urn_location, r.external_ref, r.source_summary, r.notes ].filter(Boolean).join(" ").toLowerCase(); return blob.includes(q); }); } if(!rows.length){ box.innerHTML = `<div class="mut">No results.</div>`; return; } for(const r of rows.slice(0, 120)){ const p = findPerson(r.person_id); const title = p ? p.full_name : "Unlinked / Unknown"; const sub = [ `Type: ${r.type}`, `Status: ${r.status}`, r.type === "burial" ? (r.cemetery_name ? `Cemetery: ${r.cemetery_name}` : "Cemetery: —") : r.type === "cremation" ? (r.crematory_name ? `Crematory: ${r.crematory_name}` : "Crematory: —") : "Unknown" ].join(" · "); const loc = (r.lat != null && r.lon != null) ? `${Number(r.lat).toFixed(5)}, ${Number(r.lon).toFixed(5)}` : "—"; const hasImg = !!r.tombstone_dataurl; box.insertAdjacentHTML("beforeend", ` <div class="item"> <div class="row"> <div> <h3>${escapeHtml(title)}</h3> <div class="mut small">${escapeHtml(sub)}</div> </div> <div style="display:flex; gap:8px; flex-wrap:wrap; justify-content:flex-end"> <span class="pill ${pillClass(r.status)}">${escapeHtml(r.status)}</span> <button class="btn" data-open="${r.id}">Open</button> <button class="btn" data-verify="${r.id}">Verify</button> <button class="btn danger" data-del="${r.id}">Delete</button> </div> </div> <div class="two" style="margin-top:10px"> <div><div class="k">External ref</div><div class="mut small">${escapeHtml(r.external_ref || "—")}</div></div> <div><div class="k">Coordinates</div><div class="mut small">${escapeHtml(loc)}</div></div> </div> ${hasImg ? `<img class="thumb" src="${r.tombstone_dataurl}" alt="Tombstone" />` : ``} </div> `); } box.querySelectorAll("[data-open]").forEach(b => b.addEventListener("click", () => openRecord(b.dataset.open))); box.querySelectorAll("[data-verify]").forEach(b => b.addEventListener("click", () => quickVerify(b.dataset.verify))); box.querySelectorAll("[data-del]").forEach(b => b.addEventListener("click", () => deleteRecord(b.dataset.del))); } function openRecord(rid){ const r = state.data.records.find(x => x.id === rid); if(!r) return; state.selectedId = rid; const p = findPerson(r.person_id); const name = p ? `${p.full_name}${p.alias ? ` (${p.alias})` : ""}` : "Unlinked / Unknown"; const coord = (r.lat != null && r.lon != null) ? `${Number(r.lat).toFixed(6)}, ${Number(r.lon).toFixed(6)}` : "—"; el("selected").innerHTML = ` <div class="pill">Person: <strong>${escapeHtml(name)}</strong></div> <div style="height:8px"></div> <div class="two"> <div><div class="k">Type</div><div>${escapeHtml(r.type)}</div></div> <div><div class="k">Status</div><div><span class="pill ${pillClass(r.status)}">${escapeHtml(r.status)}</span></div></div> </div> <div class="two" style="margin-top:10px"> <div><div class="k">Coordinates</div><div class="mut small">${escapeHtml(coord)}</div></div> <div><div class="k">Updated</div><div class="mut small">${escapeHtml(r.updated_utc || "—")}</div></div> </div> <div style="margin-top:10px" class="mut small"> <div class="k">Source</div>${escapeHtml(r.source_summary || "—")} </div> <div style="margin-top:10px" class="mut small"> <div class="k">Notes</div>${escapeHtml(r.notes || "—")} </div> `; // Move map if(r.lat != null && r.lon != null){ setMapPin(r.lat, r.lon, true); } } function quickVerify(rid){ const r = state.data.records.find(x => x.id === rid); if(!r) return; r.status = (r.status === "verified") ? "unverified" : "verified"; r.updated_utc = nowUTC(); save(); counts(); renderAll(); if(state.selectedId === rid) openRecord(rid); } function deleteRecord(rid){ const r = state.data.records.find(x => x.id === rid); if(!r) return; if(!confirm("Delete this record (demo data)?")) return; state.data.records = state.data.records.filter(x => x.id !== rid); // Optionally remove orphan people const used = new Set(state.data.records.map(x => x.person_id)); state.data.people = state.data.people.filter(p => used.has(p.id)); if(state.selectedId === rid){ state.selectedId = null; el("selected").textContent = "None selected."; clearMapPin(); } save(); counts(); renderAll(); } // ---------- Tabs ---------- function setTab(name){ document.querySelectorAll(".tab").forEach(t => t.classList.toggle("active", t.dataset.tab === name)); ["dashboard","add","unaccounted","export"].forEach(n => { el("tab-" + n).style.display = (n === name) ? "" : "none"; }); if(name === "dashboard") renderRecent("recent"); if(name === "unaccounted") renderRecent("unacc"); } // ---------- Map ---------- function initMap(){ state.map = L.map("map").setView([32.7767,-96.7970], 10); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, attribution: "© OpenStreetMap" }).addTo(state.map); state.map.on("click", (e) => { const lat = e.latlng.lat; const lon = e.latlng.lng; el("lat").value = lat.toFixed(6); el("lon").value = lon.toFixed(6); setMapPin(lat, lon, false); }); } function setMapPin(lat, lon, pan){ if(state.marker) state.map.removeLayer(state.marker); state.marker = L.marker([lat, lon]).addTo(state.map); if(pan) state.map.setView([lat, lon], Math.max(state.map.getZoom(), 16)); } function clearMapPin(){ if(state.marker){ state.map.removeLayer(state.marker); state.marker = null; } } // ---------- Add record ---------- async function fileToDataURL(file){ return new Promise((resolve, reject) => { const fr = new FileReader(); fr.onload = () => resolve(String(fr.result || "")); fr.onerror = reject; fr.readAsDataURL(file); }); } function splitCityState(s){ s = String(s||"").trim(); if(!s) return {city:"", state:""}; // Keep as single string for demo. return s; } async function saveNew(){ const full_name = el("full_name").value.trim(); if(!full_name){ alert("Full name is required."); return; } const p = { id: uid(), full_name, alias: el("alias").value.trim(), dob: el("dob").value.trim(), dod: el("dod").value.trim(), dna_ref: el("dna_ref").value.trim(), life_ref: el("life_ref").value.trim(), created_utc: nowUTC(), updated_utc: nowUTC() }; let tomb = ""; const f = el("tombstone").files && el("tombstone").files[0]; if(f){ try { tomb = await fileToDataURL(f); } catch { tomb = ""; } } const r = { id: uid(), person_id: p.id, type: el("dtype").value, status: el("status").value, cemetery_name: el("cem_name").value.trim(), cemetery_city_state: el("cem_city_state").value.trim(), cemetery_plot: el("cem_plot").value.trim(), plot_label: el("plot_label").value.trim(), crematory_name: el("crem_name").value.trim(), crem_city_state: el("crem_city_state").value.trim(), urn_location: el("urn_location").value.trim(), lat: el("lat").value.trim() ? Number(el("lat").value.trim()) : null, lon: el("lon").value.trim() ? Number(el("lon").value.trim()) : null, source_summary: el("source_summary").value.trim(), external_ref: el("external_ref").value.trim(), notes: el("notes").value.trim(), tombstone_dataurl: tomb, created_utc: nowUTC(), updated_utc: nowUTC() }; state.data.people.unshift(p); state.data.records.unshift(r); save(); counts(); renderAll(); openRecord(r.id); setTab("dashboard"); clearAddForm(); } function clearAddForm(){ ["full_name","alias","dob","dod","dna_ref","life_ref","cem_name","cem_city_state","cem_plot","plot_label", "crem_name","crem_city_state","urn_location","lat","lon","source_summary","external_ref","notes"].forEach(id => el(id).value = ""); el("dtype").value = "burial"; el("status").value = "unverified"; el("tombstone").value = ""; } function demoFill(){ el("full_name").value = "Bobby Darrell Saxon"; el("alias").value = "BDS"; el("dob").value = "1958-01-12"; el("dod").value = "2021-09-01"; el("dna_ref").value = "SHA256:demo_hash_9f1a..."; el("life_ref").value = "LN-BDS-0021"; el("dtype").value = "burial"; el("status").value = "unverified"; el("cem_name").value = "Example Cemetery"; el("cem_city_state").value = "Dallas, TX"; el("cem_plot").value = "Section C / Lot 12 / Grave 4"; el("plot_label").value = "DAL-EX-0012-C-4"; el("source_summary").value = "Death certificate pointer + cemetery office note"; el("external_ref").value = "Cert#TX-2021-000123"; el("notes").value = "Awaiting ledger confirmation."; } // ---------- Export / Import ---------- function exportJSON(){ el("jsonBox").value = JSON.stringify(state.data, null, 2); } function downloadJSON(){ const text = el("jsonBox").value.trim() || JSON.stringify(state.data, null, 2); const blob = new Blob([text], {type:"application/json"}); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "the_hall_export.json"; document.body.appendChild(a); a.click(); a.remove(); setTimeout(()=> URL.revokeObjectURL(a.href), 4000); } function importJSON(){ const text = el("jsonBox").value.trim(); if(!text){ alert("Paste JSON first."); return; } try{ const obj = JSON.parse(text); if(!obj || typeof obj !== "object") throw new Error("bad"); if(!Array.isArray(obj.people) || !Array.isArray(obj.records)) throw new Error("missing arrays"); state.data = obj; save(); counts(); renderAll(); alert("Imported."); } catch(e){ alert("Import failed: invalid JSON shape."); } } // ---------- Render ---------- function renderAll(){ renderRecent("recent"); renderRecent("unacc"); } function escapeHtml(s){ return String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&":"&","<":"<",">":">",'"':""","'":"'" })[c]); } // ---------- Wire UI ---------- document.querySelectorAll(".tab").forEach(t => t.addEventListener("click", () => setTab(t.dataset.tab))); document.querySelectorAll("[data-go]").forEach(b => b.addEventListener("click", () => setTab(b.dataset.go))); el("searchBtn").addEventListener("click", () => renderAll()); el("clearBtn").addEventListener("click", () => { el("q").value=""; renderAll(); }); el("saveBtn").addEventListener("click", saveNew); el("demoFillBtn").addEventListener("click", demoFill); el("exportBtn").addEventListener("click", exportJSON); el("downloadBtn").addEventListener("click", downloadJSON); el("importBtn").addEventListener("click", importJSON); el("wipeBtn").addEventListener("click", () => { if(!confirm("Wipe local demo data?")) return; state.data = {people:[],records:[]}; save(); counts(); renderAll(); el("selected").textContent = "None selected."; clearMapPin(); }); el("resetBtn").addEventListener("click", () => { if(!confirm("Reset demo data back to seeded examples?")) return; localStorage.removeItem(LS_KEY); load(); counts(); renderAll(); el("selected").textContent = "None selected."; clearMapPin(); }); el("centerBtn").addEventListener("click", () => state.map.setView([32.7767,-96.7970], 10)); el("clearPinBtn").addEventListener("click", () => { clearMapPin(); el("lat").value=""; el("lon").value=""; }); // ---------- Boot ---------- load(); counts(); initMap(); renderAll(); })(); </script> </body> </html>
Save file
Quick jump
open a path
Open