Skip to main content

Documentation

How the dashboard was built: timeline, architecture, decisions, findings, verification and resource use.

Last updated 23 September 2026.

Summary

The dashboard analyses a Matomo export for politiet.no (1 Jan 2023 – 24 Jun 2026). It runs locally, reads everything from a local SQLite database and sends nothing to external services. The interface is available in Norwegian (bokmål) and English.

Data
26,694 CSV files (UTF-16), 437 MB; daily totals for the whole period, detailed reports from 1 Dec 2025
Pages
Common questions, Overview, Content, Acquisition, Audience, Behaviour, Data health, Documentation
Exports
PDF, PowerPoint (editable charts) and Excel
Code base
≈ 5,100 lines of TypeScript in 67 files, 24 commits
Development time
About 2 hours 15 minutes in one session with Claude (Cowork)

Timeline

The work was done in four phases. Each phase was planned, approved by you and then built in small, checked steps with one commit per step.

Phases
PhaseContent
1 – FoundationNext.js project, import of the export into SQLite, query layer, five pages, verification script
2 – Data health and exportsData health page with controls, safe re-import, PDF/PowerPoint/Excel exports, local anomaly detection
3 – BehaviourBokmål/English, goal conversions, shared behaviour definitions, tracking changes, 13 common questions, new Behaviour page
4 – Accessibility and documentationWCAG 2.2 AA, mobile layout, this page
Commits
PhaseCommitDescription
1de447c7chore: scaffold Next.js 16 app
17c946bdfeat(data): import the Matomo CSV export into SQLite
11bd3880feat(data): add typed server-side query layer
111e3cb9feat(ui): dashboard shell and chart components
1df3d931feat(ui): overview, content, acquisition, audience and behaviour pages
191538d0feat(data): add data verification script
1c93be54docs: phase 1 README and plan with data findings
2bf0ee12refactor(data): share CSV helpers and make the import atomic
2f486a63feat(health): shared data-health checks
2bb34331feat(health): data health page with import controls
22285c69feat(insights): local anomaly detection and share changes
210d2838feat(export): PDF, PowerPoint and Excel exports
2bbe3dfbdocs: phase 2 notes, README and changelog
3309b70afeat(i18n): Norwegian (bokmål) and English interface
325f0c53fix(i18n): make the language switch visible on every page
39237c44fix(i18n): move the language toggle back to the sidebar
3fba0b34feat(behaviour): goal conversions, metric definitions and tracking-change detection
3f5d7ca4feat(questions): common questions page, batch 1
3f244801feat(behaviour): reorganise the Behaviour page into sections
313bbe15feat(questions): batch 2 questions and behaviour sections
36938abbfeat(export): common questions in exports and behaviour checks
49670e73feat(a11y): WCAG 2.2 AA and responsive pass
4a375387docs: documentation page (/docs) and consistent ?lang= links
4–docs: sustainability (cost, energy, CO₂, water) on the documentation page

Architecture

Data flow: Matomo export (CSV) → pnpm ingest → data/analytics.db (SQLite) → server-side queries → pages and exports. Raw data and the database stay local and are kept out of git.

Key files
LocationRole
scripts/ingest.tsReads the UTF-16 files and builds the database; swaps in the new file only when it is complete
scripts/verify.tsChecks the database against Matomo's totals and random raw files
src/lib/analytics.tsConnection, date ranges, KPIs, top lists, channels, anomalies
src/lib/behaviour/Behaviour metric definitions, queries, tracking changes and the common-question answers
src/lib/health.tsData-health checks (used by the page and by verify-data)
src/lib/report.ts, src/lib/export/One report model rendered as PDF, PowerPoint and Excel
src/lib/i18n/Dictionaries (bokmål is the source of truth, English must have the same keys) and formatting
src/components/Charts, tables, cards, filter row, navigation
  • Next.js 16 (App Router) with TypeScript and Tailwind CSS 4, pnpm as package manager.
  • Database: SQLite through Node's built-in node:sqlite — no database server and no native add-ons.
  • Charts: custom SVG components following the dataviz guidelines, always with a table view.
  • Exports: pdfkit, pptxgenjs and exceljs.

What was built

  • Import and verification: 786,000 rows in about 11 seconds on the Mac; 19 checks of totals, coverage, reconciliations and goals.
  • Overview: KPIs vs the previous period, trends, weekdays, channels, unusual periods (with Norwegian holidays as explanations) and biggest changes.
  • Common questions: 13 questions with a computed answer, key figure, chart and link to details.
  • Behaviour: online police report, form quality, services, search, navigation, consent, errors and speed, Politiloggen, devices and channels.
  • Data health: checks, monthly coverage, tracking changes, import statistics and re-import buttons (localhost only).
  • Exports: the same numbers as on screen, in the selected language and period.
  • Accessibility: contrast ≥ 4.5:1, visible focus, skip link, page titles, correct heading outline, table semantics, 24 px targets, mobile menu.

Decisions

Decisions and reasons
DecisionReasonAlternative
SQLite via node:sqliteThe npm registry was blocked in the build environment; no native add-ons to install; fast enough for 786,000 rowsDuckDB
Custom SVG chartsNo new dependencies, full control over accessibility and table viewsRecharts/ECharts
No AI/LLMYour decision; avoids sending data to external servicesClaude API via Bedrock (EU) or a local model
Search terms only shown at ≥ 20 visitsThe public can type personal information into searchShow all terms
“Direct” is derivedThe export has no direct visits; total minus other channelsLeave the channel out
Relative ranges end on the last day with dataThe export ends on 24 Jun 2026End on today (empty charts)
Re-import builds a separate file and swaps it inThe dashboard never sees a half-built databaseWrite straight into the database
Control buttons on localhost onlyThe buttons run scripts on the machineLogin (later, when shared)
Behaviour metrics defined in one placeRenamed events are fixed in one placeEvent names in every page
Goals inferred and verifiedGoal names were missing; goals 10 and 12 match events exactlyWait for names from Matomo
Hand-rolled number and date formattingServer and browser must render identical textIntl/toLocaleString (caused errors)
Bokmål by default, typed dictionariesA missing translation is a compile errorEnglish only
Commit history per stepTraceability; every step compiles on its ownOne big commit
WCAG 2.2 AARequired in the Norwegian public sector–

Findings in the data

  • 44% of visits that press “Log in and report” end with a submitted report; the biggest drop is before the report is created.
  • Crashes in the report form are rising: from about 20 to 25 per 100 submitted reports.
  • 75% of visits are on mobile, but desktop yields 3.4 times as many reports per 1,000 visits.
  • Passport appointment booking is the largest task (about 1.2 million clicks per 90 days).
  • 78% of those who make a choice accept cookies.
  • Politiloggen is about 25% of pageviews; the app hits request errors in about 96 of every 100 visits that fetch messages.
  • Tracking change on 26 Feb 2026 (Finn, passport booking click); the search keyword report stopped on 27 Apr 2026 (known).
  • Traffic spike 25 Jun – 22 Jul 2024 (+353%) and in Oct–Nov 2024, not explained by holidays.
  • Goal 10 = report submitted and goal 12 = click to passport booking; goals 1, 8 and 13 have no names yet.

Verification

  • Daily totals differ by 0.003% from Matomo's period totals; device visits add up exactly to daily visits on all 206 days.
  • Random raw files are re-read and compared on every check.
  • Every commit compiles (TypeScript) on its own; ESLint without errors.
  • All pages tested in both languages, for several ranges, in light and dark mode and at 375 px width.
  • Every export format opened and checked visually.
  • Own accessibility audit: contrast against the real background, heading order, labels, duplicate ids, table semantics and target size.

Resource use and sustainability

The numbers cover the whole development session up to 23 Sep 2026, 17:50 UTC, including this documentation. Updating this section comes on top.

Model calls
284
Tool calls
367
Duration
about 2 h 15 min
New input tokens
568
Read from cache
119.4 million
Written to cache
0.83 million
Output tokens
0.45 million
Effective use
≈ 15.9 million input-token equivalents

Effective use weights tokens by cost: cache reads ≈ 0.1×, cache writes ≈ 2× and output ≈ 5× a regular input token. The breakdown below is an estimate.

Effective use by area (million)
AreaMillion
Files and commands on your computer
8.73
Claude's instructions (re-read each time)
3.14
Replies and planning
1.66
Built-in browser (testing)
1.44
Cloud workspace (files and commands)
0.89
Web search
0.08

Money

Calculated with Anthropic's list prices for Claude Opus 5.5 (API): input USD 4, cache writes USD 8 (1 hour), cache reads USD 0.20 and output USD 20 per million tokens. Converted at 1 USD = 9.42 NOK (22 Sep 2026). If the dashboard work runs through a Claude subscription (Team/Enterprise), Politiet pays for seats rather than per token; the figures are then a reference for the resources the work used.

Cost at list price
ItemTokensPrice per millionCost
Read from cache119.4 millionUSD 0.20USD 23.87
Output0.45 millionUSD 20USD 8.99
Written to cache0.83 millionUSD 8USD 6.65
New input tokens568USD 4< USD 0.01
Total≈ USD 39.50 ≈ NOK 372
  • Per commit: ≈ NOK 16 (24 commits). Per hour of development: ≈ NOK 165.
  • Caching made the work about 12 times cheaper: without it the same tokens would have cost ≈ USD 490 (≈ NOK 4,600).
  • Although cache reads are cheap per token, they are 60% of the cost, because the whole conversation is re-read at every step.

Environment

Anthropic does not publish energy or water use per token, so these figures rely on independent research and are order-of-magnitude estimates. Every call in this session carried several hundred thousand tokens of context and is therefore counted as a “long” query: 3.9 Wh (range 0.31–7.1 Wh per call). Data-centre electricity is taken as 0.35–0.60 kg CO₂e per kWh and on-site water use as 0.18–1.2 litres per kWh.

Estimated footprint of the whole session
MeasureLowCentralHigh
Energy0.09 kWh1.1 kWh2.0 kWh
CO₂e (data-centre electricity)0.03 kg0.5 kg1.2 kg
Water (data-centre cooling)0.02 l0.8 l2.4 l

Roughly equivalent to (central estimate)

  • Energy: 40 minutes of an average Norwegian household's electricity (14,700 kWh a year), boiling 11 litres of water in a kettle, or about 75 full phone charges.
  • CO₂: driving 5 km in an average new car (96.7 g CO₂ per km in 2025); the range is 0.3–12 km.
  • Water: about three cups.
  • On Norwegian electricity (17 g CO₂e per kWh) the emissions would have been ≈ 19 grams — about 25 times lower.
  • The Mac running the dashboard and commands used roughly another 0.05–0.1 kWh on Norwegian power (1–2 g CO₂e).

What reduces the footprint

  • Caching (already used): the largest single saving, both in money and computation.
  • Less output from commands and files: large results stay in the conversation and are re-read at every step.
  • Shorter sessions, or a new session per phase, so the context does not grow large.
  • A smaller model for simple, mechanical steps.
  • The dashboard itself uses no AI and almost no resources in operation: it reads a local database.

Sources

Limitations and next steps

  • Detailed reports only exist from 1 Dec 2025; older data would have to be exported again from Matomo.
  • Large reports are capped at 500 rows per day (the rest is in “Others”).
  • Report-form steps are counted per day, not per visit all the way through.
  • Names for goals 1, 8 and 13: save Goals.getGoals as goals.json in the export folder and re-import.
  • The code is not published yet: git push needs GitHub login (SSH or gh) on the Mac.
  • Sharing with more users needs login and access control.

Running the dashboard

Commands
CommandWhat it does
pnpm installInstalls dependencies (Node 22.13 or newer)
pnpm ingestBuilds the database from the export folder (--verify checks checksums first)
pnpm verify-dataRuns the data checks in the terminal
pnpm devStarts the dashboard at http://localhost:3000
pnpm build && pnpm startProduction build