# New Client Template

Standard onboarding template for new client projects.

# Overview

# New Client Template Overview

Standard onboarding template for new client projects.

**Owner:** John
**Last Verified:** 2026-02-17

## Contents
Template structure for client onboarding workflows

# Active Tasks Template

> Last Verified: [DATE] | Owner: [OWNER]

# [CLIENT_NAME] — Active Tasks

## Open Tasks
- **#[ID]** — [Task description]

## Recently Completed
- **[Task name]** — [Completion summary, date]

## Blocked Tasks
- **#[ID]** — [Task description]
  - **Reason:** [Why blocked]

# Key Decisions Template

> Last Verified: [DATE] | Owner: [OWNER]

# [CLIENT_NAME] — Key Decisions

## Strategic Decisions

### [Decision Title] ([DATE])
**Decision:** [What was decided]
**Rationale:** [Why this decision was made]
**Implementation:** [How it will be implemented]

# Project Overview Template

> Last Verified: [DATE] | Owner: [OWNER]

# [CLIENT_NAME] — Project Overview

## What is [CLIENT_NAME]?
[Brief description of what the client does, their industry, their core business]

## Current Status
- **Phase:** [SDLC phase: Planning / Analysis / Design / Implementation / Testing / Deployment / Maintenance]
- **Budget:** [Amount and currency]
- **Timeline:** [Duration or deadline]
- **Market:** [Target market / geography]

## Key Documents
- **Architecture:** [Path to architecture doc]
- **Business Case:** [Path to business case]

# Specifications Index Template

> Last Verified: [DATE] | Owner: [OWNER]

# [CLIENT_NAME] — Specifications Index

## Core System Specs
- **[spec-name].md** — [Brief description]

## Architecture Documents
- **[doc-name].md** — [Description]

# SEO Automation v1 — Intake, Audit, Reporting Workflow

# SEO Automation v1 — Intake, Audit, Reporting Workflow

**MC:** #101968  
**Status:** v1 review-ready  
**Owner:** John / ALAI  
**Scope:** reusable templates and credential-free public-page audit workflow for Asmir/partner SEO clients and ALAI-owned internal domains.

## Operating rule

SEO/audit/optimization work must be **template-first**:

1. Reuse the same intake, audit, report, and backlog templates.
2. Improve the reusable templates/tooling whenever a one-off need appears.
3. Do not claim ranking, traffic, or conversion impact without Search Console, Analytics, or equivalent verified evidence.
4. Do not fabricate legal/Privacy/Terms content as part of SEO work.

## Package location

Local package:

```text
/Users/makinja/business/ALAI-Holding-AS/sales/seo-automation/
```

Key files:

- `README.md`
- `client-intake-email-bs.md`
- `seo-intake-form.md`
- `seo-audit-checklist.md`
- `seo-client-report-template.md`
- `seo-internal-backlog-template.md`
- `seo-automation-roadmap.md`
- `run-basic-seo-audit.py`

## Audit runner

Credential-free runner:

```text
run-basic-seo-audit.py
```

Checks include:

- HTTP status
- title
- meta description
- H1/H2
- canonical
- robots meta
- `robots.txt`
- `sitemap.xml`
- images and decorative-alt handling
- links and forms

Validation performed:

```text
python3 -m py_compile run-basic-seo-audit.py
```

## Internal run evidence

Run workspace:

```text
/Users/makinja/business/ALAI-Holding-AS/sales/seo-automation/runs/20260524-internal-domains/
```

Important evidence files:

- `00-expanded-batch-report-v2.md`
- `local-validation-v2.md`
- `pr-ready-clean-worktrees.md`
- `published-draft-prs.md`
- `code-review-report.md`
- `pr-comments-evidence.md`
- `final-pr-summary.md`

## PRs opened from the workflow

Product/site PRs:

- LumisCare canonical: https://github.com/johnatbasicas/vivacare/pull/3
- Drop SEO cleanup: https://github.com/johnatbasicas/drop/pull/57
- BasicFakta robots/sitemap: https://github.com/johnatbasicas/basicfakta/pull/1

Template package PR:

- SEO Automation v1 templates: https://github.com/johnatbasicas/alai-holding/pull/2

PR validation comments:

- LumisCare: https://github.com/johnatbasicas/vivacare/pull/3#issuecomment-4530170979
- Drop: https://github.com/johnatbasicas/drop/pull/57#issuecomment-4530171024
- BasicFakta: https://github.com/johnatbasicas/basicfakta/pull/1#issuecomment-4530171064
- SEO template: https://github.com/johnatbasicas/alai-holding/pull/2#issuecomment-4530171110

## Current status

- PRs are opened and marked ready for review.
- Drop workflow concluded success, but PR merge state still reported `UNSTABLE`; required checks must be reviewed before merge.
- No PR was merged by automation.
- No deploy was performed by automation.
- Live post-deploy verification is still required after any approved merge/deploy.

# SEO Readiness Portal MVP — Status and Evidence

# SEO Readiness Portal MVP — status and evidence

## Scope

SEO Readiness Portal is the productized continuation of SEO Automation v1. The MVP converts intake, readiness audit structure, backlog, and reporting workflow into a local-first SaaS scaffold.

## Current implementation status

- Phase 1: scaffold, app shell, RBAC types, audit domain types, initial schema, spec validation.
- Phase 2: partner/client workspace, client detail page, structured intake UI, sample demo data, intake field definitions and guardrails.
- Phase 3: local/dev persistence for intake save/submit flow.
- Phase 4: local readiness audit runner with persisted audit results and audit detail page.
- Phase 5: local draft report and backlog generator from persisted audit findings.
- Phase 6: local Markdown report review/export workflow with persisted export metadata and checksum.
- Phase 7: local export review notes and internal approval status.
- Phase 8: local client handoff checklist for internally approved exports.
- Phase 9: local client handoff summary draft for approved checklist exports.
- Phase 10: local partner follow-up package draft from approved handoff summary drafts.
- Phase 11: internal SnowIT deploy readiness with Docker packaging, persistent volume, health endpoint, and Cloudflare Access trusted-header gate.
- Phase 12: live protected Cloudflare Access route completed and strictly authenticated on `seo-tools.alai.no`; exact `seo-tools.snowit.ba` route remains blocked pending `snowit.ba` nameserver/Cloudflare zone activation.
- Post-UAT UX fix: explicit **Add new client** flow added from `/partners` to `/clients/new`, creating a lead client, active site, and draft intake before redirecting into intake.

## Add client UX delivered behavior

- `/partners` now has a primary **Add new client** call-to-action and a simple 3-step workflow explanation.
- `/clients/new` provides a dedicated form for partner/owner, company, contact, website/domain, target markets, languages, priority services, competitors, CMS/hosting notes, access-status fields, and internal notes.
- Creating a client persists a `lead` client, active site, and draft intake, then redirects to the intake page with a success notice.
- Validation rejects missing required fields, invalid email, duplicate active domains, secret-like wording, and ranking/traffic/guarantee claims.
- Evidence: `UX-ADD-CLIENT-EVIDENCE.md`, `/tmp/evidence-102370/add-client-ux-validation.log`, `/tmp/evidence-102370/browser-add-client-flow.json`, `/tmp/evidence-102370/add-client-live-deploy-and-smoke.log`, `/tmp/evidence-102370-live-auth/verification.json`, and screenshots under `/tmp/evidence-102370/screenshots/` plus `/tmp/evidence-102370-live-auth/screenshots/`.

## Phase 3 delivered behavior

- File-backed local/dev workspace repository.
- Default app data path: `.data/workspace.json` and gitignored.
- Partner/client pages read through the persistence repository.
- Intake page server actions: 
    - save draft
    - submit intake
- Repository updates client, site, and intake records.
- Submitted intake rejects missing required fields.
- Submitted intake rejects secret-like content such as password, API key, token, secret, or bearer wording.

## Phase 4 delivered behavior

- Basic local readiness audit runner, not a live crawler.
- Client workspace action to run a local audit.
- Persisted audit records in local/dev file-backed workspace data.
- Audit detail page with readiness score, findings, and guardrails.
- Guardrail checks prevent ranking/traffic/guarantee wording in generated findings.

## Phase 5 delivered behavior

- Generates a local draft readiness report from a persisted local audit.
- Generates client-safe backlog items from audit findings.
- Persists `reports` and `backlogItems` in local/dev workspace data.
- Adds audit detail action to generate a draft report.
- Adds report detail page with executive summary, guardrails, and backlog draft.
- Guardrail checks prevent ranking/traffic/guarantee wording in generated report/backlog text.

## Phase 6 delivered behavior

- Generates a local Markdown export from a persisted draft readiness report.
- Persists `reportExports` metadata in local/dev workspace data, including filename, byte length, and SHA-256 checksum.
- Stores the Markdown artifact under local `.data/exports/` storage.
- Adds report action to generate the Markdown export.
- Adds export detail route `/clients/[clientId]/reports/[reportId]/exports/[exportId]` with metadata, guardrails, and Markdown preview.
- Guardrail checks prevent positive ranking/traffic/guarantee wording in generated export text.

## Phase 7 delivered behavior

- Persists `exportReviewNotes` in local/dev workspace data.
- Adds export detail action to save a local internal review note.
- Adds export detail UI for current local review status, reviewer, note, and review history count.
- Supports internal statuses: draft review, changes requested, approved internal.
- Guardrail checks reject secret-like wording and positive ranking/traffic/guarantee wording in review notes.
- Approval status is internal/local only and does not imply public deploy or client delivery.

## Phase 8 delivered behavior

- Persists `handoffChecklists` in local/dev workspace data.
- Adds export detail action to generate a local handoff checklist.
- Requires an `approved_internal` local export review note before checklist generation.
- Adds export detail UI for latest local handoff checklist, linked review note, and pending checklist items.
- Checklist references local evidence such as export checksum and approved review note id.
- Guardrail checks reject secret-like wording and positive ranking/traffic/guarantee wording in checklist content.
- Checklist is internal/local only and does not imply deploy, publication, or client delivery.

## Phase 9 delivered behavior

- Persists `handoffSummaries` in local/dev workspace data.
- Adds export detail action to generate a local handoff summary draft.
- Requires a local handoff checklist linked to an `approved_internal` local export review note before summary generation.
- Adds export detail UI for latest local summary draft, recommended next steps, evidence references, and limitations.
- Summary references local evidence such as export filename/checksum, approved review note id, and checklist id.
- Guardrail checks reject secret-like wording and positive ranking/traffic/guarantee wording in summary content.
- Summary is local draft copy only and does not imply email sending, deploy, publication, or client delivery.

## Phase 10 delivered behavior

- Persists `partnerFollowupPackages` in local/dev workspace data.
- Adds export detail action to generate a local partner follow-up package draft.
- Requires a local handoff summary draft linked to a checklist and `approved_internal` local review note before package generation.
- Adds export detail UI for latest local partner follow-up package draft, suggested message draft, client questions, internal preparation steps, evidence references, limitations, and guardrails.
- Package references local evidence such as export filename/checksum, approved review note id, checklist id, and handoff summary id.
- Guardrail checks reject secret-like wording and positive ranking/traffic/guarantee wording in package content.
- Package is local draft copy only and does not imply automated email sending, public deploy, publication, or client delivery.

## Phase 11 delivered behavior

- Adds standalone Next.js production packaging for container runtime.
- Adds Dockerfile and internal Docker Compose profile.
- Binds the app origin to `127.0.0.1:3100` only in the internal compose profile.
- Uses file-backed Docker volume persistence at `/data/workspace.json` for internal v1.
- Adds `SEO_PORTAL_ACCESS_MODE=cf-access` mode.
- Adds app-level trusted-header access gate using `CF-Access-Authenticated-User-Email` after Cloudflare Access authenticates upstream.
- Adds `/api/health` endpoint for container/reverse-proxy checks.
- Adds internal runbook for proposed `seo-tools.snowit.ba` deployment.
- Local container verification confirms unauthenticated page access returns 401 and authenticated-header page access returns 200.
- Live DNS/Cloudflare Access for `seo-tools.snowit.ba` is not yet configured in this session.

## Phase 12 live-route status

Completed live protected route:

- `https://seo-tools.alai.no`

Observed behavior:

- unauthenticated `/partners` request returns HTTP `302` to Cloudflare Access login;
- spoofed `CF-Access-Authenticated-User-Email` request without Access session still returns HTTP `302` to Cloudflare Access login;
- local origin remains bound to `127.0.0.1:3100` and blocks unauthenticated page access with HTTP `401`;
- browser verification confirms the live unauthenticated page lands on `Sign in ・ Cloudflare Access`, not the origin app;
- strict authenticated UAT for `info@snowit.ba` passed: OTP was received/submitted and the browser loaded `https://seo-tools.alai.no` with title `SEO Readiness Portal`.

Exact SnowIT URL status:

- `https://seo-tools.snowit.ba` is confirmed as the desired SnowIT-branded URL, but is not live yet.
- `snowit.ba` is currently hosted in AWS Route53 and is not an active Cloudflare zone in the available Cloudflare account.
- Cloudflare Access rejected `seo-tools.snowit.ba` with `domain does not belong to zone`.
- A pending Cloudflare zone for `snowit.ba` exists with nameservers `aspen.ns.cloudflare.com` and `wells.ns.cloudflare.com`.
- Existing Route53 records have been mirrored into the pending Cloudflare zone as DNS-only records, and `seo-tools.snowit.ba` has been prepared there as a proxied CNAME to the Cloudflare Tunnel target.
- Nameserver migration/delegation was approved as the correct direction, but is blocked by missing registrar/NIC access path. AWS Route53Domains reports `.ba` as unsupported, and no Bitwarden item for NIC.ba/UTIC/SnowIT domain registrar credentials was found.

## Evidence

Product worktree:

`/Users/makinja/business/ALAI-Holding-AS/products/SEO-Readiness-Portal`

Evidence files:

- `PHASE-1-EVIDENCE.md`
- `PHASE-2-EVIDENCE.md`
- `PHASE-3-EVIDENCE.md`
- `PHASE-4-EVIDENCE.md`
- `PHASE-5-EVIDENCE.md`
- `PHASE-6-EVIDENCE.md`
- `PHASE-7-EVIDENCE.md`
- `PHASE-8-EVIDENCE.md`
- `PHASE-9-EVIDENCE.md`
- `PHASE-10-EVIDENCE.md`
- `PHASE-11-EVIDENCE.md`
- `PHASE-12-EVIDENCE.md`
- `UX-ADD-CLIENT-EVIDENCE.md`
- `/tmp/evidence-102370/add-client-ux-validation.log`
- `/tmp/evidence-102370/browser-add-client-flow.json`
- `/tmp/evidence-102370/browser-add-client-flow-summary.txt`
- `/tmp/evidence-102370/add-client-live-deploy-and-smoke.log`
- `/tmp/evidence-102370-live-auth/verification.json`
- `/tmp/evidence-102370-live-auth/SUMMARY.md`
- `/tmp/evidence-102370/screenshots/01-partners-add-client-cta.png`
- `/tmp/evidence-102370/screenshots/02-new-client-form.png`
- `/tmp/evidence-102370/screenshots/03-created-client-intake.png`
- `/tmp/alai/seo-readiness-phase3-http-smoke.txt`
- `/tmp/alai/redzo-102064-review.md`
- `/tmp/alai/company-mesh-responder/2026-05-26T11-20-45-604Z-mesh-msg-1c229051-b7e6-4bdc-af43-c442d3dd8fe2.json`
- `/tmp/alai/seo-readiness-phase4-http-smoke.txt`
- `/tmp/alai/redzo-102070-review.md`
- `/tmp/alai/seo-readiness-phase5-http-smoke.txt`
- `/tmp/alai/seo-readiness-phase6-hard-evidence-102091/command-log.txt`
- `/tmp/alai/seo-readiness-phase6-http-smoke.txt`
- `/tmp/alai/seo-readiness-phase7-hard-evidence-102112/command-log.txt`
- `/tmp/alai/seo-readiness-phase7-http-smoke.txt`
- `/tmp/alai/seo-readiness-phase8-hard-evidence-102220/command-log.txt`
- `/tmp/alai/seo-readiness-phase8-http-smoke.txt`
- `/tmp/alai/seo-readiness-phase9-hard-evidence-102231/command-log.txt`
- `/tmp/alai/seo-readiness-phase9-http-smoke.txt`
- `/tmp/alai/seo-readiness-phase10-hard-evidence-102323/command-log.txt`
- `/tmp/alai/seo-readiness-phase10-http-smoke.txt`
- `/tmp/alai/seo-readiness-phase11-evidence-102338/command-log.txt`
- `/tmp/alai/seo-readiness-phase11-evidence-102338/docker-build.txt`
- `/tmp/alai/seo-readiness-phase11-evidence-102338/docker-compose-ps.txt`
- `/tmp/alai/seo-readiness-phase11-evidence-102338/container-health.json`
- `/tmp/alai/seo-readiness-phase11-evidence-102338/container-unauth-partners.txt`
- `/tmp/alai/seo-readiness-phase11-evidence-102338/container-auth-partners.txt`
- `/tmp/alai/seo-readiness-phase11-evidence-102338/browser/phase11-browser-verify.json`
- `/tmp/alai/gemini-102338-phase11-review-v2.md`
- `/tmp/alai/seo-readiness-phase12-evidence-102350/seo-tools-alai-unauth-curl.txt`
- `/tmp/alai/seo-readiness-phase12-evidence-102350/seo-tools-alai-spoof-and-local.txt`
- `/tmp/alai/seo-readiness-phase12-evidence-102350/browser/live-unauth-browser-verify.json`
- `/tmp/alai/seo-readiness-phase12-evidence-102350/browser/seo-tools-alai-access-redirect.png`
- `/tmp/evidence-102350/cloudflare-tunnel-remote-config-add-seo-tools.txt`
- `/tmp/evidence-102350/cloudflare-tunnel-remote-config-seo-tools-127001.txt`
- `/tmp/evidence-102350-info-snowit-strict/verification.json`
- `/tmp/evidence-102350-info-snowit-strict/SUMMARY.md`
- `/tmp/evidence-102350-info-snowit-strict/screenshots/S04-after-login.png`
- `/tmp/evidence-102350/live-edge-and-origin-check-after-auth-uat.txt`
- `/tmp/alai/seo-readiness-phase12-evidence-102350/cloudflare-access-seo-tools-alai.txt`
- `/tmp/alai/seo-readiness-phase12-evidence-102350/cloudflare-access-seo-tools-snowit.txt`
- `/tmp/alai/seo-readiness-phase12-evidence-102350/route53-snowit-ba-records-summary.txt`
- `/tmp/alai/seo-readiness-phase12-evidence-102350/cloudflare-snowit-pending-zone-record-sync.txt`
- `/tmp/alai/seo-readiness-phase12-evidence-102350/cloudflare-access-seo-tools-snowit-retry-after-zone-sync.txt`
- `/tmp/alai/seo-readiness-phase12-evidence-102350/snowit-ba-nameserver-change-request-bs.md`
- `/tmp/alai/seo-readiness-phase12-evidence-102350/aws-registrar-snowit.txt`
- Company Mesh/P2P PASS: `mesh-thr-ede2b968-6abc-4354-a24b-be13f4f0262d` / `mesh-msg-1827f723-6ae5-4f22-b868-afd2da5d1dc2`
- `/tmp/alai/redzo-102231-review-v2-with-evidence.md`
- Company Mesh/P2P PASS: `mesh-thr-83994892-4a0c-4dc7-96c3-c553511f9a8f` / `mesh-msg-32536a8d-8826-4a74-a665-a52c07805f33`

Validation commands observed through Phase 11:

```bash
npm run type-check
npm run validate:spec
npm run validate:phase2
npm run validate:phase3
npm run validate:phase4
npm run validate:phase5
npm run validate:phase6
npm run validate:phase7
npm run validate:phase8
npm run validate:phase9
npm run validate:phase10
npm run validate:phase11
npm run build
npm audit --audit-level=high
python3 /tmp/alai/seo-readiness-phase3-smoke.py
python3 /tmp/alai/seo-readiness-phase4-smoke.py
python3 /tmp/alai/seo-readiness-phase5-smoke.py
python3 /tmp/alai/seo-readiness-phase6-smoke.py
python3 /tmp/alai/seo-readiness-phase7-smoke.py
python3 /tmp/alai/seo-readiness-phase8-smoke.py
# Phase 9 smoke was curl-based; see /tmp/alai/seo-readiness-phase9-http-smoke.txt

```

Observed results:

- type-check: PASS
- validate:spec: PASS
- validate:phase2: PASS
- validate:phase3: PASS
- validate:phase4: PASS
- validate:phase5: PASS
- validate:phase6: PASS
- validate:phase7: PASS
- validate:phase8: PASS
- validate:phase9: PASS
- build: PASS
- npm audit high threshold: PASS; moderate postcss/next advisory remains and was not force-fixed
- local HTTP smoke: PASS for `/`, `/partners`, `/clients/client-demo-nordic-clinic`, `/clients/client-demo-nordic-clinic/intake`
- Phase 4 local HTTP smoke: PASS for audit detail route `/clients/client-demo-nordic-clinic/audits/<auditId>`
- Phase 5 local HTTP smoke: PASS for report detail route `/clients/client-demo-nordic-clinic/reports/<reportId>`
- Phase 6 local HTTP smoke: PASS for export detail route `/clients/client-demo-nordic-clinic/reports/<reportId>/exports/<exportId>`
- Phase 7 local HTTP smoke: PASS for export review status markers on `/clients/client-demo-nordic-clinic/reports/<reportId>/exports/<exportId>`
- Phase 8 local HTTP smoke: PASS for local handoff checklist markers on `/clients/client-demo-nordic-clinic/reports/<reportId>/exports/<exportId>`
- Phase 9 local HTTP smoke: PASS for local handoff summary draft markers on `/clients/client-demo-nordic-clinic/reports/<reportId>/exports/<exportId>`
- Phase 9 independent review: Redzo APPROVE and Company Mesh/P2P PASS
- Phase 10 local HTTP smoke: PASS for local partner follow-up package markers on `/clients/client-demo-nordic-clinic/reports/<reportId>/exports/<exportId>`
- Phase 11 packaging validation: PASS for standalone config, Docker packaging, localhost-only origin, `/data` volume, access mode, allowlist configuration, and runbook checks
- Phase 11 Docker build: PASS for `alai/seo-readiness-portal:internal`
- Phase 11 container health: PASS for `http://127.0.0.1:3100/api/health`
- Phase 11 unauthenticated page access: PASS/blocked with HTTP 401 and `x-seo-portal-access: blocked`
- Phase 11 authenticated-header page access: PASS/allowed with HTTP 200 and `x-seo-portal-access: cf-access`
- Phase 11 browser verification: PASS for blocked unauthenticated `/partners` and allowed authenticated-header `/partners`
- Phase 11 independent read-only Gemini CLI review: PASS for deploy-readiness packaging and access-gate behavior, with live DNS/Cloudflare hookup explicitly not claimed
- Phase 11 Company Mesh/P2P pre-verifier: PASS for ready\_for\_review scope on deploy-readiness packaging and access-gate behavior
- Phase 12 `seo-tools.alai.no` live protected route: PASS for unauthenticated Cloudflare Access redirect and spoofed trusted-header redirect
- Phase 12 `seo-tools.snowit.ba`: BLOCKED by Cloudflare Access zone ownership requirement until `snowit.ba` is active/delegated to Cloudflare or the internal URL decision changes

## Non-goals preserved

- No public unauthenticated deploy.
- No live DNS/Cloudflare Access configuration completed for `seo-tools.snowit.ba` in this session.
- No nameserver migration for `snowit.ba` was executed automatically.
- No production database connection or secrets.
- No Google Search Console or Analytics integration.
- No paid keyword/SERP API integration.
- No ranking or traffic claims.

## Suggested next phase

Phase 13 candidate: obtain registrar/NIC access path for `snowit.ba`, execute the approved nameserver migration to Cloudflare, then create the exact `seo-tools.snowit.ba` Access application and run authenticated browser verification through the SnowIT URL. Pending Cloudflare zone is already pre-populated.

## 2026-06-22 — LIVE self-serve autonomous loop verified (MC #102915)

- **Status:** LIVE in production, autonomous self-serve loop confirmed end-to-end WITHOUT human involvement.
- **Azure:** app `seo-readiness-alai` (`rg-seo-readiness-prod`), state Running, image `20260618-ga4-selfheal` (real audit engine).
- **Public self-serve path:** `https://seo-tools.snowit.ba/intake/<token>` (CF Access bypass for `/intake/*`; `/partners` stays gated).
- **Independent Proveo live E2E (real browser/Playwright) — PASS:**
    - Magic-link intake form renders (HTTP 200, screenshot).
    - AI chatbot live (`/api/intake-chat`) returns real Bosnian response — tier: Groq.
    - Form submit → IntakeSubmission v1 persisted (screenshot).
    - Auto-audit pipeline fired post-submit: `live_crawl` OK + `report_draft` OK, audit (3 findings), report with 9-step Bosnian action plan — all in ~10s.
- **Evidence:** `/tmp/alai/seo-portal-e2e-104192/` (screenshots step3/step5, step4-uat-chat.json, workspace-live-after-submit.json); til-done receipt `/tmp/til-done/102915-20260622T133626Z.json`; verdict `/tmp/evidence-102915/verdict.json`.
- **Open follow-ups (non-blocking):**
    1. Production chatbot single-tier Groq only — no Anthropic fallback key on live app → 503 risk if Groq down.
    2. Minor: `pipeline_record`/`notify` stages not persisted to workspace snapshot (`intake-pipeline.ts` ~L208).
    3. Google GSC/GA restricted-scope still pending Google OAuth verification (#103428) — affects live ranking/traffic data only, NOT the crawl-based audit which works now.

**Conclusion:** ALAI/SnowIT has a working autonomous SEO tool that serves clients (audit + Bosnian action plan) without Asmir or CEO at every step except the designed partner-mediated magic-link entry.

---

## See Also — Agent Runbook

The canonical end-to-end workflow runbook (intake to John deep-report, anti-pitfalls, trigger checklist) lives here:

- [SEO Pipeline — Portal Intake to John Deep-Report (agent runbook)](https://docs.alai.no/books/seo-readiness-portal/page/seo-pipeline-portal-intake-john-deep-report-agent-runbook)

# SEO Readiness Portal — Self-Serve Intake (Architecture + Operator Runbook)

# SEO Readiness Portal — Self-Serve Intake (Architecture + Operator Runbook)

**Shipped:** 2026-06-04  
**Image:** `alairegistry.azurecr.io/seo-readiness-portal:20260604-selfserve-intake`  
**Validation:** MC #102923 Proveo PASS — `/tmp/alai/seo-wsv-evidence/VALIDATION-REPORT.md`  
**Deploy Evidence:** `/tmp/alai/seo-deploy-102929/FLOWFORGE-DEPLOY-REPORT.md`

---

## Overview

The self-serve intake feature enables **Asmir (SnowIT sales) to send zero-effort referrals** to potential SEO clients. Asmir creates a client record in the SEO Portal /partners workspace, the system automatically sends a branded magic-link email, and the client fills out a web-based intake form that triggers an automated SEO audit pipeline — all without Asmir lifting a finger after the initial client creation.

### Asmir Zero-Effort Workflow

1. **Create client:** Asmir visits `https://seo-tools.snowit.ba/partners/clients/new`, enters client name + email.
2. **System auto-sends:** Intake magic-link email sent immediately (SnowIT-branded, subject "SEO Readiness Assessment for {clientName}").
3. **Client completes intake:** Client clicks link → fills web form (or chats with AI assistant) → submits.
4. **Auto-pipeline runs:** Live SEO audit + findings + draft report generated automatically.
5. **Asmir reviews:** Asmir sees client status change from "link\_sent" → "intake\_submitted" → "report\_ready" in `/partners/referrals` dashboard.

**Total Asmir effort:** Type name + email. Done.

---

## Feature Scope (WS-A/B/C)

### WS-A: Magic-Link Token + Email Delivery

**Deliverable:** HMAC email-bound re-usable intake token + public `/intake/[token]` route + SnowIT-branded link email on client create.

- **Token model:** `src/lib/auth/intake-token.ts`
    - HMAC-SHA256 signed with `INTAKE_TOKEN_SECRET` (64-char hex, Azure env var).
    - Payload: `{clientId, email, expiresAt}`. Default expiry: 30 days.
    - Token hash stored on `Client.intakeTokenHash`; plaintext NEVER persisted.
    - Re-usable: Same token works for multiple submissions (versioned append model).
- **Public route:** `src/app/intake/[token]/page.tsx` (Next.js App Router) 
    - Token verification: `verifyIntakeToken(token)` → clientId + email.
    - Invalid/expired/forged token → 200 error page ("Link not valid / expired"), no data leak.
    - Valid token → loads single-client intake form pre-filled with client name/email.
    - Cross-client isolation: Token A cannot access Client B's data (clientId derived from token, NEVER from URL/body).
- **Email trigger:** `src/app/clients/new/actions.ts` `createClient()`
    - Calls `~/system/tools/mail-native.js` with SnowIT-branded template.
    - Subject: "SEO Readiness Assessment for {clientName}".
    - Body: SnowIT logo + personalized intro + magic link to `https://seo-tools.snowit.ba/intake/{token}`.
    - **KNOWN GAP:** Email currently sent via `--account alai` sender (snowit.ba mailbox not yet provisioned). Body is SnowIT-branded; sender is alai.no. Follow-on MC to provision Asmir@snowit.ba sender mailbox.
- **Persistence:** `src/lib/workspace/persistence.ts`
    - Versioned `IntakeSubmission[]` appended to `Client.intakeSubmissions` array.
    - Each submission timestamped; re-submit creates new version, does NOT overwrite.

**Evidence:** `/tmp/alai/seo-ws-a-evidence/`  
**Validation:** Playwright 10/10 PASS, `validate-magic-link.ts` 33/33 PASS.

---

### WS-B: SnowIT White-Label AI Chatbot

**Deliverable:** SnowIT-branded AI assistant on intake page (Ollama-first tier-router, rate-limited, secret-guarded, forbidden-claim-guarded).

- **UI Widget:** `src/components/chatbot/IntakeChatWrapper.tsx`
    - Floating Action Button (FAB) in bottom-right corner of intake page.
    - Slide-out chat panel: "SEO Assistant" header + "Powered by Snowit" footer.
    - Collects same fields as intake form (`IntakeFormFields`).
    - Two-way sync: chat updates form, form updates chat context.
- **API Route:** `src/app/api/intake-chat/route.ts` (POST) 
    - **Token-gated:** Requires `token` in body; calls `verifyIntakeToken()`.
    - Invalid/expired token → 401 `IntakeChatErrorResponse {code: 'token_invalid'}`.
    - ClientId derived from token, NEVER from body (single-client scope enforced).
    - **Rate-limited:** 10 messages / 60 seconds per token (in-process sliding window). 11th message → 429 `{code: 'rate_limited'}`.
    - **LLM tier-router:** `src/lib/chat/tier-router.ts` (Ollama FORGE → Groq → Anthropic Haiku waterfall).
    - **Secret-scan guard:** User input + assistant output scanned for `/\b(password|api[_ -]?key|secret|token|bearer)\b/i`. Match → pre-canned refusal, no credential in response.
    - **Forbidden-claim guard:** User input + assistant output scanned for `/\b(rank\s*#?1|first page|traffic lift|guaranteed results)\b/i`. Match → pre-canned refusal.
    - **Field extraction:** `src/lib/chat/field-extractor.ts` parses conversation → `Partial<IntakeFormFields>` returned as `fieldUpdates` (form ↔ chat sync).
- **System prompt:** `src/lib/chat/system-prompt.ts`
    - Hard rules: 
        - Ask ONE question at a time.
        - NEVER ask for passwords, API keys, tokens, or credentials.
        - NEVER promise rankings, traffic, or guaranteed results.
        - Friendly, adaptive to client's language.
    - Role: SEO intake assistant for SnowIT SEO readiness service.
- **Cost discipline:**
    - **Ollama FORGE (10.0.0.2:11434):** PRIMARY path, zero cost, ~50 tok/s (qwen2.5:7b-instruct-q8\_0).
    - **Groq:** Fallback #1, ~$0.10/1M input tokens.
    - **Anthropic Haiku:** Fallback #2, ~$0.80/1M input tokens.
    - Cloud only on local outage: `tier-router.ts` health-checks `/api/tags` before calling Ollama (avoid cloud cost on idle).
- **DEFERRED:** WS-B4 (pre-seed chat from live crawl) NOT implemented. Chat starts fresh without crawl data. Follow-on MC can add: "if website provided, call `runLiveCrawlAudit()` and seed assistant context with detected title/description/services."

**Evidence:** `/tmp/alai/seo-ws-b-agentforge-evidence/implementation-summary.md`  
**Validation:** `validate-intake-chat.ts` 5/5 PASS, Ollama live turn JSON verified.

---

### WS-C: Auto-Pipeline on Intake Submit

**Deliverable:** Intake submit triggers audit → findings → draft report (GBP fallback for no-website clients).

- **Pipeline trigger:** `src/lib/pipeline/run-intake-pipeline.ts`
    - Called on intake form submit (`src/app/intake/[token]/actions.ts`).
    - Runs: `runLiveCrawlAudit()` → `generateFindings()` → `generateReport()`.
    - **GBP/no-website mode:** If `intake.hasWebsite === false` OR `canonicalUrl` empty → audit runs `local_readiness_gbp` stage (12 GBP-specific checks: claim/manager/NAP/category/hours/photos/reviews/posts), skips website-dependent checks, still drafts report (labeled as readiness assessment, not ranking prediction).
    - **Security:** `rawToken` NOT passed into pipeline (no token in audit/findings/report records).
- **Stage derivation:** `src/lib/workspace/referral-stages.ts`
    - Client record status derived from data state (NOT manual flag): 
        - `created` → client created, no token yet.
        - `link_sent` → `intakeTokenHash` present.
        - `link_opened` → future (requires link-click tracking, not implemented).
        - `intake_submitted` → `intakeSubmissions.length > 0`.
        - `audit_ready` → audit record exists.
        - `report_ready` → report draft exists.
    - Monotonic progression: stage never regresses.
- **Email notification:** `src/lib/email/notify.ts`
    - Sends "New intake submission for {clientName}" to Asmir (partner email).
    - **Security:** `secretGuard()` scans notification body for credentials/tokens before send. Token redacted as `[REDACTED]` if accidentally included.
    - `forbiddenClaimWords` enforced in audit runner + report generator + exporter (aligned with `src/lib/workspace/persistence.ts`).
- **Asmir dashboard:** `src/app/partners/referrals/page.tsx`
    - Shows all clients with derived stage (created → link\_sent → intake\_submitted → report\_ready).
    - Real-time status derived from data, no manual updates needed.
- **Re-submit behavior:** New intake submission appends new version, pipeline records both runs (versioned history, not overwrite).

**Evidence:** `/tmp/alai/seo-ws-c-evidence/validate-referral-pipeline.txt`  
**Validation:** `validate-referral-pipeline.ts` 32/32 PASS.

---

## Cloudflare Access Carve-Out (Defense-in-Depth)

**Problem:** SEO Portal is protected by Cloudflare Access (trusted-header SSO). Public intake links (`/intake/[token]`) must be accessible to unauthenticated clients, but the rest of the app (`/partners`, `/api/health`) must remain gated.

**Solution:** Created a SEPARATE Cloudflare Access application with a BYPASS policy for `/intake/*` paths ONLY. The edge lets `/intake/*` through, and the app's own token gate (`verifyIntakeToken()`) protects the intake pages (defense-in-depth: CF bypass + app token gate).

### CF Access App

- **App ID:** `06f98e51-1f07-4660-b93b-c426ccff21ce`
- **Name:** "SEO Portal - Public Intake Path"
- **Domains:**
    - `seo-tools.snowit.ba/intake`
    - `seo-tools.alai.no/intake`
    - `seo-tools.snowit.ba/api/intake-chat`
    - `seo-tools.alai.no/api/intake-chat`

### Bypass Policy

- **Policy ID:** `d9e51759-c151-4ee7-8094-e5191e5f9163`
- **Name:** "Public Intake Bypass"
- **Decision:** `bypass`
- **Include:** `{ "everyone": {} }`
- **Precedence:** 1

### Verification (Live Prod)

<table id="bkmrk-path-expected-result"><thead><tr><th>Path</th><th>Expected</th><th>Result</th></tr></thead><tbody><tr><td>`/intake/bogus-token`</td><td>200 "Link not valid" (app served, no CF redirect)</td><td>✅ PASS</td></tr><tr><td>`/api/intake-chat`</td><td>401 from app (origin reached, not CF 302)</td><td>✅ PASS</td></tr><tr><td>`/partners`</td><td>302 to CF Access login (still gated)</td><td>✅ PASS</td></tr><tr><td>`/api/health`</td><td>302 to CF Access login (not in bypass)</td><td>✅ PASS</td></tr></tbody></table>

**Key signal:** Header `x-seo-portal-access: blocked` proved request hit the app, confirming CF Access carve-out working.

**CRITICAL DEPLOY STEP:** This CF Access carve-out is MANDATORY for the self-serve intake feature. Without it, all `/intake/[token]` requests are redirected to CF Access login instead of reaching the app's token-gated intake page. The first cutover attempt FAILED because this step was missing. Deploy runbook now documents this as a hard gate.

**Evidence:** `/tmp/alai/seo-deploy-102929/FLOWFORGE-DEPLOY-REPORT.md` (Part 1: CF Access carve-out creation + edge verification).

---

## Azure App Service Deploy

### Deploy Target

- **Resource group:** `rg-seo-readiness-prod`
- **App Service:** `seo-readiness-alai`
- **Region:** Sweden Central
- **Registry:** `alairegistry.azurecr.io`
- **Image:** `alairegistry.azurecr.io/seo-readiness-portal:20260604-selfserve-intake`
- **Public URLs:**
    - `https://seo-tools.snowit.ba` (Cloudflare custom hostname)
    - `https://seo-tools.alai.no` (Cloudflare custom hostname)

### Required Environment Variables

- `INTAKE_TOKEN_SECRET`: 64-char hex (HMAC signing key for intake tokens). Generated on deploy, stored in Azure App Settings (NOT in image).
- `INTAKE_BASE_URL`: `https://seo-tools.snowit.ba` (used in magic-link email).

### Post-Deploy Verification

<table id="bkmrk-check-expected-resul"><thead><tr><th>Check</th><th>Expected</th><th>Result</th></tr></thead><tbody><tr><td>Image tag</td><td>`20260604-selfserve-intake`</td><td>✅ PASS</td></tr><tr><td>App state</td><td>`Running`</td><td>✅ PASS</td></tr><tr><td>`/intake/bogus` → 200</td><td>App served (no CF redirect)</td><td>✅ PASS</td></tr><tr><td>`/partners` → 302</td><td>CF Access login</td><td>✅ PASS</td></tr></tbody></table>

**Evidence:** `/tmp/alai/seo-deploy-102929/FLOWFORGE-DEPLOY-REPORT.md` (Part 2: deploy + ZAKON PI2 post-deploy verification).

---

## Known Gaps

1. **SnowIT sender mailbox not provisioned:** Magic-link emails currently sent via `--account alai` sender (alai.no). Body is SnowIT-branded; sender is alai.no. Follow-on MC to provision `Asmir@snowit.ba` sender mailbox.
2. **WS-B4 chat pre-seed from live crawl:** Deferred. Chat starts fresh without crawl data. Follow-on MC can add: "if website provided, call `runLiveCrawlAudit()` and seed assistant context with detected title/description/services."
3. **Rate-limiter resets on container restart:** Rate limiter state is in-process Map; resets on Azure container restart. Acceptable for MVP; Postgres migration follow-on.
4. **CF token rotation:** MC #102790 paused. The global CF API key was used for this deploy (scoped token was invalid). Rotation task should be resumed.

---

## Operator Runbook

### Create Client + Send Magic Link

1. Visit `https://seo-tools.snowit.ba/partners/clients/new` (Asmir's Cloudflare Access account).
2. Enter client name + email.
3. Submit → magic-link email sent automatically.
4. Client receives: "SEO Readiness Assessment for {clientName}" with link to `https://seo-tools.snowit.ba/intake/{token}`.

### Monitor Referral Status

1. Visit `https://seo-tools.snowit.ba/partners/referrals`.
2. Client status derived from data: 
    - `link_sent` → email sent, awaiting client action.
    - `intake_submitted` → client submitted form/chat.
    - `report_ready` → audit + report draft generated.

### Troubleshoot "Link not valid"

**Possible causes:**

1. **Forged token:** Token signature invalid (client manually edited URL). Expected behavior: 200 error page, no data leak.
2. **Expired token:** Token `expiresAt` past (default 30 days). Expected behavior: 200 "Link expired" error page.
3. **INTAKE\_TOKEN\_SECRET mismatch:** Azure env var changed after token was minted. Fix: Re-send magic link from `/partners/clients/{clientId}` (generates new token with current secret).

**Debug command:**

```
az webapp config appsettings list -g rg-seo-readiness-prod -n seo-readiness-alai --query "[?name=='INTAKE_TOKEN_SECRET'].value" -o tsv

```

If secret changed → re-send magic link from portal.

### Troubleshoot Rate Limit (429)

**Symptom:** Client sees "Too many requests" after 10 chat messages in 60 seconds.

**Expected behavior:** Rate limiter prevents abuse on public `/api/intake-chat` route.

**Workaround:** Wait 60 seconds for sliding window to reset.

**Long-term fix:** Postgres-backed rate limiter (survives container restarts). Follow-on MC.

### Re-Deploy Self-Serve Intake

**Pre-flight:**

```
cd /Users/makinja/business/ALAI-Holding-AS/products/SEO-Readiness-Portal
npm run type-check && npm run build && npm run validate:spec

```

**Build image:**

```
az acr build -r alairegistry -t seo-readiness-portal:YYYYMMDD-tag-name .

```

**Deploy:**

```
az webapp config container set \
  --resource-group rg-seo-readiness-prod \
  --name seo-readiness-alai \
  --container-image-name alairegistry.azurecr.io/seo-readiness-portal:YYYYMMDD-tag-name \
  --container-registry-url https://alairegistry.azurecr.io
az webapp restart --resource-group rg-seo-readiness-prod --name seo-readiness-alai

```

**Post-deploy verify (ZAKON PI2):**

```
curl -sI https://seo-tools.snowit.ba/intake/bogus | grep -E "HTTP|content-type"
# Expect: HTTP/2 200, content-type: text/html; charset=utf-8

curl -sI https://seo-tools.snowit.ba/partners | grep -E "HTTP|location"
# Expect: HTTP/2 302, location: https://<CF-Access-login-url>

```

**Rollback:**

```
az webapp config container set \
  --resource-group rg-seo-readiness-prod \
  --name seo-readiness-alai \
  --container-image-name alairegistry.azurecr.io/seo-readiness-portal:20260602-real-audit \
  --container-registry-url https://alairegistry.azurecr.io
az webapp restart --resource-group rg-seo-readiness-prod --name seo-readiness-alai

```

Previous known-good: `20260602-real-audit`. Do NOT open origin IP-lock. Do NOT touch any Bilko domain.

---

## Evidence Ledger

<table id="bkmrk-artifact-location-sh"><thead><tr><th>Artifact</th><th>Location</th><th>SHA-256</th></tr></thead><tbody><tr><td>Validation report (Proveo PASS)</td><td>`/tmp/alai/seo-wsv-evidence/VALIDATION-REPORT.md`</td><td>`72e79a31...`</td></tr><tr><td>Playwright results (10/10 PASS)</td><td>`/tmp/alai/seo-wsv-evidence/playwright-results.json`</td><td>`6d35586e...`</td></tr><tr><td>WS-A evidence</td><td>`/tmp/alai/seo-ws-a-evidence/`</td><td>—</td></tr><tr><td>WS-B evidence</td><td>`/tmp/alai/seo-ws-b-agentforge-evidence/`</td><td>—</td></tr><tr><td>WS-C evidence</td><td>`/tmp/alai/seo-ws-c-evidence/`</td><td>—</td></tr><tr><td>Deploy report (FlowForge)</td><td>`/tmp/alai/seo-deploy-102929/FLOWFORGE-DEPLOY-REPORT.md`</td><td>—</td></tr><tr><td>Live probes</td><td>`/tmp/alai/seo-wsv-evidence/live-probes.txt`</td><td>`4e03b886...`</td></tr><tr><td>Intake page HTML (SSR)</td><td>`/tmp/alai/seo-wsv-evidence/intake-page-content.html`</td><td>`033ad52f...`</td></tr><tr><td>Screenshots (AC1-AC10)</td><td>`/tmp/alai/seo-wsv-evidence/screenshots/`</td><td>—</td></tr></tbody></table>

---

**Status:** LIVE in production as of 2026-06-04.  
**Next:** Provision `Asmir@snowit.ba` sender mailbox (WS-A follow-on). Resume CF token rotation MC #102790.

# SEO Deep Report — AI-Agent Readiness modul (v1.1, MC #105976)

# AI-Agent Readiness modul — SEO Deep Report play v1.1

**MC:** #105976 · **Dodano:** 2026-07-19 · **Play:** `~/system/prompts/seo-deep-report-play.md` (sekcija 9) · **Izvor metodologije:** growth-memo.com "Where AI agents get stuck on your site" (2026-07)

## Šta je

Nova sekcija deep-reporta: mjeri može li AI agent (ChatGPT/Perplexity/Claude) pročitati i koristiti klijentski sajt. Ako ne može, agent citira treće strane umjesto klijenta (benchmark: bez objavljenih cijena 45% pokušaja ide na treće strane; uz access greške 77%). Diferencijator SnowIT ponude — niko lokalno to ne nudi.

## 5 provjera (sve curl/grep, bez novih alata)

| # | Provjera | Bodovi |
|---|---|---|
| 9a | robots.txt: pravi fajl + AI botovi (GPTBot/ClaudeBot/PerplexityBot/Google-Extended) dozvoljeni | 20 |
| 9b | UA block matrix: browser vs AI bot UA — svi 200 (hvata WAF/bot-fight blokade) | 20 |
| 9c | No-JS čitljivost: usluge/cijene/kontakt u sirovom HTML-u; stranica <1MB | 25 |
| 9d | JSON-LD: LocalBusiness/Organization/Service/Product+Offer (najjača poluga: benchmark 73→93) | 25 |
| 9e | Opacity: ponuda/cijene kao tekst na canonical URL-u (ne kalkulator/PDF-only) | 10 |

Score 0-100 ulazi u report kao 7. dimenzija; nalazi u P1-P3. Anti-hallucination: tvrdnje o AI vidljivosti SAMO iz 9a-9c dokaza; benchmark postoci se citiraju s izvorom, nikad kao mjerenje klijenta.

## Pilot (2026-07-19, bilko.cloud — postojeći intake klijent)

Score **100/100** uz 1 advisory nalaz: `Disallow: /znanje/` u robots.txt blokira content sekciju SVIM crawlerima (Google i AI) — content marketing nevidljiv. Sirovi dokazi: `~/system/evidence/seo/bilko.cloud/` + `~/system/evidence/105976/pilot-ai-readiness-bilko-cloud-2026-07-19.md`. Modul dokazano proizvodi actionable nalaz i na zdravom sajtu.

## Operativa

Pokreće se automatski kao dio postojećeg play-a (sekcija 9, poslije Content Gap koraka). Evidence OBAVEZNO kopirati u `~/system/evidence/seo/<domain>/` (/tmp se briše). Povezani runbook: "SEO Readiness Portal — Self-Serve Intake (Architecture + Operator Runbook)" (ista knjiga).