[object Object]

← back to Permit Radar

permit-radar: full permit directory + lead funnel + admin — working prototype

de185e87da5fc00ecec2c1e7db05f1ca74263d46 · 2026-07-15 17:02:40 -0700 · Steve

Files touched

Diff

commit de185e87da5fc00ecec2c1e7db05f1ca74263d46
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jul 15 17:02:40 2026 -0700

    permit-radar: full permit directory + lead funnel + admin — working prototype
---
 data/lead_requests.json |   1 +
 data/permits.json       | 952 ++++++++++++++++++++++++++++++++++++++++++++++++
 data/products.json      |   6 -
 data/subscribers.json   |   1 +
 public/admin.html       | 241 ++++++++++++
 public/index.html       | 605 +++++++++++++++++++++++++++---
 public/permit.html      | 225 ++++++++++++
 scripts/ingest-stub.js  | 150 ++++++++
 server.js               | 292 +++++++++++++--
 9 files changed, 2385 insertions(+), 88 deletions(-)

diff --git a/data/lead_requests.json b/data/lead_requests.json
new file mode 100644
index 0000000..fe51488
--- /dev/null
+++ b/data/lead_requests.json
@@ -0,0 +1 @@
+[]
diff --git a/data/permits.json b/data/permits.json
new file mode 100644
index 0000000..79d2ff2
--- /dev/null
+++ b/data/permits.json
@@ -0,0 +1,952 @@
+[
+  {
+    "id": "PR-2024-001",
+    "address": "1425 Oak St",
+    "city": "Los Angeles",
+    "jurisdiction": "Los Angeles County",
+    "state": "CA",
+    "zip": "90041",
+    "date_filed": "2024-11-15",
+    "permit_type": "kitchen_remodel",
+    "permit_type_label": "Kitchen Remodel",
+    "valuation": 62000,
+    "contractor": "Pacific Coast Construction",
+    "contractor_license": "CA-LIC-882341",
+    "status": "approved",
+    "description": "Complete kitchen remodel including new cabinets, countertops, appliances, plumbing and electrical updates. Demo existing kitchen, install new layout.",
+    "bedrooms": 3,
+    "sqft_affected": 280,
+    "created_at": "2024-11-15T08:23:11Z"
+  },
+  {
+    "id": "PR-2024-002",
+    "address": "890 Willow Ave",
+    "city": "Pasadena",
+    "jurisdiction": "Los Angeles County",
+    "state": "CA",
+    "zip": "91104",
+    "date_filed": "2024-11-20",
+    "permit_type": "addition",
+    "permit_type_label": "Room Addition",
+    "valuation": 185000,
+    "contractor": "Apex Home Builders",
+    "contractor_license": "CA-LIC-774821",
+    "status": "pending",
+    "description": "400 sq ft room addition on rear of property. New family room and full bathroom. Foundation, framing, roofing, electrical, HVAC, plumbing.",
+    "bedrooms": 3,
+    "sqft_affected": 400,
+    "created_at": "2024-11-20T10:45:00Z"
+  },
+  {
+    "id": "PR-2024-003",
+    "address": "312 Magnolia Blvd",
+    "city": "Burbank",
+    "jurisdiction": "Los Angeles County",
+    "state": "CA",
+    "zip": "91502",
+    "date_filed": "2024-11-22",
+    "permit_type": "bath_remodel",
+    "permit_type_label": "Bathroom Remodel",
+    "valuation": 28500,
+    "contractor": "",
+    "contractor_license": "",
+    "status": "approved",
+    "description": "Owner-builder bathroom remodel. New tile, vanity, fixtures, updated plumbing supply lines and drain.",
+    "bedrooms": 2,
+    "sqft_affected": 95,
+    "created_at": "2024-11-22T14:10:00Z"
+  },
+  {
+    "id": "PR-2024-004",
+    "address": "5501 Venice Blvd",
+    "city": "Culver City",
+    "jurisdiction": "Los Angeles County",
+    "state": "CA",
+    "zip": "90232",
+    "date_filed": "2024-11-28",
+    "permit_type": "reroof",
+    "permit_type_label": "Reroof",
+    "valuation": 18000,
+    "contractor": "SoCal Roofing Solutions",
+    "contractor_license": "CA-LIC-665123",
+    "status": "approved",
+    "description": "Complete tear-off and reroof. Install Class A composition shingles over new 30-lb felt. 2,400 sq ft.",
+    "bedrooms": 4,
+    "sqft_affected": 2400,
+    "created_at": "2024-11-28T09:00:00Z"
+  },
+  {
+    "id": "PR-2024-005",
+    "address": "7720 Sunset Dr",
+    "city": "West Hollywood",
+    "jurisdiction": "Los Angeles County",
+    "state": "CA",
+    "zip": "90046",
+    "date_filed": "2024-12-03",
+    "permit_type": "full_remodel",
+    "permit_type_label": "Full Interior Remodel",
+    "valuation": 410000,
+    "contractor": "Urban Renovations Group",
+    "contractor_license": "CA-LIC-991234",
+    "status": "pending",
+    "description": "Whole-house gut remodel. Open floor plan conversion, new kitchen, 2 bath remodels, flooring throughout, electrical panel upgrade, new HVAC.",
+    "bedrooms": 3,
+    "sqft_affected": 1850,
+    "created_at": "2024-12-03T11:22:00Z"
+  },
+  {
+    "id": "PR-2024-006",
+    "address": "44 Hillcrest Rd",
+    "city": "San Marino",
+    "jurisdiction": "Los Angeles County",
+    "state": "CA",
+    "zip": "91108",
+    "date_filed": "2024-12-05",
+    "permit_type": "adu",
+    "permit_type_label": "ADU / Accessory Dwelling Unit",
+    "valuation": 225000,
+    "contractor": "SoCal ADU Specialists",
+    "contractor_license": "CA-LIC-554892",
+    "status": "approved",
+    "description": "Detached ADU construction, 640 sq ft. Full kitchen, bath, bedroom. Solar-ready. Separate utility meters.",
+    "bedrooms": 1,
+    "sqft_affected": 640,
+    "created_at": "2024-12-05T15:30:00Z"
+  },
+  {
+    "id": "PR-2024-007",
+    "address": "2201 Palm Ln",
+    "city": "Santa Monica",
+    "jurisdiction": "Los Angeles County",
+    "state": "CA",
+    "zip": "90404",
+    "date_filed": "2024-12-08",
+    "permit_type": "kitchen_remodel",
+    "permit_type_label": "Kitchen Remodel",
+    "valuation": 95000,
+    "contractor": "Shoreline Design Build",
+    "contractor_license": "CA-LIC-773401",
+    "status": "approved",
+    "description": "High-end kitchen remodel. Custom cabinetry, quartzite counters, Wolf appliances, new island, updated electrical and plumbing.",
+    "bedrooms": 4,
+    "sqft_affected": 360,
+    "created_at": "2024-12-08T09:55:00Z"
+  },
+  {
+    "id": "PR-2024-008",
+    "address": "1803 Elm St",
+    "city": "Glendale",
+    "jurisdiction": "Los Angeles County",
+    "state": "CA",
+    "zip": "91205",
+    "date_filed": "2024-12-10",
+    "permit_type": "addition",
+    "permit_type_label": "Room Addition",
+    "valuation": 145000,
+    "contractor": "Ararat Construction",
+    "contractor_license": "CA-LIC-884561",
+    "status": "pending",
+    "description": "350 sq ft master suite addition. Bedroom, walk-in closet, full ensuite bath. Ties into existing foundation, matching exterior.",
+    "bedrooms": 3,
+    "sqft_affected": 350,
+    "created_at": "2024-12-10T13:15:00Z"
+  },
+  {
+    "id": "PR-2024-009",
+    "address": "6642 Harbor View Ct",
+    "city": "Long Beach",
+    "jurisdiction": "Los Angeles County",
+    "state": "CA",
+    "zip": "90803",
+    "date_filed": "2024-12-12",
+    "permit_type": "bath_remodel",
+    "permit_type_label": "Bathroom Remodel",
+    "valuation": 42000,
+    "contractor": "Harbor Home Pros",
+    "contractor_license": "CA-LIC-662019",
+    "status": "approved",
+    "description": "Primary bath luxury remodel. Freestanding tub, heated tile floors, frameless shower enclosure, dual vanity, smart mirrors.",
+    "bedrooms": 4,
+    "sqft_affected": 140,
+    "created_at": "2024-12-12T08:45:00Z"
+  },
+  {
+    "id": "PR-2024-010",
+    "address": "903 El Camino Real",
+    "city": "Palo Alto",
+    "jurisdiction": "Santa Clara County",
+    "state": "CA",
+    "zip": "94301",
+    "date_filed": "2024-11-14",
+    "permit_type": "full_remodel",
+    "permit_type_label": "Full Interior Remodel",
+    "valuation": 680000,
+    "contractor": "Bay Area Design Build",
+    "contractor_license": "CA-LIC-443210",
+    "status": "approved",
+    "description": "Complete gut remodel of 2,400 sq ft home. Open kitchen/living, 3 bath remodels, smart home system, radiant heating, solar + battery.",
+    "bedrooms": 4,
+    "sqft_affected": 2400,
+    "created_at": "2024-11-14T10:00:00Z"
+  },
+  {
+    "id": "PR-2024-011",
+    "address": "2120 Forest Ave",
+    "city": "San Jose",
+    "jurisdiction": "Santa Clara County",
+    "state": "CA",
+    "zip": "95128",
+    "date_filed": "2024-11-18",
+    "permit_type": "kitchen_remodel",
+    "permit_type_label": "Kitchen Remodel",
+    "valuation": 78000,
+    "contractor": "Silicon Valley Renovations",
+    "contractor_license": "CA-LIC-557234",
+    "status": "approved",
+    "description": "Kitchen expansion and remodel. Removed wall to dining room, new island, quartz counters, custom cabinets, Bosch appliance package.",
+    "bedrooms": 3,
+    "sqft_affected": 310,
+    "created_at": "2024-11-18T11:30:00Z"
+  },
+  {
+    "id": "PR-2024-012",
+    "address": "540 Creekside Dr",
+    "city": "Sunnyvale",
+    "jurisdiction": "Santa Clara County",
+    "state": "CA",
+    "zip": "94087",
+    "date_filed": "2024-11-25",
+    "permit_type": "adu",
+    "permit_type_label": "ADU / Accessory Dwelling Unit",
+    "valuation": 195000,
+    "contractor": "NextHome Build",
+    "contractor_license": "CA-LIC-664578",
+    "status": "pending",
+    "description": "Garage conversion ADU, 480 sq ft. Full bath, kitchen, studio layout. Insulation, electrical upgrade, mini-split HVAC.",
+    "bedrooms": 1,
+    "sqft_affected": 480,
+    "created_at": "2024-11-25T14:00:00Z"
+  },
+  {
+    "id": "PR-2024-013",
+    "address": "7801 Mission Hills Dr",
+    "city": "San Diego",
+    "jurisdiction": "San Diego County",
+    "state": "CA",
+    "zip": "92120",
+    "date_filed": "2024-12-01",
+    "permit_type": "full_remodel",
+    "permit_type_label": "Full Interior Remodel",
+    "valuation": 320000,
+    "contractor": "SoCal Premier Builders",
+    "contractor_license": "CA-LIC-778820",
+    "status": "approved",
+    "description": "1,800 sq ft complete remodel. Kitchen, 2 baths, all new flooring, windows, doors, interior painting, electrical updates.",
+    "bedrooms": 3,
+    "sqft_affected": 1800,
+    "created_at": "2024-12-01T09:00:00Z"
+  },
+  {
+    "id": "PR-2024-014",
+    "address": "441 Coronado Ave",
+    "city": "Coronado",
+    "jurisdiction": "San Diego County",
+    "state": "CA",
+    "zip": "92118",
+    "date_filed": "2024-12-06",
+    "permit_type": "addition",
+    "permit_type_label": "Room Addition",
+    "valuation": 520000,
+    "contractor": "Coronado Custom Homes",
+    "contractor_license": "CA-LIC-889933",
+    "status": "pending",
+    "description": "Two-story addition. Ground floor: expanded kitchen and living area. Second floor: new primary suite with ocean views and spa bath.",
+    "bedrooms": 4,
+    "sqft_affected": 900,
+    "created_at": "2024-12-06T12:00:00Z"
+  },
+  {
+    "id": "PR-2024-015",
+    "address": "1102 Lemon Ave",
+    "city": "Escondido",
+    "jurisdiction": "San Diego County",
+    "state": "CA",
+    "zip": "92025",
+    "date_filed": "2024-12-09",
+    "permit_type": "reroof",
+    "permit_type_label": "Reroof",
+    "valuation": 14500,
+    "contractor": "Sun Country Roofing",
+    "contractor_license": "CA-LIC-331028",
+    "status": "approved",
+    "description": "Full reroof, 1,900 sq ft. Dimensional shingles, new underlayment, replace valley flashing.",
+    "bedrooms": 3,
+    "sqft_affected": 1900,
+    "created_at": "2024-12-09T07:30:00Z"
+  },
+  {
+    "id": "PR-2024-016",
+    "address": "309 Redwood Ct",
+    "city": "Berkeley",
+    "jurisdiction": "Alameda County",
+    "state": "CA",
+    "zip": "94705",
+    "date_filed": "2024-11-19",
+    "permit_type": "kitchen_remodel",
+    "permit_type_label": "Kitchen Remodel",
+    "valuation": 88000,
+    "contractor": "East Bay Remodel Co",
+    "contractor_license": "CA-LIC-445621",
+    "status": "approved",
+    "description": "Craftsman kitchen remodel to match historic character. Custom painted shaker cabinets, butcher block island, farmhouse sink, vintage-style hardware.",
+    "bedrooms": 3,
+    "sqft_affected": 290,
+    "created_at": "2024-11-19T09:20:00Z"
+  },
+  {
+    "id": "PR-2024-017",
+    "address": "1500 Telegraph Ave",
+    "city": "Oakland",
+    "jurisdiction": "Alameda County",
+    "state": "CA",
+    "zip": "94612",
+    "date_filed": "2024-11-29",
+    "permit_type": "bath_remodel",
+    "permit_type_label": "Bathroom Remodel",
+    "valuation": 33000,
+    "contractor": "Bay Tile & Bath",
+    "contractor_license": "CA-LIC-556709",
+    "status": "approved",
+    "description": "Master bath remodel. Heated tile floors, walk-in shower with rainhead, soaking tub, floating vanity.",
+    "bedrooms": 3,
+    "sqft_affected": 110,
+    "created_at": "2024-11-29T10:00:00Z"
+  },
+  {
+    "id": "PR-2024-018",
+    "address": "8803 Fremont Blvd",
+    "city": "Fremont",
+    "jurisdiction": "Alameda County",
+    "state": "CA",
+    "zip": "94538",
+    "date_filed": "2024-12-04",
+    "permit_type": "adu",
+    "permit_type_label": "ADU / Accessory Dwelling Unit",
+    "valuation": 165000,
+    "contractor": "Bay ADU Builders",
+    "contractor_license": "CA-LIC-664412",
+    "status": "pending",
+    "description": "New detached ADU, 500 sq ft. One bedroom, full bath, kitchenette. Separate entrance and utility meter.",
+    "bedrooms": 1,
+    "sqft_affected": 500,
+    "created_at": "2024-12-04T13:40:00Z"
+  },
+  {
+    "id": "PR-2024-019",
+    "address": "221 Maple Way",
+    "city": "Scottsdale",
+    "jurisdiction": "Maricopa County",
+    "state": "AZ",
+    "zip": "85251",
+    "date_filed": "2024-11-21",
+    "permit_type": "full_remodel",
+    "permit_type_label": "Full Interior Remodel",
+    "valuation": 275000,
+    "contractor": "Desert Modern Construction",
+    "contractor_license": "AZ-LIC-220183",
+    "status": "approved",
+    "description": "Modern desert remodel. Open plan kitchen-living, 2 bath remodels, new HVAC, pool deck expansion, outdoor kitchen rough-in.",
+    "bedrooms": 4,
+    "sqft_affected": 2100,
+    "created_at": "2024-11-21T11:00:00Z"
+  },
+  {
+    "id": "PR-2024-020",
+    "address": "5500 Saguaro Ln",
+    "city": "Tempe",
+    "jurisdiction": "Maricopa County",
+    "state": "AZ",
+    "zip": "85282",
+    "date_filed": "2024-11-26",
+    "permit_type": "kitchen_remodel",
+    "permit_type_label": "Kitchen Remodel",
+    "valuation": 55000,
+    "contractor": "Sun State Remodelers",
+    "contractor_license": "AZ-LIC-331840",
+    "status": "approved",
+    "description": "Kitchen remodel and layout change. Open to living area, quartz counters, soft-close cabinets, under-cabinet lighting, gas line for range.",
+    "bedrooms": 3,
+    "sqft_affected": 240,
+    "created_at": "2024-11-26T08:15:00Z"
+  },
+  {
+    "id": "PR-2024-021",
+    "address": "1234 Cactus Rd",
+    "city": "Mesa",
+    "jurisdiction": "Maricopa County",
+    "state": "AZ",
+    "zip": "85205",
+    "date_filed": "2024-12-02",
+    "permit_type": "addition",
+    "permit_type_label": "Room Addition",
+    "valuation": 105000,
+    "contractor": "Mesa Home Builders",
+    "contractor_license": "AZ-LIC-447291",
+    "status": "pending",
+    "description": "250 sq ft sunroom addition on south side. Insulated slab, vinyl windows, mini-split, connects to main living area.",
+    "bedrooms": 3,
+    "sqft_affected": 250,
+    "created_at": "2024-12-02T14:20:00Z"
+  },
+  {
+    "id": "PR-2024-022",
+    "address": "892 Pinecrest Ave",
+    "city": "Flagstaff",
+    "jurisdiction": "Coconino County",
+    "state": "AZ",
+    "zip": "86001",
+    "date_filed": "2024-12-07",
+    "permit_type": "reroof",
+    "permit_type_label": "Reroof",
+    "valuation": 22000,
+    "contractor": "Mountain Roofing Pros",
+    "contractor_license": "AZ-LIC-559018",
+    "status": "approved",
+    "description": "Full tear-off reroof. Snow-rated metal standing seam panels, ice-and-water shield, 2,800 sq ft mountain property.",
+    "bedrooms": 4,
+    "sqft_affected": 2800,
+    "created_at": "2024-12-07T09:10:00Z"
+  },
+  {
+    "id": "PR-2024-023",
+    "address": "10 Lakeview Terr",
+    "city": "Chicago",
+    "jurisdiction": "Cook County",
+    "state": "IL",
+    "zip": "60657",
+    "date_filed": "2024-11-13",
+    "permit_type": "kitchen_remodel",
+    "permit_type_label": "Kitchen Remodel",
+    "valuation": 71000,
+    "contractor": "Windy City Renovations",
+    "contractor_license": "IL-LIC-334120",
+    "status": "approved",
+    "description": "Gut kitchen remodel in vintage greystome condo. Custom millwork, Thermador appliances, marble herringbone backsplash, concealed range hood.",
+    "bedrooms": 2,
+    "sqft_affected": 250,
+    "created_at": "2024-11-13T10:30:00Z"
+  },
+  {
+    "id": "PR-2024-024",
+    "address": "4411 N Clark St",
+    "city": "Chicago",
+    "jurisdiction": "Cook County",
+    "state": "IL",
+    "zip": "60640",
+    "date_filed": "2024-11-30",
+    "permit_type": "addition",
+    "permit_type_label": "Room Addition",
+    "valuation": 315000,
+    "contractor": "Lakeside Build Group",
+    "contractor_license": "IL-LIC-556034",
+    "status": "pending",
+    "description": "Rear 2-story addition. New family room + mudroom at grade, primary suite above. Tuckpointing to match existing brick.",
+    "bedrooms": 3,
+    "sqft_affected": 600,
+    "created_at": "2024-11-30T12:45:00Z"
+  },
+  {
+    "id": "PR-2024-025",
+    "address": "820 Sherman Ave",
+    "city": "Evanston",
+    "jurisdiction": "Cook County",
+    "state": "IL",
+    "zip": "60202",
+    "date_filed": "2024-12-11",
+    "permit_type": "bath_remodel",
+    "permit_type_label": "Bathroom Remodel",
+    "valuation": 38500,
+    "contractor": "North Shore Baths",
+    "contractor_license": "IL-LIC-778823",
+    "status": "approved",
+    "description": "Complete master bath remodel. Radiant floor heat, custom tile shower, soaking tub, dual marble vanity.",
+    "bedrooms": 4,
+    "sqft_affected": 130,
+    "created_at": "2024-12-11T11:00:00Z"
+  },
+  {
+    "id": "PR-2024-026",
+    "address": "3302 W 5th Ave",
+    "city": "Austin",
+    "jurisdiction": "Travis County",
+    "state": "TX",
+    "zip": "78703",
+    "date_filed": "2024-11-16",
+    "permit_type": "full_remodel",
+    "permit_type_label": "Full Interior Remodel",
+    "valuation": 295000,
+    "contractor": "ATX Renovation Co",
+    "contractor_license": "TX-LIC-334871",
+    "status": "approved",
+    "description": "Full interior remodel, mid-century modern home. Open floor plan, custom kitchen, 2 new baths, polished concrete floors, clerestory windows.",
+    "bedrooms": 3,
+    "sqft_affected": 1750,
+    "created_at": "2024-11-16T14:00:00Z"
+  },
+  {
+    "id": "PR-2024-027",
+    "address": "9910 Balcones Dr",
+    "city": "Austin",
+    "jurisdiction": "Travis County",
+    "state": "TX",
+    "zip": "78759",
+    "date_filed": "2024-11-27",
+    "permit_type": "kitchen_remodel",
+    "permit_type_label": "Kitchen Remodel",
+    "valuation": 68000,
+    "contractor": "Hill Country Builders",
+    "contractor_license": "TX-LIC-558720",
+    "status": "pending",
+    "description": "Kitchen remodel. Waterfall island, leathered granite, commercial range, pot-filler, wine fridge, new recessed lighting plan.",
+    "bedrooms": 4,
+    "sqft_affected": 320,
+    "created_at": "2024-11-27T09:30:00Z"
+  },
+  {
+    "id": "PR-2024-028",
+    "address": "401 Lamar Blvd",
+    "city": "Round Rock",
+    "jurisdiction": "Williamson County",
+    "state": "TX",
+    "zip": "78664",
+    "date_filed": "2024-12-13",
+    "permit_type": "adu",
+    "permit_type_label": "ADU / Accessory Dwelling Unit",
+    "valuation": 155000,
+    "contractor": "Central TX ADU Co",
+    "contractor_license": "TX-LIC-667041",
+    "status": "approved",
+    "description": "Detached garage conversion to ADU, 520 sq ft. Studio plan, full bath, kitchenette. New electrical service, spray foam insulation, mini-split.",
+    "bedrooms": 1,
+    "sqft_affected": 520,
+    "created_at": "2024-12-13T10:10:00Z"
+  },
+  {
+    "id": "PR-2024-029",
+    "address": "1889 Royal Ln",
+    "city": "Dallas",
+    "jurisdiction": "Dallas County",
+    "state": "TX",
+    "zip": "75229",
+    "date_filed": "2024-11-23",
+    "permit_type": "full_remodel",
+    "permit_type_label": "Full Interior Remodel",
+    "valuation": 440000,
+    "contractor": "Magnolia Design Build",
+    "contractor_license": "TX-LIC-882019",
+    "status": "approved",
+    "description": "Luxury remodel, 2,600 sq ft. Designer kitchen, 3 bath remodels, primary suite expansion, new hardwoods, smart home integration.",
+    "bedrooms": 4,
+    "sqft_affected": 2600,
+    "created_at": "2024-11-23T11:45:00Z"
+  },
+  {
+    "id": "PR-2024-030",
+    "address": "5550 Lemmon Ave",
+    "city": "Dallas",
+    "jurisdiction": "Dallas County",
+    "state": "TX",
+    "zip": "75209",
+    "date_filed": "2024-12-14",
+    "permit_type": "bath_remodel",
+    "permit_type_label": "Bathroom Remodel",
+    "valuation": 52000,
+    "contractor": "Dallas Bath Studio",
+    "contractor_license": "TX-LIC-773920",
+    "status": "approved",
+    "description": "Primary bath remodel. Walk-in steam shower, sculptural soaking tub, book-matched marble walls, heated floors, lighted niche shelving.",
+    "bedrooms": 4,
+    "sqft_affected": 175,
+    "created_at": "2024-12-14T08:00:00Z"
+  },
+  {
+    "id": "PR-2024-031",
+    "address": "724 NW 28th Ave",
+    "city": "Portland",
+    "jurisdiction": "Multnomah County",
+    "state": "OR",
+    "zip": "97210",
+    "date_filed": "2024-11-17",
+    "permit_type": "kitchen_remodel",
+    "permit_type_label": "Kitchen Remodel",
+    "valuation": 82000,
+    "contractor": "PDX Design Build",
+    "contractor_license": "OR-LIC-445231",
+    "status": "approved",
+    "description": "Craftsman kitchen remodel. Painted cabinets, soapstone counters, La Cornue range, butlers pantry addition, natural wood accents.",
+    "bedrooms": 3,
+    "sqft_affected": 275,
+    "created_at": "2024-11-17T10:00:00Z"
+  },
+  {
+    "id": "PR-2024-032",
+    "address": "2110 Hawthorne Blvd",
+    "city": "Portland",
+    "jurisdiction": "Multnomah County",
+    "state": "OR",
+    "zip": "97214",
+    "date_filed": "2024-11-24",
+    "permit_type": "addition",
+    "permit_type_label": "Room Addition",
+    "valuation": 210000,
+    "contractor": "Rose City Builders",
+    "contractor_license": "OR-LIC-556840",
+    "status": "pending",
+    "description": "Rear addition, 500 sq ft. Ground floor: expanded dining and mudroom. Second floor: two new bedrooms with shared bath.",
+    "bedrooms": 2,
+    "sqft_affected": 500,
+    "created_at": "2024-11-24T13:00:00Z"
+  },
+  {
+    "id": "PR-2024-033",
+    "address": "8030 SE Division St",
+    "city": "Portland",
+    "jurisdiction": "Multnomah County",
+    "state": "OR",
+    "zip": "97206",
+    "date_filed": "2024-12-16",
+    "permit_type": "reroof",
+    "permit_type_label": "Reroof",
+    "valuation": 16500,
+    "contractor": "Pacific Northwest Roofing",
+    "contractor_license": "OR-LIC-663012",
+    "status": "approved",
+    "description": "Complete reroof with Owens Corning Duration shingles. Ice dam protection, new ridge cap, replace all pipe flashings.",
+    "bedrooms": 3,
+    "sqft_affected": 2100,
+    "created_at": "2024-12-16T08:30:00Z"
+  },
+  {
+    "id": "PR-2024-034",
+    "address": "4450 Meridian Ave N",
+    "city": "Seattle",
+    "jurisdiction": "King County",
+    "state": "WA",
+    "zip": "98103",
+    "date_filed": "2024-11-12",
+    "permit_type": "full_remodel",
+    "permit_type_label": "Full Interior Remodel",
+    "valuation": 580000,
+    "contractor": "Emerald City Renovations",
+    "contractor_license": "WA-LIC-334870",
+    "status": "approved",
+    "description": "Complete 2,300 sq ft remodel, 1940s craftsman. Structural updates, new kitchen, 2 bath remodels, primary suite, smart home, new windows.",
+    "bedrooms": 4,
+    "sqft_affected": 2300,
+    "created_at": "2024-11-12T09:00:00Z"
+  },
+  {
+    "id": "PR-2024-035",
+    "address": "1610 Queen Anne Ave N",
+    "city": "Seattle",
+    "jurisdiction": "King County",
+    "state": "WA",
+    "zip": "98109",
+    "date_filed": "2024-11-22",
+    "permit_type": "adu",
+    "permit_type_label": "ADU / Accessory Dwelling Unit",
+    "valuation": 210000,
+    "contractor": "Seattle ADU Pros",
+    "contractor_license": "WA-LIC-556203",
+    "status": "approved",
+    "description": "DADU (detached ADU) 600 sq ft. Full kitchen, bath, living, loft sleeping. Fiber cement siding to match main house. EV charger.",
+    "bedrooms": 1,
+    "sqft_affected": 600,
+    "created_at": "2024-11-22T11:30:00Z"
+  },
+  {
+    "id": "PR-2024-036",
+    "address": "310 Bellevue Way NE",
+    "city": "Bellevue",
+    "jurisdiction": "King County",
+    "state": "WA",
+    "zip": "98004",
+    "date_filed": "2024-12-15",
+    "permit_type": "kitchen_remodel",
+    "permit_type_label": "Kitchen Remodel",
+    "valuation": 115000,
+    "contractor": "Eastside Luxury Remodel",
+    "contractor_license": "WA-LIC-770234",
+    "status": "approved",
+    "description": "Ultra-luxury kitchen remodel. Full overlay custom cabinetry, Calacatta marble, Sub-Zero/Wolf appliances, butler's pantry, wine refrigeration.",
+    "bedrooms": 5,
+    "sqft_affected": 420,
+    "created_at": "2024-12-15T10:45:00Z"
+  },
+  {
+    "id": "PR-2024-037",
+    "address": "612 Piedmont Ave NE",
+    "city": "Atlanta",
+    "jurisdiction": "Fulton County",
+    "state": "GA",
+    "zip": "30308",
+    "date_filed": "2024-11-15",
+    "permit_type": "bath_remodel",
+    "permit_type_label": "Bathroom Remodel",
+    "valuation": 45000,
+    "contractor": "Peachtree Bath Studio",
+    "contractor_license": "GA-LIC-222901",
+    "status": "approved",
+    "description": "Master bath remodel. Marble shower, claw-foot tub, shiplap accent wall, dual vanity with vessel sinks.",
+    "bedrooms": 3,
+    "sqft_affected": 160,
+    "created_at": "2024-11-15T13:00:00Z"
+  },
+  {
+    "id": "PR-2024-038",
+    "address": "2234 Peachtree Rd NW",
+    "city": "Atlanta",
+    "jurisdiction": "Fulton County",
+    "state": "GA",
+    "zip": "30309",
+    "date_filed": "2024-12-03",
+    "permit_type": "addition",
+    "permit_type_label": "Room Addition",
+    "valuation": 390000,
+    "contractor": "Southern Heritage Builders",
+    "contractor_license": "GA-LIC-559120",
+    "status": "pending",
+    "description": "Three-story rear addition with elevator. Ground: expanded kitchen, pantry, mudroom. Second: primary suite and office. Third: bonus room.",
+    "bedrooms": 4,
+    "sqft_affected": 1200,
+    "created_at": "2024-12-03T10:20:00Z"
+  },
+  {
+    "id": "PR-2024-039",
+    "address": "500 Massachusetts Ave",
+    "city": "Boston",
+    "jurisdiction": "Suffolk County",
+    "state": "MA",
+    "zip": "02118",
+    "date_filed": "2024-11-20",
+    "permit_type": "full_remodel",
+    "permit_type_label": "Full Interior Remodel",
+    "valuation": 750000,
+    "contractor": "Back Bay Renovations",
+    "contractor_license": "MA-LIC-334092",
+    "status": "approved",
+    "description": "Historic Brownstone renovation, 2,800 sq ft. LPC-approved period kitchen, 3 bath gut remodels, new MEP throughout, parlor restoration.",
+    "bedrooms": 4,
+    "sqft_affected": 2800,
+    "created_at": "2024-11-20T09:15:00Z"
+  },
+  {
+    "id": "PR-2024-040",
+    "address": "78 Chestnut Hill Ave",
+    "city": "Newton",
+    "jurisdiction": "Middlesex County",
+    "state": "MA",
+    "zip": "02467",
+    "date_filed": "2024-12-10",
+    "permit_type": "kitchen_remodel",
+    "permit_type_label": "Kitchen Remodel",
+    "valuation": 125000,
+    "contractor": "Greater Boston Remodelers",
+    "contractor_license": "MA-LIC-556710",
+    "status": "pending",
+    "description": "Open-concept kitchen remodel with formal dining integration. Transitional style, quartz and walnut surfaces, Miele appliances, wine cellar.",
+    "bedrooms": 5,
+    "sqft_affected": 480,
+    "created_at": "2024-12-10T14:30:00Z"
+  },
+  {
+    "id": "PR-2024-041",
+    "address": "3300 N Ocean Blvd",
+    "city": "Fort Lauderdale",
+    "jurisdiction": "Broward County",
+    "state": "FL",
+    "zip": "33308",
+    "date_filed": "2024-11-28",
+    "permit_type": "full_remodel",
+    "permit_type_label": "Full Interior Remodel",
+    "valuation": 850000,
+    "contractor": "Coastal Luxury Builders",
+    "contractor_license": "FL-LIC-778234",
+    "status": "approved",
+    "description": "Ultra-luxury oceanfront condo remodel, 3,200 sq ft. Full gut, custom kitchen, 3 designer baths, smart home, motorized blinds, new mechanical.",
+    "bedrooms": 4,
+    "sqft_affected": 3200,
+    "created_at": "2024-11-28T10:00:00Z"
+  },
+  {
+    "id": "PR-2024-042",
+    "address": "1825 Brickell Ave",
+    "city": "Miami",
+    "jurisdiction": "Miami-Dade County",
+    "state": "FL",
+    "zip": "33129",
+    "date_filed": "2024-12-08",
+    "permit_type": "bath_remodel",
+    "permit_type_label": "Bathroom Remodel",
+    "valuation": 68000,
+    "contractor": "SoBe Design Remodel",
+    "contractor_license": "FL-LIC-442891",
+    "status": "approved",
+    "description": "Luxury spa bath. Italian porcelain, steam shower with body jets, Japanese soaking tub, waterfall faucet, LED chromotherapy.",
+    "bedrooms": 3,
+    "sqft_affected": 200,
+    "created_at": "2024-12-08T11:15:00Z"
+  },
+  {
+    "id": "PR-2024-043",
+    "address": "7721 Biscayne Blvd",
+    "city": "Miami",
+    "jurisdiction": "Miami-Dade County",
+    "state": "FL",
+    "zip": "33138",
+    "date_filed": "2024-12-17",
+    "permit_type": "addition",
+    "permit_type_label": "Room Addition",
+    "valuation": 290000,
+    "contractor": "Miami Modern Builders",
+    "contractor_license": "FL-LIC-667234",
+    "status": "pending",
+    "description": "Two-car garage addition plus bonus room above, 750 sq ft total. Hurricane-rated construction, impact windows, attic HVAC.",
+    "bedrooms": 4,
+    "sqft_affected": 750,
+    "created_at": "2024-12-17T09:00:00Z"
+  },
+  {
+    "id": "PR-2024-044",
+    "address": "410 Lakeview Dr",
+    "city": "Nashville",
+    "jurisdiction": "Davidson County",
+    "state": "TN",
+    "zip": "37205",
+    "date_filed": "2024-12-05",
+    "permit_type": "kitchen_remodel",
+    "permit_type_label": "Kitchen Remodel",
+    "valuation": 90000,
+    "contractor": "Music City Renovations",
+    "contractor_license": "TN-LIC-445891",
+    "status": "approved",
+    "description": "Modern farmhouse kitchen. Shaker cabinets with brass hardware, Carrara marble, 48-inch range, pot rack, breakfast nook addition.",
+    "bedrooms": 4,
+    "sqft_affected": 360,
+    "created_at": "2024-12-05T10:30:00Z"
+  },
+  {
+    "id": "PR-2024-045",
+    "address": "6200 Hillsboro Pike",
+    "city": "Nashville",
+    "jurisdiction": "Davidson County",
+    "state": "TN",
+    "zip": "37215",
+    "date_filed": "2024-12-18",
+    "permit_type": "adu",
+    "permit_type_label": "ADU / Accessory Dwelling Unit",
+    "valuation": 175000,
+    "contractor": "Nashville ADU Build",
+    "contractor_license": "TN-LIC-556120",
+    "status": "approved",
+    "description": "Above-garage ADU, 620 sq ft. New staircase, open plan living/kitchen, full bath, private entrance, mini-split HVAC.",
+    "bedrooms": 1,
+    "sqft_affected": 620,
+    "created_at": "2024-12-18T08:45:00Z"
+  },
+  {
+    "id": "PR-2024-046",
+    "address": "900 Sheridan Rd",
+    "city": "Wilmette",
+    "jurisdiction": "Cook County",
+    "state": "IL",
+    "zip": "60091",
+    "date_filed": "2024-12-19",
+    "permit_type": "full_remodel",
+    "permit_type_label": "Full Interior Remodel",
+    "valuation": 620000,
+    "contractor": "North Shore Estate Remodelers",
+    "contractor_license": "IL-LIC-889231",
+    "status": "pending",
+    "description": "Comprehensive remodel of 1920s Tudor. Kitchen, 3 baths, primary suite expansion, new mudroom, windows, restored millwork, new mechanical.",
+    "bedrooms": 5,
+    "sqft_affected": 3100,
+    "created_at": "2024-12-19T11:00:00Z"
+  },
+  {
+    "id": "PR-2024-047",
+    "address": "1415 Pacific Coast Hwy",
+    "city": "Malibu",
+    "jurisdiction": "Los Angeles County",
+    "state": "CA",
+    "zip": "90265",
+    "date_filed": "2024-12-20",
+    "permit_type": "full_remodel",
+    "permit_type_label": "Full Interior Remodel",
+    "valuation": 1250000,
+    "contractor": "Malibu Dream Homes",
+    "contractor_license": "CA-LIC-992001",
+    "status": "pending",
+    "description": "Complete remodel of oceanfront home, 4,200 sq ft. Designer kitchen, 4 baths, home theater, wine cellar, primary suite with ocean-view terrace.",
+    "bedrooms": 5,
+    "sqft_affected": 4200,
+    "created_at": "2024-12-20T09:00:00Z"
+  },
+  {
+    "id": "PR-2024-048",
+    "address": "805 N Michigan Ave",
+    "city": "Chicago",
+    "jurisdiction": "Cook County",
+    "state": "IL",
+    "zip": "60611",
+    "date_filed": "2024-12-21",
+    "permit_type": "bath_remodel",
+    "permit_type_label": "Bathroom Remodel",
+    "valuation": 58000,
+    "contractor": "Gold Coast Baths",
+    "contractor_license": "IL-LIC-778910",
+    "status": "approved",
+    "description": "High-rise condo master bath remodel. Dolomite marble, freestanding tub, walk-in steam shower, heated floors, radiant towel bar.",
+    "bedrooms": 3,
+    "sqft_affected": 185,
+    "created_at": "2024-12-21T10:00:00Z"
+  },
+  {
+    "id": "PR-2024-049",
+    "address": "3300 Bee Cave Rd",
+    "city": "Austin",
+    "jurisdiction": "Travis County",
+    "state": "TX",
+    "zip": "78746",
+    "date_filed": "2024-12-22",
+    "permit_type": "kitchen_remodel",
+    "permit_type_label": "Kitchen Remodel",
+    "valuation": 140000,
+    "contractor": "Westlake Design Build",
+    "contractor_license": "TX-LIC-998210",
+    "status": "pending",
+    "description": "Trophy kitchen remodel. Custom cabinetry to ceiling, 10-ft island, Quartzite waterfall, La Cornue 60-inch range, integrated Miele appliances.",
+    "bedrooms": 5,
+    "sqft_affected": 500,
+    "created_at": "2024-12-22T09:30:00Z"
+  },
+  {
+    "id": "PR-2024-050",
+    "address": "2201 Collins Ave",
+    "city": "Miami Beach",
+    "jurisdiction": "Miami-Dade County",
+    "state": "FL",
+    "zip": "33139",
+    "date_filed": "2024-12-23",
+    "permit_type": "full_remodel",
+    "permit_type_label": "Full Interior Remodel",
+    "valuation": 1100000,
+    "contractor": "South Beach Luxury Build",
+    "contractor_license": "FL-LIC-887120",
+    "status": "approved",
+    "description": "Art Deco penthouse remodel, 3,800 sq ft. Full gut, bespoke kitchen, 3 designer baths, home automation, custom millwork, terrace expansion.",
+    "bedrooms": 4,
+    "sqft_affected": 3800,
+    "created_at": "2024-12-23T11:00:00Z"
+  }
+]
diff --git a/data/products.json b/data/products.json
deleted file mode 100644
index 8eee5f2..0000000
--- a/data/products.json
+++ /dev/null
@@ -1,6 +0,0 @@
-[
-  { "title": "Sample One", "sku": "SMP-001", "price": 129.00, "hex": "#2b3a55", "image": "" },
-  { "title": "Sample Two", "sku": "SMP-002", "price": 89.00, "hex": "#d8c3a5", "image": "" },
-  { "title": "Sample Three", "sku": "SMP-003", "price": 210.00, "hex": "#8e8d8a", "image": "" },
-  { "title": "Sample Four", "sku": "SMP-004", "price": 54.00, "hex": "#e98074", "image": "" }
-]
diff --git a/data/subscribers.json b/data/subscribers.json
new file mode 100644
index 0000000..fe51488
--- /dev/null
+++ b/data/subscribers.json
@@ -0,0 +1 @@
+[]
diff --git a/public/admin.html b/public/admin.html
new file mode 100644
index 0000000..0d58167
--- /dev/null
+++ b/public/admin.html
@@ -0,0 +1,241 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Admin — Permit Radar</title>
+<style>
+:root { --cols: 3; --accent: #1d4ed8; }
+*, *::before, *::after { box-sizing: border-box; }
+body { margin: 0; font: 14px/1.5 -apple-system, Segoe UI, Roboto, sans-serif; color: #1a1a1a; background: #f1f5f9; }
+
+nav { background: #0f172a; color: #fff; padding: 12px 20px; display: flex; align-items: center; gap: 16px; }
+.logo { font-size: 17px; font-weight: 800; color: #fff; text-decoration: none; }
+.logo span { color: #60a5fa; }
+.admin-badge { background: #ef4444; color: #fff; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 4px; }
+.nav-links { margin-left: auto; display: flex; gap: 16px; }
+.nav-links a { color: #94a3b8; font-size: 13px; text-decoration: none; }
+.nav-links a:hover { color: #fff; }
+
+/* ── TABS ── */
+.tabs { display: flex; gap: 0; border-bottom: 2px solid #e2e8f0; background: #fff; padding: 0 20px; }
+.tab {
+  padding: 12px 20px; font-size: 14px; font-weight: 600; color: #64748b;
+  cursor: pointer; border-bottom: 3px solid transparent; margin-bottom: -2px;
+}
+.tab.active { color: var(--accent); border-color: var(--accent); }
+.tab-badge { background: #e2e8f0; color: #475569; font-size: 11px; font-weight: 700; padding: 1px 7px; border-radius: 99px; margin-left: 6px; }
+.tab-badge.new { background: #dbeafe; color: var(--accent); }
+
+/* ── CONTROLS ── */
+.controls {
+  display: flex; gap: 12px; align-items: center; flex-wrap: wrap;
+  padding: 12px 20px; background: #fff; border-bottom: 1px solid #e2e8f0;
+  position: sticky; top: 0; z-index: 5;
+}
+.controls select { padding: 7px 10px; border: 1px solid #d1d5db; border-radius: 6px; font: inherit; font-size: 13px; }
+.ctrl-label { font-size: 12px; color: #64748b; display: flex; gap: 5px; align-items: center; }
+.count-badge { margin-left: auto; background: #f1f5f9; color: #475569; font-size: 12px; font-weight: 600; padding: 4px 10px; border-radius: 99px; }
+
+/* ── GRID ── */
+.grid-wrap { padding: 20px; }
+.grid { display: grid; grid-template-columns: repeat(var(--cols), minmax(0, 1fr)); gap: 14px; }
+.empty { padding: 60px; text-align: center; color: #94a3b8; }
+
+/* ── ADMIN CARD ── */
+/* House rule: every admin card MUST show created date + time */
+.card {
+  background: #fff; border: 1px solid #e2e8f0; border-radius: 10px;
+  overflow: hidden; display: flex; flex-direction: column;
+}
+.card-header { padding: 14px 14px 10px; border-bottom: 1px solid #f1f5f9; }
+.card-id { font-size: 11px; color: #94a3b8; font-weight: 600; letter-spacing: .4px; margin-bottom: 4px; }
+.card-title { font-size: 15px; font-weight: 700; color: #1e293b; margin: 0 0 2px; }
+.card-subtitle { font-size: 12px; color: #64748b; }
+.card-body { padding: 12px 14px; flex: 1; }
+.field { margin-bottom: 6px; font-size: 13px; color: #374151; }
+.field .lbl { font-size: 11px; color: #94a3b8; font-weight: 600; text-transform: uppercase; letter-spacing: .4px; display: block; margin-bottom: 1px; }
+/* HARD RULE: created date+time chip — visible on every admin card */
+.when-chip {
+  display: inline-flex; align-items: center; gap: 4px;
+  font-size: 11px; color: #64748b; background: #f8fafc;
+  border: 1px solid #e2e8f0; border-radius: 6px; padding: 3px 8px;
+  margin-top: 10px; font-weight: 500;
+}
+.when-chip .icon { color: #94a3b8; }
+.type-chip {
+  display: inline-block; font-size: 11px; font-weight: 600; padding: 2px 8px;
+  border-radius: 4px; background: #f1f5f9; color: #475569; margin-right: 4px;
+}
+.plan-chip { font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 4px; }
+.plan-basic { background: #f1f5f9; color: #475569; }
+.plan-pro   { background: #dbeafe; color: #1d4ed8; }
+.plan-team  { background: #d1fae5; color: #065f46; }
+.val-chip { font-size: 13px; font-weight: 700; color: #15803d; }
+
+footer { background: #0f172a; color: #475569; padding: 20px; text-align: center; font-size: 13px; }
+footer a { color: #64748b; text-decoration: none; }
+</style>
+</head>
+<body>
+
+<nav>
+  <a class="logo" href="/"><span>📡</span> Permit Radar</a>
+  <span class="admin-badge">ADMIN</span>
+  <div class="nav-links">
+    <a href="/">Directory</a>
+    <a href="/#get-alerts">Get Alerts</a>
+  </div>
+</nav>
+
+<div class="tabs">
+  <div class="tab active" id="tab-subs" onclick="showTab('subs')">
+    Subscribers <span class="tab-badge new" id="badge-subs">…</span>
+  </div>
+  <div class="tab" id="tab-leads" onclick="showTab('leads')">
+    Lead Requests <span class="tab-badge" id="badge-leads">…</span>
+  </div>
+</div>
+
+<!-- CONTROLS — sort + density (house rule: admin grid also gets both) -->
+<div class="controls">
+  <label>
+    <select id="sort">
+      <option value="newest">Sort: Newest First</option>
+      <option value="oldest">Oldest First</option>
+    </select>
+  </label>
+  <span class="ctrl-label">
+    Density <input id="density" type="range" min="1" max="6" value="3" style="width:70px">
+  </span>
+  <span class="count-badge" id="count">…</span>
+</div>
+
+<!-- PANELS -->
+<div class="grid-wrap" id="panel-subs">
+  <div class="grid" id="grid-subs"><div class="empty">Loading subscribers…</div></div>
+</div>
+<div class="grid-wrap" id="panel-leads" style="display:none">
+  <div class="grid" id="grid-leads"><div class="empty">Loading lead requests…</div></div>
+</div>
+
+<footer>
+  <a href="/">← Back to Directory</a> &nbsp;·&nbsp; Permit Radar Admin
+</footer>
+
+<script>
+'use strict';
+const esc = s => (s||'').replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
+
+// House rule: created date+time on every admin card
+// Must show BOTH date and time via toLocaleString; full ISO in title= attribute
+function fmtDateTime(iso) {
+  if (!iso) return '—';
+  const d = new Date(iso);
+  return d.toLocaleString(undefined, { year:'numeric', month:'short', day:'numeric', hour:'numeric', minute:'2-digit' });
+}
+
+// ── State ─────────────────────────────────────────────────────────────────────
+let activeTab = 'subs';
+const sortSel = document.getElementById('sort');
+const dens    = document.getElementById('density');
+
+sortSel.value = localStorage.getItem('adm_sort') || 'newest';
+dens.value    = localStorage.getItem('adm_cols') || '3';
+function applyCols() {
+  document.documentElement.style.setProperty('--cols', dens.value);
+  localStorage.setItem('adm_cols', dens.value);
+}
+applyCols();
+
+sortSel.onchange = () => { localStorage.setItem('adm_sort', sortSel.value); loadBoth(); };
+dens.oninput = applyCols;
+
+// ── Tab switching ─────────────────────────────────────────────────────────────
+function showTab(tab) {
+  activeTab = tab;
+  document.getElementById('tab-subs').classList.toggle('active', tab === 'subs');
+  document.getElementById('tab-leads').classList.toggle('active', tab === 'leads');
+  document.getElementById('panel-subs').style.display  = tab === 'subs'   ? '' : 'none';
+  document.getElementById('panel-leads').style.display = tab === 'leads'  ? '' : 'none';
+  updateCount();
+}
+
+let _subCount = 0, _leadCount = 0;
+function updateCount() {
+  document.getElementById('count').textContent =
+    (activeTab === 'subs' ? _subCount : _leadCount) + ' records';
+}
+
+// ── Subscribers ───────────────────────────────────────────────────────────────
+async function loadSubs() {
+  const r = await fetch('/api/admin/subscribers?sort=' + sortSel.value);
+  if (r.status === 401) { document.getElementById('grid-subs').innerHTML = '<div class="empty">Auth required — reload with credentials.</div>'; return; }
+  const d = await r.json();
+  _subCount = d.count;
+  document.getElementById('badge-subs').textContent = d.count;
+  updateCount();
+  const grid = document.getElementById('grid-subs');
+  if (!d.subscribers.length) { grid.innerHTML = '<div class="empty">No subscribers yet.</div>'; return; }
+  grid.innerHTML = d.subscribers.map(s => {
+    const planClass = 'plan-' + (s.plan || 'basic');
+    const isoCreated = s.created_at || '';
+    return `
+<div class="card">
+  <div class="card-header">
+    <div class="card-id">${esc(s.id)}</div>
+    <div class="card-title">${esc(s.email)}</div>
+    <div class="card-subtitle"><span class="type-chip">${esc(s.trade_type || '—')}</span><span class="plan-chip ${planClass}">${esc(s.plan || 'basic')}</span></div>
+  </div>
+  <div class="card-body">
+    <div class="field"><span class="lbl">Service Area</span>${esc(s.service_area || '—')}</div>
+    ${s.jurisdictions && s.jurisdictions.length ? `<div class="field"><span class="lbl">Jurisdictions</span>${esc(s.jurisdictions.join(', '))}</div>` : ''}
+    ${s.permit_types && s.permit_types.length ? `<div class="field"><span class="lbl">Permit Types</span>${esc(s.permit_types.join(', '))}</div>` : ''}
+    <!-- House rule: created date + time chip — BOTH date and time, always visible -->
+    <div class="when-chip" title="${esc(isoCreated)}">
+      <span class="icon">🕓</span> ${esc(fmtDateTime(isoCreated))}
+    </div>
+  </div>
+</div>`;
+  }).join('');
+}
+
+// ── Lead Requests ─────────────────────────────────────────────────────────────
+async function loadLeads() {
+  const r = await fetch('/api/admin/lead-requests?sort=' + sortSel.value);
+  if (r.status === 401) { document.getElementById('grid-leads').innerHTML = '<div class="empty">Auth required.</div>'; return; }
+  const d = await r.json();
+  _leadCount = d.count;
+  document.getElementById('badge-leads').textContent = d.count;
+  updateCount();
+  const grid = document.getElementById('grid-leads');
+  if (!d.lead_requests.length) { grid.innerHTML = '<div class="empty">No lead requests yet.</div>'; return; }
+  grid.innerHTML = d.lead_requests.map(lr => {
+    const isoCreated = lr.created_at || '';
+    return `
+<div class="card">
+  <div class="card-header">
+    <div class="card-id">${esc(lr.id)}</div>
+    <div class="card-title">${esc(lr.name || lr.email)}</div>
+    <div class="card-subtitle">${esc(lr.email)}</div>
+  </div>
+  <div class="card-body">
+    <div class="field"><span class="lbl">Permit</span>${esc(lr.permit_id)} — ${esc(lr.permit_address || '')}</div>
+    <div class="field"><span class="lbl">Valuation</span><span class="val-chip">${lr.permit_valuation ? '$' + Number(lr.permit_valuation).toLocaleString() : '—'}</span></div>
+    <div class="field"><span class="lbl">Trade Type</span><span class="type-chip">${esc(lr.trade_type || '—')}</span></div>
+    ${lr.phone ? `<div class="field"><span class="lbl">Phone</span>${esc(lr.phone)}</div>` : ''}
+    ${lr.message ? `<div class="field"><span class="lbl">Message</span>${esc(lr.message)}</div>` : ''}
+    <!-- House rule: created date + time chip — BOTH date and time, always visible -->
+    <div class="when-chip" title="${esc(isoCreated)}">
+      <span class="icon">🕓</span> ${esc(fmtDateTime(isoCreated))}
+    </div>
+  </div>
+</div>`;
+  }).join('');
+}
+
+async function loadBoth() { await Promise.all([loadSubs(), loadLeads()]); }
+loadBoth();
+</script>
+</body>
+</html>
diff --git a/public/index.html b/public/index.html
index ceb7ab1..8679fa4 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,55 +1,564 @@
-<!doctype html><html lang="en"><head><meta charset="utf-8">
-<meta name="viewport" content="width=device-width,initial-scale=1"><title>Storefront starter</title>
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Permit Radar — Residential Remodel Permit Directory</title>
 <style>
-  :root{--cols:4}
-  *{box-sizing:border-box} body{margin:0;font:15px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;color:#1a1a1a;background:#fafafa}
-  header{display:flex;gap:14px;align-items:center;flex-wrap:wrap;padding:14px 20px;border-bottom:1px solid #e6e6e6;position:sticky;top:0;background:#fff;z-index:5}
-  h1{font-size:17px;margin:0;font-weight:700} .spacer{flex:1}
-  label{font-size:12px;color:#666;display:flex;gap:6px;align-items:center}
-  select,input[type=range]{font:inherit} .count{font-size:12px;color:#999}
-  .grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:14px;padding:20px}
-  .card{background:#fff;border:1px solid #e6e6e6;border-radius:8px;overflow:hidden}
-  .card img{width:100%;aspect-ratio:1/1;object-fit:cover;display:block;background:#f0f0f0}
-  .meta{padding:8px 10px} .t{font-size:13px;font-weight:600;margin:0 0 2px} .s{font-size:11px;color:#888}
-  .p{font-size:13px;margin-top:4px} .empty{padding:40px;color:#999;text-align:center}
-</style></head><body>
-<header>
-  <h1>Storefront starter</h1>
-  <label>Sort
+:root { --cols: 3; --accent: #1d4ed8; --accent-light: #eff6ff; }
+*, *::before, *::after { box-sizing: border-box; }
+body { margin: 0; font: 15px/1.5 -apple-system, Segoe UI, Roboto, sans-serif; color: #1a1a1a; background: #f3f4f6; }
+
+/* ── SAMPLE BANNER ── */
+.sample-banner {
+  background: #fef3c7; border-bottom: 2px solid #f59e0b;
+  padding: 8px 20px; font-size: 13px; text-align: center; color: #78350f;
+}
+.sample-banner strong { color: #92400e; }
+
+/* ── TOP NAV ── */
+nav {
+  background: #1e293b; color: #fff; padding: 12px 20px;
+  display: flex; align-items: center; gap: 16px;
+}
+.logo { font-size: 18px; font-weight: 800; letter-spacing: -0.5px; color: #fff; text-decoration: none; }
+.logo span { color: #60a5fa; }
+.nav-links { margin-left: auto; display: flex; gap: 16px; }
+.nav-links a { color: #cbd5e1; font-size: 13px; text-decoration: none; }
+.nav-links a:hover { color: #fff; }
+
+/* ── HERO ── */
+.hero {
+  background: linear-gradient(135deg, #1e293b 0%, #1d4ed8 100%);
+  color: #fff; padding: 48px 20px 40px; text-align: center;
+}
+.hero h1 { font-size: 32px; font-weight: 800; margin: 0 0 10px; }
+.hero p { font-size: 16px; color: #bfdbfe; margin: 0 0 24px; }
+.hero-cta {
+  display: inline-block; background: #f59e0b; color: #1a1a1a;
+  font-weight: 700; padding: 12px 28px; border-radius: 8px;
+  text-decoration: none; font-size: 15px; transition: background .15s;
+}
+.hero-cta:hover { background: #fbbf24; }
+
+/* ── CONTROLS ── */
+.controls {
+  display: flex; gap: 12px; align-items: center; flex-wrap: wrap;
+  padding: 14px 20px; background: #fff; border-bottom: 1px solid #e2e8f0;
+  position: sticky; top: 0; z-index: 10;
+}
+.search-box {
+  flex: 1; min-width: 200px; position: relative;
+}
+.search-box input {
+  width: 100%; padding: 8px 12px 8px 36px; border: 1px solid #d1d5db;
+  border-radius: 6px; font: inherit; font-size: 14px; outline: none;
+}
+.search-box input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(29,78,216,.12); }
+.search-box::before { content: "🔍"; position: absolute; left: 10px; top: 50%; transform: translateY(-50%); font-size: 14px; }
+.filter-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
+.filter-row select, .filter-row input[type=text] {
+  padding: 7px 10px; border: 1px solid #d1d5db; border-radius: 6px;
+  font: inherit; font-size: 13px; background: #fff; cursor: pointer;
+}
+.filter-row select:focus { outline: none; border-color: var(--accent); }
+.ctrl-label { font-size: 12px; color: #64748b; display: flex; gap: 5px; align-items: center; }
+.count-badge {
+  margin-left: auto; background: var(--accent-light); color: var(--accent);
+  font-size: 12px; font-weight: 600; padding: 4px 10px; border-radius: 99px;
+}
+
+/* ── GRID ── */
+.grid-wrap { padding: 20px; }
+.grid {
+  display: grid;
+  grid-template-columns: repeat(var(--cols), minmax(0, 1fr));
+  gap: 14px;
+}
+.empty { padding: 60px; text-align: center; color: #94a3b8; font-size: 15px; }
+
+/* ── PERMIT CARD ── */
+.card {
+  background: #fff; border: 1px solid #e2e8f0; border-radius: 10px;
+  overflow: hidden; display: flex; flex-direction: column;
+  transition: box-shadow .15s, transform .15s;
+}
+.card:hover { box-shadow: 0 4px 24px rgba(0,0,0,.10); transform: translateY(-1px); }
+.card-header {
+  padding: 14px 14px 10px; border-bottom: 1px solid #f1f5f9;
+  display: flex; justify-content: space-between; align-items: flex-start; gap: 8px;
+}
+.permit-id { font-size: 11px; color: #94a3b8; font-weight: 600; letter-spacing: .5px; }
+.status-chip {
+  font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 99px; white-space: nowrap;
+}
+.status-approved { background: #dcfce7; color: #15803d; }
+.status-pending   { background: #fef9c3; color: #854d0e; }
+.status-issued    { background: #dbeafe; color: #1d4ed8; }
+.type-chip {
+  font-size: 11px; font-weight: 600; padding: 3px 9px; border-radius: 4px;
+  background: #f1f5f9; color: #475569;
+}
+.card-body { padding: 12px 14px; flex: 1; }
+.card-address { font-size: 14px; font-weight: 700; margin: 0 0 2px; line-height: 1.3; }
+.card-city { font-size: 12px; color: #64748b; margin: 0 0 8px; }
+.card-meta { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 8px; }
+.meta-item { font-size: 12px; color: #475569; }
+.meta-item strong { color: #1e293b; }
+.card-desc { font-size: 12px; color: #64748b; line-height: 1.4;
+  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
+.card-footer { padding: 10px 14px; border-top: 1px solid #f1f5f9; display: flex; gap: 8px; align-items: center; }
+.btn-detail {
+  flex: 1; text-align: center; padding: 7px; background: var(--accent-light);
+  color: var(--accent); border: 1px solid #bfdbfe; border-radius: 6px;
+  font-size: 12px; font-weight: 600; text-decoration: none; cursor: pointer;
+}
+.btn-detail:hover { background: #dbeafe; }
+.btn-lead {
+  flex: 1; text-align: center; padding: 7px; background: #f59e0b;
+  color: #1a1a1a; border: none; border-radius: 6px;
+  font-size: 12px; font-weight: 700; cursor: pointer; transition: background .15s;
+}
+.btn-lead:hover { background: #fbbf24; }
+.valuation { font-size: 14px; font-weight: 700; color: #15803d; }
+
+/* ── LEAD FUNNEL SECTION ── */
+.funnel-section {
+  background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
+  color: #fff; padding: 56px 20px; margin-top: 32px; text-align: center;
+}
+.funnel-section h2 { font-size: 28px; margin: 0 0 10px; }
+.funnel-section p { color: #94a3b8; margin: 0 0 32px; font-size: 15px; }
+.plan-cards { display: flex; gap: 20px; justify-content: center; flex-wrap: wrap; margin-bottom: 36px; }
+.plan-card {
+  background: #1e293b; border: 1px solid #334155; border-radius: 12px;
+  padding: 24px; width: 220px; text-align: left; position: relative;
+}
+.plan-card.featured { border-color: #f59e0b; }
+.plan-badge {
+  position: absolute; top: -10px; left: 50%; transform: translateX(-50%);
+  background: #f59e0b; color: #1a1a1a; font-size: 11px; font-weight: 700;
+  padding: 2px 12px; border-radius: 99px;
+}
+.plan-name { font-size: 16px; font-weight: 700; margin: 0 0 4px; }
+.plan-price { font-size: 28px; font-weight: 800; color: #60a5fa; margin: 0 0 12px; }
+.plan-price span { font-size: 14px; color: #94a3b8; font-weight: 400; }
+.plan-features { list-style: none; margin: 0; padding: 0; font-size: 13px; color: #cbd5e1; }
+.plan-features li { padding: 3px 0; }
+.plan-features li::before { content: "✓ "; color: #60a5fa; font-weight: 700; }
+.funnel-form {
+  background: #fff; color: #1a1a1a; border-radius: 12px; padding: 28px;
+  max-width: 500px; margin: 0 auto; text-align: left;
+}
+.funnel-form h3 { font-size: 18px; margin: 0 0 16px; }
+.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 12px; }
+.form-row.single { grid-template-columns: 1fr; }
+.funnel-form label { display: block; font-size: 12px; font-weight: 600; color: #475569; margin-bottom: 4px; }
+.funnel-form select, .funnel-form input {
+  width: 100%; padding: 9px 12px; border: 1px solid #d1d5db; border-radius: 6px;
+  font: inherit; font-size: 14px; outline: none;
+}
+.funnel-form select:focus, .funnel-form input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(29,78,216,.1); }
+.btn-subscribe {
+  width: 100%; padding: 12px; background: var(--accent); color: #fff;
+  border: none; border-radius: 8px; font: 700 15px inherit; cursor: pointer; margin-top: 8px;
+}
+.btn-subscribe:hover { background: #1e40af; }
+.form-note { font-size: 11px; color: #94a3b8; text-align: center; margin-top: 8px; }
+.success-msg { background: #dcfce7; color: #15803d; border: 1px solid #bbf7d0; border-radius: 6px; padding: 12px; margin-top: 12px; font-size: 13px; text-align: center; }
+.error-msg { background: #fee2e2; color: #991b1b; border: 1px solid #fca5a5; border-radius: 6px; padding: 12px; margin-top: 12px; font-size: 13px; text-align: center; }
+
+/* ── LEAD MODAL ── */
+.modal-bg {
+  display: none; position: fixed; inset: 0; background: rgba(0,0,0,.5);
+  z-index: 100; align-items: center; justify-content: center;
+}
+.modal-bg.open { display: flex; }
+.modal {
+  background: #fff; border-radius: 12px; padding: 28px; width: 100%; max-width: 440px;
+  margin: 20px; position: relative;
+}
+.modal h3 { margin: 0 0 6px; font-size: 18px; }
+.modal p { color: #64748b; font-size: 13px; margin: 0 0 18px; }
+.modal-close {
+  position: absolute; top: 14px; right: 16px; font-size: 20px; line-height: 1;
+  cursor: pointer; color: #94a3b8; background: none; border: none;
+}
+.modal-close:hover { color: #1a1a1a; }
+
+/* ── FOOTER ── */
+footer { background: #0f172a; color: #64748b; padding: 24px 20px; text-align: center; font-size: 13px; margin-top: 32px; }
+footer a { color: #94a3b8; text-decoration: none; }
+footer a:hover { color: #fff; }
+</style>
+</head>
+<body>
+
+<!-- SAMPLE DATA BANNER -->
+<div class="sample-banner">
+  ⚠️ <strong>SAMPLE / REPRESENTATIVE DATA ONLY</strong> — These permits are illustrative, not sourced from any county portal.
+  Real-feed seam: <code>scripts/ingest-stub.js</code>
+</div>
+
+<!-- NAV -->
+<nav>
+  <a class="logo" href="/"><span>📡</span> Permit Radar</a>
+  <div class="nav-links">
+    <a href="/">Directory</a>
+    <a href="#get-alerts">Get Alerts</a>
+    <a href="/admin">Admin</a>
+  </div>
+</nav>
+
+<!-- HERO -->
+<div class="hero">
+  <h1>Residential Remodel Permits — Live</h1>
+  <p>High-intent homeowner leads at the moment they start their renovation. Reach them first.</p>
+  <a href="#get-alerts" class="hero-cta">Get Permit Alerts in Your Area →</a>
+</div>
+
+<!-- CONTROLS (sort + density + filters) -->
+<div class="controls">
+  <div class="search-box">
+    <input id="search" type="text" placeholder="Search address, city, contractor, type…">
+  </div>
+  <div class="filter-row">
     <select id="sort">
-      <option value="newest">Newest</option>
-      <option value="title">Title A→Z</option>
-      <option value="sku">SKU A→Z</option>
-      <option value="price-asc">Price ↑</option>
-      <option value="price-desc">Price ↓</option>
-      <option value="light">Light → Dark</option>
-      <option value="dark">Dark → Light</option>
+      <option value="date_desc">Sort: Newest Filed</option>
+      <option value="date_asc">Oldest Filed</option>
+      <option value="valuation_desc">Valuation High→Low</option>
+      <option value="valuation_asc">Valuation Low→High</option>
+      <option value="city">City A→Z</option>
+      <option value="type">Permit Type</option>
+      <option value="address">Address A→Z</option>
     </select>
-  </label>
-  <label>Density <input id="density" type="range" min="2" max="8" value="4"></label>
-  <span class="spacer"></span><span class="count" id="count"></span>
-</header>
-<div class="grid" id="grid"><div class="empty">Loading…</div></div>
+    <select id="filt-jurisdiction">
+      <option value="">All Jurisdictions</option>
+      <option value="los angeles county">Los Angeles County</option>
+      <option value="santa clara county">Santa Clara County</option>
+      <option value="san diego county">San Diego County</option>
+      <option value="alameda county">Alameda County</option>
+      <option value="maricopa county">Maricopa County</option>
+      <option value="coconino county">Coconino County</option>
+      <option value="cook county">Cook County</option>
+      <option value="travis county">Travis County</option>
+      <option value="williamson county">Williamson County</option>
+      <option value="dallas county">Dallas County</option>
+      <option value="multnomah county">Multnomah County</option>
+      <option value="king county">King County</option>
+      <option value="fulton county">Fulton County</option>
+      <option value="suffolk county">Suffolk County</option>
+      <option value="middlesex county">Middlesex County</option>
+      <option value="broward county">Broward County</option>
+      <option value="miami-dade county">Miami-Dade County</option>
+      <option value="davidson county">Davidson County</option>
+    </select>
+    <select id="filt-type">
+      <option value="">All Types</option>
+      <option value="kitchen_remodel">Kitchen Remodel</option>
+      <option value="bath_remodel">Bathroom Remodel</option>
+      <option value="addition">Room Addition</option>
+      <option value="full_remodel">Full Remodel</option>
+      <option value="adu">ADU</option>
+      <option value="reroof">Reroof</option>
+    </select>
+    <select id="filt-status">
+      <option value="">All Statuses</option>
+      <option value="approved">Approved</option>
+      <option value="pending">Pending</option>
+    </select>
+    <span class="ctrl-label">Density <input id="density" type="range" min="1" max="6" value="3" style="width:70px"></span>
+  </div>
+  <span class="count-badge" id="count">… permits</span>
+</div>
+
+<!-- PERMIT GRID -->
+<div class="grid-wrap">
+  <div class="grid" id="grid"><div class="empty">Loading permits…</div></div>
+</div>
+
+<!-- LEAD FUNNEL SECTION -->
+<div class="funnel-section" id="get-alerts">
+  <h2>Turn Permits Into Revenue</h2>
+  <p>Trades, designers, and home-goods vendors — get alerts the moment a high-value permit is filed in your area.</p>
+
+  <div class="plan-cards">
+    <div class="plan-card">
+      <div class="plan-name">Basic</div>
+      <div class="plan-price">$79<span>/mo</span></div>
+      <ul class="plan-features">
+        <li>Up to 3 jurisdictions</li>
+        <li>Weekly digest emails</li>
+        <li>1 permit type</li>
+        <li>Permits &gt;$25K valuation</li>
+      </ul>
+    </div>
+    <div class="plan-card featured">
+      <div class="plan-badge">MOST POPULAR</div>
+      <div class="plan-name">Pro</div>
+      <div class="plan-price">$199<span>/mo</span></div>
+      <ul class="plan-features">
+        <li>Unlimited jurisdictions</li>
+        <li>Real-time email alerts</li>
+        <li>All permit types</li>
+        <li>All valuations</li>
+        <li>Contractor contacts</li>
+      </ul>
+    </div>
+    <div class="plan-card">
+      <div class="plan-name">Team</div>
+      <div class="plan-price">$449<span>/mo</span></div>
+      <ul class="plan-features">
+        <li>Everything in Pro</li>
+        <li>5 team seats</li>
+        <li>CRM/CSV export</li>
+        <li>DW Trade referrals</li>
+        <li>Priority support</li>
+      </ul>
+    </div>
+  </div>
+
+  <div class="funnel-form">
+    <h3>Get Permit Alerts</h3>
+    <form id="subscribe-form" onsubmit="submitSubscribe(event)">
+      <div class="form-row">
+        <div>
+          <label>Trade Type *</label>
+          <select name="trade_type" required>
+            <option value="">Select…</option>
+            <option value="general_contractor">General Contractor</option>
+            <option value="kitchen_bath">Kitchen &amp; Bath Designer</option>
+            <option value="interior_designer">Interior Designer</option>
+            <option value="roofing">Roofing Contractor</option>
+            <option value="plumber">Plumber</option>
+            <option value="electrician">Electrician</option>
+            <option value="flooring">Flooring Contractor</option>
+            <option value="hvac">HVAC</option>
+            <option value="home_goods_vendor">Home Goods Vendor</option>
+            <option value="wallcovering_vendor">Wallcovering Vendor</option>
+            <option value="real_estate">Real Estate Agent</option>
+            <option value="other">Other</option>
+          </select>
+        </div>
+        <div>
+          <label>Plan</label>
+          <select name="plan">
+            <option value="basic">Basic — $79/mo</option>
+            <option value="pro" selected>Pro — $199/mo</option>
+            <option value="team">Team — $449/mo</option>
+          </select>
+        </div>
+      </div>
+      <div class="form-row">
+        <div>
+          <label>Email *</label>
+          <input type="email" name="email" placeholder="you@company.com" required>
+        </div>
+        <div>
+          <label>Service Area / City</label>
+          <input type="text" name="service_area" placeholder="e.g. Los Angeles, CA">
+        </div>
+      </div>
+      <button type="submit" class="btn-subscribe">Start Getting Leads →</button>
+      <div class="form-note">No charge yet — this prototype doesn't process payment. We'll contact you to onboard.</div>
+      <div id="sub-msg"></div>
+    </form>
+  </div>
+</div>
+
+<!-- LEAD REQUEST MODAL -->
+<div class="modal-bg" id="lead-modal" onclick="if(event.target===this)closeLead()">
+  <div class="modal">
+    <button class="modal-close" onclick="closeLead()">×</button>
+    <h3>Request This Lead</h3>
+    <p id="modal-permit-label">Permit details…</p>
+    <form id="lead-form" onsubmit="submitLead(event)">
+      <input type="hidden" id="modal-permit-id" name="permit_id">
+      <div class="form-row">
+        <div>
+          <label>Your Name</label>
+          <input type="text" name="name" placeholder="Jane Smith">
+        </div>
+        <div>
+          <label>Trade Type *</label>
+          <select name="trade_type" required>
+            <option value="">Select…</option>
+            <option value="general_contractor">General Contractor</option>
+            <option value="kitchen_bath">Kitchen &amp; Bath</option>
+            <option value="interior_designer">Interior Designer</option>
+            <option value="roofing">Roofer</option>
+            <option value="flooring">Flooring</option>
+            <option value="hvac">HVAC</option>
+            <option value="home_goods_vendor">Home Goods Vendor</option>
+            <option value="wallcovering_vendor">Wallcovering Vendor</option>
+            <option value="other">Other</option>
+          </select>
+        </div>
+      </div>
+      <div class="form-row">
+        <div>
+          <label>Email *</label>
+          <input type="email" name="email" required placeholder="you@company.com">
+        </div>
+        <div>
+          <label>Phone</label>
+          <input type="tel" name="phone" placeholder="555-555-5555">
+        </div>
+      </div>
+      <div class="form-row single">
+        <div>
+          <label>Message</label>
+          <input type="text" name="message" placeholder="What services are you offering?">
+        </div>
+      </div>
+      <button type="submit" class="btn-subscribe">Request This Lead →</button>
+      <div id="lead-msg"></div>
+    </form>
+  </div>
+</div>
+
+<footer>
+  <p>© 2024 Permit Radar — <a href="#get-alerts">Get Alerts</a> · <a href="/admin">Admin</a></p>
+  <p style="margin-top:6px;font-size:11px">⚠️ Sample data only — not sourced from any county permit portal. Real-feed seam: <code>scripts/ingest-stub.js</code></p>
+</footer>
+
 <script>
-// House rule: sort + density both persist in localStorage so the choice survives reloads.
-const $=s=>document.querySelector(s), esc=s=>(s||'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
-const sortSel=$('#sort'), dens=$('#density');
-sortSel.value=localStorage.getItem('sort')||'newest'; dens.value=localStorage.getItem('cols')||'4';
-function applyCols(){document.documentElement.style.setProperty('--cols',dens.value);localStorage.setItem('cols',dens.value);}
+'use strict';
+const esc = s => (s||'').replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
+const fmtVal = v => v ? '$' + Number(v).toLocaleString() : '—';
+const fmtDate = d => d ? new Date(d + 'T12:00:00').toLocaleDateString('en-US', { month:'short', day:'numeric', year:'numeric' }) : '—';
+
+// ── Persist controls ──────────────────────────────────────────────────────────
+const sortSel = document.getElementById('sort');
+const dens    = document.getElementById('density');
+const search  = document.getElementById('search');
+const filtJ   = document.getElementById('filt-jurisdiction');
+const filtT   = document.getElementById('filt-type');
+const filtS   = document.getElementById('filt-status');
+
+sortSel.value = localStorage.getItem('pr_sort') || 'date_desc';
+dens.value    = localStorage.getItem('pr_cols') || '3';
+
+function applyCols() {
+  document.documentElement.style.setProperty('--cols', dens.value);
+  localStorage.setItem('pr_cols', dens.value);
+}
 applyCols();
-async function load(){
-  const r=await fetch('/api/products?sort='+encodeURIComponent(sortSel.value)); const d=await r.json();
-  $('#count').textContent=d.count+' items';
-  $('#grid').innerHTML = d.products.length ? d.products.map(p=>`
-    <div class="card">
-      <img src="${esc(p.image||'')}" alt="${esc(p.title||'')}" loading="lazy">
-      <div class="meta"><p class="t">${esc(p.title||'Untitled')}</p>
-        <div class="s">${esc(p.sku||p.handle||'')}</div>
-        ${p.price?`<div class="p">$${Number(p.price).toFixed(2)}</div>`:''}
-      </div>
-    </div>`).join('') : '<div class="empty">No products yet — fill data/products.json.</div>';
+
+// ── Fetch + render ────────────────────────────────────────────────────────────
+let debounce;
+async function load() {
+  const params = new URLSearchParams({
+    sort: sortSel.value,
+    q: search.value,
+    jurisdiction: filtJ.value,
+    permit_type: filtT.value,
+    status: filtS.value,
+  });
+  const r = await fetch('/api/permits?' + params);
+  const d = await r.json();
+  document.getElementById('count').textContent = d.count + ' permits';
+  const grid = document.getElementById('grid');
+  if (!d.permits.length) {
+    grid.innerHTML = '<div class="empty">No permits match your filters.</div>';
+    return;
+  }
+  grid.innerHTML = d.permits.map(p => {
+    const statusClass = 'status-' + (p.status || 'pending');
+    const contractor = p.contractor ? `<span class="meta-item"><strong>Contractor:</strong> ${esc(p.contractor)}</span>` : '';
+    return `
+<div class="card">
+  <div class="card-header">
+    <div>
+      <div class="permit-id">${esc(p.id)}</div>
+      <span class="type-chip">${esc(p.permit_type_label || p.permit_type)}</span>
+    </div>
+    <span class="status-chip ${statusClass}">${esc(p.status)}</span>
+  </div>
+  <div class="card-body">
+    <p class="card-address">${esc(p.address)}</p>
+    <p class="card-city">${esc(p.city)}, ${esc(p.state)} ${esc(p.zip)} — ${esc(p.jurisdiction)}</p>
+    <div class="card-meta">
+      <span class="meta-item"><strong>Filed:</strong> ${fmtDate(p.date_filed)}</span>
+      <span class="valuation">${fmtVal(p.valuation)}</span>
+      ${contractor}
+    </div>
+    <div class="card-desc">${esc(p.description)}</div>
+  </div>
+  <div class="card-footer">
+    <a class="btn-detail" href="/permit/${esc(p.id)}">View Details</a>
+    <button class="btn-lead" onclick='openLead(${JSON.stringify(p.id)}, ${JSON.stringify(p.address + ", " + p.city)})'>Request Lead</button>
+  </div>
+</div>`;
+  }).join('');
 }
-sortSel.onchange=()=>{localStorage.setItem('sort',sortSel.value);load();};
-dens.oninput=applyCols;
+
+sortSel.onchange = () => { localStorage.setItem('pr_sort', sortSel.value); load(); };
+dens.oninput = applyCols;
+search.oninput = () => { clearTimeout(debounce); debounce = setTimeout(load, 300); };
+filtJ.onchange = filtT.onchange = filtS.onchange = load;
+
 load();
-</script></body></html>
+
+// ── Subscribe form ────────────────────────────────────────────────────────────
+async function submitSubscribe(e) {
+  e.preventDefault();
+  const form = e.target;
+  const body = Object.fromEntries(new FormData(form));
+  const msg = document.getElementById('sub-msg');
+  msg.innerHTML = '';
+  try {
+    const r = await fetch('/api/subscribe', {
+      method: 'POST', headers: {'Content-Type':'application/json'},
+      body: JSON.stringify(body)
+    });
+    const d = await r.json();
+    if (d.ok) {
+      msg.innerHTML = '<div class="success-msg">✓ Subscribed! ID: ' + esc(d.id) + '<br>We\'ll be in touch to complete your onboarding.</div>';
+      form.reset();
+    } else {
+      msg.innerHTML = '<div class="error-msg">Error: ' + esc(d.error || 'Unknown error') + '</div>';
+    }
+  } catch (err) {
+    msg.innerHTML = '<div class="error-msg">Network error — please try again.</div>';
+  }
+}
+
+// ── Lead modal ────────────────────────────────────────────────────────────────
+function openLead(id, label) {
+  document.getElementById('modal-permit-id').value = id;
+  document.getElementById('modal-permit-label').textContent = label;
+  document.getElementById('lead-msg').innerHTML = '';
+  document.getElementById('lead-form').reset();
+  document.getElementById('modal-permit-id').value = id; // re-set after reset
+  document.getElementById('lead-modal').classList.add('open');
+}
+function closeLead() {
+  document.getElementById('lead-modal').classList.remove('open');
+}
+
+async function submitLead(e) {
+  e.preventDefault();
+  const form = e.target;
+  const body = Object.fromEntries(new FormData(form));
+  const msg = document.getElementById('lead-msg');
+  msg.innerHTML = '';
+  try {
+    const r = await fetch('/api/lead-request', {
+      method: 'POST', headers: {'Content-Type':'application/json'},
+      body: JSON.stringify(body)
+    });
+    const d = await r.json();
+    if (d.ok) {
+      msg.innerHTML = '<div class="success-msg">✓ Lead request submitted! ID: ' + esc(d.id) + '</div>';
+      setTimeout(closeLead, 2500);
+    } else {
+      msg.innerHTML = '<div class="error-msg">Error: ' + esc(d.error) + '</div>';
+    }
+  } catch (err) {
+    msg.innerHTML = '<div class="error-msg">Network error.</div>';
+  }
+}
+</script>
+</body>
+</html>
diff --git a/public/permit.html b/public/permit.html
new file mode 100644
index 0000000..3b3a924
--- /dev/null
+++ b/public/permit.html
@@ -0,0 +1,225 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Permit Detail — Permit Radar</title>
+<style>
+:root { --accent: #1d4ed8; --accent-light: #eff6ff; }
+*, *::before, *::after { box-sizing: border-box; }
+body { margin: 0; font: 15px/1.5 -apple-system, Segoe UI, Roboto, sans-serif; color: #1a1a1a; background: #f3f4f6; }
+
+.sample-banner { background: #fef3c7; border-bottom: 2px solid #f59e0b; padding: 8px 20px; font-size: 13px; text-align: center; color: #78350f; }
+nav { background: #1e293b; color: #fff; padding: 12px 20px; display: flex; align-items: center; gap: 16px; }
+.logo { font-size: 18px; font-weight: 800; color: #fff; text-decoration: none; }
+.logo span { color: #60a5fa; }
+.nav-links { margin-left: auto; display: flex; gap: 16px; }
+.nav-links a { color: #cbd5e1; font-size: 13px; text-decoration: none; }
+.nav-links a:hover { color: #fff; }
+
+.container { max-width: 800px; margin: 32px auto; padding: 0 20px 40px; }
+.back-link { font-size: 13px; color: var(--accent); text-decoration: none; margin-bottom: 16px; display: inline-block; }
+.back-link:hover { text-decoration: underline; }
+
+.permit-card { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; }
+.permit-header { background: linear-gradient(135deg, #1e293b 0%, #1d4ed8 100%); color: #fff; padding: 28px; }
+.permit-header-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; flex-wrap: wrap; }
+.permit-id-label { font-size: 12px; color: #93c5fd; font-weight: 600; letter-spacing: .5px; margin-bottom: 4px; }
+.permit-address-h { font-size: 22px; font-weight: 800; margin: 0 0 2px; }
+.permit-location { font-size: 14px; color: #bfdbfe; }
+.status-chip { font-size: 12px; font-weight: 700; padding: 4px 12px; border-radius: 99px; white-space: nowrap; margin-top: 4px; }
+.status-approved { background: #dcfce7; color: #15803d; }
+.status-pending   { background: #fef9c3; color: #854d0e; }
+
+.permit-body { padding: 28px; }
+.section-title { font-size: 11px; font-weight: 700; color: #94a3b8; letter-spacing: 1px; text-transform: uppercase; margin: 0 0 12px; }
+.fields-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 16px; margin-bottom: 24px; }
+.field { }
+.field-label { font-size: 11px; color: #94a3b8; font-weight: 600; margin-bottom: 2px; }
+.field-value { font-size: 15px; font-weight: 600; color: #1e293b; }
+.field-value.big { font-size: 20px; color: #15803d; }
+.permit-desc { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 16px; font-size: 14px; color: #374151; line-height: 1.6; margin-bottom: 24px; }
+
+.cta-box { background: var(--accent-light); border: 1px solid #bfdbfe; border-radius: 10px; padding: 20px; }
+.cta-box h3 { font-size: 16px; margin: 0 0 6px; color: #1e40af; }
+.cta-box p { font-size: 13px; color: #3b82f6; margin: 0 0 14px; }
+.cta-row { display: flex; gap: 10px; flex-wrap: wrap; }
+.btn-lead {
+  padding: 10px 20px; background: #f59e0b; color: #1a1a1a; border: none;
+  border-radius: 8px; font: 700 14px inherit; cursor: pointer;
+}
+.btn-lead:hover { background: #fbbf24; }
+.btn-subscribe {
+  padding: 10px 20px; background: var(--accent); color: #fff; border: none;
+  border-radius: 8px; font: 700 14px inherit; cursor: pointer; text-decoration: none; display: inline-block;
+}
+.btn-subscribe:hover { background: #1e40af; }
+
+/* Lead modal */
+.modal-bg { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 100; align-items: center; justify-content: center; }
+.modal-bg.open { display: flex; }
+.modal { background: #fff; border-radius: 12px; padding: 28px; width: 100%; max-width: 420px; margin: 20px; position: relative; }
+.modal h3 { margin: 0 0 6px; }
+.modal p { color: #64748b; font-size: 13px; margin: 0 0 16px; }
+.modal-close { position: absolute; top: 14px; right: 16px; font-size: 20px; cursor: pointer; color: #94a3b8; background: none; border: none; }
+.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px; }
+.form-row.single { grid-template-columns: 1fr; }
+label { display: block; font-size: 12px; font-weight: 600; color: #475569; margin-bottom: 4px; }
+select, input { width: 100%; padding: 9px 12px; border: 1px solid #d1d5db; border-radius: 6px; font: inherit; font-size: 14px; }
+.btn-submit { width: 100%; padding: 12px; background: var(--accent); color: #fff; border: none; border-radius: 8px; font: 700 15px inherit; cursor: pointer; margin-top: 8px; }
+.success-msg { background: #dcfce7; color: #15803d; border-radius: 6px; padding: 10px; margin-top: 10px; font-size: 13px; text-align: center; }
+.error-msg { background: #fee2e2; color: #991b1b; border-radius: 6px; padding: 10px; margin-top: 10px; font-size: 13px; text-align: center; }
+.loading { color: #94a3b8; padding: 60px; text-align: center; }
+</style>
+</head>
+<body>
+
+<div class="sample-banner">⚠️ <strong>SAMPLE / REPRESENTATIVE DATA</strong> — not from any county portal.</div>
+
+<nav>
+  <a class="logo" href="/"><span>📡</span> Permit Radar</a>
+  <div class="nav-links">
+    <a href="/">Directory</a>
+    <a href="/#get-alerts">Get Alerts</a>
+    <a href="/admin">Admin</a>
+  </div>
+</nav>
+
+<div class="container">
+  <a class="back-link" href="/">← Back to Directory</a>
+  <div id="content" class="loading">Loading permit…</div>
+</div>
+
+<!-- Lead modal -->
+<div class="modal-bg" id="lead-modal" onclick="if(event.target===this)closeLead()">
+  <div class="modal">
+    <button class="modal-close" onclick="closeLead()">×</button>
+    <h3>Request This Lead</h3>
+    <p id="modal-permit-label">—</p>
+    <form id="lead-form" onsubmit="submitLead(event)">
+      <input type="hidden" id="modal-permit-id" name="permit_id">
+      <div class="form-row">
+        <div>
+          <label>Your Name</label>
+          <input type="text" name="name" placeholder="Jane Smith">
+        </div>
+        <div>
+          <label>Trade Type *</label>
+          <select name="trade_type" required>
+            <option value="">Select…</option>
+            <option value="general_contractor">General Contractor</option>
+            <option value="kitchen_bath">Kitchen &amp; Bath</option>
+            <option value="interior_designer">Interior Designer</option>
+            <option value="roofing">Roofer</option>
+            <option value="flooring">Flooring</option>
+            <option value="hvac">HVAC</option>
+            <option value="home_goods_vendor">Home Goods Vendor</option>
+            <option value="wallcovering_vendor">Wallcovering Vendor</option>
+            <option value="other">Other</option>
+          </select>
+        </div>
+      </div>
+      <div class="form-row">
+        <div>
+          <label>Email *</label>
+          <input type="email" name="email" required placeholder="you@company.com">
+        </div>
+        <div>
+          <label>Phone</label>
+          <input type="tel" name="phone" placeholder="555-555-5555">
+        </div>
+      </div>
+      <div class="form-row single">
+        <div>
+          <label>Message</label>
+          <input type="text" name="message" placeholder="Services you offer">
+        </div>
+      </div>
+      <button type="submit" class="btn-submit">Request Lead →</button>
+      <div id="lead-msg"></div>
+    </form>
+  </div>
+</div>
+
+<script>
+'use strict';
+const esc = s => (s||'').replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
+const fmtVal  = v => v ? '$' + Number(v).toLocaleString() : '—';
+const fmtDate = d => d ? new Date(d + 'T12:00:00').toLocaleDateString('en-US', { month:'long', day:'numeric', year:'numeric' }) : '—';
+
+async function load() {
+  const id = decodeURIComponent(location.pathname.replace('/permit/', '').replace(/\/$/, ''));
+  const r = await fetch('/api/permits/' + encodeURIComponent(id));
+  const content = document.getElementById('content');
+  if (!r.ok) { content.innerHTML = '<p style="color:#ef4444">Permit not found.</p>'; return; }
+  const { permit: p } = await r.json();
+  document.title = p.address + ' — Permit Radar';
+  const statusClass = 'status-' + (p.status || 'pending');
+  content.innerHTML = `
+<div class="permit-card">
+  <div class="permit-header">
+    <div class="permit-header-top">
+      <div>
+        <div class="permit-id-label">Permit ${esc(p.id)}</div>
+        <div class="permit-address-h">${esc(p.address)}</div>
+        <div class="permit-location">${esc(p.city)}, ${esc(p.state)} ${esc(p.zip)} — ${esc(p.jurisdiction)}</div>
+      </div>
+      <span class="status-chip ${statusClass}">${esc(p.status)}</span>
+    </div>
+  </div>
+  <div class="permit-body">
+    <div class="section-title">Permit Details</div>
+    <div class="fields-grid">
+      <div class="field"><div class="field-label">Date Filed</div><div class="field-value">${fmtDate(p.date_filed)}</div></div>
+      <div class="field"><div class="field-label">Permit Type</div><div class="field-value">${esc(p.permit_type_label || p.permit_type)}</div></div>
+      <div class="field"><div class="field-label">Valuation</div><div class="field-value big">${fmtVal(p.valuation)}</div></div>
+      <div class="field"><div class="field-label">Sq Ft Affected</div><div class="field-value">${p.sqft_affected ? p.sqft_affected.toLocaleString() + ' sq ft' : '—'}</div></div>
+      <div class="field"><div class="field-label">Status</div><div class="field-value">${esc(p.status)}</div></div>
+      <div class="field"><div class="field-label">Bedrooms (home)</div><div class="field-value">${p.bedrooms || '—'}</div></div>
+    </div>
+    ${p.contractor ? `
+    <div class="section-title">Contractor on Record</div>
+    <div class="fields-grid">
+      <div class="field"><div class="field-label">Contractor</div><div class="field-value">${esc(p.contractor)}</div></div>
+      <div class="field"><div class="field-label">License</div><div class="field-value">${esc(p.contractor_license || '—')}</div></div>
+    </div>` : ''}
+    <div class="section-title">Description</div>
+    <div class="permit-desc">${esc(p.description)}</div>
+    <div class="cta-box">
+      <h3>Reach this homeowner at the renovation moment</h3>
+      <p>Request this lead directly, or subscribe for alerts on every permit like it in your area.</p>
+      <div class="cta-row">
+        <button class="btn-lead" onclick="openLead('${esc(p.id)}', '${esc(p.address + ', ' + p.city)}')">Request This Lead</button>
+        <a class="btn-subscribe" href="/#get-alerts">Get Area Alerts →</a>
+      </div>
+    </div>
+  </div>
+</div>`;
+}
+load();
+
+function openLead(id, label) {
+  document.getElementById('modal-permit-id').value = id;
+  document.getElementById('modal-permit-label').textContent = label;
+  document.getElementById('lead-msg').innerHTML = '';
+  document.getElementById('lead-form').reset();
+  document.getElementById('modal-permit-id').value = id;
+  document.getElementById('lead-modal').classList.add('open');
+}
+function closeLead() { document.getElementById('lead-modal').classList.remove('open'); }
+
+async function submitLead(e) {
+  e.preventDefault();
+  const body = Object.fromEntries(new FormData(e.target));
+  const msg = document.getElementById('lead-msg');
+  msg.innerHTML = '';
+  const r = await fetch('/api/lead-request', {
+    method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body)
+  });
+  const d = await r.json();
+  if (d.ok) { msg.innerHTML = '<div class="success-msg">✓ Lead request submitted! ID: ' + esc(d.id) + '</div>'; setTimeout(closeLead, 2500); }
+  else { msg.innerHTML = '<div class="error-msg">Error: ' + esc(d.error) + '</div>'; }
+}
+</script>
+</body>
+</html>
diff --git a/scripts/ingest-stub.js b/scripts/ingest-stub.js
new file mode 100644
index 0000000..47e98b8
--- /dev/null
+++ b/scripts/ingest-stub.js
@@ -0,0 +1,150 @@
+// permit-radar / scripts/ingest-stub.js
+//
+// REAL PERMIT FEED SEAM
+// ─────────────────────────────────────────────────────────────────────────────
+// Replace data/permits.json sample data with real county permit records here.
+//
+// HOW A REAL FEED WORKS (per-county options):
+//
+// 1. MUNICIPAL / COUNTY OPEN DATA PORTALS (free, Socrata/ArcGIS APIs)
+//    Many counties publish permit data via Socrata: e.g.
+//    - Los Angeles County: data.lacounty.gov  (search "building permits")
+//    - San Francisco:      data.sfgov.org      /resource/i98e-djp9.json
+//    - Chicago:            data.cityofchicago.org  /resource/ydr8-5enu.json
+//    Use the Socrata SODA API — free, JSON, filterable by date & type.
+//    Example:
+//      const { SodaClient } = require('soda-js');
+//      const client = new SodaClient('data.lacounty.gov');
+//      client.query().withDataset('permit_dataset_id').where('permit_type_desc', 'REMODEL')
+//            .getRows().on('row', row => permits.push(normalizeRow(row)));
+//
+// 2. BUILDFAX / PERMITZIP / SALESDRIVE (paid commercial feeds)
+//    These aggregate nationwide permit data behind a subscription API.
+//    - BuildFax: https://buildfax.com/api  (REST, JSON, $$ per county)
+//    - PermitZip: bulk CSV download or webhook per jurisdiction
+//    Wire credentials via env: BUILDFAX_API_KEY=... or PERMITZIP_API_KEY=...
+//
+// 3. DIRECT COUNTY PORTAL SCRAPING (inconsistent, often paywalled)
+//    Some counties use iParc / EnerGov / Accela portals. Most require auth
+//    or session tokens. NOT recommended for production without legal review.
+//
+// NORMALIZATION CONTRACT
+// ─────────────────────────────────────────────────────────────────────────────
+// Every upstream record must be normalized to this schema before writing to
+// data/permits.json so the server + frontend work without changes:
+//
+//   {
+//     id: string,           // unique e.g. "LA-2024-100001"
+//     address: string,      // street address
+//     city: string,
+//     jurisdiction: string, // "Los Angeles County"
+//     state: string,        // "CA"
+//     zip: string,
+//     date_filed: string,   // "YYYY-MM-DD"
+//     permit_type: string,  // machine key: kitchen_remodel | bath_remodel | addition | full_remodel | adu | reroof
+//     permit_type_label: string, // human label
+//     valuation: number,    // dollar amount
+//     contractor: string,   // may be ""
+//     contractor_license: string,
+//     status: string,       // "approved" | "pending" | "issued"
+//     description: string,
+//     bedrooms: number,
+//     sqft_affected: number,
+//     created_at: string,   // ISO timestamp of when WE ingested it
+//   }
+//
+// USAGE (to be run as a cron job or manual refresh):
+//   node scripts/ingest-stub.js --county "los-angeles" --days 7
+//   node scripts/ingest-stub.js --source buildfax --zips 90210,90401
+//
+// The stub below shows the structure; replace with real API calls.
+// ─────────────────────────────────────────────────────────────────────────────
+
+'use strict';
+const fs   = require('fs');
+const path = require('path');
+
+const DATA_FILE = path.join(__dirname, '..', 'data', 'permits.json');
+
+// ── Normalization helper ──────────────────────────────────────────────────────
+function normalizeRow(raw, jurisdiction, source) {
+  // Adapt field names from whichever source is feeding this.
+  // This is a skeleton — fill in per-source mappings.
+  return {
+    id: source + '-' + (raw.permit_number || raw.id || Date.now()),
+    address: raw.address || raw.street_address || '',
+    city: raw.city || raw.city_name || '',
+    jurisdiction: jurisdiction,
+    state: raw.state || '',
+    zip: raw.zip_code || raw.zip || '',
+    date_filed: (raw.issue_date || raw.filed_date || raw.date_filed || '').slice(0, 10),
+    permit_type: mapPermitType(raw.permit_type || raw.permit_type_desc || ''),
+    permit_type_label: raw.permit_type_desc || raw.permit_type || '',
+    valuation: parseFloat(raw.valuation || raw.estimated_cost || 0) || 0,
+    contractor: raw.contractor_name || raw.contractor || '',
+    contractor_license: raw.contractor_license || '',
+    status: (raw.status || 'pending').toLowerCase(),
+    description: raw.description || raw.work_description || '',
+    bedrooms: parseInt(raw.bedrooms || 0, 10) || 0,
+    sqft_affected: parseInt(raw.sqft || raw.sq_ft_affected || 0, 10) || 0,
+    created_at: new Date().toISOString(),
+  };
+}
+
+function mapPermitType(raw) {
+  const r = (raw || '').toLowerCase();
+  if (r.includes('kitchen'))                      return 'kitchen_remodel';
+  if (r.includes('bath'))                         return 'bath_remodel';
+  if (r.includes('addition') || r.includes('adu') || r.includes('accessory')) return r.includes('adu') || r.includes('accessory') ? 'adu' : 'addition';
+  if (r.includes('reroof') || r.includes('roof')) return 'reroof';
+  if (r.includes('remodel') || r.includes('renovation') || r.includes('interior')) return 'full_remodel';
+  return 'full_remodel';
+}
+
+// ── Stub: fake fetch from a Socrata endpoint ──────────────────────────────────
+async function fetchFromSocrata(domain, dataset, jurisdiction) {
+  // Real impl: replace URL below with actual county Socrata dataset ID.
+  // const url = `https://${domain}/resource/${dataset}.json?$where=date_filed>='${since}'&$limit=1000`;
+  // const rows = await fetch(url).then(r => r.json());
+  // return rows.map(r => normalizeRow(r, jurisdiction, domain));
+  console.log(`[ingest-stub] STUB: would fetch from https://${domain}/resource/${dataset}.json`);
+  return [];
+}
+
+// ── Main ──────────────────────────────────────────────────────────────────────
+async function main() {
+  const args = process.argv.slice(2);
+  const dryRun = args.includes('--dry-run');
+  console.log('[ingest-stub] Real-feed seam — replace stubs with live API calls.');
+  console.log('[ingest-stub] Source options: Socrata open data, BuildFax, PermitZip — see comments above.');
+  if (dryRun) { console.log('[ingest-stub] --dry-run: no writes.'); return; }
+
+  // Example county feed registrations:
+  const sources = [
+    { domain: 'data.lacounty.gov',      dataset: 'PLACEHOLDER_DATASET_ID', jurisdiction: 'Los Angeles County' },
+    { domain: 'data.sfgov.org',         dataset: 'i98e-djp9',              jurisdiction: 'San Francisco County' },
+    { domain: 'data.cityofchicago.org', dataset: 'ydr8-5enu',              jurisdiction: 'Cook County' },
+  ];
+
+  let all = [];
+  for (const s of sources) {
+    const rows = await fetchFromSocrata(s.domain, s.dataset, s.jurisdiction);
+    all = all.concat(rows);
+  }
+
+  if (!all.length) {
+    console.log('[ingest-stub] No records fetched (stubs return empty). Wire real APIs to populate.');
+    return;
+  }
+
+  // Merge with existing — deduplicate by id
+  let existing = [];
+  try { existing = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8')); } catch {}
+  const existingIds = new Set(existing.map(p => p.id));
+  const newRecords = all.filter(p => !existingIds.has(p.id));
+  const merged = [...existing, ...newRecords].sort((a, b) => b.date_filed.localeCompare(a.date_filed));
+  fs.writeFileSync(DATA_FILE, JSON.stringify(merged, null, 2), 'utf8');
+  console.log(`[ingest-stub] Wrote ${merged.length} total permits (${newRecords.length} new) to data/permits.json`);
+}
+
+main().catch(err => { console.error('[ingest-stub] Error:', err.message); process.exit(1); });
diff --git a/server.js b/server.js
index 57f5df5..0d18448 100644
--- a/server.js
+++ b/server.js
@@ -1,41 +1,265 @@
-// Storefront starter — zero-dep Node http. Serves data/products.json with SERVER-SIDE sort
-// (house rule: every product grid gets sort + a density slider). Optional basic auth via BASIC_AUTH.
-const http = require('http'), fs = require('fs'), path = require('path');
-const PORT = parseInt(process.env.PORT || '3900', 10);
-const BASIC_AUTH = process.env.BASIC_AUTH || ''; // "user:pass" to gate; empty = open
+// permit-radar — zero-dep Node http server
+// Serves permit directory with sort/filter, lead funnel, and admin dashboard.
+// PORT from env (default 3905). BASIC_AUTH=user:pass gates /admin.
+//
+// REAL FEED SEAM: see scripts/ingest-stub.js for how a real county permit
+// feed would populate data/permits.json. The sample data is representative only.
+'use strict';
+const http = require('http');
+const fs = require('fs');
+const path = require('path');
+
+const PORT = parseInt(process.env.PORT || '3905', 10);
+const BASIC_AUTH = process.env.BASIC_AUTH || ''; // "user:pass" to gate /admin
 const DIR = __dirname;
 
-function load() { try { return JSON.parse(fs.readFileSync(path.join(DIR, 'data', 'products.json'), 'utf8')); } catch { return []; } }
-function authed(req) {
-  if (!BASIC_AUTH) return true;
-  const m = (req.headers.authorization || '').match(/^Basic\s+(.+)$/i);
-  if (!m) return false; try { return Buffer.from(m[1], 'base64').toString() === BASIC_AUTH; } catch { return false; }
+// ── Data helpers ─────────────────────────────────────────────────────────────
+
+function loadJSON(file) {
+  try { return JSON.parse(fs.readFileSync(path.join(DIR, 'data', file), 'utf8')); } catch { return []; }
+}
+function saveJSON(file, data) {
+  fs.writeFileSync(path.join(DIR, 'data', file), JSON.stringify(data, null, 2), 'utf8');
 }
-// Sort modes mirror the DW sort-skill: newest, title/sku A→Z, price ↑↓, light→dark (by dominant hex).
-function luminance(hex) { if (!/^#?[0-9a-f]{6}$/i.test(hex || '')) return 999; const h = hex.replace('#', '');
-  const r = parseInt(h.slice(0, 2), 16), g = parseInt(h.slice(2, 4), 16), b = parseInt(h.slice(4, 6), 16);
-  return 0.2126 * r + 0.7152 * g + 0.0722 * b; }
-function sortProducts(items, mode) {
+function loadPermits()      { return loadJSON('permits.json'); }
+function loadSubscribers()  { return loadJSON('subscribers.json'); }
+function loadLeadRequests() { return loadJSON('lead_requests.json'); }
+
+// ── Permit sort ───────────────────────────────────────────────────────────────
+
+function sortPermits(items, mode) {
   const a = items.slice();
   switch (mode) {
-    case 'title': return a.sort((x, y) => (x.title || '').localeCompare(y.title || ''));
-    case 'sku': return a.sort((x, y) => (x.sku || x.handle || '').localeCompare(y.sku || y.handle || ''));
-    case 'price-asc': return a.sort((x, y) => (x.price || 0) - (y.price || 0));
-    case 'price-desc': return a.sort((x, y) => (y.price || 0) - (x.price || 0));
-    case 'light': return a.sort((x, y) => luminance(y.hex) - luminance(x.hex));
-    case 'dark': return a.sort((x, y) => luminance(x.hex) - luminance(y.hex));
-    default: return a; // 'newest' = natural order
+    case 'date_asc':       return a.sort((x, y) => x.date_filed.localeCompare(y.date_filed));
+    case 'date_desc':      return a.sort((x, y) => y.date_filed.localeCompare(x.date_filed));
+    case 'valuation_asc':  return a.sort((x, y) => (x.valuation || 0) - (y.valuation || 0));
+    case 'valuation_desc': return a.sort((x, y) => (y.valuation || 0) - (x.valuation || 0));
+    case 'city':           return a.sort((x, y) => (x.city || '').localeCompare(y.city || ''));
+    case 'type':           return a.sort((x, y) => (x.permit_type || '').localeCompare(y.permit_type || ''));
+    case 'address':        return a.sort((x, y) => (x.address || '').localeCompare(y.address || ''));
+    default:               return a; // 'newest' = file order (newest filed last in seed; reversed by date_desc)
+  }
+}
+
+// ── Permit filter ─────────────────────────────────────────────────────────────
+
+function filterPermits(items, params) {
+  let out = items;
+  const q             = (params.get('q') || '').toLowerCase().trim();
+  const jurisdiction  = (params.get('jurisdiction') || '').toLowerCase().trim();
+  const permit_type   = (params.get('permit_type') || '').toLowerCase().trim();
+  const status        = (params.get('status') || '').toLowerCase().trim();
+  const val_min       = parseFloat(params.get('val_min') || '0') || 0;
+  const val_max       = parseFloat(params.get('val_max') || '0') || 0;
+  const date_from     = params.get('date_from') || '';
+  const date_to       = params.get('date_to') || '';
+
+  if (q) {
+    out = out.filter(p =>
+      (p.address || '').toLowerCase().includes(q) ||
+      (p.city || '').toLowerCase().includes(q) ||
+      (p.jurisdiction || '').toLowerCase().includes(q) ||
+      (p.description || '').toLowerCase().includes(q) ||
+      (p.contractor || '').toLowerCase().includes(q) ||
+      (p.permit_type_label || '').toLowerCase().includes(q)
+    );
   }
+  if (jurisdiction) out = out.filter(p => (p.jurisdiction || '').toLowerCase().includes(jurisdiction));
+  if (permit_type)  out = out.filter(p => (p.permit_type || '').toLowerCase() === permit_type);
+  if (status)       out = out.filter(p => (p.status || '').toLowerCase() === status);
+  if (val_min > 0)  out = out.filter(p => (p.valuation || 0) >= val_min);
+  if (val_max > 0)  out = out.filter(p => (p.valuation || 0) <= val_max);
+  if (date_from)    out = out.filter(p => p.date_filed >= date_from);
+  if (date_to)      out = out.filter(p => p.date_filed <= date_to);
+  return out;
 }
-http.createServer((req, res) => {
-  if (!authed(req)) { res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="preview"' }); return res.end('auth required'); }
-  const u = new URL(req.url, 'http://x');
-  if (u.pathname === '/api/products') {
-    const out = sortProducts(load(), u.searchParams.get('sort') || 'newest');
-    res.writeHead(200, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ count: out.length, products: out }));
-  }
-  const f = u.pathname === '/' ? 'index.html' : u.pathname.replace(/^\//, '');
-  const fp = path.join(DIR, 'public', path.basename(f));
-  if (fs.existsSync(fp)) { res.writeHead(200, { 'Content-Type': f.endsWith('.html') ? 'text/html' : 'text/plain' }); return res.end(fs.readFileSync(fp)); }
-  res.writeHead(404); res.end('not found');
-}).listen(PORT, function () { console.log('[' + path.basename(DIR) + '] http://localhost:' + this.address().port); });
+
+// ── Auth helpers ──────────────────────────────────────────────────────────────
+
+function adminAuthed(req) {
+  if (!BASIC_AUTH) return true;
+  const m = (req.headers.authorization || '').match(/^Basic\s+(.+)$/i);
+  if (!m) return false;
+  try { return Buffer.from(m[1], 'base64').toString() === BASIC_AUTH; } catch { return false; }
+}
+
+// ── MIME ──────────────────────────────────────────────────────────────────────
+
+function mime(f) {
+  if (f.endsWith('.html')) return 'text/html; charset=utf-8';
+  if (f.endsWith('.css'))  return 'text/css';
+  if (f.endsWith('.js'))   return 'application/javascript';
+  if (f.endsWith('.json')) return 'application/json';
+  if (f.endsWith('.ico'))  return 'image/x-icon';
+  return 'text/plain';
+}
+
+// ── Body parser ───────────────────────────────────────────────────────────────
+
+function readBody(req) {
+  return new Promise((resolve, reject) => {
+    const chunks = [];
+    req.on('data', c => chunks.push(c));
+    req.on('end', () => {
+      try { resolve(JSON.parse(Buffer.concat(chunks).toString())); }
+      catch { resolve({}); }
+    });
+    req.on('error', reject);
+  });
+}
+
+// ── Server ────────────────────────────────────────────────────────────────────
+
+const server = http.createServer(async (req, res) => {
+  const parsed = new URL(req.url, `http://localhost:${PORT}`);
+  const pathname = parsed.pathname;
+  const params = parsed.searchParams;
+  const method = req.method.toUpperCase();
+
+  // ── JSON API ────────────────────────────────────────────────────────────────
+
+  if (pathname === '/api/permits' && method === 'GET') {
+    const raw = loadPermits();
+    const filtered = filterPermits(raw, params);
+    const sorted = sortPermits(filtered, params.get('sort') || 'date_desc');
+    res.writeHead(200, { 'Content-Type': 'application/json' });
+    return res.end(JSON.stringify({ count: sorted.length, permits: sorted, sample: true }));
+  }
+
+  if (pathname.startsWith('/api/permits/') && method === 'GET') {
+    const id = decodeURIComponent(pathname.replace('/api/permits/', ''));
+    const permit = loadPermits().find(p => p.id === id);
+    if (!permit) {
+      res.writeHead(404, { 'Content-Type': 'application/json' });
+      return res.end(JSON.stringify({ error: 'not found' }));
+    }
+    res.writeHead(200, { 'Content-Type': 'application/json' });
+    return res.end(JSON.stringify({ permit, sample: true }));
+  }
+
+  if (pathname === '/api/subscribe' && method === 'POST') {
+    const body = await readBody(req);
+    if (!body.email || !body.trade_type) {
+      res.writeHead(400, { 'Content-Type': 'application/json' });
+      return res.end(JSON.stringify({ error: 'email and trade_type required' }));
+    }
+    const subs = loadSubscribers();
+    const record = {
+      id: 'SUB-' + Date.now(),
+      email: body.email,
+      trade_type: body.trade_type,
+      service_area: body.service_area || '',
+      jurisdictions: body.jurisdictions || [],
+      permit_types: body.permit_types || [],
+      plan: body.plan || 'basic',
+      created_at: new Date().toISOString()
+    };
+    subs.push(record);
+    saveJSON('subscribers.json', subs);
+    res.writeHead(200, { 'Content-Type': 'application/json' });
+    return res.end(JSON.stringify({ ok: true, id: record.id, message: 'Subscription recorded.' }));
+  }
+
+  if (pathname === '/api/lead-request' && method === 'POST') {
+    const body = await readBody(req);
+    if (!body.permit_id || !body.email) {
+      res.writeHead(400, { 'Content-Type': 'application/json' });
+      return res.end(JSON.stringify({ error: 'permit_id and email required' }));
+    }
+    const permits = loadPermits();
+    const permit = permits.find(p => p.id === body.permit_id);
+    const reqs = loadLeadRequests();
+    const record = {
+      id: 'LR-' + Date.now(),
+      permit_id: body.permit_id,
+      permit_address: permit ? (permit.address + ', ' + permit.city) : '',
+      permit_valuation: permit ? permit.valuation : 0,
+      name: body.name || '',
+      email: body.email,
+      phone: body.phone || '',
+      trade_type: body.trade_type || '',
+      message: body.message || '',
+      created_at: new Date().toISOString()
+    };
+    reqs.push(record);
+    saveJSON('lead_requests.json', reqs);
+    res.writeHead(200, { 'Content-Type': 'application/json' });
+    return res.end(JSON.stringify({ ok: true, id: record.id, message: 'Lead request received.' }));
+  }
+
+  // Admin API — gated
+
+  if (pathname === '/api/admin/subscribers' && method === 'GET') {
+    if (!adminAuthed(req)) {
+      res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="permit-radar-admin"' });
+      return res.end('auth required');
+    }
+    const subs = loadSubscribers();
+    const sort = params.get('sort') || 'newest';
+    const sorted = sort === 'oldest'
+      ? subs.slice().sort((a, b) => a.created_at.localeCompare(b.created_at))
+      : subs.slice().sort((a, b) => b.created_at.localeCompare(a.created_at));
+    res.writeHead(200, { 'Content-Type': 'application/json' });
+    return res.end(JSON.stringify({ count: sorted.length, subscribers: sorted }));
+  }
+
+  if (pathname === '/api/admin/lead-requests' && method === 'GET') {
+    if (!adminAuthed(req)) {
+      res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="permit-radar-admin"' });
+      return res.end('auth required');
+    }
+    const reqs = loadLeadRequests();
+    const sort = params.get('sort') || 'newest';
+    const sorted = sort === 'oldest'
+      ? reqs.slice().sort((a, b) => a.created_at.localeCompare(b.created_at))
+      : reqs.slice().sort((a, b) => b.created_at.localeCompare(a.created_at));
+    res.writeHead(200, { 'Content-Type': 'application/json' });
+    return res.end(JSON.stringify({ count: sorted.length, lead_requests: sorted }));
+  }
+
+  // ── Static files ────────────────────────────────────────────────────────────
+
+  // /admin → admin.html (gated)
+  if (pathname === '/admin' || pathname === '/admin/') {
+    if (!adminAuthed(req)) {
+      res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="permit-radar-admin"' });
+      return res.end('auth required');
+    }
+    const fp = path.join(DIR, 'public', 'admin.html');
+    if (fs.existsSync(fp)) {
+      res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
+      return res.end(fs.readFileSync(fp));
+    }
+    res.writeHead(404); return res.end('admin.html not found');
+  }
+
+  // /permit/:id → permit.html (SPA-style; ID parsed on client from location)
+  if (pathname.startsWith('/permit/')) {
+    const fp = path.join(DIR, 'public', 'permit.html');
+    if (fs.existsSync(fp)) {
+      res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
+      return res.end(fs.readFileSync(fp));
+    }
+    res.writeHead(404); return res.end('not found');
+  }
+
+  // static files in /public
+  const relPath = pathname === '/' ? 'index.html' : pathname.replace(/^\//, '');
+  const safe = path.normalize(relPath).replace(/^(\.\.(\/|\\|$))+/, '');
+  const fp = path.join(DIR, 'public', safe);
+  if (fs.existsSync(fp) && fs.statSync(fp).isFile()) {
+    res.writeHead(200, { 'Content-Type': mime(fp) });
+    return res.end(fs.readFileSync(fp));
+  }
+
+  res.writeHead(404, { 'Content-Type': 'text/plain' });
+  res.end('not found');
+});
+
+server.listen(PORT, function () {
+  const addr = this.address();
+  console.log('[permit-radar] http://localhost:' + addr.port);
+  console.log('[permit-radar] Admin: BASIC_AUTH=admin:DW2024! PORT=3905 node server.js, then visit /admin');
+  console.log('[permit-radar] NOTE: permit data is SAMPLE/REPRESENTATIVE — not real county data.');
+  console.log('[permit-radar] Real feed seam: see scripts/ingest-stub.js');
+});

← f1fb193 initial scaffold (permit-radar) via web-dev accelerator  ·  back to Permit Radar  ·  feat: copy shopify-catalog.json + add /api/shopify-crosssell 591ba0d →