← back to Prestige Car Wash
Contrarian FIX-FIRST pass: real hero copy (no terminal cmd), location/hours block from Places, honest contact copy, contact rate-limit + field caps + optional webhook notify, prod default-password warning; gitignore runtime leads
36db139ec7c5befdb25e44b0e521227dfbbbffcc · 2026-07-04 13:46:04 -0700 · Steve
Files touched
M .env.exampleM .gitignoreM public/contact.htmlM public/index.htmlD reports/leads.jsonlM server.js
Diff
commit 36db139ec7c5befdb25e44b0e521227dfbbbffcc
Author: Steve <steve@designerwallcoverings.com>
Date: Sat Jul 4 13:46:04 2026 -0700
Contrarian FIX-FIRST pass: real hero copy (no terminal cmd), location/hours block from Places, honest contact copy, contact rate-limit + field caps + optional webhook notify, prod default-password warning; gitignore runtime leads
---
.env.example | 3 +++
.gitignore | 3 +++
public/contact.html | 2 +-
public/index.html | 33 ++++++++++++++++++++++++++++++++-
reports/leads.jsonl | 1 -
server.js | 40 ++++++++++++++++++++++++++++++++++------
6 files changed, 73 insertions(+), 9 deletions(-)
diff --git a/.env.example b/.env.example
index ceebbe2..ebb5790 100644
--- a/.env.example
+++ b/.env.example
@@ -18,3 +18,6 @@ PCW_PLACE_ID= # Prestige Car Wash Google Place ID (once claimed)
# Instagram (public pull now; owner token later)
IG_HANDLE=prestigecwash
+
+# Optional lead notification (Slack/Discord/Zapier webhook URL)
+NOTIFY_WEBHOOK=
diff --git a/.gitignore b/.gitignore
index 46722de..6069e71 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,6 @@ media/*.mp4
media/*.png
media/*.jpg
media/.cache/
+
+# runtime lead capture (PII) — never commit
+reports/leads.jsonl
diff --git a/public/contact.html b/public/contact.html
index 7514473..777abc1 100644
--- a/public/contact.html
+++ b/public/contact.html
@@ -30,7 +30,7 @@
<section class="section"><div class="wrap">
<h2>Book your wash</h2>
- <p class="sub">Tell us what you need — we'll text back to confirm a time. No upsell pressure, ever.</p>
+ <p class="sub">Tell us what you need — we'll follow up to confirm a time, usually within a few hours. No upsell pressure, ever.</p>
<div class="ok" id="ok"></div>
<form id="f">
<div class="two">
diff --git a/public/index.html b/public/index.html
index 824342c..aa87c08 100644
--- a/public/index.html
+++ b/public/index.html
@@ -47,7 +47,7 @@
<p class="sub">Wash & wax, up close.</p>
<div class="card" style="max-width:820px;margin:0 auto">
<div class="media" id="heroClip" style="aspect-ratio:16/9">
- <div class="ph">🎬 Wash/wax clip renders here once media is generated<br>(Nano Banana still → SeeDance video · run <code>npm run gen:media</code>)</div>
+ <div class="ph"><div style="font-size:22px;font-weight:800;color:var(--ink)">✨ The two-bucket hand wash</div><div style="opacity:.75;margin-top:6px">Foam cannon · spot-free rinse · ceramic seal</div></div>
</div>
</div>
</div></section>
@@ -60,6 +60,23 @@
<div class="grid" id="feat" style="--cols:4"></div>
</div></section>
+<section class="section"><div class="wrap">
+ <h2>Visit us</h2>
+ <p class="sub">Serving the whole San Fernando Valley.</p>
+ <div class="grid" style="--cols:2">
+ <div class="card"><div class="body">
+ <div class="ttl" id="locName">Prestige Car Wash</div>
+ <div class="blurb" id="locAddr">San Fernando Valley, CA</div>
+ <div id="locHours" style="margin-top:8px"></div>
+ </div></div>
+ <div class="card"><div class="body">
+ <div class="ttl">Neighborhoods we serve</div>
+ <div class="blurb">Sherman Oaks · Encino · Van Nuys · Studio City · Northridge · Reseda · Tarzana · Woodland Hills · North Hollywood</div>
+ <a class="btn" href="/contact" style="margin-top:8px">Book a wash</a>
+ </div></div>
+ </div>
+</div></section>
+
<footer class="foot"><div class="wrap">
© 2026 Prestige Car Wash · San Fernando Valley, CA · <a href="/services">Services</a> · <a href="/contact">Book</a>
<div style="margin-top:6px;opacity:.7">Instagram @prestigecwash</div>
@@ -84,6 +101,20 @@ function clip(s){
return `<video src="/${s.media_clip}" muted loop autoplay playsinline
onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'ph',innerHTML:'🚿 ${s.name}'}))"></video>`;
}
+// Hero: swap in the wash/wax clip ONLY if it exists; otherwise the branded copy stays.
+(function(){
+ var v=document.createElement('video');
+ v.src='/media/full-service.mp4'; v.muted=v.loop=v.autoplay=v.playsInline=true;
+ v.style.cssText='width:100%;height:100%;object-fit:cover';
+ v.addEventListener('canplay',function(){ v.play&&v.play(); document.getElementById('heroClip').replaceChildren(v); });
+})();
+// Location block from Google Places (mock until the API key is added).
+fetch('/api/places').then(r=>r.json()).then(p=>{
+ if(p.name) document.getElementById('locName').textContent=p.name;
+ document.getElementById('locAddr').textContent=(p.address||'San Fernando Valley, CA')+(p.phone?' · '+p.phone:'');
+ document.getElementById('locHours').innerHTML=(p.hours||[]).map(h=>
+ `<div style="padding:5px 0;border-top:1px solid var(--line);font-size:13px;color:var(--mut)">${h}</div>`).join('');
+});
</script>
</body>
</html>
diff --git a/reports/leads.jsonl b/reports/leads.jsonl
deleted file mode 100644
index 2f83476..0000000
--- a/reports/leads.jsonl
+++ /dev/null
@@ -1 +0,0 @@
-{"name":"Test Driver","phone":"818-555-0100","email":"","vehicle":"","service":"Premium Hand Wash","preferred":"","message":"fragrance-free please","created_at":"2026-07-04T20:32:16.169Z","source":"web-form"}
diff --git a/server.js b/server.js
index 92c8e2b..8d4474c 100644
--- a/server.js
+++ b/server.js
@@ -167,13 +167,29 @@ app.post('/api/admin/place/draft-update', (req, res) => {
});
});
-// Contact / booking lead — appended locally (no outbound send; email fan-out is gated).
-app.post('/api/contact', (req, res) => {
+// Simple per-IP rate limiter for the public form (prevents spam / unbounded lead file).
+const _hits = new Map();
+function rateLimit(max, windowMs) {
+ return (req, res, next) => {
+ const ip = req.ip || req.connection.remoteAddress || 'unknown';
+ const now = Date.now();
+ const arr = (_hits.get(ip) || []).filter(t => now - t < windowMs);
+ if (arr.length >= max) return res.status(429).json({ ok: false, error: 'Too many requests — try again shortly.' });
+ arr.push(now); _hits.set(ip, arr);
+ next();
+ };
+}
+const cap = (s, n) => String(s || '').slice(0, n);
+
+// Contact / booking lead — saved locally + optional webhook notify (env-gated).
+// NOTIFY_WEBHOOK (Slack/Discord/Zapier URL) gives leads a real notification path;
+// without it the lead is captured in the admin Leads tab. No mass email (that stays gated).
+app.post('/api/contact', rateLimit(5, 10 * 60 * 1000), (req, res) => {
const b = req.body || {};
if (!b.name || !(b.phone || b.email)) return res.status(400).json({ ok: false, error: 'name + phone/email required' });
const lead = {
- name: b.name, phone: b.phone || '', email: b.email || '', vehicle: b.vehicle || '',
- service: b.service || '', preferred: b.preferred || '', message: b.message || '',
+ name: cap(b.name, 120), phone: cap(b.phone, 40), email: cap(b.email, 160), vehicle: cap(b.vehicle, 120),
+ service: cap(b.service, 120), preferred: cap(b.preferred, 120), message: cap(b.message, 1000),
created_at: new Date().toISOString(), source: 'web-form'
};
try {
@@ -181,7 +197,14 @@ app.post('/api/contact', (req, res) => {
fs.mkdirSync(dir, { recursive: true });
fs.appendFileSync(path.join(dir, 'leads.jsonl'), JSON.stringify(lead) + '\n');
} catch (e) { return res.status(500).json({ ok: false, error: 'could not save lead' }); }
- res.json({ ok: true, message: "Thanks! We'll reach out to confirm your booking." });
+ // Fire-and-forget notification if a webhook is configured.
+ if (process.env.NOTIFY_WEBHOOK) {
+ fetch(process.env.NOTIFY_WEBHOOK, {
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ text: `🚗 New Prestige lead: ${lead.name} (${lead.phone || lead.email}) — ${lead.service || 'general'}${lead.message ? ' · ' + lead.message : ''}` })
+ }).catch(() => {});
+ }
+ res.json({ ok: true, message: "Thanks! We'll follow up to confirm your booking — usually within a few hours." });
});
// Admin: view captured leads.
@@ -201,4 +224,9 @@ app.use('/media', express.static(path.join(__dirname, 'media')));
app.get(/^\/(.+)\.html$/, (req, res) => res.redirect(301, '/' + req.params[0]));
app.use(express.static(path.join(__dirname, 'public'), { extensions: ['html'] }));
-app.listen(PORT, () => console.log(`[prestige-car-wash] http://localhost:${PORT} (admin: /admin)`));
+app.listen(PORT, () => {
+ console.log(`[prestige-car-wash] http://localhost:${PORT} (admin: /admin)`);
+ if (process.env.NODE_ENV === 'production' && !process.env.ADMIN_PASS) {
+ console.warn('⚠ SECURITY: running in production with the DEFAULT admin password. Set ADMIN_PASS in .env before exposing this host.');
+ }
+});
← 4f61ddf Fix CSP: allow inline event-handler attrs (script-src-attr)
·
back to Prestige Car Wash
·
Fix service media paths to match generated svc-<id>.{png,mp4 9dc4f07 →