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