← back to Rentv V1
desk: Sublease scope (empty-honest, CRCP actions) + admin-gated entry form + /api/sublease proxy
bed96bd76795dcbacb48aaebb1e7ae415c945d14 · 2026-07-22 19:29:37 -0700 · Steve
Files touched
A admin/desk-admin.htmlM data/markets.jsonM data/news.jsonM data/ticker.jsonM public/desk.htmlM server.js
Diff
commit bed96bd76795dcbacb48aaebb1e7ae415c945d14
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Jul 22 19:29:37 2026 -0700
desk: Sublease scope (empty-honest, CRCP actions) + admin-gated entry form + /api/sublease proxy
---
admin/desk-admin.html | 118 ++++++++++++++++++++++++++++++++++++++++++++++++++
data/markets.json | 2 +-
data/news.json | 2 +-
data/ticker.json | 2 +-
public/desk.html | 39 +++++++++++++----
server.js | 26 +++++++++++
6 files changed, 177 insertions(+), 12 deletions(-)
diff --git a/admin/desk-admin.html b/admin/desk-admin.html
new file mode 100644
index 0000000..f987f7d
--- /dev/null
+++ b/admin/desk-admin.html
@@ -0,0 +1,118 @@
+<!doctype html><html lang="en"><head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>RENTV · Sublease Desk — Admin Entry</title>
+<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@800&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+<style>
+ :root{--red:#c8102e;--line:#232a32}*{box-sizing:border-box;margin:0}
+ body{background:#0d1014;color:#e7ecf1;font-family:'Inter',system-ui,sans-serif;font-size:14px}
+ a{color:#7cc0ff;text-decoration:none}
+ .top{position:sticky;top:0;z-index:9;display:flex;align-items:center;justify-content:space-between;height:52px;padding:0 16px;background:#0b0e12;border-bottom:1px solid var(--line)}
+ .brand{font-family:'Playfair Display',serif;font-weight:800;font-size:19px}.brand span{color:var(--red)}
+ .brand small{font-family:'Inter';font-weight:700;font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:#8b939b;margin-left:10px}
+ .wrap{max-width:1100px;margin:0 auto;padding:24px 18px 80px;display:grid;grid-template-columns:minmax(0,420px) 1fr;gap:26px}
+ @media(max-width:820px){.wrap{grid-template-columns:1fr}}
+ h2{font-size:13px;letter-spacing:.1em;text-transform:uppercase;color:#8b939b;margin:0 0 12px}
+ form{background:#11161c;border:1px solid var(--line);border-radius:14px;padding:18px}
+ .fld{margin-bottom:12px}.fld label{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#8b939b;margin-bottom:5px}
+ .fld label .req{color:var(--red)}
+ input,select,textarea{width:100%;background:#0d1116;border:1px solid var(--line);border-radius:9px;color:#e7ecf1;padding:10px 12px;font:inherit}
+ input:focus,select:focus,textarea:focus{outline:none;border-color:var(--red)}
+ .row2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
+ .btn{background:var(--red);border:0;color:#fff;font-weight:800;padding:12px 18px;border-radius:10px;cursor:pointer;font-size:14px;width:100%;margin-top:6px}.btn:hover{filter:brightness(1.1)}
+ .note{font-size:12px;color:#6b747d;margin-top:12px;line-height:1.6}
+ .msg{margin-top:12px;font-size:13px;font-weight:700;display:none}.msg.ok{display:block;color:#37d67a}.msg.err{display:block;color:#ff6b6b}
+ .list{min-width:0}
+ .card{background:#11161c;border:1px solid var(--line);border-radius:12px;padding:14px 16px;margin-bottom:12px}
+ .card .hd{display:flex;justify-content:space-between;align-items:flex-start;gap:10px}
+ .card .addr{font-weight:800;font-size:16px}
+ .card .sub{color:#9aa4ad;font-size:13px;margin-top:2px}
+ .card .facts{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
+ .chip{background:#0d1116;border:1px solid var(--line);border-radius:7px;padding:5px 9px;font-size:12px;color:#c3cbd3}
+ .chip b{color:#e7ecf1}
+ .when{background:#0d1116;border:1px solid var(--line);border-radius:7px;padding:5px 9px;font-size:11.5px;color:#8b939b}
+ .wd{background:none;border:1px solid #3a2a2e;color:#ff8b8b;border-radius:8px;padding:6px 11px;font-weight:700;font-size:12px;cursor:pointer;white-space:nowrap}.wd:hover{background:#2a1418;border-color:var(--red)}
+ .empty{color:#6b747d;font-size:13px;padding:24px 4px;line-height:1.6}
+</style></head>
+<body>
+<header class="top">
+ <a class="brand" href="/desk">REN<span>TV</span><small>Sublease Desk · Admin</small></a>
+ <a href="/desk" style="font-weight:700;font-size:13px">← Back to Desk</a>
+</header>
+<div class="wrap">
+ <div>
+ <h2>Add a sublease listing</h2>
+ <form id="f" autocomplete="off">
+ <div class="fld"><label>Address <span class="req">*</span></label><input name="address" required placeholder="6100 Wilshire Blvd, 12th Fl"></div>
+ <div class="row2">
+ <div class="fld"><label>City</label><input name="city" placeholder="Los Angeles"></div>
+ <div class="fld"><label>Submarket</label><input name="submarket" placeholder="Miracle Mile"></div>
+ </div>
+ <div class="row2">
+ <div class="fld"><label>Floor / Suite</label><input name="floor_suite" placeholder="Suite 1200"></div>
+ <div class="fld"><label>SqFt</label><input name="sqft" type="number" min="0" placeholder="18500"></div>
+ </div>
+ <div class="row2">
+ <div class="fld"><label>Asking rate ($/SF)</label><input name="asking_rate" type="number" step="0.01" min="0" placeholder="3.25"></div>
+ <div class="fld"><label>Per</label><select name="rate_period"><option value="yr">/ year</option><option value="mo">/ month</option></select></div>
+ </div>
+ <div class="row2">
+ <div class="fld"><label>Type</label><select name="lease_type"><option value="sublease">Sublease</option><option value="direct">Direct lease</option></select></div>
+ <div class="fld"><label>Term (months)</label><input name="term_months" type="number" min="0" placeholder="36"></div>
+ </div>
+ <div class="row2">
+ <div class="fld"><label>Available</label><input name="available_date" type="date"></div>
+ <div class="fld"><label>Sublandlord (tenant)</label><input name="sublandlord" placeholder="Acme Media Corp"></div>
+ </div>
+ <div class="fld"><label>Listing brokerage</label><input name="firm_name" placeholder="CBRE"></div>
+ <div class="row2">
+ <div class="fld"><label>Broker name</label><input name="broker_name" placeholder="Jane Doe"></div>
+ <div class="fld"><label>Broker phone</label><input name="broker_phone" placeholder="(310) 555-0100"></div>
+ </div>
+ <div class="fld"><label>Broker email</label><input name="broker_email" type="email" placeholder="jane@cbre.com"></div>
+ <div class="fld"><label>Listing URL</label><input name="listing_url" type="url" placeholder="https://…"></div>
+ <div class="fld"><label>Notes</label><textarea name="notes" rows="2" placeholder="Furnished, plug-and-play, 40 workstations…"></textarea></div>
+ <button class="btn" type="submit">Add listing</button>
+ <div class="msg" id="msg"></div>
+ <div class="note">Facts-only, real deals from the RENTV broker network. Each entry appears live on the <a href="/desk">Intelligence Desk → Sublease</a> scope.</div>
+ </form>
+ </div>
+ <div class="list">
+ <h2>Current inventory (<span id="cnt">0</span>)</h2>
+ <div id="rows"></div>
+ </div>
+</div>
+<script>
+const $=s=>document.getElementById(s);
+const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
+const fmtWhen=d=>{try{return new Date(d).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'});}catch(e){return d;}};
+const rate=x=>x.asking_rate?('$'+Number(x.asking_rate).toFixed(2)+'/'+(x.rate_period||'yr')+' SF'):'Rate on request';
+async function load(){
+ const r=await fetch('/api/sublease?status=active&limit=500',{credentials:'same-origin'}).then(r=>r.json()).catch(()=>({rows:[]}));
+ const rows=r.rows||[];$('cnt').textContent=rows.length;
+ $('rows').innerHTML=rows.length?rows.map(x=>`<div class="card"><div class="hd"><div><div class="addr">${esc(x.address)}</div><div class="sub">${esc([x.floor_suite,x.submarket,x.city].filter(Boolean).join(' · '))}</div></div><button class="wd" data-id="${x.id}">Withdraw</button></div>
+ <div class="facts">
+ ${x.sqft?`<span class="chip"><b>${Number(x.sqft).toLocaleString()}</b> SF</span>`:''}
+ <span class="chip">${esc(rate(x))}</span>
+ ${x.term_months?`<span class="chip"><b>${x.term_months}</b> mo</span>`:''}
+ ${x.sublandlord?`<span class="chip">Sublandlord: <b>${esc(x.sublandlord)}</b></span>`:''}
+ ${x.firm_name?`<span class="chip">${esc(x.firm_name)}</span>`:''}
+ <span class="when" title="${esc(x.created_at)}">🕓 ${esc(fmtWhen(x.created_at))}</span>
+ </div></div>`).join(''):'<div class="empty">No listings yet. Add the first one on the left — it goes live on the desk immediately.</div>';
+ $('rows').querySelectorAll('.wd').forEach(b=>b.onclick=async()=>{
+ if(!confirm('Withdraw this listing from the desk?'))return;
+ await fetch('/api/admin/sublease/'+b.dataset.id,{method:'DELETE',credentials:'same-origin'});load();
+ });
+}
+$('f').onsubmit=async e=>{
+ e.preventDefault();const m=$('msg');m.className='msg';m.textContent='';
+ const body={};new FormData(e.target).forEach((v,k)=>{if(String(v).trim()!=='')body[k]=v;});
+ try{
+ const r=await fetch('/api/admin/sublease',{method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',body:JSON.stringify(body)});
+ const j=await r.json();
+ if(r.ok&&j.ok){m.className='msg ok';m.textContent='✓ Added — live on the desk.';e.target.reset();load();}
+ else{m.className='msg err';m.textContent='✕ '+(j.error||('HTTP '+r.status));}
+ }catch(err){m.className='msg err';m.textContent='✕ '+err.message;}
+};
+load();
+</script>
+</body></html>
diff --git a/data/markets.json b/data/markets.json
index 8291017..43f81f8 100644
--- a/data/markets.json
+++ b/data/markets.json
@@ -1,5 +1,5 @@
{
- "fetched_at": "2026-07-23T02:04:58.555Z",
+ "fetched_at": "2026-07-23T02:24:59.512Z",
"source": "Yahoo Finance — live (CBOE Treasury yields, Dow Jones/MSCI REIT indices)",
"curve": [
{
diff --git a/data/news.json b/data/news.json
index 525c433..e8c5af0 100644
--- a/data/news.json
+++ b/data/news.json
@@ -1,6 +1,6 @@
{
"source": "rentv.com — live",
- "fetched_at": "2026-07-23T02:04:58.245Z",
+ "fetched_at": "2026-07-23T02:24:59.075Z",
"count": 30,
"items": [
{
diff --git a/data/ticker.json b/data/ticker.json
index 73c7324..327b37a 100644
--- a/data/ticker.json
+++ b/data/ticker.json
@@ -1,5 +1,5 @@
{
- "fetched_at": "2026-07-23T02:04:58.530Z",
+ "fetched_at": "2026-07-23T02:24:59.484Z",
"la_office_vac": {
"label": "LA OFFICE VAC",
"value": "23.1%",
diff --git a/public/desk.html b/public/desk.html
index b8bdc66..f72b779 100644
--- a/public/desk.html
+++ b/public/desk.html
@@ -81,6 +81,7 @@
<button data-s="brokers" class="on scope"><span class="i">👤</span>Brokers <span class="c" id="cB">0</span></button>
<button data-s="firms" class="scope"><span class="i">🏢</span>Firms <span class="c" id="cF">0</span></button>
<button data-s="owners" class="scope"><span class="i">🏛️</span>Owners <span class="c" id="cO">0</span></button>
+ <button data-s="sublease" class="scope"><span class="i">🔑</span>Sublease <span class="c" id="cS">0</span></button>
<div class="fresh" id="fresh"></div>
</nav>
<div class="content">
@@ -101,9 +102,20 @@ const A=(h,i,l)=>`<a class="act" href="${h}" target="_blank" rel="noopener" oncl
function actions(k,x){
if(k==='firms')return [A(LI_C(x.name),'🏢','LinkedIn'),A(WEB(x.name),'🌐','Website'),A(MAPS(x.name+' '+loc(x)),'📍','HQ / Map'),x.license_no?A(DRE(x.license_no),'🪪','DRE'):'',A(GOOG(x.name+' brokers '+loc(x)),'🔎','Google')].filter(Boolean).join('');
if(k==='owners'){const ad=[x.address,x.city].filter(Boolean).join(', ');return [A(MAPS(ad),'📍','Map'),A(GOOG(ad+' '+(x.use_desc||'')+' owner'),'🔎','Owner / deal'),A('https://www.google.com/maps/@?api=1&map_action=pano&viewpoint='+enc(ad),'📸','Street View'),A(ASSR(x.ain),'🏛️','Assessor')].join('');}
+ if(k==='sublease'){const ad=[x.address,x.city].filter(Boolean).join(', ');return [
+ x.listing_url?A(x.listing_url,'🔗','Listing'):'',
+ A(MAPS(ad),'📍','Map'),
+ A('https://www.google.com/maps/@?api=1&map_action=pano&viewpoint='+enc(ad),'📸','Street View'),
+ x.broker_name?A(LI_P(x.broker_name,x.firm_name),'👤','Broker'):'',
+ x.firm_name?A(LI_C(x.firm_name),'🏢','Firm'):'',
+ x.broker_email?A('mailto:'+x.broker_email,'✉️','Email'):'',
+ x.broker_phone?A('tel:'+String(x.broker_phone).replace(/[^\d+]/g,''),'📞','Call'):'',
+ x.sublandlord?A(GOOG('"'+x.sublandlord+'" '+(x.city||'')+' lease'),'🔎','Sublandlord'):''
+ ].filter(Boolean).join('');}
return [A(LI_P(x.name,x.firm_name),'👤','LinkedIn'),A(GOOG(x.name+' '+(x.firm_name||'')+' '+loc(x)),'🔎','Google'),x.firm_name?A(WEB(x.firm_name),'🌐','Firm site'):'',x.firm_name?A(LI_C(x.firm_name),'🏢','Firm'):'',loc(x)?A(MAPS((x.firm_name||x.name)+' '+loc(x)),'📍','Map'):'',x.license_no?A(DRE(x.license_no),'🪪','DRE'):''].filter(Boolean).join('');
}
-const nm=x=>x.name||x.address||x.ain, sub=(k,x)=>k==='brokers'?(x.firm_name||'—'):k==='firms'?((x.agent_count||0).toLocaleString()+' agents'):(x.type_label||'');
+const subRate=x=>x.asking_rate?('$'+Number(x.asking_rate).toFixed(2)+'/'+(x.rate_period||'yr')+' SF'):'Rate on request';
+const nm=x=>x.name||x.address||x.ain, sub=(k,x)=>k==='brokers'?(x.firm_name||'—'):k==='firms'?((x.agent_count||0).toLocaleString()+' agents'):k==='sublease'?[x.sqft?Number(x.sqft).toLocaleString()+' SF':null,subRate(x),x.submarket||x.city].filter(Boolean).join(' · '):(x.type_label||'');
let D={scope:'brokers',view:'directory',selDir:0,selMap:0};
// chrome
$('burger').onclick=()=>{$('scrim').classList.add('open');$('drawer').classList.add('open');};
@@ -111,16 +123,18 @@ const cd=()=>{$('scrim').classList.remove('open');$('drawer').classList.remove('
$('user').onclick=()=>location.href='/subscribe';
async function jget(u,fb){try{const r=await fetch(u,{credentials:'same-origin'});if(!r.ok)return fb;return await r.json();}catch(e){return fb;}}
(async()=>{
- const [b,f,la]=await Promise.all([
+ const [b,f,la,sl]=await Promise.all([
jget('/api/brokers?state=CA&limit=500',null).then(v=>(v&&v.rows&&v.rows.length)?v:jget('desk-assets/data/brokers.json',{rows:[]})),
jget('/api/firms?state=CA&limit=200',null).then(v=>(v&&v.rows&&v.rows.length)?v:jget('desk-assets/data/firms.json',{rows:[]})),
- jget('/api/la-commercial',null).then(v=>v||jget('desk-assets/data/la-commercial.json',{marquee:[]}))]);
- D.brokers=b.rows||[];D.firms=f.rows||[];D.owners=la.marquee||[];D.totalB=b.total||D.brokers.length;D.totalF=f.total||D.firms.length;
- $('cB').textContent=D.brokers.length;$('cF').textContent=D.firms.length;$('cO').textContent=D.owners.length;
+ jget('/api/la-commercial',null).then(v=>v||jget('desk-assets/data/la-commercial.json',{marquee:[]})),
+ jget('/api/sublease?limit=500',{rows:[],total:0})]);
+ D.brokers=b.rows||[];D.firms=f.rows||[];D.owners=la.marquee||[];D.subleases=(sl&&sl.rows)||[];
+ D.totalB=b.total||D.brokers.length;D.totalF=f.total||D.firms.length;D.totalS=(sl&&sl.total)||D.subleases.length;
+ $('cB').textContent=D.brokers.length;$('cF').textContent=D.firms.length;$('cO').textContent=D.owners.length;$('cS').textContent=D.subleases.length;
$('fresh').textContent='CA DRE registry · '+D.brokers.length+' brokers of '+D.totalB.toLocaleString()+' statewide';
renderAll();
})();
-function scopeData(){return D.scope==='brokers'?D.brokers:D.scope==='firms'?D.firms:D.owners;}
+function scopeData(){return D.scope==='brokers'?D.brokers:D.scope==='firms'?D.firms:D.scope==='sublease'?D.subleases:D.owners;}
const debounce=(fn,ms)=>{let t;return(...a)=>{clearTimeout(t);t=setTimeout(()=>fn(...a),ms);};};
// live server-side search across the FULL registry (382k brokers / 34k firms) via the proxy;
// owners are only 12 so they filter client-side; empty term returns the loaded set.
@@ -128,6 +142,7 @@ async function liveRows(scope,term){
term=(term||'').trim();
if(!term) return scopeData();
if(scope==='owners') return D.owners.filter(x=>JSON.stringify(x).toLowerCase().includes(term.toLowerCase()));
+ if(scope==='sublease'){const v=await jget('/api/sublease?limit=200&q='+encodeURIComponent(term),null);if(v&&v.rows)return v.rows;return D.subleases.filter(x=>JSON.stringify(x).toLowerCase().includes(term.toLowerCase()));}
const ep=scope==='firms'?'/api/firms':'/api/brokers';
const v=await jget(ep+'?state=CA&q='+encodeURIComponent(term)+'&limit=100',null);
if(v&&v.rows&&v.rows.length) return v.rows;
@@ -146,9 +161,15 @@ function directory(){
const q='';
function list(){let r=rows.slice();const v=($('dq').value||'').toLowerCase();if(v)r=r.filter(x=>JSON.stringify(x).toLowerCase().includes(v));return r;}
function detail(x){if(!x){$('ddetail').innerHTML='';return;}
- const grid=D.scope==='owners'?[['Assessed',shortUsd(x.assessed_total)],['SqFt',(x.sqft||0).toLocaleString()],['Built',x.year_built||'—'],['AIN',x.ain]]:D.scope==='firms'?[['Agents',(x.agent_count||0).toLocaleString()],['HQ',loc(x)||'—'],['License',x.license_no||'—'],['Source',x.source||'—']]:[['License',x.license_no||'—'],['Type',x.license_type||'—'],['Firm',x.firm_name||'—'],['City',x.city||'—']];
- $('ddetail').innerHTML=`<div class="k">${esc(D.scope==='owners'?(x.type_label||'Asset'):D.scope==='firms'?'Brokerage':(x.license_type||'Licensee'))}</div><h2>${esc(nm(x))}</h2><div class="meta">${esc(D.scope==='owners'?[x.address,x.city].filter(Boolean).join(', '):(sub(D.scope,x)+' · '+(loc(x)||'')))}</div><div class="acts">${actions(D.scope,x)}</div><div class="grid">${grid.map(([k,v])=>`<div class="cell"><div class="kk">${k}</div><div class="vv">${esc(v)}</div></div>`).join('')}</div>`;}
- async function draw(){const r=await liveRows(D.scope,$('dq').value);$('drows').innerHTML=r.map((x,i)=>`<div class="row${i===D.selDir?' sel':''}" data-i="${i}"><div class="nm">${esc(nm(x))}</div><div class="f">${esc(sub(D.scope,x))}</div></div>`).join('')||'<div class="fresh">No matches in the registry.</div>';
+ const grid=D.scope==='sublease'?[['SqFt',x.sqft?Number(x.sqft).toLocaleString():'—'],['Asking',subRate(x)],['Term',x.term_months?x.term_months+' mo':'—'],['Available',x.available_date?dstr(x.available_date):'Now'],['Sublandlord',x.sublandlord||'—'],['Listing broker',x.firm_name||'—']]:D.scope==='owners'?[['Assessed',shortUsd(x.assessed_total)],['SqFt',(x.sqft||0).toLocaleString()],['Built',x.year_built||'—'],['AIN',x.ain]]:D.scope==='firms'?[['Agents',(x.agent_count||0).toLocaleString()],['HQ',loc(x)||'—'],['License',x.license_no||'—'],['Source',x.source||'—']]:[['License',x.license_no||'—'],['Type',x.license_type||'—'],['Firm',x.firm_name||'—'],['City',x.city||'—']];
+ const dk=D.scope==='sublease'?(x.lease_type==='direct'?'Direct Lease':'Sublease Available'):D.scope==='owners'?(x.type_label||'Asset'):D.scope==='firms'?'Brokerage':(x.license_type||'Licensee');
+ const dmeta=D.scope==='sublease'?[x.floor_suite,x.submarket,x.city].filter(Boolean).join(' · '):D.scope==='owners'?[x.address,x.city].filter(Boolean).join(', '):(sub(D.scope,x)+' · '+(loc(x)||''));
+ $('ddetail').innerHTML=`<div class="k">${esc(dk)}</div><h2>${esc(nm(x))}</h2><div class="meta">${esc(dmeta)}</div><div class="acts">${actions(D.scope,x)}</div><div class="grid">${grid.map(([k,v])=>`<div class="cell"><div class="kk">${k}</div><div class="vv">${esc(v)}</div></div>`).join('')}</div>`;}
+ const emptyMsg=D.scope==='sublease'
+ ? '<div class="fresh" style="padding:20px 16px;line-height:1.6">No sublease listings yet.<br>Inventory is entered by the RENTV desk from live broker deals — <a class="field-link" href="/desk-admin">add a listing →</a></div>'
+ : '<div class="fresh">No matches in the registry.</div>';
+ async function draw(){const r=await liveRows(D.scope,$('dq').value);$('drows').innerHTML=r.map((x,i)=>`<div class="row${i===D.selDir?' sel':''}" data-i="${i}"><div class="nm">${esc(nm(x))}</div><div class="f">${esc(sub(D.scope,x))}</div></div>`).join('')||emptyMsg;
+ if(!r.length&&D.scope==='sublease'){$('ddetail').innerHTML='<div class="fresh" style="padding:30px 36px;line-height:1.7;max-width:520px">This desk surfaces <b style="color:#e7ecf1">real sublease space</b> — sqft, asking rate, term, the sublandlord, and the listing broker, each with click-outs to the listing, map, and broker. It fills as the RENTV team logs deals from their broker network.</div>';return;}
$('drows').querySelectorAll('.row').forEach(row=>row.onclick=()=>{D.selDir=+row.dataset.i;$('drows').querySelectorAll('.row').forEach(x=>x.classList.remove('sel'));row.classList.add('sel');detail(r[D.selDir]);});detail(r[D.selDir]||r[0]);}
$('dq').oninput=debounce(()=>{D.selDir=0;draw();},300);draw();
}
diff --git a/server.js b/server.js
index 1d5ca9f..083f8b4 100644
--- a/server.js
+++ b/server.js
@@ -241,6 +241,32 @@ async function usreProxy(upstream, req, res) {
}
app.get('/api/brokers', (req, res) => usreProxy('/api/brokers', req, res));
app.get('/api/firms', (req, res) => usreProxy('/api/firms', req, res));
+app.get('/api/sublease', (req, res) => usreProxy('/api/sublease', req, res));
+
+// Admin write-forwarder → usre backend (JSON body). Same-origin so /desk admin can add/withdraw
+// sublease listings; the upstream is basic-auth-gated and loopback-only, so we inject the auth.
+async function usreWrite(method, upstream, req, res) {
+ try {
+ const r = await fetch(`${USRE}${upstream}`, {
+ method,
+ headers: { Authorization: USRE_AUTH, 'Content-Type': 'application/json' },
+ body: method === 'POST' ? JSON.stringify(req.body || {}) : undefined,
+ signal: AbortSignal.timeout(12000),
+ });
+ res.status(r.status).json(await r.json());
+ } catch (e) { res.status(502).json({ ok: false, error: 'registry unavailable' }); }
+}
+// Admin-ONLY gate: the global basic-auth above accepts both the admin cred and the client
+// browse-login (Boomer); sublease writes must be admin-only, so a client can view /desk but
+// never inject/withdraw inventory. Served from OUTSIDE public/ so express.static can't bypass it.
+const ADMIN_CRED = 'Basic ' + Buffer.from(process.env.RENTV_ADMIN_CRED || 'admin:DW2024!').toString('base64');
+function adminGate(req, res, next) {
+ if ((req.headers.authorization || '') === ADMIN_CRED) return next();
+ return res.status(403).send('Admin only');
+}
+app.get('/desk-admin', adminGate, (_req, res) => res.sendFile(path.join(__dirname, 'admin', 'desk-admin.html')));
+app.post('/api/admin/sublease', adminGate, (req, res) => usreWrite('POST', '/api/admin/sublease', req, res));
+app.delete('/api/admin/sublease/:id', adminGate, (req, res) => usreWrite('DELETE', '/api/admin/sublease/' + encodeURIComponent(req.params.id), req, res));
app.use(express.static(PUB, { extensions: ['html'] }));
app.listen(PORT, () => console.log('rentv-v1 (full site, gated, live data) on ' + PORT));
← 50c72c9 auto-save: 2026-07-22T19:16:13 (4 files) — data/markets.json
·
back to Rentv V1
·
auto-save: 2026-07-22T19:46:22 (3 files) — data/markets.json 9e29c2b →