بنقرة واحدة
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;"