← back to Socrata Cre Viewer
socrata-cre-viewer: searchable/sortable grid over realestate.socrata_cre_prospects (pg-parameterized, sort+density, basic-auth)
bfc886fdb07b3b5fa4856fc1c4a25b8e4e23a5b5 · 2026-08-10 22:00:50 -0700 · steve
Files touched
A .gitignoreA package-lock.jsonA package.jsonA public/index.htmlA server.js
Diff
commit bfc886fdb07b3b5fa4856fc1c4a25b8e4e23a5b5
Author: steve <steve@designerwallcoverings.com>
Date: Mon Aug 10 22:00:50 2026 -0700
socrata-cre-viewer: searchable/sortable grid over realestate.socrata_cre_prospects (pg-parameterized, sort+density, basic-auth)
---
.gitignore | 4 ++
package-lock.json | 161 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
package.json | 13 +++++
public/index.html | 94 +++++++++++++++++++++++++++++++
server.js | 110 +++++++++++++++++++++++++++++++++++++
5 files changed, 382 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1bcdbdc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+node_modules/
+.env*
+*.log
+.DS_Store
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..7519d4d
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,161 @@
+{
+ "name": "socrata-cre-viewer",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "socrata-cre-viewer",
+ "version": "0.1.0",
+ "dependencies": {
+ "pg": "^8.23.0"
+ }
+ },
+ "node_modules/pg": {
+ "version": "8.23.0",
+ "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz",
+ "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-connection-string": "^2.14.0",
+ "pg-pool": "^3.14.0",
+ "pg-protocol": "^1.16.0",
+ "pg-types": "2.2.0",
+ "pgpass": "1.0.5"
+ },
+ "engines": {
+ "node": ">= 16.0.0"
+ },
+ "optionalDependencies": {
+ "pg-cloudflare": "^1.4.0"
+ },
+ "peerDependencies": {
+ "pg-native": ">=3.0.1"
+ },
+ "peerDependenciesMeta": {
+ "pg-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pg-cloudflare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
+ "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pg-connection-string": {
+ "version": "2.14.0",
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz",
+ "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==",
+ "license": "MIT"
+ },
+ "node_modules/pg-int8": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pg-pool": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
+ "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "pg": ">=8.0"
+ }
+ },
+ "node_modules/pg-protocol": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz",
+ "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==",
+ "license": "MIT"
+ },
+ "node_modules/pg-types": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "postgres-array": "~2.0.0",
+ "postgres-bytea": "~1.0.0",
+ "postgres-date": "~1.0.4",
+ "postgres-interval": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pgpass": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.1.0"
+ }
+ },
+ "node_modules/postgres-array": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postgres-bytea": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+ "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-date": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-interval": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..d8b19ad
--- /dev/null
+++ b/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "socrata-cre-viewer",
+ "version": "0.1.0",
+ "private": true,
+ "description": "Searchable/sortable viewer over realestate.socrata_cre_prospects (LA Socrata CRE prospect firms)",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js"
+ },
+ "dependencies": {
+ "pg": "^8.23.0"
+ }
+}
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..f350401
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,94 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>LA Socrata CRE Prospects</title>
+<style>
+ :root{--bg:#0e1116;--panel:#161b22;--line:#232b36;--ink:#e6edf3;--dim:#8b98a8;--accent:#c9793a;--chip:#1d2530;--rowh:34px;--fs:13px}
+ *{box-sizing:border-box}
+ body{margin:0;font:14px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:var(--bg);color:var(--ink)}
+ header{padding:14px 18px;border-bottom:1px solid var(--line);display:flex;align-items:baseline;gap:14px;flex-wrap:wrap}
+ header h1{font-size:16px;margin:0;font-weight:650}
+ header .sub{color:var(--dim);font-size:12px}
+ .bar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;padding:12px 18px;border-bottom:1px solid var(--line);position:sticky;top:0;background:var(--bg);z-index:5}
+ input,select{background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:7px;padding:7px 9px;font-size:13px}
+ input#q{min-width:300px}
+ .bar label{color:var(--dim);font-size:11px;text-transform:uppercase;letter-spacing:.4px;margin-right:-4px}
+ .density{display:flex;align-items:center;gap:7px;margin-left:auto}
+ .density input[type=range]{accent-color:var(--accent)}
+ .count{color:var(--dim);font-size:12px;white-space:nowrap}.count b{color:var(--ink)}
+ .wrap{overflow:auto;height:calc(100vh - 118px)}
+ table{border-collapse:collapse;width:100%;font-size:var(--fs)}
+ thead th{position:sticky;top:0;background:var(--panel);border-bottom:1px solid var(--line);text-align:left;padding:8px 10px;white-space:nowrap;cursor:pointer;user-select:none;font-weight:600;color:#cdd6e0}
+ thead th .ar{color:var(--accent);font-size:10px;margin-left:3px}
+ tbody td{border-bottom:1px solid var(--line);padding:0 10px;height:var(--rowh);white-space:nowrap;max-width:320px;overflow:hidden;text-overflow:ellipsis}
+ tbody tr:hover{background:#131922}
+ .role{display:inline-block;padding:1px 7px;border-radius:10px;background:var(--chip);color:#e0b48a;font-size:11px}
+ .src{font-size:11px;color:var(--dim)}
+ .num{text-align:right;font-variant-numeric:tabular-nums}
+ .when{color:var(--dim);font-size:11px}.muted{color:var(--dim)}
+ .pager{display:flex;gap:8px;align-items:center;padding:9px 18px;border-top:1px solid var(--line);background:var(--bg)}
+ .pager button{background:var(--panel);border:1px solid var(--line);color:var(--ink);border-radius:7px;padding:6px 12px;cursor:pointer}
+ .pager button:disabled{opacity:.4;cursor:default}
+ .empty{padding:40px;text-align:center;color:var(--dim)}
+</style>
+</head>
+<body>
+<header>
+ <h1>LA Socrata CRE Prospects</h1>
+ <span class="sub">Net-new firms from LA Active Businesses + LADBS permits · deduped vs the curated LABJ set</span>
+</header>
+<div class="bar">
+ <input id="q" placeholder="Search firm, contact, address, NAICS, work…" autocomplete="off">
+ <label>Role</label><select id="role"></select>
+ <label>Source</label><select id="source"></select>
+ <label>City</label><select id="city"></select>
+ <label>NAICS</label><select id="naics_desc"></select>
+ <span class="count" id="count">…</span>
+ <div class="density"><span class="muted" style="font-size:11px">Density</span><input type="range" id="density" min="0" max="4" step="1" value="1"></div>
+</div>
+<div class="wrap"><table><thead><tr id="head"></tr></thead><tbody id="rows"></tbody></table><div class="empty" id="empty" hidden>No matches.</div></div>
+<div class="pager">
+ <button id="prev">‹ Prev</button>
+ <span class="count">Page <b id="page">1</b> <span class="muted">of</span> <b id="pages">1</b></span>
+ <button id="next">Next ›</button><span class="muted" id="range"></span>
+</div>
+<script>
+const LS='socrata-cre-view';
+const COLS=[['firm_name','Firm'],['role','Role'],['contact_name','Contact'],['city','City'],['naics_desc','NAICS'],['permit_sub_type','Permit'],['valuation','Valuation'],['issue_date','Date'],['source','Source'],['first_seen','First seen']];
+const DENS=[['26px','12px'],['34px','13px'],['40px','14px'],['48px','15px'],['58px','16px']];
+const state=Object.assign({q:'',role:'',source:'',city:'',naics_desc:'',sort:'firm_name',dir:'asc',page:1,limit:50,density:1},JSON.parse(localStorage.getItem(LS)||'{}'));
+const save=()=>localStorage.setItem(LS,JSON.stringify(state));const $=id=>document.getElementById(id);
+function dens(){const[h,f]=DENS[state.density]||DENS[1];document.documentElement.style.setProperty('--rowh',h);document.documentElement.style.setProperty('--fs',f);$('density').value=state.density;}
+function esc(s){return(s==null?'':String(s)).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));}
+function fmt(s){if(!s)return'';try{return new Date(s).toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'});}catch{return s;}}
+function money(v){if(!v)return'<span class=muted>—</span>';const n=+String(v).replace(/[^0-9.]/g,'');return isNaN(n)||!n?'<span class=muted>—</span>':'$'+n.toLocaleString();}
+function head(){$('head').innerHTML=COLS.map(([k,l])=>`<th data-k="${k}">${l}${state.sort===k?`<span class="ar">${state.dir==='asc'?'▲':'▼'}</span>`:''}</th>`).join('');
+ $('head').querySelectorAll('th').forEach(th=>th.onclick=()=>{const k=th.dataset.k;if(state.sort===k)state.dir=state.dir==='asc'?'desc':'asc';else{state.sort=k;state.dir='asc';}state.page=1;save();load();});}
+function rows(d){$('empty').hidden=d.rows.length>0;$('rows').innerHTML=d.rows.map(r=>`<tr>
+ <td title="${esc(r.firm_name)}"><b>${esc(r.firm_name)}</b></td>
+ <td><span class="role">${esc(r.role)}</span></td>
+ <td>${esc(r.contact_name)||'<span class=muted>—</span>'}</td>
+ <td>${esc(r.city)}</td>
+ <td class="muted" title="${esc(r.naics_desc)}">${esc(r.naics_desc)||'—'}</td>
+ <td class="muted">${esc(r.permit_sub_type)||'—'}</td>
+ <td class="num">${money(r.valuation)}</td>
+ <td class="when">${fmt(r.issue_date)}</td>
+ <td class="src">${esc(r.source)}</td>
+ <td class="when" title="${esc(r.first_seen)}">${fmt(r.first_seen)}</td></tr>`).join('');}
+async function load(){const p=new URLSearchParams();for(const k of['q','role','source','city','naics_desc','sort','dir','page','limit'])if(state[k]!==''&&state[k]!=null)p.set(k,state[k]);
+ const d=await(await fetch('/api/rows?'+p)).json();head();rows(d);
+ const pages=Math.max(1,Math.ceil(d.total/d.limit)),start=d.total?(d.page-1)*d.limit+1:0,end=Math.min(d.page*d.limit,d.total);
+ $('count').innerHTML=`<b>${d.total.toLocaleString()}</b> firms`;$('page').textContent=d.page;$('pages').textContent=pages.toLocaleString();
+ $('range').textContent=d.total?`showing ${start.toLocaleString()}–${end.toLocaleString()}`:'';$('prev').disabled=d.page<=1;$('next').disabled=d.page>=pages;}
+function fill(id,list,label){const cur=state[id];$(id).innerHTML=`<option value="">All ${label}</option>`+list.map(r=>`<option value="${esc(r.v)}">${esc(r.v)} (${r.n.toLocaleString()})</option>`).join('');$(id).value=cur;$(id).onchange=()=>{state[id]=$(id).value;state.page=1;save();load();};}
+async function boot(){dens();$('q').value=state.q;const f=await(await fetch('/api/facets')).json();
+ fill('role',f.role,'roles');fill('source',f.source,'sources');fill('city',f.city,'cities');fill('naics_desc',f.naics_desc,'NAICS');
+ let t;$('q').oninput=()=>{clearTimeout(t);t=setTimeout(()=>{state.q=$('q').value.trim();state.page=1;save();load();},250);};
+ $('density').oninput=()=>{state.density=+$('density').value;save();dens();};
+ $('prev').onclick=()=>{if(state.page>1){state.page--;save();load();}};$('next').onclick=()=>{state.page++;save();load();};load();}
+boot();
+</script>
+</body>
+</html>
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..9bbc7d0
--- /dev/null
+++ b/server.js
@@ -0,0 +1,110 @@
+#!/usr/bin/env node
+'use strict';
+/*
+ * socrata-cre-viewer
+ * Searchable / sortable grid over realestate.socrata_cre_prospects — LA Socrata
+ * CRE advertiser-prospect firms (Active Businesses + LADBS permits), net-new vs
+ * the curated LABJ set. Node built-in http + pg (parameterized queries only).
+ * Basic Auth admin / DW2024! (override via VIEWER_USER / VIEWER_PASS).
+ */
+const http = require('http');
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+const { Pool } = require('pg');
+
+const PORT = Number(process.env.PORT || 0);
+const HOST = process.env.HOST || '127.0.0.1';
+const USER = process.env.VIEWER_USER || 'admin';
+const PASS = process.env.VIEWER_PASS || 'DW2024!';
+
+const pool = new Pool({ host: process.env.PGHOST || '/tmp', database: process.env.PGDATABASE || 'realestate', max: 6 });
+pool.on('error', (err) => console.error('pg pool error', err));
+const TABLE = 'socrata_cre_prospects';
+
+const SORTABLE = new Set(['firm_name', 'role', 'contact_name', 'city', 'naics_desc',
+ 'permit_sub_type', 'valuation', 'issue_date', 'source', 'first_seen', 'zip']);
+const FILTERS = ['role', 'source', 'city', 'naics_desc', 'council_district'];
+const SEARCH_COLS = ['firm_name', 'contact_name', 'address', 'naics_desc', 'work_description'];
+
+function safeEq(a, b) {
+ const ab = Buffer.from(a), bb = Buffer.from(b);
+ return ab.length === bb.length && crypto.timingSafeEqual(ab, bb);
+}
+function authed(req) {
+ const h = req.headers.authorization || '';
+ if (!h.startsWith('Basic ')) return false;
+ const [u, p] = Buffer.from(h.slice(6), 'base64').toString('utf8').split(':');
+ return safeEq(u || '', USER) && safeEq(p || '', PASS);
+}
+
+function buildWhere(qs) {
+ const clauses = [], params = [];
+ for (const col of FILTERS) {
+ const v = qs.get(col);
+ if (v) { params.push(v); clauses.push(`${col} = $${params.length}`); }
+ }
+ const q = (qs.get('q') || '').trim();
+ if (q) {
+ params.push(`%${q}%`);
+ const i = params.length;
+ clauses.push('(' + SEARCH_COLS.map((c) => `${c} ILIKE $${i}`).join(' OR ') + ')');
+ }
+ return { where: clauses.length ? 'WHERE ' + clauses.join(' AND ') : '', params };
+}
+
+async function apiRows(qs) {
+ const { where, params } = buildWhere(qs);
+ let sort = qs.get('sort') || 'firm_name';
+ if (!SORTABLE.has(sort)) sort = 'firm_name';
+ const dir = (qs.get('dir') || 'asc').toLowerCase() === 'desc' ? 'DESC' : 'ASC';
+ // valuation is stored as text; sort it numerically when chosen.
+ const sortExpr = sort === 'valuation' ? "NULLIF(valuation,'')::bigint" : sort;
+ const limit = Math.min(Math.max(parseInt(qs.get('limit'), 10) || 50, 1), 200);
+ const page = Math.max(parseInt(qs.get('page'), 10) || 1, 1);
+ const offset = (page - 1) * limit;
+
+ const total = Number((await pool.query(`SELECT count(*)::bigint n FROM ${TABLE} ${where}`, params)).rows[0].n);
+ const rowsSql =
+ `SELECT firm_name, role, contact_name, address, city, state, zip, naics, naics_desc,
+ permit_type, permit_sub_type, valuation, issue_date, source, work_description,
+ council_district, first_seen
+ FROM ${TABLE} ${where}
+ ORDER BY ${sortExpr} ${dir} NULLS LAST, firm_name ASC
+ LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
+ const rows = (await pool.query(rowsSql, [...params, limit, offset])).rows;
+ return { total, page, limit, sort, dir, rows };
+}
+
+let fc = null, fa = 0;
+async function apiFacets() {
+ if (fc && Date.now() - fa < 3 * 60 * 1000) return fc;
+ const facet = async (col, lim) => (await pool.query(
+ `SELECT ${col} AS v, count(*)::int AS n FROM ${TABLE}
+ WHERE ${col} IS NOT NULL AND ${col} <> '' GROUP BY ${col} ORDER BY n DESC LIMIT ${lim}`)).rows;
+ const [role, source, city, naics_desc] = await Promise.all([
+ facet('role', 20), facet('source', 10), facet('city', 40), facet('naics_desc', 40)]);
+ const total = Number((await pool.query(`SELECT count(*)::bigint n FROM ${TABLE}`)).rows[0].n);
+ fc = { total, role, source, city, naics_desc }; fa = Date.now();
+ return fc;
+}
+
+function send(res, code, body, type = 'application/json') {
+ res.writeHead(code, { 'Content-Type': type, 'Cache-Control': 'no-store' });
+ res.end(typeof body === 'string' || Buffer.isBuffer(body) ? body : JSON.stringify(body));
+}
+
+const server = http.createServer(async (req, res) => {
+ if (!authed(req)) { res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="socrata-cre"' }); return res.end('Auth required'); }
+ const url = new URL(req.url, 'http://x');
+ try {
+ if (url.pathname === '/' || url.pathname === '/index.html')
+ return send(res, 200, fs.readFileSync(path.join(__dirname, 'public', 'index.html')), 'text/html; charset=utf-8');
+ if (url.pathname === '/api/facets') return send(res, 200, await apiFacets());
+ if (url.pathname === '/api/rows') return send(res, 200, await apiRows(url.searchParams));
+ if (url.pathname === '/healthz') return send(res, 200, { ok: true });
+ return send(res, 404, { error: 'not found' });
+ } catch (e) { console.error(e); return send(res, 500, { error: String(e.message || e) }); }
+});
+
+server.listen(PORT, HOST, () => console.log(`socrata-cre-viewer live: http://${HOST}:${server.address().port} (login ${USER})`));
(oldest)
·
back to Socrata Cre Viewer
·
nav-agent: universal grid-controls drop-in on internal dashb e9ca4ee →