一键导入
aidbox-dashboard
Creating dashboards on top of Aidbox FHIR Server using ViewDefinitions
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Creating dashboards on top of Aidbox FHIR Server using ViewDefinitions
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | aidbox-dashboard |
| description | Creating dashboards on top of Aidbox FHIR Server using ViewDefinitions |
Aidbox runs locally on port 8080 via Docker Compose. All FHIR API requests use the /fhir/ prefix.
Dashboards are built using the SQL on FHIR approach:
bun run build:init-bundle --upload to rebuild the init bundle, upload it to Aidbox, and materialize all ViewDefinitions into SQL tables in the sof schemaBun.SQL to connect directly to PostgreSQL and queries sof.<view_name> for dashboard datasrc/index.ts and wire it into the appropriate route handlerViewDefinitions are stored as JSON files in fhir/definitions/view-definitions/. Use numeric prefixes for ordering (e.g., 01-body-weight.json).
Each file is a bundle entry with a PUT request:
{
"request": {
"method": "PUT",
"url": "/ViewDefinition/patient-demographics"
},
"resource": {
"resourceType": "ViewDefinition",
"id": "patient-demographics",
"name": "patient_demographics",
"status": "active",
"resource": "Patient",
"select": [
{
"column": [
{ "path": "getResourceKey()", "name": "id" },
{ "path": "gender", "name": "gender" },
{ "path": "birthDate", "name": "birth_date" }
]
},
{
"forEachOrNull": "name.where(use = 'official').first()",
"column": [
{ "path": "given.join(' ')", "name": "given_name" },
{ "path": "family", "name": "family_name" }
]
}
]
}
}
After creating or editing a ViewDefinition file, run:
bun run build:init-bundle --upload
This single command:
init-bundle.json from all files in fhir/definitions/$materialize on each ViewDefinition to create/refresh the corresponding SQL table in the sof schemaWithout --upload, it only rebuilds the JSON file locally.
The app uses Bun.SQL (configured in src/index.ts) to query the sof schema directly:
import { SQL } from "bun";
const db = new SQL({
url: process.env.DATABASE_URL ?? "postgresql://aidbox:<POSTGRES_PASSWORD>@localhost:5432/aidbox",
});
// Query a materialized view
const rows = await db.unsafe(
`SELECT effective_date, weight_kg, unit FROM sof.body_weight WHERE patient_id = $1 ORDER BY effective_date`,
[patientId],
);
Get the actual POSTGRES_PASSWORD from docker-compose.yaml (services.postgres.environment.POSTGRES_PASSWORD).
Charts are rendered using Chart.js v4 loaded via CDN (<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script> in the Layout <head>).
See BodyWeightChart in src/index.ts for the existing pattern:
<canvas> element + an inline <script> that calls new Chart()labels and data arrays into the Chart.js configExample chart function pattern:
function MyChart({ data }: { data: MyDataPoint[] }) {
if (data.length === 0) {
return `<div class="empty">No data found</div>`;
}
const chartId = `my-chart-${++chartIdCounter}`;
const labels = JSON.stringify(data.map((d) => d.date_column));
const values = JSON.stringify(data.map((d) => d.value_column));
return `<div class="card">
<canvas id="${chartId}"></canvas>
<script>
new Chart(document.getElementById('${chartId}'), {
type: 'line',
data: {
labels: ${labels},
datasets: [{
label: 'My Label',
data: ${values},
borderColor: '#2563eb',
backgroundColor: 'rgba(37, 99, 235, 0.1)',
fill: true,
tension: 0.3,
pointRadius: 5,
pointHoverRadius: 7,
pointBackgroundColor: '#2563eb',
pointBorderColor: '#fff',
pointBorderWidth: 2,
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: (ctx) => ctx.parsed.y + ' unit'
}
}
},
scales: {
x: { title: { display: true, text: 'Date' }, grid: { display: false } },
y: { title: { display: true, text: 'Value' }, grace: '5%' }
}
}
});
</script>
</div>`;
}
Chart.js supports many chart types: line, bar, pie, doughnut, radar, scatter, bubble. See Chart.js docs for the full API.
Use a unique chartId per chart instance (via the chartIdCounter) to support multiple charts on one page.
PostgreSQL is exposed on port 5432. Credentials are in docker-compose.yaml under services.postgres.environment:
| Parameter | Source in docker-compose.yaml |
|---|---|
| Host | localhost |
| Port | 5432 |
| Database | POSTGRES_DB |
| User | POSTGRES_USER |
| Password | POSTGRES_PASSWORD |
Connection string format: postgresql://<POSTGRES_USER>:<POSTGRES_PASSWORD>@localhost:5432/<POSTGRES_DB>
| Field | Required | Description |
|---|---|---|
resourceType | yes | "ViewDefinition" |
name | yes | Database table name (used as sof.<name>). Must match ^[A-Za-z][A-Za-z0-9_]*$ |
resource | yes | Target FHIR resource type (e.g., "Patient", "Observation") |
status | yes | "active", "draft", "retired", or "unknown" |
select | yes | Array of select blocks defining output columns |
where | no | Array of FHIRPath filter expressions |
constant | no | Named constants referenced as %name in FHIRPath |
| Field | Description |
|---|---|
column | Array of { path, name } — FHIRPath expression and output column name |
forEach | FHIRPath expression to iterate (creates multiple rows per resource) |
forEachOrNull | Like forEach but emits a row with nulls when the collection is empty |
unionAll | Combine multiple select structures |
select | Nested select (cross-join with parent) |
| Expression | Description |
|---|---|
getResourceKey() | Resource ID |
subject.getReferenceKey(Patient) | Referenced Patient ID (for joins) |
gender | Direct field access |
birthDate | Direct field access |
name.where(use = 'official').first() | Filter and pick first |
given.join(' ') | Join array into string |
effective.ofType(dateTime) | Polymorphic field access |
value.ofType(Quantity).value | Quantity value |
value.ofType(Quantity).unit | Quantity unit |
code.coding | Iterate over codings |
code.coding.where(system='http://loinc.org').first() | Pick specific coding |
code.coding.where(system = 'http://loinc.org' and code = '29463-7').exists() | Filter by coding system + code |
{
"resourceType": "ViewDefinition",
"id": "body-weight",
"name": "body_weight",
"status": "active",
"resource": "Observation",
"where": [
{
"path": "code.coding.where(system = 'http://loinc.org' and code = '29463-7').exists()"
}
],
"select": [
{
"column": [
{ "path": "getResourceKey()", "name": "id" },
{ "path": "subject.getReferenceKey(Patient)", "name": "patient_id" },
{ "path": "effective.ofType(dateTime)", "name": "effective_date" },
{ "path": "value.ofType(Quantity).value", "name": "weight_kg" },
{ "path": "value.ofType(Quantity).unit", "name": "unit" },
{ "path": "status", "name": "status" }
]
}
]
}
This creates sof.body_weight with columns: id, patient_id, effective_date, weight_kg, unit, status.
{
"resourceType": "ViewDefinition",
"id": "observation-values",
"name": "observation_values",
"status": "active",
"resource": "Observation",
"select": [
{
"column": [
{ "path": "getResourceKey()", "name": "id" },
{ "path": "subject.getReferenceKey(Patient)", "name": "patient_id" },
{ "path": "status", "name": "status" },
{ "path": "effective.ofType(dateTime)", "name": "effective_date" },
{ "path": "value.ofType(Quantity).value", "name": "value" },
{ "path": "value.ofType(Quantity).unit", "name": "unit" }
]
},
{
"forEachOrNull": "code.coding.first()",
"column": [
{ "path": "system", "name": "code_system" },
{ "path": "code", "name": "code" },
{ "path": "display", "name": "code_display" }
]
}
]
}
Two clients are available. Look up passwords in docker-compose.yaml and fhir/definitions/access-control/:
| Client | Username | Password source | Use for |
|---|---|---|---|
| Application | basic | fhir/definitions/access-control/01-client.json (resource.secret) | Normal CRUD + transactions |
| Root | root | docker-compose.yaml (BOX_ROOT_CLIENT_SECRET) | Admin operations, uploading init bundle |
The basic client is used by the running app (src/aidbox.ts) for normal FHIR CRUD operations. The root client is only used by build-init-bundle script to upload the init bundle and materialize ViewDefinitions — operations that require admin-level access.
# Read a specific resource (get BOX_ROOT_CLIENT_SECRET from docker-compose.yaml)
curl -s -u "root:<BOX_ROOT_CLIENT_SECRET>" "http://localhost:8080/fhir/Patient/<id>" | bun -e 'console.log(JSON.stringify(JSON.parse(await Bun.stdin.text()),null,2))'
# Search resources
curl -s -u "root:<BOX_ROOT_CLIENT_SECRET>" "http://localhost:8080/fhir/Patient?name=John&_count=10" | bun -e 'console.log(JSON.stringify(JSON.parse(await Bun.stdin.text()),null,2))'
Always use the /fhir/ prefix. Without it, you get the Aidbox-native format instead of FHIR.
FHIR definitions live in fhir/definitions/ as individual JSON files. Never edit init-bundle.json directly.
Files are sorted by filename, so use numeric prefixes to control order (e.g., 01-client.json loads before 02-access-policy.json).
# Rebuild, upload, and materialize ViewDefinitions in one step
bun run build:init-bundle --upload
# Rebuild only (no upload)
bun run build:init-bundle
The init bundle is also auto-loaded on Aidbox startup via BOX_INIT_BUNDLE in docker-compose.yaml. Note: ViewDefinitions loaded this way still need a $materialize call to create the SQL tables.
The app uses src/aidbox.ts which wraps the @health-samurai/aidbox-client SDK:
import { aidbox } from "./aidbox";
// Read
const result = await aidbox.read<Patient>({ type: "Patient", id: "pt-1" });
// Search
const result = await aidbox.searchType({ type: "Patient", query: [["name", "John"], ["_count", "10"]] });
// Transaction
await aidbox.transaction({ format: "application/fhir+json", bundle: { resourceType: "Bundle", type: "transaction", entry: [...] } });
For dashboard queries, use direct SQL via Bun.SQL against the sof schema instead of the FHIR API.
curl -s "http://localhost:8080/health" | bun -e 'console.log(JSON.stringify(JSON.parse(await Bun.stdin.text()),null,2))'
curl -s -u "root:<BOX_ROOT_CLIENT_SECRET>" "http://localhost:8080/fhir/<ResourceType>/<id>" | bun -e 'console.log(JSON.stringify(JSON.parse(await Bun.stdin.text()),null,2))'
curl -s -u "root:<BOX_ROOT_CLIENT_SECRET>" "http://localhost:8080/ViewDefinition?_count=50" | bun -e 'console.log(JSON.stringify(JSON.parse(await Bun.stdin.text()),null,2))'
# Via docker exec
docker compose exec postgres psql -U aidbox -d aidbox -c "SELECT * FROM sof.<view_name> LIMIT 5;"