Stars of Design

A curated directory of the most influential interior designers + their firms + their real DW clients.

repo: ~/Projects/starsofdesign · 100 commits · 0 in last 24h, 0 in last 7d ·

Search the build

100 commits indexed

  1. 06be86b 2026-05-31 Add per-site favicon (kills /favicon.ico 404)
  2. 8fc1daa 2026-05-19 Static-bak guard returns plain text, not EJS template
  3. e2ee6aa 2026-05-19 Guard against serving .bak / .pre- snapshot artifacts from public/
  4. a7ac2a9 2026-05-19 Add noreferrer to all rel=nofollow noopener external links
  5. 18acbe8 2026-05-19 Hydrate saved sort BEFORE first grid render
  6. e85f238 2026-05-13 Makefile: audit target
  7. af307bc 2026-05-13 smoke: /api/health perf budget 500→1000ms
  8. f86aef5 2026-05-13 polish: editorial 2-line hero + by-era anthology section
  9. 03cea56 2026-05-13 smoke: assert /api/{designers,firms,clients}/featured SWR Cache-Control (3 checks)
  10. ea921b6 2026-05-13 smoke: cover /healthz + HEAD method + harness opts.method
  11. 4817b26 2026-05-13 smoke: assert Last-Modified on /sitemap.{xml,txt}
  12. 6650847 2026-05-13 smoke: home perf budget <1500ms
  13. d4b32db 2026-05-13 Makefile: add help target listing all available commands
  14. 54ab22f 2026-05-13 smoke: /api/health perf budget (<500ms)
  15. fe4a506 2026-05-13 Makefile: add fleet-status target
  16. bcf553b 2026-05-13 smoke: assert /firms + /clients sort + density (grid standard)
  17. 86dc319 2026-05-13 smoke: assert /designers has sort + density controls (Steve's grid standard)
  18. 73e8ba0 2026-05-13 smoke: 3 more home assertions — Permissions-Policy, canonical, color-scheme (asim parity)
  19. 4be3a23 2026-05-13 smoke: assert X-Frame-Options on home (asim parity)
  20. 338bd3e 2026-05-13 smoke: assert HSTS + CSP headers (helmet regression guard)
  21. e88edf9 2026-05-13 smoke: assert Vary: Accept-Encoding
  22. 3f74a86 2026-05-13 smoke: assert /api/version git !== 'unknown'
  23. d45d22b 2026-05-13 smoke: assert /api/version Cache-Control no-store
  24. fd7960e 2026-05-13 smoke: 2 assertions for branded 404 page
  25. 95b38f5 2026-05-13 smoke: 4 assertions for /api/version
  26. 49fd5af 2026-05-13 api/version: deploy fingerprint endpoint (asim parity)
  27. 90ada6d 2026-05-13 smoke: uptime_s + counts.designers assertions on /api/health
  28. eef66c3 2026-05-13 api/health: include counts (designers/firms) + uptime_s + as_of (asim parity)
  29. 3774c03 2026-05-13 smoke: 6 new checks for /api/{designers,firms,clients}/featured
  30. deb329f 2026-05-13 API: add /api/{designers,firms,clients}/featured (parity with asim featured)
  31. ce39c6b 2026-05-13 sitemap.txt: emit Last-Modified header for conditional GET (asim parity)
  32. 4edf44d 2026-05-13 smoke: 9 new checks for /api/{designers,firms,clients}/random + /random/{designer,firm,client}
  33. da27142 2026-05-13 API: add /random/designer + /random/firm + /random/client redirect helpers
  34. ba5954a 2026-05-13 API: add /api/designers/random + /api/firms/random + /api/clients/random (parity with asim)
  35. d54a257 2026-05-13 sitemap.xml: emit Last-Modified header for conditional GET (asim parity)
  36. fe83a3f 2026-05-13 sod robots.txt: also reference /sitemap.txt. 54/54.
  37. a808c8d 2026-05-13 sod Makefile — common dev targets (mirrors asim)
  38. 6d21b22 2026-05-13 sod sitemap.xml + .txt Cache-Control 1h→6h + SWR 24h (mirrors asim). 54/54.
  39. c0ba760 2026-05-13 sod /api discovery — list /api/health + smoke +2 /api/health checks. 54/54.
  40. fd9f828 2026-05-13 sod GET /api/health — mirrors asim. SELECT 1 liveness via statsPool, no-store, JSON ok+ts.
  41. 660a173 2026-05-13 sod smoke --json suppresses header
  42. b90e2bb 2026-05-13 sod smoke +--json flag (parity)
  43. a909b13 2026-05-13 sod .deploy.conf — point HEALTH_URL at /healthz (cheap liveness, no-store, faster than full home render). Mirrors asim.
  44. f337110 2026-05-13 sod footer +/api link — matches asim. 52/52.
  45. 920bdd7 2026-05-13 sod smoke +2 /sitemap.txt assertions. 52/52.
  46. 9136d4a 2026-05-13 sod GET /sitemap.txt — plain-text sitemap (Google+Bing supported). Also removed duplicate /healthz stub that was overshadowed. 50/50.
  47. ef12c2c 2026-05-13 sod /healthz Cache-Control: no-store (parity with asim)
  48. 9a655f6 2026-05-13 sod smoke +3 ItemList checks on /designers /firms /clients index pages. 50/50.
  49. 43b04f5 2026-05-13 starsofdesign: ItemList JSON-LD on /designers + /clients index pages — top-60 each as Person ListItems. 47/47.
  50. 2502570 2026-05-13 starsofdesign: /firms index ItemList JSON-LD — first 60 firms as Organization ListItems with positions. Rich-snippet eligible. 47/47.
  51. da98378 2026-05-13 sod head — +<meta name=color-scheme content="light dark"> since sod has runtime theme toggle. Form controls + scrollbars now respect the chosen theme. 47/47.
  52. 5b0cc55 2026-05-13 sod robots.txt — Disallow /api/ (mirrors asim, prevents wasted crawl on JSON endpoints).
  53. 47f5530 2026-05-13 sod /favicon.ico Cache-Control max-age=86400 (was 0). Mirrors asim.
  54. bb0e392 2026-05-13 sod smoke +gzip /firms check
  55. 5379dd0 2026-05-13 sod-smoke-quiet
  56. c9d3997 2026-05-13 starsofdesign: smoke +4 security-header checks (X-Content-Type-Options, Referrer-Policy, HSTS, CSP via helmet). 46/46.
  57. 0160b3c 2026-05-13 sodhz
  58. 4682bab 2026-05-12 sod-comments-cache
  59. 571d289 2026-05-12 sod-cache-stats
  60. 0954ae8 2026-05-12 starsofdesign: Cache-Control public max-age=3600 on /sitemap.xml. Mirrors asim.
  61. 13a504c 2026-05-12 starsofdesign: gzip compression middleware (threshold 1KB) — mirrors asim. JSON-LD-heavy /firms /designers /clients pages drop over the wire. Smoke 42/42.
  62. a32feca 2026-05-12 starsofdesign: smoke — add /api discovery doc check (+2 assertions). 42/42.
  63. 6e94cda 2026-05-12 starsofdesign: GET /api — JSON discovery doc for public API endpoints (stats/comments/like/comment) + PII policy notes. Mirrors asim /api pattern.
  64. 9d16cb3 2026-05-12 starsofdesign: smoke test harness — test/smoke.js (zero deps, pure node) hits every public route and asserts 200/3xx + key DOM markers (canonical, WebSite/Person/Organization/BreadcrumbList JSON-LD, footer privacy/terms links). 40 checks. npm test wired. 40/40 pass on initial run. Mirrors asim smoke harness.
  65. 00a2a60 2026-05-12 starsofdesign: GET /privacy + /terms — minimal legal pages (no-collect privacy notice, public-data terms, editorial-only liability). Mirrors asim. Linked from footer + sitemap. Closes parity gap.
  66. ed2f2d8 2026-05-12 starsofdesign: BreadcrumbList JSON-LD across all 3 detail page types — /firms/:slug (Home > Firms > {firm}), /designers/:slug (Home > Designers > {name}), /clients/:slug (Home > Clients > {name}). Packaged in @graph alongside entity schema. /firms also now emits employee→Person list for each associated designer.
  67. 7348d91 2026-05-12 starsofdesign: home page — WebSite + Organization JSON-LD with SearchAction (urlTemplate→/designers?q=…) for Google sitelinks search-box eligibility. Closes the asim/sod parity gap on home-page schema.
  68. 044e06c 2026-05-12 starsofdesign: JSON-LD Person schema on /clients/:slug (DW client tier) — name + jobTitle + homeLocation + worksFor→Organization. Mirrors firm/designer pattern. Preserves PII rule (city/state only, no street).
  69. 113ac14 2026-05-12 starsofdesign: JSON-LD schema — Organization on /firms/:slug (name/website/foundingDate/address/areaServed), Person on /designers/:slug (name/jobTitle/homeLocation/knowsAbout); injected via optional json_ld locals in head.ejs partial
  70. f17b258 2026-05-12 starsofdesign: GET /favicon.ico fallback — returns public/favicon.svg so bots/legacy clients don't 404 on conventional path
  71. 728ecab 2026-05-12 starsofdesign: GET /api/stats — JSON counts (designers/firms/claimed/links/likes/comments/candidates) with shared module-level pool; mirrors asim /api/spotted/stats pattern; consumed by status-loop + external dashboards
  72. 346478c 2026-05-12 starsofdesign: seed data/vendor-domains.txt (~80 lines) — vendor manufacturers + transactional SaaS + DW family + free-mail + testing domains; consumed by any future audit-promote pipeline
  73. e00c483 2026-05-12 promote-candidates: harden — restore 'status=new' filter; document why blanket is_designer promote is unsafe (LLM mis-labels Shopify/Twitter/vendor reps as designers)
  74. cca9c68 2026-05-12 starsofdesign: +40 firms (20 arch, 20 interior) — Selldorf, Steven Holl, COOKFOX, KieranTimberlake, Lake|Flato, Tod Williams Billie Tsien, Brad Ford ID, Kara Mann, Jonathan Adler, Carrier and Company, S.R. Gambrel etc.
  75. 10f3ae0 2026-05-12 starsofdesign: sort dropdown on /firms + /clients (per standing sort+density rule)
  76. f55b3a7 2026-05-12 feat(starsofdesign): sitemap.xml now includes /firms/:slug × 184 + /clients/:slug × 61 + /feed (403 URLs total)
  77. 23833cd 2026-05-12 feat(starsofdesign): add /firms /clients /feed to global nav
  78. adfe6ed 2026-05-12 feat(starsofdesign): /firms tier — listFirms+getFirm lib, /firms+/firms/:slug routes, firms.ejs + firm.ejs views
  79. ddd574f 2026-05-12 feat(starsofdesign): expand top-us-firms seed +39 (arch 39, interior 39); strip Schumacher per vendor rule
  80. 6f7c2dd 2026-05-12 feat(starsofdesign): bring Big Red back — widget tag + CSP whitelist for :9935
  81. da8a343 2026-05-12 feat(starsofdesign/deep-recon-v2): --windows={v1|v2|all} for historical sweeps
  82. 5927cb6 2026-05-12 feat(starsofdesign): /feed phone UI — vertical scroll, Like + Comment
  83. 1f43d2b 2026-05-12 feat(starsofdesign): regex pass extracts URLs from stored email signatures
  84. feeab56 2026-05-12 feat(starsofdesign): /clients route — surface real DW client designers in the directory
  85. fcd8cd8 2026-05-12 feat(starsofdesign): areas_served + PII guardrails + sig-extract pass 2
  86. 7dd41c8 2026-05-12 video gallery — 75 verified YouTube videos from AIA/ASID/AD/House Beautiful/Studio McGee + firm channels, cross-linked to 66 designer profiles via designer_slug, new /videos route with category+source+search filters, per-profile 'Watch X' rail, sitemap inclusion, fixed regex literal U+2028/9 EJS compile bug
  87. 553c74a 2026-05-12 security hardening: helmet (CSP/HSTS/X-Frame-Options/Referrer-Policy/Permissions-Policy/COOP), express-rate-limit (200/min/IP), JSON-LD <script> escape, javascript: URL filter, designerwallcoverings.com URL allowlist for preferred_dw, slug regex validation, q clamp to 120 chars, rel=noopener noreferrer everywhere, error message hardcoded
  88. a7bdb25 2026-05-12 feat(starsofdesign): DEEP George info@ recon (7-window × 8-template walk)
  89. f5688ed 2026-05-12 feat(starsofdesign): signature-block LLM extraction → Jo Hempelmann shipped
  90. 4ced542 2026-05-12 expand to 150 designers — added 62 active US firms with firm websites, IG, and source credits to AD/House Beautiful/Elle Decor/Veranda/Robb Report/Luxe/New England Home; designer pages now render 'Find the firm' + 'Sources & press' blocks
  91. 41d434d 2026-05-12 feat(starsofdesign): pull DW client contacts from George info@ Gmail
  92. 7022cb6 2026-05-12 expand directory: 26 → 88 designers (added historic foundational + mid-century + contemporary diverse voices, era_sort sorted)
  93. ecb46aa 2026-05-12 deploy.conf for kamatera deploy script (port 9928 health check)
  94. 4e7adb5 2026-05-12 fix(starsofdesign): vendors are NOT clients — apply blocklist, seed top-40 US firms
  95. 51fa79b 2026-05-12 gucci hero anatomy — logo UL + hamburger UR + huge wordmark/tagline hero with star-field ground (per standing rule)
  96. fed7183 2026-05-12 feat(starsofdesign/tick2): local-LLM triage on remaining candidates
  97. 1542658 2026-05-12 feat(starsofdesign/tick1): Gmail + Shopify recon → 236 candidates / 44 designers / 41 firms
  98. bde4448 2026-05-12 starsofdesign tick0: PG schema + AI placeholder + scaffold
  99. 70b7912 2026-05-10 add /sitemap.xml route + JSON-LD Person schema on designer detail
  100. aa14a02 2026-05-10 initial scaffold + GA gtag (G-YCT0F2Q692) + 27 designer profiles

Authors

Agents used

  • secrets-manager1

Skills used

  • /clients32
  • /firms20
  • /designers13
  • /feed8
  • /random8
  • /health7
  • /sitemap7
  • /favicon4
  • /healthz4
  • /version4
  • /comments4
  • /featured3
  • /like3
  • /comment3
  • /state3
  • /role3
  • /vendor-blocklist3
  • /designer2
  • /terms2
  • /website2
  • /stats2
  • /localhost2
  • /george-deep-recon2
  • /studio2
  • /city2
  • /head1
  • /something1
  • /grid-controls1
  • /mobile1
  • /firm1

Creative ideas + design notes

Commits with substantial prose (≥120 chars) — the rationale behind each move.

8fc1daa · 2026-05-19 · Static-bak guard returns plain text, not EJS template
The previous version of this guard tried to render public/404.ejs, which
in turn includes partials/head.ejs that depends on res.locals.meta_desc.
That local is wired inside the publicRoutes router-level middleware —
later in the stack than this guard runs — so a request for a .bak/.pre-
path threw "meta_desc is not defined" and surfaced a 500 instead of a
404, defeating the point.

Plain-text response sidesteps the template chain entirely. Verified with
a smoke run: /test.html.bak / /test.bak / /something.pre-refactor.html
all return 404, regular routes still 200.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
e2ee6aa · 2026-05-19 · Guard against serving .bak / .pre- snapshot artifacts from public/
Refactor passes across the DW fleet sometimes leave behind .bak /
.bak.<ts> / .pre-<label> snapshots next to the file they replace. If
one of those slips into public/ it ships to prod as a world-readable
static asset. Two-layer fix:

1. .gitignore — *.bak, *.bak.*, *.pre-* so new snapshots stop entering
   the tree to begin with.
2. server.js — small Express middleware in front of express.static()
   that 404s any URL path containing those patterns, so even an
   un-ignored slip can't be fetched. Uses 404 (not 403) so the
   existence of a snapshot can't be confirmed by path-probing.

No tracked .bak / .pre-* files exist in the repo today; this is a
forward-looking guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
a7ac2a9 · 2026-05-19 · Add noreferrer to all rel=nofollow noopener external links
External links across /clients, /clients/:slug, /firms, /firms/:slug,
and /feed previously used rel="nofollow noopener" — noreferrer was
missing, which leaks the originating profile URL in the Referer header
to third-party sites (LinkedIn, Instagram, firm websites) every time a
visitor clicks through. Spec-canonical pairing for target="_blank" is
"noopener noreferrer"; nofollow stays for SEO. All six view files
updated; videos.ejs and designer.ejs already had the full triple.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
18acbe8 · 2026-05-19 · Hydrate saved sort BEFORE first grid render
Per the standing rule: when a user has picked a sort in /designers or on
the home grid, that preference must apply on the very first render of a
fresh visit, not only after they touch the dropdown again. Server-side
sorting is driven by ?sort= in the URL, so this commit makes
public/js/grid-controls.js do a one-time replace() redirect to the saved
sort value when the URL has no sort param yet.

Guards against:
- cross-page contamination — only redirects if the saved value is one of
  the options actually present in this page's <select> (so a "default"
  saved from /clients won't crash /designers).
- redirect loops — gated by a sessionStorage flag.
- form-driven pages — /clients and /firms wrap their <select> in a <form
  onchange="this.form.submit()">; the JS now defers to the form submit
  instead of double-navigating.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
f86aef5 · 2026-05-13 · polish: editorial 2-line hero + by-era anthology section
Hero — replace single-line tagline with 2-line italic editorial
headline ("The designers who set the room. / The wallcoverings that
set the tone.") plus 1-line sans subtitle naming Draper through
Wearstler. Bumps tagline color to #f0e3b8 + text-shadow for stronger
APCA on the dark hero ground.

New "By the era" section between the directory grid and the editorial
two-col. Four cards (Early 20th / Mid-Century / Late 20th /
Contemporary) each with a gold hairline rule, era blurb, 3 exemplar
designer names, and a count-deep-link to /designers?era=<bucket>.
Specific to the directory vertical — gives the homepage a real
"anthology by era" identity instead of template-fresh blocks.

Server passes eraIndex into the home view as a single in-memory
pass over data.load() — no DB hit. Hero-readability-auditor passes
APCA Lc 100/99 desktop/mobile, well above the 75 threshold.
6f7c2dd · 2026-05-12 · feat(starsofdesign): bring Big Red back — widget tag + CSP whitelist for :9935
Big Red avatar launcher was missing from StarsOfDesign + AsSeenInMovies
even though the big-red server at :9935 was up and healthy. Earlier
embed work only touched corkwallcovering + factory-floor.

views/partials/footer.ejs — adds the <script src=:9935/widget.js> tag
in retail mode before </body>. Widget self-mounts to body as a fixed
lower-right launcher; goes green when chat is live.

server.js helmet CSP — whitelists http://localhost:9935 + ws://localhost:9935
across script-src, img-src (avatar), connect-src (fetch + WebSocket),
and frame-src (chat iframe). Drops these entries when the named tunnel
bigred.agentabrams.com goes live.
da8a343 · 2026-05-12 · feat(starsofdesign/deep-recon-v2): --windows={v1|v2|all} for historical sweeps
Adds the 5y-7y and 7y-10y date windows to scrapers/george-deep-recon.js,
gated by --windows=v2 (only the deep sweep) or --windows=all. Default
remains the v1 windows (last-30d through 3y-5y) so prior automation
behavior is unchanged.

Run results (--windows=v2 --max-per-q=50):
  messages_scanned       350
  clients_new_or_bumped  213
  vendors_caught          71
  no_email                66

Net new historical DW clients (2018-2021 traffic to info@) bumped
sod_email_candidates 793 → 884.
5927cb6 · 2026-05-12 · feat(starsofdesign): /feed phone UI — vertical scroll, Like + Comment
Facebook / X.com-style mobile-first feed for the /clients tier. One
designer per card stacked vertically, anonymous like (cookie-tracked),
anonymous comment thread (name + body required, optional email).

Schema (idempotent CREATE TABLE):
  sod_likes      (designer_id, anon_id, created_at)
                 UNIQUE (designer_id, anon_id) — one like per visitor per post
  sod_comments   (designer_id, parent_id, author_name, author_email, body,
                  is_hidden, anon_id, ip_hash, created_at)
                 partial index on (designer_id, created_at DESC) WHERE NOT hidden

lib/feed.js
  - ensureAnonId(req,res) — sets long-lived sod_anon cookie (12 hex,
    httpOnly=false so client JS can show liked-state immediately)
  - feedPage({anonId, limit, offset}) — pulls designers with embedded
    like_count, comment_count, i_liked (boolean per caller), grouped
    links. Ordered by created_at DESC (newest first).
  - commentsFor(designerId, limit) — top-level comments only for v1
  - toggleLike — flips like state, returns new count + liked boolean
  - postComment — name+body validation, 1500-char body cap, 5/hour
    burst limit per anon, simple spam regex
                 (viagra/crypto/loan/casino/etc.), max 2 URLs allowed

Routes (in routes/public.js):
  GET  /feed                              — page=N (15 per page)
  GET  /api/clients/:id/comments          — JSON list, top-level comments
  POST /api/clients/:id/like              — toggle, JSON {liked, like_count}
  POST /api/clients/:id/comment           — JSON {author_name, author_email?, body}

views/public/feed.ejs — max-width 520px column. Each card:
  - Avatar circle (made-with-ai fallback for unclaimed)
  - Name (links to /clients/:slug) + firm/role · city,state
  - Areas-served chips
  - Site/LinkedIn/Instagram link row
  - Like / Comment / Share action row
  - Collapsed comments section that lazy-loads on toggle
  - Inline comment form (name+email+body, optimistic insert on success)
  - At <520px width, posts go edge-to-edge (FB-style mobile)

server.js — added cookie-parser middleware.

Smoke-tested:
  GET  /feed                                → 200, 15 posts rendered
  GET  /api/clients/45/comments             → 200, JSON list
  POST /api/clients/45/like                 → 200 {liked:true, like_count:1}
  POST /api/clients/45/comment              → 200, comment landed

v1 scope cuts (defer to next ticks if Steve wants):
  - Nested replies (parent_id is in schema but not used yet)
  - Infinite scroll (page=N + Older/Newer pagers for now)
  - User accounts (anon cookie is enough)
  - Notifications + email-on-comment
  - Designer claim flow to enable Verified badge on their post
1f43d2b · 2026-05-12 · feat(starsofdesign): regex pass extracts URLs from stored email signatures
scrapers/extract-links-from-signatures.js — cheap free pass over every
sod_email_candidates.last_signature blob. Regex pulls all http(s) URLs,
classifies as:
  linkedin   linkedin.com/in/* OR linkedin.com/company/*
  instagram  instagram.com/*
  houzz      houzz.com/*
  pinterest  pinterest.com/*
  facebook   facebook.com/*
  twitter    twitter.com/* or x.com/*
  firm-site  any URL whose host matches the candidate's email domain
             (so jane@studio.com signature with https://studio.com/about
              gets recognized as firm-site)
Otherwise dropped (calendar/mailer/asset hosts are noise without curation).

Idempotent via ON CONFLICT DO NOTHING on sod_links. Designer-attached
when sod_designers.primary_email matches the candidate, otherwise
inserted as orphan (designer_id=NULL, firm_id=NULL) waiting to be
attached when the designer's profile gets materialized in a later
sig-extract pass.

First run results:
  scanned 158 signatures
  inserted 24 new links
  by-kind: firm-site=11  twitter=5  instagram=4  facebook=2  houzz=1  pinterest=1
  dupes=3

Why this matters — the LLM in extract-from-signatures captures 1-2 URLs
per signature at most (usually just the primary website or the linkedin),
but real designer signatures often include 4-6 URLs across Instagram /
Houzz / personal site / portfolio host / etc. This deterministic pass
sweeps up the rest for free.

Top-40 firm LinkedIns from seed-top-firms.js confirmed still in place
(40 LinkedIn URLs at firm_id level for Gensler/HOK/Kelly Wearstler/etc.).
Designer-level LinkedIn coverage is the next gap — needs Google CSE
keys to resolve via 'site:linkedin.com/in NAME FIRM' search. Steve does
not have GOOGLE_CSE_KEY / GOOGLE_CSE_ID registered in secrets-manager
yet; tick blocked on Steve providing those.

Background drains running:
  PID 59491 — asseeninmovies wikidata bg4 (15k movies, popularity-sorted)
  PID 59492 — starsofdesign sig-extract pass 4 (limit=200)
feeab56 · 2026-05-12 · feat(starsofdesign): /clients route — surface real DW client designers in the directory
New tier in the directory:
  /designers          editorial 150-name JSON marquee (Kelly Wearstler etc.) — unchanged
  /clients            NEW — materialized real DW client designers from
                      sig-extract + Gmail recon
  /clients/:slug      NEW — individual client profile page

Files:
  lib/clients.js         PG query layer. listClients({q,area}) does the
                         sod_designers ↔ sod_designer_firm ↔ sod_firms ↔
                         sod_links join, groups links by kind (linkedin /
                         firm-site / instagram), rolls up city/state/areas
                         preferring firm's over designer's. getClient(slug)
                         fetches one + its links.
  routes/public.js       Adds GET /clients (list, search by q + area filter)
                         and GET /clients/:slug (detail). 80-char slug regex
                         guard. PG queries via pg Pool.
  views/public/clients.ejs    List page. Same card pattern as /designers but
                              with area chips + LinkedIn/Website/Instagram
                              link row. Hero says 'The designers and firms
                              who actually buy from Designer Wallcoverings.'
  views/public/dwclient.ejs   Detail page. Headshot (made-with-ai fallback)
                              + name + VERIFIED chip (when claimed) + role +
                              city/state + areas-served tags + Links list +
                              Claim CTA + Firm section.

Gotcha codified to memory (feedback_express_ejs_client_local_reserved.md):
  Passing { client: c } to res.render silently shadows EJS's include()
  helper via with(locals). Symptom: 'include is not a function' at line 1
  of any <%- include('...') %>. Took 6 restarts + minimal-template
  bisection to find. Workaround: use any local name other than 'client'.
  Renamed to 'profile' here.

Sig-extract pass 3 (--limit=150, ran in background during this UI work):
  ok=74  withName=11  withFirm=14  skipped=76
  Real sod_designers: 24 → 31 (+7)
  New surfaces include: Putri Sarnadi @ Studio Left, Jesus Raya @ Tokiwa
  Tsusho (Long Beach CA, with 2 areas served — the new field working).

Smoke-tested live:
  GET /clients               → 200, 21 designer cards
  GET /clients/jo-hempelmann → 200, Katie's Wallpaper Installation, LA
  GET /clients/jerry-sanflippo → 200, Fos Interiors, Palm Springs
fcd8cd8 · 2026-05-12 · feat(starsofdesign): areas_served + PII guardrails + sig-extract pass 2
Per Steve's correction 2026-05-12: 'city and state ok, no zip, no
street address, and AREAS THEY SERVE.'

Schema change (idempotent ALTER):
  ALTER TABLE sod_designers ADD COLUMN areas_served TEXT[];
  ALTER TABLE sod_firms     ADD COLUMN areas_served TEXT[];
  CREATE INDEX (gin) on each.

LLM extractor prompt rewrite (extract-from-signatures.js SYSTEM):
  - Asks for {full_name, firm_name, firm_city, firm_state, firm_country,
    firm_role, areas_served[], linkedin_personal, linkedin_company,
    website, instagram}.
  - Explicit rule: NEVER extract street address, NEVER extract zip,
    NEVER extract phone. Phone is opt-in via claim flow, not scraping.
  - For areas_served: look for 'Serving:', 'Areas:', 'We cover', or
    parenthetical region phrases. Otherwise safely infer from city+state.
  - If a signature contains '123 Main Street, Suite 4B, Brooklyn, NY 11201'
    we keep 'Brooklyn'/'NY' only — never the street or zip.

Server-side PII cleanup of LLM output (in case the model echoes one
through anyway):
  - firmCity scrubbed of any 'N <street> Street/Ave/etc.' patterns
  - firmCity scrubbed of zip patterns (\d{5} and \d{5}-\d{4})
  - areas_served filtered: no leading digits, no zip-shaped entries,
    no <2-char or >60-char entries, dedupe + cap at 12.

Hallucination hardening (carryover from Steve's earlier note about
'Designer Wallcoverings' false firm):
  - Reject bare first-name without local-part match: gemma3:12b
    repeatedly returns 'Samantha' / 'Steve' / 'Sarah' when it can't find
    a real name. New guard requires the first-name's first 4 chars to
    appear in the email local-part.
  - Reject firms that are just a website ('domaineinteriors.com').

Two-phase candidate select for the pass:
  Priority 1 — empty display_name (need name to materialize designer)
  Priority 2 — has name but no sod_designers row yet (firm/role enrichment)

Persisted in this pass:
  city / state_or_region / country / areas_served on both sod_firms and
  sod_designers. Instagram URL gets its own kind='instagram' row in
  sod_links (in addition to existing linkedin / firm-site kinds).

Sig-extract pass 2 results (limit=120):
  ok=51  withName=9  withFirm=13  skipped=69
  9 new real designers materialized after hardening cleanup:
    Lynsey Humphrey  · Alder & Tweed
    Lara Heffernan   · Berg Development     · Brookfield
    Ashley Kruchoski · Katiebassett         · Minneapolis
    Jerry Sanflippo  · Fos Interiors        · Palm Springs
    Amy L Young      · Eric Hedlund Design  · Coeur d'Alene
    (+ 4 noise rows wiped pre-hardening)

Memory codified: feedback_designer_directory_pii_policy.md.

Ledger now:
  sod_designers (real)   24   (was 15)
  sod_firms             104   (was 92)
  sod_links              86   (was 83)
  candidates_real       562
  candidates_with_name  427

Background jobs:
  PID 29524 asseeninmovies wikidata bg3 (10k more, 7873/290945 done)
a7bdb25 · 2026-05-12 · feat(starsofdesign): DEEP George info@ recon (7-window × 8-template walk)
scrapers/george-deep-recon.js — date-sliced sweep across info@
designerwallcoverings.com. Seven date windows (last-30d, 30d-90d,
90d-180d, 180d-1y, 1y-2y, 2y-3y, 3y-5y) × eight query templates:

  info-replied-to     from:info@dw  — outgoing reply, designer in To:
  info-replied-steve  from:steve@dw — same pattern for Steve's DW account
  sample              subject:sample|memo|swatch|free-sample
  trade               subject:trade|wholesale|trade-account|program
  quote               subject:quote|pricing|custom|net-price
  project             subject:project|residence|hospitality|hotel|...
  role-keyword        subject:designer|interior|residential|specifier|...
  mailer-reply        subject:Re:|RE: in:inbox  (designer replied to a
                                                 DW campaign)

For outgoing-info@ messages (info-replied-to / -steve), pulls the full
message via /api/messages/:id and reads the To: header — that's where
the actual designer lives. For incoming, parses From:.

Every email checked against scrapers/vendor-blocklist.json before
landing as is_designer.

Result of this run (max-per-q=50):
  messages_scanned         2306
  clients_new_or_bumped    1459
  vendors_caught            554
  no_email                  293

Blocklist additions for vendors that slipped this sweep (28 more
caught + flipped): bmwallpaper, miltonandking, wolfgordon, wolf-gordon,
mindtheg, cowtan, surfaceprint, em.spoonflower, spoonflower, fypon,
quintushome, brewp, lelievreparis, shawcontract, patcraft, interface,
tarkett, milliken, mannington, armstrong, carnegiefabrics,
robertallendesign, lee-jofa, thibautcontract, stoutindustries,
pkaufmann, rmcoco, barrowfabrics, duralee, swavelle, knolltextiles,
carnegiexorel, wallpaperworld, wallpaperboulevard, wallpaperdirect,
rebelwalls, graham-brown, anewall, wallsneedlove.

Also widened extract-from-signatures.js to fall back through accounts:
info → steve-office. Many candidates only have signal in steve@dw
mailbox not info@.

Final ledger:
  candidates_total          793  (was 343, +450)
  real_designers            585  (was 200, +385 net)
  real_designers_with_name  412  (was 77, +335 — direct From: parsing
                                  caught most of them)
  real_designers_empty_name 173  (still need sig-extract or manual)
  vendors_quarantined       150  (was 86, +64 — blocklist working)
  noise                      58

Top new domains: gmail.com (75, mostly real interior designers using
personal gmail addresses), yahoo.com (9), hotmail.com (6), aol.com (6),
lbiboyd.com, apbuildersusa.com, dugganla.com, rudindonner.com, etc.

Sig-extract --limit=100 running in background (PID 94284) on the 173
empty-name pool. Wikidata bulk drain 10k batch 2 also kicked for
asseeninmovies (PID 96412).
f5688ed · 2026-05-12 · feat(starsofdesign): signature-block LLM extraction → Jo Hempelmann shipped
scrapers/extract-from-signatures.js — for each candidate with empty
display_name (148 such), pull most-recent George info@ message body,
slice to signature region (after -- delimiter or last 14 non-empty
lines), gemma3:12b in JSON-mode extracts:
  full_name, firm_name, firm_city, firm_role,
  linkedin_personal, linkedin_company, website, phone.

Then materializes:
  - UPDATE sod_email_candidates.display_name + last_signature
  - INSERT sod_designers + sod_firms with role/city
  - INSERT sod_designer_firm join with extracted title
  - INSERT linkedin/website URLs into sod_links (kind='sig-extract')

Two important guardrails:
  - Length sanity check on name (3-80 chars) and firm (3-120 chars).
  - Hard refusal of 'Designer Wallcoverings*' as firm_name — that's
    DW's OWN outgoing signature embedded in the contact's reply, NOT
    their firm. LLM grabbed it from rpaschall@pinecrestinsurance.com,
    kcfox007@gmail.com, angela@handwerk.company first run; guard
    added, hallucinated 'Designer Wallcoverings and Fabrics' firm row
    deleted.

First-pass result on 25 highest-signal empty-name candidates:
  jo@katieswallpaper.ca → Jo Hempelmann, Katie's Wallpaper Installation
    Inc., Los Angeles, Wallpaper Installation Project Manager
  export@bmwallpaper.com → Jenny Liu (name only — no firm in sig)
  jcamacho427@gmail.com → Jerry Camacho (later flagged is_noise —
    Steve's accountant, not a designer)
  rpaschall / kcfox007 / angela@handwerk → hallucinated DW (guarded
    after run)
  17 of 25 had no message in info@ (personal-Gmail-only contacts)

Manual cleanup tied to this tick (27 candidates flipped is_noise):
  Family: nataliaabrams (Steve's wife), bobfox729, billfox92117,
  jiabramsla, dylan.r.fox, kcfox007 (all relatives).
  Service providers / vendors: hrftax53 (accountant), jcamacho427
  (accountant), tom@osbornepartners (financial advisor), michael@
  montereyrentals (landlord), mike@5to1web (IT vendor), rpaschall@
  pinecrestinsurance (insurance), angela@handwerk, email@rentmanager,
  circulation1@labj*, jimdultz@comcast (vendor),
  carole.fabrics@carolefabrics, helen.brier.b6@mail.toray (Toray),
  kundservice@photowall (Photowall), noreply@zopim (transactional),
  natalia/sabrina@studentdebtcrisis (wife's nonprofit), chounanian
  (ex-DW VP), browntwn / mitz / greg_perry (personal).

Ledger after this tick:
  real_designer_candidates  200  (was 225 — 25 dropped to noise)
  noise_total               58   (was 31)
  vendors_total             84
  sod_designers (real)       4   (was 1 — Jo Hempelmann added)
  sod_designers (reps)      40
  sod_firms_total           82   (+1 Katie's Wallpaper, hallucinated DW removed)
41d434d · 2026-05-12 · feat(starsofdesign): pull DW client contacts from George info@ Gmail
scrapers/george-info-recon.js — walks 6 client-flavored Gmail searches
on info@designerwallcoverings.com via the now-portable George agent
(commit a4d0448 fixed the Mac/Kamatera path issue). Each search target
is a different DW-client signal:

  shopify-order      from Shopify order-notification mailers; extracts
                     customer billing email FROM THE BODY (not the
                     From: shopifyemail.com header). signal=0.97
  trade-signup       subject:trade|wholesale.                signal=0.93
  sample-request     subject:sample|memo|swatch.            signal=0.85
  quote-request      subject:quote|custom|pricing.          signal=0.82
  project-inquiry    subject:project|spec|residence|hosp.   signal=0.78
  designer-subject   subject:interior|residential|hosp.     signal=0.78

Every email is checked against scrapers/vendor-blocklist.json before
landing as is_designer — vendors get logged at status='is_vendor' (audit
trail) with score=0 so they can't be promoted later.

Tick results — 480 messages scanned across 6 queries:
  clients (new is_designer)   212
  vendors filtered             36
  self (info@dw)               151
  no extractable email         81

Plus 3 new vendor domains caught + added to blocklist + flipped:
  koroseal.com, innovationsusa.com, kneedlerfauchere.com,
  oalusa.com, archiproducts.com, elitis.us, ccsend.com,
  billtrust.com, battagliaincmarble.ccsend.com → flipped 16 candidates
  to is_vendor.

Final ledger after George recon + cleanup:
  candidates_total          343  (was 236, +107 net new clients)
  real_designer_candidates  225
  vendors_filtered           86
  noise                      31
  sod_firms                  81  (top-40 seed + 40 reps + 1)

Real DW-client surfaces newly captured (top by domain):
  dugganla.com         (Duggan Interiors LA)
  apbuildersusa.com    (AP Builders)
  rudindonner.com      (Rudin & Donner NYC)
  dkmdesignstudio.com  (DKM Design Studio)
  egg-and-dart.com     (San Diego interiors)
  davidsonlondon.com   (Davidson London)
  + 64 more.

Next tick: signature-block parser to extract firm/role/city from each
candidate's most-recent message body, then promote to sod_designers.
4e7adb5 · 2026-05-12 · fix(starsofdesign): vendors are NOT clients — apply blocklist, seed top-40 US firms
Steve correction 2026-05-12:
  paulmontgomery, desima, newwall, beverlyhillswallpaper are VENDORS
  (manufacturers DW buys from), NOT designer clients. And
  beverlyhillswallpaper.com is dead — never use.

Three changes:

scrapers/vendor-blocklist.json — codified the rule. Every domain with a
~/.claude/skills/*-scraper-manager/ skill is by definition a vendor we
buy from. Plus the wider blocklist from Steve's prior corrections
(Kravet, Schumacher, Scalamandre, Fabricut, Materialbank, Donghia,
Stroheim, Maharam, Pollack, Phillip Jeffries, Cole&Son, Romo, Osborne&
Little, Thibaut, Pierre Frey, Mind the Gap, etc.).

scrapers/apply-vendor-blocklist.js — bulk-flips existing candidates that
got auto-promoted to status='is_designer' in tick 1+2. Removes any
sod_designers/sod_firms rows whose primary_email is on the blocklist.
Idempotent.

  Result: 43 candidates → is_vendor, 1 → is_noise (beverlyhillswallpaper),
  3 wrongly-created sod_designers rows deleted.

data/top-us-firms.json + scrapers/seed-top-firms.js — deterministic
seed of 40 top US architecture (Gensler / HOK / Perkins&Will / SOM /
KPF / NBBJ / HKS / Cannon / HDR / Stantec / Corgan / Page / Gresham
Smith / Cooper Carry / Leo A Daly / Pickard Chilton / DS+R / Snohetta /
CallisonRTKL / AECOM) + 20 top US interior design (Kelly Wearstler /
Studio Sofield / Nate Berkus / Studio McGee / Shamshiri / Magnolia /
Champalimaud / Yabu Pushelberg / Roman&Williams / Rockwell / Bunny
Williams / Victoria Hagan / Markham Roberts / Steven Gambrel / Robert
Stilin / Ken Fulk / Suzanne Kasler / Bilhuber / Miles Redd / Sheila
Bridges) firms. Inserts into sod_firms with public LinkedIn company
slugs + firm-website URLs in sod_links. Logos stay made-with-ai
placeholders until each firm claims its profile.

Ledger after correction:
  sod_firms_total            81  (+40 from seed)
  sod_firms_50plus           18
  sod_links_linkedin         40
  sod_links_firmsite         40
  candidates_is_vendor       48  (was 5)
  candidates_is_designer    156  (was 200 — vendors flipped out)
  candidates_total          236

Memory codified: feedback_starsofdesign_clients_not_vendors.md.

BLOCKER surfaced for Steve:
  George Gmail agent at 127.0.0.1:9850 has hasCredentials=false.
  Need OAuth consent flow run so George can read DW info@ inbox.
  That inbox (Shopify order notifications, trade signups, sample
  requests) is where DW's actual designer CLIENTS live — much higher
  signal than Steve's personal Gmail (which is heavy on vendor sales
  reps emailing him for buy-side relationships).
fed7183 · 2026-05-12 · feat(starsofdesign/tick2): local-LLM triage on remaining candidates
scrapers/triage-candidates.js — drives Ollama (gemma3:12b by default)
in JSON-mode to label each sod_email_candidates row as one of:
is_designer / is_firm / is_vendor / is_press / is_noise. Writes both
the status enum and a human-readable triage note (label, confidence,
short reason) into notes via CONCAT_WS so the original signal-score
note is preserved.

Why gemma3:12b not qwen3:14b — first pass at qwen3:14b returned empty
content for all 39 rows because its <think> mode swallows the entire
num_predict=200 budget before producing JSON. Switched default to
gemma3:12b which is ~0.5s/row and JSON-stable.

Deterministic short-circuit for masked customer rows — the 25 digital_
sample_orders rows wear synthetic customer{1..25}@example.com addresses
(privacy-mask in dw_unified.digital_sample_orders); those skip the LLM
entirely and resolve straight to is_noise.

Tick 2 results on 39 still-new candidates:
  is_designer  2  (paulmontgomery.com, beverlyhillswallpaper.com)
  is_firm      2  (desima.com, newwall.com)
  is_vendor    5  (priti, printmurals, fahertybrand, toplinegraphics, profectionpainting)
  is_press     0
  is_noise    30  (25 masked-customer + studentdebtcrisis/filmla/trueaccord/etc.)
  errors       0

Status snapshot post-tick:
  candidates_total      236   (was 236)
  status_new              0   (was 39 — fully drained)
  status_is_designer    200
  status_is_firm          1   (4 firm-labeled rows kept their LLM label after
                                promote-candidates.js attempted name extraction)
  status_is_vendor        5
  status_is_noise        30

Known gap (for tick 3): promote-candidates.js can't materialize sod_designers
/sod_firms rows when display_name is empty — guessNameAndFirm() returns
{person:null, firm:null} and produces designer=null firm=null. Tick 3's
signature-block parser fixes this by extracting names from each
candidate's last-seen email signature. Tick 2 deliberately doesn't try to
backfill name-from-domain because that's tick 3's job and the cleaner
fix.
1542658 · 2026-05-12 · feat(starsofdesign/tick1): Gmail + Shopify recon → 236 candidates / 44 designers / 41 firms
Two-source ingest into sod_email_candidates per Steve's spec.

Gmail side (scrapers/gmail-recon-ingest.js):
- Sub-agent walked steveabramsdesigns@gmail.com via claude.ai Gmail MCP,
  scoping queries to 'from:me newer_than:5y' + 8 design-flavored variants
  going back to 2017. 279 reply threads scanned, 203 unique non-Steve
  participants extracted to /tmp/sod-candidates.ndjson.
- Drop-list filters noreply/notifications/SaaS-vendor noise (~25 domains).
- Receiver script computes signal_score = reply_ratio*0.85 + domain_bonus
  (design/studio/interiors/architect = +0.20).
- 190/203 land at signal >= 0.8.

Shopify side (scrapers/shopify-recon.js) — added per Steve 'also use shoppy database':
- dw_unified.trade_signups (7 rows): verified DW trade-program signups.
  Land at signal=0.95, status='is_designer' (auto-promoted, skip triage).
- dw_unified.digital_sample_orders (25 unique): paid sample customers.
  Land at signal=0.70, status='new' for later triage.
- designer_representatives.designer_reps (40 rows): manufacturer rep
  agencies. Direct-insert into sod_designers + sod_firms (skip candidate
  stage entirely — they're already curated) with role='Manufacturer Rep'
  and a 'made-with-ai' headshot.

Auto-promotion (existing scrapers/promote-candidates.js, --min=0.85):
- 197/236 candidates flipped to status='is_designer' or 'is_firm'.
- Most Gmail rows have empty display_name (MCP returns bare emails for
  many threads); follow-up tick will do get_thread name extraction.

Idempotent: all INSERTs use ON CONFLICT with GREATEST(signal_score) and
LEAST/GREATEST first_seen/last_seen so re-runs only enrich.

Ledger after this tick:
  sod_email_candidates    236  (197 promoted, 39 awaiting triage)
  sod_designers            44
  sod_firms                41
  sod_designer_firm        41
  sod_links                 0  (designer_reps have no URLs/emails on file)
70b7912 · 2026-05-10 · add /sitemap.xml route + JSON-LD Person schema on designer detail
- robots.txt already advertised /sitemap.xml; route was missing (404).
  Now serves all 4 static pages + every designer slug with lastmod/priority.
- Detail pages now emit schema.org Person JSON-LD (name, bio, styles via
  knowsAbout, city via workLocation, DW pairings as subjectOf) so SERP
  cards get rich-result eligibility.

File tree

60 files tracked. Click any to browse the source at HEAD.

Other build journals

← AsSeenInMovies  ·  all 4 projects  ·  build-pages →

Export

commits.csv · feed.atom · project.json · commits.json

rendered in 1ms