Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/sprints/backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: "Produktbacklog"
description: "Alla kända stories och uppgifter, speglar roadmap.md. Plockas in i sprintar vid behov."
category: sprint
status: active
last_updated: 2026-05-08
last_updated: 2026-05-13
tags: [backlog, roadmap, planning]
sections:
- Blockerare
Expand Down Expand Up @@ -62,6 +62,7 @@ sections:
| **Supabase Auth callback-route saknas** | 0.5 dag | Magic link och OAuth-callback från Supabase Auth landar bara på root-page utan att session etableras. Vi har egen email/password-flow så det är inte affärskritiskt idag, men blockerar framtida features (social login, magic link login, Supabase egna reset-mail). Bygg `/auth/callback`-route som anropar `supabase.auth.exchangeCodeForSession(code)` och redirectar till `/dashboard`. Hittad 2026-04-30 vid felsökning av password reset. |
| **URL-konfigurationsmatris** | 30 min | Skapa `docs/operations/url-configuration.md` som listar alla URL-config-platser och vad varje styr: Vercel `APP_URL`, Supabase Site URL + Redirect URLs, Stripe webhook endpoint, Resend domän-verifiering, ev. iOS prod-URL i `AppConfig.swift`. Vi har precis brunnit oss på trippel-miss (APP_URL + Site URL + Redirect URLs alla satta fel) — dokumentationen ska göra det tydligt vad som måste uppdateras vid byte av domän. Inkluderar checklista och länkar till dashboard-vägar. |
| **Städa Vercel env-variabler med literal `\\n` på slutet** | 15 min | `NEXT_PUBLIC_SUPABASE_URL` och `NEXT_PUBLIC_SUPABASE_ANON_KEY` i Vercel prod har literal backslash-n (`\\n`) på slutet av värdena. Next.js runtime kan strippa det, men `vercel env pull` skriver det som literal i .env-filen vilket bryter direktanrop mot Supabase API från lokala scripts. Hittad 2026-04-30. Fix: Vercel UI → respektive variabel → ta bort `\\n`-suffixet → spara → redeploy. |
| **Help-data drift protection** | 15-30 min | CI-validation som regenererar `articles-data.ts` och diffar mot committed version — failar PR om out-of-sync. Plus README-rad om `npm run generate:help`-workflow. Bevisat behov efter PR #333 (2026-05-13): staging hade tom hjälpsektion eftersom build-time-generator + `.vercelignore` (`*.md`) filtrerade bort markdown-källor. Detaljerad story med A/B/C/D-alternativ: [help-data-drift-protection.md](../stories/help-data-drift-protection.md). |

## Kodeffektivitet (tech debt)

Expand Down
148 changes: 148 additions & 0 deletions docs/stories/help-data-drift-protection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
---
title: "Help-data drift protection"
description: "Hindra att src/lib/help/articles-data.ts blir out-of-sync med markdown-källorna, eller att Vercel-build råkar generera tom help-data igen."
category: plan
status: draft
last_updated: 2026-05-13
tags: [help, build, vercel, drift-prevention, ci]
related:
- ../../package.json
- ../../scripts/generate-help-data.ts
- ../../src/lib/help/articles-data.ts
- ../../.vercelignore
sections:
- Problem
- Risk
- Mål
- Förslag på lösningar
- Rekommendation
- Acceptanskriterier
- Risk och tradeoffs
- Verifieringsstrategi
- Prioritet
---

# Help-data drift protection

## Problem

`src/lib/help/articles-data.ts` är auto-genererad av `scripts/generate-help-data.ts` från markdown-filer i `src/lib/help/articles/<role>/*.md`. Filen committas och importeras synkront i `src/lib/help/index.ts`. Hjälpsektionen är beroende av att den committade versionen faktiskt matchar källorna.

**Failure mode A — Vercel-byggets regenerering (fixad 2026-05-13, PR #333)**: Build-scriptet anropade `tsx scripts/generate-help-data.ts` före `next build`. Vercel kör `vercel build` som respekterar `.vercelignore` (`*.md`), så markdown-filerna filtrerades bort. Generatorn hittade noll filer och skrev `[]` till `articles-data.ts`, vilket överskrev den committade versionen. Build-loggen sa `Generated ... with 0 articles`, men bygget rapporterades som SUCCESS. Hjälpsidorna blev tomma i staging (och troligen prod) — lokalt fungerade allt eftersom markdown-filerna fanns på disk.

**Failure mode B — Glömd regenerering**: En utvecklare redigerar en markdown-artikel men glömmer `npm run generate:help` innan commit. `articles-data.ts` är då out-of-sync med källorna. Hjälpsidorna visar gammal data tills någon märker det, ofta lång tid efter.

**Varför det är svårt att upptäcka:**
- Build är "grön" — exit code 0, alla checks passerar, deploy READY.
- TypeScript-kompilering passerar (tom array är giltig).
- Inga tester fångar tom data eftersom unit-tester typiskt mockar artikelladdning.
- Lokalt fungerar allt, så bug:en syns bara i staging/prod efter deploy.
- Slutanvändaren ser "Inga artiklar matchade din sökning" — kan tolkas som UI-bug, inte data-bug.

## Risk

| Risk | Sannolikhet | Påverkan |
|------|-------------|----------|
| Staging/prod får tom hjälpsektion | Medel (failure mode A inträffat 1 gång; B kan ske vid varje markdown-edit) | Hög — användare får ingen hjälp, försämrad demo, supportbörda |
| Build blir "grön" trots fel innehåll | Hög | Hög — falsk trygghet, inga automatiska larm |
| Lokalt fungerar fortfarande | Hög | Medel — utvecklaren ser inte buggen, ingen incident-driver |
| Hjälpsökningar pekar på saknade slugs | Medel | Låg-medel — 404-spikar i loggar utan tydlig orsak |

## Mål

Skydda mot:
- **Stale generated data** — `articles-data.ts` är gammal jämfört med markdown-källor.
- **Missing regeneration efter markdown-edits** — utvecklaren commitar markdown utan att uppdatera den genererade filen.
- **Build/runtime drift mellan lokalt och Vercel** — bygg-pipelinen producerar annan output än utvecklaren ser.

Icke-mål:
- Att flytta tillbaka regenereringen till build-time. Det är vad som orsakade originalbuggen.
- Att designa om help-systemet (synkron import-modell fungerar bra).

## Förslag på lösningar

### A. Pre-commit check

Hook som varnar (eller blockerar) om någon `articles/**/*.md` har nyare mtime än `articles-data.ts`, eller om en diff mellan regenerering och committed version inte är tom.

**Implementation:** Bash i `.husky/pre-commit` (eller motsvarande hook-katalog). Kör `tsx scripts/generate-help-data.ts` mot en tempfil, diffa mot committed `articles-data.ts`. Om olika → ge ett tydligt felmeddelande som pekar på `npm run generate:help`.

**Pros:** Fångar buggen vid commit-tillfället. Snabb feedback. Påverkar bara utvecklaren som glömde regenerera.
**Cons:** Lokala hooks kan skippas med `--no-verify`. Kräver att alla utvecklare har hooks installerade.

### B. CI validation

GitHub Actions-step som regenererar `articles-data.ts` i CI-miljön och diffar mot committed version. Om olika → CI rött.

**Implementation:** Ett `validate-help-data`-job i `.github/workflows/check.yml`. Kör `tsx scripts/generate-help-data.ts`, sedan `git diff --exit-code src/lib/help/articles-data.ts`. Failar med tydligt meddelande.

**Pros:** Kan inte skippas. Fångar både A och B. Krävs som status-check på PR.
**Cons:** Lägger till några sekunder per CI-körning. Kräver att markdown-filer faktiskt finns i CI (vilket de gör — CI är inte Vercel-build-miljö).

### C. Runtime sanity check

Vid app-start eller per-request på `/api/feature-flags` (eller liknande low-frequency endpoint): logga `error` (eller skicka Sentry-larm) om `allArticles.length === 0` när `help_center`-flaggan är på.

**Implementation:** En init-check i `src/lib/help/index.ts` eller i en `instrumentation.ts`-fil. `if (process.env.NODE_ENV === 'production' && allArticles.length === 0) logger.error("help_center: zero articles in bundle")`.

**Pros:** Fångar buggen i produktion även om CI/pre-commit missar. Self-healing detection.
**Cons:** Reagerar först efter deploy, inte före. Sentry-brus om tom data är legitim någonstans (vilket den inte borde vara).

### D. Developer UX

- Tydlig README-rad i `src/lib/help/articles/`: "Efter ändring av .md-filer: kör `npm run generate:help` och commita både md + articles-data.ts".
- Felmeddelande i `npm run generate:help` om dir saknas — annars skriver det tyst tom fil.
- `npm run lint:help` eller liknande alias som diffar utan att skriva (read-only check).

**Pros:** Hjälper utvecklare göra rätt från början. Komplement till A/B/C.
**Cons:** Hjälper inte om utvecklaren inte läser docs eller glömmer.

## Rekommendation

**MVP: B (CI validation), plus minimal D (README-rad).**

Motivation:
- B är det enda alternativet som **inte kan skippas** av utvecklare och **inte beror på lokal miljö**. CI är auktoritativ.
- B fångar **både failure mode A och B** med samma mekanism — diffen mellan regenererad och committed version.
- Implementation är trivial (~10 rader yaml + 3 rader bash) och tar <5 sek per CI-körning.
- D är gratis tillägg som minskar friktion för utvecklare som faktiskt vill göra rätt.
- A (pre-commit) är värdefull men inte tillräcklig som primärt skydd (`--no-verify` finns; nya utvecklare kanske inte har hooks). Kan läggas till senare om CI-misstag blir vanliga.
- C (runtime sanity) är overkill när B fångar buggen pre-merge. Kan läggas till om vi vill ha defense-in-depth, men inte värt MVP-tiden.

**Avgränsning:** Inte göra något åt `.vercelignore`-raden `*.md` — den är inte längre relevant eftersom vi inte kör generatorn under Vercel-build. Att ta bort den skulle bara öka deploy-storleken utan vinst.

## Acceptanskriterier

- [ ] CI failar (rött PR-check) om en PR ändrar `articles/**/*.md` utan att uppdatera `articles-data.ts`, eller om någon ändrar `articles-data.ts` på sätt som inte matchar markdown.
- [ ] CI failar med tydligt felmeddelande som nämner `npm run generate:help` (inte bara en stack trace).
- [ ] `npm run generate:help` är dokumenterad i README eller `src/lib/help/articles/README.md` (filen behöver kanske skapas) som det enda sättet att uppdatera help-data.
- [ ] Bevisad: PR som introducerar en orelaterad markdown-edit utan regenerering blockeras av CI-checken.
- [ ] Bevisad: PR som regenererar korrekt passerar utan friktion.

## Risk och tradeoffs

| Risk | Mitigation |
|------|------------|
| Generated file in git skapar merge-conflicts vid samtidiga markdown-edits | Acceptabel — konflikten är trivial att lösa (kör `npm run generate:help` post-merge). Alternativet (generera vid build) skapade värre bugg. |
| Längre CI-tid | Försumbart — generatorn tar <2 sek. CI är redan flera minuter. |
| False positives om `JSON.stringify`-output blir non-deterministisk | Generatorn är deterministisk per inspektion (samma input → samma output). Om problem uppstår: normalisera diff:en (whitespace-insensitive) eller lägg till sort-key. |
| CI-checken blockerar legitima docs-only PR:s med markdown-format-fixar | Inget problem — sådana PR:s ska faktiskt regenerera, det är hela poängen. |
| Pre-commit hook (om vi lägger till A senare) ger friktion | Kan göras opt-in via husky, eller bara varning utan blockering. |

## Verifieringsstrategi

1. **Negative test**: Skapa en test-PR som ändrar `src/lib/help/articles/customer/boka-en-tjanst.md` (t.ex. byter ett ord i summary) utan att köra generatorn. Förvänta: CI rött med tydligt felmeddelande.
2. **Positive test**: I samma PR, kör `npm run generate:help` och committa båda. Förvänta: CI grönt.
3. **Roundtrip test**: Verifiera att `npm run generate:help` är idempotent — kör två gånger, ingen diff andra gången.
4. **No-op test**: PR som inte rör help-systemet alls — CI-checken ska passera utan extra tid (alternativt: bara köra checken om relevanta paths ändrats, via `paths:` i workflow trigger).

## Prioritet

| Dimension | Bedömning |
|-----------|-----------|
| Severity | **Medel-Hög**. Hjälpsektionen är inte affärskritisk (ingen pengar-flöde), men är central för demo och onboarding. Tom help i staging undergräver demokänslan. |
| Likelihood | **Medel**. Failure mode A är fixad. Failure mode B (glömd regenerering) kan ske vid varje markdown-edit — kanske 1 gång per 5 edits utan skydd. |
| Effort | **Small (15-30 min)** för MVP (B + D). Kan göras som en separat slice utan beroenden. |
| Sprint-prio | **Nästa lediga sprint**, inte hotfix. Vi har redan fixat akut-buggen (PR #333). Detta är härdning. Inte värt att bryta pågående sprint-arbete för. |

**Rekommenderad sprint-prio: Lägsta i nästa sprint.** Sätt på backlogen som "S68-X: Help-data drift protection" eller liknande. Plocka när någon har 30 min utrymme.
59 changes: 43 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"dev:offline": "FEATURE_OFFLINE_MODE=true next dev --webpack",
"generate:help": "tsx scripts/generate-help-data.ts",
"prebuild": "tsx scripts/check-prod-env.ts",
"build": "prisma generate && tsx scripts/generate-help-data.ts && next build --webpack",
"build": "prisma generate && next build --webpack",
"postinstall": "prisma generate",
"start": "next start",
"lint": "eslint",
Expand Down Expand Up @@ -113,9 +113,9 @@
"next": "16.1.7",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"react": "19.2.5",
"react": "19.2.6",
"react-day-picker": "^9.11.1",
"react-dom": "19.2.5",
"react-dom": "19.2.6",
"react-hook-form": "^7.66.0",
"react-leaflet": "^4.2.1",
"recharts": "^3.7.0",
Expand All @@ -125,7 +125,7 @@
"swr": "^2.4.1",
"tailwind-merge": "^3.4.1",
"vaul": "^1.1.2",
"zod": "^4.1.12"
"zod": "^4.4.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.3",
Expand Down
46 changes: 46 additions & 0 deletions src/domain/customer-insight/CustomerInsightService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,52 @@ describe("CustomerInsightService", () => {
expect(result.value.vipScore).toBe("low")
})

// Regression tests for Sonnet 4.6 returning prose around JSON
// (matches salvage-vision learning: tolerant JSON extraction needed)
it("handles prose before JSON object", async () => {
const json = JSON.stringify(VALID_LLM_RESPONSE)

mockCreate.mockResolvedValue({
content: [{ type: "text", text: "Här är analysen:\n" + json }],
})

const result = await service.generateInsight(SAMPLE_DATA, SAMPLE_METRICS)
expect(result.isSuccess).toBe(true)
expect(result.value.frequency).toBe("Regelbunden (var 8:e vecka)")
})

it("handles prose after JSON object", async () => {
const json = JSON.stringify(VALID_LLM_RESPONSE)

mockCreate.mockResolvedValue({
content: [{ type: "text", text: json + "\n\nHoppas detta hjälper!" }],
})

const result = await service.generateInsight(SAMPLE_DATA, SAMPLE_METRICS)
expect(result.isSuccess).toBe(true)
expect(result.value.frequency).toBe("Regelbunden (var 8:e vecka)")
})

it("handles markdown code block wrapped in prose", async () => {
const json = JSON.stringify(VALID_LLM_RESPONSE)

mockCreate.mockResolvedValue({
content: [
{
type: "text",
text:
"Visst, här kommer analysen:\n```json\n" +
json +
"\n```\nHör av dig om något.",
},
],
})

const result = await service.generateInsight(SAMPLE_DATA, SAMPLE_METRICS)
expect(result.isSuccess).toBe(true)
expect(result.value.frequency).toBe("Regelbunden (var 8:e vecka)")
})

describe("mapInsightErrorToStatus", () => {
it("maps NO_DATA to 400", () => {
expect(mapInsightErrorToStatus({ type: "NO_DATA", message: "" })).toBe(400)
Expand Down
Loading