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.
| Phase | Content |
|---|---|
| 1 – Foundation | Next.js project, import of the export into SQLite, query layer, five pages, verification script |
| 2 – Data health and exports | Data health page with controls, safe re-import, PDF/PowerPoint/Excel exports, local anomaly detection |
| 3 – Behaviour | Bokmål/English, goal conversions, shared behaviour definitions, tracking changes, 13 common questions, new Behaviour page |
| 4 – Accessibility and documentation | WCAG 2.2 AA, mobile layout, this page |
| Phase | Commit | Description |
|---|---|---|
| 1 | de447c7 | chore: scaffold Next.js 16 app |
| 1 | 7c946bd | feat(data): import the Matomo CSV export into SQLite |
| 1 | 1bd3880 | feat(data): add typed server-side query layer |
| 1 | 11e3cb9 | feat(ui): dashboard shell and chart components |
| 1 | df3d931 | feat(ui): overview, content, acquisition, audience and behaviour pages |
| 1 | 91538d0 | feat(data): add data verification script |
| 1 | c93be54 | docs: phase 1 README and plan with data findings |
| 2 | bf0ee12 | refactor(data): share CSV helpers and make the import atomic |
| 2 | f486a63 | feat(health): shared data-health checks |
| 2 | bb34331 | feat(health): data health page with import controls |
| 2 | 2285c69 | feat(insights): local anomaly detection and share changes |
| 2 | 10d2838 | feat(export): PDF, PowerPoint and Excel exports |
| 2 | bbe3dfb | docs: phase 2 notes, README and changelog |
| 3 | 309b70a | feat(i18n): Norwegian (bokmål) and English interface |
| 3 | 25f0c53 | fix(i18n): make the language switch visible on every page |
| 3 | 9237c44 | fix(i18n): move the language toggle back to the sidebar |
| 3 | fba0b34 | feat(behaviour): goal conversions, metric definitions and tracking-change detection |
| 3 | f5d7ca4 | feat(questions): common questions page, batch 1 |
| 3 | f244801 | feat(behaviour): reorganise the Behaviour page into sections |
| 3 | 13bbe15 | feat(questions): batch 2 questions and behaviour sections |
| 3 | 6938abb | feat(export): common questions in exports and behaviour checks |
| 4 | 9670e73 | feat(a11y): WCAG 2.2 AA and responsive pass |
| 4 | a375387 | docs: 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.
| Location | Role |
|---|---|
| scripts/ingest.ts | Reads the UTF-16 files and builds the database; swaps in the new file only when it is complete |
| scripts/verify.ts | Checks the database against Matomo's totals and random raw files |
| src/lib/analytics.ts | Connection, date ranges, KPIs, top lists, channels, anomalies |
| src/lib/behaviour/ | Behaviour metric definitions, queries, tracking changes and the common-question answers |
| src/lib/health.ts | Data-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
| Decision | Reason | Alternative |
|---|---|---|
| SQLite via node:sqlite | The npm registry was blocked in the build environment; no native add-ons to install; fast enough for 786,000 rows | DuckDB |
| Custom SVG charts | No new dependencies, full control over accessibility and table views | Recharts/ECharts |
| No AI/LLM | Your decision; avoids sending data to external services | Claude API via Bedrock (EU) or a local model |
| Search terms only shown at ≥ 20 visits | The public can type personal information into search | Show all terms |
| “Direct” is derived | The export has no direct visits; total minus other channels | Leave the channel out |
| Relative ranges end on the last day with data | The export ends on 24 Jun 2026 | End on today (empty charts) |
| Re-import builds a separate file and swaps it in | The dashboard never sees a half-built database | Write straight into the database |
| Control buttons on localhost only | The buttons run scripts on the machine | Login (later, when shared) |
| Behaviour metrics defined in one place | Renamed events are fixed in one place | Event names in every page |
| Goals inferred and verified | Goal names were missing; goals 10 and 12 match events exactly | Wait for names from Matomo |
| Hand-rolled number and date formatting | Server and browser must render identical text | Intl/toLocaleString (caused errors) |
| Bokmål by default, typed dictionaries | A missing translation is a compile error | English only |
| Commit history per step | Traceability; every step compiles on its own | One big commit |
| WCAG 2.2 AA | Required 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.
| Area | Million |
|---|---|
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.
| Item | Tokens | Price per million | Cost |
|---|---|---|---|
| Read from cache | 119.4 million | USD 0.20 | USD 23.87 |
| Output | 0.45 million | USD 20 | USD 8.99 |
| Written to cache | 0.83 million | USD 8 | USD 6.65 |
| New input tokens | 568 | USD 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.
| Measure | Low | Central | High |
|---|---|---|---|
| Energy | 0.09 kWh | 1.1 kWh | 2.0 kWh |
| CO₂e (data-centre electricity) | 0.03 kg | 0.5 kg | 1.2 kg |
| Water (data-centre cooling) | 0.02 l | 0.8 l | 2.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
- Anthropic – pricing for Claude models(platform.claude.com)
- Joule (2026): Energy use of AI inference, efficiency pathways, and test-time scaling(www.sciencedirect.com)
- How Hungry is AI? Energy, water and carbon of LLM inference (arXiv 2025)(arxiv.org)
- Statistics Norway – average household electricity use(www.ssb.no)
- EEA – CO₂ emissions from new cars 2025(www.eea.europa.eu)
- Nowtricity – CO₂ per kWh in Norway(www.nowtricity.com)
- USD/NOK exchange rate(www.valuta-kurser.no)
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
| Command | What it does |
|---|---|
| pnpm install | Installs dependencies (Node 22.13 or newer) |
| pnpm ingest | Builds the database from the export folder (--verify checks checksums first) |
| pnpm verify-data | Runs the data checks in the terminal |
| pnpm dev | Starts the dashboard at http://localhost:3000 |
| pnpm build && pnpm start | Production build |