`); w.document.close(); } function commandActions(){ const A=[],CR=CLIENTS_REAL||[]; const ap=APPROVALS?((APPROVALS.content||[]).length+(APPROVALS.builds||[]).length):0; if(ap)A.push(['◈',ap+(ap===1?' decision':' decisions')+' waiting on you','Content and build requests need your yes or no',"go('manage')",'Review']); const packs=CR.filter(c=>c.site_url&&/proposal|\/packs\//.test(c.site_url)); if(packs.length)A.push(['\u2726',packs.length+' client pack'+(packs.length>1?'s':'')+' ready to present', packs.slice(0,3).map(c=>c.n).join(', ')+(packs.length>3?', +'+(packs.length-3)+' more':'')+' \u00b7 proposal built, open it in the meeting', "go('clients')",'Open']); const notes=CR.flatMap(c=>((c.intake&&c.intake.notes)||[]).filter(n=>(n.status||'working')==='working')); if(notes.length)A.push(['✎',notes.length+' of your notes in progress','The team is working on what you flagged in meetings',"go('clients')",'Clients']); const atrisk=CR.filter(c=>(c.score||0)<40); if(atrisk.length)A.push(['⚑',atrisk.length+' clients need a proof-point fast',atrisk.slice(0,3).map(c=>c.n).join(', ')+(atrisk.length>3?', +'+(atrisk.length-3)+' more':''),"go('retention')",'Retention']); const prospects=CR.filter(c=>c.status==='prospect'||['new','niche','luxury','high-proof','case-study'].includes(c.st)); if(prospects.length)A.push(['✦',prospects.length+' prospects ready to onboard','Proposal, contract and deposit are drafted for each',"go('onboarding')",'Onboard']); cmpLoad();const drafts=(CAMPAIGNS||[]).filter(c=>c.status==='Draft'); if(drafts.length)A.push(['✉',drafts.length+(drafts.length===1?' campaign draft':' campaign drafts')+' ready to launch','Submit for launch when the moment is right',"go('win');setSub('win','campaigns')",'Campaigns']); return A; } function command(){ if(!APPROVALS&&!APPROVALS_ERR)loadApprovals(); const CR=CLIENTS_REAL||[]; const mrrRaw=ANALYTICS?ANALYTICS.mrr_estimate_eur:null;const mrrKnown=mrrRaw!=null&&!isNaN(mrrRaw); const halfDisp=mrrKnown?('€'+Math.round(mrrRaw/2).toLocaleString()):'—'; const tierMo={GROUP:600,GROW:600,'MINI→GROW':300,'active→GROW':299,MINI:300}; const active=CR.filter(c=>c.status==='active'||['live','flagship'].includes(c.st)).length; const pipeline=CR.filter(c=>c.status==='prospect'||['new','niche','luxury','high-proof','case-study'].includes(c.st)).reduce((s,c)=>s+(tierMo[c.tier]||300),0); const avg=CR.length?Math.round(CR.reduce((s,c)=>s+(c.score||0),0)/CR.length):0; const strong=CR.filter(c=>(c.score||0)>=70).length,watch=CR.filter(c=>(c.score||0)<40).length,building=CR.length-strong-watch; const acts=commandActions(); return `

Good to see you, Somer

Your agency at a glance, and exactly what needs you today. ${CR.length} clients on the book.
${eur(mrrRaw)}
Live MRR (est)
${active} active clients
${halfDisp}
Your half, monthly
50/50 after platform fee
€${pipeline.toLocaleString()}
Pipeline, potential/mo
if you sign the prospects
${CR.length}
Clients on the book
avg visibility ${avg}/100
What needs you today
${acts.length?`
${acts.map(a=>`
${a[0]}
${a[1]}
${a[2]}
`).join('')}
` :`
You're all caught up.
No decisions, notes or at-risk clients right now. Good time to sign someone new.
`}
Your client health
${strong}
Strong · 70+
${building}
Building
${watch}
Watch · under 40
What's working on the Costa
Top-10% performers average 3,882 reviews vs 436 for the rest, at the same ~4.4★. The play: relentless review generation.
9× more reviewssame rating→ Reviews Engine
Your patch · ${fmt(UNIV.total)} businesses to sell into
Top towns
${TOWNS.map(([n,v])=>`
${n}
${fmt(v)}
`).join('')}
Top verticals
${CATS.map(([n,v])=>`
${n}
${fmt(v)}
`).join('')}
${fmt(UNIV.noSite)} have no website and ${fmt(UNIV.email)} are email-reachable. Turn a segment into a campaign in Win.
`; } /* ===== Campaign composer · Somer builds & manages real outreach campaigns from the 76k universe. Drafts persist on her device; "Submit for launch" files a Craig-gated build packet (real backend). ===== */ const CMP_SEGMENTS=[['No-website businesses',UNIV.noSite],['Reachable by email now',UNIV.email], ...TOWNS.map(t=>[t[0]+' (all)',t[1]]),...CATS.map(c=>[c[0]+'s',c[1]])]; const CMP_CHANNELS=['Email','WhatsApp','Email + WhatsApp','Audit → call','Email sequence']; let CAMPAIGNS=[],CMP_OPEN=false; function cAttr(s){return String(s==null?'':s).replace(/&/g,'&').replace(/"/g,'"').replace(//g,'>');} function cTxt(s){return String(s==null?'':s).replace(/&/g,'&').replace(//g,'>');} function cmpLoad(){try{CAMPAIGNS=JSON.parse(localStorage.getItem('costaCampaigns'))||[];}catch(e){CAMPAIGNS=[];}} function cmpStore(){try{localStorage.setItem('costaCampaigns',JSON.stringify(CAMPAIGNS));}catch(e){}} function cmpToggle(){CMP_OPEN=!CMP_OPEN;render();} function cmpAutoTargets(){const seg=document.getElementById('cmpSeg'),tgt=document.getElementById('cmpTgt');if(!seg||!tgt)return;const f=CMP_SEGMENTS.find(x=>x[0]===seg.value);if(f&&!tgt.value)tgt.value=f[1];} function cmpAdd(){const g=id=>document.getElementById(id);const seg=g('cmpSeg').value;if(!seg){toast('Pick a segment from the universe first.',true);return;} const off=g('cmpOffer').value,ch=g('cmpChan').value,tgt=Math.max(0,+g('cmpTgt').value||0); const name=(g('cmpName').value||'').trim()||(seg+' · '+off); CAMPAIGNS.unshift({id:'c'+Date.now(),name,seg,ch,off,tgt,status:'Draft'});cmpStore();CMP_OPEN=false;render();} function cmpDel(id){CAMPAIGNS=CAMPAIGNS.filter(c=>c.id!==id);cmpStore();render();} function cmpSubmit(id){const c=CAMPAIGNS.find(x=>x.id===id);if(!c)return; fireBuild('Campaign · '+c.name,'Segment: '+c.seg+' · Channel: '+c.ch+' · Offer: '+c.off+' · ~'+c.tgt+' targets. Scope + approve sends (Craig-gated; needs the WhatsApp number live).'); c.status='Submitted';cmpStore();render();} function win(){ const s=SUB.win||'universe'; const sn=['universe|Lead Universe','audit|AI-Visibility Audit','pipeline|Pipeline','campaigns|Campaigns'] .map(x=>{const[id,l]=x.split('|');return ``}).join(''); let body=''; if(s==='universe'){ let rows=LEADS.slice();if(FILTER==='nosite')rows=rows.filter(l=>!l.site);if(FILTER==='beauty')rows=rows.filter(l=>/beauty|hair/i.test(l.c)); const uniBody=rows.length ?`
${rows.map(l=>``).join('')}
BusinessTypeTownEmailWeb
${l.n}${l.c}${l.t}${l.e} ${l.site?'Site':'None'}
Live sample · the full ${fmt(UNIV.total)} streams in once enrichment finishes.
` :`
No enriched leads loaded yet
The lead universe (${fmt(UNIV.total)} Costa businesses) streams in from the free-data machine as enrichment completes — no contact details are stored in this page. In the meantime, work your Pipeline or run an audit from a client.
`; body=`
${uniBody}`; }else if(s==='audit'){ const b=AUDIT_BIZ; if(!b||!b.n){ body=`
No business selected
Pick a business from Pipeline (or a client's Run live audit) and hit Audit — the AI-visibility score and the intelligence panel open here.
`; return `

Find, audit & sign clients

The 76k universe → audit → pipeline → outreach.
${body}`; } const r=scoreAV(deriveInput(b)),circ=2*Math.PI*74,off=circ*(1-r.overall/100); const bars=[['Visible in AI answers',r.ai,r.ai<35?'c':''],['Local search presence',r.local,r.local<60?'c':''],['Proof strength',r.proof,r.proof<55?'c':''],['No-send guardrails',r.guard,'']]; body=`
Audited: ${b.n} · ${b.t}${b.site?'':' · no website'} model estimate
${r.overall}/100 · est.
${bars.map(([l,v,c])=>`
${l}
${v}
`).join('')}
Model estimate — not yet measured. Run the live probe below for a real check. Recommended: ${r.product[2]} · ${r.product[1]}.${r.scoreable?'':' To finalise: '+r.missing.join(', ')+'.'}
${(()=>{const la=LIVE_AUDIT[b.n]; if(!la)return `
Asks a live AI to recommend ${b.c||'businesses'} in ${b.t} and checks if ${b.n} shows up.
`; if(la.loading)return `
Probing live AI…
`; if(la.error||!la.scores)return `
Live probe unavailable right now.
`; const s=la.scores,c2=2*Math.PI*74,o2=c2*(1-s.overall/100),pr=la.probe||{}; return `
Live AI-visibility probe LIVE · real
${s.overall}/100 real
${[['Visible in AI answers',s.ai_visibility],['Local search (real Google)',s.local_search_presence],['Proof strength',s.proof_strength]].map(x=>`
${x[0]}
${x[1]}
`).join('')}
${pr.in_list?`The AI recommends ${b.n}.`:`The AI did not recommend ${b.n}. It named ${((pr.competitors&&pr.competitors.length?pr.competitors:pr.recommended)||[]).slice(0,3).join(', ')||'others'} instead. That gap is the sale.`}${la.signals&&la.signals.reviews?` (${la.signals.reviews.toLocaleString()} real reviews · ${la.signals.rating}★)`:''}
`;})()} ${intelPanel(b)}
`; }else if(s==='pipeline'){ const pipeRows=PIPE.length?PIPE.map(p=>`${p.n}${p.c}${p.t} ${p.sc} ${p.st} `).join(''):`No researched prospects loaded yet — they stream in from the free-data machine, highest-opportunity first.`; body=`
${pipeRows}
ProspectTypeTownOpportunityStage
${UNIV.researched} researched prospects (expat hospitality, Fuengirola/Benalmádena/Marbella). Opportunity = a data-driven priority estimate; most need contact enrichment before outreach.
`; }else{ cmpLoad(); const inp='padding:9px 11px;background:var(--ink);border:1px solid var(--line);border-radius:8px;color:var(--paper);font-family:var(--body);font-size:13px;width:100%'; const composer=CMP_OPEN?`
Build a campaign
`:''; const rows=CAMPAIGNS.length?CAMPAIGNS.map(c=>`${cTxt(c.name)}
${cTxt(c.seg)}
${cTxt(c.ch)}${cTxt(c.off)}${fmt(c.tgt||0)}${c.status}${c.status==='Draft'?` `:''}`).join(''):`No campaigns yet — build your first from the 76k universe with + New campaign.`; body=` ${composer}
${rows}
CampaignChannelOfferTargetsStatus
Compose any campaign from the real 76k universe — pick a segment, channel and offer. Submit for launch files it to your Approvals inbox; sending goes out once it's approved and the WhatsApp number is live. Drafts save on this device.
`; } return `

Find, audit & sign clients

The 76k universe → audit → pipeline → outreach.
${body}`; } function gallery(items,kind){return `
${items.map(i=>`
${i[0]}${i[2]?`${i[3]}`:''}
${i[1]}
`).join('')}
`;} function clientBuild(ic,name,sub,tag,href){ const cta=href?`` :(/^(Live|Meta pending|Active)$/.test(tag[1])?`${tag[1]}` :``); return `
${ic}
${name}
${sub}
${cta}
`;} /* ---- live brand customiser: colour pickers recolour the preview in real time ---- */ function colorRow(id,label,val){return `
${label}
`;} function preset(name,p,a,bg,t){return ``;} function recolor(){const g=id=>document.getElementById(id),prev=g('bpPrev');if(!prev)return; prev.style.setProperty('--p',g('bpP').value);prev.style.setProperty('--a',g('bpA').value); prev.style.setProperty('--bg',g('bpBg').value);prev.style.setProperty('--t',g('bpT').value); document.querySelectorAll('.bpName').forEach(e=>e.textContent=g('bpName').value||'Client');} function applyPreset(p,a,bg,t){['bpP','bpA','bpBg','bpT'].forEach((id,i)=>document.getElementById(id).value=[p,a,bg,t][i]);recolor();} /* ---- per-sector live preview: the site + app mock changes with the VERTICAL and the colours, so Somer can show ANY client their own look — not just the salon. ---- */ const PREVIEW={ salon:{label:'Salon & Beauty',name:'Bronzed Riviera',tagline:'Your best look, booked in seconds',blurb:'Premium studio on the Costa del Sol.',nav:['Home','Services','Book'],cta:'Book now',appTitle:'Studio app',menu:['Tanning','Nails','Lashes','Facials'],pal:['#C9A96E','#E11D74','#1A1A1A','#FFFFFF']}, restaurant:{label:'Restaurant & Bar',name:'Terraza 42',tagline:'Reserve your table on the water',blurb:'Sunset dining on the Marbella marina.',nav:['Menu','Book','Reviews'],cta:'Reserve a table',appTitle:'Bookings',menu:['Starters','Mains','Wine list','Desserts'],pal:['#B5462A','#F2B441','#1C1418','#FFF7F2']}, gym:{label:'Gym & Studio',name:'Costa Strength',tagline:'Start your 7-day free trial',blurb:'Coaching, classes & community.',nav:['Classes','Membership','Trial'],cta:'Start free trial',appTitle:'Member app',menu:['Timetable','Book a class','My plan','Membership'],pal:['#16A34A','#84CC16','#0B1220','#EAF6EA']}, estate:{label:'Estate Agent',name:'Marbella Homes',tagline:'Find your next home on the coast',blurb:'Sales, rentals & valuations.',nav:['Buy','Rent','Valuation'],cta:'Book a valuation',appTitle:'Property app',menu:['Listings','Viewings','Valuation','Mortgage'],pal:['#1E3A8A','#5AA9FF','#0B1826','#EAF1F8']}, clinic:{label:'Clinic & Practice',name:'Costa Dental',tagline:'Book your consultation today',blurb:'Modern dental & aesthetic care.',nav:['Treatments','Team','Book'],cta:'Book consultation',appTitle:'Patient app',menu:['Appointments','Treatments','Consent','Recalls'],pal:['#0EA5A4','#2DD4BF','#FFFFFF','#0F2E2D']}, trades:{label:'Trades & Home',name:'Solair Pools',tagline:'Get your free quote in 60 seconds',blurb:'Pools, gardens & maintenance.',nav:['Services','Gallery','Quote'],cta:'Get a free quote',appTitle:'Job app',menu:['Request a quote','My jobs','Invoices','Reviews'],pal:['#C2410C','#F59E0B','#0F172A','#FFF7ED']}}; const PREVIEW_ORDER=['salon','restaurant','gym','estate','clinic','trades']; let BP_SECTOR='salon'; function setPreviewSector(s){if(!PREVIEW[s])return;BP_SECTOR=s;render();} function sectorChips(){return PREVIEW_ORDER.map(id=>``).join('');} function previewPane(){const c=PREVIEW[BP_SECTOR]; return `
${cTxt(c.name)} ${c.nav.join(' · ')}
${cTxt(c.tagline)}
${cTxt(c.blurb)}
${[0,1,2].map(()=>`
`).join('')}
${cTxt(c.name)}
${cTxt(c.appTitle)}
${c.menu.map(x=>`
${cTxt(x)}
`).join('')}
`;} function deliver(){ const s=SUB.deliver||'studio'; const sn=['studio|Studio','app|App Builder','web|Website Builder','whatsapp|WhatsApp Menu','os|Client OS','brand|Brand & Preview'] .map(x=>{const[id,l]=x.split('|');return ``}).join(''); let body=''; if(s==='studio'){ const cards=[ ['Content Studio','MASON','Bilingual EN/ES posts + reels · calendar · approve → Blotato','14 posts/wk live for Bronzed','ok'], ['SEO & AI-Visibility','BEACON','Local SEO + AEO/GEO · keyword tracking · re-score','Ready per client','sky'], ['Reviews & Reputation','ReviewForge','Auto review requests · monitor rating · respond','Wired','ok'], ['Sector Add-ons','SNAP suite','EstateAgentSnap · PropVault · HospSnap per client','Available','sky']]; body=`
${cards.map(c=>`
${c[0]}
${c[2]}
${c[3]}
powered by ${c[1]}
`).join('')}
The four builders · App · Website · WhatsApp Menu · Client OS · are in the tabs above. Everything is scoped to Somer's clients.
`; } else if(s==='app'){ body=`
App templates no-code
${gallery([ ['Booking app','Appointments, deposits, no-show reminders','ok','popular'], ['Loyalty & membership','Points, tiers, automatic win-back'], ['Job-sheet app','Quotes → jobs → invoices (trades)'], ['Order / menu app','Click-collect + table ordering (hospitality)'], ['Class & membership','Timetables, trials, memberships (gyms)'], ['Client portal app','White-label login portal for the client']],'App')}
Built for clients
${clientBuild('▣','Bronzed Riviera · Booking app','9-staff diary · deposits · reminders',['ok','Live'],'https://brg-app-bronzed-riviera.pages.dev')} ${clientBuild('+','New app','Pick a client → template → publish',['sky','Start'])}`; } else if(s==='web'){ body=`
Site templates Client Engine
${gallery([ ['Presence one-pager','The fix for the 30,845 no-site businesses','ok','fastest'], ['Salon / beauty site','Services, team, booking, gallery'], ['Restaurant site','Menu, reservations, reviews'], ['Estate-agent site','Listings, leads, valuations'], ['Gym / studio site','Timetable, trials, memberships'], ['Clinic / practice site','Appointments, consent forms']],'Website')}
Published sites
${clientBuild('◱','Bronzed Riviera · studio site','Booking + gallery · brand-matched',['ok','Live'])} ${clientBuild('+','New site','Client → template → domain → publish',['sky','Start'])}`; } else if(s==='whatsapp'){ body=`
WhatsApp menu blocks
${gallery([ ['Booking menu','“Book / reschedule / cancel” → diary','ok','core'], ['Services & prices','Interactive list, tap to enquire'], ['Opening hours & location','Auto-reply + map pin'], ['Offers & anchor days','Push the happy-hour / package deals'], ['Order menu','Food/retail order → collect or deliver'], ['Win-back','Lapsed-client message on a trigger']],'WhatsApp menu')}
Client menus
${clientBuild('◐','Bronzed Riviera · booking bot','Confirm · 24h reminder · win-back',['mid','Meta pending'])} ${clientBuild('+','New WhatsApp menu','Client → number → blocks → submit to Meta',['sky','Start'])}`; } else if(s==='os'){ body=`
Deploy a full Client OS vertical runtimes
${gallery([ ['Salon / Beauty OS','Multi-staff diary, clients, treatments, reviews, loyalty','ok','proven'], ['Restaurant OS','Tables, deposits, menu, reviews, suppliers'], ['Gym / Studio OS','Classes, trials, memberships, retention'], ['Estate-Agent OS','Listings, leads, comparables, valuations'], ['Clinic OS','Appointments, recalls, consent, compliance'], ['Trades OS','Jobs, quotes, materials pricing, invoices']],'Client OS')}
Deployed
${clientBuild('◆','Bronzed Riviera · Salon OS','9 staff · full front-of-house · running',['ok','Live'],'https://brg-app-bronzed-riviera.pages.dev')} ${clientBuild('+','Deploy an OS','Pick client vertical → brand → spin up',['sky','Deploy'])}
The Salon OS running for Bronzed is the template · every vertical OS deploys the same way, brand-matched to the client.
`; } else if(s==='brand'){ const c=PREVIEW[BP_SECTOR]; body=`
Sector
${sectorChips()}
Client brand
${colorRow('bpP','Primary',c.pal[0])}${colorRow('bpA','Accent / button',c.pal[1])}${colorRow('bpBg','Background',c.pal[2])}${colorRow('bpT','Text',c.pal[3])}
Quick palettes
${preset('Bronzed','#C9A96E','#C9A96E','#1A1A1A','#FFFFFF')}${preset('Ocean','#2DD4BF','#5AA9FF','#0B1826','#EAF1F8')} ${preset('Sunset','#FF6A3D','#F2B441','#1C1418','#FFF7F2')}${preset('Fresh','#16A34A','#84CC16','#FFFFFF','#0F172A')} ${preset('Rosé','#E11D74','#F9A8D4','#FFF1F7','#3B0A24')}${preset('Mono','#111111','#111111','#FFFFFF','#111111')}
${previewPane()}
Pick the client's sector (top-left) and colours · the site + app preview switch to that vertical instantly. This is exactly what the client sees before you publish · the same theme flows into their OS.
`; } return `

Somer's build studio · the full works

Her own App builder, Website builder, WhatsApp menu builder and Client-OS deployer · all scoped to her clients.
${body}`; } function approvalCard(kind,id,title,sub,body){ const busy=APPROVALS_BUSY===id; return `
${kind==='content'?'✎':'▣'}
${title}
${sub}
${body?`
${body}
`:''}
`; } function manage(){ if(!APPROVALS&&!APPROVALS_ERR)loadApprovals(); const content=(APPROVALS&&APPROVALS.content)||[],builds=(APPROVALS&&APPROVALS.builds)||[]; const total=content.length+builds.length; let inbox; if(APPROVALS_ERR)inbox=`
Couldn't load your approvals · ${APPROVALS_ERR}.
`; else if(!APPROVALS)inbox=`
Loading your approvals…
`; else if(total===0)inbox=`
✓ Nothing awaiting your decision right now — new content and build requests land here the moment they clear the quality gate.
`; else inbox=`${content.map(c=>approvalCard('content',c.id,(c.topic||c.platform||'Content')+' · '+(c.partner_id||''),'Content · gate score '+(c.gate_score??'·')+' · '+(c.gate_verdict||''),(c.final_text||c.draft_text||c.hook_1||'').slice(0,220))).join('')} ${builds.map(b=>approvalCard('build',b.id,b.title||'Build request','Website/App · requested by '+(b.requested_by||'·'),b.why_now||'')).join('')}`; return `

Run the agency day-to-day

Your approvals queue — decide here, nothing waits on Craig.
Approvals inbox ${total?`${total}`:''}
${inbox}
Client workspaces
${(CLIENTS_REAL||[]).length?CLIENTS_REAL.map((c,i)=>`
${initials2(c.n)}
${c.n}
${c.sub||c.tier} · fenced workspace
${c.st==='live'?'Live':'Active'}${c.site_url&&/proposal|\/packs\//.test(c.site_url)?'Pack ready':''}
`).join(''):(CLIENTS_ERR?`
⚠ ${cTxt(CLIENTS_ERR)}.
`:`
+
${CLIENTS_LOADED?'No clients yet':'Loading your clients…'}
Sign one from Win → pipeline
`)}
Team
Somer · owner+ add staff / freelancer
Each client fenced by partner_tag · never sees another client or the 76k.
`; } function money(){ const tierMo={GROUP:600,GROW:600,'MINI→GROW':300,'active→GROW':299,MINI:300}; const potential=(CLIENTS_REAL||[]).reduce((s,c)=>s+(tierMo[c.tier]||300),0); const mrrRaw=ANALYTICS?ANALYTICS.mrr_estimate_eur:null; return `

Packages, billing & the 50/50 split

Premium, quality-led · the best marketing on the Costa, priced for it. We don't undercut.
${eur(mrrRaw)}
Current MRR (est)
live from your workspace
${potential?('€'+potential.toLocaleString()):'—'}
Potential MRR (if all signed)
across your book, before add-ons
50/50
Split after platform fee
Craig · Somer
Service menu
${PRICING.map(p=>``).join('')}
ProductSetupMonthlyWhatEngine
${p[0]}${p[1]}${p[2]}${p[3]}${p[4]}
Lead with the free AI-Visibility Audit (quality on show) → convert to a retainer. Anchor on the flagship Full Back-Office €1,900/mo. Sell the outcome · more bookings, more reviews, found first in AI · never a discount.
`; } function initials2(n){return n.replace(/[^A-Za-z ]/g,'').split(' ').filter(Boolean).map(w=>w[0]).slice(0,2).join('').toUpperCase();} function clients(){ if(SEL_CLIENT!==null&&!CLIENTS_REAL[SEL_CLIENT])SEL_CLIENT=null; if(SEL_CLIENT!==null){ const c=CLIENTS_REAL[SEL_CLIENT],circ=2*Math.PI*30; const secKey=(c.sector&&SECTORS[c.sector])?c.sector:(/(restaurant|bar|caf|bistro|pub|kova|tibu|zanetti|india|mooney|sol )/i.test(c.n+' '+c.sub)?'hospitality':/(beaut|hair|nail|salon|bronzed|sea beauty)/i.test(c.n+' '+c.sub)?'beauty':/(jewel|watch)/i.test(c.n+' '+c.sub)?'jeweller':/(tile|floor)/i.test(c.n+' '+c.sub)?'tile':/(celebrant|wedding|marks)/i.test(c.n+' '+c.sub)?'celebrant':/(estate|propert)/i.test(c.n+' '+c.sub)?'estate':/(gym|fit|studio|freak)/i.test(c.n+' '+c.sub)?'gym':'generic'); const pb=SECTORS[secKey], intake=(c.intake&&c.intake.answers)||{}; const header=`

${c.n}

${c.sub}${c.owner!=='·'?' · '+c.owner:''}
`; if(CLIENT_TAB==='prep'){ return header+`
Before the meeting · what we already know
Visibility
${c.score}→${c.target}
Recommended tier
${c.tier}
Guaranteed outcome
${c.outcome}
Where they are: ${c.metric}
Top-1% benchmark: ${pb.benchmark}
Their pain · what to sell against from our data
${(()=>{const d=DOSSIER[c.n],ready=d&&!d.loading&&!d.error&&!d.empty&&d.name;const pains=clientPains(c,ready?d:null); return `
${pains.length?`
${pains.map(p=>`⚑ ${cTxt(p)}`).join('')}
`:`
${d&&d.loading?'Pulling the live intelligence on this client…':'Tap “Deep-research this client” below and we’ll surface their pain points.'}
`} ${ready&&d.outreach_angle?`
Lead with: ${cTxt(d.outreach_angle)}
`:''}
`;})()} ${intelPanel({n:c.n,t:(c.sub||'').split('·')[0].trim()})}
Full solution set · ready to deploy
${pb.solutions.map(s=>`
✦ ${s}
`).join('')}
Onboarding · the main asks for this sector
Sector ${Object.keys(intake).length?'captured':'not captured'}
${pb.asks.map((q,i)=>`
`).join('')}
Meeting notes · we action these straight away
${(c.intake&&Array.isArray(c.intake.notes)&&c.intake.notes.length)?`
${c.intake.notes.map(n=>`
${cTxt(n.text)}
${cTxt(n.at)}
${cTxt(n.status||'working')}
`).join('')}
`:`
Your notes land here with a status · the team picks each one up the moment you send it.
`}
The intelligence loads itself when you open this tab · capture what you learn as notes (actioned straight away) · Execute fires the build.
`; } return header+`
${c.score}→${c.target}
Visibility score · 90-day target
+${c.target-c.score} pts planned
${c.price}
Tier / pricing
${c.tier}
Guaranteed outcome (data-backed)
${c.outcome}
Where they are now
${c.metric}
90-day growth plan
${(c.plan&&c.plan.length)?c.plan.map((p,i)=>`
${i+1}${p}
`).join(''):`
Open Meeting Prep for this client's full sector solution set and the asks to capture. Goal: ${c.outcome||'set an outcome'}.
`}
${c.id?`
Manage
Status
Tier
Visibility now
Agreed price €/mo
Why this price
Changes save live to her workspace. ${c.agreed!=null ?`This client is billed €${c.agreed.toLocaleString()}/mo · their pack and your Money tab both quote this number.` :`No agreed price on record. Until you set one, the Money tab estimates them from the tier (${TIER_GUIDE[c.tier]||'€500 / €300/mo'}) and their pack shows no price. €500 is the list starter price; €300 is a legacy rate some clients are on — set what they actually pay.`}
`:''}
${c.site_url?``:''}
`; } const avg=CLIENTS_REAL.length?Math.round(CLIENTS_REAL.reduce((s,c)=>s+c.score,0)/CLIENTS_REAL.length):0; const tierMix={};CLIENTS_REAL.forEach(c=>tierMix[c.tier.split(/[ →]/)[0]]=(tierMix[c.tier.split(/[ →]/)[0]]||0)+1); const inp='padding:9px 11px;background:var(--ink);border:1px solid var(--line);border-radius:8px;color:var(--paper);font-family:var(--body);font-size:13px;width:100%'; const addForm=ADD_OPEN?`
New client
`:''; return `

Somer's clients · scored, planned & self-managed

Add, score and manage her own clients. Saved live to her workspace (${CLIENTS_REAL.length} on record).
${addForm}
${CLIENTS_REAL.length}${CLIENTS_PENDING.length?`+${CLIENTS_PENDING.length}`:''}
On the book
${CLIENTS_REAL.length} scored${CLIENTS_PENDING.length?(', '+CLIENTS_PENDING.length+' awaiting a Maps link'):''}
${avg}
Avg visibility now
low = the opportunity
${CLIENTS_REAL.filter(c=>c.st==='flagship'||/GROUP/.test(c.tier||'')).length}
Flagship / group
biggest retainers
${CLIENTS_REAL.filter(c=>c.st==='live'||c.status==='active').length}
Active clients
delivering now
Your roster · tap any client
${[['health','Needs attention'],['score','Top score'],['name','A–Z']].map(o=>``).join('')}
${(()=>{ let list=CLIENTS_REAL.map((c,i)=>({c,i})); const query=(CLIENT_Q||'').toLowerCase().trim(); if(query)list=list.filter(x=>((x.c.n||'')+' '+(x.c.sub||'')+' '+(x.c.tier||'')).toLowerCase().includes(query)); if(CLIENT_SORT==='name')list.sort((a,b)=>(a.c.n||'').localeCompare(b.c.n||'')); else if(CLIENT_SORT==='score')list.sort((a,b)=>(b.c.score||0)-(a.c.score||0)); else list.sort((a,b)=>(a.c.score||0)-(b.c.score||0)); if(!CLIENTS_REAL.length){ if(CLIENTS_ERR)return `
Couldn't load your clients
${cTxt(CLIENTS_ERR)}.
`; if(!CLIENTS_LOADED)return `
Loading your clients…
`; return `
No clients on the book yet
Add your first with + Add client above, or sign one from Win → Pipeline.
`; } if(!list.length)return `
No clients match “${cTxt(CLIENT_Q)}”. Clear the search to see all ${CLIENTS_REAL.length}.
`; return `
${list.map(({c,i})=>{const na=nextAction(c);const h=(c.score||0)<40?['gap','watch']:(c.score||0)<70?['mid','building']:['ok','strong']; return `
${initials2(c.n)}
${c.n}
${c.sub}
${c.score}→${c.target}
${h[1]}
${c.outcome||c.tier} ${na[0]} ${na[1]}
`;}).join('')}
`;})()} ${CLIENTS_PENDING.length?`
Awaiting a Maps link / handle from Somer
${CLIENTS_PENDING.map(p=>`${cTxt(p)}`).join('')}
Can't score without a confirmed listing · one Google Maps link each unlocks the audit + plan.
`:''}`; } function assetCard(a){ const st={live:'ok',available:'sky','on-disk':'mid'}[a[2]]||'sky'; const nm=a[0].replace(/'/g,''); // real href/inline-action → use it. 'on-disk' with no link is a reference doc, not a click- // able asset · no fake action. Everything else ('available' with no link) queues a real // switch-on build request instead of an alert that used to just claim it was "available". const clickable=!!a[3]||a[2]!=='on-disk'; const act=a[3]?(a[3].startsWith('http')?`onclick="window.open('${a[3]}','_blank')"`:`onclick="${a[3]}"`) :(a[2]==='on-disk'?'':`onclick="fireBuild('Switch on · ${nm}','Requested from the Library — turn this asset on for my clients',this)"`); return `
${a[0]}${a[2]}
${a[1]}
${(!a[3]&&a[2]==='on-disk')?'
In your workspace · ask Craig for the doc.
':''}
`; } function library(){ const groups=[ ['Content & Social',[ ['MASON content engine','Bilingual EN/ES posts + reels · calendar · approve → Blotato','live',"go('deliver');setSub('deliver','studio')"], ['30-day social pack','Caption + calendar + email + SMS generators per client','available',''], ['Video stack','ATTOH social + CrewOS cinematic + Somer Soul AI presenter','available',''], ['Owned content brands','MARBELLA-LIFESTYLE / EXPAT / PROPERTY · feature clients into real audiences','live','']]], ['Websites & Templates',[ ['Style Studio','74 styles + templates + sections · skin any client site','live','https://brg-style-studio.pages.dev'], ['Website builder','Presence one-pager + per-vertical site templates','live',"go('deliver');setSub('deliver','web')"], ['Built demos','T42 Calahonda + Mini India Elviria · before/after proof','available',''], ['App + Client-OS builder','Booking apps, loyalty, full vertical OS per client','live',"go('deliver');setSub('deliver','app')"]]], ['SEO & AI-Visibility',[ ['AI-Visibility Audit','Score any business + generate the client PDF · the wedge','live',"go('win');setSub('win','audit')"], ['BEACON local SEO','Local SEO + AEO/GEO + keyword tracking','live',''], ['Local SEO / GBP pack','Google Business optimisation playbook','on-disk',''], ['AI-Visibility Workbench','The scoring engine behind every audit','live','']]], ['Reviews & Reputation',[ ['ReviewForge','Auto review requests + monitor + AI responder','live',"go('deliver');setSub('deliver','studio')"], ['Top-1% “what works” feed','Live: review velocity is the #1 differentiator','live',"go('command')"], ['WhatsApp booking bot','Confirm + 24h reminder + win-back (Twilio)','available',"go('deliver');setSub('deliver','whatsapp')"]]], ['Playbooks & Best-Practice',[ ['Client Capture Playbook','The €300/mo install machine · WhatsApp front door, review moat, marketplace economics','on-disk',''], ['Per-vertical playbooks','Hospitality · Salon · Tile · Jeweller · Celebrant · the exact 90-day sprint','on-disk',''], ['Guarantee architecture','SLA + one outcome guarantee per client · legally safe, evidence-based','on-disk',''], ['Marketplace economics','Fresha/Booksy/TripAdvisor tax per vertical · billboards not homes','on-disk','']]], ['Verticals & Cross-sell',[ ['SNAP verticals','HospSnap · EstateAgentSnap · PropVault · slot in per client type','available',"go('deliver');setSub('deliver','os')"], ['BRG cross-sell rails','Formation · payroll · finance · VAULT brokerage for luxury clients','available',''], ['Client roster + plans','Your scored clients with 90-day growth plans','live',"go('clients')"], ['Brand & Preview','Set a client’s colours, show them live before publish','live',"go('deliver');setSub('deliver','brand')"]]]]; return `

Everything Somer needs to run the agency

Every content, marketing, SEO, review, playbook and vertical asset we own · wired to her clients. The better she does, the better we do.
${groups.map(g=>`
${g[0]}
${g[1].map(assetCard).join('')}
`).join('')}
live = running now · available = switch on per client · on-disk = playbook/pack in your workspace. Nothing here is a mock · it's the real arsenal.
`; } function guide(){ const tabs=[ ['Command','Your dashboard · the whole business + the 76k Costa businesses to sell to, and what’s working right now.'], ['Win','Find & sign clients · search the universe, run an AI-visibility audit, work the pipeline, launch outreach.'], ['Deliver','Build for clients · apps, websites, WhatsApp menus, full client OS, and set brand colours live.'], ['Manage','Run the day-to-day · approvals waiting, each client’s workspace, your team.'], ['Money','Your packages & pricing, plus your 50/50 split.'], ['Clients','Add, score, plan and manage your own clients · and prep for every meeting.'], ['Library','Every asset you own · content, sites, SEO, reviews, playbooks · in one place.']]; const daily=[ ['Approvals first · 2 min','Manage → anything waiting on you. Content, campaigns and builds sit here until you tick them. Clear this before anything else · it is the only queue that blocks other people.'], ['Your live clients · 5 min','Clients → open each active client. Has their content gone out? Any new reviews? Anything they asked for last time still open? Update the status while you are in there · a stale roster is a roster you stop trusting.'], ['Feed the pipeline · 5 min','WinLead Universe. Filter to one town and one trade, pick three businesses, Run audit on each. Send the PDF. Three a day is fifteen a week and it compounds · chasing fifty in one burst does not.'], ['One follow-up · 3 min','WinPipeline. Anyone sent an audit more than three days ago and gone quiet gets one message. Most signings happen on the second contact, not the first.']]; const recipes=[ ['🏊 A worked example · how The Pool Guyz actually ran',['Clients → The Pool Guyz. Everything below already exists on this record · open it and follow the shape for any new client.','What we found: their site had not been touched since 2021, no Instagram, no TikTok, no way to book them. Their competitors had none of those either · that gap was the pitch.','What we built before asking for anything: a live preview site (in site_url on their record) and a working booking engine · six services, three technician routes. Showing beats describing.','The number that closed it: 32,607 registered holiday lets sit in their three towns, and 72 property managers hold ten or more properties each. One manager conversation carries dozens of pools. That came from our data, not from them.','The price: €500/mo, setup waived — they are our first pool client and the reference case for the trade. That number lives on their record as the agreed price, which is what the Money tab bills and what their pack quotes. €500 is the list starter price; some earlier clients sit on a €300 legacy rate, which is exactly why the price belongs on the client, not on the tier.','What we did NOT promise: no lead guarantees, no ad spend, no contact list we do not hold. Read the proposal on their record · that honesty is the sales method, not a limitation of it.']], ['➕ Add a new client',['Go to Clients+ Add client.','Type their name, location or @handle, pick a tier.','Add a visibility score (or leave 0 · the audit sets it) and a 90-day target.','Save · they appear in your roster straight away, stored to your workspace.']], ['🗒️ Prep for a meeting · do this before EVERY meeting',['Open the client → the Meeting Prep tab.','Read “What we already know” · their score, the top-1% benchmark, and exactly where they are today.','Look at the Full solution set · that’s what you’re selling them.','Work down the sector asks and capture what you learn.','Save meeting brief · it’s kept for next time.','When they say yes, hit Execute the plan → to start the build.']], ['🔎 Show a client their AI-visibility score',['Go to WinLead Universe or Pipeline.','Find the business → Run audit.','Read the score + the biggest gaps.','Generate client PDF and send it · that’s your door-opener.']], ['🎨 Set a client’s brand colours (show them live)',['Go to DeliverBrand & Preview.','Move the colour pickers or tap a ready palette.','The website + app recolour instantly · share your screen with the client.','Apply to build when they love it.']], ['🛠️ Build something for a client',['Go to Deliver → pick App / Website / WhatsApp menu / Client OS.','Choose a template → Build. That creates a job Craig approves before anything goes live.','Nothing publishes until it’s approved · you can’t break anything.']], ['📈 Manage a client as things move',['Open the client → Manage.','Set the status (prospect → active → won).','Change their tier or update the visibility score as you improve it.','It all saves live.']]]; return `

How to run your agency from here

Your command centre. This is everything you need · no tech skills required. Read it once, keep it handy.
The one loop that runs the whole business
Find → Know → Meet → Plan → Execute → Manage → Grow. Find a business (Win), let the system tell you everything (Meeting Prep), walk in ready, agree the plan, hit Execute, then manage and grow them in Clients. The better each client does, the more you earn · you’re 50/50 with us.
Your day · 15 minutes, in this order
Do these four every working day and the business runs itself between them. The order matters: clear what blocks other people, then look after who already pays you, then go hunting. Most agencies do it backwards and wonder why clients churn.
${daily.map((d,i)=>`
${i+1}
${d[0]}
${d[1]}
`).join('')}
Your main workspaces
${tabs.map(t=>`
${t[0]}
${t[1]}
`).join('')}
How to · step by step
${recipes.map(r=>`
${r[0]}
${r[1].map((s,i)=>`
${i+1}${s}
`).join('')}
`).join('')}
How you sell (the golden rules)
The offer ladder
Lead with the free AI-Visibility Audit (proves you know more than they do) → convert to a retainer → anchor on the flagship Full Back-Office €1,900/mo. See Money for the full menu.
Two guarantees, never a discount
Every client gets an SLA guarantee (what we deliver monthly) + one outcome guarantee (e.g. “found in 90 days”). Sell the outcome · more bookings, more reviews, found first in AI · never the price. You’re the best on the Costa, priced for it.
Who does what
On you
Win clients, run the meetings, capture the asks, set their colours, keep their status up to date. You’re the face and the local expert.
On us (behind the scenes)
All the building, content, SEO, reviews, data, audits and automations. Craig approves anything that spends money or sends externally · so nothing goes wrong on your watch.
Stuck on anything? Drop it in a client's Meeting notes or raise a request from Manage — the team picks it up and it lands in your approvals inbox. This portal grows with you; everything you do here is saved to your workspace and lives online.
`; } /* ===== back-office departments: live analytics + Finance (get paid) + Reports (retention) ===== */ let ANALYTICS=null,ANALYTICS_ERR=null; /* Sends the signed-in user's token: costa-analytics returns the client roster, MRR and the revenue split, so it is allowlist-gated like every other costa-* function (it was open to anyone with the URL until 2026-08-04). */ async function loadAnalytics(){try{const r=await fetch(SB_URL+'/functions/v1/costa-analytics',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+(USER_TOKEN||SB_ANON),'apikey':SB_ANON}}); if(!r.ok)throw new Error('HTTP '+r.status); ANALYTICS=await r.json();ANALYTICS_ERR=null; if(ANALYTICS&&ANALYTICS.universe){UNIV.total=ANALYTICS.universe.total||UNIV.total;UNIV.noSite=ANALYTICS.universe.no_website||UNIV.noSite;} }catch(e){ANALYTICS_ERR=String(e.message||e);} if(V==='command'||V==='finance'||V==='win'||V==='money')render();} /* ===== real approvals inbox · costa-approvals edge function · Somer decides, never waits on Craig ===== */ let APPROVALS=null,APPROVALS_ERR=null,APPROVALS_BUSY=null; async function loadApprovals(){try{const r=await fetch(SB_URL+'/functions/v1/costa-approvals',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+(USER_TOKEN||SB_ANON),'apikey':SB_ANON},body:JSON.stringify({action:'list'})});const d=await r.json();if(r.ok){APPROVALS=d;APPROVALS_ERR=null;}else{APPROVALS_ERR=d.error||'Could not load approvals';}}catch(e){APPROVALS_ERR=String(e.message||e);}if(V==='manage'||V==='command')render();} async function decideApproval(kind,id,verdict){APPROVALS_BUSY=id;render(); try{const r=await fetch(SB_URL+'/functions/v1/costa-approvals',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+(USER_TOKEN||SB_ANON),'apikey':SB_ANON},body:JSON.stringify({action:'decide',kind,id,verdict})}); if(!r.ok){const d=await r.json().catch(()=>({}));toast('Could not save your decision: '+(d.error||r.status),true);} else{toast('✓ Decision saved · '+verdict);}} catch(e){toast('Network error saving your decision: '+e.message,true);} APPROVALS_BUSY=null;await loadApprovals();} function finance(){ const a=ANALYTICS; if(!a)return `

Finance · get paid

${ANALYTICS_ERR ?`
What's billed, what's paid, your split.
Couldn't load your live figures
${cTxt(ANALYTICS_ERR)}.
` :`
Pulling your live figures…
`}`; const cl=a.clients||{},mrrRaw=a.mrr_estimate_eur; const half=(mrrRaw!=null&&!isNaN(mrrRaw))?('€'+Math.round(mrrRaw/2).toLocaleString()):'—'; /* The Monthly column shows the AGREED price where there is one, and says "est." where the number is still assumed from the tier. Before 2026-08-04 it printed the tier guide's first figure (€500 for MINI) as if it were billed, while the MRR KPI above counted €300 for the same client — the table contradicted the headline in one glance, and both contradicted the client's own pack. */ const fbNames=new Set(((a.pricing&&a.pricing.needs_agreed_price)||[]).map(x=>x.name)); const billRows=(CLIENTS_REAL||[]).length?CLIENTS_REAL.map(c=>{const nm=(c.n||'').replace(/'/g,''); const monthly=c.agreed!=null?`€${c.agreed.toLocaleString()}`:`${((c.price||'·').split('/')[0]||'·').trim()} est.`; return `${c.n}${fbNames.has(c.n)?' set price':''}${c.tier}${c.status||c.st||''}${monthly}`;}).join(''):`No clients on the book yet — add one in the Clients tab and billing lines appear here.`; const px=a.pricing||{}; const priceGap=px.fallback_clients?`
${px.fallback_clients} client${px.fallback_clients===1?'':'s'} billed on an estimate
€${(px.from_agreed_price_eur||0).toLocaleString()} of the MRR above is a real agreed price. The other €${(px.from_tier_fallback_eur||0).toLocaleString()} is assumed from the tier and may be wrong in either direction — €500 is the list starter price, €300 a legacy rate some clients are on. Set what each one actually pays on their record: ${((px.needs_agreed_price)||[]).map(x=>x.name).join(' · ')}.
`:''; return `

Finance · what's billed, what's paid, your split

Live from your workspace. Premium, outcome-led — we never discount.
${eur(mrrRaw)}
Live MRR${px.fallback_clients?' (est)':''}
${num(cl.live)} active clients${px.fallback_clients?` · ${px.fallback_clients} on an estimated price`:' · all on an agreed price'}
${num(cl.total)}
Clients on record
${num(cl.by_status&&cl.by_status.prospect)} prospects in pipeline
${half}
Your half / month
50/50 after platform fee
${num(cl.with_site)}
Client sites live
delivering now
Billing · per client
${billRows}
ClientTierStatusMonthly
${priceGap}
Invoicing + collections run through the get-paid engine · Stripe Payment Links + Apple/Google Pay (fastest-payable). Nothing charges or sends until Craig approves it.
`; } function reports(){ return `

Reports · prove it, keep them

The monthly results report is your retention engine — clients who see the wins, stay. Each one leads with what the free-data machine found.
Generate a client report
${(CLIENTS_REAL||[]).length?`
${CLIENTS_REAL.map((c,i)=>`
${c.n}
${cTxt((c.sub||'').split('·')[0])}
`).join('')}
`:`
${CLIENTS_ERR?('⚠ '+cTxt(CLIENTS_ERR)):'No clients on the book yet — add one in the Clients tab and their monthly report opens from here.'}
`}
Opens the client's audit + full dossier (reviews, solar, owner, licence) → Generate client PDF gives the branded, golden-hour report to send.
`; } function onboarding(){ const prospects=CLIENTS_REAL.filter(c=>(c.status||'')==='prospect'||['new','niche','luxury','high-proof','case-study'].includes(c.st)); return `

Onboarding · proposal → contract → deposit → go

Turn a yes into a paying client in four steps. Everything's drafted for you; Craig approves anything that sends or charges.
${['1 · Proposal|Branded, from their audit + sector solution set','2 · Contract|SLA + one outcome guarantee, legally safe','3 · Deposit|Fastest-payable Stripe link · Apple/Google Pay','4 · Kick-off|Fire the build · client goes live'].map(s=>{const[a,b]=s.split('|');return `
${a}
${b}
`}).join('')}
Clients to onboard ${prospects.length}
${prospects.length?prospects.map(c=>{const nm=(c.n||'').replace(/'/g,'');return ``;}).join(''):``}
ClientTierTarget outcome
${c.n}${c.tier}${cTxt((c.outcome||'').slice(0,58))}
${CLIENTS_ERR?('⚠ '+cTxt(CLIENTS_ERR)):'No prospects to onboard right now — mark a client as prospect in the Clients tab and they queue here.'}
Proposals reuse the Meeting-Prep solution set (Clients tab). Contract = SLA + one outcome guarantee. Deposit via Stripe Payment Links. Nothing sends or charges until Craig ticks it.
`; } function retention(){ const active=CLIENTS_REAL.filter(c=>(c.status||'')==='active'||['live','flagship'].includes(c.st)); const atrisk=CLIENTS_REAL.filter(c=>(c.score||0)<40); return `

Retention · keep them, grow them

Clients who see the wins, stay. Watch health, prove results monthly, renew + upsell before they wobble.
${active.length}
Active clients
delivering now
${atrisk.length}
Watch · low visibility
need a proof-point fast
${CLIENTS_REAL.length}
On the book
total relationships
100%
Review-response SLA target
the retention standard
Client health
${(CLIENTS_REAL||[]).length?CLIENTS_REAL.map((c,i)=>{const nm=(c.n||'').replace(/'/g,'');const h=(c.score||0)<40?['gap','watch']:(c.score||0)<70?['mid','steady']:['ok','strong'];return ``;}).join(''):``}
ClientVisibility now → targetHealth
${c.n}${c.score||0}→${c.target||60}${h[1]}
${CLIENTS_ERR?('⚠ '+cTxt(CLIENTS_ERR)):'No clients on the book yet — add one in the Clients tab to track health here.'}
Health = current visibility vs 90-day target. Low = send a proof-point fast. Every client: monthly report + 100% review response + rebook automation (the FELIX health loop).
`; } /* ===== Leads · "Where to Attack" · costa-leads edge function (auth-gated, tenant costa) ===== Somer's real prospect intelligence — TOP-FIRST, broken down by tier/vertical/town so ~105k never overwhelms. Never dumps the universe: 25 ranked leads at a time, facet counts on the chips. No PII is baked into view-source; every field comes live from the authed function, missing = "not held". */ let LEADS_ROWS=[], LEADS_FACETS=null, LEADS_TOTAL=0, LEADS_ERR=null, LEADS_LOADING=false, LEADS_LOADED=false; const LEADS_LIMIT=25; let LEADS_FILTER={tier:'hot',vertical:null,municipality:null}, LEADS_OFFSET=0; let LEADS_REPORTS={}, LEADS_REPORT_BUSY=null; const VERTICAL_LABELS={restaurant:'Restaurants',bar:'Bars',cafe:'Cafés',hotel:'Hotels',fast_food:'Fast food', beautician:'Beauty',healthcare:'Healthcare',estate_agent:'Estate agents',retail:'Retail',clothes:'Clothing', nightclub:'Nightclubs',pub:'Pubs',gym:'Gyms',other:'Other',non_commercial:'Non-commercial'}; const vLabel=v=>VERTICAL_LABELS[v]||(v?String(v).replace(/_/g,' ').replace(/\b\w/g,c=>c.toUpperCase()):'—'); function leadDate(s){if(!s)return null;const d=new Date(s);return isNaN(d)?null:d.toLocaleDateString('en-GB',{day:'numeric',month:'short',year:'numeric'});} function telHref(p){return String(p||'').replace(/[^\d+]/g,'');} function safeUrl(u){u=String(u||'').trim();if(!u)return null;if(!/^https?:\/\//i.test(u))u='https://'+u.replace(/^\/+/,'');return /^https?:\/\/[^\s"'<>]+$/i.test(u)?u:null;} async function leadsFetch(payload){ const r=await fetch(SB_URL+'/functions/v1/costa-leads',{method:'POST', headers:{'Content-Type':'application/json','Authorization':'Bearer '+(USER_TOKEN||SB_ANON),'apikey':SB_ANON}, body:JSON.stringify(payload)}); return {ok:r.ok,status:r.status,data:await r.json().catch(()=>null)};} async function loadLeads(reset){ if(LEADS_LOADING)return; LEADS_LOADING=true;LEADS_ERR=null; if(reset){LEADS_OFFSET=0;LEADS_ROWS=[];} if(V==='leads')render(); const f=LEADS_FILTER; const {ok,status,data}=await leadsFetch({action:'list',tier:f.tier,vertical:f.vertical,municipality:f.municipality,limit:LEADS_LIMIT,offset:LEADS_OFFSET}); if(ok&&data&&Array.isArray(data.leads)){ LEADS_ROWS=LEADS_ROWS.concat(data.leads); LEADS_TOTAL=data.total||LEADS_ROWS.length; if(data.facets)LEADS_FACETS=data.facets; LEADS_ERR=null; }else{LEADS_ERR=(data&&data.error)||('Could not load your leads'+(status?' ('+status+')':''));} LEADS_LOADING=false;LEADS_LOADED=true; if(V==='leads')render();} function loadMoreLeads(){LEADS_OFFSET+=LEADS_LIMIT;loadLeads(false);} function setLeadTier(t){if(LEADS_FILTER.tier===t)return;LEADS_FILTER.tier=t;loadLeads(true);} function setLeadVertical(v){LEADS_FILTER.vertical=(LEADS_FILTER.vertical===v?null:v);loadLeads(true);} function setLeadMuni(m){LEADS_FILTER.municipality=(LEADS_FILTER.municipality===m?null:m);loadLeads(true);} async function pullLeadReport(id){ if(LEADS_REPORT_BUSY)return; LEADS_REPORT_BUSY=id;LEADS_REPORTS[id]={loading:true};render(); const {ok,data}=await leadsFetch({action:'report',id}); if(ok&&data&&data.ok){LEADS_REPORTS[id]={text:data.report_text||'',score:data.gate_total,ship:data.ship_ready};} else{LEADS_REPORTS[id]={error:(data&&(data.error||data.reason))||'Report unavailable right now'};} LEADS_REPORT_BUSY=null;render();} function leadContactBit(label,val,href,conf){ if(!val)return `${label}: not held`; const inner=href?`${cTxt(val)}`:cTxt(val); return `${label}: ${inner}${conf?` · confirmed ${cTxt(conf)}`:''}`;} function leadCard(l){ const score=l.opportunity_score==null?'—':l.opportunity_score; const scoreCol=(l.opportunity_score>=85)?'var(--coral2)':(l.opportunity_score>=70?'var(--warn)':'var(--sky)'); const gaps=Array.isArray(l.gaps)?l.gaps:[]; const prods=Array.isArray(l.best_products)?l.best_products:[]; const contactDate=leadDate(l.contact_backfilled_at), siteDate=leadDate(l.website_checked_at), socialDate=leadDate(l.social_at); const webVal=l.website?l.website:(l.website_alive===false?'website is DOWN':null); const webHref=l.website?safeUrl(l.website):null; const rating=(l.google_rating!=null)?`${cTxt(l.google_rating)}★${l.reviews_count!=null?` · ${fmt(l.reviews_count)} reviews`:''}`:'no Google rating held'; const rep=LEADS_REPORTS[l.id]; let repBlock=''; if(rep){ if(rep.loading)repBlock=`
Pulling the Digital Presence & Growth Audit…
`; else if(rep.error)repBlock=`
⚠ ${cTxt(rep.error)}
`; else repBlock=`
Digital Presence & Growth Audit gate ${rep.score==null?'·':cTxt(rep.score)}/100${rep.ship?' · ship-ready':''}
${cTxt(rep.text)||'No report text returned.'}
`; } return `
${cTxt(l.name)||'Unnamed venue'}
${cTxt(vLabel(l.vertical))} · ${cTxt(l.municipality)||'—'}
${cTxt(score)}
opportunity
${gaps.length?`
Their pain
${gaps.map(g=>`⚑ ${cTxt(g)}`).join('')}
`:''} ${prods.length?`
What Somer does
${prods.map(p=>`✦ ${cTxt(p)}`).join('')}
`:''}
Contact · what we hold
${leadContactBit('Phone',l.phone,l.phone?'tel:'+telHref(l.phone):null,contactDate)} ${leadContactBit('Web',webVal,webHref,siteDate)} ${leadContactBit('IG',l.instagram,safeUrl(l.instagram),socialDate)} ${leadContactBit('FB',l.facebook,safeUrl(l.facebook),socialDate)} ${cTxt(rating)}
${l.phone?``:``}
${repBlock}
`;} function facetChips(facet,list,activeKey,labeler,setter){ if(!Array.isArray(list)||!list.length)return ''; return list.map(x=>``).join('');} function leads(){ if(!LEADS_LOADED&&!LEADS_LOADING&&!LEADS_ERR)loadLeads(true); const f=LEADS_FILTER, fac=LEADS_FACETS||{}; const tierList=Array.isArray(fac.tier)?fac.tier.slice():[]; const tierOrder={hot:0,warm:1,cold:2}; tierList.sort((a,b)=>(tierOrder[a.k]??9)-(tierOrder[b.k]??9)); const tierChips=tierList.map(x=>``).join(''); const vertChips=facetChips('vertical',fac.vertical,f.vertical,vLabel,'setLeadVertical'); const muniChips=facetChips('municipality',fac.municipality,f.municipality,(m)=>m,'setLeadMuni'); let body; if(LEADS_ERR&&!LEADS_ROWS.length){ body=`
Couldn't load your leads
${cTxt(LEADS_ERR)}. This list is private to your workspace — make sure you're signed in.
`; }else if(!LEADS_LOADED&&LEADS_LOADING&&!LEADS_ROWS.length){ body=`
Ranking your best prospects…
`; }else if(!LEADS_ROWS.length){ body=`
No leads in this filter
Nothing matches ${cTxt(f.tier||'all')}${f.vertical?' · '+cTxt(vLabel(f.vertical)):''}${f.municipality?' · '+cTxt(f.municipality):''}. Widen the filters above.
`; }else{ const more=LEADS_ROWS.length${LEADS_ROWS.map(leadCard).join('')}
Showing ${fmt(LEADS_ROWS.length)} of ${fmt(LEADS_TOTAL)} ${cTxt(f.tier||'all')} lead${LEADS_TOTAL===1?'':'s'}${f.vertical?' · '+cTxt(vLabel(f.vertical)):''}${f.municipality?' · '+cTxt(f.municipality):''} — highest opportunity first.
${more?`
`:''}`; } return `

Where to attack

Your best prospects, hottest first — broken down so the whole universe never overwhelms. Filter, then work the top of each list.
${tierChips||`Loading tiers…`}
${vertChips?`
${vertChips}
`:''} ${muniChips?`
${muniChips}
`:''} ${body}`;} const VIEWS={command,leads,win,deliver,finance,reports,onboarding,retention,manage,money,clients,library,guide}; function render(){$('#main').innerHTML=VIEWS[V]();document.querySelectorAll('#nav button').forEach(b=>{const on=b.dataset.v===V;b.classList.toggle('on',on);if(on)b.setAttribute('aria-current','page');else b.removeAttribute('aria-current');});} function go(v){V=v;render();window.scrollTo({top:0,behavior:'smooth'});} function setSub(t,s){SUB[t]=s;render();}function setF(f){FILTER=f;render();} document.querySelectorAll('#nav button').forEach(b=>b.onclick=()=>go(b.dataset.v)); if(consumeOAuthRedirect()||loadSession()){render();loadClients();loadAnalytics();loadApprovals();}else{loginScreen();}