sundhed-dk
npx skills add https://github.com/mikkelkrogsholm/ai-laegens-bord --skill sundhed-dk
Agent 安装分布
Skill 文档
Sundhed.dk Health Data
This skill fetches personal health data from sundhed.dk by opening a browser, letting the user log in with MitID, then intercepting the JSON API responses and parsing them into readable text.
Quick Reference
| Section | What it contains |
|---|---|
| Medicin | Active medications, dosages, prescriptions |
| Prøvesvar | Blood tests, microbiology, pathology results |
| Journaler | Hospital records, episodes, diagnoses |
| Vaccinationer | Vaccination history |
| Aftaler | Upcoming appointments |
| Henvisninger | Referrals to specialists |
| Egen læge | GP practice info, doctors, hours |
| Røntgen | X-ray and scan descriptions |
| Diagnoser | Diagnoses from GP |
| Hjemmemålinger | Home measurements |
| Forløbsplaner | Care plans |
Before fetching: check for existing data
Before opening a browser, check if data already exists:
node -e "
const fs = require('fs');
const db = 'data/sundhed-dk/health.db';
const parsed = 'data/sundhed-dk/parsed';
console.log('Database exists:', fs.existsSync(db));
console.log('Parsed dir exists:', fs.existsSync(parsed));
if (fs.existsSync(parsed)) {
const files = fs.readdirSync(parsed).filter(f => f.endsWith('.md'));
console.log('Parsed files:', files.join(', '));
}
"
If data exists, skip straight to querying or reading:
- For broad questions â read the relevant
data/sundhed-dk/parsed/<section>.mdfile - For specific/time-based questions â query
data/sundhed-dk/health.db(see “Querying the database” below) - Only re-fetch from sundhed.dk if the user asks for a refresh or the data is stale
Querying the database
When data/sundhed-dk/health.db exists, use SQL to answer targeted questions. The database uses Node.js built-in SQLite (node:sqlite).
node -e "
const { DatabaseSync } = require('node:sqlite');
const db = new DatabaseSync('data/sundhed-dk/health.db');
const rows = db.prepare('<SQL_QUERY>').all();
rows.forEach(r => console.log(JSON.stringify(r)));
db.close();
"
Common query patterns
Trend a lab value over time:
SELECT result_date, value, reference_text, assessment
FROM lab_results_biochemistry
WHERE analyse_name LIKE '%Urat%'
ORDER BY result_date
Find all elevated lab results:
SELECT analyse_name, result_date, value, reference_text
FROM lab_results_biochemistry
WHERE assessment = 'Forhoejet'
ORDER BY result_date DESC
List active medications with duration:
SELECT drug_name, indication, start_date,
CAST((julianday('now') - julianday(start_date)) / 365.25 AS INT) AS years_on
FROM medications
WHERE status = 'Active'
ORDER BY start_date
Search microbiology results:
SELECT result_date, test_name, material, conclusion, finding_name, finding_interpretation
FROM lab_results_microbiology
WHERE test_name LIKE '%Chlamydia%'
ORDER BY result_date DESC
Upcoming appointments:
SELECT title, start_time, organisation, address, phone
FROM appointments
WHERE start_time > datetime('now')
ORDER BY start_time
Hospital history for a diagnosis:
SELECT diagnosis_name, diagnosis_code, hospital, department, date_from, date_to
FROM hospital_episodes
WHERE diagnosis_name LIKE '%eksem%'
ORDER BY date_from DESC
Database schema reference
| Table | Key columns |
|---|---|
patient |
name, cpr |
medications |
drug_name, active_substance, dosage, indication, start_date, end_date, status |
lab_requisitions |
id, sample_time, requester, lab_area |
lab_results_biochemistry |
requisition_id, analyse_name, value, unit, reference_lower, reference_upper, reference_text, assessment, result_date |
lab_results_microbiology |
requisition_id, test_name, material, conclusion, finding_name, finding_interpretation, finding_value, clinical_info, result_date |
hospital_episodes |
diagnosis_name, diagnosis_code, hospital, department, sector, date_from, date_to |
vaccinations |
vaccine, date, effectuated_by |
appointments |
title, start_time, end_time, organisation, address, phone |
referrals |
referral_date, expiry_date, referring_clinic, receiving_clinic, specialty, clinical_notes, is_active |
gp_practice |
name, address, phone, website |
gp_doctors |
name, role, specialty (FK: practice_id) |
xrays |
name, date, producer |
diagnoses |
diagnosis_code, diagnosis_name, organisation |
Note: Biochemistry analyse_name uses full IUPAC names (e.g. PâC-reaktivt protein; massek. = ? mg/L). Use LIKE '%keyword%' for searching (e.g. LIKE '%Urat%', LIKE '%Cholesterol%', LIKE '%Cobalamin%').
Fetching fresh data from sundhed.dk
Only follow the steps below if data doesn’t exist yet or the user wants to refresh it.
Step 1: Open browser and log in
The browser MUST be headed (so the user can interact with MitID) and persistent (to keep the session).
playwright-cli open https://sundhed.dk --browser=chrome --headed --persistent
Then navigate the login flow:
- Dismiss cookies – Take a snapshot, find “Nej tak” button, click it
- Click “Log pÃ¥” – Top-right corner
- Choose “Borger” – In the login dialog
- MitID authentication – Tell the user to complete MitID login in the browser. Wait for them to confirm.
- Verify login – After login, the browser should be at
https://www.sundhed.dk/borger/min-side/
playwright-cli snapshot
playwright-cli click <ref-for-nej-tak>
playwright-cli snapshot
playwright-cli click <ref-for-log-paa>
playwright-cli snapshot
playwright-cli click <ref-for-borger>
# Ask user to complete MitID login
Step 2: Fetch the requested data
Navigate to the relevant page and intercept the JSON API responses. The data gets saved to data/sundhed-dk/ via a browser download trick (since require('fs') is not available in the run-code context).
First ensure the data directory exists:
mkdir -p data/sundhed-dk
Use this pattern to intercept and save JSON for any section:
playwright-cli run-code "async page => {
const responses = [];
const handler = async response => {
const rUrl = response.url();
if (rUrl.includes('/api/') && rUrl.includes('<API_KEYWORD>') && response.headers()['content-type']?.includes('json')) {
try {
const body = await response.json();
responses.push({ url: rUrl, status: response.status(), body });
} catch(e) {}
}
};
page.on('response', handler);
await page.goto('<PAGE_URL>');
await page.waitForTimeout(6000);
page.removeListener('response', handler);
const json = JSON.stringify(responses, null, 2);
await page.evaluate((opts) => {
const blob = new Blob([opts.data], {type: 'application/json'});
const u = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = u; a.download = opts.fn;
document.body.appendChild(a); a.click();
document.body.removeChild(a); URL.revokeObjectURL(u);
}, { data: json, fn: '<FILENAME>.json' });
await page.waitForTimeout(1000);
return { count: responses.length };
}"
Then move the downloaded file to the data directory:
cp .playwright-cli/<FILENAME>.json data/sundhed-dk/<FILENAME>.json
Section-specific parameters
| Section | PAGE_URL | API_KEYWORD | FILENAME |
|---|---|---|---|
| Medicin | https://www.sundhed.dk/borger/min-side/min-sundhedsjournal/medicinkortet/ |
medicinkort2borger |
medicin |
| Prøvesvar | https://www.sundhed.dk/borger/min-side/min-sundhedsjournal/laboratoriesvar/ |
labsvar |
proevesvar |
| Journaler | https://www.sundhed.dk/borger/min-side/min-sundhedsjournal/journal-fra-sygehus/ |
ejournal |
journaler |
| Vaccinationer | https://www.sundhed.dk/borger/min-side/min-sundhedsjournal/vaccinationer/ |
vaccination |
vaccinationer |
| Aftaler | https://www.sundhed.dk/borger/min-side/min-sundhedsjournal/aftaler/ |
aftaler |
aftaler |
| Henvisninger | https://www.sundhed.dk/borger/min-side/min-sundhedsjournal/henvisninger/ |
envisning |
henvisninger |
| Egen læge | https://www.sundhed.dk/borger/min-side/min-sundhedsjournal/min-laege/ |
organisation |
egen-laege |
| Røntgen | https://www.sundhed.dk/borger/min-side/min-sundhedsjournal/billedbeskrivelser/ |
billedbeskrivelser |
roentgen |
| Diagnoser | https://www.sundhed.dk/borger/min-side/min-sundhedsjournal/diagnoser/ |
diagnoser |
diagnoser |
| Hjemmemålinger | https://www.sundhed.dk/borger/min-side/min-sundhedsjournal/hjemmemaalinger/ |
maalinger |
hjemmemaalinger |
| Forløbsplaner | https://www.sundhed.dk/borger/min-side/min-sundhedsjournal/planer/ |
planer |
forloebsplaner |
Step 3: Parse and save the data
Each section has a parser script that converts raw JSON into clean, LLM-friendly markdown. The parsers are bundled with this skill.
First ensure the parsed output directory exists:
mkdir -p data/sundhed-dk/parsed
Parse the JSON and save the markdown output:
node .claude/skills/sundhed-dk/parsers/parse-<section>.js < data/sundhed-dk/<section>.json > data/sundhed-dk/parsed/<section>.md
Available parsers:
| Parser | Input file | Output file |
|---|---|---|
parsers/parse-medicin.js |
data/sundhed-dk/medicin.json |
data/sundhed-dk/parsed/medicin.md |
parsers/parse-proevesvar.js |
data/sundhed-dk/proevesvar.json |
data/sundhed-dk/parsed/proevesvar.md |
parsers/parse-journaler.js |
data/sundhed-dk/journaler.json |
data/sundhed-dk/parsed/journaler.md |
parsers/parse-vaccinationer.js |
data/sundhed-dk/vaccinationer.json |
data/sundhed-dk/parsed/vaccinationer.md |
parsers/parse-aftaler.js |
data/sundhed-dk/aftaler.json |
data/sundhed-dk/parsed/aftaler.md |
parsers/parse-henvisninger.js |
data/sundhed-dk/henvisninger.json |
data/sundhed-dk/parsed/henvisninger.md |
parsers/parse-egen-laege.js |
data/sundhed-dk/egen-laege.json |
data/sundhed-dk/parsed/egen-laege.md |
parsers/parse-roentgen.js |
data/sundhed-dk/roentgen.json |
data/sundhed-dk/parsed/roentgen.md |
parsers/parse-diagnoser.js |
data/sundhed-dk/diagnoser.json |
data/sundhed-dk/parsed/diagnoser.md |
parsers/parse-hjemmemaalinger.js |
data/sundhed-dk/hjemmemaalinger.json |
data/sundhed-dk/parsed/hjemmemaalinger.md |
parsers/parse-forloebsplaner.js |
data/sundhed-dk/forloebsplaner.json |
data/sundhed-dk/parsed/forloebsplaner.md |
Read the parsed markdown from data/sundhed-dk/parsed/<section>.md and present it to the user in a clear, helpful way.
Step 4: Build the SQLite database
After fetching JSON data, build a queryable SQLite database from all available sections:
node .claude/skills/sundhed-dk/parsers/build-db.js
This creates data/sundhed-dk/health.db with tables for all health data. The database is rebuilt fresh each time (idempotent). Use this for targeted queries across time, e.g.:
node -e "
const { DatabaseSync } = require('node:sqlite');
const db = new DatabaseSync('data/sundhed-dk/health.db');
const rows = db.prepare(\"SELECT result_date, value, assessment FROM lab_results_biochemistry WHERE analyse_name LIKE '%Urat%' ORDER BY result_date\").all();
rows.forEach(r => console.log(r.result_date?.split('T')[0], r.value, r.assessment));
db.close();
"
Database tables
| Table | Contents |
|---|---|
patient |
Name, CPR |
medications |
Drug name, substance, dosage, indication, dates, status |
lab_requisitions |
Sample date, requester, lab area |
lab_results_biochemistry |
Analyse name, value, unit, reference range, assessment, date |
lab_results_microbiology |
Test name, material, findings, interpretation, clinical info |
hospital_episodes |
Diagnosis, hospital, department, dates |
vaccinations |
Vaccine, date, location |
appointments |
Title, time, organisation, address |
referrals |
Referring/receiving clinic, specialty, clinical notes |
gp_practice |
Practice name, address, phone, website |
gp_doctors |
Doctor name, role, specialty |
xrays |
Name, date, producer |
diagnoses |
Code, name, organisation |
When to use markdown vs SQLite:
- Markdown (
parsed/*.md): Dump into LLM context for broad questions (“summarize my health”, “what medications am I on”) - SQLite (
health.db): Targeted queries across time (“trend my CRP”, “which medications have I taken longest”, “show all elevated lab results”)
Important Notes
- Data storage: Raw JSON goes to
data/sundhed-dk/, parsed markdown todata/sundhed-dk/parsed/, SQLite database todata/sundhed-dk/health.db. All are gitignored – personal health data never gets committed. - Browser downloads land in
.playwright-cli/and must be copied todata/sundhed-dk/. - Prøvesvar defaults to 6 months back. To see older results, change the date filter on the page before intercepting.
- Journaler has full history from 1999 onwards.
- Person selector on most pages lets the user switch between self and family members (children under 15).
page.evaluate()only accepts a single argument – always wrap in{ data, fn }.- Element refs from snapshots are session-specific. Always take a fresh snapshot before clicking.
- Session persistence: Once logged in, the session stays active. No need to re-authenticate for subsequent navigations within the same browser session.
Detailed API Reference
For complete endpoint documentation, JSON structures, filter options, and sorting parameters, see navigation.md.