☐STOPPED BEFORE GENERATING 2026-08-26, and the reason is a conflict between two of your own lists.▸2026-08-26
Three findings, in the order they blocked the work. (1) The premise is still true. Silva PB's live GBP (runtime/seo/sites/172662/gbp.json) still lists exactly ten services including CoolSculpting, and still omits testosterone/pellet therapy, mental wellness, ED and sexual wellness, IV therapy, cancer screening and whole-body MRI. Generating from GBP today would still produce a CoolSculpting page. (2) Two operator-authoritative lists disagree about CoolSculpting. This entry (2026-06-26) says "they do NOT offer it, they run EmSculpt NEO". process/features/seo-audit/references/silva-master-services.md (operator-provided 2026-06-25, one day earlier, and far more detailed) lists "Body Contouring (CoolSculpting, EMSCULPT NEO, EMFACE Eye and Neck)". One of the two is wrong and I will not pick. Generating a page for a treatment a medical client does not perform is exactly the class of error worth stopping for. Needs one sentence: does Silva offer CoolSculpting or not. (3) The capability to drive generation from an operator list does not exist. app/api/seo/sites/[oid]/service-registry/route.ts says so in its own header: "Read-only assembly for now (the source data stays authoritative); persistence/edit is phase 2." Every generation path reads services from GBP via extractServiceCatalog. There is nowhere to PUT an operator-authoritative list, so "drive generation from this list, not GBP" is not a run, it is a build: persist the registry, make the generator read it, and only then the city-scoped slug rule this entry specifies (botox-fillers-pacific-beach, because PB and Chula Vista 2596 share one WordPress site and create-page upserts by slug) has somewhere to live. Nothing was generated, staged, drafted or pushed. ORIGINAL: Silva Pacific Beach (oid 172662) — use OPERATOR-AUTHORITATIVE service list, not GBP (2026-06-26). GBP-derived alignment is incomplete + WRONG: it lists "CoolSculpting" (they do NOT offer it — they run EmSculpt NEO) and misses 5 real services. Canonical PB services: Bioidentical Hormones; Testosterone & Pellet Therapy; Weight Loss Options and Consulting; Mental Wellness; EmSculpt NEO & Body Contouring; Botox & Fillers; ED & Sexual Wellness & EMFEMME; DNA Testing & Customized Care; IV Therapy & B12 Shots; Cancer Screening - Blood Test; Whole Body MRI Testing - Referral Required; Platelet Rich Plasma Therapy. NOTE: PB shares ONE WordPress site (drmelindasilva.com) with Chula Vista (oid 2596) → service-page slugs must be city-scoped (e.g. botox-fillers-pacific-beach) to avoid slug collisions, since create-page upserts by slug. Drive generation from this list (not GBP); do NOT generate a CoolSculpting page.
☐#6 durable parallel queue — Option C plan (needs operator steer)▸
plan at process/general-plans/active/db-backed-jobs-queue_PLAN_04-07-26.md. Finding: maxConcurrent:2 is an artificial in-process cap, NOT a durability limit; DB-primary would violate "DB never SoT for critical flows". Recommend Option C: files stay authoritative, lift concurrency with per-kind caps (intake/client-build pinned to 1), best-effort DB mirror for visibility. 5 open questions in the plan.
☐Beach Physio — NOT a go-live; it's serving-but-starved (re-audited 2026-07-24)▸2026-07-24
. CORRECTION: the 3 B2 campaigns (Near Me / Brand / Locations, $25/day each) in GA 9316408200 are ALREADY ENABLED — groups + ads enabled underneath, primaryStatus=ELIGIBLE with no blocking reasons, conversions clean (Server-Upload primary + Calls-from-ads primary, no double-count). So there is nothing to "enable." The real state: impression-starved — 17 impressions total in 7 days, 0 clicks, $0 spend, in a tiny Carpinteria market on MANUAL_CPC/TARGET_SPEND (not a tCPA-no-data trap). Nothing is broken. REMAINING (operator decision, low stakes): if you want volume there it's a bid bump / broader match — otherwise monitor. Per operator rule 2026-07-24, I never unpause/enable ads unprompted anyway (moot here — already enabled). Evidence: structure-snapshot + ad-health, this session.
☐Valley Insurance — partial go-live in progress (re-audited 2026-07-24)▸2026-07-24
. CURRENT STATE in GA 7032156594: Near Me (24027161889) is LIVE and spending ($18.43/7d). Locations (24027161919) + Brand (24032201465) are still PAUSED (groups+ads paused underneath); RSA (23988518351) paused. Enabling those is OPERATOR-ONLY (never-unpause rule 2026-07-24) — I will not initiate it. LP: quote-valleyinsurance.pages.dev is a clean Builder page; a structured-regen dry-run 2026-07-24 passed ALL 7 gates → the old "score 62, consider regen" note is STALE, LP is not a blocker. CONVERSION FINDING (corrected): the conversion-audit found exactly ONE genuine duplicate — Calls from ads (1) (AD_CALL, …7694318637) is a second identical AD_CALL to downgrade → secondary. The other 5 primaries are LEGITIMATELY DISTINCT sources (operator confirmed 2026-07-24: page lead-form WEBPAGE / Google lead-form extension / ad-call / website-call / click-to-call are different conversion types — do NOT dedupe them). 1 primary is Google-managed = UI-only. REMAINING (operator-gated mutations, suggest-then-confirm): (1) downgrade the one duplicate AD_CALL; (2) enable Locations/Brand when YOU decide. Original AUTO-ONLY trimming (serviceTypes ["Auto Insurance"], AAAble keyword port, "cheap" negative removed) still stands.
☐~~GitHub Actions billing~~ — folded into the single entry above (CI now DISABLED + push tr…▸
~~GitHub Actions billing~~ — folded into the single entry above (CI now DISABLED + push trigger narrowed, so it is quiet; payment still outstanding).
☐Client service lists contain offer/price claims▸2026-07-19
(raised 2026-07-19): "Free In-Home Estimate" (Angel Touch kitchen), "Free Written Quotes & Virtual Walkthroughs" (Quantum office-commercial), "Free Written Quotes" (Quantum long-distance) are stored as SERVICES so they render as service card titles. Client-supplied data, not generated by us, so nothing was deleted — but same claim class we removed from generated copy. Operator decision on whether to clean the source lists.
☐Who published Kothmann's 28 drafts?▸2026-07-28
All 28 went draft→publish 2026-07-28 evening outside our tooling (content hashes unchanged, only status moved). If operator did it in WP admin: fine, footers since repaired live. If not: investigate. Also: 404 hub-scorer synonym blindness (car vs auto slugs) + stale service-alignment covered-by mapping (Health insurance → dead hext URL) are open assistant-side fixes.
☐Three follow-ups from 2470 ops wave (29-07)▸
(1) routes.js:1236 404-recovery ?publish=1 sends status which create-page IGNORES on update — publishing an existing draft silently no-ops; fix to post_status (same success-on-no-op class as update-page content). (2) Client WP plugin related-links endpoint fatals AFTER saving (Ratesight_Logger::log() undefined, class-ratesight-related-links.php:137) — 194 real links wrote 0 fix-log rows; needs plugin fix + add to plugin spec. (3) sameAs delivery gap: snippet emits Organization schema only on schema_missing pages; Yoast-only sites (no Org node anywhere) get NOTHING — recorded-applied with zero live effect on 2470; needs an "Org node absent everywhere" hook or plugin write_entity_schema.
☐DEFECT (found 2026-08-26, NOT fixed — it needs an operator call): two client records share ONE Search Console property, so Silva's search data is counted twice in every fleet aggregate.▸2026-08-26
OID 172662 ("drmelindasilva.com") and OID 2596 ("Melinda Silva, MD Anti-Aging & Wellness") are both joined to sc-domain:drmelindasilva.com, with byte-identical row counts (719,109 each) and therefore byte-identical metrics — which is exactly why they appear as two indistinguishable rows in the outcomes table above. A fleet-wide sweep confirms this is the ONLY shared property (select property, count(distinct ratesight_id) ... having count(*)>1 returns one row, 42,088 clicks). Consequences: every fleet GSC total double-counts this client, and any per-client SEO verdict for those two OIDs is one verdict wearing two names. Not machine-fixable: both OIDs are legitimately distinct clients (2026-07-30 precedent — acting on a "duplicate" here would have destroyed Silva Pacific Beach, #1542), so WHICH OID owns the domain property, or whether the two need separate URL-prefix properties, is a decision about the client relationship and not one to derive. Needs from you: one sentence saying which OID owns sc-domain:drmelindasilva.com, or that both should move to per-site URL-prefix properties.
☐Serve a real robots.txt on dashboard.ratesight.com▸
(currently 302s to CF Access login = allow-all by accident): OPERATOR adds a Cloudflare Access bypass rule for /robots.txt, then we add public/robots.txt with explicit Allow for /ratesight-seo.js + /api/seo/snippet/* and Disallow for app pages. Snippet infra itself verified Googlebot-fetchable 29-07.
☐Search Console render check (operator, 5 min)▸
URL-inspect kothmanninsurance.com homepage → "View crawled page" → confirm rendered HTML carries applied title/meta/alt. Last unverifiable link in JS-delivered fixes.
☐DRAFTED 2026-08-26, awaiting your read: `process/features/seo-audit/reports/KOTHMANN-CONTENT-REWRITE-DRAFT_26-08-26.md`.▸2026-08-26
Both flags re-verified against the live site the same day before writing a word. (1) Confirmed and quantified: four of the eight H2s on /health-insurance/ sell home insurance, including "A Homeowners Insurance Agency That Sweats the Details" and "Contact R.D. Kothmann Insurance for a Free Home Insurance Quote", and the body under them reads "we work tirelessly for each client to find home insurance". The two paragraphs that ARE about health are filler with a broken hyphen and an unsourced national statistic. Full replacement body drafted, with the three home-insurance sections marked for deletion rather than rewriting, since each already exists on /home-insurance/. Claims are held to what their own site already publishes: no carrier count, no savings figure, no rates claim, and one marked operator slot if the agency will confirm its health carriers. (2) All three metas rewritten under 160 characters, each naming the product, Texas and the phone. /car-insurance/ current meta never says "car" or "Texas" and is duplicated verbatim as the page first paragraph; /business-owner-package/ opens on a fragment that does not parse and claims we put clients "above profits". Nothing published, queued or pushed, and the body half CANNOT be pushed by our tooling anyway, because wp-apply lists content in its fields while the plugin update-page silently ignores it (the open item two lines above). Worth knowing before anyone grades this: OID 2470 averages 0.1 clicks and 44 impressions a day, so this is a correctness fix and not a traffic play. ORIGINAL: Kothmann content flags from title wave (29-07): (1) /health-insurance/ BODY is copy-pasted home-insurance prose under a Health Insurance H1 — real content defect, needs a rewrite pass (page just got a proper title+meta). (2) Weak meta descriptions on /car-insurance/ (never says car insurance or Texas), /home-insurance/, /business-owner-package/ (generic brand copy) — in-scope for a description pass if approved.
☐Post-"approve all" residue (29-07)▸2026-08-26
(1) 3 GBP curations HELD pending structured-item preservation (2470: 7 structured, 174637: 1, 2596: 2) — apply after the planner fix deploys + fresh dry-runs. (2) DONE 2026-08-26 — all three re-run and verified, and the empty-city root cause was already gone. The city ladder now resolves each of them from the client's own GBP without any config change: onetwotree Atlanta, GA (gbp-service-area), brentwood 37064 (gbp-postal-code), confidently.fit Cary, NC (gbp-service-area), and every seeded set carries real geo terms again ("tree stump grinding marietta", "custom blinds franklin tn", "volleyball coach raleigh"). Pre-flighted with persist:false first: drops 0 on all three, so no tracked keyword could be lost and the re-seed-resets-history hazard did not apply. Persisted, then read back from /rank/targets: 13 / 12 / 22 keywords, all cadence: weekly, all seededAt: 2026-08-26T01:32Z. 174697 gained 2 keywords and lost none; the other two were exact no-ops on the keyword set. No new spend: all three already had local targets and the route correctly refused to duplicate them. (3) Plugin repo fixes accumulating for next plugin release: Ratesight_Logger::log fatal in related-links, dry_run ignored on update-page, media-alt route, 410 support, post_status publish contract. (4) Silva GBP: 2 marketing paragraphs stored as service names + 7 create_page recs from categories — revisit after cleanup wave's cleanService gate.
☐Regen hard-crashes on transient assistant ECONNREFUSED▸2026-07-29
(2026-07-29, seen 3x): an unguarded assistant call in structuredRegen (client-build-lp-regen.ts ~1163) throws fetch failed when :9122 flaps, killing the whole regen run instead of degrading (the fill closure and brand-kit fetch already degrade; this call does not). Wrap with retry-once + graceful degrade, matching the never-hard-depend convention. Also worth checking WHY the assistant flaps briefly (watchdog restarts? journal around 14:0x and 04:0x UTC).
☐Client-build avatar step needs transient-window retry▸2026-07-29
(2026-07-29): three operator-driven Newman's builds 16:26-16:31 UTC all failed at avatar-generate — the assistant's Claude CLI returned exit 1 with a constant 49-byte error for EVERYTHING (avatar + lead-score) during 16:25-17:27, then recovered on its own (verified live 18:06, suggest-offer exit 0). Upstream transient (rate/overload window), not config. The build marks the whole service failed on one avatar attempt; add one retry-after-delay at the avatar step (same never-hard-depend convention as the regen fix). ALSO: lead-score failed at 17:27 in the same window — check runtime/lead-scores for a gap and re-score any leads that arrived 16:25-17:27.
☐share-token self-check tests wrong property▸
(found 29-07, PR #1470 review): Node's lenient base64 drops an appended trailing char (decodes to same claim) — NOT a forgery vector (GCM tag verified), but the inline assertion is misleading; fix in its own PR since the file is on the live partner-link path.
☐localRank in CORE_SECTIONS?▸
Product decision: should a missing local-rank target raise a setup finding in Connected Audit (recommend YES for maps-first clients); changes audit scoring — Wave 2 or standalone.
☐Playbook upstream: aggregator produced 0 ripe cells▸
(29-07): tools cron aggregate-playbook-outcomes reports cells:0 / ripeRowsTotal:0 against 193 chatbot + 263 voice rows — the LP question-type lane sources nothing; ripening logic needs its own look.
☐GitHub Actions is not running — billing.▸2026-07-30
Every check in every repo fails at job start with "recent account payments have failed or your spending limit needs to be increased". Trunk itself is red. All 2026-07-30 merge verification was local. Fix before the next sweep, or CI stays decorative.
☐Remaining PR backlog, verdicts recorded▸2026-08-26
dashboard issue #1513 (13 PRs; #694 9/10 symbols unmerged, #1026 8/10, #720 7/10, #689 and #1201 6/10 are the real unmerged work; #1201 is ALREADY LANDED — verified 2026-08-26: all five of its files are on main (lib/lead-flow-check.ts, lib/lead-flow-collect.ts, app/api/leads/flow-check/route.ts, its test and probe script) and the live route answers 400-on-missing-params, not 404. GitHub still shows it MERGED into fix/conversion-upload-monitor; its content reached main by another path. The "cheapest win / merges clean" note is stale — there is nothing to land) and assistant issue #260 (16 PRs in 5 stacks; #171 loopback-bind is the smallest high-value one; assistant #168/#169 are the other half of #1511's GTM residue).
☐START HERE 2026-07-30 →▸2026-07-30
full session handoff at process/general-plans/reports/SESSION-HANDOFF_30-07-26.md. Deployed prod b530ea2a, main level with trunk, suite 8432/0, all services healthy. Three operator-only blockers: (1) GitHub Actions billing — every check in every repo fails at job start, so tonight's merges were locally verified only; (2) the prospect-audit contradiction — lib/prospect-audit.ts grades "Advantage+ Audience is ON" as CRITICAL for prospects while our builds now ship it, one-line decision needed; (3) page access for act_312101480750200, the one account whose placement probe still fails (subcode 1487202, any build there fails until BM-side access lands). Watch item: ad copy + placements + Advantage+ audience all changed today, so the next real Meta build is the first test of all three — read per-placement CPL *and* lead quality, and subtract underperformers via flaggedPlacements rather than reverting. Near-miss on record: the widening shipped facebook_positions: video_feeds straight from Meta's own docs; v25.0 rejects it (code 100 / subcode 2490562) and the floor applies to every ad set, so it would have failed EVERY campaign build — caught 40 min post-deploy by the new validate-only preflight (POST :9127/adsets/create with validate_only:true, creates nothing). Use that preflight before any placement/targeting default change.
☐UNRESOLVED INCIDENT 2026-08-26 15:28-15:30 UTC: unknown actor stopped + runtime-masked dashboard, assistant AND watchdog▸2026-08-26
(masks at 15:41; watchdog stop = no self-heal, no alert; public bad gateway ~15 min until operator noticed). Peer session f4 denies it; no migration process found; site 2596 record shows a wordpressCredentialEncrypted field so a credential migration exists SOMEWHERE. All services restored 15:50 UTC. Find the masker (check shell histories/other session logs) before it recurs; a runtime-mask survives nothing but reboot yet defeats the watchdog completely.
◐Context-first platform (MCC re-architecture, task #27)▸2026-07-24
SWEEP 2026-07-24 (PARTIAL, on track): Phases 1-3 + section accents + two-layer nav all LIVE in trunk code (lib/use-client-scope.ts + ClientScopeBar = P1; lib/scope-google.ts wired into insights = P2; app/by-client/[oid]/_client.tsx Voice/Chatbot panels + per-section ?client= deep-links = P3/360-as-hub). REMAINING: (a) all-99-client roster — DATA-GATED 2026-07-24 (verified, NOT a UI change): the switcher keys on OID and sources its roster from loadClientProfiles() (lib/client-account-resolver.ts:53) = only clients with a scraped client-profiles.json entry carrying BOTH ratesightId + businessName (live count 34, was 23 — grows as clients get profiled). client-config.json is the full own-client roster but it is page_id → name with NO OID (client-account-resolver-core.ts:9), so there is no all-99 OID roster to widen to — the ~65 unprofiled clients have no switcher-usable OID. Reaching all-99 is gated on the per-client account-binding-roster initiative (populate OIDs), NOT a switcher edit; showing a name-only 99-list would be a select-but-can't-scope lying surface, deliberately not built. The switcher correctly shows every OID-resolvable client today. (b) P4 portfolio rollups + niche intel (buildable on the cross-channel precompute data, needs a spec), (c) P5 design polish. (orig) Fable discover → operator approve → Codex code → Fable verify → orchestrator eyeball. PHASE 1 LIVE 2026-07-04 (PR #717): global CLIENT SCOPE bar (All Clients default = Portfolio; pick a client → opens 360 + auto-scopes Meta cluster via shared:pageId; deep-link ?client=). Fable-gated 2 rounds (caught + fixed a platform-wide manual-filter clobber, then a cross-client leak for meta-null clients); 13/13 tests; all pages 200; screenshot-verified. Design law: [[unified-ux-section-identity]] — one UX + subtle per-section accent (Meta blue/purple · Google restrained · SEO green · Leads orange/gold · Warm Transfers red/coral · Chatbot teal · Intelligence/360 navy/violet), later design-system pass. NEXT: fast-follow = full ~99-client roster in the switcher (currently 23-client cohort); then Phase 2 (Google/SEO adapters), P3 (voice/chatbot + 360-as-hub), P4 (portfolio rollups + niche intelligence), P5 (accent polish). Spec: process/features/context-first-ux/references/CONTEXT-FIRST-ARCHITECTURE_04-07-26.md.
◐Post-deploy follow-ups (2026-07-14 deployment 40d742b)▸2026-07-14
SWEEP 2026-07-24: most sub-items DONE. per-ad-group finalUrl consumption (google-ads-create.ts:424), dry-run on unsaved intake draft (build-preview/route.ts:48), niche-aware negatives (google-ads-architecture.ts:74 INDUSTRY_NEGATIVE_KEYWORDS), generator phone/clickbait rules (assistant server.js) — all shipped. snapshot-mandatory = WONTFIX (see below). ONLY REMAINING = GATED: assistant PRs #179 (no fabricated reviews) + #181 (call-click/dataLayer) are still OPEN + undeployed → need operator merge + assistant restart + Beach Physio LP regen to activate tap tracking. (orig): per-ad-group page generation + graph finalUrl consumption in google-ads-create.ts (now unblocked — #951/#953/#956 merged); dry-run on unsaved intake draft; ~~make snapshot ref mandatory on live create-graph~~ RESOLVED-AS-WONTFIX 2026-07-24 (verified): do NOT do this — it would regress a core platform rule. lib/client-build.ts:1889-1900 (ensureGraphSnapshotRef, the ONLY live caller of /api/google-ads/create-graph) already creates+pins a snapshot on every live build; it proceeds UNREFERENCED only when snapshot persistence itself fails (:1899 "live create will proceed unreferenced"). That fail-open is required by "a DB-down must not break campaign creation" (snapshots are best-effort control-plane DB). The route already re-validates whenever a snapshot is present (create-graph/route.ts:82); making it hard-mandatory would only ever block the DB-failure fail-open path = a DB outage blocks all live builds. Leave fail-open. (The in-code comment's "once snapshots are backfilled for every account" precondition is moot — the tension is with DB availability, not backfill.); assistant PRs #179 (no fabricated reviews) + #181 (call-click/dataLayer port) still OPEN + undeployed — merge + redeploy Beach Physio LP to activate tap tracking; watch first Meta ad-build (throwing register validator from tools #84, no kill-switch); tools live checkout still on chore/consolidate-duplicates w/ dirty lib/env.js (out of deploy scope, byte-identical untracked motivation-registers.js in place); niche-aware DEFAULT_NEGATIVE_KEYWORDS split; generator-prompt phone/clickbait rules (assistant).
◐Auto-insurance keyword template in builder▸2026-07-24
SWEEP-CORRECTED 2026-07-24: PR #968 MERGED 2026-07-15 (+ follow-ups #1043/#1064 merged) — the "PR open" note was STALE. NICHE_KEYWORD_TEMPLATES on trunk at lib/google-ads-architecture.ts:162 (insurance statewide geoScope + synonym expansion, physio). /build UI wiring DONE 2026-07-24 (PR #1228, merged to trunk 93437a3e, STAGING-VALIDATED; prod pending operator sign-off — new feature, staging-first): an "Advanced: geo overrides" block on the Google build panel (region override + physical-premises cities) threaded through ClientBuildInput → /api/client-build → generateBuildGraph. Safe-by-default (both optional; absent = byte-identical to today: region auto-derives, all cities authored "near"). physicalCities is an honesty OPT-IN (only real-premises cities may claim "in {city}") — locked by a new in-vs-near test (was untested). REMAINING: Spanish opt-in only. ORIGINAL: (added 2026-07-14, from AAAble+Kothmann cross-account audit): encode in lib/google-ads-architecture.ts a niche keyword template for insurance builds — (a) {state} cluster (car insurance {state} / {state} car insurance / car insurance quotes {state}, PHRASE+BROAD — carried 100% of Kothmann conv and AAAble's #1), (b) "car insurance" phrasing alongside "auto insurance" everywhere, (c) optional Seniors themed group (converted $2-9 at Kothmann), (d) Spanish terms as market-dependent opt-in. Reference: process/features/google-ads-build/references/INSURANCE-NICHE-WINNING-PATTERN_14-07-26.md (Kothmann section). Pairs with the collision-guard fix (same file). Also fold in SHARED_LIB_REFACTOR leftovers if still wanted: W5 lib/leads/ + W6 lib/errors/ extraction (plan archived 2026-07-14 as done-enough W1-W4).
◐CHECKPOINT RUN 2026-08-26 (overdue). Valley is improving sharply and is still worse than both benchmarks. The per-theme half could not be run.2026-08-26
◐Build-page parity gaps (from PR #958 consolidation audit, 2026-07-14)▸2026-07-14
legacy /google-ads generator + account-intake capabilities dropped by the Build consolidation (full audit vs manifest process/features/google-ads-build/reports/BUILD-CANONICAL-GOOGLE-CONSOLIDATION_14-07-26.md; manifest overstates C.5 — content-rule/alignment flags only render below the 95 quality threshold, 'info' never). Approved batch (operator 2026-07-14) — ALL FIVE SHIP-ITEMS DONE + VERIFIED 2026-07-24 (code re-read this session; line numbers had drifted since the 07-14 audit): (1) generated-copy review panel SHIPPED (PR #962, 2e74552). (4) unusable-locations warning DONE — allLocationsUnusable(raw) sets locationsUnusable state (_build-client.tsx:254) and a warning renders at :862 ({locationsUnusable && ...}). (5) saved-intakes overview list DONE — _google-panel.tsx:304-310 is an SWR-backed overview list (useSWR<{intakes}>("/api/google-ads/account-intake")), absorbed from the retired generator. (8) CSV geo wiring DONE — no longer a hardcoded []; _build-client.tsx:265-277 is a cancellable effect that keeps csvLocationCriteriaIds in sync with the location-editor selection via the same resolveLocationCriteriaIds the build path uses, and :913 passes it to CsvExportPanel. (7) client-scope auto-pick + Client-360 hint DONE — :342-389 seed the picker from global scope with anti-clobber + showScopedHint (:365). OPERATOR-DECISION items (still open, need your call — not code-blocked): (2) Brand/Both CSV export (CSV panel hardcodes campaignKind:"service"; brand-extensions CSV path gone) — needed only if the Editor path is still used; (3) rating/review-count grounding for brand headlines (re-add server-side from GBP/Ratesight if wanted post-Beach-Physio top-rated removal); (6) intake editing for non-ENABLED/arbitrary customer IDs (pre-staging escape hatch). Safe-to-drop (agreed): manual/no-client mode, global zip toggle, header deep links.
◐Messaging program (buyer-avatar framework)▸2026-07-24
SWEEP-CORRECTED 2026-07-24: Phase 3 is NOT "in flight" — the Messaging Matrix engine is SHIPPED + WIRED in the dashboard (lib/messaging-matrix.ts: 4 anchors/8 angles/5 awareness + scoreMessageDiversity Big-Idea gate), consumed by lib/lp-builder/hero-message.ts (source:'messaging-matrix') + app/api/intake/route.ts. Only REMAINING is the operator's RESULT-TO-CHECK: observe register-appropriate copy in live generated Meta/Google/LP output (not code work). (added 2026-07-18): Phase 1 LIVE (per-register frames + pre-flight scorer in Meta intake, prod 042bea90→). Phase 2 LIVE (register engine → Google RSA advisory flags + LP builder registerFit, prod b3421f1b). Phase 3 engine (Messaging Matrix: 4 anchors/8 angles/5 awareness + diversity scorer). Framework ref: process/general-plans/references/messaging-matrix-framework.md. RESULT TO CHECK: watch generated Meta/Google/LP copy for register-appropriate headlines/CTAs/guarantees + fewer generic one-size-fits-all offers.
◐SEO redirect-repair: 15 of 34 sites time out every run▸2026-07-23
(found 2026-07-23; ROOT CAUSE FIXED 2026-07-23, prod 161082ce — 15→7). Sequential loop let ~6 slow sites drain the run budget so 8 more were skipped for no reason. Fix: bounded worker pool (4 sites at a time) so slow sites overlap instead of starving the rest, per-site cap 30s→40s (pool lanes run in parallel so worst-case wall time stays 285s <295s curl cap), and the assistant probe 12s→6s (PR #210, deployed). ALL 34 sites now reached; residual 7 heaviest mover sites (fatcatmovers, quantummoving, optimusmoving, speedytax, valleyinsurance, drmelindasilva, huexpress) legitimately need >40s to crawl — gated (write nothing), retry each run. Known crawl-time ceiling, not a bug; see [[seo-redirect-repair-residual]]. Do NOT re-fix by raising the cap or cutting max. ORIGINAL: (visibility FIXED in cbc2ddd0, root cause NOT fixed). The service exits 0 and journalctl prints "success" while large sites hit the 30s per-site cap and their redirect repair never completes — fatcatmovers, quantummoving, drmelindasilva, kothmanninsurance, optimusmovingservices, speedytaxpreparation, expressmoversdenver and more. Live dry-run 2026-07-23: sites 34, succeeded 19, FAILED 15. Failures are now surfaced at the top of the response (sitesFailed + named failed[]) instead of buried in a 34-element array. Cannot fix by raising the timeout — the budget is deliberate (softDeadline 250s + siteTimeout 30s = 280s, under the 295s curl cap); raising it re-creates the exit-28 overshoot. Real fix is upstream: studiosight-assistant's /seo/sites/:oid/404-recovery is too slow for large sites. SEPARATE REPO.
◐Audits find only ISSUES, never WINS▸2026-07-24
SWEEP-CORRECTED 2026-07-24: Google + SEO wins are DONE + WIRED (lib/audit-findings.ts win|issue contract, lib/google-ads-wins.ts, /api/cron/google-ads-wins, /api/cron/seo-audit + /seo/audit). The ONLY remainder is Meta ACCOUNT-health (disapprovals/fatigue), which is out of Meta Phase-1 scope and tracked at item 148 (Meta Ads Webhooks) — GATED on Ratesight. So this item is effectively complete except for the separately-gated Meta path. (operator, 2026-07-23: "the purpose of an audit is to find the wins and issues", "for each platform"). VERIFIED: lib/google-ads-ad-health.ts emits exactly three finding kinds (disapproved_ad, unreachable_url, zero_impressions), all negative; no positive finding existed anywhere in the codebase. An operator could see everything broken and nothing working, so could not tell what to REPLICATE. DONE: lib/audit-findings.ts (shared win|issue contract, evidence required, unproven[] kept separate) + lib/google-ads-wins.ts (3 detectors) merged, 26 tests, 15/15 mutants killed. REMAINING: (a) Google wire to cron route + sidecar + UI — DONE (meta-ad-health + google-ads-wins live); (b) Meta wins/issues — the Meta audit is Phase-1 claims-only by its own plan, account health explicitly out of scope (see line 133 webhooks proposal for the real Meta health path); (c) SEO wins — DONE 2026-07-23, prod 161082ce: /api/cron/seo-audit + /seo/audit page + timer 13:30 UTC, same audit-findings contract; baseline run 33 sites / 34 findings (23 issues, 11 wins, 1 urgent, 9 confident wins), 35 tests. So all three platforms now emit wins AND issues; only Meta ACCOUNT-health (disapprovals/fatigue) remains, tracked at line 133. CAVEAT ON ONE DETECTOR: the RSA-asset win uses Google's performance_label, which is a RELATIVE serving rating, not a conversion outcome — capped at low impact and carries an explicit caveat, but it is the weakest of the three and is the one to cut if you want realized-outcome-only. THRESHOLDS (5 campaign conversions / 2 term conversions / 500 impressions) are conservative judgement calls, NOT calibrated against this account set.
◐Meta Ads Webhooks — evaluate + implement▸2026-07-23
(operator raised 2026-07-23; EVAL DONE + Phase 0 shipped, prod d6dfe4db; BLOCKED on Ratesight). Plan: process/general-plans/active/meta-account-health-webhooks_PLAN_23-07-26.md. Findings: Meta disapproval detection ALREADY EXISTS via the nightly meta-ad-health poller (ad_delivery_blocked), so the webhook is a latency+2-signals upgrade, not net-new. Dry-run (GET /api/meta/webhooks/subscribe) surfaced the blocker: the Meta app 1238926602897158 is shared with Ratesight and the ad_account webhook callback is THEIRS (apihook.ratesight.com, zero fields today) — can't route ad_account events to our proxy without hijacking their stream. See [[meta-app-shared-with-ratesight]]. Operator chose option 1 (coordinate with Ratesight); integration ask written: process/general-plans/references/RATESIGHT-ADACCOUNT-WEBHOOK-ASK_23-07-26.md (forward raw ad_account events + X-Hub-Signature-256 to our proxy; no new auth since app secret is shared). All 11 accounts admin via FACEBOOK_ACCESS_TOKEN. Phase 0 shipped: lib/meta helpers + read-only bootstrap route + inert receiver stub. NEXT: Ratesight confirms relay shape → we ship the ~1-path receiver half + Phase 1 detection. ORIGINAL: Push notifications on the ad_account object instead of polling: effective_status (disapprovals / delivery blocks / return to active), creative_fatigue, ad_recommendations, with_issues_ad_objects, in_process_ad_objects, and subscriptions (insights metric crossing a threshold WE define). Directly serves the operator's "audits must find wins AND issues, for each platform" direction — Meta currently has NO disapproval or fatigue detection at all, while Google has lib/google-ads-ad-health.ts. Two enabling calls: POST /<APP_ID>/subscriptions (app access token, echoes hub.challenge) then POST /act_<AD_ACCOUNT_ID>/subscribed_apps per account (ad-account admin token). Every POST must be verified against X-Hub-Signature-256 (HMAC-SHA256 of the RAW body, keyed with the app secret) before being trusted. We already run a meta-webhook-proxy (port 3200) for LEADGEN webhooks, so callback infrastructure exists — feasibility research IN FLIGHT, including whether that existing endpoint verifies signatures today.
◐Meta copy program — post-merge verification + Phase 6 readout▸2026-07-28
(added 2026-07-28; ADVANCED same day). ALL 4 PRs (#1377/#1380/#1379/#1384) MERGED; dashboard rebuilt @ b81308e4 + restarted 15:02 UTC w/ operator authorization — healthy (health 200, watchdog green, no journal errors). LIVE-SERVER DRY-RUN VERIFIED via POST /api/intake/preview (test OID 2, zero Meta objects): movers run → all 3 offers tagged register+angle+hook_formula, proof_quotes VERBATIM from supplied testimonials (one correctly omitted), hooks follow assigned formula shapes; med-spa run → zero banned medical word-forms, identity_problem never selected. DEPLOYED WAVE 2 2026-07-28T16:52Z @ de2d917d (PRs #1390/#1391/#1394/#1387 + tools #94 + meta #46 all merged; copy-stack variations, decay findings, LEAD-SEMANTICS FIX live — CPL semantics boundary = 2026-07-28T16:52Z, prior Meta CPLs ~half of true). REMAINING (1): verify first REAL completed Meta build post-15:02Z writes aligned offers w/ all tags in builds.jsonl + its learning record carries matrixAngle/hookFormula (6-hourly session cron watching; if session dies, check manually). (2) ~4 weeks after live (≈2026-08-25): Phase 6 readout — angle/register/hook_formula → CPL via offer×placement matrix + learning-log; fold winners into niche defaults; set copy-refresh checkpoint per AAAble decay (59→6 conv/mo). (3) Phase 4 (Sultanic LP hero formula) closed NO-BUILD 2026-07-28: live hero path is deterministic single-clause code (composeHeroMessage), and the proof-led fixed-template experiment was reverted same day for fleet uniformity — the formula's arc already exists as headline(problem/desire)+subhead(blocking belief). Reopen only by operator override. Memory: meta-ads-copy-program.
◐Ads-LP refinement queue▸2026-07-29
(2026-07-29, post-comparator re-run): (1) STILL OPEN — Goliath ad-copy rewrite: ads land on our residential-Niagara LP now but still carry the old broad Ontario/office/packing RSA (13% message match); rewrite through the taxonomy prompt to match the page. (2) DONE, verified STALE 2026-08-26 — verbatim review-quote text is already excluded from the page-promise scan: extractHeroFromHtml strips <blockquote>, the lpb-review-inline band, the lpb-final-proof closing quote AND the lpb-compliance legal footer before building fullText, with Valley's "same day"/"warranty" false mismatches named in the source comment. No code change needed. H&U unbacked ad lines FIXED in the original pass.
◐Face-off follow-ups (29-07, feature LIVE)▸2026-08-26
(1) DONE, verified STALE 2026-08-26 — organic-scan already persists topResults from the list the provider returned on the same call (zero extra SERP spend), and ../rank/serp reads it, noting when a scan predates the field. (2) STILL OPEN, OPERATOR: enable PageSpeed Insights API on the Google project or set PAGESPEED_API_KEY — speed category honestly "not comparable" until then. (3) STILL OPEN: optionally cover positions 4-6 (button currently rides the striking-distance lane's 7-20 filter).
◐HALF DONE + PREMISE REFUTED 2026-08-26. The voice logging path is NOT dead, and no studiosight-voice fix is needed.▸2026-08-26
Raw rows, read today: runtime/voice-agent-learning.jsonl holds 379 rows, newest 2026-08-26T01:41:28Z — written that same morning — and voice-agent-log.jsonl 5101 rows at the same timestamp. The "newest voice row 2026-05-22 / no writes since" claim is refuted; the voice half recovered at some point and nobody re-measured. The CHATBOT half is still stale exactly as recorded (chatbot-sessions.jsonl newest 2026-06-02T17:40Z). The real blocker is neither: the aggregator's own verdict today is blockedBy.reason = below-calibrating-threshold — "2 cells exist but none reach N_TIER_CALIBRATING=5; all rates are tier-fallback and must not be read as signal." The funnel names where rows go: voice 375 -> 90 past the playbook gate; chatbot 74 -> 1; 91 after gates -> 38 in window. So it is a VOLUME and GATE problem, not a plumbing one, and the chatbot gate is the severe end. DONE: blockedBy + funnel are now declared on PlaybookAggregates and rendered as an amber "Not readable yet" banner in /whats-working > Playbook Effectiveness, and the empty state no longer says the cheerful "they accumulate" line when the aggregator has already said why nothing counts (__tests__/playbook-blocked-by.test.ts). The API always served blockedBy; nothing declared or drew it, which is precisely how this lane got recorded as a dead service path. STILL OPEN: why only 1 of 74 chatbot sessions passes the playbook gate.
◐RE-SCOPED 2026-08-26 — the TOP-PRIORITY port must NOT happen: it would duplicate a live auth surface.▸2026-08-26
Issue #1511 ranks the "tokenised client-facing audit share link" first, on the evidence that mintAuditReportToken / verifyAuditReportToken / revokeAuditReportLinks have no equivalent symbols on trunk. Those SYMBOLS are indeed absent; the FEATURE is not. Main already ships it under different names — lib/share-token.ts (mintShareToken/verifyShareToken), lib/audit-share-store.ts (createAuditShare/revokeAuditShare/listAuditShares), the public page app/share/audit/[token]/page.tsx, and POST /api/audit/client-report-link which mints an oid-sealed, tamper-proof, 30-day, operator-only link and refuses to mint before a generation exists. That is PR #1470's quarterly client audit (29-07), and it is a better-scoped design than the one queued for porting (30d not 90d, oid sealed inside the token so a holder cannot pivot). Porting #1511's version would stand up a SECOND token system over client-facing audit data — a duplicated trust boundary. Strike it from the issue. lib/build-gtm-rerender.ts (priority 2) has also since landed on main. STILL GENUINELY ABSENT (7): build-resume, conversion-surface-qa, lp-lead-dedup, gbp-review-cache, gsc-opportunity-bands, audit-client-report, audit-report-access. Each must be checked FEATURE-first, not symbol-first, before any port — symbol-first is exactly what made the top two items look outstanding. Do not delete those 7 branches — they are the only copy.
✓SEO Audit — full pipeline SHIPPED▸2026-06-23
crawl/audit + GSC + GBP connect/import, opportunity engine (brand-filtered, deduped, priority + confidence bands), GBP↔website alignment matrix (with deletion candidates), AI suggest (grounded+claims-gated), WordPress connector (HMAC, partial, self-verifying), apply→log→re-measure loop, one-click Generate→review(fit)→apply, revert, nightly autopilot (dry-run + systemd units), tabbed UI + scorecard. Verified end-to-end on Quantum (live meta_title written + verified). 2026-06-23.
✓SEO Phase 4b — GBP↔website alignment matrix▸
fuzzy matcher (GBP category/service ↔ page slug/title/H1 + GSC queries) → aligned / weak / missing-page / not-in-GBP, each with a recommendation + auto-tag. Thresholds 65/35 so shared niche-words don't false-align. AlignmentPanel on /seo/[oid]. Verified. Deployed.
✓Google OAuth env rename safe▸
Calendar/GTM/Ads/GSC/GBP now read GOOGLE_CLIENT_ID with GOOGLE_CALENDAR_* fallback, so the rename can't break voice booking/GTM/Ads. GSC+GBP callback URLs: /api/gsc/callback, /api/gbp/callback.
✓SEO Phase 3 (GBP) connect + import▸
OAuth (business.manage, NOT api-key), account+location pick, stores categories/services/description. 'Connect Business Profile' on /seo/[oid]. Needs business.manage scope + /api/gbp/callback in the Google app. Deployed.
✓SEO content webhook config + /seo/[oid] width▸
per-site 'where to POST improved content' field (bridge to the external page builder); page constrained to max-w-5xl. Deployed.
✓SEO Phase 4 (GSC opportunity engine)▸
striking-distance / low-CTR / cannibalization from Search Console, ranked by impression-weighted potential, each tagged auto-fix / auto+you / needs-you. Endpoint + OpportunitiesPanel on /seo/[oid]. Verified end-to-end with synthetic data. Deployed.
✓SEO Phase 2 (GSC) — built + deployed▸
operator-owned OAuth (lib + /api/gsc/{auth,callback,properties}), per-OID flat-file storage, GscPanel on /seo/[oid] (connect/property/sync 90d/query-page table). Dark until OAuth-app config. Design doc: seo-audit-design_22-06-26.md.
✓SEO Audit full design audit▸
answered the GSC+GBP spec; decisions: flat-file, operator-only, plaintext-MVP, reuse existing OAuth app, GBP-approval-is-the-long-pole.
✓Nav reorg Phase 1▸
deleted 1-item Integrate group, Alerts→System, new Grow group (Prospect+SEO Audit). nav-config only, reversible. Deployed.
✓Cloudflare/hermes 502 diagnosed▸
CF + tunnel healthy; tunnel routes hermes→this box's dead localhost:9119 but hermes runs elsewhere → repoint the Public Hostname (operator/CF dashboard). Nothing on our box.
✓Webhook diag (Express Movers)▸
CF A/B test proves CF passes bodies intact; Ratesight posts EMPTY body. Added query-key logging to settle body-empty vs data-in-query on next real POST.
✓Credential-expiry monitor (SEC-P0-1) MVP▸
/api/cron/credential-health checks both Meta system-user tokens (debug_token), mirrors to credential_health (now Control Tower has a writer), emails on expiring(<=14d)/invalid. Verified live: both tokens valid/non-expiring. Deployed. (Scheduling pending — see open item.)
✓SEO activity: 'Copy client summary'▸
one-click shareable plain-text summary of work performed. Deployed.
✓By-client (Ads) view shows page-connection status▸
'Page not connected' / 'Needs MANAGE_LEADS' chip per client row, links to /clients (instructions + Recheck). Deployed.
✓/todo: "Notes for Claude" panel▸
operator leaves unblocking context per item (POST /api/todo/note -> process/todo-notes.json); Claude reads each session. Deployed.
✓Landing-form service buttons: clearer hover (lift+shadow) on all 5 templates▸
was border-only/brand-dependent. Deployed (existing pages get it on re-render).
✓SEO Phase 1: client "What we performed" log▸
/seo/[oid] ActivityPanel + /api/seo/sites/:oid/activity (reads fix-log.jsonl: every fix, before/after, source, ts). Deployed.
✓Brentwood timeframe / forms editor empty step 2▸
RESOLVED: timeframe is live on the page (v51 verified); editor showed empty because Refresh form strips saved fields. Editor now falls back to the same derived qualifiers the page renders. Deployed.
✓/leads: account filter no longer hides leads from unmapped pages▸
a just-connected page (no campaigns yet, e.g. White Rose) maps to no account, so its leads were silently dropped under any account filter; now only different-account leads are dropped. Deployed.
✓SEO Audit architecture review▸2026-06-22
full research → process/general-plans/references/seo-audit-architecture-2026-06-22.md (snippet=fix-applier, server crawl exists, GSC/GBP not connected, MVP = connect 2 Google sources + alignment matrix). No code yet, per request.
✓To-Do page▸
in the console (System group) — renders process/TODO.md live. Deployed.
✓Clients: permission-aware "Recheck now"▸
checkPageReadiness reports real readiness (ok / missing_tasks / no_token_owns_page) by deriving the page token AND reading leadgen_forms (MANAGE_LEADS test), not just connectivity. Deployed.
✓White Rose "not connected" RESOLVED▸
live recheck returns status:ok (connected + MANAGE_LEADS); the 04:02 audit row was fully stale. Operator was right.
✓Clients page: full-width sorted rows▸
reverted the grid to one row per line; sort needs-attention first (not connected → missing tasks → unchecked → ready), alphabetical within. Deployed.
✓Lead form width → 620px on the rendered form; editor canvas back to full-width▸
(both repos, deployed)
✓FULL UX audit▸2026-06-22
keep/merge/drop + target nav + sequenced plan → process/general-plans/references/ux-audit-2026-06-22.md
✓TODO watchdog (in-session)▸
SessionStart hook surfaces open items every session
✓Form service step: auto-advance on pick + drop redundant Next▸
(assistant, deployed)
✓Single-service auto-skip + focus first field on advance▸
(assistant, deployed)
✓Forms editor canvas width → 620px▸
(dashboard, deployed)
✓Forms: "Service options" under step 1 + coherent counts + blended styling▸
(dashboard, deployed)
✓"Refresh form" button▸
re-render lead form from current qualifiers, keep copy, no AI cost (dashboard, deployed)
✓Brentwood "Which rooms?" qualifier removed▸
(assistant, deployed)
✓SEO Phase 1: bulk-approve / undo / revert-auto▸
proxies + UI + backend (both repos, deployed)
✓Control Tower: full-width layout + credential-health dedup▸
(dashboard, deployed)
✓Context docs refresh▸
token health 8/96 → 31/99, added jobs/email services (dashboard)
✓UX Week-1: nav discoverability + accuracy fixes▸2026-07-04
(2026-07-04) — DEPLOYED. Nav: surfaced Lead Delivery/Morning Report/Recovery/SMS Agent/Dream Avatars/Bugs/Management (PR #711). Accuracy: killed 5 "status that lies" surfaces incl. revenue-critical retry-all truthfulness + 3-state delivery badge (PR #712, Fable review=SHIP). Both live, health 200.
✓DONE 2026-07-23 — Leads-service delivery status (studiosight-leads `9670cea` + `56f1bec`).▸2026-07-23
Fixed as precedence-then-recency, not naive latest-wins: delivered is STICKY against a later failed, because the only writer that appends failed after delivered is the manual retry force-path — that means "this extra attempt failed", not "the CRM never got it". Naive latest-wins would have re-created the same lie in the other direction. Found a 4th status the report missed: delivery_failed (the landing-page spelling). Mutation-verified INDEPENDENTLY (not just agent-reported): first-entry-wins fails 6, naive latest-wins fails 2. ALSO FOUND: npm test ran 1 of 11 test files — ten never ran, including today's guards against silent lead loss. Now runs all: 121 tests, 121 pass. ORIGINAL: Leads-service follow-up (studiosight-leads repo, NOT dashboard) — server.js:423 builds the delivery display map first-entry-wins, so a lead whose FIRST log entry is failed keeps showing red "failed" even after a successful retry appends delivered. Fix in the leads repo (prefer latest-entry-wins for terminal status). Surfaced by the UX accuracy review 2026-07-04.
✓Leads delivery label (studiosight-leads or dashboard lib/lead-delivery.ts)▸2026-07-22
[VERIFIED DONE 2026-07-22] — lib/lead-delivery.ts:94-102 returns a neutral gray no contact badge titled "Lead provided no phone or email - nothing to deliver (deliberately skipped, not a failed delivery)". No longer amber "pending". ORIGINAL: — skipped_no_contact renders as amber "pending delivery" though it will never deliver (deliberately skipped, no phone/email). Give it its own honest label. Low priority.
✓UX Week-1 remainder (A+B)▸2026-07-04
(2026-07-04) — DEPLOYED (PR #713). A: 40 informational alert()→Toast across 9 client components (left 1: CampaignsTab local toast binding); ZERO of the 51 confirm() blocking gates touched (EmergencyStop/cleanup-spend-guard/archive stay native). B: surfaced the working /pipeline 8-agent view in the SYSTEM nav. Health 200, /pipeline 200. DECLINED by judgement (not shipped): deleting redirect shells (11-line bookmark redirects, 2 referenced by finding-actions.ts — negative-value churn); /pipeline+/seo/moving-360 deletions (real features; moving-360 is NOT a dup of google-ads/moving-360, audit was wrong) = operator product call; date-control unify (consistency-only, medium risk, low payoff); confirm()→modal (load-bearing spend-guard/EmergencyStop gates must stay blocking).
✓studiosight-jobs crash-safety — double-build bug (PR #10, RESTARTED + LIVE 2026-07-04)▸2026-07-04
. Found while scoping #6: a worker crash mid intake-build/client-build re-queued the job → DOUBLE Meta campaign (+ for client-build, 2nd deployed LP + 2nd PAUSED Google campaign). Fixed: crash-recovery fails non-idempotent kinds terminally + alerts; idempotent (performance-snapshot/rank-scrape/seo-crawl) still retry. 28/28 tests green, Fable-reviewed (caught + fixed a client-build misclassification). RESTARTED + LIVE 2026-07-04 (health 200, clean boot). PR #10 awaiting operator merge.
✓#7 GSC time-series (branch feat/gsc-timeseries, commit afacfac)▸2026-07-22
[DONE 2026-07-22] — code was already merged to trunk (516ac912 + aad55c75) and migration 0008 applied; the REAL gap was that no systemd timer ever called the ingest, so gsc_metrics was empty fleet-wide and /gsc/trend returned count:0 while still answering ok:true (a blind spot that reads as clean). Created + enabled studiosight-gsc-metrics.timer (daily 06:45 UTC, after sync-gsc 06:30 since the ingest skips clients not in the warehouse mirror; days=3 self-heals; upsert makes it idempotent). First run: 5/32 sites populated (26 skipped 'no GSC property' / 'not in warehouse mirror', 1 permission error on quantummoving.com). Also backfilled ?months=16 → 16 months stored for 170652/172658/174637, 10 for 174693. KNOWN REMAINING: 172662 backfill fails 413 on 2026-06 (payload too large, needs chunking); coverage is capped at 5-8 sites by GSC-property + warehouse-mirror gaps. ORIGINAL: — new gsc_metrics Postgres table + idempotent daily ingest + query/page trend API. GSC already connected. Deferred: UI + systemd timer. NEEDS: DB migration apply (0008_gsc_metrics.sql) + cron timer wiring.
✓#8 scan-trust signal — ALREADY SHIPPED (verified 2026-07-23).▸2026-07-23
Merged via PR #890 from feat/scan-trust-signal-v2 (merge 986780b6), confirmed an ancestor of trunk HEAD; lib/seo/scan-trust.ts is live. The TODO tracked the v1 branch and never got closed when v2 landed. ORIGINAL: #8 scan-trust signal (branch feat/scan-trust-signal, commit a118f8d) — pure scanTrust() + client-facing filter hides only fully-untrusted scans (all-null/proxy-fail), keeps legit "not ranked" visible. Self-check passes, additive. Ready to review+ship.
✓Brentwood promo — LIVE on all 6 pages 2026-07-23▸2026-07-23
(was silently dark 15 days). Fixed 3 bugs: (1) reconciler gate stripped copy -> active always false; (2) scope held stale/regenerated UUIDs; (3) THE blocker — reconciler regenerated from slots but current LP pages are html-only. Built the durable fix: HTML-injection path (assistant e69b15a on fix/seo-meta-quality-gate) — the self-contained bar (inline CSS) is injected into deployed html after <body>, marker-wrapped so it's idempotent + reversible; drops the slots dependency, works on ALL pages incl. 89b1e08f (zero slotted versions). Repointed runtime scope to the 6 real UUIDs (banner, all pages), live-fired via canary-then-full, VERIFIED: all 6 live pages show the bar with exact copy + ® marks (screenshot-confirmed on 2 incl. the previously-unfixable window-treatments page); auto-clear after 08-08 confirmed by --now=2026-08-09 dry-run (removes exactly -1333b). Daily cron now self-manages it. 5 injection unit tests + gate regression test. Rollback CF deploy IDs captured per page. PHASE 2 still deferred: "Add promotion" UI, Google PromotionAsset, per-placement (hero/below-hero) injection anchors. HISTORY (2 bugs, gate fix): Verification 2026-07-23 (15 days after the 07-08 auto-fire date) found the promo silently OFF the whole time. Bug 1 (FIXED, assistant 24bdadf on fix/seo-meta-quality-gate): the reconciler gate called promoIsActive({startDate,endDate}) with NO copy; a "copy-required -> invalid" rule had been added to promoState without updating the reconciler, so it computed active=false every day regardless of the window. Gate now evaluates the same promoInput(record) object the renderer uses; real-record dry-run now = active=true (1288-byte bar); regression test added. Live for the next 06:30 run (cron runs the dev working tree). Bug 2 (scope drift): 6 of the 8 scoped project UUIDs were regenerated by the LP portfolio migration (only d771b646 + 494ce468 survive); the 6 live Brentwood projects are d771b646, 76424f94, 494ce468, bd92db3f, 4cf2f281, 89b1e08f. Bug 3 (THE REAL BLOCKER, needs a build): the reconciler injects the promo by regenerating from a version's slots, but the current LP generation stores html only, NO slots — 89b1e08f has ZERO slotted versions ever; d771b646's last slotted version is 2026-07-09 with 7 html-only versions on top. So NONE of the 6 can be injected via reuseSlots without either reverting live content (reuse the stale 07-09 slotted version) or a promo-injection redesign. Recommendation: a promo bar is chrome, not content — build an HTML-injection path (insert the bar div after <body> / at the {{PROMO_BAR}} anchor on the deployed html, reverse on expiry) instead of regenerate-from-slots, OR make the LP builder persist slots on every version. Held the fire AND the runtime scope-repoint (pointless until injection works on html-only pages). Fire-now-vs-reset-window is moot until then. NOTE: whole promo system is branch-only (fix/seo-meta-quality-gate, 140 ahead of main, never merged) — see [[assistant-dev-main-topology]]. ORIGINAL: Prior session (Jul 3) already built the reusable promo system ~90%; I verified it end-to-end: timer studiosight-promotion-reconciler.timer enabled (daily 06:30 UTC); reconciler dry-run proves activate on 07-08 (all 8 Brentwood pages: promoBar=true, exactCopy $500/"Save Up to"=true, qualityGate.pass, complianceErrors=0, per-page CF rollback IDs captured) + auto-revert after 08-08 (clear branch); deploys via HTTP to assistant :9122 (has CF creds, service active) so the bare .service env is fine; promo-bar tests 16/16. RESIDUAL: assistant must be up at 06:30 07-08 (watchdog covers). DEFERRED to phase 2: hero emphasis on motorized/HD pages, "Add promotion" UI, Google PromotionAsset (account not spending). ® MARKS DONE (operator: "use the real ® marks"): renderPromoBar now normalizes (R)->® / (TM)->™ before escaping (assistant render-html.js, commit 57f3641 on origin fix/seo-meta-quality-gate); unit test asserts the transform (16/16); assistant RESTARTED so it's live for the 07-08 render; direct check confirms "has ®: true, has (R): false". Record stays ASCII-safe; glyph is a render-time transform (respects the never-write-runtime rule).
✓GBP snapshots cron — 290s timeout FIXED + LIVE▸2026-07-04
(2026-07-04). studiosight-gbp-snapshots.service was failing daily (curl exit 28): the full-settings pass ran captureSnapshot sequentially for all 611→613 GBP locations, growing past the 290s curl budget (change-history loop was fine/complete). Fix (app/api/cron/gbp-snapshots/route.ts, PR #716): concurrency-5 batches + 250s soft-deadline returning 200 w/ fullRemaining+deadlineHit instead of timing out; idempotent (captureSnapshot no-ops on failed fetch, retries next run; conservative concurrency since GBP fetch has no 429 retry). Deployed + verified live: 613 locations in 26.3s, deadlineHit=false, healed 12 missed snapshots. failed-state cleared.
✓CTA business-fit fix (2026-07-14) — DONE 2026-07-14 late▸2026-07-14
PRs #960 (dashboard, +5 review-bypass closures B1-B5 + N1) and assistant #183 MERGED + DEPLOYED (dashboard 0184fba, assistant 3f04167); live validator proof PASS; 3 canonical campaigns rebuilt with appointment-model ads (16 groups/16 ads/304 kws, ALL PAUSED incl. ads); ManualCPC ramp RETIRED (paused+empty). See process/features/google-ads-build/reports/BEACH-PHYSIO-CTA-REBUILD_14-07-26.md. Original scope: Beach Physio B2 ads shipped "free quote" CTAs — root cause = trades-biased deterministic RSA template banks (google-ads-architecture.ts slot copy + google-ads-rsa.ts banks, incl. fabricated "rated 5 stars" default claims). Fix PRs: dashboard feat/ga-cta-business-fit (conversion-model classifier + evidence-gated validator hard gate in Build path) + assistant fix/ga-prompt-cta-business-fit (prompt example + fallback banks). AFTER merge+deploy: one-shot in-place ad-group rebuild of the 3 PAUSED B2 campaigns to replace the quote-language ads (separate authorization; campaigns must stay paused until then).
✓Builder-wide negative self-block bug▸2026-07-14
FIXED for future builds (PR #964 merged+deployed 2026-07-14 2e74552; guard strips self-blocking default negatives at graph assembly, recorded on graph.negativeCollisions). REMEDIATION DONE 2026-07-14: Valley's broad cheap negative removed via API (validateOnly→live→read-back); deterministic scan scripts/google-ads-negative-collisions.mjs <cid> [live] now confirms ZERO collisions on BOTH Beach Physio 9316408200 and Valley 7032156594 (Beach Physio's was already clean post-rebuild — the "no removal tooling" claim was wrong, the script does removals). ORIGINAL: (found 2026-07-14 during Valley build): canonical graph DEFAULT negatives include BROAD cheap/free, which self-block the Near Me "Price" group's own cheap {service} keyword on EVERY graph build (affects Beach Physio too). Fix = collision guard in lib/google-ads-architecture.ts: strip any default negative that conflicts with the graph's own positive keywords. Small deterministic PR.
✓DONE 2026-07-23 — LP fleet-backfill enrichment protection (merged to trunk `cbc2ddd0`).▸2026-07-23
The audit found the task HALF-DONE and the done half fragile: a content-diff guard existed but keyed off a hand-maintained field list (the exact fragility the TODO named), and the enriched marker it was meant to rely on was write-only — written by the backfill, typed in the store, read by NOTHING. Also: the module shipped with a raw NUL byte so git treated it as binary and it merged effectively unreviewed. Now a field-agnostic provenance guard runs first, diff kept as second detector, loud named refusals, .gitattributes forces source to diff as text. 19/19 tests, 7/7 mutants killed. NOT VERIFIED: never executed against real LP storage (staging-gated) — refusal path has unit evidence only; and the guard only fires for enrichment writers that stamp enrichedAt (one confirmed, not all audited). ORIGINAL: LP fleet-backfill erases enrichments (found 2026-07-15, operator-visible on ASAR): scripts/lp-fleet-backfill.mjs (staging lane) re-derives drafts from prod source and flattens post-migration enrichments (hero-image-queue assets, review-thin-queue curated reviews). ASAR 1b8ac149 restored by snapshot merge. FIX before next backfill run: carry-over enrichment fields or refuse-to-overwrite-diverged records + add an enriched provenance marker so tooling can protect them mechanically. Noted on PR #969.
✓Productize the GBP review fetcher▸2026-07-22
[VERIFIED DONE 2026-07-22] — studiosight-gbp-reviews.timer is enabled + active (last run 2026-07-20, next 2026-07-27); no longer a one-off ad-hoc fetch. ORIGINAL: (added 2026-07-15): runtime/gbp-reviews/<oid>.json was populated by a one-off ad-hoc fetch on 2026-07-08 (lp-review-selector PR5 era) — no tool/route/timer exists to refresh it. Needed so review-thin LPs can truthfully reach the >=2 carousel threshold (ASAR 2402013 has 1 sourced review, slider hidden until more real reviews exist). Build: small dashboard route or /home/ubuntu/studiosight/tools script fetching GBP reviews per OID (v4 API, existing GBP OAuth) + timer; then re-attach via the (enrichment-safe, see fleet-backfill defect above) migration path. Assistant reads via loadGbpReviews() — format already normalized.
✓SEO redirect-repair: UI toggle + kill-switch▸2026-07-22
[VERIFIED DONE 2026-07-22] (toggle half) — app/seo/[oid]/_seo-detail-client.tsx:741-749 renders an autoApplyRedirects toggle POSTing to the 404-recovery-config route; no longer API-only. ORIGINAL: (added 2026-07-16): (1) autoApplyRedirects per-site toggle is API-only (POST /api/seo/sites/<oid>/404-recovery-config) — add a dashboard control on the SEO site page per the self-service rule; (2) add a global enable/disable toggle that stops the redirect-repair script from executing at all (cron currently always runs, gating only affects writes) — cleanest as a feature flag in runtime/feature-flags.json checked at the top of app/api/cron/seo-redirect-repair/route.ts, surfaced as a UI switch alongside the per-site toggles.
✓Lead-events 405 fix — RESOLVED 2026-07-18▸2026-07-18
(assistant PR #189 merged+deployed; dashboard browser-gate PR #1021 telemetry-origin fix; Aloha regen v21 live, beacon absolute, endpoint 200) — was (added 2026-07-18): running assistant (main) missing the /api/lead-events beacon rewrite (exists only on unmerged feat/lp-click-to-call-tracking/a436415). Aloha v18 (and any structured-builder LP) emits a relative /api/lead-events → 405 on *.pages.dev. Telemetry only — lead delivery unaffected. Fix ready: assistant PR #189 (one-line rewrite). GATED ops to land: (1) merge #189, (2) deploy+restart studiosight-assistant, (3) regen Aloha v18. All 3 need operator go (service restart + LP regen are hard-rule gated).
✓Trek Movers tCPA removal — TRIPWIRE CHECKED 2026-07-28: KEEP, do not revert.▸2026-07-28
9-day read on account 6553318158: PMax 24023551353 ENABLED, 9,630 impressions, 245 clicks, 4 conversions, $174.97 spend, CPL $43.74. The revert trigger was "spend converging on the ceiling with STILL ZERO conversions after ~1 week of real delivery" — it did not fire, because conversions are not zero. Delivery is real (9.6k impressions vs the pre-change starvation) and primaryStatus is now LIMITED, not MISCONFIGURED, which was one of the named GOOD markers. Spend is $19.44/day against the ~$31/day authorized ceiling, so it has NOT converged on the ceiling either. CPL improved against the prior 30-day read ($66.02 -> $43.74), though that comparison is soft until the Phase 0 conversion cleanup lands. NOTE: RSA 24018289413 is now PAUSED (was enabled when the tripwire was set) with $73.35 spent and 0 conversions across 350 impressions — I did not pause it and per the never-unpause rule I will not touch it; flagging so the state is not a surprise. Original entry follows for the audit trail. (applied 2026-07-19): cleared target CPA on RSA 24018289413 ($15.00 -> NONE) and PMax 24023551353 ($7.50 -> NONE), account 6553318158. Budgets UNCHANGED ($16 + $15/day). Expected effect: daily spend rises from ~$2.67/day combined toward the ~$31/day authorized ceiling (~$217/wk). Check 2026-07-28. GOOD: rank-lost IS falling from 90% on PMax, non-zero impressions, primary_status leaving MISCONFIGURED, RSA budget-lost RISING (budget not bid becomes the constraint). BAD / REVERT TRIGGER: spend converging on ceiling with STILL ZERO conversions after ~1 week of real delivery = paying full budget for nothing; revert by POSTing the restore ops (recorded in reports/debugger-260719-1804-trek-misconfigured.md) to /api/google-ads/campaign-target-cpa on the production dashboard — the CLI script was deleted in favour of that route. Root cause remains INFERRED (Google returns UNKNOWN reason codes); if it fails, next hypotheses: stale new-account status (campaigns 6-8d old), PMax thin inventory/URL expansion, campaign-level conversion_goal, or read the Google Ads UI diagnostics panel for the decoded human-readable reason.
✓`reviewsRefreshedAt` — INVESTIGATED + REFUTED 2026-07-23.▸2026-07-23
It does NOT time the read. studiosight-assistant/server.js:1900-1910 (GET /client/profile/:id) performs a REAL network pull (fetchRatesightReviews -> app.ratesight.com widget, 12s timeout) on every read, and stamps the marker ONLY when that pull succeeded AND returned data (reviews.ok && (reviews.count || reviews.quotes?.length)). So "age 0.0 min on every read" is CORRECT — the route is deliberately fully-live ("re-pull reviews on every read"), not a stale stamp. The staleness guard is sound: a failed or empty pull leaves the marker untouched, so it correctly ages. Residual nuance found instead (NOT the reported bug): the stamp mutates the in-memory profile and is never persisted — there is no save in that handler — so any consumer reading the STORED profile sees an absent/older marker than a live read would. Harmless today because every consumer path goes through this route (which re-pulls anyway), but it means the marker cannot be trusted as a durable record of last-pull time. Guard NOT weakened; omit-on-stale behaviour untouched. ORIGINAL SUSPICION: reviewsRefreshedAt may measure the read, not the pull (found 2026-07-19): the marker shows age 0.0 min on every profile read, suggesting it refreshes when we read the profile rather than when Ratesight data is actually pulled. This is the guard that stops us publishing a stale review aggregate as verified proof on landing pages, so if it times the wrong event the staleness protection is weaker than it appears. Did not affect the 2026-07-19 regen run (review data was genuinely live, 5.0 from 36 reviews, matched). Investigate in studiosight-assistant (separate repo). Do NOT weaken the check; the correct behaviour on stale data is to OMIT the review line, which it already does.
✓RESOLVED (verified 2026-07-24) — LP evidence provenance for builder-native pages: decision already MADE + implemented + test-locked.▸2026-07-24
The stale ask asked us to CHOOSE between (a) builder-native inputData.services counts as verified provenance vs (b) harvest a sibling page. Option (a) was already shipped (PR #1088 + the 2026-07-20 declared-not-website label) in lib/lp-builder/evidence-from-legacy.ts (declaredBuilderNativeServices + servicesFromIntake + acquireEvidence): a builder-native project sources its services grid from operator intake tagged declared provenance ("A SERVICE NAME IS NOT A CLAIM"), superseding the single profile service_type label — with a hard boundary that a claim-shaped service NAME ("Free In-Home Estimate", "Licensed & Insured") stays a label and never grants a risk-remover. Option (b) was explicitly rejected (importing a sibling's evidence = provenance laundering). Verified this session: real prod data for brentwood-blind-company-window-treatments is generator structured-builder-v1, inputData.services=6 (incl. the "Custom window treatments" umbrella), structuredDraft.services=5 → a regen now yields the 5 real service cards (umbrella correctly resolved out), NOT the 1-card collapse. __tests__/lp-evidence-declared-services.test.ts locks it — 12/12 green (incl. "THE REGRESSION: 5 cards, not 1" and "PRECEDENCE, NOT UNION: umbrella not re-added"). Only remaining (optional, operator-gated, NOT blocked): actually regenerate + redeploy the live window-treatments page if you want the deployed *.pages.dev refreshed — a normal client-facing deploy, not a decision. The honesty-semantics question is settled.
✓RESOLVED + LIVE-VERIFIED 2026-07-24 — Quantum 6 pages honesty win: already shipped.▸2026-07-24
The stale premise ("those unevidenced claims stay live") is no longer true. All 6 Quantum LPs (OID 174752: long-distance a9dde43e, bay-area 3f1c646d, piano 719275be, packing-services a59b0099, full-service-packing b18bd2ee, office-commercial 25b71046) are now structured-builder-v1 (lpb-), re-deployed 2026-07-20→07-24. The dominance-pin mechanism the ask required already exists — seed-draft.ts:104 primaryServiceOverride (hero-only pin, doesn't touch serviceTypes) resolved+honesty-guarded in client-build-lp-cutover.ts:334-366 (pin refused unless corroborated) — and the render-boundary claim gate (draft-claim-gate.ts:256 gateDraftClaims, FAIL-CLOSED, wired at render.ts:1524) strips unevidenced licensed/insured/bonded because moving is not an auto-grant profession. Live grep this session (piano + office pages): lpb- present, licensed/insured/bonded/same-day = 0/0/0/0, H1s "Piano & Specialty Movers in Alameda" / "Office & Commercial Movers in Alameda" (targeted service dominant, NOT collapsed). CAVEAT: I did not find an operator-approval record for the 6 deploys (may have been automated fleet regen) — outcome is correct + honest regardless. SEPARATE + STILL OPEN: the offer/price SERVICE-TITLE claims ("Free Written Quotes" on long-distance + office-commercial) are client-supplied source data outside the render gate — that's line 134's operator-decision item, unchanged.
✓[OPERATOR / BM] Page `1172577005928820` — RESOLVED 2026-07-22.▸2026-07-22
Real cause was NOT a missing MANAGE_LEADS task (my first diagnosis, which the operator acted on and which changed nothing): the page was not owned by our Business Manager at all, so no task could attach. GET /{page}?fields=business returned an owning business for the healthy control page and NOTHING for this one. Operator claimed the page into BM; retrieval unblocked immediately and the 5-minute sweep delivered all 6 leads to Ratesight (status=delivered, OID 2402003) with no manual step. The 4 leads on the sibling form were delivered back on 2026-05-13 — no stale-lead delivery occurred. ORIGINAL NOTE: Confidently Fit - Personal Training (OID 2402003). We are subscribed to leadgen and correctly routed, but Meta returns 0 leads against leads_count 6 — we cannot retrieve the data. Sibling page 1030924733435890 returns all 124. Proven by differential test. Code now reports this loudly (RETRIEVAL BLOCKED, severity high + bug report) but the fix is a Business-Manager permission grant only the operator can make. Every lead on this page is lost until then. Verify after with: curl 'localhost:9125/leads/form-leads?formId=1467046624917773' → expect returned > 0.
✓Re-audit fleet for the same retrieval block▸2026-07-22
(added + DONE 2026-07-22): built GET /leads/retrieval-audit. Result: 33 forms probed, 31 retrievable, 2 blocked — both on Confidently Fit page 1172577005928820 (10 leads unreachable), 1 page unknown (no page token, 3 forms). The block is CONTAINED to one page, not the fleet-wide epidemic I speculated. ORIGINAL NOTE: the nightly token audit tracks MANAGE_LEADS but nothing cross-checks it against *actual lead retrieval*. GET /leads/form-leads?formId= per routed form with leads_count > 0 gives the empirical answer. Memory says 23 clients are missing MANAGE_LEADS — each is a candidate for silent total lead loss.
✓35 unaccounted leads fleet-wide▸2026-07-22
(added + RESOLVED 2026-07-22): measured with /leads/form-leads. All 29 unlogged leads are 74-89 days old (pre-logging era, May). None within any deliverable window; they age out of Meta's 90-day retention shortly. The 'partial residual older than window' label was CORRECT — no lost revenue, no action. ORIGINAL NOTE: H&U Express Moving (22 count / 12 accounted) and Dejavu Move-In-Out (85 / 66) carry partial residuals older than the 7-day window. Correctly cursor-silenced and NOT the same defect as Confidently Fit, but never investigated. Deciding whether to recover any requires an explicit exception to the 7-day delivery policy.
✓Two routes silently ignore filters▸2026-07-22
(added + FIXED 2026-07-22): /api/leadforms now 400s on unknown params (dashboard 42fdcd45, staging-validated, NOT yet promoted to prod); studiosight-meta /ads now 400s on camelCase aliases (PR #44, live). CORRECTION: the claimed filterItemsByPage bug is NOT real — it DROPS unmappable items rather than returning everything. ORIGINAL NOTE: /api/leadforms reads page_id (a ?pageId= caller gets every page); studiosight-meta/server.js:869 ignores campaignId, and filterItemsByPage reads promoted_object.page_id off *ads*, where that field does not exist, falling back to a cache that returns everything on a miss. Both answer confidently with unfiltered data. Cost real diagnostic time this session.
✓`resolvePageToken` picked a token that can MINT, not one that can READ▸2026-07-22
(added + FIXED 2026-07-22): latent trap, not the cause of the Confidently Fit loss. A system user with ADVERTISE but no MANAGE_LEADS mints a page token fine and then returns an EMPTY lead list (Meta does not error on unauthorised retrieval), so it wins the race, gets cached, and blocks lead reads silently. fetchLeadsForForm now has opt-in cross-token retry on empty (enabled only in reconcile's deep-fetch, where empty is evidence of a wall rather than of quiet) and REPOINTS the cached index, filing a high-severity report rather than self-healing silently. leads 4d24af2, 19/19 tests.
✓DONE 2026-07-23 — Rank cadence now WEEKLY (prod `31ad5a3a`).▸2026-07-23
Operator decision. Found a second, quieter bug while doing it: updateTarget SILENTLY DROPPED cadence and nextRunAt, so only the create path ever set them — the 23 existing targets could not have been moved off monthly by ANY code path. Both now pass through, and re-seeding normalises the schedule rather than only the keywords. All 23 migrated and verified: cadence=weekly, nextRunAt=null so the next sweep picks them up instead of waiting for 2026-07-26. NOTE: the work queue reads a precomputed sidecar (runtime/seo/worklist/<oid>.json) rebuilt at 11:45 UTC, so its untracked count still lags the underlying data by up to a day. ORIGINAL: Rank cadence is MONTHLY and the first scan has not happened (found 2026-07-23). POST /rank/seed-keywords hardcodes cadence: 'monthly'. The 23 clients seeded 2026-07-22/23 show lastRunAt: None, nextRunAt: 2026-07-26 — so the SEO work queue's "untracked" count CANNOT move until the chain runs (organic-rank 07:25 -> page-rank 11:30 -> worklist rebuild 11:45), and after that it only refreshes monthly. Monthly is too slow to be a feedback loop for a system meant to prove improvement. Cost is not the constraint (~$0.32/month fleet-wide), so weekly is affordable — but changing cadence is an OPERATOR DECISION, not a side effect. Also note the work queue reads a PRECOMPUTED sidecar (runtime/seo/worklist/<oid>.json), so it always lags the underlying data by up to a day.
✓VERIFIED 2026-07-29 13:00 UTC: first timer-driven cycle SENT with all 7 sweep areas fresh, zero missing.▸2026-07-29
(was: Morning correctness sweep — confirm first timer-driven cycle (check 2026-07-29 after 13:05 UTC)**: six sweep timers installed 28-07 (lp-live-render 08:20, alignment-audit 10:15, conformance 10:25, lp-alignment 12:00, evidence-verify 12:15, artifact-fit 12:20 UTC). Verify the 13:00 morning report email carries the sweep section with all 6 areas non-stale. Also: (a) RESOLVED 28-07: Quantum hero finding was a sweep false positive (stale approved-asset record vs badge-refused hero; page correctly serves illustration hero) — fixed in PR #1401, merged+deployed, page now passes; (b) RESOLVED 28-07: was instrument staleness (rule forbade canonical reCAPTCHA/GTM scripts) — fixed via script-inventory classification PR #1409; fleet now 31 aligned / 0 off-template / 3 stale (never-deployed ASAR); (c) Phase 2b verification: next real build per class should carry avatarContentHash/inputSnapshot — check a fresh builds.jsonl / client-build-runs record; (d) PARTIAL 28-07: LP regen capture wired (PR #1409, confirmed on fresh versions); still open: assistant-side/UI LP creation uncaptured, verifyServedAssets not wired into Phase 3. NEW 28-07: hero copy fleet refresh shipped (per-niche anchor banks PRs #1407/#1413, 22 pages regenerated, 10 new heroes); departed-client pages (Quantum x6, White Rose) still live with old copy — operator decision whether to take down; served-verify procedure should add CDN propagation wait + cache-buster (race produced false 'unchanged' readings twice). Plan: process/general-plans/active/morning-correctness-sweep_PLAN_28-07-26.md
✓SEO service-alignment apply needs batching/jobs▸2026-07-28
(DONE: UI batching PR #1418; Kothmann plugin updated by operator; 28 drafts + boerne recreate exist on WP awaiting publish) (found 2026-07-28, PR #1408 deployed the missing /apply proxy route): a full "Preview drafts (dry-run)" for a client with many missing services (Kothmann 2470 = 28 missing × ~50s LLM call ≈ 25 min) exceeds the 290s proxy timeout — the UI button now reaches the backend but times out client-side while the assistant keeps generating. Fix: cap services per request in the UI (send explicit services:[...] batches) or move apply to studiosight-jobs. Also: Kothmann's WP plugin is a minimal build (/capabilities = rest_no_route, only /create-page) — plugin update needed before real draft writes.
✓SEO alt-fix follow-ups after assistant PR #239 (ALL DONE 28-07: v5 origin traced [prod working-tree drift, C23]; already_present premise refuted → junk-alt detector #240 + snippet v6 #1424 + value quality #241; skip reasons in UI #1427/#1428)▸2026-07-28
original text: (after assistant PR #239 (deployed 2026-07-28)**: the "selector_miss" theory was REFUTED by live-DOM measurement — selectors match; the server fabricated the label for v4 clients that report no reason. #239 (live) now records client reasons verbatim, de-identifies ambiguous v4 attribution, and dedupes duplicate fixes (kothmann 16→15, verified live). Remaining: (1) TRACE which origin serves snippet v5 to 5 client sites (drmelindasilva, brentwood, optimusmovingservices, quantummoving, fatcatmovers — 3068 events) while dashboard.ratesight.com serves v4 and branch fix/seo-snippet-v5-dashboard (commit d120a5e0) is unmerged; (2) the REAL alt problem is 1460 already_present skips = stale crawl/audit state claiming alt missing when the live page has one — needs a crawl-freshness/reconcile fix, separate scope; (3) dashboard UI still doesn't show skip reasons on issue rows (audit finding 2, now feasible since reasons are truthful).
✓Hero frame collisions — DONE, served-verified 2026-07-28 ~20:00 UTC▸2026-07-28
(was: PR #1420 ready; regen+redeploy the unfinished half) (2026-07-28): fix/hero-variant-per-page (adopted an idle uncommitted in-flight change, completed + verified: per-page variantKey + no-fit angle rotation + per-anchor salt + bank variants; composing all 14 affected pages → findHeroFrameCollisions NONE, distinct H1 5/5 / 4/4 / 5/5; suite 7709 pass, tsc/build clean). AFTER MERGE the 14 live pages must be REGENERATED AND REDEPLOYED then served-verified with cache-buster (undeployed = unfixed, C21). Operator decisions: (a) Quantum is a departed client — redeploy their 5 pages or take them down? (b) merge authorization (CI dead on billing, checks ran locally). Follow-up in PR: findHeroFrameCollisions cannot see comma-free templates ("Your written quote is the whole cost of X.") — detector gap, separate scope.
✓DONE 2026-08-26 (dashboard PR: ledger-grind r3) — ads-side geo language now reads `service_area_label`, as a REFUSAL rather than a generator.▸2026-08-26
Premise re-verified: still zero ads-side consumers at ad0a08ba. But the deterministic RSA angle banks CANNOT produce the contradiction — they interpolate only {City}, and those cities come from the operator's own declared service area, which is the same rung as the asserted label. The contradiction enters through overlayLlmCopyOntoGraph, where free-text AI copy replaces the unpinned slots. So lib/ads-geo-language.ts reads profile.service_area_label.value (the exact cell client-facts.ts and the LP cutover read) and the overlay DROPS an AI line naming an administrative area the asserted label does not — the Brentwood defect verbatim: "Williamson County" refused against "Greater Nashville". Bank copy fills the slot, so a refusal costs an ASSET, never a headline count, and each refusal is reported ONCE on the existing contentRuleFlags operator-review channel. Deliberately narrow: only county/parish/borough/metro nouns are policed — the targeting-speak register the operator rejected by name — because a general place-name test needs a gazetteer and a false refusal costs a real live headline. NO asserted label => nothing is ever dropped and the overlay is byte-identical to before (pinned in test). Inventing an {Area} copy token was considered and rejected: it would have changed live ad copy fleet-wide to fix a contradiction only AI copy creates. __tests__/ads-geo-language.test.ts, 13 tests.
✓Assistant altFromFilename value quality▸2026-07-28
(FIXED 2026-07-28, assistant PR #241 merged+deployed: values now composed from page context, 1089→0 filename-equal fleet-wide, verified live) (found 2026-07-28 during snippet v6 replay): for many junk-alt images the assistant's own fix VALUE equals the filename (car.png → "car"), so snippet v6 correctly declines them as no-ops — 14 of 15 at kothmann. The 1460-image backlog only partially converts until altFromFilename generates real descriptions (or fixes are regenerated from page context). Assistant repo, separate scope.
✓Mixed-build 500s: build-in-place is the defect class▸2026-07-28
(FIXED: PR #1430 atomic safe-build, proven 0/400 missing-manifest samples vs 400/400 before; first live swap observed) (2026-07-28, bit twice today): concurrent sessions running next build in /home/ubuntu/studiosight-dashboard while the service serves from the same .next caused "client reference manifest does not exist" 500s (/seo/activity) and two crash-loops (partial .next at restart). Fix the class: build to a temp dir and atomically swap (or Next output: standalone + versioned deploy dirs), so a running server never has its chunks replaced underneath it. Related: C23 in process/defect-classes.md.
✓STALE, verified 2026-08-26 — already fixed on 2026-07-28, the same day this was filed.▸2026-08-26
app/api/seo/sites/[oid]/wp-apply/route.ts has no CONTENT_FIELDS: it carries SEO_FIELDS = ['meta_title', 'meta_description'] and a REFUSED_FIELDS = ['content','post_content','article','html'] list that returns a loud 422 BEFORE any network call, naming the assistant's create-page upsert as the endpoint that CAN write a body. The route header records the same reasoning this entry asks for (update-page never echoes post_content, so a content write could only ever be unverifiable or a full-body REPLACE that destroys a builder layout). Covered by __tests__/wp-apply-dry-run.test.ts (6 passed). No code change needed.
✓FIXED 2026-07-29 (PRs #1450/#1451/#1460 — evidence chips + rung-ladder canonicalization; verified serving on Aloha/Brentwood/Trek).▸2026-07-29
(was: Trust chips are fleet-generic filler** (2026-07-28, from operator's Aloha rating): "Local team" / "Clear next steps" / "Fast response" come from the vertical copy banks' floor and say nothing to a comparing buyer. Right fix is chips-from-evidence: derive chips from declared risk-remover kinds + niche-bank anchors (e.g. window cleaning: "Inside & outside", "Frames & sills included"), never invented per client. Design change, separate scope from PR #1436.
✓DONE 2026-07-29: deployed, 12 clients flagged, fleet filled + served-verified.▸2026-07-29
(was: AI content-fill: deploy + first live client** (2026-07-29): the missing AI half of the Builder copy system is BUILT AND MERGED — assistant PR #248 (endpoint + direct-response prompt from the operator's LP/ads templates) + dashboard PR #1441 (regen wiring behind lp-content-fill flag, per-OID, default OFF; every slot validated by applyContentFill). Offline acceptance PASSED on Aloha (mechanism-led hero accepted, numeric chips correctly rejected). REMAINING, both operator-gated: (1) restart studiosight-assistant to serve the endpoint (merged code inert until then); (2) flip lp-content-fill for the first client (recommend Aloha 174773) and regen. Historical finding for the record: the dashboard has called this endpoint since the Builder shipped and it NEVER existed — every fill silently degraded to floor copy (C21 class).
✓DONE 2026-08-26 (dashboard PR: ledger-grind r3) — `certified_or_accredited` is its own kind.▸2026-08-26
certified/accredited left licensed_and_insured (which now detects insured/bonded only), so Brentwood can say the true thing without being handed the unevidenced one; the "authorized" workaround can be retired at the next copy pass. Detection is narrow on purpose — never the bare word ("certified organic", "certified pre-owned" are refused); every pattern requires the credential attached to the business or its people (named issuing body, practitioner/dealer noun, or the accreditation vocabulary, which has no consumer-goods reading). MIGRATION: IMPLIED_KINDS.licensed_and_insured now implies certified_or_accredited, so every existing holder of the stored grant keeps every word they could say the day before — one direction only, since the reverse is the conflation being undone. The load-bearing decision: the new kind renders NO CHIP (chip: null). Most clients reach it via the migration, and the evidence behind that grant may have been the insurance half alone — a chip would have stamped "Certified & Accredited" onto live pages for clients who never showed a certificate, which is fabricated provenance and a strictly worse defect than the conflation. Caught by the existing five-grant fixture test during the run, not by reasoning after it. Nothing on any page gains a chip. __tests__/risk-remover-certified-split.test.ts (14 tests) plus updated expectations in risk-removers / risk-remover-evidence / risk-remover-regulatory / lp-risk-remover-copy, where the single insurance-exclusivity invariant became two (insurance language to licensed_and_insured alone, credential language to certified_or_accredited alone).
✓RESOLVED 2026-07-29: Cincinnati has NO brand palette to match — their site is a Ratesight/SimpleSight template on Bootstrap default #0d6efd; template LP colors are not a mismatch. Operator may supply colors if the client has offline brand guidelines. Rhino still empty-colors but no live LP.▸2026-07-29
(was: Brand-color kits: 2 remaining gaps) (2026-07-29, from operator "the blue doesn't match their logo"): Valley + H&U fixed from site-verified palettes (PR-less data fix + regen). REMAINING: Cincinnati/Premier Kitchen 174719 — plain fetch of cincinnatiapplianceinstall.com yields zero brand-ish colors (JS-rendered or neutral site); needs the browser-render scrape or operator-supplied colors before their pages leave template blue. Rhino 2402010 also empty-colors but has no live LP. Class prevention idea: brand-kit save should flag empty brandColors on any client with a live LP.
✓RESOLVED 2026-07-29 04:47 UTC — deployed via the guarded pipeline (staging-validated 11d4201d, health PASS, .last-good-deploy updated)▸2026-07-29
(was: Dashboard restart pending — on-disk build diverged from running service) (2026-07-29 ~04:00 UTC): the running dashboard (:3002) started 01:13 UTC on the authorized bfef53c1 build, but session build-verification runs overwrote .next with current-trunk builds — a crash/watchdog restart would now silently deploy trunk. Trunk is GREEN (7881 tests, all today's changes served-verified via the CLI regen path). RECOMMEND: operator authorizes a deliberate systemctl --user restart studiosight-dashboard onto current trunk (also activates today's new routes: content-fill in-service, ads-lp comparator, risk-remover confirm on latest code). Until then, avoid relying on new dashboard ROUTES in the running service; CLI paths are current. PREVENTION: future build checks should build into a scratch dir (BUILD_DIR/--outDir) or a worktree with its own node_modules, never the production .next.
✓RUN 2026-08-26 (overdue). The honest answer is NO — nothing measured improved.▸2026-08-26
Two independent reads, both against raw rows.
✓"Fix all remaining items" wave COMPLETE (2026-07-29 ~05:30 UTC)▸2026-07-29
(1) Horn Law — 9 zoning/land-use/code-violation PHRASE negatives applied to live campaign 23914735589 (validateOnly previewed; reversible). (2) Goliath — live search ad 801119744113 final URL flipped http://goliathmoving.ca/ -> https://goliath-local-residential-moving.pages.dev/ (destination 200 verified; completes click->gclid->server-upload loop; watch first lead). (3) Valley — free_no_obligation_quote DECLARED (operator tier), all 4 live RSAs rewritten through the claim gate: savings/same-day/no-pressure/in-minutes claims OUT, "Shop 15 Carriers at Once"/"One Form, 15 Carriers"/"No Calling Each Provider" IN (gate refused first pass until evidence landed — working as designed). (4) White Rose — refreshed to evidence-led copy v20; the logo-mismatch blocker was the IPv6 flake, bytes identical to approval. (5) Popup — ALREADY LIVE fleet-wide: the "0 of 29" premise grepped the LEGACY marker (lp-popup-config) which structured pages never emit; correct signal = .lpb-popup + data-exit=1, verified serving on all 28 active pages, exit-intent only, single capture surface by construction. Zero popup mutations needed.
✓SNAPSHOT→OUTCOME program (operator directive 29-07)▸
ALL 4 PHASES LIVE 29-07 (PRs asst#257, dash#1461/#1462): 100% baseline-or-reason attribution, 42/42 clients baselined (t0-on-add live, 40 legacy marked late), 16 Google Ads routes ledgered, daily 12:46 sweep + by-client card; first run graded 1303 changes (5 gradable: 3 regressed/1 improved/1 flat — investigate the 3 regressions). Follow-ups: thread campaignId through keywords/ad-assets/ad-final-url/restructure rows; SEO playbook reader is the next build.: P1 baselines on every SEO change (assistant, applyOneFix + outcomes no-baseline verdict + ageDays floor) · P2 snapshot-on-connect (client-profile/save trigger + identity-join re-capture + hasBaseline badge + late-backfill script) · P3 change-outcomes morning-sweep area + UI surface (engine merged PR #1195, zero consumers today) · P4 Google Ads mutation ledger (zero ledger rows today). All three agents launched 29-07; deploy+verify as PRs land; after deploy run P2 backfill. This completes vision loop stages 2+7; SEO playbook (the reader) is the build after.
✓BOTH HALVES DONE, verified STALE 2026-08-26▸2026-08-26
(2026-07-29, surfaced by the operator seeing "2 separate Aloha lead pages"). (a) PORTFOLIO DEDUP: app/landing-pages/_landing-pages-client.tsx no longer carries the "migrated is staging-only" assumption — it calls splitMigratedRows and renders ONE ROW PER PAGE, with a migrated record keyed to a listed legacy project shown as that page's saved-draft badge rather than a second page; only STANDALONE migrated records get their own row. (b) THE CUTOVER: structuredRegen composes from the migrated draft when one exists — loadMigratedRecordResult feeds operatorDraft into StructuredLpParams, and an existing-but-CORRUPT record REFUSES loudly (422) rather than silently regenerating without the operator's edits, which is the stronger version of what this entry asked for. Re-read at ad0a08ba; no code change needed.
✓DONE 2026-08-26 (dashboard PR: ledger-grind r3) — comparator tokenizer refinements.▸2026-08-26
(1) STEMMING: themeStem folds plurals and gerunds onto one form (mover/movers/moving), applied at COMPARISON time in messageMatch and for slot dedup in themeTokens, so the operator still reads "movers" and not "mov". Suffix-only, no dictionary, no irregulars — every extra rule is a chance to collide two genuinely different words, and a false MATCH here hides a real message-match failure. A trailing -e fold was tried and REVERTED: it made "movers" equal "move", which stopped a "Piano Movers" ad group raising V-11 against an "Affordable Help For Your Next Move" hero that says nothing about pianos — caught by lp-ad-match-validate, and the ledger's case is movers/MOVING anyway. (2) BOILERPLATE: an AD_GROUP_NAME_BOILERPLATE stop-set applies to the ad-group-NAME source only, so H&U's "Ad Group Optimized" stops leaking optimized into its theme, while the same word in a keyword or pinned headline still counts as the ad's real subject. (3) re-measure Goliath/H&U after Google read-lag clears — STILL OPEN, an observation task. __tests__/ads-lp-theme-tokenizer.test.ts, 10 tests.
✓GOAL: Snapshot→Audit→Playbook completion▸
BOTH WAVES LIVE 29-07: W1 localRank-in-t0 (#1467) + quarterly client audit w/ 30d link + real PDF (#1470, Kothmann first report verified, timer 16:41 daily); W2 durable playbook entries (#1472, 11 entries/9 niches, timer 04:45 daily, promote/demote + approvals-queue bridge). Deferred by decision: Bing. Next natural build: promotion→versioned-bank PR generation; localRank CORE_SECTIONS decision.
✓FIXED 2026-08-26 (PR #1995, awaiting review).▸2026-08-26
Both halves the entry asked for are in lib/seo/seed-drop-guard.ts and wired into the route: (a) refuse-or-warn - a persist: true that would stop tracking any currently-tracked keyword now returns 409 would-shrink-tracked-set, naming every keyword at stake, and proceeds only on an explicit acceptDrops: true (a truthy value is not consent; only literal true counts). Refusal, never repair: nothing re-adds a dropped term, because the hygiene reason for dropping it may be right. (b) echo the resulting tracked list - every response, dry run and persist alike, now carries tracked.before / tracked.after plus hygiene.added, and the dry run also returns wouldRefuse so a caller can ask the guard's question without writing. Normalization is shared, so a formatting difference never reads as one drop plus one addition. 13 unit tests pin the original incident's three brand terms. Shipped as PR #1995, awaiting review; merged code is inert until the dashboard next ships. The SAB-first-place note in the original is still open and is now tracked on the H&U line above. ORIGINAL: seed-keywords persist semantics pruned existing brand terms while echoing 0 seeds (29-07, caught+restored within minutes by post-persist verification): a persist:true call on 170652/172658/174697 silently dropped tracked brand terms ('one two tree', 'confidently fit', 'raven finley', 'brentwood blind*') and the response gave no indication. Restored via targets PATCH, verified. Route needs: (a) refuse-or-warn when persisting would shrink the tracked set, (b) echo the resulting tracked list, not just seeds. Also minor: SAB ladder picks FIRST service-area place (174697 resolved Cary; Raleigh arguably primary) — consider population/GSC-demand ordering.
✓SELF-IMPROVEMENT LOOP CLOSED END-TO-END 29-07▸
automatic promotion observed working on real data: reconcile detected+repaired a false 'promoted' state, policy re-decided (n=7 side-by-side clears the 5 required), and PR #1501 opened automatically adding 'Standard' to PROMOTED_NICHE_AD_STYLES.movers with provenance (entry id, n, evidence strength, confidence, date). Second run: 0 promote / 0 reconcile / 12 hold = stable, no churn. Chain of fixes required to get here, all merged: feeder lanes #1495 (Google BEST-label never populated -> winnerAsset w/ basis; Meta ad-style lane), typecheck runner #1498 (service PATH had no node -> false 'does not typecheck' forever; now typecheck-unavailable is distinct), state atomicity #1500 (status flipped before PR landed -> evidence-based reconcile). REMAINING: merge #1501 (or set PLAYBOOK_AUTO_MERGE); demotion-PR-failure has no auto-retry (known gap, own fix).
✓PR/merge sweep 2026-07-30 (operator-authorized): main is now level with the production trunk.▸2026-07-30
Before: 39 open dashboard PRs, main 1825 commits behind fix/conversion-upload-monitor, and 5 branches whose only copy of the work was an uncommitted worktree. Done: (1) preserved the 5 never-pushed branches as labelled wip(...) snapshots — fix/risk-remover-certified-split (8 libs + a new test), fix/oid2-qa-artifact-noindex, fix/brentwood-geo-targeting, feat/seo-scorecard, docs/defect-classes-split-c8. (2) fast-forwarded main twice (#1204, then #1512) — main is a strict ancestor of the trunk, so nothing was lost. (3) merged, cross-repo pairs shared-lib-first: tools#99→dash#1507 (copy spec + claim gates incl. the invented-CPL gate), tools#100→dash#1509 (placements = every lead-form surface EXCEPT Marketplace + Audience Network; Advantage+ audience ON) → dash#1510 (include/exclude audience pickers on /build). (4) merged 3 real auto-bug fixes still unfixed on trunk: #975 /alerts force-static→dynamic, #878 /email React #418, #952 /api/comments 25s cap. (5) closed 10 with per-PR evidence (symbol-sampled against trunk): #711/#712/#724/#949 landed, #856 docs-only, #967/#1014/#1015/#1016/#1018 dup-or-false per the auto-bug ledger. Verification: typecheck clean, full suite 8355 pass / 1 fail — the one failure is the cross-repo e2e proving the live shared lib is not yet synced. NOT DEPLOYED, three gated follow-ups: (a) sync /home/ubuntu/studiosight to its merged main + restart studiosight-meta, or ad-set creation keeps using the OLD placement floor and advantage_audience: 0; (b) build + restart the dashboard; (c) purge the Cloudflare cache for /alerts and /email.