← back to Sample Followup Sweep
Fleet sweep (37 vendors/106 items) + selection console (server.js)
d5b70224205a324f19965856b101c5939f50508d · 2026-08-14 13:38:03 -0700 · Steve Abrams
Files touched
A data/contacts.jsonA data/fleet.jsonA scripts/build-fleet.jsA server.js
Diff
commit d5b70224205a324f19965856b101c5939f50508d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Aug 14 13:38:03 2026 -0700
Fleet sweep (37 vendors/106 items) + selection console (server.js)
---
data/contacts.json | 3 +
data/fleet.json | 1047 ++++++++++++++++++++++++++++++++++++++++++++++++
scripts/build-fleet.js | 55 +++
server.js | 163 ++++++++
4 files changed, 1268 insertions(+)
diff --git a/data/contacts.json b/data/contacts.json
new file mode 100644
index 0000000..edf4626
--- /dev/null
+++ b/data/contacts.json
@@ -0,0 +1,3 @@
+{
+ "dgd": { "sample_email": "Polancol@oalusa.com", "account_number": "1433803" }
+}
diff --git a/data/fleet.json b/data/fleet.json
new file mode 100644
index 0000000..3fd5bb1
--- /dev/null
+++ b/data/fleet.json
@@ -0,0 +1,1047 @@
+{
+ "generated": "2026-08-14",
+ "window": "10–60 days (>60 = dead lead, excluded)",
+ "totalItems": 106,
+ "skippedNoVid": 15,
+ "skippedWindow": 9,
+ "vendors": [
+ {
+ "slug": "dgd",
+ "vid": "DGD",
+ "name": "Designers Guild @ Osborne & Little",
+ "items": [
+ {
+ "mfr": "LCW1035.004.0",
+ "dw": "DWKK123171",
+ "client": "Emily Guth",
+ "age": 53,
+ "req": "06/22/2026"
+ },
+ {
+ "mfr": "PCL7033 Prête-Moi Ta Plume! Feather Panel 02",
+ "dw": "EUR80616",
+ "client": "Peyton Pawlicki",
+ "age": 43,
+ "req": "07/02/2026"
+ },
+ {
+ "mfr": "FRL5229/01",
+ "dw": "RALPH1077",
+ "client": "Jay RInehart",
+ "age": 36,
+ "req": "07/09/2026"
+ },
+ {
+ "mfr": "PJD6009/01",
+ "dw": "DWDG987895",
+ "client": "Alia Garcia",
+ "age": 32,
+ "req": "07/13/2026"
+ },
+ {
+ "mfr": "P555-17-Ajanta Plaster",
+ "dw": "EUR90140",
+ "client": "Christine Levy",
+ "age": 30,
+ "req": "07/15/2026"
+ },
+ {
+ "mfr": "P555-16-Ajanta Plaster",
+ "dw": "EUR90139",
+ "client": "Christine Levy",
+ "age": 30,
+ "req": "07/15/2026"
+ },
+ {
+ "mfr": "PCL7033-02",
+ "dw": "EUR80616",
+ "client": "The Hunter House",
+ "age": 28,
+ "req": "07/17/2026"
+ },
+ {
+ "mfr": "PDG646/07",
+ "dw": "EUR70041",
+ "client": "Margie Pesicka",
+ "age": 28,
+ "req": "07/17/2026"
+ },
+ {
+ "mfr": "PCL7033-02",
+ "dw": "EUR80616",
+ "client": "The Hunter House",
+ "age": 25,
+ "req": "07/20/2026"
+ },
+ {
+ "mfr": "FRL5217/01",
+ "dw": "RALPH1007",
+ "client": "Kathiann Crews",
+ "age": 24,
+ "req": "07/21/2026"
+ },
+ {
+ "mfr": "W7780-13-Regency Flock Velvet Stripe-Red",
+ "dw": "EUR71111",
+ "client": "Charlotte Harrison",
+ "age": 23,
+ "req": "07/22/2026"
+ },
+ {
+ "mfr": "Belles Rives WALLPAPER",
+ "dw": "DWBW600417",
+ "client": "Charlotte Harrison",
+ "age": 23,
+ "req": "07/22/2026"
+ }
+ ],
+ "count": 12
+ },
+ {
+ "slug": "spn",
+ "vid": "SPN",
+ "name": "Spoonflower (online — no rep desk)",
+ "items": [
+ {
+ "mfr": "DIG441001 on textured vinyl: 12455410 Vinyl",
+ "dw": "DIG441001V",
+ "client": "Jacqueline Edwards",
+ "age": 60,
+ "req": "06/15/2026"
+ },
+ {
+ "mfr": "Dig-700005-25-type-2-vinyl",
+ "dw": "Dig700005",
+ "client": "SEND 2ND DAY SAMPLES*** Marnie Ann Stebner",
+ "age": 60,
+ "req": "06/15/2026"
+ },
+ {
+ "mfr": "DIG624430Spn:20481474",
+ "dw": "DIG624430",
+ "client": "Andrew B Steinau",
+ "age": 24,
+ "req": "07/21/2026"
+ },
+ {
+ "mfr": "DIG-624415 SPN: 20275385",
+ "dw": "DIG624415-25 vinyl",
+ "client": "Andrew B Steinau",
+ "age": 24,
+ "req": "07/21/2026"
+ },
+ {
+ "mfr": "DIG_977030 spn: 22827698",
+ "dw": "DIG977030",
+ "client": "Charlotte Harrison",
+ "age": 23,
+ "req": "07/22/2026"
+ },
+ {
+ "mfr": "DIG623071 (SPN#DIG623071 SPN#(13656114)",
+ "dw": "DIG623071-25-vin",
+ "client": "Charlotte Harrison",
+ "age": 23,
+ "req": "07/22/2026"
+ },
+ {
+ "mfr": "SUNSHINE-MODE-PLAID-WALLPAPER spn:22827966",
+ "dw": "DIG240013",
+ "client": "Charlotte Harrison",
+ "age": 23,
+ "req": "07/22/2026"
+ },
+ {
+ "mfr": "DIG-74795SM",
+ "dw": "DIG74795SM",
+ "client": "Charlotte Harrison",
+ "age": 23,
+ "req": "07/22/2026"
+ },
+ {
+ "mfr": "DIG_74793sm",
+ "dw": "DIG74793",
+ "client": "Charlotte Harrison",
+ "age": 23,
+ "req": "07/22/2026"
+ },
+ {
+ "mfr": "90026",
+ "dw": "WPEX90026",
+ "client": "Charlotte Harrison",
+ "age": 23,
+ "req": "07/22/2026"
+ },
+ {
+ "mfr": "60400",
+ "dw": "WPEX60400",
+ "client": "Charlotte Harrison",
+ "age": 23,
+ "req": "07/22/2026"
+ }
+ ],
+ "count": 11
+ },
+ {
+ "slug": "green",
+ "vid": "GREEN",
+ "name": "(vid green)",
+ "items": [
+ {
+ "mfr": "FD280-10",
+ "dw": "GRS800010",
+ "client": "Janet Gaza Kolb",
+ "age": 23,
+ "req": "07/22/2026"
+ },
+ {
+ "mfr": "BX10180",
+ "dw": "PWV10180",
+ "client": "Donna Horner",
+ "age": 21,
+ "req": "07/24/2026"
+ },
+ {
+ "mfr": "H172NG5083",
+ "dw": "PWV500960",
+ "client": "JRP Design & Remodel, Inc.",
+ "age": 15,
+ "req": "07/30/2026"
+ },
+ {
+ "mfr": "S300NQ8242",
+ "dw": "Cork501520",
+ "client": "JRP Design & Remodel, Inc.",
+ "age": 15,
+ "req": "07/30/2026"
+ },
+ {
+ "mfr": "S300NQ8243",
+ "dw": "Cork501530",
+ "client": "JRP Design & Remodel, Inc.",
+ "age": 15,
+ "req": "07/30/2026"
+ },
+ {
+ "mfr": "S176NQ8341",
+ "dw": "Cork501390",
+ "client": "JRP Design & Remodel, Inc.",
+ "age": 15,
+ "req": "07/30/2026"
+ },
+ {
+ "mfr": "G0174NQ8330",
+ "dw": "Cork500620",
+ "client": "JRP Design & Remodel, Inc.",
+ "age": 15,
+ "req": "07/30/2026"
+ }
+ ],
+ "count": 7
+ },
+ {
+ "slug": "kra",
+ "vid": "KRA",
+ "name": "Kravet",
+ "items": [
+ {
+ "mfr": "2019101.34.0",
+ "dw": "DWKK134939",
+ "client": "Lisa McIlhenny FEDEX",
+ "age": 18,
+ "req": "07/27/2026"
+ },
+ {
+ "mfr": "2019101.313.0",
+ "dw": "DWKK134938",
+ "client": "Lisa McIlhenny FEDEX",
+ "age": 18,
+ "req": "07/27/2026"
+ },
+ {
+ "mfr": "F1562/04.CAC.0",
+ "dw": "DWKK119396",
+ "client": "Lisa McIlhenny FEDEX",
+ "age": 18,
+ "req": "07/27/2026"
+ },
+ {
+ "mfr": "F1562/02.CAC.0",
+ "dw": "DWKK119394",
+ "client": "Lisa McIlhenny FEDEX",
+ "age": 18,
+ "req": "07/27/2026"
+ },
+ {
+ "mfr": "GWP-3415.16.0",
+ "dw": "DWKK139575",
+ "client": "Denise Duggan",
+ "age": 17,
+ "req": "07/28/2026"
+ },
+ {
+ "mfr": "ALTHEA COTTON P.CITRON.0",
+ "dw": "DWKK142456",
+ "client": "Kristiina Sutherland",
+ "age": 11,
+ "req": "08/03/2026"
+ },
+ {
+ "mfr": "ALTHEA LINEN PR.CITRON.0",
+ "dw": "DWKK142457",
+ "client": "Kristiina Sutherland",
+ "age": 11,
+ "req": "08/03/2026"
+ }
+ ],
+ "count": 7
+ },
+ {
+ "slug": "sca",
+ "vid": "SCA",
+ "name": "Scalamandre",
+ "items": [
+ {
+ "mfr": "sc_0008g1194",
+ "dw": "DWA89306",
+ "client": "Michelle kungis",
+ "age": 45,
+ "req": "06/30/2026"
+ },
+ {
+ "mfr": "SC WP88355D 0006",
+ "dw": "DWA82596",
+ "client": "Leah E Ramsay",
+ "age": 34,
+ "req": "07/11/2026"
+ },
+ {
+ "mfr": "wh000013326",
+ "dw": "DWA88957",
+ "client": "GoldLeaf Interior",
+ "age": 23,
+ "req": "07/22/2026"
+ },
+ {
+ "mfr": "wtt661518",
+ "dw": "DWA88660",
+ "client": "GoldLeaf Interior",
+ "age": 23,
+ "req": "07/22/2026"
+ },
+ {
+ "mfr": "CL WP26420 0003",
+ "dw": "DWA89455",
+ "client": "Lisa McIlhenny FEDEX",
+ "age": 18,
+ "req": "07/27/2026"
+ }
+ ],
+ "count": 5
+ },
+ {
+ "slug": "wq",
+ "vid": "WQ",
+ "name": "WallQuest / Malibu",
+ "items": [
+ {
+ "mfr": "FISHNET-SEQUIN-GLITTER-WALLCOVERING-BLACK-COPPER",
+ "dw": "GLM4134",
+ "client": "Margie Pesicka",
+ "age": 28,
+ "req": "07/17/2026"
+ },
+ {
+ "mfr": "FISHNET-SEQUIN-GLITTER-WALLCOVERING-BLACK-COPPER",
+ "dw": "GLM4134",
+ "client": "Margie Pesicka",
+ "age": 28,
+ "req": "07/17/2026"
+ },
+ {
+ "mfr": "LN50035",
+ "dw": "GRS800776",
+ "client": "JRP Design & Remodel, Inc.",
+ "age": 15,
+ "req": "07/30/2026"
+ },
+ {
+ "mfr": "LN50083-1",
+ "dw": "GRS800956",
+ "client": "JRP Design & Remodel, Inc.",
+ "age": 15,
+ "req": "07/30/2026"
+ },
+ {
+ "mfr": "LN50084",
+ "dw": "DWC50084",
+ "client": "JRP Design & Remodel, Inc.",
+ "age": 15,
+ "req": "07/30/2026"
+ }
+ ],
+ "count": 5
+ },
+ {
+ "slug": "thi",
+ "vid": "THI",
+ "name": "Thibaut",
+ "items": [
+ {
+ "mfr": "",
+ "dw": "ROP73313",
+ "client": "Haley McKinley",
+ "age": 39,
+ "req": "07/06/2026"
+ },
+ {
+ "mfr": "",
+ "dw": "ROP73020",
+ "client": "Haley McKinley",
+ "age": 39,
+ "req": "07/06/2026"
+ },
+ {
+ "mfr": "",
+ "dw": "ROP73216",
+ "client": "Haley McKinley",
+ "age": 39,
+ "req": "07/06/2026"
+ },
+ {
+ "mfr": "",
+ "dw": "ROP73150",
+ "client": "Shi Shi Gallery",
+ "age": 39,
+ "req": "07/06/2026"
+ }
+ ],
+ "count": 4
+ },
+ {
+ "slug": "pf",
+ "vid": "PF",
+ "name": "Pierre Frey",
+ "items": [
+ {
+ "mfr": "BP203001",
+ "dw": "DWPF150036",
+ "client": "Kristiina Sutherland",
+ "age": 36,
+ "req": "07/09/2026"
+ },
+ {
+ "mfr": "BP203004",
+ "dw": "DWPF150039",
+ "client": "Kristiina Sutherland",
+ "age": 36,
+ "req": "07/09/2026"
+ },
+ {
+ "mfr": "BP203005",
+ "dw": "DWPF150040",
+ "client": "Kristiina Sutherland",
+ "age": 36,
+ "req": "07/09/2026"
+ },
+ {
+ "mfr": "BP203002",
+ "dw": "DWC321345",
+ "client": "Sincere Things, LLC",
+ "age": 25,
+ "req": "07/20/2026"
+ }
+ ],
+ "count": 4
+ },
+ {
+ "slug": "yor",
+ "vid": "YOR",
+ "name": "York",
+ "items": [
+ {
+ "mfr": "PSW1113RL",
+ "dw": "DWJS17726",
+ "client": "lauren shapiro",
+ "age": 35,
+ "req": "07/10/2026"
+ },
+ {
+ "mfr": "56877",
+ "dw": "DWJS90066",
+ "client": "Caitlin Rae Designs",
+ "age": 16,
+ "req": "07/29/2026"
+ },
+ {
+ "mfr": "56876",
+ "dw": "DWJS90065",
+ "client": "Caitlin Rae Designs",
+ "age": 16,
+ "req": "07/29/2026"
+ },
+ {
+ "mfr": "56878",
+ "dw": "DWJS90067",
+ "client": "Caitlin Rae Designs",
+ "age": 16,
+ "req": "07/29/2026"
+ }
+ ],
+ "count": 4
+ },
+ {
+ "slug": "sch",
+ "vid": "SCH",
+ "name": "Schumacher",
+ "items": [
+ {
+ "mfr": "80080",
+ "dw": "DWPIL7744",
+ "client": "Holly Bauer",
+ "age": 58,
+ "req": "06/17/2026"
+ },
+ {
+ "mfr": "5000412",
+ "dw": "DWLK829860",
+ "client": "judy heiny",
+ "age": 53,
+ "req": "06/22/2026"
+ },
+ {
+ "mfr": "WOODPERRY, MIMOSA WALLCOVERINGS |",
+ "dw": "DWLK803930",
+ "client": "Elizabeth H Hollingsworth",
+ "age": 36,
+ "req": "07/09/2026"
+ }
+ ],
+ "count": 3
+ },
+ {
+ "slug": "chin",
+ "vid": "CHIN",
+ "name": "(vid Chin)",
+ "items": [
+ {
+ "mfr": "Arbre De Matisse Reverse Neutral Soft Windsor Blue on Tinted Linen Screen Printed Fabric",
+ "dw": "DWCS893848",
+ "client": "Jessica Noble",
+ "age": 37,
+ "req": "07/08/2026"
+ },
+ {
+ "mfr": "6460-41",
+ "dw": "DWCS930248",
+ "client": "Alison Iglehart",
+ "age": 36,
+ "req": "07/09/2026"
+ },
+ {
+ "mfr": "6620 13WWP",
+ "dw": "DWCW734546",
+ "client": "Alison Iglehart",
+ "age": 36,
+ "req": "07/09/2026"
+ }
+ ],
+ "count": 3
+ },
+ {
+ "slug": "mor",
+ "vid": "MOR",
+ "name": "Morris & Co",
+ "items": [
+ {
+ "mfr": "212564",
+ "dw": "DWWM14464",
+ "client": "Nicole Stauffer",
+ "age": 33,
+ "req": "07/12/2026"
+ },
+ {
+ "mfr": "216035",
+ "dw": "DWWM14707",
+ "client": "Martha Ratay",
+ "age": 11,
+ "req": "08/03/2026"
+ },
+ {
+ "mfr": "216538",
+ "dw": "DWWM14752",
+ "client": "Pamela Thomaston",
+ "age": 11,
+ "req": "08/03/2026"
+ }
+ ],
+ "count": 3
+ },
+ {
+ "slug": "qua",
+ "vid": "QUA",
+ "name": "Quadrille",
+ "items": [
+ {
+ "mfr": "2475wp-00",
+ "dw": "DWCW777346",
+ "client": "",
+ "age": 31,
+ "req": "07/14/2026"
+ },
+ {
+ "mfr": "AP303 10PV",
+ "dw": "DWCW762146",
+ "client": "Kate Harrell",
+ "age": 25,
+ "req": "07/20/2026"
+ },
+ {
+ "mfr": "306520F-102",
+ "dw": "DWQC600140",
+ "client": "Sally P Brady",
+ "age": 11,
+ "req": "08/03/2026"
+ }
+ ],
+ "count": 3
+ },
+ {
+ "slug": "ver",
+ "vid": "VER",
+ "name": "(vid VER)",
+ "items": [
+ {
+ "mfr": "935224",
+ "dw": "VER47203",
+ "client": "Chanele Williams",
+ "age": 53,
+ "req": "06/22/2026"
+ },
+ {
+ "mfr": "AS343264",
+ "dw": "VER46222",
+ "client": "FEDEX",
+ "age": 35,
+ "req": "07/10/2026"
+ }
+ ],
+ "count": 2
+ },
+ {
+ "slug": "tbd",
+ "vid": "TBD",
+ "name": "(vid tbd)",
+ "items": [
+ {
+ "mfr": "PI-DUNDEE-BLACK",
+ "dw": "DWPN100750",
+ "client": "Jody Malan",
+ "age": 53,
+ "req": "06/22/2026"
+ },
+ {
+ "mfr": "*** ENTER MFR # ***",
+ "dw": "DWAF74886",
+ "client": "Sarah Robinson",
+ "age": 44,
+ "req": "07/01/2026"
+ }
+ ],
+ "count": 2
+ },
+ {
+ "slug": "arte-international",
+ "vid": "ARTE INTERNATIONAL",
+ "name": "Arte International",
+ "items": [
+ {
+ "mfr": "29243",
+ "dw": "DWAR320016",
+ "client": "Suzi Graef",
+ "age": 53,
+ "req": "06/22/2026"
+ },
+ {
+ "mfr": "29243",
+ "dw": "DWAR320016",
+ "client": "rachael roberts",
+ "age": 45,
+ "req": "06/30/2026"
+ }
+ ],
+ "count": 2
+ },
+ {
+ "slug": "schumacher",
+ "vid": "SCHUMACHER",
+ "name": "Schumacher",
+ "items": [
+ {
+ "mfr": "5005652",
+ "dw": "DWLK833920",
+ "client": "Terri Matthews",
+ "age": 39,
+ "req": "07/06/2026"
+ },
+ {
+ "mfr": "5005652",
+ "dw": "DWLK833920",
+ "client": "Terri Matthews",
+ "age": 39,
+ "req": "07/06/2026"
+ }
+ ],
+ "count": 2
+ },
+ {
+ "slug": "sand",
+ "vid": "SAND",
+ "name": "Sandberg",
+ "items": [
+ {
+ "mfr": "ZOW0127-02",
+ "dw": "DWZF187058",
+ "client": "FEDEX",
+ "age": 35,
+ "req": "07/10/2026"
+ },
+ {
+ "mfr": "217209",
+ "dw": "DWWM14281",
+ "client": "Chris Landis",
+ "age": 25,
+ "req": "07/20/2026"
+ }
+ ],
+ "count": 2
+ },
+ {
+ "slug": "con",
+ "vid": "CON",
+ "name": "(vid CON)",
+ "items": [
+ {
+ "mfr": "RD1893FR",
+ "dw": "LIN980008",
+ "client": "Margie Pesicka",
+ "age": 28,
+ "req": "07/17/2026"
+ },
+ {
+ "mfr": "RD1893FR",
+ "dw": "LIN980008",
+ "client": "Margie Pesicka",
+ "age": 28,
+ "req": "07/17/2026"
+ }
+ ],
+ "count": 2
+ },
+ {
+ "slug": "thib",
+ "vid": "THIB",
+ "name": "Thibaut",
+ "items": [
+ {
+ "mfr": "AT24577",
+ "dw": "DWAF65004",
+ "client": "Paola Salomon",
+ "age": 25,
+ "req": "07/20/2026"
+ },
+ {
+ "mfr": "AT24575",
+ "dw": "DWAF65000",
+ "client": "Paola Salomon",
+ "age": 25,
+ "req": "07/20/2026"
+ }
+ ],
+ "count": 2
+ },
+ {
+ "slug": "sandb",
+ "vid": "SANDB",
+ "name": "Sandberg",
+ "items": [
+ {
+ "mfr": "444-66",
+ "dw": "DWSS71172",
+ "client": "Lisa McIlhenny FEDEX",
+ "age": 18,
+ "req": "07/27/2026"
+ },
+ {
+ "mfr": "S10176",
+ "dw": "DWSS74114",
+ "client": "Lisa McIlhenny FEDEX",
+ "age": 18,
+ "req": "07/27/2026"
+ }
+ ],
+ "count": 2
+ },
+ {
+ "slug": "astsand",
+ "vid": "ASTSAND",
+ "name": "(vid ASTSAND)",
+ "items": [
+ {
+ "mfr": "d834",
+ "dw": "PRN62373",
+ "client": "Andrea Reiser",
+ "age": 14,
+ "req": "07/31/2026"
+ },
+ {
+ "mfr": "wk111 - $20.90 net - 1/12/12)",
+ "dw": "REW6211",
+ "client": "JODI MILLER",
+ "age": 11,
+ "req": "08/03/2026"
+ }
+ ],
+ "count": 2
+ },
+ {
+ "slug": "pin",
+ "vid": "PIN",
+ "name": "(vid PIN)",
+ "items": [
+ {
+ "mfr": "F2162020",
+ "dw": "DWPF600399",
+ "client": "Marci Stewart",
+ "age": 14,
+ "req": "07/31/2026"
+ },
+ {
+ "mfr": "B7570001",
+ "dw": "DWPF600091",
+ "client": "Marci Stewart",
+ "age": 14,
+ "req": "07/31/2026"
+ }
+ ],
+ "count": 2
+ },
+ {
+ "slug": "coor",
+ "vid": "COOR",
+ "name": "(vid COOR)",
+ "items": [
+ {
+ "mfr": "B00316 Clover Clay",
+ "dw": "DWDC330786",
+ "client": "Kristine Bennett",
+ "age": 11,
+ "req": "08/03/2026"
+ },
+ {
+ "mfr": "B00317 Clover Cacao",
+ "dw": "DWDC330787",
+ "client": "Kristine Bennett",
+ "age": 11,
+ "req": "08/03/2026"
+ }
+ ],
+ "count": 2
+ },
+ {
+ "slug": "san",
+ "vid": "SAN",
+ "name": "(vid San)",
+ "items": [
+ {
+ "mfr": "AS398571-0, as creations Livingwalls Design Panel the Wall 3 Cream Wallcovering",
+ "dw": "DWAC1010582",
+ "client": "Susan Berghouse",
+ "age": 53,
+ "req": "06/22/2026"
+ }
+ ],
+ "count": 1
+ },
+ {
+ "slug": "bre",
+ "vid": "BRE",
+ "name": "Brewster",
+ "items": [
+ {
+ "mfr": "CHR11676",
+ "dw": "DWJS90185",
+ "client": "Reed Reynolds",
+ "age": 46,
+ "req": "06/29/2026"
+ }
+ ],
+ "count": 1
+ },
+ {
+ "slug": "jofa",
+ "vid": "JOFA",
+ "name": "Lee Jofa",
+ "items": [
+ {
+ "mfr": "2010136.6.0*",
+ "dw": "DWKK134236",
+ "client": "Rachel Kiedrowski",
+ "age": 46,
+ "req": "06/29/2026"
+ }
+ ],
+ "count": 1
+ },
+ {
+ "slug": "rad",
+ "vid": "RAD",
+ "name": "(vid RAD)",
+ "items": [
+ {
+ "mfr": "Dig-4350114V",
+ "dw": "DIg4350114V",
+ "client": "Jenna Quam",
+ "age": 45,
+ "req": "06/30/2026"
+ }
+ ],
+ "count": 1
+ },
+ {
+ "slug": "twi",
+ "vid": "TWI",
+ "name": "(vid TWI)",
+ "items": [
+ {
+ "mfr": "Cabochon Verdigris",
+ "dw": "DWAR320018",
+ "client": "Edidiong Odiong",
+ "age": 39,
+ "req": "07/06/2026"
+ }
+ ],
+ "count": 1
+ },
+ {
+ "slug": "lbi",
+ "vid": "LBI",
+ "name": "(vid LBI)",
+ "items": [
+ {
+ "mfr": "61147",
+ "dw": "HWC61147",
+ "client": "Evelyn Dame",
+ "age": 38,
+ "req": "07/07/2026"
+ }
+ ],
+ "count": 1
+ },
+ {
+ "slug": "pj",
+ "vid": "PJ",
+ "name": "Phillip Jeffries",
+ "items": [
+ {
+ "mfr": "4185",
+ "dw": "DWCPJ4185LA",
+ "client": "Lake Flato Architects",
+ "age": 36,
+ "req": "07/09/2026"
+ }
+ ],
+ "count": 1
+ },
+ {
+ "slug": "brewster",
+ "vid": "BREWSTER",
+ "name": "Brewster",
+ "items": [
+ {
+ "mfr": "2999-39028",
+ "dw": "DWBR170698",
+ "client": "lauren shapiro",
+ "age": 35,
+ "req": "07/10/2026"
+ }
+ ],
+ "count": 1
+ },
+ {
+ "slug": "osb",
+ "vid": "OSB",
+ "name": "Osborne & Little",
+ "items": [
+ {
+ "mfr": "W7330-2-Green Wall-",
+ "dw": "EUR70871",
+ "client": "Lisa McIlhenny FEDEX",
+ "age": 18,
+ "req": "07/27/2026"
+ }
+ ],
+ "count": 1
+ },
+ {
+ "slug": "maya",
+ "vid": "MAYA",
+ "name": "Maya Romanoff",
+ "items": [
+ {
+ "mfr": "FV-21X02",
+ "dw": "Maya50408",
+ "client": "Pierce Condominium c/o FirstService Residential",
+ "age": 18,
+ "req": "07/27/2026"
+ }
+ ],
+ "count": 1
+ },
+ {
+ "slug": "kor",
+ "vid": "KOR",
+ "name": "(vid kor)",
+ "items": [
+ {
+ "mfr": "TAK-CA01-05",
+ "dw": "DWK29028",
+ "client": "Sheryl Repasky",
+ "age": 16,
+ "req": "07/29/2026"
+ }
+ ],
+ "count": 1
+ },
+ {
+ "slug": "rbl",
+ "vid": "RBL",
+ "name": "Rebel Walls",
+ "items": [
+ {
+ "mfr": "RS16171",
+ "dw": "DWRW76354",
+ "client": "Elizabeth Kent",
+ "age": 15,
+ "req": "07/30/2026"
+ }
+ ],
+ "count": 1
+ },
+ {
+ "slug": "olp",
+ "vid": "OLP",
+ "name": "(vid OLP)",
+ "items": [
+ {
+ "mfr": "OP-ART-DECO-WAVES",
+ "dw": "DWCOP-ART-DECO-WAVES",
+ "client": "Andrea Reiser",
+ "age": 14,
+ "req": "07/31/2026"
+ }
+ ],
+ "count": 1
+ }
+ ]
+}
\ No newline at end of file
diff --git a/scripts/build-fleet.js b/scripts/build-fleet.js
new file mode 100644
index 0000000..02231d6
--- /dev/null
+++ b/scripts/build-fleet.js
@@ -0,0 +1,55 @@
+'use strict';
+// Reads a saved `REPORT ON SAMPLES ORDERED` fm_find result and writes data/fleet.json,
+// grouped by vendor (vid), enforcing the 10–60 day window.
+const fs = require('fs');
+const path = require('path');
+
+const src = process.argv[2];
+if (!src) { console.error('usage: node scripts/build-fleet.js <fm-result.txt>'); process.exit(1); }
+const j = JSON.parse(fs.readFileSync(src, 'utf8'));
+const today = new Date('2026-08-14T00:00:00');
+
+// Best-effort vid → display name. Editable in the viewer; unknowns show the raw vid.
+const NAME = {
+ DGD: 'Designers Guild @ Osborne & Little', SCA: 'Scalamandre', KRA: 'Kravet',
+ THIB: 'Thibaut', THI: 'Thibaut', PF: 'Pierre Frey', SPN: 'Spoonflower (online — no rep desk)',
+ WQ: 'WallQuest / Malibu', SCH: 'Schumacher', SCHUMACHER: 'Schumacher', QUA: 'Quadrille',
+ BRE: 'Brewster', BREWSTER: 'Brewster', JOFA: 'Lee Jofa', YOR: 'York', MOR: 'Morris & Co',
+ MAYA: 'Maya Romanoff', PJ: 'Phillip Jeffries', RBL: 'Rebel Walls', OSB: 'Osborne & Little',
+ 'ARTE INTERNATIONAL': 'Arte International', SAND: 'Sandberg', SANDB: 'Sandberg',
+};
+
+const g = {};
+let skippedNoVid = 0, skippedWindow = 0;
+for (const r of j.records) {
+ const d = r.fieldData;
+ const rawvid = (d.vid || '').trim();
+ if (!rawvid) { skippedNoVid++; continue; }
+ const vid = rawvid.toUpperCase();
+ const dt = d['today for client'];
+ const age = dt ? Math.floor((today - new Date(dt)) / 86400000) : null;
+ if (age === null || age <= 10 || age > 60) { skippedWindow++; continue; }
+ const slug = vid.toLowerCase().replace(/[^a-z0-9]+/g, '-');
+ g[slug] = g[slug] || { slug, vid, name: NAME[vid] || `(vid ${rawvid})`, items: [] };
+ g[slug].items.push({
+ mfr: (d['Mfr Pattern'] || '').replace(/\r/g, '').replace(/\($/, '').trim(),
+ dw: (d['combo sku'] || '').trim(),
+ client: (d['Clients::Company'] || d['company for client fileS'] || '').replace(/\r/g, '').trim(),
+ age, req: dt,
+ });
+}
+
+const vendors = Object.values(g)
+ .map(v => ({ ...v, items: v.items.sort((a, b) => b.age - a.age), count: v.items.length }))
+ .sort((a, b) => b.count - a.count);
+
+const out = {
+ generated: '2026-08-14',
+ window: '10–60 days (>60 = dead lead, excluded)',
+ totalItems: vendors.reduce((s, v) => s + v.count, 0),
+ skippedNoVid, skippedWindow,
+ vendors,
+};
+fs.writeFileSync(path.join(__dirname, '..', 'data', 'fleet.json'), JSON.stringify(out, null, 2));
+console.log(`vendors=${vendors.length} items=${out.totalItems} (skipped: no-vid ${skippedNoVid}, out-of-window ${skippedWindow})`);
+vendors.forEach(v => console.log(` ${v.slug.padEnd(14)} ${String(v.count).padStart(2)} ${v.name}`));
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..8ef57ac
--- /dev/null
+++ b/server.js
@@ -0,0 +1,163 @@
+'use strict';
+// Sample Follow-Up Sweep — selection console.
+// Sweep → group → pick vendors (1..all) → enter sample email/account → preview → QUEUE.
+// Queue writes out/send-queue.json; actual info@ Gmail drafts are created on the
+// authenticated MCP path (Claude), so George's external-send gate stays intact.
+
+const http = require('http');
+const fs = require('fs');
+const path = require('path');
+const { compose } = require('./lib/compose');
+
+const ROOT = __dirname;
+const AUTH = 'Basic ' + Buffer.from('admin:DW2024!').toString('base64');
+const SHIP_TO = { name: 'Designer Wallcoverings', line1: '15442 Ventura Blvd. #102', city_state_zip: 'Sherman Oaks, CA 91403', phone: '1-888-373-4564' };
+
+const p = (...a) => path.join(ROOT, ...a);
+const readJSON = (f, d) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return d; } };
+const fleet = () => readJSON(p('data', 'fleet.json'), { vendors: [] });
+const contacts = () => readJSON(p('data', 'contacts.json'), {});
+
+function vendorObj(v, c) {
+ const cc = c[v.slug] || {};
+ return { name: cc.name || v.name, account_number: cc.account_number || '', sample_email: cc.sample_email || '', ship_to: SHIP_TO };
+}
+function draftFor(v, c) {
+ const rows = v.items.map(i => ({ mfr: i.mfr }));
+ return compose(vendorObj(v, c), rows);
+}
+
+function body(req) {
+ return new Promise(res => { let b = ''; req.on('data', d => b += d); req.on('end', () => res(b ? JSON.parse(b) : {})); });
+}
+function send(res, code, obj, type) {
+ res.writeHead(code, { 'Content-Type': type || 'application/json' });
+ res.end(typeof obj === 'string' ? obj : JSON.stringify(obj));
+}
+
+const server = http.createServer(async (req, res) => {
+ if (req.headers.authorization !== AUTH) { res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="DW"' }); return res.end('auth required'); }
+ const u = new URL(req.url, 'http://x');
+
+ if (u.pathname === '/' ) return send(res, 200, PAGE, 'text/html; charset=utf-8');
+ if (u.pathname === '/api/state') return send(res, 200, { fleet: fleet(), contacts: contacts() });
+
+ if (u.pathname === '/api/contact' && req.method === 'POST') {
+ const b = await body(req); const c = contacts();
+ c[b.slug] = Object.assign({}, c[b.slug], { sample_email: b.sample_email || '', account_number: b.account_number || '' });
+ if (b.name) c[b.slug].name = b.name;
+ fs.writeFileSync(p('data', 'contacts.json'), JSON.stringify(c, null, 2));
+ return send(res, 200, { ok: true });
+ }
+
+ if (u.pathname === '/api/preview' && req.method === 'POST') {
+ const b = await body(req); const v = fleet().vendors.find(x => x.slug === b.slug);
+ if (!v) return send(res, 404, { error: 'no vendor' });
+ const d = draftFor(v, contacts());
+ return send(res, 200, { to: d.to, subject: d.subject, html: d.html, count: v.items.length });
+ }
+
+ if (u.pathname === '/api/queue' && req.method === 'POST') {
+ const b = await body(req); const c = contacts(); const vs = fleet().vendors;
+ const picked = (b.slugs || []).map(s => vs.find(v => v.slug === s)).filter(Boolean);
+ const queue = picked.map(v => {
+ const cc = c[v.slug] || {}; const d = draftFor(v, c);
+ const missing = []; if (!cc.sample_email) missing.push('sample_email'); if (!cc.account_number) missing.push('account_number');
+ return { slug: v.slug, name: cc.name || v.name, to: d.to, subject: d.subject, body: d.html, items: v.items.length, ready: missing.length === 0, missing };
+ });
+ fs.mkdirSync(p('out'), { recursive: true });
+ fs.writeFileSync(p('out', 'send-queue.json'), JSON.stringify({ generated: new Date().toISOString(), queue }, null, 2));
+ return send(res, 200, { queued: queue.length, ready: queue.filter(q => q.ready).length, notReady: queue.filter(q => !q.ready).map(q => ({ name: q.name, missing: q.missing })) });
+ }
+
+ send(res, 404, { error: 'not found' });
+});
+
+const srv = server.listen(0, '127.0.0.1', () => {
+ const port = srv.address().port;
+ fs.writeFileSync(p('out', 'viewer-port.txt'), String(port));
+ console.log(`Sample Follow-Up console → http://127.0.0.1:${port}/ (admin / DW2024!)`);
+});
+
+const PAGE = `<!doctype html><html><head><meta charset="utf-8"><title>Sample Follow-Up — Vendor Console</title>
+<style>
+ :root{--b:#e2e2e2}
+ body{margin:0;font-family:Arial,Helvetica,sans-serif;color:#1c1c1c;background:#f4f4f5}
+ header{position:sticky;top:0;background:#fff;border-bottom:1px solid var(--b);padding:14px 20px;z-index:5}
+ h1{font-size:17px;margin:0 0 4px} .sub{color:#777;font-size:13px}
+ .bar{display:flex;gap:10px;align-items:center;margin-top:10px;flex-wrap:wrap}
+ button{font:inherit;padding:8px 14px;border:1px solid #bbb;border-radius:6px;background:#fff;cursor:pointer}
+ button.primary{background:#111;color:#fff;border-color:#111}
+ select,input{font:inherit;padding:6px 8px;border:1px solid #bbb;border-radius:6px}
+ table{border-collapse:collapse;width:100%;background:#fff}
+ th,td{border-bottom:1px solid var(--b);padding:8px 10px;font-size:13px;text-align:left;vertical-align:middle}
+ th{background:#fafafa;position:sticky;top:96px;color:#555;font-weight:600}
+ .badge{font-size:11px;padding:2px 8px;border-radius:20px;font-weight:700}
+ .ok{background:#e5f6ea;color:#137a34} .need{background:#fdf0dd;color:#9a6b12} .dead{background:#eee;color:#888}
+ .vid{color:#999;font-size:11px}
+ input.em{width:190px} input.ac{width:100px}
+ .wrap{padding:0 20px 40px}
+ #modal{position:fixed;inset:0;background:rgba(0,0,0,.4);display:none;align-items:center;justify-content:center;z-index:20}
+ #modal .card{background:#fff;max-width:680px;width:92%;max-height:86vh;overflow:auto;border-radius:10px;padding:22px 26px}
+ #status{font-size:13px;color:#137a34}
+ .muted{color:#999;font-size:12px}
+</style></head><body>
+<header>
+ <h1>Sample Follow-Up — Vendor Console</h1>
+ <div class="sub" id="meta">loading…</div>
+ <div class="bar">
+ <label><input type="checkbox" id="all"> select all</label>
+ <select id="sort">
+ <option value="items">Sort: most items</option>
+ <option value="age">Sort: oldest first</option>
+ <option value="name">Sort: vendor A–Z</option>
+ <option value="ready">Sort: ready first</option>
+ </select>
+ <button class="primary" id="queueBtn">Queue selected → Drafts</button>
+ <span id="status"></span>
+ </div>
+</header>
+<div class="wrap"><table><thead><tr>
+ <th></th><th>Vendor</th><th>Items</th><th>Oldest</th><th>Status</th><th>Sample email</th><th>DW acct #</th><th></th>
+</tr></thead><tbody id="rows"></tbody></table>
+<p class="muted">Queue writes <code>out/send-queue.json</code>. Ready = sample email + acct present. Then in chat: “create the queued drafts” → they land in info@ Drafts for you to send. 15 rows had no vendor id and aren’t shown.</p>
+</div>
+<div id="modal" onclick="if(event.target.id==='modal')this.style.display='none'"><div class="card" id="mbody"></div></div>
+<script>
+let S={fleet:{vendors:[]},contacts:{}};
+const sortKey=localStorage.getItem('sfsort')||'items';
+async function load(){const r=await fetch('/api/state');S=await r.json();document.getElementById('sort').value=sortKey;render();
+ document.getElementById('meta').textContent=S.fleet.vendors.length+' vendors · '+S.fleet.totalItems+' items · window '+S.fleet.window;}
+function ready(v){const c=S.contacts[v.slug]||{};return c.sample_email&&c.account_number;}
+function sorted(){const k=document.getElementById('sort').value;localStorage.setItem('sfsort',k);const a=[...S.fleet.vendors];
+ if(k==='items')a.sort((x,y)=>y.count-x.count);if(k==='age')a.sort((x,y)=>y.items[0].age-x.items[0].age);
+ if(k==='name')a.sort((x,y)=>x.name.localeCompare(y.name));if(k==='ready')a.sort((x,y)=>(ready(y)?1:0)-(ready(x)?1:0));return a;}
+function render(){const t=document.getElementById('rows');t.innerHTML='';for(const v of sorted()){const c=S.contacts[v.slug]||{};const r=ready(v);
+ const tr=document.createElement('tr');tr.innerHTML=
+ '<td><input type=checkbox class=pick data-slug="'+v.slug+'"></td>'+
+ '<td><b>'+esc(v.name)+'</b><div class=vid>'+esc(v.vid)+'</div></td>'+
+ '<td>'+v.count+'</td><td>'+v.items[0].age+'d</td>'+
+ '<td><span class="badge '+(r?'ok':'need')+'">'+(r?'READY':'NEEDS')+'</span></td>'+
+ '<td><input class="em" data-slug="'+v.slug+'" data-f="sample_email" value="'+esc(c.sample_email||'')+'" placeholder="sample email"></td>'+
+ '<td><input class="ac" data-slug="'+v.slug+'" data-f="account_number" value="'+esc(c.account_number||'')+'" placeholder="acct #"></td>'+
+ '<td><button data-prev="'+v.slug+'">preview</button></td>';
+ t.appendChild(tr);}
+ t.querySelectorAll('input.em,input.ac').forEach(i=>i.addEventListener('change',saveContact));
+ t.querySelectorAll('button[data-prev]').forEach(b=>b.addEventListener('click',()=>preview(b.dataset.prev)));}
+async function saveContact(e){const el=e.target;const slug=el.dataset.slug;const row=[...document.querySelectorAll('input[data-slug="'+slug+'"]')];
+ const em=row.find(x=>x.dataset.f==='sample_email').value.trim();const ac=row.find(x=>x.dataset.f==='account_number').value.trim();
+ await fetch('/api/contact',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({slug,sample_email:em,account_number:ac})});
+ S.contacts[slug]=Object.assign({},S.contacts[slug],{sample_email:em,account_number:ac});render();}
+async function preview(slug){const r=await fetch('/api/preview',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({slug})});const d=await r.json();
+ document.getElementById('mbody').innerHTML='<div class=muted>To: '+esc(d.to)+' · '+d.count+' items</div><hr>'+d.html;document.getElementById('modal').style.display='flex';}
+document.getElementById('all').addEventListener('change',e=>{document.querySelectorAll('.pick').forEach(c=>c.checked=e.target.checked);});
+document.getElementById('sort').addEventListener('change',render);
+document.getElementById('queueBtn').addEventListener('click',async()=>{
+ const slugs=[...document.querySelectorAll('.pick:checked')].map(c=>c.dataset.slug);
+ if(!slugs.length){alert('Select at least one vendor.');return;}
+ const r=await fetch('/api/queue',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({slugs})});const d=await r.json();
+ let m='Queued '+d.queued+' vendor(s): '+d.ready+' ready to draft.';if(d.notReady.length)m+=' Needs fields: '+d.notReady.map(x=>x.name).join(', ')+'.';
+ m+=' → Now tell Claude in chat: “create the queued drafts”.';document.getElementById('status').textContent=m;});
+function esc(s){return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
+load();
+</script></body></html>`;
← d8d9ad8 Add 60-day dead-lead ceiling + next vendor (Scalamandre) fro
·
back to Sample Followup Sweep
·
Console: re-request detection + initial-request date, expand 55d6879 →