05ed037fe8
- DNS: pilot.landvex.com -> 16.170.83.169 - TLS: Let's Encrypt certificate (expires 2026-09-30) - Nginx: reverse proxy with SSL termination - API: https://pilot.landvex.com/api/v1/missions - UI: https://pilot.landvex.com/ - Upload: POST /api/v1/missions/import (multipart/form-data) Verified: ✅ https://pilot.landvex.com/health ✅ https://pilot.landvex.com/version ✅ https://pilot.landvex.com/api/v1/missions (list) ✅ https://pilot.landvex.com/api/v1/missions/:id (get) ✅ POST /api/v1/missions/import (video upload) ✅ UI loads with title 'LandveX Intelligence Lab' Next: Pilot 001 — Break the system!
25 KiB
25 KiB
LandveX Enterprise Platform — Rapporterings- och Analysmodul
Översikt
Denna modul ger kommuner och andra kunder self-service rapportering med schemaläggning, delning och export. Den integreras med LandveX befintliga datakällor och exponeras via ett REST-API.
1. Datamodell
1.1 Entiteter (sammansatt vy)
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Report │ │ ReportSchedule │ │ ReportShare │
├─────────────────┤ ├─────────────────┤ ├─────────────────┤
│ id (PK) │ │ id (PK) │ │ id (PK) │
│ tenantId (FK) │◄────┤ reportId (FK) │ │ reportId (FK) │
│ name │ │ cronExpression │ │ sharedWithUserId│
│ description │ │ nextRunAt │ │ sharedWithEmail │
│ type │ │ lastRunAt │ │ permission │
│ config (JSON) │ │ status │ │ expiresAt │
│ dataSourceId │ │ createdBy │ │ createdAt │
│ createdBy │ └─────────────────┘ └─────────────────┘
│ createdAt │
│ updatedAt │ ┌─────────────────┐ ┌─────────────────┐
└─────────────────┘ │ ReportRun │ │ DataSource │
├─────────────────┤ ├─────────────────┤
│ id (PK) │ │ id (PK) │
│ reportId (FK) │ │ tenantId (FK) │
│ status │ │ name │
│ startedAt │ │ type │
│ completedAt │ │ connection │
│ resultUrl │ │ config (JSON) │
│ rowCount │ │ status │
│ errorMessage │ └─────────────────┘
└─────────────────┘
1.2 PostgreSQL DDL
-- Rapporter
CREATE TABLE reports (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
name VARCHAR(255) NOT NULL,
description TEXT,
type VARCHAR(50) NOT NULL CHECK (type IN ('table','chart','pivot','map','dashboard')),
config JSONB NOT NULL DEFAULT '{}',
data_source_id UUID REFERENCES data_sources(id),
created_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Körningshistorik
CREATE TABLE report_runs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
report_id UUID NOT NULL REFERENCES reports(id) ON DELETE CASCADE,
status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','running','completed','failed')),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
result_url TEXT,
row_count INTEGER,
error_message TEXT,
triggered_by UUID REFERENCES users(id),
created_at TIMESTAMPTZ DEFAULT now()
);
-- Scheman
CREATE TABLE report_schedules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
report_id UUID NOT NULL REFERENCES reports(id) ON DELETE CASCADE,
cron_expression VARCHAR(100) NOT NULL,
timezone VARCHAR(50) DEFAULT 'Europe/Stockholm',
next_run_at TIMESTAMPTZ,
last_run_at TIMESTAMPTZ,
status VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active','paused','disabled')),
created_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ DEFAULT now()
);
-- Delning
CREATE TABLE report_shares (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
report_id UUID NOT NULL REFERENCES reports(id) ON DELETE CASCADE,
shared_with_user_id UUID REFERENCES users(id),
shared_with_email VARCHAR(255),
permission VARCHAR(20) NOT NULL DEFAULT 'read' CHECK (permission IN ('read','write','admin')),
expires_at TIMESTAMPTZ,
created_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ DEFAULT now()
);
-- Datakällor (befintlig tabell, utökas)
CREATE TABLE data_sources (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
name VARCHAR(255) NOT NULL,
type VARCHAR(50) NOT NULL CHECK (type IN ('postgresql','mysql','api','s3','bigquery','snowflake')),
connection JSONB NOT NULL,
config JSONB DEFAULT '{}',
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMPTZ DEFAULT now()
);
-- Index
CREATE INDEX idx_reports_tenant ON reports(tenant_id);
CREATE INDEX idx_report_runs_report ON report_runs(report_id, created_at DESC);
CREATE INDEX idx_schedules_next_run ON report_schedules(next_run_at) WHERE status = 'active';
CREATE INDEX idx_shares_user ON report_shares(shared_with_user_id);
2. Rapport-API
2.1 REST Endpoints
| Metod | Endpoint | Beskrivning |
|---|---|---|
| GET | /api/v1/reports |
Lista rapporter (tenant-scoped) |
| POST | /api/v1/reports |
Skapa ny rapport |
| GET | /api/v1/reports/:id |
Hämta rapport med config |
| PUT | /api/v1/reports/:id |
Uppdatera rapport |
| DELETE | /api/v1/reports/:id |
Ta bort rapport |
| POST | /api/v1/reports/:id/run |
Kör rapport on-demand |
| GET | /api/v1/reports/:id/runs |
Körningshistorik |
| GET | /api/v1/reports/:id/runs/:runId/result |
Hämta resultat (JSON/CSV) |
| POST | /api/v1/reports/:id/export |
Exportera rapport |
| POST | /api/v1/reports/:id/schedule |
Skapa schema |
| GET | /api/v1/reports/:id/schedule |
Visa schema |
| PUT | /api/v1/reports/:id/schedule |
Uppdatera schema |
| DELETE | /api/v1/reports/:id/schedule |
Ta bort schema |
| POST | /api/v1/reports/:id/share |
Dela rapport |
| GET | /api/v1/reports/:id/shares |
Lista delningar |
| DELETE | /api/v1/reports/:id/shares/:shareId |
Återkalla delning |
2.2 OpenAPI-spec (kärn-endpoints)
openapi: 3.0.3
info:
title: LandveX Reporting API
version: 1.0.0
paths:
/api/v1/reports:
get:
summary: Lista rapporter
parameters:
- name: tenantId
in: query
required: true
schema: { type: string, format: uuid }
- name: type
in: query
schema: { type: string, enum: [table, chart, pivot, map, dashboard] }
- name: q
in: query
schema: { type: string }
responses:
200:
description: Lista med rapporter
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Report'
meta:
type: object
properties:
total: { type: integer }
page: { type: integer }
pageSize: { type: integer }
post:
summary: Skapa rapport
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ReportInput'
responses:
201:
description: Skapad
content:
application/json:
schema:
$ref: '#/components/schemas/Report'
/api/v1/reports/{id}/run:
post:
summary: Kör rapport
parameters:
- name: id
in: path
required: true
schema: { type: string, format: uuid }
requestBody:
content:
application/json:
schema:
type: object
properties:
parameters:
type: object
description: Körningsparametrar (ersätter default)
format:
type: string
enum: [json, csv, xlsx, pdf]
default: json
responses:
202:
description: Körs
content:
application/json:
schema:
$ref: '#/components/schemas/ReportRun'
/api/v1/reports/{id}/export:
post:
summary: Exportera rapport
parameters:
- name: id
in: path
required: true
schema: { type: string, format: uuid }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
format:
type: string
enum: [csv, xlsx, pdf, json]
default: csv
filters:
type: object
email:
type: string
description: Skicka via e-post istället för nedladdning
responses:
200:
description: Fil eller bekräftelse
202:
description: E-post kommer skickas
/api/v1/reports/{id}/schedule:
post:
summary: Skapa schema
parameters:
- name: id
in: path
required: true
schema: { type: string, format: uuid }
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ScheduleInput'
responses:
201:
description: Schema skapat
components:
schemas:
Report:
type: object
properties:
id: { type: string, format: uuid }
tenantId: { type: string, format: uuid }
name: { type: string }
description: { type: string }
type: { type: string, enum: [table, chart, pivot, map, dashboard] }
config: { type: object }
dataSourceId: { type: string, format: uuid }
createdBy: { type: string, format: uuid }
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
ReportInput:
type: object
required: [name, type, config, dataSourceId]
properties:
name: { type: string, minLength: 1, maxLength: 255 }
description: { type: string }
type: { type: string, enum: [table, chart, pivot, map, dashboard] }
config: { type: object }
dataSourceId: { type: string, format: uuid }
ReportRun:
type: object
properties:
id: { type: string, format: uuid }
reportId: { type: string, format: uuid }
status: { type: string, enum: [pending, running, completed, failed] }
startedAt: { type: string, format: date-time }
completedAt: { type: string, format: date-time }
resultUrl: { type: string }
rowCount: { type: integer }
errorMessage: { type: string }
ScheduleInput:
type: object
required: [cronExpression]
properties:
cronExpression: { type: string, example: "0 6 * * 1" }
timezone: { type: string, default: "Europe/Stockholm" }
status: { type: string, enum: [active, paused, disabled], default: active }
3. Tre Standardrapporter
3.1 Rapport A: "Fastighetsöversikt per Kommun"
Typ: Tabell + Karta
Beskrivning: Översikt över alla fastigheter inom kommunens gränser med nyckeltal.
{
"name": "Fastighetsöversikt per Kommun",
"type": "dashboard",
"description": "Sammanställning av fastigheter, areal och värden per kommun",
"dataSourceId": "ds-landvex-gis",
"config": {
"widgets": [
{
"type": "metric",
"title": "Totalt antal fastigheter",
"query": "SELECT COUNT(*) FROM properties WHERE municipality_id = :municipalityId"
},
{
"type": "metric",
"title": "Total areal (ha)",
"query": "SELECT SUM(area_hectares) FROM properties WHERE municipality_id = :municipalityId"
},
{
"type": "table",
"title": "Fastigheter per typ",
"query": "SELECT type, COUNT(*) as count, SUM(area_hectares) as total_area FROM properties WHERE municipality_id = :municipalityId GROUP BY type",
"columns": [
{ "field": "type", "header": "Fastighetstyp" },
{ "field": "count", "header": "Antal" },
{ "field": "total_area", "header": "Total areal (ha)" }
]
},
{
"type": "map",
"title": "Fastighetskarta",
"layer": "properties",
"filter": { "municipality_id": ":municipalityId" }
}
],
"parameters": [
{
"name": "municipalityId",
"type": "string",
"required": true,
"label": "Kommun",
"source": "tenant.config.municipality_id"
}
],
"defaultFormat": "pdf"
}
}
3.2 Rapport B: "Transaktionshistorik med Trender"
Typ: Diagram + Tabell
Beskrivning: Historik över fastighetstransaktioner med prisutveckling och trender.
{
"name": "Transaktionshistorik med Trender",
"type": "chart",
"description": "Transaktioner över tid med prisutveckling",
"dataSourceId": "ds-landvex-transactions",
"config": {
"charts": [
{
"type": "line",
"title": "Prisutveckling över tid",
"xAxis": { "field": "transaction_month", "label": "Månad" },
"yAxis": { "field": "avg_price_per_hectare", "label": "Kr/ha (genomsnitt)" },
"query": "SELECT DATE_TRUNC('month', transaction_date) as transaction_month, AVG(price_per_hectare) as avg_price_per_hectare FROM transactions WHERE municipality_id = :municipalityId AND transaction_date >= :fromDate GROUP BY 1 ORDER BY 1"
},
{
"type": "bar",
"title": "Antal transaktioner per månad",
"xAxis": { "field": "transaction_month" },
"yAxis": { "field": "transaction_count" },
"query": "SELECT DATE_TRUNC('month', transaction_date) as transaction_month, COUNT(*) as transaction_count FROM transactions WHERE municipality_id = :municipalityId AND transaction_date >= :fromDate GROUP BY 1 ORDER BY 1"
}
],
"detailTable": {
"title": "Senaste transaktioner",
"query": "SELECT t.id, t.property_id, p.address, t.transaction_date, t.price, t.area_hectares, t.price_per_hectare FROM transactions t JOIN properties p ON t.property_id = p.id WHERE t.municipality_id = :municipalityId ORDER BY t.transaction_date DESC LIMIT 100",
"columns": [
{ "field": "id", "header": "Transaktions-ID" },
{ "field": "address", "header": "Fastighet" },
{ "field": "transaction_date", "header": "Datum", "format": "date" },
{ "field": "price", "header": "Pris (kr)", "format": "currency" },
{ "field": "area_hectares", "header": "Areal (ha)", "format": "number" },
{ "field": "price_per_hectare", "header": "Kr/ha", "format": "currency" }
]
},
"parameters": [
{
"name": "municipalityId",
"type": "string",
"required": true,
"source": "tenant.config.municipality_id"
},
{
"name": "fromDate",
"type": "date",
"required": false,
"default": "-1 year",
"label": "Från datum"
}
],
"defaultFormat": "xlsx"
}
}
3.3 Rapport C: "Skogsbruksplan — Åtgärdsförslag"
Typ: Tabell + Pivot
Beskrivning: Åtgärdsförslag från skogsbruksplaner med tidsplan och volymuppskattning.
{
"name": "Skogsbruksplan — Åtgärdsförslag",
"type": "pivot",
"description": "Åtgärdsförslag från skogsbruksplaner sorterade på prioritet och tidsplan",
"dataSourceId": "ds-landvex-forestry",
"config": {
"pivot": {
"rows": ["action_type", "forest_stand_id"],
"columns": ["planned_year"],
"values": [
{ "field": "estimated_volume", "aggregation": "sum", "format": "number" },
{ "field": "estimated_cost", "aggregation": "sum", "format": "currency" }
]
},
"sourceQuery": "SELECT fa.action_type, fa.forest_stand_id, fa.planned_year, fa.estimated_volume_m3 as estimated_volume, fa.estimated_cost, fa.priority, fa.status FROM forestry_actions fa JOIN forest_stands fs ON fa.forest_stand_id = fs.id WHERE fs.municipality_id = :municipalityId AND fa.planned_year BETWEEN :fromYear AND :toYear",
"detailTable": {
"title": "Åtgärdslista",
"query": "SELECT fa.id, fa.action_type, fs.stand_name, fa.planned_year, fa.estimated_volume_m3, fa.estimated_cost, fa.priority, fa.status, fa.notes FROM forestry_actions fa JOIN forest_stands fs ON fa.forest_stand_id = fs.id WHERE fs.municipality_id = :municipalityId AND fa.planned_year BETWEEN :fromYear AND :toYear ORDER BY fa.priority, fa.planned_year",
"columns": [
{ "field": "action_type", "header": "Åtgärd" },
{ "field": "stand_name", "header": "Bestånd" },
{ "field": "planned_year", "header": "Planerat år" },
{ "field": "estimated_volume_m3", "header": "Volym (m³)", "format": "number" },
{ "field": "estimated_cost", "header": "Kostnad (kr)", "format": "currency" },
{ "field": "priority", "header": "Prioritet" },
{ "field": "status", "header": "Status" }
]
},
"parameters": [
{
"name": "municipalityId",
"type": "string",
"required": true,
"source": "tenant.config.municipality_id"
},
{
"name": "fromYear",
"type": "integer",
"required": false,
"default": "current_year",
"label": "Från år"
},
{
"name": "toYear",
"type": "integer",
"required": false,
"default": "current_year + 5",
"label": "Till år"
}
],
"defaultFormat": "xlsx"
}
}
4. Arkitektur & Integration
4.1 Komponentdiagram
┌─────────────────────────────────────────────────────────────┐
│ LandveX Enterprise Platform │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Web UI │ │ Mobile App │ │ API-klient │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └──────────────────┼──────────────────┘ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ Reporting API │ │
│ │ (REST + Auth/tenant) │ │
│ └───────────┬─────────────┘ │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Scheduler │ │ Query Eng. │ │ Export Svc │ │
│ │ (BullMQ) │ │ (PostgREST)│ │ (CSV/XLSX/ │ │
│ │ │ │ (Direct) │ │ PDF/JSON) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ LandveX Datakällor │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌────────┐│ │
│ │ │GIS/PostGIS│ │Transakt.│ │Skogsbr. │ │ Extern ││ │
│ │ │ │ │ DB │ │ DB │ │ API ││ │
│ │ └─────────┘ └─────────┘ └─────────┘ └────────┘│ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Delning & Notifikationer │ │
│ │ • E-post (SendGrid/AWS SES) │ │
│ │ • Länk-baserad delning (token) │ │
│ │ • Inbäddad rapport (iframe) │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
4.2 Scheduler-flöde (BullMQ)
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Cron-jobb │────►│ Query Engine │────►│ Store result│
│ (var 1 min) │ │ (timeout 5m) │ │ (S3/DB) │
└──────────────┘ └──────────────┘ └──────────────┘
│
▼
┌──────────────┐
│ Notifiera │
│ (e-post/ │
│ webhook) │
└──────────────┘
4.3 Exempel: Köra rapport via API
# 1. Skapa rapport
curl -X POST https://api.landvex.se/v1/reports \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @fastighetsöversikt.json
# 2. Kör rapport
curl -X POST https://api.landvex.se/v1/reports/$REPORT_ID/run \
-H "Authorization: Bearer $TOKEN" \
-d '{"format": "pdf", "parameters": {"municipalityId": "0180"}}'
# → { "id": "run-uuid", "status": "pending" }
# 3. Polling tills klar
curl https://api.landvex.se/v1/reports/$REPORT_ID/runs/$RUN_ID \
-H "Authorization: Bearer $TOKEN"
# → { "status": "completed", "resultUrl": "https://cdn.landvex.se/..." }
# 4. Ladda ner resultat
curl -O "$RESULT_URL"
5. Säkerhet & Rättigheter
| Nivå | Behörighet |
|---|---|
read |
Se rapport och resultat |
write |
Redigera config, köra manuellt |
admin |
Ändra schema, dela, ta bort |
- Tenant-isolering: Alla queries filtreras automatiskt på
tenant_id - Parametriserade queries: Inga strängkonkateneringar — använd prepared statements
- Resultat-URL:er: Tidsbegränsade signerade URL:er (S3 presigned, 1h default)
- Audit-logg: Alla körningar och exporter loggas med
user_idochtimestamp
6. Leverabler
| Fil | Innehåll |
|---|---|
DESIGN.md |
Denna fil — översikt och design |
schema.sql |
PostgreSQL DDL |
openapi.yaml |
Fullständig OpenAPI-spec |
examples/ |
3 standardrapporter (JSON) |
architecture.md |
Djupare arkitekturbeskrivning |