DigitalArchitect β€” Build Kanban

Coreshift Γ— Growth Partners Β· live build status

Last updated 2026-05-12
repo Β· plan
Build phases
M0 Foundation
M1 Project loading
M2 DA engine
M3 Client dashboard
M4 Polish + GrowthCast

Ricky’s tasks

Action items

Things that need a human (you) to click through a sign-up flow or paste credentials. Nothing here is technical. Don’t worry about the “Later” column β€” Claude will tell you when each one becomes time-sensitive.

Do now

App is client-ready on staging (13 May). Second-wave reskin landed — every inner surface is now glass-themed end-to-end. Ticker, work-board, results-charts (dark recharts palette), ACT funnel, benchmark finding renderers, work-items panel, all 5 project-workspace panels, new-project form, knowledge-base chat — all reskinned. The legacy bg-emerald-100/dark:bg-emerald-950 badge patterns are gone; all status / category / priority surfaces use the <Pill> primitive in DA tones. Tech-debt cleanup also done (PR #11): staging β†’ dev back-merge brought the ga4-sync hotfix into dev so the next promotion won’t silently undo it. Hand to the client. Staging URL https://staging.digitalarchitect.co.nz (GP) + storepro.staging.digitalarchitect.co.nz (read-only client view) are both serving the new build. Ask them to play with it and feed back β€” that list becomes the brief for the M2.5b polish / M3.4 GP work-mgmt / M4.3 PDF redesign / M4.4 cost dashboard / M4.5 realtime cycle. Production cutover (staging β†’ main + Cloudflare wildcard DNS for *.digitalarchitect.co.nz) is the next discrete step but only when the client signs off on the experience — no rush.

πŸ“– Read the Storepro deliverable side-by-side with GP’s actual ~45 min Β· the brief-for-everything-next moment

The review screen for run d0ef9677 has all 10 sections + 215 work_items + the PDF. Open the GP team’s actual Storepro deliverable next to it. For each section: where does ours match Β· where does it miss Β· where does it find things the human analyst missed. That comparison is the brief for M2.5b (review screen polish) + M3.4 (GP work-management UI) + tells us which sections are good enough vs need sharpening.

πŸ“… Schedule the 30-min walk-through with the GP team ~5 min to schedule Β· then 30 min

Was on the “Later” list, now triggered. Walk GP through the app + the Storepro output. Calibrate which sections they want sharpened, which to drop, which they’d add. Their feedback is the actual brief for M2.5b polish.

πŸ” Rotate exposed credentials after benchmark β€” 15 min

Several secrets pasted into chat history during the build: Supabase service role, DataForSEO password, Anthropic / OpenAI / Perplexity / Gemini / xAI keys, PSI key, service-account private key. None are publicly exposed β€” only inside the Anthropic conversation log. Once the benchmark run is in the bag, rotate each: regenerate in its provider console β†’ update .env.local + Railway env vars β†’ delete old key. Don’t do this before the benchmark since rotating the SA private key would temporarily break the DWD impersonation flow until the new key is wired up.

Later β€” heads-up only

Each becomes time-sensitive when its milestone starts. I’ll prompt you. No action required now.

Asana OAuth app + workspace handover M3.4 starts

For pushing approved work_items into Asana so GP staff work from their existing tool. Two ways: (a) register an OAuth app in Asana developer console (preferred β€” per-user auth, multi-team), or (b) generate a Personal Access Token from a GP admin account (faster, single-team only). Plus confirm the Asana team / project hierarchy that DA tasks should land in. Pick one, paste credentials back to me.

Postmark sign-up + sender domain verify when client emails start mattering

For DA-complete + weekly digest + change-alert emails. Sign up β†’ verify digitalarchitect.co.nz as a sending domain (DNS records β€” you control them via Cloudflare) β†’ grab the server token β†’ paste back as POSTMARK_SERVER_TOKEN. Pipeline already gracefully no-ops without it; adding it just turns notifications on.

Cloudflare wildcard DNS for client subdomains M3.1 starts

Add wildcard *.digitalarchitect.co.nz A or CNAME pointing to Railway production, so each client gets storepro.digitalarchitect.co.nz, etc. I can do this for you via the Cloudflare MCP if you grant access β€” otherwise the manual flow is in Cloudflare β†’ DNS β†’ Add record.

Decide on the Storepro pricing / contract for DA

Foundation decisions said DA = $10k NZD with 10% margin (~$9k NZD spend headroom). Once we have a benchmark output to show GP, finalise: do they sell DA to Storepro at the same price? Same scope? What’s the contract pattern (one-off DA β†’ optional Growth Plan)? Inputs to this would be: how the benchmark output reads, the actual cost-per-run we’re seeing, and what GP wants to commit to.

Sentry auth token prod releases

Optional. Makes production stack traces readable in Sentry by uploading source maps on deploy. Get from Sentry β†’ Auth tokens, scopes project:releases + org:read. Without it Sentry still captures errors β€” they’re just minified. Skip until a real prod incident makes it pay off.

Schedule a 30-min walk-through with the GP team after benchmark run

Once we have the Storepro benchmark deliverable in hand, walk the GP team through the app + the output. Calibrate which sections they want sharpened, which to drop, which they’d add. That session is the brief for M2.5b polish (review-screen approval workflow, edit-before-export, etc.).

Build board

M0 Foundation M1 Project loading M2 DA engine M3 Client dashboard M4 Polish βš™ Setup / external
Backlog
13
M5REPORTING-RUN

Reporting Run β€” lighter monthly pulse between DA cycles

New cadence concept. A full DA run captures the strategic picture once per contract cycle (3 or 6 months). In between, a Reporting Run fires monthly to compare current data/results against the last DA baseline and surface what's moved, what's verified, and what's drifted. Lighter than a full DA β€” no synthesis section LLM passes, no PDF render β€” and feeds the client read-out without burning Anthropic Opus tokens. Inputs: GSC + GA4 deltas vs the DA baseline, AI-search visibility deltas (M3.5b/M3.6 already capture monthly snapshots), ranking shifts on tracked keywords, competitor-delta items (Cloudflare Worker β†’ competitor_content_deltas), work-item verification probes (M3.5 measurement worker already runs these β€” Reporting Run reads + summarises). Outputs: per-pillar progress summary (work items shipped / verified / overdue under each of Technical Β· On-page Β· Off-page Β· AI), top movers + drifters on rankings + visibility, "what we shipped since last report" feed. UI: a tighter version of the DA run page β€” same 4-pillar tab structure but reporting tiles per pillar (deltas, work-item attestation, charts). Could share the executive-summary slot at the top. Trigger: Inngest cron monthly, plus on-demand from the GP project page. Data substrate already exists from M3.5 (measurement worker), M3.5b (competitor poller), M3.6 (ticker aggregator); this builds the aggregation + view on top. Schema-wise probably needs a reporting_runs table with baseline_da_run_id + finished_at + a JSON payload of the deltas. Why it matters: clients see continuous progress without waiting 3-6 months for the next DA; GP can stand up monthly client calls on hard data instead of vibes.

M4M4.4-fk

Cost ledger β€” propagate da_run_id across Inngest step boundaries

Run #7’s post-run audit surfaced that every api_usage row had da_run_id = null despite the pipeline body being wrapped in withCostContext({ daRunId, projectId }, …). AsyncLocalStorage doesn’t survive across step.run() callback boundaries β€” Inngest’s durable-execution model creates a fresh async context per step. The cost rows still land with the supplier + operation + $ amount, so global rollups + per-supplier + per-day dashboards work fine β€” but per-DA-run breakdowns can’t join back to da_runs. Fix: drop AsyncLocalStorage; accept an explicit { daRunId, projectId } parameter on the instrumented call sites (Anthropic callTool, DfS dfsPost, AI search singleEngineCall). The DA pipeline already has both IDs in scope at every step. Out-of-band callers (cron, ticker) pass null as today. Cheap mechanical change; just hadn’t shown up until we ran the audit query.

M4M4.6-pol

Tier 3 polish β€” per-domain pages rollup is empty

Run #7’s competitor_content_analysis finding has perDomain[].pages empty for every competitor (and the client), even though shared.topSupportingPages is correctly populated with the same data cross-domain. The shared-themes aggregation works fine β€” gaps + shared themes have real URLs and etv values. The per-domain breakdown (intended to show “competitor X’s top 5 pages”) needs the per-domain serialization fixed. Minor β€” review screen + PDF render off shared, not perDomain; this is for the “dig into a single competitor” drill-down view.

M4DB-DRIFT

Schema drift β€” ga4_snapshots.start_date / end_date declaration

Drizzle packages/db/src/schema/da.ts declares start_date + end_date as text, but the live DB columns are date. The GA4-FIX card above unblocked the runtime by passing valid YYYY-MM-DD strings (which both types accept). Align the schema declaration to date({ mode: ‘string’ }) so Drizzle’s types match reality and any future inserts can’t silently pass a non-date string again. No data migration needed.

setupPMK

Postmark account

For DA-complete + weekly digest emails. Pipeline gracefully no-ops without the token.

M2M2.5b

Internal review screen v2 β€” section approval workflow

Per-section approve / reject / edit-before-export. Side-by-side “client says vs data shows” view. Brief in the M2.5a screen + the work-items panel; this layer adds the explicit GP sign-off + edit history before a PDF is finalised.

M3M3.4

GP work-management UI in the app (replaces Asana sync)

Decision 2026-04-30: scrap Asana sync. GP team will work natively in the app. The GP portal needs full work-management UX: assigned-to, due-date editor, kanban move-between-columns, comments / activity log, completion checkmark with optional “completed by” metadata. The work_items table already supports the lifecycle; this just adds richer UI on the GP project pages. external_provider + external_ref stay for future tracker integration if a client demands one.

setupPROD

Production cutover (Railway prod + Cloudflare DNS for *.digitalarchitect.co.nz)

When we onboard the first paying client tenant: spin up Railway prod service connected to main branch, add custom domains app.digitalarchitect.co.nz + *.digitalarchitect.co.nz, get CNAME targets, add DNS records (we already have a CF token with DNS:Edit). Plus consider splitting Supabase to a separate digital-architect-prod project for Auth/Storage isolation.

M1M1.5

Google Drive folder watcher (deferred)

Drag-drop covers ingestion 95% β€” Drive watcher deferred until GP says they want it. Schema + UI hooks already in place.

M4M4.5

Live UI β€” automatic state updates without refresh

App-wide real-time updates: file processing status pills, DA-run progress + section findings appearing as they land, ticker items, task completions. Likely Supabase Realtime subscriptions on the relevant tables + per-page loading skeletons during transitions. Removes the “refresh to see what’s happening” UX friction.

M4M4.3

PDF template redesign

Up next
2
verifiedBENCHβœ“7 May

Storepro benchmark DA run β€” first clean end-to-end βœ“

Run d0ef9677 succeeded in ~47 min after the M3.9 resilience refactor. Output: 10/10 synthesis findings (brief, SWOT, opportunities, IA, content, agentic, brand, performance snapshot, site audit, FAQ), 125 ai_search_runs (clean 5Γ—25, zero retry overshoot), 215 work_items, GSC + GA4 snapshots, site audit, PDF rendered. Heartbeat tracked us live through every step transition; no failures, no retries, no container blips. Customer dashboard at storepro.staging.digitalarchitect.co.nz now populated with real data. Side-by-side review against GP’s actual Storepro deliverable is the brief for M2.5b polish + M3.4 GP-UI shape.

M3M3.4

GP work-management UI in the app

Now that Asana is scrapped, the GP portal needs richer work-management: assigned-to, due-date editor, comments, completion attestation. Built on the existing work_items lifecycle.

In progress
0
Done
51
verifiedRESKIN-V2βœ“13 May

V1 reskin part 2 β€” every inner surface client-ready on staging βœ“βœ“

Round 2 of the visual reskin β€” covers every inner component a client could click into, so the staging URL is ready to hand over for them to play with and feed back on. Client dashboard (storepro.staging.digitalarchitect.co.nz): ticker uses <Pill> kind-tags + glass card per item; work-board swaps category pills + glassed columns + eyebrow headers; results-charts gets a dark recharts palette (violet/teal on rgba grid lines, glass-treated tooltip, ink-2 ticks) + glass table for top queries; act-metrics uses eyebrow + display-font KPI tiles with accent dots per funnel stage. Benchmark report: ten hard-coded bg-emerald-100 / dark:bg-emerald-950 badge patterns across STATE / STAGE / RANK badges replaced with <Pill tone="…"> β€” the finding renderers (brief / SWOT / opportunities / IA / content / agentic / brand) now read on glass without the light-mode colour clash. Work-items panel: full rewrite with Pill tones for status / priority / effort / impact, glassed category sections, dark-themed edit/reject forms with violet focus rings. Project workspace inner panels (/projects/[id]/_components/): assumptions (glassed category cards), connected-accounts (Pill for verification status, dark dropdown instructions), client-access (Pill for password-set state, mono URLs), da-run-panel (Pill + LiveDot for in-flight runs, mono timestamps), file-upload (violet drop-zone hover, rose error band). Other: /projects/new form glassed, /chat page + chat-ui get a glass chat surface with dark composer textarea and gradient role badge. Path to live: PR #12 (feat β†’ dev), PR #13 (dev β†’ staging), Railway redeploy. Total: 14 files, 719 insertions, 804 deletions (net leaner because lots of inline className lists collapsed into Pill/Eyebrow/GlassCard primitives). Smoke: pnpm typecheck green, pnpm lint shows only pre-existing warnings. Together with the V1 reskin card below, the entire app β€” login, Today HQ, project workspace, benchmark report, work items, knowledge-base chat, new-project form, client passphrase gate, client read-only dashboard β€” is now glass-themed end-to-end. Ready to hand to the client.

infraBACK-MERGE13 May

Tech-debt cleanup: back-merge staging β†’ dev (ga4-sync hotfix)

Closes the divergence between dev and staging created on 12 May when the ga4-sync hotfix (4d6de0d) was applied direct to staging without flowing back. Without this, the next dev β†’ staging promotion would have silently undone the fix and broken ga4-sync again on the next DA run. Fix: PR #11 (staging β†’ dev) merged. Touched files: only apps/web/lib/google/sync.ts (12 insertions, 2 deletions) β€” no conflicts with parallel UI work. Both branches now in sync.

verifiedRESKINβœ“12 May

V1 visual reskin β€” DA glassmorphic design system live on staging βœ“βœ“

The UI sprint kickoff, now live at https://staging.digitalarchitect.co.nz. Glassmorphic dark "future-tech HUD" system from the design_handoff_digital_architect bundle ported into the live Next.js + Tailwind + shadcn codebase as an in-place reskin β€” every existing route, data wiring, auth flow preserved. Shipped: (1) Foundation β€” app/globals.css with full DA token system (3 surface tiers, 5 ink tiers, 5 accents β€” violet/blue/teal/amber/rose, 2 radii, fixed grid + noise overlays, body bg gradients); shadcn CSS-var contract mapped to DA palette so existing primitives inherit automatically; Space Grotesk + Inter + JetBrains Mono wired via next/font; Tailwind config extended with DA semantic tokens. (2) shadcn primitives updated β€” Button default is now violet-gradient w/ sheen + glow; Card renders as glass; Input is dark fill + violet focus ring. (3) DA primitives added in components/ui/ β€” GlassCard (3 variants), Eyebrow (mono uppercase + violet dot), Pill (5 tones), ScoreRing (SVG donut + 60 ticks + glow filter), Sparkline, BarsChart, KPI, SectionHead, BrandMark (conic-gradient square), LiveDot. (4) App shell β€” new (gp)/_components/app-shell.tsx with 220px sidebar (brand mark + nav + user footer + sign-out) + 56px topbar (breadcrumbs + ⌘K search stub + bell). (5) IA pivot per design-flow critique β€” GP "Re-run" no longer a sibling screen (compare becomes view-mode on Benchmark, future work); "Client report" β†’ "Share" semantics; /projects upgraded from plain list to Today HQ with signal tiles (benchmarks ready / in flight / awaiting first run) above the project list. (6) Screens reskinned in place β€” /login (Google-OAuth-only, HUD corner brackets, glow orbs), /client/[subdomain]/login (passphrase gate w/ concentric dashed rings + trust strip), /projects (Today HQ), /projects/[id] (eyebrow + display + section heads), /projects/[id]/da/[runId] (status pill + live-dot for in-flight), /client/[subdomain] (sticky tenant header w/ LIVE pill + PDF button over glassed hero stats). Path to live: PR #7 (reskin β†’ dev), PR #8 (dev β†’ staging), Railway redeploy. Plus PR #9 / #10 hotfix below for the Today-HQ render. User-verified: “V1 is looking incredible.”

UIRESKIN-fix12 May

Fix: Today HQ β€” convert lastRunFinishedAt to Date before formatting

First post-reskin staging hit: /projects threw an Application-error boundary with TypeError: lastRunFinishedAt.toLocaleDateString is not a function. Root cause: drizzle returns raw SQL subquery columns as ISO strings, not Date objects, so the render-site call failed. Two-part fix: wrap the value in new Date(…) at the render site, and correct the sql<Date | null> annotation on the subquery to sql<string | null> so the type reflects reality. PR #9 (fix β†’ dev), PR #10 (dev β†’ staging), Railway redeploy — total cycle ~9 min from bug report to live.

verifiedRUN#7βœ“12 May

Storepro DA run #7 β€” backend validated end-to-end βœ“βœ“βœ“

The full-backend validation pass the user asked for. Run 71273775 SUCCEEDED in ~42 min, with every M4 feature exercised against real data. Competitor discovery: DataForSEO competitors_domain returned 10 actual NZ racking/shelving businesses β€” shelvingshopgroup, nzshelving, kiwichoice, stackit, palletrackingsolutions, racknstack, centralconcepts, topmaq, shelfman, dexion β€” vs the earlier SERP-aggregation pulling in newzealand.com / auckland council / yelp. Tier 3 (M4.6): 5 real shared themes (work bench, woodworking bench, industrial pallet racking, pallet rack, industrial shelving) + 1 real client-doesn’t-cover gap (shelf shelving, etv 489 across 7 competitors). AI search loop: 25 queries Γ— 5 engines, 125 calls, zero hangs, ~12 min wall (the 0e7ddbcd-anthropic step that hung run #5 cleared in 13s). ga4-sync: cleared in ~47s after this morning’s date-coercion fix. GrowthCast (M4.1): finding persisted. Cost ledger (M4.4): 136 rows / $10.18 total β€” anthropic 33 calls $8.89 (synthesis steps $0.30–$0.87 each), dataforseo 53 calls $0.56, perplexity 25 calls $0.43, openai 25 calls $0.30. Findings: 12 total (brief, tier 3, gsc, ga4, swot, opportunities, IA, content strategy, agentic strategy, performance snapshot, site audit, FAQ, growthcast). Cleared the validation gate for the UI sprint. Run viewable at https://digital-architect-staging.up.railway.app/projects/bbf42771-0dc5-498a-aa28-321b5b4b8a4b/da/71273775-2c1a-4a0b-8161-3f4707103530.

M2GA4-FIX12 May

Fix: ga4-sync date coercion β€” literal ‘last 12 months’ into date column

Run #6 failed with invalid input syntax for type date: “last 12 months”. The GA4 snapshot was passing GA4 API convenience strings (‘last 12 months’, ‘yesterday’) straight through to ga4_snapshots.start_date / end_date, which Postgres expects as date. GA4 itself accepts those tokens β€” they’re valid for the API call, just not for the snapshot persistence. Fix: compute the concrete window once at the top of syncGa4 using the existing ymd() helper (now - 365d for start, now - 1d for end), then pass those YYYY-MM-DD strings into the snapshot. The GA4 API call itself still uses the convenience tokens. Surfaced by run #6 being the first run to reach ga4-sync end-to-end (run #5 had hung in AI search earlier). Validated by run #7 clearing ga4-sync in 47s. Schema drift noted as a follow-up: Drizzle declares those columns as text but the DB has them as date β€” runtime fix unblocks; declaration alignment is a separate polish item.

M4M4.4-be12 May

Cost-tracking data layer β€” every paid call writes to api_usage

Backend half of M4.4. The data layer the admin cost dashboard reads. UI lands later; this commit makes the data definitive so the dashboard work is pure render-and-format. Schema: new api_usage table (one row per paid call), indexed by da_run_id + project_id + (supplier, created_at). Helpers: recordApiUsage() (best-effort, never throws), withCostContext() async-local-storage scope (DA pipeline body wrapped, inner calls inherit IDs automatically β€” no plumbing through every wrapper). Anthropic + OpenAI pricing tables centralised. Instrumentation: every Anthropic Opus synthesis call (8 per DA run) via callTool(); every DataForSEO call via dfsPost() using the exact $ from DfS’s envelope; every per-engine AI search call via singleEngineCall(). Query helpers: costByDaRun(), costBySupplier(), dailyCostTrend(), recentRunsWithCost() β€” the four shapes the dashboard renders. Out-of-band callers (cron, ticker, manual snapshot) record with null daRunId for global rollups. OpenAI embeddings + Postmark intentionally skipped (~<1% of total cost; easy to add via same hook).

M4M4.6+12 May

Fix: competitor discovery uses DataForSEO competitors_domain + blocklist hardened

Storepro DA run #3 surfaced that the SERP-aggregation approach to competitor discovery is too broad for niche verticals β€” Storepro (NZ pallet racking) got back The Warehouse Group, Auckland Council, Tripadvisor, newzealand.com (tourism), yelp.com, healthpoint.co.nz. None are competitors; all rank generically for “Auckland storage”-style queries. Two compounding causes, both fixed. (1) Blocklist exact-string match missed www. variants β€” yelp.com was in the set but www.yelp.com slipped through. normalizeHost() now strips www./m./mobile. prefixes before comparing. Set also extended with NZ retail (The Warehouse / Kmart / Briscoes / Mitre10 / Bunnings / Noel Leeming), tourism (newzealand.com / aucklandnz / heartofthecity / getyourguide / eventfinda), council + government (Auckland / Wellington / Christchurch councils + govt.nz / beehive / health / ird / mbie), healthpoint, universities. (2) SERP-aggregation replaced as the primary discovery path with DataForSEO Labs competitors_domain β€” takes the client domain, returns OTHER domains with high keyword-overlap. A NZ pallet-racking company gets back actual NZ pallet-racking companies. SERP aggregation kept as fallback when the endpoint returns <3 domains (small / new client with no DfS overlap data) or transient API failure. Blocklist applied on top either way. competitors.notes records which discovery path was used so GP can see at a glance. Next DA run on Storepro will produce a clean competitor list + relevant Tier 3 themes (run #3 was cancelled mid-AI-search after this diagnosis).

docsGCP-DOC12 May

GCP one-time setup runbook (docs/setup-gcp.md)

Captures the GCP project-level setup that makes every future client onboarding just “add access@growthpartners.co.nz to the GSC + GA4 properties” (two clicks). Covers: GCP project + SA identity, DWD config in Workspace admin, the four required APIs (webmasters / analyticsadmin / analyticsdata / iamcredentials) with one-click enable URLs, plus the legacy OAuth client fallback. Surfaced when this morning’s verify-access hit analyticsdata.googleapis.com 'API not enabled' β€” captured so the next person setting up a fresh GCP project or recreating the SA doesn’t have to rediscover the requirements.

infraDOMβœ“12 May

Staging apex domain bound β€” staging.digitalarchitect.co.nz

Railway staging service now has both the tenant wildcard (*.staging.digitalarchitect.co.nz, port 8080) and the bare apex (staging.digitalarchitect.co.nz, port 8080) as custom domains. Cloudflare wildcards don’t cover the apex β€” needed to add as a separate Custom Domain entry. Verified: bare URL 307s to /projects (GP realm), tenant wildcard still serves the client realm. The prettier GP staging URL is now live alongside the bare Railway one.

M4M4.112 May

GrowthCast β€” 3-year clicks + leads forecast from empirical baseline

The forecast model GP charges for. Conservative / Moderate / Aggressive 36-month projections built from the DA pipeline’s existing baselines: monthly clicks from GSC last-90d Γ· 3, lead rate from GA4 organic conversionRate, compounding-growth model with optional taper (+20% / +50% / +120%-tapered-at-24mo annually). lib/forecast/growthcast.ts is a pure function β€” no DB, no external calls, <50ms runtime. Persisted as a da_findings row (section=‘growthcast’) so PDF + review screen pick it up via the existing findings query. AOV not yet captured in the brief β†’ revenue projections null with a structured notes array surfacing the gap; when AOV lands in the brief schema (or as a GP-fillable project input), buildGrowthcast() already accepts the parameter and revenue lights up across all three scenarios with no further code change. Also handles zero-baseline / zero-lead-rate gracefully β€” model runs with zeros + the notes explain the missing inputs.

M4M4.612 May

Tier 3 β€” Competitor content strategy analysis (Pareto + theming)

Closes a known gap vs GP’s Storepro deliverable: identify the top 20% of each competitor’s content driving 80% of their organic traffic, roll up across competitors to surface dominant market themes + content gaps where the client doesn’t rank but 2+ competitors do. How: new rankedKeywords() DataForSEO Labs helper (/dataforseo_labs/google/ranked_keywords/live, organic-only filter, ordered by etv); lib/seo/competitor-content-analysis.ts pulls ranked_keywords per (client + competitors) in parallel, aggregates by URL summing etv per page, Pareto-trims to pages contributing 80% of traffic (capped at 30/competitor), then clusters keywords across competitors by cheap stem (lowercased + stopwords stripped + plurals normalised + top-3 tokens sorted) into themes, tags each as gap/shared based on client coverage. New Inngest step competitor-content-analysis between discover-and-score-competitors and the synthesis sections; persists a da_findings row + extends SynthesisContext so the content-strategy LLM prompt receives the gaps and is instructed to ground the roadmap in proven competitor pages rather than inventing topics. Cheap-fail: null when DataForSEO is down or no competitors β€” Tier 3 is an add-on, not load-bearing. Cost: ~13 calls Γ— 200 results Γ— $0.075/1k β‰ˆ $0.20 per DA run. Also extends the M3.5b platform blocklist with NZ/AU TLD variants of tripadvisor/glassdoor/indeed/seek/finder.

M3M3.612 May

Auto-ticker β€” fills the customer dashboard from competitor deltas + news + CEO hobby

M3.3’s ticker UI had been rendering an empty state since launch because nothing was writing to ticker_items. M3.6 closes that loop. Two Inngest functions: ticker_aggregator_cron (daily 17:30 UTC β‰ˆ 05:30 NZT, sequenced 30 min after the M3.5b worker) and ticker_aggregator_manual (event ticker/refresh.requested, empty payload = all clients, { clientOrgId } = one). Three sources (in lib/ticker/sources.ts): competitor activity from competitor_content_deltas (last 48h, top 5), market news via Google News RSS keyed off the latest brief’s industry + offerings topics (free, region-aware, no API key needed β€” top 4), hobby items via the same RSS shape keyed off client_assumptions.category=hobby CEO hobbies extracted in M1.3 (top 2). Write semantics: per-client refresh expires the previous auto-aggregated entries (competitor_activity / market_news / hobby β€” gp_curated and milestone are deliberately untouched, human-controlled), inserts a fresh batch with publishAt=now + expireAt=now+7d. URL dedupe against still-active rows so the same article doesn’t flip in and out. Per-client failures captured to Sentry + swallowed β€” one client’s broken sitemap can’t pull down the cron. Inngest function_count 9 β†’ 11. M4.2 deleted as a duplicate.

M3M3.5b12 May

Competitor delta poller β€” Cloudflare Worker live

Cron Cloudflare Worker daily at 18:00 UTC (~06:00 NZT). Asks DA for the active (clientOrgId, competitorDomain) list via GET /api/internal/competitors-to-poll, then sequentially fetches sitemap.xml + a handful of RSS/Atom feed paths per competitor with 8s upstream timeouts. Diffs URL set against per-domain KV state (COMPETITOR_KV, capped to 500 most-recent per competitor), classifies content type from the URL slug (blog/press/product/page), POSTs new entries in one batch to POST /api/internal/competitor-deltas, refreshes KV. Per-competitor failures logged but never escalated β€” next daily run picks them up. Auth: shared Bearer token INTERNAL_WORKER_SECRET (set on Railway + via wrangler secret put, doc’d in .env.example); lib/internal-auth.ts gate fails closed if the env var is missing or under 32 chars. /run fetch handler exposes a manual trigger with the same auth so we can test without waiting for cron. Worker live at https://da-competitor-delta.access-64f.workers.dev. Cloudflare cron caveat: day-of-week ranges (0-4) and lists (0,1,2,3,4) are both rejected; only * works, so we run 7Γ—/week β€” weekend no-ops cost nothing.

infraCI-SYNC12 May

CI auto-sync Inngest after Railway deploys

Both Deploy Staging and Deploy Production workflows now end with a sleep 150s + curl -X PUT /api/inngest step. Previously a Railway redeploy that changed Next.js’s bundle hash would leave Inngest cloud holding a stale function registration; events arriving in the post-deploy window would either sit queued forever or fire against the old container. Cost ~70 min on the first Storepro benchmark before we caught it manually. The new step makes the sync automatic β€” idempotent ({modified: false} when already current) and continue-on-error: true so a transient Inngest API blip doesn’t fail the workflow.

M3M3.97 May

Pipeline resilience β€” per-call AI search + step heartbeat + Sentry

First Storepro benchmark exposed the structural fragility of the DA pipeline. The original ai-search-snapshot Inngest step wrapped 125 sequential external API calls in one ~50-min HTTP request to our container. A mid-run container restart (auto-refresh deploy) killed the in-flight HTTP/2 stream β€” Inngest reported INTERNAL_ERROR; received from peer, the function couldn’t reach its own mark-failed catch, and the DB row sat at status='running' indefinitely with no error message, no Sentry capture, and no progress signal. Refactor scope: (1) new last_step_name / last_step_at / last_error_step / last_error_stack columns on da_runs; (2) lib/inngest/step-trace.ts with a makeTracer(step, runId) helper that wraps every step.run() with a DB heartbeat write on success + structured error capture + Sentry.captureException on throw; (3) every step in da-pipeline.ts goes through trace() (single global rename); (4) function-level retries: 1 β†’ 3; (5) the giant ai-search-snapshot step replaced with ai-search-prepare β†’ 125 ai-search-call-{promptId8}-{provider} sub-steps (5-engines-per-prompt parallel via Promise.all of step.run, sequential across prompts) β†’ ai-search-finalize; (6) mark-failed reads back the heartbeat to compose a richer errorMessage like “at step ‘ai-search-call-abc12345-perplexity’: ECONNRESET”; (7) review-screen “Run in progress” card surfaces lastStepName + seconds-ago, “Run failed” card surfaces failing step + collapsible stack. Engine descriptors don’t survive Inngest’s JSON serialization across step boundaries β€” passed as provider strings + re-resolved via lookupEngine(provider) on the receive side. Result: container restarts now lose at most ~5 in-flight calls instead of the whole 50-min snapshot; failures land with full context on the review screen + in Sentry; AI search runs in ~12 min instead of ~50 because of the per-prompt engine parallelism.

M3M3.87 May

Verify-only Connected Accounts panel (leanseo pattern)

UX refactor of the project-page Connected Accounts panel. Today’s GA4 bug surfaced because the previous panel led with the M3.7 OAuth grant flow (“Connect with Google” / “Retry” buttons) which is redundant under DWD β€” GP onboarding already adds access@growthpartners.co.nz to each client’s GSC + GA4 properties, and our SA reads the data by impersonating that user. The panel now mirrors leanseo’s flow: shows the access account email front-and-centre, gives per-provider step-by-step add instructions, and exposes a single per-provider Verify access button that hits the existing syncGsc / syncGa4 primitives (split into verifyGscAccess + verifyGa4Access server actions). The OAuth grant routes (/api/integrations/connect + /callback) stay in the codebase as a fallback for clients outside GP’s Workspace, just no longer surfaced. Also fixed: panel was surfacing the SA address (da-service@….iam.gserviceaccount.com) β€” now correctly shows access@growthpartners.co.nz, the user the client actually adds.

M3M3.7d7 May

Fix: Railway redirect Location URLs (publicOrigin everywhere)

Surfaced when testing the Storepro tenant’s sign-in / sign-out before triggering the benchmark. Symptoms: entering the password on storepro.staging.digitalarchitect.co.nz landed the browser on the GP “Sign in with Google” page (apex realm); clicking Sign out from the dashboard redirected to http://0.0.0.0:8080/login with ERR_CONNECTION_REFUSED. Same root cause as the M3.7 OAuth callback fix on 30 Apr β€” Railway’s reverse proxy makes Next.js see request.url as the container’s internal listen address, so any new URL(path, request.url) Location header carries the wrong host. Four redirect sites were missed in the original fix: client signout, GP signout, middleware’s already-authed-on-/login redirect, and the post-sign-in server action. Refactored publicOrigin in lib/google/oauth.ts + added publicOriginFromHeaders for server-action contexts (where there’s no Request object directly). All four sites now build absolute URLs against x-forwarded-host + x-forwarded-proto. Verified on staging: signout 303 Location now https://storepro.staging.digitalarchitect.co.nz/login.

M3M3.7c7 May

Fix: GA4 OAuth grant β€” v1beta accessBindings:batchCreate

Bug surfaced when verifying Storepro’s GA4 connection ahead of the benchmark run. The M3.7 OAuth grant flow was POSTing to /v1beta/properties/{id}/accessBindings β€” but v1beta only exposes :batchCreate (the singular create endpoint exists in v1alpha only). Hitting the missing route returned an HTML 404 from Google’s frontend, which got captured into project_integrations.last_error and displayed as raw HTML in the Connected Accounts panel. The bug was masked because DWD (M3.7b) is the primary auth path; the SA-via-impersonation reads worked fine. But each “Retry” click on the panel re-broke the visible state. Fix: URL β†’ accessBindings:batchCreate, body wraps the binding in {requests:[{accessBinding:{user,roles}}]}, idempotency branch (already-bound detection) unchanged. Verified DWD-side via accountSummaries impersonating access@growthpartners.co.nz β€” sees 97 GA4 properties including Storepro - GA4 (properties/355604569). Storepro’s panel row reset from error β†’ connected.

M3M3.7b30 Apr

Domain-wide delegation β€” primary auth path for GSC + GA4

Pivot from M3.7’s per-client OAuth flow to domain-wide delegation. The SA now impersonates access@growthpartners.co.nz via DWD, and that user already has admin on every GP-managed client’s GA4 + GSC. Result: zero per-client onboarding β€” no client emails to send, no consent screens to walk through, no Connect-with-Google clicks. Pipeline calls the same Google APIs as before; bearer tokens just come from a JWT subject-impersonation flow instead of direct SA auth. New: googleImpersonatedClient() + bearerAuthHeaderImpersonated() in lib/google/auth.ts; gscBearer() + ga4Bearer() try DWD first, fall back to direct SA bearer when GOOGLE_IMPERSONATE_SUBJECT isn’t set. M3.7’s OAuth code stays as the legacy/fallback path for any future client outside GP’s Workspace. Setup cost: one-time add of the SA’s client ID to GP Workspace admin β†’ Security β†’ API controls β†’ Manage Domain Wide Delegation, scoped to webmasters.readonly + analytics.readonly. Verified live with Storepro property.

M3M3.730 Apr

Self-serve OAuth flow for client GA4 + GSC grants (superseded by M3.7b β€” kept as fallback)

Originally shipped to replace “send the SA email + walk client through GA4/GSC user-management UI” with one-click Connect-with-Google buttons. Built because Google added a UI restriction in 2026 that hard-validates user emails against the Google Account directory and rejects *.iam.gserviceaccount.com addresses with “This email doesn’t match a Google Account.” Admin API still accepts SAs as access-binding subjects β€” just the UI blocks. Same-day pivot: realised access@growthpartners.co.nz already has admin on every GP-managed client property, so DWD impersonation removes the per-client OAuth dance entirely (M3.7b). The OAuth code stays in place as a fallback for any future client whose GA4/GSC isn’t under GP’s admin reach. Code shipped: lib/google/oauth.ts (URL builder + state JWT + code exchange), lib/google/grant-access.ts (GA4 grant via Admin API; GSC verify since Search Console removed user-add from the API), API routes /api/integrations/connect + /api/integrations/callback, ConnectedAccountsPanel with status-aware buttons. OAuth client User Type set to Internal (locked to growthpartners.co.nz workspace, skips Google’s verification queue). Plus a Railway-specific deploy fix: publicOrigin(request) uses x-forwarded-host headers so callback redirects don’t resolve to the container’s 0.0.0.0:8080 internal listen address. Built with three sub-agents in parallel (~77s + 71s + 148s).

M3M3.330 Apr

Client dashboard layout β€” Work / Results / AΒ·CΒ·T

CEO-facing surface. Reads the substrate from M2 + M3.0 + M3.5 + Tier 2 through one round-trip via lib/dashboard/queries.ts. Three sections: Work board (3-col kanban β€” Up next / In progress / Done last 30d β€” grouped by category with priority dots; cards prefer the latest measurement over expected impact once shipped, so “Up next” advertises the promise and “Done” shows the actual movement); Results vs benchmark (Recharts: organic search performance line chart with dual y-axes from GA4 byMonth + AI-search visibility bar chart per snapshot + last-90d top GSC queries table; each card has an empty-state placeholder prompting to connect the missing data source); AΒ·CΒ·T metrics (Awareness Β· Consideration Β· Transaction funnel sourced from metrics_snapshots with live GSC/GA4 fallback). Plus a top-of-page ticker with kind chips softened for client-facing tone (hobby β†’ For you, gp_curated β†’ From GP). Built with two background sub-agents in parallel (~145s + 65s wall) against typed shapes in lib/dashboard/types.ts; main thread did the data layer + integration. Production build verified at 115 kB for the dashboard route.

deployRW30 Apr

Railway staging deploy β€” live + auto-deploying

First real production-shape deploy. https://digital-architect-staging.up.railway.app auto-deploys on every push to staging branch via Railway↔GitHub. Took 7 build attempts to crack β€” all because Railway sets NODE_ENV=production at install time, so anything Next.js needs at build time (typescript / postcss / tailwind / @types/* / turbo) had to be moved from devDependencies to dependencies. CI didn't catch this because GitHub Actions doesn’t default to NODE_ENV=production. Plus Supabase Auth URL config patched (via Management API + the new ~/bin/da-supabase-token keychain helper) so OAuth callback lands on the staging URL instead of localhost. Branch flow re-established: dev β†’ staging β†’ main all in sync. Production env stays cold until first real client onboarding.

M3M3.1+230 Apr

Wildcard subdomain routing + client-CEO password auth

Two tightly-coupled milestones shipped together. Middleware extended: detects subdomain (prod {tenant}.digitalarchitect.co.nz + dev {tenant}.localhost:3000 β€” browsers resolve *.localhost with no /etc/hosts edits), resolves tenant via cached DB lookup, rewrites to /client/{subdomain}/* route group, gates with JWT cookie. Per-tenant shared password (CEO + team) β€” bcrypt hash on client_orgs.password_hash, JWT session signed via jose with HS256 + scoped to subdomain (no Domain attr = automatic cross-tenant isolation). New routes: /client/[subdomain]/login, /client/[subdomain]/page.tsx (placeholder dashboard), /api/client/signout. New GP-side admin: Client Access panel on each project page shows live + dev URLs + password status pill + inline set/reset form. Smoke-tested: bcrypt hash+verify βœ“, JWT mint+verify βœ“, tampered token rejected βœ“, short-password validation throws βœ“. Foundation for M3.3 dashboard.

M3M3.530 Apr

Measurement worker β€” auto-probe dispatcher + window measurements

Closes the third loop (“did it work?”). Three Inngest functions: (1) daily probe runner β€” for each active work_item with an auto_probe spec, runs the probe and on pass advances status by one rung (approved β†’ in_progress β†’ done β†’ verified); (2) baseline capturer β€” event-triggered when status flips to in_progress, captures current value of expected_impact.metric as the baseline + writes a windowDays=0 measurement row; (3) measurement sweeper β€” daily cron, finds items completed ~30/60/90 days ago and captures a measurement at that window with delta-vs-baseline. Six probe kinds wired (page_exists / schema_present / psi_metric / serp_rank / gsc_query_position) + two skipped on the daily cadence (ai_search_cited via monthly snapshot, manual via human review). All 10 smoke-test scenarios pass against storepro.co.nz. Architecture deviation: chose Inngest over the original Cloudflare Worker spec β€” shares types with the rest of the pipeline, reuses GSC/GA4/SERP/PSI clients, durable per-step retries, existing precedent in monthlyAiSearchSnapshot. Cloudflare still slated for the competitor-delta poller (different shape).

M3M3.030 Apr

Work-items lifecycle β€” the keystone

Substrate for “send work to GP / receive done / measure if it worked.” Every DA opportunity, IA page, content roadmap entry, audit fix, FAQ Q, agentic action + brand format now becomes a first-class work_items row with: status lifecycle (proposed β†’ approved β†’ in_progress β†’ done β†’ verified β†’ cancelled / blocked), expected_impact hypothesis (metric / lift / window), auto_probe spec for verification (page_exists / schema_present / psi_metric / serp_rank / ai_search_cited / gsc_query_position / manual), tags + priority + effort + due-date. New pipeline step generate-work-items emits 30–80 seeds per DA run. New review-screen panel: cards grouped by category (content / technical / strategy / agentic / brand), per-status counts, Approve / Edit (inline form) / Reject (with reason) / Bulk-approve-by-category. Server actions enforce status-transition guards. Plus work_item_measurements table for the per-item baseline β†’ +30/+60/+90d series that powers the “did it work?” loop. Two agents built lib/work-items/ + GP review UI in parallel; main thread handled schema + pipeline integration.

M2M2.929 Apr

Tier 2 β€” GSC + GA4 via service account

Closes the biggest remaining gap vs the GP Storepro deliverable. Service-account auth pattern (no per-client OAuth flow / consent screens / token refresh): GP staff sends each client a one-line instruction to add da-service@… as a viewer in their GSC + GA4 properties. Pipeline now syncs (a) GSC last-90-day clicks/impressions/CTR/avg-pos + top queries / pages / countries / devices, and (b) GA4 organic-channel sessions/users/conversions/lead-rate over last 12 months + monthly trend + top organic landing pages + source/medium splits. Status row in project_integrations tracks per-project connection state (connected / no_access / no_match / error) and shows on the project screen with a “Refresh connection status” button. New PDF page “Performance Snapshot” renders the raw numbers with no LLM call. Synthesis prompts now reference real rankings + real lead-rate when grounding SWOT / opportunities / IA / content / agentic / brand sections.

M2M2.8e29 Apr

SEO Audit Checklist β€” 10-section graded scorecard

Mirrors GP’s Storepro-style audit: Tech SEO, Site architecture, Internal links, Metadata & images, Desktop & mobile loading, Content issues, Schema markup, Web design UI/UX, Inbound links, Algorithmic penalties. Each rubric scored 0–max with A–F grades. PSI + on-page crawl + DataForSEO Backlinks feed the rubrics; heuristic-only sections are flagged for GP review. Renders as a dedicated “Technical SEO audit” PDF page with overall grade pill + per-section commentary + must-fix-now / needs-planning / nice-to-have prioritisation.

M2M2.8d29 Apr

PageSpeed Insights API integration

Lighthouse scores (perf / accessibility / best-practices / SEO) + Core Web Vitals (LCP / CLS / INP / TTFB / FCP) for desktop & mobile, fed into the audit checklist. Soft-fails when no key set β€” pipeline keeps running. Needs Ricky to add PAGESPEED_API_KEY (anonymous calls hit shared IP quota).

M2M2.8c29 Apr

Multi-dim keyword categorisation (3 β†’ 14 dim)

Sonnet 4.6 batched categorisation over the top 200 keywords across GP’s 14 dimensions: intent, category, sub-category, configuration, weight, quality, condition/availability, usage, price, material, NZ location, AU location, audience, journey stage. Categorised data persists on keywords.categories, feeds the per-category visibility split, and surfaces in synthesis prompts.

M2M2.8b29 Apr

PAA mining + FAQ section

Captures “People Also Ask” questions from every SERP query, stored on serp_snapshots.paa_questions. New “FAQ” synthesis section themes 30+ buyer questions into publishable Q&A β€” doubles as on-site FAQ + answer-engine source content. Plus seasonality (12-month volumes) captured per keyword and surfaced in the synthesis context.

M2M2.8a29 Apr

Per-category Visibility Score split

Closes a major gap vs GP’s actual deliverable: instead of one blended visibility number, computes the volume-weighted Visibility Score per category (e.g. “Pallet Racking 67% / Mezzanine 12% / Workbenches 4%”) for the client + top competitors. Far more actionable than a single number.

M2M2.229 Apr

In-house AI-search visibility engine

Replaces Peec AI (no annual lock-in, ~50% cost savings, native pipeline integration). 5 engines: Perplexity sonar-pro, OpenAI gpt-5-mini + web_search, Claude + web_search_20250305, Gemini 2.5-flash googleSearch grounding, Grok 4-fast Live Search. Persisted prompt list per project for stable monthly deltas.

M2M2.129 Apr

DataForSEO β€” keyword research + SERP + competitor discovery

Real empirical data layer. Per run: ~8 LLM seeds β†’ keyword_ideas (top 500 keywords with volume / intent / KD / monthly seasonality / serp features) β†’ SERP organic for top 30 (now also extracting PAA + related searches) β†’ volume-weighted Visibility Score per competitor. Synthesis sections reference real keywords + domains in output.

M2M2.729 Apr

DA-complete email via Postmark

Triggering GP user gets a clean transactional email with the review URL when a run finishes. Graceful no-op until POSTMARK_SERVER_TOKEN is set.

M2M2.629 Apr

Templated PDF deliverable

8-page PDF (cover + 7 synthesis sections) auto-rendered after each succeeded run. @react-pdf/renderer β€” no Chromium dep. Download button on the review screen, signed-URL gated.

M2M2.429 Apr

LLM synthesis β€” 6 sections

Brief / SWOT / Opportunities / IA / Content roadmap / Agentic / Brand. Each step uses Claude Opus 4.7 with tool-use for typed JSON. Bespoke renderer per section in the review screen.

M2M2.5a29 Apr

Internal review screen v1

/projects/[id]/da/[runId] β€” status header, in-flight banner, error rendering, dedicated section components. Polish + approval workflow continues in M2.5b.

M2M2.329 Apr

DA pipeline backbone

Run-DA button + da/run.requested event + multi-step durable Inngest workflow with try/finally status flips. Loads project context, synthesises a project brief end-to-end.

M1M1.429 Apr

KB chat β€” grounded RAG with streaming

"Ask anything about this client" β€” pgvector cosine search over project_chunks β†’ top 6 excerpts β†’ Claude Sonnet 4.6 streamed via @ai-sdk/react. Source filenames cited inline.

M1M1.329 Apr

Auto-extract client_assumptions

Claude tool-use pulls 8 categories (competitors / products / audiences / priorities / pain points / differentiators / geographies / CEO hobbies) with verbatim quotes + confidence scores. Idempotent on retry.

M1M1.229 Apr

File processing β€” Whisper / vision / PDF / docx

Real extraction shipped: audio β†’ Whisper, images β†’ Sonnet vision OCR, PDF/docx/text/markdown handled. Chunking + 3072-dim embeddings into pgvector. Each step durable in Inngest.

setupSentry29 Apr

Sentry wired β€” error monitoring + tracing + replay

All three runtimes (browser/server/edge) instrumented. OpenAI + Anthropic auto-instrumented for AI Monitoring.

verifiedM1.0βœ“29 Apr

Sign-in flow verified end-to-end

Ricky tested at localhost:3000 β€” Google SSO β†’ /projects β†’ create project β†’ sign-out all working.

M1M1.129 Apr

File ingestion plumbing

Storage bucket + drag-drop UI + Inngest file/uploaded event firing end-to-end.

setupInngest29 Apr

Inngest account + keys

M1M1.028 Apr

Auth + project shell

Google SSO restricted to @growthpartners.co.nz, gp_users mirror, /projects list / new / detail.

setupOAuth28 Apr

Google OAuth client + Supabase Auth

setupLLM28 Apr

Anthropic + OpenAI keys

M0M028 Apr

Foundation scaffold

pnpm + Turborepo monorepo, Next.js 15, shadcn/ui, CI/CD green.

M0M028 Apr

Supabase schema applied

18 tables across tenants / projects / DA / dashboard. pgvector enabled. RLS deny-all default.

M0M028 Apr

GitHub setup

Branch protection on main, environments, CI green.

M0M028 Apr

Legacy v1 archived

Preserved on legacy/v1-railway-monorepo branch.

docsπŸ“š28 Apr

Methodology IP captured

Pitch deck + Mermed + Janssen sources structured under docs/methodology/.

docsπŸ“š29 Apr

Foundation plan + brief + decisions log

M0 Foundation β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ done M1 Phase 1 β€” Project Loading β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ done (M1.5 deferred) M2 Phase 2 β€” DA engine β”œβ”€ M2.1 DataForSEO β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ done β”œβ”€ M2.2 AI-search engine β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ done (in-house) β”œβ”€ M2.3 Pipeline backbone β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ done β”œβ”€ M2.4 6 synthesis sectionsβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ done (now 8 + perf snapshot) β”œβ”€ M2.5a Review screen v1 β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ done β”œβ”€ M2.5b Review screen v2 β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ backlog β”œβ”€ M2.6 PDF deliverable β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ done β”œβ”€ M2.7 Postmark email β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ done (no-op without token) β”œβ”€ M2.8 Tier 1 enrichment β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ done (per-cat vis Β· seasonality Β· PAA Β· 14-dim Β· PSI Β· audit) └─ M2.9 Tier 2 GSC + GA4 β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ done (service account) M3 Phase 3 β€” Client dashboard β”œβ”€ M3.0 Work-items lifecycleβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ done (keystone) β”œβ”€ M3.1 Subdomain routing β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ done (with M3.2) β”œβ”€ M3.2 Client-CEO auth β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ done (with M3.1) β”œβ”€ M3.5 Measurement worker β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ done (auto-probe + window measurements) β”œβ”€ M3.3 Dashboard layout β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ done (Work / Results / AΒ·CΒ·T) β”œβ”€ M3.7 Self-serve OAuth β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ done (GA4 grant + GSC verify) β”œβ”€ M3.4 GP work-mgmt UI β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ upcoming (replaces Asana sync) β”œβ”€ M3.5b Competitor delta β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ upcoming (Cloudflare Worker) └─ M3.6 Auto-ticker β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ upcoming M4 Polish + GrowthCast β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ backlog