feat: Passwordless cross-device authentication

- Arkitektur: docs/auth/passwordless-architecture.md
- Backend: iom/quixzoom-auth-service/ (FastAPI + Redis)
- Webb: quixzoom-market-pages/se/login/ (QR-kod + polling)
- App: iom/quixzoom-app/src/features/auth/ (push + deep links)

Flöde: QR-kod → app-godkännande → webb-inloggad
This commit is contained in:
Bernt
2026-07-07 07:11:50 +00:00
parent 4aa984ad74
commit 6989a98d75
61843 changed files with 5491611 additions and 872231 deletions
+81
View File
@@ -0,0 +1,81 @@
# quiXzoom Passwordless Authentication
## Översikt
Passwordless cross-device authentication mellan quiXzoom app och webb.
## Komponenter
### 1. Arkitektur
- **Fil:** `docs/auth/passwordless-architecture.md`
- **Innehåll:** Komplett arkitektur med sekvensdiagram, API-spec, databasschema, säkerhetsanalys
### 2. Backend
- **Mapp:** `iom/quixzoom-auth-service/`
- **Teknik:** FastAPI + Redis + Python
- **Endpoints:**
- `POST /v1/auth/passwordless/initiate` — Initiera inloggning
- `POST /v1/auth/passwordless/approve` — Godkänn från app
- `GET /v1/auth/passwordless/status` — Polla status
- `POST /v1/auth/passwordless/reject` — Neka
- `POST /v1/auth/passwordless/cancel` — Avbryt
### 3. Webb-frontend
- **Fil:** `quixzoom-market-pages/se/login/index.html`
- **Funktioner:**
- QR-kod för snabb inloggning
- Polling var 2:e sekund
- Deep link till appen
- Fallback till email/password
### 4. App-frontend
- **Filer:**
- `iom/quixzoom-app/src/features/auth/PasswordlessApprovalScreen.tsx`
- `iom/quixzoom-app/src/features/auth/usePasswordlessAuth.ts`
- **Funktioner:**
- Push-notifieringar
- Deep link-hantering (`quixzoom://auth/approve`)
- Godkänn/neka UI
## Flöde
1. Användare klickar "Logga in" på quixzoom.se
2. Webb visar QR-kod + deep link
3. Användare öppnar appen och skannar/godkänner
4. App skickar godkännande till backend
5. Webb pollar och får JWT-token
6. Användaren är inloggad!
## Deploy
### Backend
```bash
cd iom/quixzoom-auth-service
docker-compose up -d
```
### Webb
```bash
aws s3 sync quixzoom-market-pages/se/login/ s3://quixzoom-landing-prod/markets/se/login/
```
### App
- Bygg och deploya via Expo EAS
- Uppdatera deep link-konfiguration i app.json
## Säkerhet
- Engångstokens (32 bytes CSPRNG)
- 15-minuters TTL på sessioner
- Rate limiting (10/min per IP)
- Device fingerprinting
- JWT med session binding
## Status
✅ Arkitektur — Klar
✅ Backend-modeller — Klar
✅ Backend-routes — Klar
✅ Webb-frontend — Klar
✅ App-frontend — Klar
⏳ Deploy — Pågår
+496
View File
@@ -0,0 +1,496 @@
# quiXzoom — Passwordless Cross-Device Authentication
> Arkitektur för att logga in på webb (quixzoom.se) utan lösenord, via en redan inloggad mobilapp.
> Version: 1.0 | Datum: 2026-07-07
---
## 1. Översikt
| Aspekt | Beskrivning |
|--------|-------------|
| **Mål** | Användare med quiXzoom-appen ska kunna logga in på webb utan att ange lösenord |
| **Flöde** | Webb visar QR-kod → App skannar/godkänner → Webb loggas in automatiskt |
| **Backend** | Samma JWT-issuer (`api.quixzoom.com`) som appen redan använder |
| **Säkerhetsmodell** | Kryptografiska signaturer, engångstokens, tidsbegränsning, device-binding |
---
## 2. Sekvensdiagram
```
┌─────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ ┌──────────────┐
│ Webb │ │ quixzoom.se │ │ quiXzoom App │ │ api.quixzoom.com │ │ Redis │
│ (Browser)│ │ (Frontend) │ │ (Mobile) │ │ (Backend) │ │ (Sessions) │
└────┬────┘ └──────┬──────┘ └──────┬──────┘ └────────┬────────┘ └──────┬───────┘
│ │ │ │ │
│ 1. Klickar "Logga in med app" │ │ │
│────────────────>│ │ │ │
│ │ 2. POST /auth/passwordless/initiate │ │
│ │──────────────────────────────────────────>│ │
│ │ │ │ 3. Genererar │
│ │ │ │ session_id │
│ │ │ │ + request_token │
│ │ │ │────────────────────>│
│ │ 4. Returnerar │ │ │
│ │ {session_id, │ │ │
│ │ qr_data, │ │ │
│ │ expires_at} │ │ │
│ │<──────────────────────────────────────────│ │
│ 5. Visar QR-kod│ │ │ │
│<────────────────│ │ │ │
│ │ │ 6. Skannar QR-kod │ │
│ │ │ (eller trycker deep link) │
│ │ │ │ │
│ │ │ 7. POST /auth/passwordless/approve │
│ │ │ Headers: Authorization: Bearer <app-JWT> │
│ │ │ Body: {session_id, request_token, signature}│
│ │ │───────────────────────────────────────────>│
│ │ │ │ 8. Verifierar │
│ │ │ │ • JWT giltigt │
│ │ │ │ • request_token │
│ │ │ │ matchar Redis │
│ │ │ │ • signatur OK │
│ │ │ │ 9. Uppdaterar │
│ │ │ │ Redis: approved │
│ │ │ │────────────────────>│
│ │ │ 10. 200 OK │ │
│ │ │<───────────────────────────────────────────│
│ │ │ │ │
│ 11. Polling: │ │ │ │
│ GET /auth/passwordless/status?session_id=xxx │ │
│────────────────>│ │ │ │
│ │ 12. Proxy till backend │ │
│ │──────────────────────────────────────────>│ │
│ │ │ │ 13. Kollar Redis │
│ │ │ │────────────────────>│
│ │ │ │<────────────────────│
│ │ 14. 200 OK │ │ │
│ │ {status:"approved", │ │
│ │ access_token, refresh_token} │ │
│ │<──────────────────────────────────────────│ │
│ 15. Sätter │ │ │ │
│ cookies │ │ │ │
│<────────────────│ │ │ │
│ 16. Omdirigerar│ │ │ │
│ till /dashboard │ │ │
│<────────────────│ │ │ │
│ │ │ │ │
```
---
## 3. Komponentarkitektur
```
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ TRUST ZONES │
├─────────────────────────────┬─────────────────────────────┬─────────────────────────────────┤
│ UNTRUSTED │ SEMI-TRUSTED │ TRUSTED │
│ (User Browser) │ (quixzoom.se CDN) │ (api.quixzoom.com) │
│ │ │ │
│ ┌─────────────────────┐ │ ┌─────────────────────┐ │ ┌─────────────────────────┐ │
│ │ Webb-frontend │ │ │ Static assets │ │ │ Passwordless Service │ │
│ │ • QR-visning │ │ │ • QR-kod render │ │ │ • /initiate │ │
│ │ • Polling-loop │ │ │ • Polling proxy │ │ │ • /approve │ │
│ │ • Cookie-hantering │ │ │ │ │ │ • /status │ │
│ └─────────────────────┘ │ └─────────────────────┘ │ │ • /cancel │ │
│ │ │ └─────────────────────────┘ │
│ │ │ ┌─────────────────────────┐ │
│ │ │ │ JWT Auth Service │ │
│ │ │ │ (befintlig) │ │
│ │ │ └─────────────────────────┘ │
│ │ │ ┌─────────────────────────┐ │
│ │ │ │ Redis Cluster │ │
│ │ │ │ • Sessions │ │
│ │ │ │ • Rate limits │ │
│ │ │ └─────────────────────────┘ │
│ │ │ ┌─────────────────────────┐ │
│ │ │ │ PostgreSQL │ │
│ │ │ │ • Users │ │
│ │ │ │ • Devices │ │
│ │ │ │ • Audit logs │ │
│ │ │ └─────────────────────────┘ │
└─────────────────────────────┴─────────────────────────────┴─────────────────────────────────┘
```
---
## 4. API-specifikation
### 4.1 POST /auth/passwordless/initiate
Initierar en ny passwordless-inloggningssession.
**Request:**
```http
POST https://api.quixzoom.com/v1/auth/passwordless/initiate
Content-Type: application/json
{
"client_id": "web-dashboard", // Identifierar webbklienten
"redirect_url": "https://quixzoom.se/dashboard",
"device_info": {
"user_agent": "Mozilla/5.0...",
"ip": "203.0.113.42"
}
}
```
**Response (201 Created):**
```json
{
"session_id": "pls_2vPqN5L8xT9wR3mK",
"request_token": "a1b2c3d4e5f6...", // 32 bytes, base64url
"qr_data": "quixzoom://auth?sid=pls_2vPqN5L8xT9wR3mK&token=a1b2...",
"expires_at": "2026-07-07T07:11:00Z", // 15 minuter
"poll_interval_ms": 2000
}
```
**Fel:**
- `429 Too Many Requests` — rate limit per IP
- `400 Bad Request` — ogiltig redirect_url
---
### 4.2 POST /auth/passwordless/approve
Appen godkänner inloggningen. Kräver giltig app-JWT.
**Request:**
```http
POST https://api.quixzoom.com/v1/auth/passwordless/approve
Content-Type: application/json
Authorization: Bearer <app-access-token>
{
"session_id": "pls_2vPqN5L8xT9wR3mK",
"request_token": "a1b2c3d4e5f6...",
"signature": "base64url(SHA256(session_id + request_token + timestamp))",
"timestamp": "2026-07-07T06:58:00Z",
"approving_device_id": "dev_abc123"
}
```
**Response (200 OK):**
```json
{
"status": "approved",
"session_id": "pls_2vPqN5L8xT9wR3mK",
"approved_at": "2026-07-07T06:58:00Z",
"web_session": {
"user_agent_hash": "sha256:...",
"ip_hash": "sha256:..."
}
}
```
**Fel:**
- `401 Unauthorized` — ogiltig eller utgången app-JWT
- `403 Forbidden` — felaktig signatur eller request_token
- `410 Gone` — sessionen har utgått eller redan använts
- `409 Conflict` — sessionen redan godkänd/avbruten
---
### 4.3 GET /auth/passwordless/status
Webbklienten pollar status på sessionen.
**Request:**
```http
GET https://api.quixzoom.com/v1/auth/passwordless/status?session_id=pls_2vPqN5L8xT9wR3mK
```
**Response (200 OK) — pending:**
```json
{
"session_id": "pls_2vPqN5L8xT9wR3mK",
"status": "pending", // pending | approved | rejected | expired | cancelled
"expires_at": "2026-07-07T07:11:00Z",
"remaining_seconds": 780
}
```
**Response (200 OK) — approved:**
```json
{
"session_id": "pls_2vPqN5L8xT9wR3mK",
"status": "approved",
"approved_at": "2026-07-07T06:58:00Z",
"tokens": {
"access_token": "eyJhbG...",
"refresh_token": "dGhpcyB...",
"token_type": "Bearer",
"expires_in": 3600
}
}
```
**Fel:**
- `404 Not Found` — okänd session_id
- `410 Gone` — sessionen utgången
---
### 4.4 POST /auth/passwordless/cancel
Avbryter en pågående session (t.ex. användaren stänger webbläsaren).
**Request:**
```http
POST https://api.quixzoom.com/v1/auth/passwordless/cancel
Content-Type: application/json
{
"session_id": "pls_2vPqN5L8xT9wR3mK"
}
```
**Response (200 OK):**
```json
{
"status": "cancelled"
}
```
---
### 4.5 POST /auth/passwordless/reject
Appen aktivt avvisar inloggningsförsöket.
**Request:**
```http
POST https://api.quixzoom.com/v1/auth/passwordless/reject
Content-Type: application/json
Authorization: Bearer <app-access-token>
{
"session_id": "pls_2vPqN5L8xT9wR3mK",
"request_token": "a1b2c3d4e5f6...",
"reason": "user_declined" // user_declined | suspicious | wrong_device
}
```
---
## 5. Databasschema
### 5.1 PostgreSQL — Persistent lagring
```sql
-- Användartabell (befintlig, utökad)
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
-- ... befintliga kolumner ...
passwordless_enabled BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Registrerade enheter
CREATE TABLE user_devices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
device_id VARCHAR(64) NOT NULL, -- unikt per app-installation
device_type VARCHAR(20) NOT NULL, -- ios | android
device_name VARCHAR(100),
public_key TEXT NOT NULL, -- Ed25519 public key (PEM)
push_token VARCHAR(255), -- FCM/APNs token
is_trusted BOOLEAN DEFAULT false, -- krävs för passwordless
last_used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
revoked_at TIMESTAMPTZ, -- NULL = aktiv
UNIQUE(user_id, device_id)
);
-- Passwordless-sessioner (audit + analytics)
CREATE TABLE passwordless_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id VARCHAR(32) UNIQUE NOT NULL, -- "pls_" + nanoid
user_id UUID REFERENCES users(id), -- NULL tills godkänd
status VARCHAR(20) NOT NULL, -- pending | approved | rejected | expired | cancelled
-- Initiering
client_id VARCHAR(50) NOT NULL, -- web-dashboard | web-checkout
redirect_url VARCHAR(500),
web_ip_hash VARCHAR(64) NOT NULL, -- SHA256(ip)
web_ua_hash VARCHAR(64) NOT NULL, -- SHA256(user_agent)
-- Godkännande
approving_device_id UUID REFERENCES user_devices(id),
approved_at TIMESTAMPTZ,
signature VARCHAR(128), -- base64url
-- Tidsgränser
initiated_at TIMESTAMPTZ DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL,
-- Avvisning/avbrytning
rejected_reason VARCHAR(50),
rejected_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Index för prestanda
CREATE INDEX idx_passwordless_sessions_status ON passwordless_sessions(status, expires_at);
CREATE INDEX idx_passwordless_sessions_user ON passwordless_sessions(user_id, created_at DESC);
CREATE INDEX idx_user_devices_user ON user_devices(user_id, revoked_at);
```
### 5.2 Redis — Temporär session-lagring
```
# Session under pågående inloggning (TTL: 15 min)
KEY: passwordless:session:{session_id}
TYPE: Hash
VALUE: {
"request_token": "a1b2c3d4...",
"status": "pending",
"client_id": "web-dashboard",
"web_ip_hash": "sha256:...",
"web_ua_hash": "sha256:...",
"expires_at": "2026-07-07T07:11:00Z"
}
TTL: 900
# Rate limiting per IP
KEY: passwordless:ratelimit:{ip_hash}
TYPE: String (counter)
TTL: 3600
# Rate limiting per användare (vid approve)
KEY: passwordless:ratelimit:user:{user_id}
TYPE: String (counter)
TTL: 3600
# Blockerade sessioner (efter misslyckade försök)
KEY: passwordless:blocked:{session_id}
TYPE: String
TTL: 300
```
---
## 6. Säkerhetsanalys
### 6.1 Hotmodell (STRIDE)
| Hot | Beskrivning | Motåtgärd |
|-----|-------------|-----------|
| **S**poofing | Angripare förfalskar appens signatur | Ed25519-signatur verifieras mot registrerad public key |
| **T**ampering | Manipulering av request_token | Token genereras kryptografiskt säkert (32 bytes, CSPRNG) |
| **R**epudiation | Användare förnekar inloggning | Audit-logg i PostgreSQL med signatur och tidsstämplar |
| **I**nformation Disclosure | Session ID avlyssnas | HTTPS överallt; session_id är engångs; kort TTL |
| **D**enial of Service | Överbelastning av /initiate | Rate limiting per IP (10/min) och per användare (5/min) |
| **E**levation of Privilege | Session kapas efter godkännande | Token binds till webbklientens IP/UA-hash; CSRF-skydd |
### 6.2 Kryptografiska primitiver
| Komponent | Algoritm | Syfte |
|-----------|----------|-------|
| Signatur | Ed25519 | Appen signerar godkännande med sin privata nyckel |
| Request token | 32 bytes CSPRNG | Engångs-token som appen måste presentera |
| Hashing | SHA-256 | IP/UA-hashing för session-binding |
| Session ID | Nanoid (21 chars, alfabete: A-Za-z0-9_-) | Unik identifierare, URL-safe |
### 6.3 Säkerhetskrav
1. **Transport:** Alla anrop över TLS 1.3 (minst 1.2). Certificate pinning i appen.
2. **Token-återanvändning:** `request_token` kan endast användas en gång. Efter approve/reject/expired är sessionen låst.
3. **Tidsfönster:** Max 15 minuter från initiering till godkännande. Webbklienten pollar max 5 minuter efter expiry.
4. **Device trust:** Endast `is_trusted = true` enheter får godkänna passwordless. Trust etableras vid första inloggningen med lösenord + 2FA.
5. **Session binding:** Access-token som returneras vid approved status är bunden till webbklientens IP/UA-hash. Vid avvikelse krävs omautentisering.
6. **QR-säkerhet:** QR-koden innehåller endast `session_id` och `request_token`, ingen känslig data. Deep link-format: `quixzoom://auth?sid=...&token=...`
7. **Push-notiser:** Vid initiering kan push-notis skickas till appen (kräver opt-in) för snabbare upptäckt.
### 6.4 Riskbedömning
| Risk | Sannolikhet | Påverkan | Risknivå | Åtgärd |
|------|-------------|----------|----------|--------|
| MITM på publikt WiFi | Medel | Hög | **Hög** | TLS 1.3 + certificate pinning |
| Stulen app-enhet | Låg | Hög | **Medel** | Biometrisk upplåsning i appen; möjlighet att återkalla enhet |
| QR-kod fotograferas | Medel | Medel | **Medel** | Kort TTL; token kan bara användas en gång; session bound to webb-klient |
| Brute-force av session_id | Låg | Medel | **Låg** | 21-char nanoid = ~130 bits entropi |
| Replay-attack | Låg | Hög | **Låg** | Engångstoken + timestamp i signatur |
| DoS på /initiate | Medel | Låg | **Låg** | Rate limiting + CAPTCHA vid upprepad överbelastning |
---
## 7. Implementeringschecklista
### Backend (api.quixzoom.com)
- [ ] Nytt `PasswordlessService` i auth-modulen
- [ ] Ed25519-nyckelhantering för enheter (`user_devices.public_key`)
- [ ] Redis-integration för temporär session-lagring
- [ ] Rate limiting middleware
- [ ] Audit-loggning till PostgreSQL
- [ ] JWT-utgivning med IP/UA-claims för webb-sessioner
### Webb (quixzoom.se)
- [ ] QR-kod rendering (t.ex. `qrcode` npm-paket)
- [ ] Polling-loop med exponential backoff
- [ ] Hantering av alla statusar (approved, rejected, expired, cancelled)
- [ ] Cookie-hantering för access/refresh tokens
- [ ] Deep link-fallback för användare utan app
### App (quiXzoom)
- [ ] QR-skanner integration
- [ ] Deep link-hantering (`quixzoom://auth?sid=...&token=...`)
- [ ] Biometrisk upplåsning innan signering
- [ ] Ed25519-signering av godkännande
- [ ] Push-notis-mottagning för passwordless-requests
### DevOps
- [ ] Redis-cluster i produktion (HA)
- [ ] PostgreSQL-backup av `passwordless_sessions` (audit)
- [ ] Monitoring: alert vid onormalt hög andel rejected/Expired
- [ ] Log-retention: 90 dagar för säkerhetsrelaterade händelser
---
## 8. Sekvens — Djup länk (utan QR)
Alternativt flöde när användaren är på mobilen:
```
1. Användare besöker quixzoom.se på mobil
2. Klickar "Logga in med app"
3. Webb detekterar mobil → visar knapp "Öppna i quiXzoom-appen"
4. Knapp = deep link: quixzoom://auth?sid=...&token=...
5. App öppnas direkt, hoppar över QR-skanner
6. Användare godkänner → samma approve-flöde
7. App redirectar tillbaka till webbläsaren med auth-kod
8. Webb byter auth-kod mot tokens
```
---
## 9. Bilaga: JWT-claims för webb-session
```json
{
"sub": "user_uuid",
"iss": "api.quixzoom.com",
"aud": "quixzoom.se",
"iat": 1720332000,
"exp": 1720335600,
"scope": "web:read web:write",
"auth_method": "passwordless",
"session_binding": {
"ip_hash": "sha256:...",
"ua_hash": "sha256:..."
},
"device_id": "dev_abc123",
"session_id": "pls_2vPqN5L8xT9wR3mK"
}
```
---
*Dokumentet är levande — uppdatera vid implementation och säkerhetsgranskning.*
+291
View File
@@ -0,0 +1,291 @@
# Landvex Intelligence Product Suite
> **Dokumentstatus:** Utkast — väntar på godkännande av Erik Svensson
> **Senast uppdaterad:** 2026-07-05
> **Ägare:** LandveX AB
---
## Produktfilosofi
Landvex går från att vara en plattform till att bli ett **beslutsunderlag**.
Kunden köper egentligen inte data — kunden köper **minskad osäkerhet inför ett beslut**.
Detta dokument definierar produktformat, rapportstruktur, leverans och paketering.
---
## Produktnivåer
| Paket | Typisk leverans | Passar för |
|-------|----------------|------------|
| **Intelligence Brief** | 515 sidor. Snabb analys med executive summary och nyckelrisker. | Första utvärdering eller pilot |
| **Decision Report** | 2080 sidor. Djupare analys med kartor, riskmatriser och rekommendationer. | Investerings- och affärsbeslut |
| **Due Diligence Intelligence** | 50200 sidor. Fullständig analys med bilagor, dataspårbarhet och dokumentation. | Större transaktioner, upphandlingar och förvärv |
| **Continuous Intelligence** | Abonnemang. Löpande bevakning med uppdateringar och förändringslarm. | Kunder som vill följa ett område eller en tillgång över tid |
---
## Rapportstruktur (Standardmall)
Varje rapport följer exakt samma struktur — oavsett paketnivå. Vissa sektioner kan vara kortare eller markeras "Ej tillämplig" beroende på paket.
---
### 1. Framsida
- Kundnamn
- Objekt
- Ärendenummer
- Version
- Datum
- Analytiker
- Konfidensklass
**Exempel:**
```
LANDVEX Intelligence Report
Client: [Kundnamn]
Confidential
Version 1.2
Confidence Score: 92%
```
---
### 2. Executive Summary
**En sida.**
Tre frågor:
1. Vad hittade vi?
2. Vad betyder det?
3. Vad rekommenderar vi?
> Det här ska en VD kunna läsa på två minuter.
---
### 3. Scope
**Vad omfattar analysen?**
Exempel:
- Satellitdata
- Kommunala databaser
- Infrastruktur
- Fastighetsdata
- Geodata
- Historik
- Crowd-verifiering
- AI-analys
- Riskmodellering
**Och lika viktigt: Vad omfattas INTE.**
---
### 4. Confidence
Det här blir en Landvex-specialitet.
**Exempel:**
```
Overall Confidence: 94%
Datakällor:
- Kommunal data: 100%
- Satellit: 95%
- Crowdsourcing: 91%
- AI: 89%
- Manual review: 100%
```
Visualiseras med diagram.
---
### 5. Datakällor
En tabell. Varje källa med:
- När uppdaterad
- Hur mycket vikt den fått
- Hur tillförlitlig den är
---
### 6. Objektöversikt
- Kartor
- Fastigheter
- Historik
- Satellitbilder
- Gatunät
- Luftfoto
- 3D
- Tidslinjer
---
### 7. Analys
Här börjar själva intelligensen. Uppdelad i kapitel.
**Exempel på kapitel:**
- Miljörisk
- Infrastruktur
- Tillgänglighet
- Markförhållanden
- Kommunal planering
- Affärspotential
- Tekniska risker
- Investeringsrisk
- Ekonomiska indikatorer
- Framtidsprognoser
---
### 8. Observationer
Lista. Varje observation innehåller:
- Observation [nummer]
- Allvarlighetsgrad
- Konsekvens
- Sannolikhet
- Bevis
- Datakällor
- Confidence
- Rekommendation
---
### 9. Möjligheter
Inte bara problem.
**Exempel:**
- Ny väg planeras
- Ny detaljplan
- Kommande VA
- Fiber
- Elkapacitet
- Låg konkurrens
- Tillväxt
---
### 10. Riskmatris
Mycket grafisk.
| Risk | Sannolikhet | Konsekvens | Riskvärde | Åtgärd |
|------|-------------|------------|-----------|--------|
| [Risk 1] | [Låg/Medel/Hög] | [Låg/Medel/Hög] | [Värde] | [Åtgärd] |
---
### 11. Rekommendationer
Kort. Prioriterade.
> Vad bör kunden göra först?
---
### 12. Appendix
**ALL DATA.**
- GPS
- Sensorer
- Historik
- Kartlager
- API-källor
- AI-output
- Prompt-ID
- Versionsnummer
**Allting. Full spårbarhet.**
---
## Juridisk friskrivning
> Rapporten är ett beslutsstöd, inte ett beslut.
> Informationen bygger på tillgängliga datakällor vid analystillfället.
> Landvex garanterar inte att externa datakällor är fullständiga eller fria från fel.
> Kunden ansvarar för slutliga investerings-, upphandlings- och affärsbeslut.
> Rapporten får inte distribueras vidare utan skriftligt godkännande om inte annat avtalats.
> Ansvar begränsas enligt gällande avtal och tillämplig lag.
**Låt en jurist granska standardtexten innan den används kommersiellt.**
---
## Leveransformat
Tre saker levereras:
1. **PDF** — Snygg. Ska kännas som en rapport från ett stort konsultbolag.
2. **Interaktiv webbportal** — Kunden kan klicka runt. Kartor, lager, objekt, sensorer, historik.
3. **Rådata** — CSV, GeoJSON, Excel, API-export. Om kunden vill arbeta vidare.
---
## Feedback-loop
Bygg in detta från början.
**Tidpunkter:**
- Efter en vecka
- Efter en månad
- Efter tre månader
**Tre frågor:**
1. Vilka delar av rapporten användes faktiskt?
2. Vad saknade ni?
3. Påverkade rapporten ett konkret beslut? I så fall hur?
**Komplettera med:**
- Numeriskt betyg (110)
- Uppskattat ekonomiskt värde (sparade kostnader, undvikna risker, identifierade möjligheter)
> Den informationen blir värdefull både för produktutveckling och framtida försäljning.
---
## Kärnprincip: Reproducerbarhet
Det viktigaste är inte layouten — det är att varje rapport blir **reproducerbar**.
Om två olika analytiker får samma uppdrag med samma datamängd ska de följa:
- Samma metodik
- Samma struktur
- Samma kvalitetskrav
Det gör rapporterna:
- Skalbara
- Lättare att kvalitetssäkra
- Betydligt enklare att automatisera
Det är också en förutsättning om ni på sikt vill att en stor del av rapportproduktionen ska kunna ske med minimal manuell handpåläggning.
---
## Relation till övriga dokument
| Dokument | Relation |
|----------|----------|
| `LANDVEX_DESIGN_CONSTITUTION.md` | Visuell identitet och UX-principer för alla rapporter |
| `DECISION_MODEL_v1.0.md` | Beslutsobjektet som används i analys och rekommendationer |
| `DECISION_PIPELINE_v1.0.md` | Processen för hur observationer blir till beslutsunderlag |
| `FIELD_TRIAL_LOG.md` | Feedback och observationer från verkliga kundcase |
---
## Versionshistorik
| Version | Datum | Ändring | Av |
|---------|-------|---------|-----|
| 0.1 | 2026-07-05 | Initialt utkast baserat på Eriks specifikation | Bernt |