[object Object]

← back to AbramsOS

claims: George-backed backup-doc finder (home-sale/closing paperwork, receipts, breach notices) + /docs endpoint + surfaced in prep panel amber lane

451cbcf19a6976d6720c984f7959b0da62bcc155 · 2026-08-18 16:17:38 -0700 · Steve

Files touched

Diff

commit 451cbcf19a6976d6720c984f7959b0da62bcc155
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Aug 18 16:17:38 2026 -0700

    claims: George-backed backup-doc finder (home-sale/closing paperwork, receipts, breach notices) + /docs endpoint + surfaced in prep panel amber lane
---
 lib/claim-docs.js                                  |  54 +++++++++++++++++++++
 .../claim_01kzvyhm9m9h86b92zqg40fbjj.png           | Bin 0 -> 744042 bytes
 public/claim-shots/demo-kw6.png                    | Bin 0 -> 181930 bytes
 routes/settlements.js                              |  12 +++++
 views/settlements.ejs                              |  20 ++++++--
 5 files changed, 83 insertions(+), 3 deletions(-)

diff --git a/lib/claim-docs.js b/lib/claim-docs.js
new file mode 100644
index 0000000..d675647
--- /dev/null
+++ b/lib/claim-docs.js
@@ -0,0 +1,54 @@
+// lib/claim-docs.js — find the SUPPORTING DOCUMENTS a claim needs, in Steve's email, via George.
+// The claim auto-fill covers identity+payment; the "🟡 needs you" lane often wants a PROOF upload
+// (home-sale/closing paperwork for the real-estate antitrust claim, receipts for product claims,
+// the breach notice / Claim ID for data-breach claims). This searches Gmail (through the George
+// bridge) for those e-docs so they're one click from the claim. READ-ONLY — never sends, never files.
+
+const GEORGE_URL = (process.env.GEORGE_URL || '').replace(/\/$/, '');
+const GEORGE_BASIC_AUTH = process.env.GEORGE_BASIC_AUTH || '';
+// which mailboxes to search for personal claim docs
+const ACCOUNTS = (process.env.CLAIM_DOC_ACCOUNTS || 'info,steve-personal,steve-office').split(',').map((s) => s.trim());
+
+// Map a claim to the doc-hunt query by its type. Each is a Gmail search string.
+function queriesFor(claim) {
+  const name = (claim.name || '').toLowerCase();
+  const cat = (claim.category || '').toLowerCase();
+  const isRealEstate = /home ?buyer|real ?estate|keller williams|re\/?max|realtor|nar|commission|broker/.test(name + cat);
+  const isBreach = /data breach|breach|privacy|incident|health|imaging|medical/.test(name + cat);
+  const isProduct = /product|inhaler|magnesium|nutricost|pump|spa|paper towel/.test(name + cat);
+  const out = [];
+  if (isRealEstate) out.push({ label: 'home purchase / closing docs', q: 'has:attachment (closing statement OR "settlement statement" OR HUD-1 OR "ALTA settlement" OR escrow OR "purchase agreement" OR "final closing" OR deed OR "buyer\'s statement" OR "commission")' });
+  if (isBreach) out.push({ label: 'breach notice / Claim ID', q: '("notice of data breach" OR "data security incident" OR "class member" OR "claim number" OR "claimant id" OR "settlement notice") ' + (claim.name ? '"' + claim.name.split(' ')[0] + '"' : '') });
+  if (isProduct) out.push({ label: 'receipt / proof of purchase', q: 'has:attachment (receipt OR invoice OR "order confirmation" OR "proof of purchase")' });
+  // generic fallback: the settlement's own mailed notice
+  out.push({ label: 'settlement mailed notice', q: '("' + (claim.name || 'settlement').replace(/["]/g, '') + '" OR settlement OR "class action") (notice OR claim OR "ID" OR postcard)' });
+  return out;
+}
+
+async function george(account, q, max) {
+  const url = `${GEORGE_URL}/api/messages?account=${encodeURIComponent(account)}&maxResults=${max || 8}&q=${encodeURIComponent(q)}`;
+  const res = await fetch(url, { headers: { Authorization: 'Basic ' + GEORGE_BASIC_AUTH } });
+  if (!res.ok) throw new Error('George ' + res.status);
+  const j = await res.json();
+  return j.messages || j || [];
+}
+
+async function findDocs(claim) {
+  if (!GEORGE_URL || !GEORGE_BASIC_AUTH) return { ok: false, skipped: 'george-not-configured', groups: [] };
+  const groups = [];
+  for (const { label, q } of queriesFor(claim)) {
+    const hits = [];
+    for (const acct of ACCOUNTS) {
+      let msgs = [];
+      try { msgs = await george(acct, q, 6); } catch (e) { continue; }
+      for (const m of msgs) hits.push({ account: acct, id: m.id, subject: m.subject, from: m.from, date: m.date, snippet: (m.snippet || '').slice(0, 90) });
+    }
+    // newest first, dedup by id
+    const seen = new Set();
+    const uniq = hits.filter((h) => (seen.has(h.id) ? false : (seen.add(h.id), true))).sort((a, b) => (b.date || '').localeCompare(a.date || '')).slice(0, 6);
+    groups.push({ label, query: q, count: uniq.length, docs: uniq });
+  }
+  return { ok: true, groups };
+}
+
+module.exports = { findDocs, queriesFor };
diff --git a/public/claim-shots/claim_01kzvyhm9m9h86b92zqg40fbjj.png b/public/claim-shots/claim_01kzvyhm9m9h86b92zqg40fbjj.png
new file mode 100644
index 0000000..f95795e
Binary files /dev/null and b/public/claim-shots/claim_01kzvyhm9m9h86b92zqg40fbjj.png differ
diff --git a/public/claim-shots/demo-kw6.png b/public/claim-shots/demo-kw6.png
new file mode 100644
index 0000000..bc1328f
Binary files /dev/null and b/public/claim-shots/demo-kw6.png differ
diff --git a/routes/settlements.js b/routes/settlements.js
index a1084e0..a44ff24 100644
--- a/routes/settlements.js
+++ b/routes/settlements.js
@@ -9,6 +9,7 @@ const filler = require('../lib/openclaw-claim-filler');
 const { extractAddress } = require('../lib/claims-autopilot');
 const { rankClaims } = require('../lib/settlement-score');
 const { runFill } = require('../lib/claim-openclaw-run');
+const { findDocs } = require('../lib/claim-docs');
 
 const router = express.Router();
 const USER = 'user_steve';
@@ -143,4 +144,15 @@ router.post('/api/settlements/:id/run', async (req, res) => {
   } catch (e) { res.status(503).json({ error: e.message }); }
 });
 
+// Find the claim's SUPPORTING DOCS in Steve's email via George (home-sale/closing paperwork,
+// receipts, breach notices). Read-only. Needs George config; no-ops cleanly without it.
+router.get('/api/settlements/:id/docs', async (req, res) => {
+  try {
+    const r = await db.query(`SELECT * FROM settlement_claim WHERE id=$1 AND user_id=$2`, [req.params.id, USER]);
+    if (!r.rows.length) return res.status(404).json({ error: 'not found' });
+    const out = await findDocs(r.rows[0]);
+    res.json(out);
+  } catch (e) { res.status(503).json({ error: e.message }); }
+});
+
 module.exports = router;
diff --git a/views/settlements.ejs b/views/settlements.ejs
index 16927f0..dd8eb4f 100644
--- a/views/settlements.ejs
+++ b/views/settlements.ejs
@@ -132,7 +132,8 @@
     <p class="claim-meta" id="prep-note"></p>
     <div class="lane green"><h4>🟢 We'll fill this — verify &amp; paste</h4><div id="prep-green"></div></div>
     <div class="lane amber"><h4>🟡 Needs you (once)</h4>
-      <div class="claim-meta">If the form asks for a <strong>Claim ID / PIN</strong> it's on your mailed notice (many claims allow filing without it). Claim-specific facts (property address, dates) and any proof upload are entered by you on the portal.</div>
+      <div class="claim-meta">Claim ID / PIN (on your mailed notice — many claims file without it), claim-specific facts, and any proof upload go in on the portal.</div>
+      <div id="prep-docs" style="margin-top:8px"></div>
     </div>
     <div class="lane red"><h4>🔴 Locked — by law</h4>
       <div class="claim-meta">The "under penalty of perjury I am a class member" attestation + Submit are <strong>yours</strong>. Nothing is submitted for you.</div>
@@ -170,18 +171,31 @@
     document.getElementById('prep-open').style.display='none';
     document.getElementById('prep-bd').style.display='flex';
   }
+  function loadDocs(id){
+    var box=document.getElementById('prep-docs'); box.innerHTML='<span class="claim-meta">🔎 searching your email for backup docs…</span>';
+    fetch('/api/settlements/'+id+'/docs').then(function(r){return r.json();}).then(function(j){
+      if(!j.ok){ box.innerHTML='<span class="claim-meta">📎 doc search needs George configured (GEORGE_BASIC_AUTH).</span>'; return; }
+      var html='';
+      (j.groups||[]).forEach(function(g){ if(!g.count) return;
+        html+='<div style="margin:6px 0"><b style="font-size:12px">📎 '+g.label+' ('+g.count+')</b>';
+        g.docs.forEach(function(d){ html+='<div class="kv"><span class="v" style="font-weight:400">'+ (d.subject||'').replace(/[<>]/g,'') +'</span><a class="k" style="color:#2d5bff" href="https://mail.google.com/mail/u/0/#search/rfc822msgid" target="_blank">'+(d.date||'').slice(0,16)+'</a></div>'; });
+        html+='</div>';
+      });
+      box.innerHTML = html || '<span class="claim-meta">📎 no backup docs found in email for this claim.</span>';
+    }).catch(function(){ box.innerHTML='<span class="claim-meta">📎 doc search error.</span>'; });
+  }
   document.querySelectorAll('.go').forEach(function(btn){ btn.onclick=function(){
     var id=btn.dataset.id, name=btn.dataset.name||'Claim';
     btn.disabled=true; btn.textContent='🤖 auto-filling in Chrome…';
     // 1) live openclaw auto-fill (zero-click)
     fetch('/api/settlements/'+id+'/run',{method:'POST'}).then(function(r){return r.json();}).then(function(j){
       if(j && j.ok){ btn.disabled=false; btn.textContent='✅ Filled — review in Chrome';
-        showPrep(name, 'Auto-filled '+((j.filled||[]).length)+' fields in your Chrome ('+(j.title||'')+'). Switch to Chrome, verify, check the attestation & submit. Nothing was submitted for you.', null, j.screenshot); return; }
+        showPrep(name, 'Auto-filled '+((j.filled||[]).length)+' fields in your Chrome ('+(j.title||'')+'). Switch to Chrome, verify, check the attestation & submit. Nothing was submitted for you.', null, j.screenshot); loadDocs(id); return; }
       // 2) fallback: openclaw not up -> paste card
       fetch('/api/settlements/'+id+'/claim-and-go',{method:'POST'}).then(function(r){return r.json();}).then(function(k){
         btn.disabled=false; btn.textContent='✅ Claim it & Go';
         if(k.error){ alert(k.error); return; }
-        showPrep(name, k.note||'', k.fields, null);
+        showPrep(name, k.note||'', k.fields, null); loadDocs(id);
         var op=document.getElementById('prep-open'); if(k.mode_url){ op.href=k.mode_url; op.style.display=''; window.open(k.mode_url,'_blank','noopener'); }
       });
     }).catch(function(){ btn.disabled=false; btn.textContent='✅ Claim it & Go'; alert('error'); });

← 9676eb8 claim-fill: combined First/Middle/Last name field -> full na  ·  back to AbramsOS  ·  auto-data-snapshot: 2026-08-18T16:40:41 (2 data files) — pub 00f2109 →