fhirsmith 0.7.4 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.md +8 -0
- package/library/html.js +4 -0
- package/package.json +1 -1
- package/packages/package-crawler.js +104 -49
- package/packages/packages.js +14 -0
- package/publisher/publisher.js +124 -33
- package/registry/registry.js +97 -89
- package/root-bare-template.html +93 -0
- package/security.md +32 -0
- package/server.js +94 -47
- package/stats.js +6 -4
- package/translations/Messages.properties +2 -0
- package/tx/README.md +6 -6
- package/tx/cs/cs-api.js +3 -0
- package/tx/cs/cs-api.md +285 -0
- package/tx/cs/cs-country.js +804 -801
- package/tx/importers/readme.md +3 -1
- package/tx/library.js +33 -6
- package/tx/provider.js +2 -1
- package/tx/tx-html.js +36 -9
- package/tx/tx.fhir.org.yml +3 -0
- package/tx/tx.js +34 -11
- package/tx/vs/vs-database.js +42 -5
- package/tx/vs/vs-vsac.js +48 -0
- package/tx/workers/validate.js +11 -6
- package/tx/workers/worker.js +2 -5
- package/utilities/dashboard.html +274 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>FHIR Server Dashboard</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
|
|
10
|
+
body {
|
|
11
|
+
font-family: system-ui, sans-serif;
|
|
12
|
+
background: #111;
|
|
13
|
+
color: #e0e0e0;
|
|
14
|
+
height: 100vh;
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
header {
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
justify-content: space-between;
|
|
24
|
+
padding: 8px 16px;
|
|
25
|
+
background: #1a1a1a;
|
|
26
|
+
border-bottom: 1px solid #333;
|
|
27
|
+
flex-shrink: 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
header h1 {
|
|
31
|
+
font-size: 15px;
|
|
32
|
+
font-weight: 500;
|
|
33
|
+
color: #ccc;
|
|
34
|
+
letter-spacing: 0.03em;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#global-status {
|
|
38
|
+
font-size: 12px;
|
|
39
|
+
color: #666;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.grid {
|
|
43
|
+
flex: 1;
|
|
44
|
+
display: grid;
|
|
45
|
+
grid-template-columns: 1fr 1fr;
|
|
46
|
+
grid-template-rows: 1fr 1fr;
|
|
47
|
+
gap: 1px;
|
|
48
|
+
background: #222;
|
|
49
|
+
overflow: hidden;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.panel {
|
|
53
|
+
background: #0e0e0e;
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
position: relative;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.panel-header {
|
|
61
|
+
display: flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
gap: 8px;
|
|
64
|
+
padding: 7px 12px;
|
|
65
|
+
background: #181818;
|
|
66
|
+
border-bottom: 1px solid #2a2a2a;
|
|
67
|
+
flex-shrink: 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.panel-header .hostname {
|
|
71
|
+
font-size: 13px;
|
|
72
|
+
font-weight: 500;
|
|
73
|
+
color: #bbb;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.panel-header .status-dot {
|
|
77
|
+
width: 7px;
|
|
78
|
+
height: 7px;
|
|
79
|
+
border-radius: 50%;
|
|
80
|
+
background: #444;
|
|
81
|
+
flex-shrink: 0;
|
|
82
|
+
transition: background 0.4s;
|
|
83
|
+
}
|
|
84
|
+
.status-dot.ok { background: #3a9c5a; }
|
|
85
|
+
.status-dot.error { background: #b84040; }
|
|
86
|
+
.status-dot.loading { background: #888; animation: pulse 1s ease-in-out infinite; }
|
|
87
|
+
|
|
88
|
+
@keyframes pulse {
|
|
89
|
+
0%, 100% { opacity: 1; }
|
|
90
|
+
50% { opacity: 0.3; }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.panel-header .last-updated {
|
|
94
|
+
margin-left: auto;
|
|
95
|
+
font-size: 11px;
|
|
96
|
+
color: #555;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.countdown {
|
|
100
|
+
font-size: 11px;
|
|
101
|
+
color: #444;
|
|
102
|
+
margin-left: 6px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.panel-body {
|
|
106
|
+
flex: 1;
|
|
107
|
+
overflow: auto;
|
|
108
|
+
padding: 0;
|
|
109
|
+
position: relative;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.panel-body iframe {
|
|
113
|
+
width: 100%;
|
|
114
|
+
height: 100%;
|
|
115
|
+
border: none;
|
|
116
|
+
display: block;
|
|
117
|
+
background: #fff;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.panel-body .message {
|
|
121
|
+
position: absolute;
|
|
122
|
+
inset: 0;
|
|
123
|
+
display: flex;
|
|
124
|
+
flex-direction: column;
|
|
125
|
+
align-items: center;
|
|
126
|
+
justify-content: center;
|
|
127
|
+
gap: 8px;
|
|
128
|
+
color: #555;
|
|
129
|
+
font-size: 13px;
|
|
130
|
+
text-align: center;
|
|
131
|
+
padding: 16px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.message .error-detail {
|
|
135
|
+
font-size: 11px;
|
|
136
|
+
color: #3d3d3d;
|
|
137
|
+
max-width: 340px;
|
|
138
|
+
line-height: 1.5;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.refresh-btn {
|
|
142
|
+
font-size: 11px;
|
|
143
|
+
padding: 4px 10px;
|
|
144
|
+
background: transparent;
|
|
145
|
+
border: 1px solid #333;
|
|
146
|
+
border-radius: 4px;
|
|
147
|
+
color: #666;
|
|
148
|
+
cursor: pointer;
|
|
149
|
+
margin-top: 4px;
|
|
150
|
+
}
|
|
151
|
+
.refresh-btn:hover { border-color: #555; color: #999; }
|
|
152
|
+
</style>
|
|
153
|
+
</head>
|
|
154
|
+
<body>
|
|
155
|
+
|
|
156
|
+
<header>
|
|
157
|
+
<h1>FHIR Server Dashboard</h1>
|
|
158
|
+
<span id="global-status">Initialising...</span>
|
|
159
|
+
</header>
|
|
160
|
+
|
|
161
|
+
<div class="grid" id="grid"></div>
|
|
162
|
+
|
|
163
|
+
<script>
|
|
164
|
+
const SERVERS = [
|
|
165
|
+
{ id: 'npm', host: 'npm.fhir.org' },
|
|
166
|
+
{ id: 'tx', host: 'tx.fhir.org' },
|
|
167
|
+
{ id: 'www', host: 'www.fhir.org' },
|
|
168
|
+
{ id: 'health', host: 'www.healthintersections.com.au' },
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
const INTERVAL_MS = 60_000;
|
|
172
|
+
|
|
173
|
+
function dashboardUrl(host) {
|
|
174
|
+
return `https://${host}/dashboard`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function now() {
|
|
178
|
+
return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const panels = {};
|
|
182
|
+
|
|
183
|
+
function buildPanels() {
|
|
184
|
+
const grid = document.getElementById('grid');
|
|
185
|
+
for (const srv of SERVERS) {
|
|
186
|
+
const panel = document.createElement('div');
|
|
187
|
+
panel.className = 'panel';
|
|
188
|
+
panel.innerHTML = `
|
|
189
|
+
<div class="panel-header">
|
|
190
|
+
<div class="status-dot loading" id="dot-${srv.id}"></div>
|
|
191
|
+
<span class="hostname">${srv.host}</span>
|
|
192
|
+
<span class="last-updated" id="updated-${srv.id}">—</span>
|
|
193
|
+
<span class="countdown" id="countdown-${srv.id}"></span>
|
|
194
|
+
</div>
|
|
195
|
+
<div class="panel-body" id="body-${srv.id}">
|
|
196
|
+
<div class="message"><span>Loading...</span></div>
|
|
197
|
+
</div>`;
|
|
198
|
+
grid.appendChild(panel);
|
|
199
|
+
panels[srv.id] = srv;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function fetchDashboard(srv) {
|
|
204
|
+
const dot = document.getElementById(`dot-${srv.id}`);
|
|
205
|
+
const body = document.getElementById(`body-${srv.id}`);
|
|
206
|
+
const updatedEl = document.getElementById(`updated-${srv.id}`);
|
|
207
|
+
|
|
208
|
+
dot.className = 'status-dot loading';
|
|
209
|
+
|
|
210
|
+
const url = dashboardUrl(srv.host);
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const res = await fetch(url, { cache: 'no-store' });
|
|
214
|
+
|
|
215
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
216
|
+
|
|
217
|
+
const html = await res.text();
|
|
218
|
+
|
|
219
|
+
dot.className = 'status-dot ok';
|
|
220
|
+
updatedEl.textContent = now();
|
|
221
|
+
|
|
222
|
+
const iframe = document.createElement('iframe');
|
|
223
|
+
iframe.sandbox = 'allow-same-origin allow-scripts';
|
|
224
|
+
body.innerHTML = '';
|
|
225
|
+
body.appendChild(iframe);
|
|
226
|
+
|
|
227
|
+
const doc = iframe.contentDocument || iframe.contentWindow.document;
|
|
228
|
+
doc.open();
|
|
229
|
+
doc.write(html);
|
|
230
|
+
doc.close();
|
|
231
|
+
|
|
232
|
+
} catch (err) {
|
|
233
|
+
dot.className = 'status-dot error';
|
|
234
|
+
updatedEl.textContent = now();
|
|
235
|
+
body.innerHTML = `
|
|
236
|
+
<div class="message">
|
|
237
|
+
<span>Could not load dashboard</span>
|
|
238
|
+
<span class="error-detail">${url}<br>${err.message}</span>
|
|
239
|
+
<button class="refresh-btn" onclick="fetchDashboard(panels['${srv.id}'])">Retry now</button>
|
|
240
|
+
</div>`;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const nextRefresh = {};
|
|
245
|
+
|
|
246
|
+
function scheduleRefresh(srv) {
|
|
247
|
+
nextRefresh[srv.id] = Date.now() + INTERVAL_MS;
|
|
248
|
+
setTimeout(() => {
|
|
249
|
+
fetchDashboard(srv).then(() => scheduleRefresh(srv));
|
|
250
|
+
}, INTERVAL_MS);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function updateCountdowns() {
|
|
254
|
+
for (const srv of SERVERS) {
|
|
255
|
+
const el = document.getElementById(`countdown-${srv.id}`);
|
|
256
|
+
if (!el || !nextRefresh[srv.id]) continue;
|
|
257
|
+
const secs = Math.max(0, Math.round((nextRefresh[srv.id] - Date.now()) / 1000));
|
|
258
|
+
el.textContent = secs > 0 ? `↻ ${secs}s` : '';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const gs = document.getElementById('global-status');
|
|
262
|
+
gs.textContent = `Auto-refresh every ${INTERVAL_MS / 1000}s`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
buildPanels();
|
|
266
|
+
|
|
267
|
+
for (const srv of SERVERS) {
|
|
268
|
+
fetchDashboard(srv).then(() => scheduleRefresh(srv));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
setInterval(updateCountdowns, 1000);
|
|
272
|
+
</script>
|
|
273
|
+
</body>
|
|
274
|
+
</html>
|