← back to Consulting Intake
scaffold consulting portal for Consulting Intake
eb9a6247a05b7b991481d751eebabbc61cd23219 · 2026-07-17 07:56:06 -0700 · Steve
Files touched
A .deploy.confA .gitignoreA build.mjsA data/ads.jsonA data/best-times.jsonA data/client.jsonA data/competitors.jsonA data/content-calendar.jsonA data/directories.jsonA data/intakes.jsonA data/media.jsonA data/socials.jsonA data/suggestions.jsonA package.jsonA public/admin/index.htmlA public/intake.htmlA public/portal.htmlA public/signin.htmlA server.js
Diff
commit eb9a6247a05b7b991481d751eebabbc61cd23219
Author: Steve <steve@designerwallcoverings.com>
Date: Fri Jul 17 07:56:06 2026 -0700
scaffold consulting portal for Consulting Intake
---
.deploy.conf | 10 ++++
.gitignore | 10 ++++
build.mjs | 57 ++++++++++++++++++++
data/ads.json | 1 +
data/best-times.json | 16 ++++++
data/client.json | 19 +++++++
data/competitors.json | 1 +
data/content-calendar.json | 1 +
data/directories.json | 18 +++++++
data/intakes.json | 21 ++++++++
data/media.json | 1 +
data/socials.json | 12 +++++
data/suggestions.json | 32 ++++++++++++
package.json | 17 ++++++
public/admin/index.html | 67 ++++++++++++++++++++++++
public/intake.html | 126 +++++++++++++++++++++++++++++++++++++++++++++
public/portal.html | 80 ++++++++++++++++++++++++++++
public/signin.html | 68 ++++++++++++++++++++++++
server.js | 76 +++++++++++++++++++++++++++
19 files changed, 633 insertions(+)
diff --git a/.deploy.conf b/.deploy.conf
new file mode 100644
index 0000000..dc1d9e5
--- /dev/null
+++ b/.deploy.conf
@@ -0,0 +1,10 @@
+# Consulting Intake consulting portal — deploy config (~/Projects/_shared/scripts/deploy.sh)
+# NOTE: deploy to Kamatera + DNS are Steve-gated — do NOT run autonomously.
+PROJECT_NAME=consulting-intake
+DEPLOY_HOST=45.61.58.125
+DEPLOY_PATH=/root/public-projects/consulting-intake
+HEALTH_URL=http://localhost:9700/api/health
+REQUIRED_ENVS=""
+INSTALL_CMD="npm ci --production"
+BUILD_CMD="node build.mjs"
+RSYNC_EXTRA_EXCLUDES="reports"
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3bebb33
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+node_modules/
+.env
+.env.*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
+reports/
diff --git a/build.mjs b/build.mjs
new file mode 100644
index 0000000..d99dac7
--- /dev/null
+++ b/build.mjs
@@ -0,0 +1,57 @@
+#!/usr/bin/env node
+// Consulting Intake — deliverable generator (fantasea-consulting pattern).
+// Emits public/versions/<aesthetic>.html: ONE editorial template skinned by a
+// palette/type/copy object per aesthetic direction, so the client picks a look.
+import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
+import { dirname, join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const HERE = dirname(fileURLToPath(import.meta.url));
+const OUT = join(HERE, 'public', 'versions');
+mkdirSync(OUT, { recursive: true });
+
+const client = JSON.parse(readFileSync(join(HERE, 'data', 'client.json'), 'utf8'));
+
+// Each aesthetic = a skin. Add/edit freely; the client chooses from these.
+const SKINS = {
+ editorial: { bg:'#f6f2ec', ink:'#14110e', accent:'#a9884f', font:"Georgia,serif", vibe:'Luxury / editorial' },
+ modern: { bg:'#ffffff', ink:'#0b0b0d', accent:'#2b6cff', font:"'Helvetica Neue',system-ui,sans-serif", vibe:'Modern / minimal' },
+ bold: { bg:'#0d0d12', ink:'#f5f5f7', accent:'#ff4d6d', font:"'Arial Black',system-ui,sans-serif", vibe:'Bold / energetic' },
+ warm: { bg:'#fbf3e9', ink:'#3a2a1c', accent:'#d98a3d', font:"'Georgia',serif", vibe:'Warm / approachable' },
+ classic: { bg:'#f4f4f0', ink:'#1c2530', accent:'#7a5b34', font:"'Times New Roman',serif", vibe:'Classic / trustworthy' },
+};
+
+const esc = (s) => String(s || '').replace(/[&<>]/g, c => ({ '&':'&','<':'<','>':'>' }[c]));
+const goals = (client.goals || []).slice(0, 4);
+
+for (const [name, s] of Object.entries(SKINS)) {
+ const html = `<!doctype html><html lang="en"><head><meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>${esc(client.business_name)} — ${s.vibe}</title>
+<style>
+:root{--bg:${s.bg};--ink:${s.ink};--accent:${s.accent}}
+*{box-sizing:border-box}body{margin:0;font-family:${s.font};background:var(--bg);color:var(--ink)}
+.bar{padding:14px 30px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--accent)}
+.bar b{letter-spacing:.06em}.bar span{color:var(--accent);font-size:12px;text-transform:uppercase;letter-spacing:.2em}
+.hero{padding:90px 30px;text-align:center}
+.hero .k{color:var(--accent);letter-spacing:.3em;text-transform:uppercase;font-size:12px}
+.hero h1{font-size:56px;margin:.2em 0;font-weight:700}
+.hero p{font-size:20px;max-width:620px;margin:0 auto;opacity:.8}
+.cta{display:inline-block;margin-top:26px;background:var(--accent);color:var(--bg);padding:14px 32px;text-decoration:none;text-transform:uppercase;letter-spacing:.16em;font-size:13px}
+.goals{max-width:900px;margin:40px auto;padding:0 30px;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:18px}
+.goals div{border:1px solid var(--accent);padding:22px;font-size:17px}
+.foot{padding:40px 30px;text-align:center;opacity:.6;font-size:13px}
+</style></head><body>
+<div class="bar"><b>${esc(client.business_name)}</b><span>${s.vibe} concept</span></div>
+<div class="hero">
+ <div class="k">${esc(client.industry || '')} ${client.location ? '· ' + esc(client.location) : ''}</div>
+ <h1>${esc(client.business_name)}</h1>
+ <p>${esc(client.tagline || 'A new website concept, ready to make your first impression unforgettable.')}</p>
+ <a class="cta" href="#">Get in touch</a>
+</div>
+${goals.length ? '<div class="goals">' + goals.map(g => '<div>' + esc(g) + '</div>').join('') + '</div>' : ''}
+<div class="foot">Concept "${name}" · generated ${client.generated_at || ''} · one of several directions to choose from.</div>
+</body></html>`;
+ writeFileSync(join(OUT, `${name}.html`), html);
+}
+console.log('Generated ' + Object.keys(SKINS).length + ' concept versions into public/versions/');
diff --git a/data/ads.json b/data/ads.json
new file mode 100644
index 0000000..0637a08
--- /dev/null
+++ b/data/ads.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/data/best-times.json b/data/best-times.json
new file mode 100644
index 0000000..6a612d6
--- /dev/null
+++ b/data/best-times.json
@@ -0,0 +1,16 @@
+{
+ "instagram": [
+ "Tue 11:00",
+ "Thu 19:00",
+ "Sat 10:00"
+ ],
+ "facebook": [
+ "Wed 13:00",
+ "Sun 12:00"
+ ],
+ "tiktok": [
+ "Tue 18:00",
+ "Fri 20:00"
+ ],
+ "note": "Generic starting cadence — replace with account-specific data via scripts/best-times.js."
+}
\ No newline at end of file
diff --git a/data/client.json b/data/client.json
new file mode 100644
index 0000000..be05bd2
--- /dev/null
+++ b/data/client.json
@@ -0,0 +1,19 @@
+{
+ "business_name": "Consulting Intake",
+ "industry": "Website + integrated-social growth for local businesses",
+ "location": "",
+ "contact": {
+ "name": "",
+ "email": "",
+ "phone": ""
+ },
+ "current_website": null,
+ "tagline": "Tell us about your business — we'll build your new website and social presence.",
+ "goals": [
+ "Collect new-client intakes that spawn a website + social growth plan",
+ "Showcase the concept-version approach (several high-end directions to pick from)",
+ "Front door for the Consulting engine"
+ ],
+ "competitors": [],
+ "generated_at": "2026-07-17"
+}
\ No newline at end of file
diff --git a/data/competitors.json b/data/competitors.json
new file mode 100644
index 0000000..0637a08
--- /dev/null
+++ b/data/competitors.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/data/content-calendar.json b/data/content-calendar.json
new file mode 100644
index 0000000..0637a08
--- /dev/null
+++ b/data/content-calendar.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/data/directories.json b/data/directories.json
new file mode 100644
index 0000000..9545008
--- /dev/null
+++ b/data/directories.json
@@ -0,0 +1,18 @@
+[
+ {
+ "name": "Google Business Profile",
+ "status": "todo"
+ },
+ {
+ "name": "Yelp",
+ "status": "todo"
+ },
+ {
+ "name": "Apple Maps",
+ "status": "todo"
+ },
+ {
+ "name": "Bing Places",
+ "status": "todo"
+ }
+]
\ No newline at end of file
diff --git a/data/intakes.json b/data/intakes.json
new file mode 100644
index 0000000..52cc2f2
--- /dev/null
+++ b/data/intakes.json
@@ -0,0 +1,21 @@
+[
+ {
+ "received_at": "2026-07-17T14:56:06.152Z",
+ "source": "scaffold",
+ "intake": {
+ "slug": "intake",
+ "business_name": "Consulting Intake",
+ "industry": "Website + integrated-social growth for local businesses",
+ "location": "",
+ "tagline": "Tell us about your business — we'll build your new website and social presence.",
+ "current_website": null,
+ "no_website": true,
+ "goals": [
+ "Collect new-client intakes that spawn a website + social growth plan",
+ "Showcase the concept-version approach (several high-end directions to pick from)",
+ "Front door for the Consulting engine"
+ ],
+ "aesthetic": "Modern / minimal"
+ }
+ }
+]
\ No newline at end of file
diff --git a/data/media.json b/data/media.json
new file mode 100644
index 0000000..0637a08
--- /dev/null
+++ b/data/media.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/data/socials.json b/data/socials.json
new file mode 100644
index 0000000..6075deb
--- /dev/null
+++ b/data/socials.json
@@ -0,0 +1,12 @@
+{
+ "instagram": null,
+ "facebook": null,
+ "tiktok": null,
+ "youtube": null,
+ "linkedin": null,
+ "x": null,
+ "pinterest": null,
+ "google_business": null,
+ "connected": false,
+ "notes": "Connect accounts in the command center. Posting stays DRAFT-ONLY until Steve enables."
+}
\ No newline at end of file
diff --git a/data/suggestions.json b/data/suggestions.json
new file mode 100644
index 0000000..6559e41
--- /dev/null
+++ b/data/suggestions.json
@@ -0,0 +1,32 @@
+[
+ {
+ "id": "sug-1",
+ "title": "Collect new-client intakes that spawn a website + social growth plan",
+ "category": "Growth",
+ "impact": 4,
+ "effort": 2,
+ "rationale": "Seeded from the client intake goals — refine with market research.",
+ "created_at": "2026-07-17T14:56:06.151Z",
+ "source": "intake"
+ },
+ {
+ "id": "sug-2",
+ "title": "Showcase the concept-version approach (several high-end directions to pick from)",
+ "category": "Growth",
+ "impact": 4,
+ "effort": 2,
+ "rationale": "Seeded from the client intake goals — refine with market research.",
+ "created_at": "2026-07-17T14:56:06.151Z",
+ "source": "intake"
+ },
+ {
+ "id": "sug-3",
+ "title": "Front door for the Consulting engine",
+ "category": "Growth",
+ "impact": 4,
+ "effort": 2,
+ "rationale": "Seeded from the client intake goals — refine with market research.",
+ "created_at": "2026-07-17T14:56:06.151Z",
+ "source": "intake"
+ }
+]
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..9d97c06
--- /dev/null
+++ b/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "consulting-intake",
+ "version": "0.1.0",
+ "private": true,
+ "description": "Consulting Intake — consulting portal (site concepts + integrated-social growth command center)",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js",
+ "build": "node build.mjs",
+ "enrich": "node ../../.claude/skills/consulting/assets/enrich.mjs --dir ."
+ },
+ "dependencies": {
+ "express": "^4.21.2",
+ "dotenv": "^16.4.7"
+ },
+ "engines": { "node": ">=18" }
+}
diff --git a/public/admin/index.html b/public/admin/index.html
new file mode 100644
index 0000000..d6ac709
--- /dev/null
+++ b/public/admin/index.html
@@ -0,0 +1,67 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Consulting Intake · Growth & Social Command Center</title>
+<style>
+ :root{--ink:#14110e;--bg:#12100e;--panel:#1c1915;--gold:#c6a765;--line:#2c2823;--txt:#e9e2d6}
+ *{box-sizing:border-box}
+ body{margin:0;font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--txt)}
+ header{padding:16px 24px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:center}
+ header h1{font-size:16px;font-weight:600;letter-spacing:.08em;margin:0}
+ header .eyebrow{color:var(--gold);font-size:11px;letter-spacing:.28em;text-transform:uppercase}
+ header a{color:var(--gold);text-decoration:none;font-size:13px}
+ .wrap{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:16px;padding:20px 24px}
+ .panel{background:var(--panel);border:1px solid var(--line);border-radius:6px;padding:16px 18px}
+ .panel h2{font-size:13px;letter-spacing:.14em;text-transform:uppercase;color:var(--gold);margin:0 0 10px;display:flex;justify-content:space-between;align-items:center}
+ .panel h2 .tag{font-size:10px;color:#8a8272;letter-spacing:.06em}
+ textarea{width:100%;min-height:150px;background:#0e0c0a;color:#cfe6cf;border:1px solid var(--line);border-radius:4px;font-family:'SF Mono',ui-monospace,monospace;font-size:12px;padding:10px;resize:vertical}
+ button{background:var(--gold);color:#12100e;border:0;padding:8px 16px;font-weight:600;letter-spacing:.08em;font-size:12px;border-radius:4px;cursor:pointer;margin-top:8px}
+ button:hover{filter:brightness(1.08)}
+ .saved{color:#7ad17a;font-size:12px;margin-left:10px}
+ .note{font-size:12px;color:#8a8272;margin:0 0 8px}
+</style>
+</head>
+<body>
+ <header>
+ <div><div class="eyebrow">Integrated Social · Growth</div><h1>Consulting Intake — Command Center</h1></div>
+ <div><a href="/portal">← Deliverable</a> <a href="/">Sign-in</a></div>
+ </header>
+
+ <div class="wrap" id="wrap"></div>
+
+<script>
+// Each panel is a live-editable view of one data bucket. Edit the JSON, Save,
+// and the server rewrites data/<bucket>.json. Script runners (competitors-scan,
+// best-times, ig-pull, places-read, gen-media) repopulate these from real data.
+const PANELS = [
+ ['socials','Connected Accounts','Instagram · Facebook · TikTok · YouTube · LinkedIn · GBP'],
+ ['best-times','Best Times to Post','per-platform posting cadence'],
+ ['content-calendar','Content Calendar','planned posts / campaigns — DRAFT ONLY'],
+ ['competitors','Competitor Radar','who else is in the market + their social/ads'],
+ ['ads','Ad Signals','which platforms competitors advertise on'],
+ ['directories','Listings & Directories','GBP · Yelp · Apple · Bing'],
+ ['suggestions','Growth Moves','impact / effort ranked ideas'],
+ ['media','Generated Media','reels / graphics produced for this client'],
+];
+const wrap = document.getElementById('wrap');
+async function j(u,opt){ const r=await fetch(u,opt); return r.ok?r.json():null; }
+for (const [key,title,tag] of PANELS){
+ const p = document.createElement('div'); p.className='panel';
+ p.innerHTML = '<h2>'+title+'<span class="tag">'+tag+'</span></h2>'
+ + '<textarea id="ta-'+key+'">loading…</textarea>'
+ + '<div><button data-k="'+key+'">Save '+key+'</button><span class="saved" id="ok-'+key+'"></span></div>';
+ wrap.appendChild(p);
+ j('/api/admin/'+key).then(d=>{ document.getElementById('ta-'+key).value = JSON.stringify(d ?? [], null, 2); });
+}
+wrap.addEventListener('click', async (e)=>{
+ const b = e.target.closest('button[data-k]'); if(!b) return;
+ const k = b.dataset.k; const ta = document.getElementById('ta-'+k); let body;
+ try { body = JSON.parse(ta.value); } catch { document.getElementById('ok-'+k).textContent='✗ invalid JSON'; return; }
+ const r = await fetch('/api/admin/'+k,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
+ document.getElementById('ok-'+k).textContent = r.ok ? '✓ saved '+new Date().toLocaleTimeString() : '✗ error';
+ setTimeout(()=>{document.getElementById('ok-'+k).textContent='';},4000);
+});
+</script>
+</body>
+</html>
diff --git a/public/intake.html b/public/intake.html
new file mode 100644
index 0000000..305aaf3
--- /dev/null
+++ b/public/intake.html
@@ -0,0 +1,126 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
+<title>New Website + Social — Intake · Consulting Intake</title>
+<style>
+ :root{--ink:#14110e;--paper:#f6f2ec;--gold:#a9884f;--line:#dcd3c5}
+ *{box-sizing:border-box}
+ body{margin:0;font-family:system-ui,-apple-system,sans-serif;background:var(--paper);color:var(--ink);padding:0}
+ header{padding:40px 24px 20px;text-align:center;border-bottom:1px solid var(--line);background:#fff}
+ header .eyebrow{letter-spacing:.32em;text-transform:uppercase;font-size:11px;color:var(--gold)}
+ header h1{font-family:Georgia,serif;font-weight:400;font-size:30px;margin:.3em 0 .1em}
+ header p{color:#6b6357;margin:0}
+ form{max-width:760px;margin:0 auto;padding:28px 24px 80px}
+ fieldset{border:1px solid var(--line);background:#fff;margin:18px 0;padding:20px 22px}
+ legend{font-family:Georgia,serif;font-size:18px;padding:0 10px;color:var(--gold)}
+ label{display:block;font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a7266;margin:14px 0 5px;font-weight:600}
+ input,textarea,select{width:100%;padding:11px 12px;border:1px solid var(--line);background:#faf8f4;font-size:15px;font-family:inherit;border-radius:2px}
+ textarea{min-height:76px;resize:vertical}
+ .two{display:grid;grid-template-columns:1fr 1fr;gap:0 18px}
+ .hint{font-size:12px;color:#9a9082;text-transform:none;letter-spacing:0;font-weight:400;margin-top:4px}
+ .check{display:flex;align-items:center;gap:8px;margin-top:10px}
+ .check input{width:auto}
+ button{background:var(--ink);color:var(--paper);border:0;padding:15px 34px;letter-spacing:.16em;text-transform:uppercase;font-size:13px;cursor:pointer;margin-top:8px}
+ button:hover{background:var(--gold)}
+ #done{display:none;background:#eef6ee;border:1px solid #b7d6b7;padding:18px;margin:18px 0;color:#25632a}
+</style>
+</head>
+<body>
+ <header>
+ <div class="eyebrow">Consulting Intake</div>
+ <h1>Let's build your website & social</h1>
+ <p>Answer what you can — this spawns your new site concepts and a social growth plan. No current website required.</p>
+ </header>
+
+ <form id="f">
+ <div id="done"></div>
+
+ <fieldset><legend>The Business</legend>
+ <div class="two">
+ <div><label>Business name*</label><input name="business_name" required></div>
+ <div><label>Industry / what you do</label><input name="industry" placeholder="e.g. car wash, yacht charters, law firm"></div>
+ </div>
+ <div class="two">
+ <div><label>City / service area</label><input name="location"></div>
+ <div><label>In business since</label><input name="since" placeholder="year"></div>
+ </div>
+ <label>One-line pitch (tagline)</label><input name="tagline" placeholder="What makes you the obvious choice?">
+ </fieldset>
+
+ <fieldset><legend>Contact</legend>
+ <div class="two">
+ <div><label>Your name</label><input name="contact_name"></div>
+ <div><label>Role</label><input name="contact_role"></div>
+ </div>
+ <div class="two">
+ <div><label>Email</label><input name="contact_email" type="email"></div>
+ <div><label>Phone</label><input name="contact_phone"></div>
+ </div>
+ </fieldset>
+
+ <fieldset><legend>Current Presence</legend>
+ <div class="check"><input type="checkbox" id="nosite" name="no_website" value="1"><label for="nosite" style="margin:0;text-transform:none;letter-spacing:0;font-weight:400">We don't have a website yet — build one from scratch</label></div>
+ <label>Current website (if any)</label><input name="current_website" placeholder="https://">
+ <div class="hint">Social handles — leave blank what you don't have; we'll set up the rest.</div>
+ <div class="two">
+ <div><label>Instagram</label><input name="ig"></div>
+ <div><label>Facebook</label><input name="fb"></div>
+ <div><label>TikTok</label><input name="tt"></div>
+ <div><label>YouTube</label><input name="yt"></div>
+ <div><label>LinkedIn</label><input name="li"></div>
+ <div><label>Google Business Profile</label><input name="gbp"></div>
+ </div>
+ </fieldset>
+
+ <fieldset><legend>Goals & Growth</legend>
+ <label>Top goals (one per line)</label><textarea name="goals" placeholder="More bookings Rank on Google Grow Instagram"></textarea>
+ <label>Who is your ideal customer?</label><textarea name="audience"></textarea>
+ <label>Direct competitors (one per line)</label><textarea name="competitors"></textarea>
+ </fieldset>
+
+ <fieldset><legend>Brand & Aesthetic</legend>
+ <label>Aesthetic direction</label>
+ <select name="aesthetic">
+ <option value="">— pick a starting feel —</option>
+ <option>Luxury / editorial</option>
+ <option>Modern / minimal</option>
+ <option>Bold / energetic</option>
+ <option>Warm / approachable</option>
+ <option>Classic / trustworthy</option>
+ </select>
+ <div class="hint">We'll generate several concept versions of your site in different high-end looks to choose from.</div>
+ <label>Brand colors / anything you love or hate</label><textarea name="brand_notes"></textarea>
+ <label>Assets you have (logo, photos, video?)</label><textarea name="assets"></textarea>
+ </fieldset>
+
+ <button type="submit">Submit intake →</button>
+ </form>
+
+<script>
+document.getElementById('f').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const fd = new FormData(e.target);
+ const o = Object.fromEntries(fd.entries());
+ const payload = {
+ business_name: o.business_name, industry: o.industry, location: o.location, since: o.since,
+ tagline: o.tagline, contact_name: o.contact_name, contact_role: o.contact_role,
+ contact_email: o.contact_email, contact_phone: o.contact_phone,
+ current_website: o.no_website ? null : (o.current_website || null),
+ no_website: !!o.no_website,
+ socials: { instagram: o.ig||null, facebook: o.fb||null, tiktok: o.tt||null, youtube: o.yt||null, linkedin: o.li||null, google_business: o.gbp||null },
+ goals: (o.goals||'').split('\n').map(s=>s.trim()).filter(Boolean),
+ audience: o.audience, competitors: (o.competitors||'').split('\n').map(s=>s.trim()).filter(Boolean),
+ aesthetic: o.aesthetic, brand_notes: o.brand_notes, assets: o.assets,
+ };
+ const r = await fetch('/api/intake', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
+ const j = await r.json();
+ const d = document.getElementById('done');
+ d.textContent = j.message || 'Received — thank you.';
+ d.style.display = 'block';
+ d.scrollIntoView({ behavior:'smooth' });
+ e.target.reset();
+});
+</script>
+</body>
+</html>
diff --git a/public/portal.html b/public/portal.html
new file mode 100644
index 0000000..01c6a6e
--- /dev/null
+++ b/public/portal.html
@@ -0,0 +1,80 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Consulting Intake · Deliverable</title>
+<style>
+ :root{--ink:#14110e;--paper:#f6f2ec;--gold:#a9884f;--line:#dcd3c5}
+ *{box-sizing:border-box}
+ body{margin:0;font-family:Georgia,serif;background:var(--paper);color:var(--ink)}
+ nav{display:flex;justify-content:space-between;align-items:center;padding:16px 28px;background:#fff;border-bottom:1px solid var(--line);font-family:system-ui,sans-serif}
+ nav .brand{letter-spacing:.06em}
+ nav a{color:var(--gold);text-decoration:none;font-size:13px;letter-spacing:.1em;text-transform:uppercase;margin-left:18px}
+ .hero{padding:56px 28px 34px;text-align:center}
+ .hero .eyebrow{letter-spacing:.32em;text-transform:uppercase;font-size:11px;color:var(--gold);font-family:system-ui,sans-serif}
+ .hero h1{font-weight:400;font-size:44px;margin:.2em 0}
+ .hero p{font-style:italic;color:#6b6357;max-width:640px;margin:0 auto}
+ section{max-width:1080px;margin:0 auto;padding:28px}
+ h2{font-weight:400;font-size:26px;border-bottom:1px solid var(--line);padding-bottom:8px}
+ .cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:18px;margin-top:18px}
+ .vcard{background:#fff;border:1px solid var(--line);text-decoration:none;color:inherit;transition:.2s}
+ .vcard:hover{box-shadow:0 16px 40px rgba(20,17,14,.1);transform:translateY(-2px)}
+ .vcard .swatch{height:120px;background:var(--gold)}
+ .vcard .body{padding:14px 16px}
+ .vcard b{font-family:system-ui,sans-serif;font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:#9a9082}
+ .list{font-family:system-ui,sans-serif;font-size:15px}
+ .list li{margin:8px 0}
+ .badge{display:inline-block;font-family:system-ui,sans-serif;font-size:11px;background:#faf7f1;border:1px solid var(--gold);color:var(--gold);padding:2px 9px;border-radius:20px;margin-left:8px}
+</style>
+</head>
+<body>
+ <nav>
+ <div class="brand" id="brand">Consulting Intake</div>
+ <div><a href="/portal">Deliverable</a><a href="/admin">Growth & Social</a><a href="/">Sign-in</a></div>
+ </nav>
+
+ <div class="hero">
+ <div class="eyebrow">Consulting Deliverable</div>
+ <h1 id="name">Consulting Intake</h1>
+ <p id="tag">Tell us about your business — we'll build your new website and social presence.</p>
+ </div>
+
+ <section>
+ <h2>Website concept directions <span class="badge" id="vcount">—</span></h2>
+ <p class="list" style="color:#6b6357">Several high-end redesign concepts of your new site — pick the direction that feels right. (Generate/refresh with <code>node build.mjs</code>.)</p>
+ <div class="cards" id="versions"></div>
+ </section>
+
+ <section>
+ <h2>Priority growth moves</h2>
+ <ul class="list" id="sugs"></ul>
+ </section>
+
+ <section>
+ <h2>Integrated social snapshot</h2>
+ <ul class="list" id="social"></ul>
+ </section>
+
+<script>
+async function j(u){ const r = await fetch(u); return r.ok ? r.json() : null; }
+(async () => {
+ const c = await j('/api/client'); if (c && c.business_name){ document.getElementById('name').textContent = c.business_name; document.getElementById('brand').textContent = c.business_name; if(c.tagline) document.getElementById('tag').textContent = c.tagline; }
+ // version concepts (generated into /versions by build.mjs)
+ const known = ['editorial','modern','bold','warm','classic'];
+ const vc = document.getElementById('versions'); let found = 0;
+ for (const v of known){ const ok = await fetch('/versions/'+v+'.html',{method:'HEAD'}).then(r=>r.ok).catch(()=>false);
+ if(ok){ found++; const a=document.createElement('a'); a.className='vcard'; a.href='/versions/'+v+'.html';
+ a.innerHTML='<div class="swatch"></div><div class="body"><b>'+v+'</b></div>'; vc.appendChild(a); } }
+ document.getElementById('vcount').textContent = found + ' concepts';
+ if(!found) vc.innerHTML = '<p style="font-family:system-ui;color:#9a9082">No concepts generated yet — run <code>node build.mjs</code>.</p>';
+ // suggestions
+ const s = await j('/api/admin/suggestions')||[]; const su=document.getElementById('sugs');
+ su.innerHTML = s.length ? s.map(x=>'<li><b>'+(x.title||'')+'</b> <span class="badge">impact '+(x.impact||'?')+' / effort '+(x.effort||'?')+'</span><br><span style="color:#6b6357">'+(x.rationale||'')+'</span></li>').join('') : '<li style="color:#9a9082">No growth moves seeded yet.</li>';
+ // socials
+ const so = await j('/api/admin/socials')||{}; const el=document.getElementById('social');
+ const rows = Object.entries(so).filter(([k])=>!['connected','notes'].includes(k)).map(([k,v])=>'<li><b>'+k+'</b> — '+(v?('<a href="'+(String(v).startsWith('http')?v:'#')+'">'+v+'</a>'):'<span style="color:#c66">not set up yet</span>')+'</li>');
+ el.innerHTML = rows.join('') + (so.notes?'<li style="color:#9a9082;font-size:13px">'+so.notes+'</li>':'');
+})();
+</script>
+</body>
+</html>
diff --git a/public/signin.html b/public/signin.html
new file mode 100644
index 0000000..3cd570c
--- /dev/null
+++ b/public/signin.html
@@ -0,0 +1,68 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Consulting Intake · Consulting Portal</title>
+<style>
+ :root{--ink:#14110e;--paper:#f6f2ec;--gold:#a9884f;--line:#dcd3c5}
+ *{box-sizing:border-box}
+ body{margin:0;font-family:'Georgia',serif;background:var(--paper);color:var(--ink);min-height:100vh;display:grid;place-items:center;padding:32px}
+ .card{max-width:560px;width:100%;background:#fff;border:1px solid var(--line);box-shadow:0 24px 60px rgba(20,17,14,.08);padding:44px 48px}
+ .eyebrow{letter-spacing:.32em;text-transform:uppercase;font-size:11px;color:var(--gold);font-family:system-ui,sans-serif}
+ h1{font-size:34px;margin:.35em 0 .1em;font-weight:400;letter-spacing:.01em}
+ .sub{color:#6b6357;font-style:italic;margin:0 0 24px}
+ .grid{display:grid;grid-template-columns:1fr 1fr;gap:14px 22px;border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:20px 0;margin:20px 0;font-family:system-ui,sans-serif;font-size:14px}
+ .grid b{display:block;font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:#9a9082;font-weight:600;margin-bottom:2px}
+ .creds{background:#faf7f1;border:1px dashed var(--gold);padding:16px 18px;margin:18px 0;font-family:'SF Mono',ui-monospace,monospace;font-size:13px}
+ .creds .row{display:flex;justify-content:space-between;padding:3px 0}
+ .creds .k{color:#9a9082}
+ a.enter{display:inline-block;margin-top:10px;background:var(--ink);color:#f6f2ec;text-decoration:none;padding:13px 26px;letter-spacing:.16em;text-transform:uppercase;font-size:12px;font-family:system-ui,sans-serif}
+ a.enter:hover{background:var(--gold)}
+ .foot{margin-top:22px;font-size:12px;color:#9a9082;font-family:system-ui,sans-serif}
+ .foot a{color:var(--gold)}
+ .gwrap{margin-top:16px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;font-family:system-ui,sans-serif}
+ .gbtn{display:inline-flex;align-items:center;gap:10px;background:#fff;border:1px solid var(--line);color:#3c4043;padding:11px 18px;font-size:13px;border-radius:4px;cursor:not-allowed;opacity:.65}
+ .soon{font-size:11px;color:#9a9082;letter-spacing:.04em;text-transform:uppercase}
+</style>
+</head>
+<body>
+ <div class="card">
+ <div class="eyebrow">Consulting Engagement</div>
+ <h1>Consulting Intake</h1>
+ <p class="sub">Tell us about your business — we'll build your new website and social presence.</p>
+
+ <div class="grid" id="meta">
+ <div><b>Industry</b>Website + integrated-social growth for local businesses</div>
+ <div><b>Location</b></div>
+ <div><b>Current Site</b>none yet</div>
+ <div><b>Engagement Opened</b>2026-07-17</div>
+ <div><b>Primary Contact</b></div>
+ <div><b>Reach</b> · </div>
+ </div>
+
+ <!-- Steve's standing rule: the initial sign-in page CARRIES the credentials. -->
+ <div class="creds">
+ <div class="row"><span class="k">Portal URL</span><span>/portal & /admin</span></div>
+ <div class="row"><span class="k">Client login</span><span>client / Consul2026!</span></div>
+ <div class="row"><span class="k">Team login</span><span>admin / DW2024!</span></div>
+ </div>
+
+ <a class="enter" href="/portal">Enter the deliverable →</a>
+
+ <a class="enter" style="background:transparent;color:var(--ink);border:1px solid var(--ink)" href="/admin">Growth command center</a>
+
+ <!-- Google login is a FUTURE upgrade; standard protection (Basic Auth) is live now. -->
+ <div class="gwrap">
+ <button class="gbtn" disabled title="Google sign-in is coming soon — use the credentials above for now">
+ <svg width="16" height="16" viewBox="0 0 48 48" aria-hidden="true"><path fill="#4285F4" d="M45 24.5c0-1.6-.1-2.8-.4-4H24v7.3h11.9c-.2 1.9-1.5 4.7-4.3 6.6l6.6 5.1C42 41.9 45 33.9 45 24.5z"/><path fill="#34A853" d="M24 46c5.7 0 10.5-1.9 14-5.1l-6.6-5.1c-1.8 1.2-4.2 2.1-7.4 2.1-5.7 0-10.5-3.8-12.2-9l-6.8 5.3C8.5 41.2 15.7 46 24 46z"/><path fill="#FBBC05" d="M11.8 28.9C11.3 27.5 11 26 11 24.5s.3-3 0.8-4.4l-6.8-5.3C3.7 17.6 3 20.9 3 24.5s.7 6.9 2 9.7l6.8-5.3z"/><path fill="#EA4335" d="M24 12.5c3.2 0 5.3 1.4 6.6 2.5l5.8-5.7C33 5.9 28.4 4 24 4 15.7 4 8.5 8.8 5 15.1l6.8 5.3C13.5 15.3 18.3 12.5 24 12.5z"/></svg>
+ Continue with Google
+ </button>
+ <span class="soon">coming soon · standard protection is active now</span>
+ </div>
+
+ <div class="foot">
+ New client, or no website yet? <a href="/intake">Start the intake questionnaire →</a>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..d39194c
--- /dev/null
+++ b/server.js
@@ -0,0 +1,76 @@
+'use strict';
+/**
+ * Consulting Intake — Consulting portal.
+ * Generated by the `consulting` skill (fuses fantasea-consulting + prestige-car-wash).
+ *
+ * Routes:
+ * GET / -> signin.html (UNGATED) — client info + credentials
+ * GET /intake -> intake.html (UNGATED) — new-client questionnaire
+ * POST /api/intake -> append to data/intakes.json (UNGATED submit)
+ * GET /api/health -> {ok:true} (UNGATED)
+ * GET /portal -> portal.html (GATED) — the consulting deliverable
+ * GET /admin -> admin/index.html (GATED) — integrated-social command center
+ * GET /api/admin/:bucket -> data/<bucket>.json (GATED read)
+ * POST /api/admin/:bucket -> overwrite data/<bucket>.json (GATED write)
+ */
+const fs = require('fs');
+const path = require('path');
+const express = require('express');
+try { require('dotenv').config(); } catch { /* dotenv optional */ }
+
+const app = express();
+app.use(express.json({ limit: '1mb' }));
+const PORT = process.env.PORT || 9700;
+const PUB = path.join(__dirname, 'public');
+const DATA = path.join(__dirname, 'data');
+
+// ---- credentials -----------------------------------------------------------
+// Unified fleet login + this client's own login. Extra pairs via BASIC_AUTH_EXTRA
+// (comma-separated user:pass). These same creds are shown on the sign-in page.
+const CREDS = ['admin:DW2024!', 'client:Consul2026!']
+ .concat((process.env.BASIC_AUTH_EXTRA || '').split(',').map((s) => s.trim()).filter(Boolean));
+const ACCEPTED = new Set(CREDS.map((c) => 'Basic ' + Buffer.from(c).toString('base64')));
+function gate(req, res, next) {
+ if (ACCEPTED.has(req.headers.authorization || '')) return next();
+ res.set('WWW-Authenticate', 'Basic realm="Consulting Intake Portal"');
+ return res.status(401).send('Authentication required');
+}
+
+// ---- data helpers ----------------------------------------------------------
+const readJSON = (f, fb) => { try { return JSON.parse(fs.readFileSync(path.join(DATA, f), 'utf8')); } catch { return fb; } };
+const writeJSON = (f, v) => fs.writeFileSync(path.join(DATA, f), JSON.stringify(v, null, 2));
+const BUCKETS = ['client', 'socials', 'suggestions', 'competitors', 'best-times', 'content-calendar', 'ads', 'directories', 'media', 'intakes'];
+
+// ---- ungated routes --------------------------------------------------------
+app.get('/', (_req, res) => res.sendFile(path.join(PUB, 'signin.html')));
+app.get('/intake', (_req, res) => res.sendFile(path.join(PUB, 'intake.html')));
+app.get('/api/health', (_req, res) => res.json({ ok: true, client: 'Consulting Intake', at: new Date().toISOString() }));
+app.get('/api/client', (_req, res) => res.json(readJSON('client.json', {}))); // public-safe summary for signin page
+app.post('/api/intake', (req, res) => {
+ const all = readJSON('intakes.json', []);
+ all.push({ received_at: new Date().toISOString(), source: 'form', intake: req.body || {} });
+ writeJSON('intakes.json', all);
+ res.json({ ok: true, message: 'Thanks — your consulting intake was received.' });
+});
+
+// ---- gated routes ----------------------------------------------------------
+app.get('/portal', gate, (_req, res) => res.sendFile(path.join(PUB, 'portal.html')));
+app.get('/admin', gate, (_req, res) => res.sendFile(path.join(PUB, 'admin', 'index.html')));
+app.get('/api/admin/:bucket', gate, (req, res) => {
+ if (!BUCKETS.includes(req.params.bucket)) return res.status(404).json({ error: 'unknown bucket' });
+ res.json(readJSON(req.params.bucket + '.json', []));
+});
+app.post('/api/admin/:bucket', gate, (req, res) => {
+ if (!BUCKETS.includes(req.params.bucket)) return res.status(404).json({ error: 'unknown bucket' });
+ writeJSON(req.params.bucket + '.json', req.body);
+ res.json({ ok: true });
+});
+
+// gated static (portal assets + generated version concepts)
+app.use('/portal', gate, express.static(path.join(PUB), { extensions: ['html'] }));
+app.use('/versions', gate, express.static(path.join(PUB, 'versions'), { extensions: ['html'] }));
+app.use('/admin', gate, express.static(path.join(PUB, 'admin'), { extensions: ['html'] }));
+// ungated static (only the two entry pages + shared img)
+app.use('/img', express.static(path.join(PUB, 'img')));
+
+app.listen(PORT, () => console.log('Consulting Intake consulting portal on ' + PORT));
(oldest)
·
back to Consulting Intake
·
Consulting Intake template home — public questionnaire front cc8c413 →