fhirsmith 0.18.1 → 1.0.0

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.
Files changed (214) hide show
  1. package/ACCESSIBILITY.md +119 -0
  2. package/CHANGELOG.md +190 -9
  3. package/CONFIGURATION.md +305 -0
  4. package/LICENSE +29 -0
  5. package/README.md +29 -122
  6. package/cluster.js +1220 -0
  7. package/cluster.md +707 -0
  8. package/config-template.json +77 -8
  9. package/extension-tracker/extension-tracker-template.html +5 -8
  10. package/extension-tracker/extension-tracker.js +0 -2
  11. package/folder/folder.js +7 -6
  12. package/ips/analysis.js +560 -0
  13. package/ips/ips-template.html +5 -8
  14. package/ips/ips.js +331 -33
  15. package/ips/jurisdictions.js +65 -0
  16. package/ips/nationality.js +120 -0
  17. package/ips/pipeline.js +131 -17
  18. package/ips/presenter.js +1 -1
  19. package/ips/profile-editor.js +283 -4
  20. package/ips/readme.md +170 -20
  21. package/ips/render.js +769 -109
  22. package/ips/scripts.js +46 -0
  23. package/ips/store.js +123 -11
  24. package/ips/structure.js +247 -0
  25. package/ips/terminology.js +1 -1
  26. package/library/body-limit.js +22 -0
  27. package/library/cache-generation.js +15 -0
  28. package/library/cgroup-memory.js +133 -1
  29. package/library/cluster-counts.js +121 -0
  30. package/library/cluster-events.js +153 -0
  31. package/library/cluster-metrics.js +193 -0
  32. package/library/cluster-status.js +468 -0
  33. package/library/config-reader.js +96 -0
  34. package/library/daily-report.js +100 -2
  35. package/library/html-server.js +27 -1
  36. package/library/i18nsupport.js +23 -1
  37. package/library/keep-alive.js +18 -0
  38. package/library/languages.js +17 -3
  39. package/library/logger.js +31 -0
  40. package/library/memory-guard.js +428 -0
  41. package/library/native-memory.js +52 -0
  42. package/library/npm-audit.js +51 -25
  43. package/library/openapi-doc.js +7 -5
  44. package/library/package-manager.js +8 -0
  45. package/library/role.js +197 -0
  46. package/library/server-credentials.js +169 -0
  47. package/library/sqlite-utilities.js +59 -0
  48. package/library/ssrf-guard.js +100 -0
  49. package/library/static-response-stats.js +16 -0
  50. package/library/version-utilities.js +50 -0
  51. package/library/worker-count.js +192 -0
  52. package/nginx.md +30 -11
  53. package/npmprojector/indexer.js +18 -10
  54. package/package.json +11 -8
  55. package/packages/feed-check-page.js +117 -0
  56. package/packages/feed-check.js +279 -0
  57. package/packages/npm-check.js +57 -3
  58. package/packages/npm-signing.js +219 -0
  59. package/packages/openapi.yaml +207 -1
  60. package/packages/package-crawler.js +322 -41
  61. package/packages/packages-template.html +6 -8
  62. package/packages/packages.js +604 -57
  63. package/packages/readme.md +113 -1
  64. package/publisher/publisher-template.html +2 -5
  65. package/publisher/publisher.js +121 -110
  66. package/publisher/readme.md +75 -0
  67. package/registry/api.js +179 -1
  68. package/registry/crawler.js +86 -108
  69. package/registry/documentation/api.md +68 -2
  70. package/registry/documentation/configuration.md +34 -0
  71. package/registry/documentation/internals.md +4 -1
  72. package/registry/documentation/readme.md +3 -2
  73. package/registry/model.js +8 -0
  74. package/registry/openapi.yaml +84 -0
  75. package/registry/registry-template.html +5 -8
  76. package/registry/registry.js +58 -6
  77. package/root-bare-template.html +7 -4
  78. package/root-template.html +5 -8
  79. package/server.js +398 -198
  80. package/shl/shl.js +38 -37
  81. package/static/a11y/a11y-check.js +832 -0
  82. package/static/a11y/axe.min.js +12 -0
  83. package/static/assets/css/bootstrap-fhir.css +15 -15
  84. package/static/assets/images/flags/au.svg +18 -0
  85. package/static/assets/images/flags/unknown.svg +11 -0
  86. package/static/assets/images/flags/us.svg +63 -0
  87. package/static/assets/images/icon_audit.png +0 -0
  88. package/static/assets/images/icon_clinical.png +0 -0
  89. package/static/assets/images/icon_document.png +0 -0
  90. package/static/assets/images/icon_external.png +0 -0
  91. package/static/assets/images/icon_medication.png +0 -0
  92. package/static/assets/images/icon_organization.png +0 -0
  93. package/static/assets/images/icon_other.png +0 -0
  94. package/static/assets/images/icon_person.png +0 -0
  95. package/static/assets/images/icon_section.png +0 -0
  96. package/stats-db.js +45 -0
  97. package/stats.js +197 -10
  98. package/testing/testing-template.html +5 -8
  99. package/token/nginx_passport_setup.md +1 -1
  100. package/token/security_guide.md +1 -1
  101. package/token/token-template.html +2 -5
  102. package/token/token.js +40 -36
  103. package/translations/Messages.properties +19 -0
  104. package/tx/README.md +158 -4
  105. package/tx/closure/closure-store.js +126 -3
  106. package/tx/cm/cm-database.js +69 -57
  107. package/tx/cm/cm-package.js +50 -16
  108. package/tx/cs/cs-api.js +284 -19
  109. package/tx/cs/cs-api.md +41 -4
  110. package/tx/cs/cs-areacode.js +1 -1
  111. package/tx/cs/cs-country.js +17 -5
  112. package/tx/cs/cs-cpt.js +33 -16
  113. package/tx/cs/cs-cs.js +199 -24
  114. package/tx/cs/cs-currency.js +1 -1
  115. package/tx/cs/cs-icd11.js +21 -13
  116. package/tx/cs/cs-icf.js +22 -3
  117. package/tx/cs/cs-lang.js +2 -2
  118. package/tx/cs/cs-loinc.js +193 -67
  119. package/tx/cs/cs-mimetypes.js +12 -2
  120. package/tx/cs/cs-ndc.js +40 -25
  121. package/tx/cs/cs-omop.js +54 -29
  122. package/tx/cs/cs-rxnorm.js +300 -196
  123. package/tx/cs/cs-snomed.js +861 -95
  124. package/tx/cs/cs-ucum.js +14 -4
  125. package/tx/cs/cs-unii.js +18 -14
  126. package/tx/cs/cs-virtual.js +210 -0
  127. package/tx/ecosystem-resolver.js +413 -0
  128. package/tx/html/cluster-dash-metrics.liquid +26 -0
  129. package/tx/html/cluster-home-metrics.liquid +31 -0
  130. package/tx/html/cluster-metrics-script.liquid +251 -0
  131. package/tx/html/codesystem-operations.liquid +3 -3
  132. package/tx/html/ecl-panel.liquid +1 -1
  133. package/tx/html/external-metrics.liquid +17 -0
  134. package/tx/html/operations-form.liquid +17 -17
  135. package/tx/html/search-form.liquid +11 -11
  136. package/tx/html/status-line.liquid +58 -0
  137. package/tx/html/status-panel.liquid +140 -0
  138. package/tx/html/status-tasks.liquid +25 -0
  139. package/tx/html/tx-template.html +2 -5
  140. package/tx/importers/atc-to-fhir.js +17 -16
  141. package/tx/importers/export-icd11-codesystem.js +3 -2
  142. package/tx/importers/import-icd11.js +4 -3
  143. package/tx/importers/import-loinc.module.js +118 -56
  144. package/tx/importers/import-ndc.module.js +56 -44
  145. package/tx/importers/import-rxnorm.module.js +118 -79
  146. package/tx/importers/import-sct.module.js +125 -61
  147. package/tx/importers/import-unii.module.js +37 -36
  148. package/tx/importers/subset-loinc.module.js +49 -48
  149. package/tx/importers/subset-rxnorm.module.js +29 -28
  150. package/tx/importers/terminal.js +18 -0
  151. package/tx/importers/tx-import-base.js +13 -12
  152. package/tx/importers/tx-import-settings.js +5 -4
  153. package/tx/importers/tx-import.js +53 -52
  154. package/tx/library/canonical-resource.js +49 -0
  155. package/tx/library/codesystem.js +94 -5
  156. package/tx/library/designations.js +45 -2
  157. package/tx/library/extensions.js +13 -7
  158. package/tx/library/implicit-valuesets.js +66 -0
  159. package/tx/library/renderer.js +77 -21
  160. package/tx/library/sqlite-utilities.js +2 -0
  161. package/tx/library/tho-precedence.js +40 -0
  162. package/tx/library/ucum-parsers.js +11 -1
  163. package/tx/library/wait-for-cache.js +43 -0
  164. package/tx/library.js +321 -22
  165. package/tx/openapi-operations.js +6 -4
  166. package/tx/operation-context.js +460 -99
  167. package/tx/params.js +25 -0
  168. package/tx/problems.js +3 -5
  169. package/tx/provider.js +314 -67
  170. package/tx/resource-cache-sqlite.js +1050 -0
  171. package/tx/sct/documentation.md +45 -21
  172. package/tx/sct/ecl.js +81 -17
  173. package/tx/sct/editions.js +39 -0
  174. package/tx/sct/expressions.js +10 -1
  175. package/tx/sct/load-config.js +107 -0
  176. package/tx/sct/mmap.js +109 -0
  177. package/tx/sct/structures.js +175 -15
  178. package/tx/sct/text-index.js +5 -1
  179. package/tx/syndication/syndication-feed.js +2 -2
  180. package/tx/tx-html.js +44 -8
  181. package/tx/tx.fhir.org.yml +8 -8
  182. package/tx/tx.js +282 -80
  183. package/tx/usage-tracker.js +39 -11
  184. package/tx/vs/vs-api.js +19 -0
  185. package/tx/vs/vs-database.js +386 -62
  186. package/tx/vs/vs-package.js +44 -17
  187. package/tx/vs/vs-vsac.js +649 -101
  188. package/tx/workers/batch-validate.js +37 -10
  189. package/tx/workers/batch.js +6 -1
  190. package/tx/workers/cache-control.js +15 -4
  191. package/tx/workers/closure.js +156 -13
  192. package/tx/workers/compare.js +8 -5
  193. package/tx/workers/expand.js +1173 -289
  194. package/tx/workers/metadata.js +35 -0
  195. package/tx/workers/search.js +53 -1
  196. package/tx/workers/supplement-filters.js +145 -0
  197. package/tx/workers/translate.js +169 -4
  198. package/tx/workers/validate.js +223 -119
  199. package/tx/workers/worker.js +123 -31
  200. package/tx/xversion/xv-valueset.js +7 -4
  201. package/utilities/submit-test-report.js +3 -1
  202. package/utilities/tx-stress-test.js +482 -0
  203. package/vcl/readme.md +37 -0
  204. package/vcl/vcl-parser.js +15 -0
  205. package/xig/readme.md +52 -0
  206. package/xig/xig-template.html +5 -8
  207. package/xig/xig.js +238 -422
  208. package/changelog-template.md +0 -22
  209. package/registry/test-runner.js +0 -237
  210. package/release.sh +0 -80
  211. package/test-scripts/repro-re2-wasm-leak.js +0 -34
  212. package/tsconfig.eslint.json +0 -9
  213. package/tx/html/dash-metrics.liquid +0 -147
  214. package/tx/html/home-metrics.liquid +0 -225
@@ -0,0 +1,251 @@
1
+ <style>
2
+ /* the status panel's grid (tx/html/status-panel.liquid): its six columns, two a chart */
3
+ .server-metrics-grid { display:grid; grid-template-columns:repeat(3, minmax(0, 1fr)); gap:16px; margin-bottom:16px; }
4
+ .server-metrics-chart { min-width:0; }
5
+ .server-metrics-chart > :first-child { font-size:12px; font-weight:600; margin:0 0 4px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
6
+ .server-metrics-canvas { position:relative; height:200px; }
7
+ @media (max-width:600px) { .server-metrics-grid { grid-template-columns:minmax(0, 1fr); } }
8
+ .server-metrics-legend { display:flex; flex-wrap:wrap; gap:3px 14px; font:11px sans-serif; margin:10px 0 8px; }
9
+ .server-metrics-key { display:inline-block; width:22px; text-align:center; margin-right:4px; }
10
+ .server-metrics-shedding { font:11px sans-serif; margin:0 0 8px; }
11
+ .server-metrics-shedding::before { content:''; display:inline-block; width:14px; height:10px; margin-right:5px; vertical-align:middle;
12
+ border-top:2px solid #333; background:repeating-linear-gradient(135deg, #555 0 1px, transparent 1px 4px); }
13
+ </style>
14
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.js"></script>
15
+ <script>
16
+ (function() {
17
+ const metrics = {{ metricsJson }};
18
+ // Colour (by lightness, not red against green), line pattern and marker each tell the
19
+ // processes apart. Past eight processes only the first eight are drawn; none share a style.
20
+ const styles = [
21
+ { color:'#08306b', dash:[], point:'circle', symbol:'●' },
22
+ { color:'#5b9bd5', dash:[6,3], point:'rect', symbol:'■' },
23
+ { color:'#d98c00', dash:[2,2], point:'triangle', symbol:'▲' },
24
+ { color:'#6a3d9a', dash:[8,3,2,3], point:'rectRot', symbol:'◆' },
25
+ { color:'#9e9e9e', dash:[12,4], point:'star', symbol:'★' },
26
+ { color:'#8a5a00', dash:[3,5], point:'crossRot', symbol:'✕' },
27
+ { color:'#b4a7d6', dash:[8,2,2,2,2,2], point:'cross', symbol:'✚' },
28
+ { color:'#404040', dash:[4,2], point:'rectRounded', symbol:'▢' }
29
+ ];
30
+ const total = { color:'#111111', dash:[], symbol:'', width:3 };
31
+ // the memory guard's refusal threshold: mid grey, long dash and dot, no markers, and labelled
32
+ const threshold = { color:'#666666', dash:[10,3,2,3], symbol:'', width:2 };
33
+ const drawn = metrics.series.slice(0, styles.length);
34
+ const showTotal = metrics.series.length > 1;
35
+ // private memory (RssAnon) where the processes could read it, else (not Linux) heap change
36
+ const hasAnon = metrics.series.some(series => series.history.some(point => Number.isFinite(point.anon)));
37
+ const memoryTotal = hasAnon && metrics.total && metrics.total.history.some(point => Number.isFinite(point.anon)) ? metrics.total : null;
38
+ const rejectAt = hasAnon && metrics.total && Number.isFinite(metrics.total.rejectAt) && metrics.total.rejectAt > 0 ? metrics.total.rejectAt : null;
39
+ const memoryTitle = document.getElementById('memoryChartTitle');
40
+ if (memoryTitle && !hasAnon) memoryTitle.textContent = 'Heap change since start';
41
+
42
+ // the key's items: put in #serverMetricsLegend, which can be further down the page than this
43
+ // script (under "More" on / and /dashboard), so once the page has loaded
44
+ const legendItems = [];
45
+ function legendItem(style, text) {
46
+ const item = document.createElement('span');
47
+ const key = document.createElement('span');
48
+ key.className = 'server-metrics-key';
49
+ key.style.color = style.color;
50
+ key.style.borderBottom = (style.width || 2) + 'px ' + (style.dash.length ? 'dashed' : 'solid') + ' ' + style.color;
51
+ key.textContent = style.symbol;
52
+ item.appendChild(key);
53
+ item.appendChild(document.createTextNode(text));
54
+ legendItems.push(item);
55
+ }
56
+ drawn.forEach((series, i) => legendItem(styles[i], series.label + (series.status === 'ready' ? '' : ' · ' + series.status) +
57
+ (series.restarts ? ' · ' + series.restarts + (series.restarts === 1 ? ' restart' : ' restarts') : '')));
58
+ if (showTotal || memoryTotal) {
59
+ legendItem(total, 'Total: ' + [showTotal ? 'requests per minute' : null,
60
+ showTotal ? (metrics.sharedCache ? 'client concepts (one shared store, counted once)' : 'client concepts') : null,
61
+ memoryTotal ? (memoryTotal.group ? 'memory (the cgroup\'s private)' : 'memory (the processes\' private, summed)') : null]
62
+ .filter(Boolean).join(', '));
63
+ }
64
+ if (rejectAt) legendItem(threshold, 'Memory guard refuses above ' + (rejectAt / 2 ** 30).toFixed(1) + ' GB' +
65
+ (metrics.total.local ? (metrics.total.group ? ' (the cgroup, every process)' : ' (this process)')
66
+ : metrics.total.group ? ' (the cgroup)' : ' (each process)'));
67
+ if (metrics.series.length > drawn.length) {
68
+ legendItems.push(document.createTextNode((metrics.series.length - drawn.length) +
69
+ ' more workers are not drawn individually; the Total includes them.'));
70
+ }
71
+ function placeLegend() {
72
+ const legend = document.getElementById('serverMetricsLegend');
73
+ if (legend) legendItems.forEach(item => legend.appendChild(item));
74
+ }
75
+ // (while the page is loading, at the end, with the external requests' chart)
76
+ if (document.readyState !== 'loading') placeLegend();
77
+
78
+ const times = metrics.series.flatMap(series => series.history.map(point => point.time));
79
+ const latest = times.reduce((latest, time) => Math.max(latest, time), metrics.sampledAt);
80
+ const earliest = times.length ? times.reduce((earliest, time) => Math.min(earliest, time), latest) : latest - 60000;
81
+ // Every chart uses the same numeric time axis: each sample sits at its actual time.
82
+ // (at least a minute, so a new cluster's first samples don't make an axis of one instant)
83
+ const minTime = Math.min(earliest, latest - 60000);
84
+
85
+ // The cluster's total of a field: every process's samples summed per sample interval,
86
+ // counted from the earliest sample (the processes start, and so sample, seconds apart).
87
+ // once: the processes all report the same figure (the shared client cache), so it is taken
88
+ // once (the largest, the latest seen) rather than summed
89
+ function totalOf(field, once = false) {
90
+ const interval = metrics.series.map(series => series.intervalMs).find(ms => ms > 0) || 600000;
91
+ const points = metrics.series.flatMap(series => series.history.filter(point => Number.isFinite(point[field])));
92
+ const start = points.reduce((start, point) => Math.min(start, point.time), Infinity);
93
+ const sums = new Map();
94
+ const counts = new Map();
95
+ for (const point of points) {
96
+ const x = start + Math.round((point.time - start) / interval) * interval;
97
+ sums.set(x, once ? Math.max(sums.get(x) || 0, point[field]) : (sums.get(x) || 0) + point[field]);
98
+ counts.set(x, (counts.get(x) || 0) + 1);
99
+ }
100
+ const rows = [...sums].sort((a, b) => a[0] - b[0]);
101
+ // the newest interval, while some processes have yet to send their sample for it, would
102
+ // show a fall that isn't there: left off until they have
103
+ while (rows.length > 1 && counts.get(rows[rows.length - 1][0]) < counts.get(rows[rows.length - 2][0])) rows.pop();
104
+ return rows.map(([x, y]) => ({ x, y }));
105
+ }
106
+ const totalLine = data => ({ label:'Total', data, borderColor:total.color, backgroundColor:total.color,
107
+ borderWidth:3, pointRadius:0, pointHitRadius:5, fill:false, tension:0, spanGaps:false });
108
+
109
+ // A hatched interval means at least one memory-pressure 503 was counted in it, not that
110
+ // the guard refused all through it. Overlapping intervals of different processes merge.
111
+ const refusals = metrics.series.flatMap(series => series.history.filter(point =>
112
+ Number.isFinite(point.intervalStart) && point.intervalStart < point.time && point.refused > 0));
113
+ const windows = [];
114
+ refusals.map(point => [point.intervalStart, point.time]).sort((a, b) => a[0] - b[0]).forEach(window => {
115
+ const last = windows[windows.length - 1];
116
+ if (last && window[0] <= last[1]) last[1] = Math.max(last[1], window[1]);
117
+ else windows.push(window);
118
+ });
119
+ const notice = document.getElementById('serverMetricsShedding');
120
+ if (notice && refusals.length) {
121
+ notice.hidden = false;
122
+ notice.textContent = refusals.reduce((sum, point) => sum + point.refused, 0).toLocaleString() +
123
+ ' memory-pressure 503 refusals in view. Hatching marks the sample intervals with refusals; request lines count admitted work.';
124
+ }
125
+ const hatch = {
126
+ id:'memoryShedding',
127
+ beforeDraw(chart) {
128
+ if (!windows.length || !chart.chartArea) return;
129
+ const {ctx, chartArea:area, scales:{x}} = chart;
130
+ for (const [from, to] of windows) {
131
+ const left = Math.max(area.left, x.getPixelForValue(from)), right = Math.min(area.right, x.getPixelForValue(to));
132
+ if (right <= left) continue;
133
+ ctx.save();
134
+ ctx.beginPath();
135
+ ctx.rect(left, area.top, right - left, area.bottom - area.top);
136
+ ctx.clip();
137
+ ctx.strokeStyle = 'rgba(60,60,60,.35)';
138
+ ctx.lineWidth = 1;
139
+ ctx.beginPath();
140
+ for (let at = left - (area.bottom - area.top); at < right; at += 6) {
141
+ ctx.moveTo(at, area.bottom);
142
+ ctx.lineTo(at + area.bottom - area.top, area.top);
143
+ }
144
+ ctx.stroke();
145
+ ctx.fillStyle = '#333';
146
+ ctx.fillRect(left, area.top, right - left, 2);
147
+ ctx.restore();
148
+ }
149
+ }
150
+ };
151
+
152
+ // until there are two samples to draw a line between, the chart says so
153
+ const waiting = {
154
+ id:'waitingForSamples',
155
+ afterDatasetsDraw(chart) {
156
+ if (!chart.chartArea || chart.data.datasets.some(d => !d.threshold && d.data.filter(p => Number.isFinite(p.y)).length > 1)) return;
157
+ const {ctx, chartArea:area} = chart;
158
+ ctx.save();
159
+ ctx.font = '11px sans-serif';
160
+ ctx.fillStyle = '#5f6368';
161
+ ctx.textAlign = 'center';
162
+ ctx.fillText('Waiting for samples', (area.left + area.right) / 2, (area.top + area.bottom) / 2);
163
+ ctx.restore();
164
+ }
165
+ };
166
+
167
+ // the threshold's value, written at the right end of its line
168
+ const thresholdLabel = {
169
+ id:'thresholdLabel',
170
+ afterDatasetsDraw(chart) {
171
+ const dataset = chart.data.datasets.find(d => d.threshold);
172
+ if (!dataset || !chart.chartArea) return;
173
+ const {ctx, chartArea:area, scales:{y}} = chart;
174
+ const at = y.getPixelForValue(dataset.data[0].y);
175
+ if (at < area.top || at > area.bottom) return;
176
+ ctx.save();
177
+ ctx.font = '10px sans-serif';
178
+ ctx.fillStyle = '#333';
179
+ ctx.textAlign = 'right';
180
+ ctx.textBaseline = 'bottom';
181
+ ctx.fillText('refuses above ' + (dataset.data[0].y / 1024).toFixed(1) + ' GB', area.right - 2, at - 2);
182
+ ctx.restore();
183
+ }
184
+ };
185
+
186
+ function formatTime(time) {
187
+ return new Date(time).toLocaleTimeString([], { hour:'2-digit', minute:'2-digit',
188
+ ...(latest - minTime < 120000 ? { second:'2-digit' } : {}) });
189
+ }
190
+ // pointRadius: bigger where the samples are few and far between (the external requests)
191
+ function chart(id, field, unit, divisor = 1, pointRadius = 1.5) {
192
+ const canvas = document.getElementById(id);
193
+ if (!canvas) return;
194
+ const datasets = drawn.map((series, i) => ({
195
+ label:series.label,
196
+ data:series.history.map(point => ({ x:point.time, y:Number.isFinite(point[field]) ? point[field] / divisor : null,
197
+ refused:point.refused, ext:point.ext })),
198
+ borderColor:styles[i].color, backgroundColor:styles[i].color + '18', borderDash:styles[i].dash,
199
+ pointStyle:styles[i].point, pointBackgroundColor:styles[i].color, pointRadius:pointRadius,
200
+ pointHitRadius:5, pointHoverRadius:4, borderWidth:1.5,
201
+ fill:false, tension:0, spanGaps:false
202
+ }));
203
+ if (field === 'rpm' && showTotal) datasets.push(totalLine(totalOf('rpm')));
204
+ if (field === 'clientConcepts' && showTotal) datasets.push(totalLine(totalOf('clientConcepts', metrics.sharedCache)));
205
+ if (field === 'anon' && memoryTotal) {
206
+ datasets.push(totalLine(memoryTotal.history.map(point => ({ x:point.time, y:Number.isFinite(point.anon) ? point.anon / divisor : null }))));
207
+ }
208
+ if (field === 'anon' && rejectAt) {
209
+ datasets.push({ label:'Refusal threshold', threshold:true, data:[{ x:minTime, y:rejectAt / divisor }, { x:latest, y:rejectAt / divisor }],
210
+ borderColor:threshold.color, backgroundColor:threshold.color, borderDash:threshold.dash, borderWidth:threshold.width,
211
+ pointRadius:0, pointHitRadius:5, fill:false, tension:0 });
212
+ }
213
+ new Chart(canvas, {
214
+ type:'line',
215
+ data:{ datasets },
216
+ options:{
217
+ responsive:true, maintainAspectRatio:false, animation:false,
218
+ scales:{
219
+ x:{ type:'linear', min:minTime, max:latest, ticks:{ maxTicksLimit:4, callback:formatTime, font:{ size:10 } } },
220
+ y:{ beginAtZero:true, ticks:{ font:{ size:10 } }, title:{ display:true, text:unit, font:{ size:10 } } }
221
+ },
222
+ plugins:{
223
+ legend:{ display:false },
224
+ tooltip:{ callbacks:{
225
+ title:items => items.length ? formatTime(items[0].parsed.x) : '',
226
+ afterLabel:item => field === 'extTat'
227
+ ? (item.raw.ext > 0 ? item.raw.ext.toLocaleString() + ' external ' + (item.raw.ext === 1 ? 'request' : 'requests') + ' in this sample' : '')
228
+ : item.raw.refused > 0
229
+ ? 'Memory-pressure 503s: ' + item.raw.refused.toLocaleString() + ' in this sample'
230
+ : ''
231
+ } }
232
+ }
233
+ },
234
+ plugins:[hatch, thresholdLabel, waiting]
235
+ });
236
+ }
237
+ if (hasAnon) chart('memoryChart', 'anon', 'MB private (anon)', 1024 * 1024);
238
+ else chart('memoryChart', 'mem', 'MB change since start', 1024 * 1024);
239
+ chart('tatChart', 'tat', 'ms');
240
+ chart('requestChart', 'rpm', 'req/min');
241
+ chart('clientCacheChart', 'clientConcepts', 'concepts');
242
+ // Separate mean and max scales keep a single long stall from flattening the mean.
243
+ chart('blockChart', 'block', 'ms');
244
+ chart('blockMaxChart', 'blockMax', 'ms');
245
+ // the external requests' turnaround (tx/html/external-metrics.liquid): further down the page
246
+ // than this script (just above Storage), so once the page has loaded
247
+ const drawExternal = () => chart('externalChart', 'extTat', 'seconds', 1000, 3);
248
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => { placeLegend(); drawExternal(); });
249
+ else drawExternal();
250
+ })();
251
+ </script>
@@ -46,9 +46,9 @@
46
46
  </thead>
47
47
  <tbody id="{{ filterId }}_rows">
48
48
  <tr>
49
- <td><input type="text" name="property" list="{{ filterId }}_props" size="24" autocomplete="off"/></td>
50
- <td><select name="op">{{ filterOpOptions }}</select></td>
51
- <td><input type="text" name="value" size="24"/></td>
49
+ <td><input type="text" aria-label="Property" name="property" list="{{ filterId }}_props" size="24" autocomplete="off"/></td>
50
+ <td><select name="op" aria-label="operator">{{ filterOpOptions }}</select></td>
51
+ <td><input type="text" aria-label="Value" name="value" size="24"/></td>
52
52
  <td><button type="button" class="btn btn-sm" title="Remove this filter" onclick="txFilterRemoveRow(this)">&#215;</button></td>
53
53
  </tr>
54
54
  </tbody>
@@ -12,7 +12,7 @@
12
12
  style="width: 90%;" autocomplete="off" placeholder="&lt; 404684003 |Clinical finding|"/></td>
13
13
  </tr>
14
14
  <tr>
15
- <td>Edition: <select name="version" id="{{ eclId }}_version">{{ versionOptions }}</select>
15
+ <td>Edition: <select name="version" aria-label="Edition" id="{{ eclId }}_version">{{ versionOptions }}</select>
16
16
  <button type="submit" class="btn btn-sm btn-primary" id="{{ eclId }}_go">Go</button>
17
17
  </td>
18
18
  </tr>
@@ -0,0 +1,17 @@
1
+ {%- comment -%}
2
+ The external requests (server.js externalStatus): requests whose answer had to ask another
3
+ server (the tx module's ecosystem resolution). They take seconds, so they are kept out of the
4
+ turnaround time graph above and graphed here, just above Storage on / and /dashboard, when
5
+ there have been any in the graphs' 24 hours. The chart is drawn by the graphs' script
6
+ (cluster-metrics-script.liquid), from the same samples (point.ext, point.extTat), in the same
7
+ styles and key. `level`: the heading's level
8
+ {%- endcomment -%}
9
+ <section class="fs-status fs-status-external">
10
+ <h{{ level | default: 4 }} class="fs-status-head">External requests <span class="fs-status-meta">· {{ external.count }} in the last {{ external.span }}, average turnaround {{ external.average }}</span></h{{ level | default: 4 }}>
11
+ <div class="server-metrics-grid">
12
+ <div class="server-metrics-chart">
13
+ <h{{ level | plus: 1 | default: 5 }}>Turnaround Time (external)</h{{ level | plus: 1 | default: 5 }}>
14
+ <div class="server-metrics-canvas"><canvas id="externalChart" aria-label="Turnaround time of the requests that had to ask another server"></canvas></div>
15
+ </div>
16
+ </div>
17
+ </section>
@@ -1,24 +1,24 @@
1
- <h2>Expand ValueSet</h2>
1
+ <h3>Expand ValueSet</h3>
2
2
  <form method="get" action="ValueSet/$expand" style="margin-left: 10px; margin-top: 5px;">
3
3
  <table class="grid" cellpadding="0" cellspacing="0">
4
4
  <tr>
5
5
  <td style="position: relative;">
6
- URL: <input type="text" name="url" id="vsUrlInput" size="40" autocomplete="off"/><span style="color: maroon">*</span>
6
+ URL: <input type="text" aria-label="URL" name="url" id="vsUrlInput" size="40" autocomplete="off"/><span style="color: maroon">*</span>
7
7
  <div id="vsUrlSuggestions" class="typeahead-suggestions" style="display:none; position:absolute; background:white; border:1px solid #ccc; max-height:200px; overflow-y:auto; z-index:1000; width:100%;"></div>
8
8
  </td>
9
- <td>Version: <input type="text" name="valueSetVersion" size="10"/></td>
9
+ <td>Version: <input type="text" aria-label="Version" name="valueSetVersion" size="10"/></td>
10
10
  </tr>
11
11
  <tr>
12
- <td>Filter: <input type="text" name="filter" size="20"/></td>
13
- <td>Language: <input type="text" name="displayLanguage" size="10"/></td>
12
+ <td>Filter: <input type="text" aria-label="Filter" name="filter" size="20"/></td>
13
+ <td>Language: <input type="text" aria-label="Language" name="displayLanguage" size="10"/></td>
14
14
  </tr>
15
15
  <tr>
16
16
  <td colspan="2">
17
- <input type="checkbox" name="includeDesignations" id="expand_desig" value="true"/>
17
+ <input type="checkbox" name="includeDesignations" aria-label="Include Designations" id="expand_desig" value="true"/>
18
18
  <label for="expand_desig">Include Designations</label>
19
- <input type="checkbox" name="activeOnly" id="expand_active" value="true"/>
19
+ <input type="checkbox" name="activeOnly" aria-label="Active Only" id="expand_active" value="true"/>
20
20
  <label for="expand_active">Active Only</label>
21
- <input type="checkbox" name="excludeNested" id="expand_excludeNested" value="true"/>
21
+ <input type="checkbox" name="excludeNested" aria-label="Not nested" id="expand_excludeNested" value="true"/>
22
22
  <label for="expand_excludeNested">Not nested</label>
23
23
  </td>
24
24
  </tr>
@@ -26,29 +26,29 @@
26
26
  <button type="submit" class="btn btn-sm btn-primary">Expand</button>
27
27
  </form>
28
28
 
29
- <h2>Validate Code</h2>
29
+ <h3>Validate Code</h3>
30
30
  <form method="get" action="ValueSet/$validate-code" style="margin-left: 10px; margin-top: 5px;">
31
31
  <input type="hidden" name="inferSystem" id="{{ inferSystemId }}" value="true"/>
32
32
  <table class="grid" cellpadding="0" cellspacing="0">
33
33
  <tr>
34
34
  <td style="position: relative;">
35
- ValueSet URL: <input type="text" name="url" id="vcVsUrlInput" size="40" autocomplete="off"/><span style="color: maroon">*</span>
35
+ ValueSet URL: <input type="text" aria-label="ValueSet URL" name="url" id="vcVsUrlInput" size="40" autocomplete="off"/><span style="color: maroon">*</span>
36
36
  <div id="vcVsUrlSuggestions" class="typeahead-suggestions" style="display:none; position:absolute; background:white; border:1px solid #ccc; max-height:200px; overflow-y:auto; z-index:1000; width:100%;"></div>
37
37
  </td>
38
- <td>ValueSet Version: <input type="text" name="valueSetVersion" size="10"/></td>
38
+ <td>ValueSet Version: <input type="text" aria-label="ValueSet Version" name="valueSetVersion" size="10"/></td>
39
39
  </tr>
40
40
  <tr>
41
- <td>System: <input type="text" name="system" id="{{ vcSystemId }}" size="30" onchange="updateInferSystem('{{ vcSystemId }}', '{{ inferSystemId }}')"/></td>
42
- <td>Version: <input type="text" name="version" size="10"/></td>
41
+ <td>System: <input type="text" aria-label="System" name="system" id="{{ vcSystemId }}" size="30" onchange="updateInferSystem('{{ vcSystemId }}', '{{ inferSystemId }}')"/></td>
42
+ <td>Version: <input type="text" aria-label="Version" name="version" size="10"/></td>
43
43
  </tr>
44
44
  <tr>
45
- <td>Code: <input type="text" name="code" size="20" required/><span style="color: maroon">*</span></td>
46
- <td>Display: <input type="text" name="display" size="20"/></td>
45
+ <td>Code: <input type="text" aria-label="Code" name="code" size="20" required/><span style="color: maroon">*</span></td>
46
+ <td>Display: <input type="text" aria-label="Display" name="display" size="20"/></td>
47
47
  </tr>
48
48
  <tr>
49
- <td>Language: <input type="text" name="displayLanguage" size="10"/></td>
49
+ <td>Language: <input type="text" aria-label="Language" name="displayLanguage" size="10"/></td>
50
50
  <td>
51
- <input type="checkbox" name="abstract" value="true"/>
51
+ <input type="checkbox" aria-label="Abstract" name="abstract" value="true"/>
52
52
  <label>Abstract</label>
53
53
  </td>
54
54
  </tr>
@@ -3,22 +3,22 @@
3
3
  <form method="get" id="searchForm" action="{{ baseUrl }}/CodeSystem">
4
4
  <table class="grid" cellpadding="0" cellspacing="0">
5
5
  <tr>
6
- <td>URL: <input type="text" name="url" size="40"/></td>
7
- <td>Version: <input type="text" name="version"/></td>
6
+ <td>URL: <input type="text" aria-label="URL" name="url" size="40"/></td>
7
+ <td>Version: <input type="text" aria-label="Version" name="version"/></td>
8
8
  </tr>
9
9
  <tr>
10
- <td title="Searches in name, title, description, publisher">Text: <input type="text" name="text"/></td>
11
- <td>Status: <select name="status" class="form-select"><option value="">(any status)</option>
10
+ <td title="Searches in name, title, description, publisher">Text: <input type="text" aria-label="Text" name="text"/></td>
11
+ <td>Status: <select name="status" aria-label="Status" class="form-select"><option value="">(any status)</option>
12
12
  <option value="draft">draft</option><option value="active">active</option>
13
13
  <option value="retired">retired</option><option value="unknown">unknown</option></select></td>
14
14
  </tr>
15
15
  <tr>
16
- <td>Language: <input type="text" name="lang" size="10"/> (ietf code)</td>
17
- <td>Source: <select name="source" class="form-select">{{ sourceOptions }}</select></td>
16
+ <td>Language: <input type="text" aria-label="Language" name="lang" size="10"/> (ietf code)</td>
17
+ <td>Source: <select name="source" aria-label="Source" class="form-select">{{ sourceOptions }}</select></td>
18
18
  </tr>
19
19
  <tr>
20
- <td title="CodeSystem - for supplements, value sets, and concept maps">System: <input type="text" name="system" size="40"/></td>
21
- <td>CS Content: <select name="content-mode" class="form-select"><option value="">(any content)</option>
20
+ <td title="CodeSystem - for supplements, value sets, and concept maps">System: <input type="text" aria-label="System" name="system" size="40"/></td>
21
+ <td>CS Content: <select name="content-mode" aria-label="CS Content" class="form-select"><option value="">(any content)</option>
22
22
  <option value="not-present">not-present</option><option value="example">example</option>
23
23
  <option value="fragment">fragment</option><option value="complete">complete</option>
24
24
  <option value="supplement">supplement</option></select></td>
@@ -28,9 +28,9 @@
28
28
  <input type="hidden" name="_elements" id="elementsInput" value="url,version,name,title,status,content,date" disabled/>
29
29
 
30
30
  <button type="submit" class="btn btn-primary">Search:</button>
31
- <input type="radio" name="mode" value="cs" checked onchange="updateSearchAction('{{ baseUrl }}')"/> CodeSystems
32
- <input type="radio" name="mode" value="vs" onchange="updateSearchAction('{{ baseUrl }}')"/> ValueSets
33
- <input type="radio" name="mode" value="cm" onchange="updateSearchAction('{{ baseUrl }}')"/> ConceptMaps
31
+ <input type="radio" name="mode" aria-label="CodeSystems" value="cs" checked onchange="updateSearchAction('{{ baseUrl }}')"/> CodeSystems
32
+ <input type="radio" name="mode" aria-label="ValueSets" value="vs" onchange="updateSearchAction('{{ baseUrl }}')"/> ValueSets
33
+ <input type="radio" name="mode" aria-label="ConceptMaps" value="cm" onchange="updateSearchAction('{{ baseUrl }}')"/> ConceptMaps
34
34
  |
35
35
  <input type="checkbox" id="summaryTable" onchange="updateSummaryTable()"/> <label for="summaryTable">Summary Table</label>
36
36
  </form>
@@ -0,0 +1,58 @@
1
+ {%- comment -%}
2
+ The top of / and /dashboard (library/cluster-status.js, clusterSummary and processSummary):
3
+ one line of requests, uptime and heap, then the problems, only when there are any, each with an
4
+ icon and a sentence (status-tasks.liquid has the background tasks). The page's own space is what it had before the status panel: the rest is under "More".
5
+ Every style is under .fs-status.
6
+ {%- endcomment -%}
7
+ <style>
8
+ /* .fs-status twice: status-panel.liquid's .fs-status rules come later in the page */
9
+ .fs-status.fs-status-line { --fs-muted:#5f6368; --fs-rule:#e3e3e3; margin:0 0 8px; color:#202124; container-type:inline-size; }
10
+ .fs-status-line, .fs-status-line dt, .fs-status-line dd, .fs-status-line li, .fs-status-jobs, .fs-status-more summary {
11
+ font-family:"Helvetica Neue", Helvetica, Arial, sans-serif; }
12
+ /* headings that only name a section for screen readers take no space */
13
+ .fs-status-sr { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0, 0, 0, 0); white-space:nowrap; border:0; }
14
+ .fs-status-strip { display:flex; flex-wrap:wrap; align-items:baseline; margin:0; padding:0; row-gap:4px; }
15
+ .fs-status-item { display:flex; align-items:baseline; gap:6px; padding:0 16px; border-left:1px solid #9e9e9e; line-height:24px; white-space:nowrap; }
16
+ .fs-status-item:first-child { padding-left:0; border-left:0; }
17
+ .fs-status-item dt { font-size:12px; font-weight:400; color:var(--fs-muted); margin:0; }
18
+ .fs-status-item dd { margin:0; font-size:13px; color:var(--fs-muted); }
19
+ .fs-status-item .fs-status-v { font-size:18px; font-weight:600; color:#202124; font-variant-numeric:tabular-nums; }
20
+ .fs-status-item .fs-status-u { margin-left:3px; }
21
+ .fs-status-item .fs-status-s { margin-left:6px; }
22
+ /* narrow, the line wraps: no rule at the start of a line */
23
+ @container (max-width:600px) { .fs-status-item, .fs-status-item:first-child { border-left:0; padding:0 16px 0 0; } }
24
+ .fs-status-problems { list-style:none; margin:6px 0 0; padding:0; }
25
+ .fs-status-problems li { width:fit-content; max-width:100%; box-sizing:border-box; font-size:13px; line-height:18px; padding:1px 12px 1px 8px; margin:0 0 2px;
26
+ border-left:4px solid #202124; background:#fff3d6; }
27
+ .fs-status-problems .fs-status-icon { font-weight:700; margin-right:6px; }
28
+ .fs-status.fs-status-jobs { --fs-muted:#5f6368; --fs-rule:#e3e3e3; margin:0 0 8px; font-size:12px; color:#202124; }
29
+ .fs-status-jobs-head { display:flex; flex-wrap:wrap; align-items:baseline; gap:4px 8px; }
30
+ .fs-status-jobs-head > :first-child { font-size:13px; font-weight:600; margin:0; line-height:20px; }
31
+ .fs-status-jobs .fs-status-idle { color:var(--fs-muted); }
32
+ .fs-status-jobs table { border-collapse:collapse; margin:4px 0 0; }
33
+ .fs-status-jobs th, .fs-status-jobs td { text-align:left; padding:2px 12px 2px 0; border-bottom:1px solid var(--fs-rule); font-size:12px; line-height:1.4; vertical-align:top; }
34
+ .fs-status-jobs th { font-weight:600; color:var(--fs-muted); border-bottom-color:#9e9e9e; }
35
+ .fs-status-more { margin:0 0 8px; }
36
+ /* list-item: the site's CSS makes summary a block, which loses the triangle */
37
+ .fs-status-more > summary { display:list-item; font-size:12px; color:var(--fs-muted); cursor:pointer; line-height:20px; }
38
+ .fs-status-more > summary > :first-child { display:inline; font:inherit; margin:0; color:#202124; font-weight:600; }
39
+ .fs-status-more[open] > summary { margin-bottom:4px; }
40
+ </style>
41
+ <section class="fs-status fs-status-line" aria-labelledby="fs-status-line-title">
42
+ <h{{ level }} id="fs-status-line-title" class="fs-status-sr">Status</h{{ level }}>
43
+ <dl class="fs-status-strip">
44
+ {%- for i in line.items %}
45
+ <div class="fs-status-item"><dt>{{ i.label | escape }}</dt><dd><span class="fs-status-v">{{ i.value | escape }}</span>
46
+ {%- if i.unit %}<span class="fs-status-u">{{ i.unit | escape }}</span>{% endif %}
47
+ {%- if i.sub %}<span class="fs-status-s">{{ i.sub | escape }}</span>{% endif %}</dd></div>
48
+ {%- endfor %}
49
+ {%- if line.problems.size > 0 %}
50
+ <div class="fs-status-item"><dt>Problems</dt><dd><span class="fs-status-v"><span aria-hidden="true">⚠ </span>{{ line.problems.size }}</span></dd></div>
51
+ {%- endif %}
52
+ </dl>
53
+ {%- if line.problems.size > 0 %}
54
+ <ul class="fs-status-problems">
55
+ {%- for p in line.problems %}<li><span class="fs-status-icon" aria-hidden="true">⚠</span>{{ p | escape }}</li>{% endfor %}
56
+ </ul>
57
+ {%- endif %}
58
+ </section>
@@ -0,0 +1,140 @@
1
+ {%- comment -%}
2
+ The status panel under "More" on / and /dashboard (library/cluster-status.js builds `view`): a
3
+ header (`level`: its heading's level, 3 by default), a row of tiles, the memory meter, a row per
4
+ process, footnotes. Every style is under .fs-status, and
5
+ the graphs below share its grid (.fs-status-grid: six columns, 16 px apart).
6
+ {%- endcomment -%}
7
+ <style>
8
+ .fs-status { --fs-muted:#5f6368; --fs-rule:#e3e3e3; --fs-card:#f7f7f7; margin:24px 0 16px; color:#202124; container-type:inline-size; }
9
+ /* the site's body font throughout (fhir.css gives p and td their own) */
10
+ .fs-status, .fs-status p, .fs-status td, .fs-status th { font-family:"Helvetica Neue", Helvetica, Arial, sans-serif; }
11
+ .fs-status .fs-status-head { font-size:18px; margin:0 0 8px; font-weight:600; }
12
+ /* under "More" (status-line.liquid), closer together */
13
+ .fs-status-more .fs-status { margin:8px 0 12px; }
14
+ .fs-status-more .fs-status .fs-status-head { font-size:15px; }
15
+ .fs-status-head .fs-status-meta { font-size:13px; font-weight:400; color:var(--fs-muted); }
16
+ /* six columns, or as many as there are tiles (storage), so a row is never short */
17
+ .fs-status-grid { display:grid; grid-template-columns:repeat(var(--fs-cols, 6), minmax(0, 1fr)); gap:16px; }
18
+ .fs-status-tile { background:var(--fs-card); border:1px solid var(--fs-rule); border-radius:4px; padding:8px; min-width:0; }
19
+ .fs-status-label { font-size:12px; color:var(--fs-muted); line-height:1.3; }
20
+ .fs-status-value { font-size:22px; font-weight:600; line-height:1.25; font-variant-numeric:tabular-nums; white-space:nowrap; }
21
+ .fs-status-unit { font-size:13px; font-weight:400; color:var(--fs-muted); margin-left:4px; }
22
+ .fs-status-sub { font-size:12px; color:var(--fs-muted); line-height:1.3; }
23
+ .fs-status-meter { margin-top:16px; }
24
+ .fs-status-meter-head { display:flex; flex-wrap:wrap; justify-content:space-between; gap:0 16px; font-size:13px; margin-bottom:4px; }
25
+ .fs-status-meter-head strong { font-weight:600; }
26
+ .fs-status-meter-head .fs-status-meter-value { font-variant-numeric:tabular-nums; }
27
+ .fs-status-bar { position:relative; height:16px; background:#fff; border:1px solid #9e9e9e; border-radius:2px; display:flex; }
28
+ .fs-status-seg { height:100%; }
29
+ .fs-status-striped { background-image:repeating-linear-gradient(135deg, rgba(255,255,255,.55) 0 2px, transparent 2px 5px); }
30
+ .fs-status-tick { position:absolute; top:-4px; bottom:-4px; width:2px; margin-left:-1px; background:#202124; }
31
+ /* each label on one line, ending at its tick; placeTicks() moves the refuse label left of the
32
+ abort label if they would overlap, and only if it can't fit there puts the abort label on a
33
+ second line */
34
+ .fs-status-ticks { position:relative; height:1.6em; margin-top:2px; font-size:11px; color:var(--fs-muted); }
35
+ .fs-status-ticks.fs-status-stacked { height:3em; }
36
+ .fs-status-ticks span { position:absolute; top:0; white-space:nowrap; }
37
+ .fs-status-ticks.fs-status-stacked span.fs-status-tick-2 { top:1.4em; }
38
+ .fs-status-notes { font-size:12px; color:var(--fs-muted); margin:0; line-height:1.4; }
39
+ .fs-status-scroll { max-width:100%; overflow-x:auto; margin-top:16px; }
40
+ .fs-status table.fs-status-table { border-collapse:collapse; font-size:12px; width:100%; }
41
+ .fs-status-table th { font-size:12px; font-weight:600; color:var(--fs-muted); text-align:left; padding:4px 6px;
42
+ border-bottom:1px solid #9e9e9e; vertical-align:bottom; white-space:nowrap; }
43
+ .fs-status-table td { padding:4px 6px; border-bottom:1px solid var(--fs-rule); white-space:nowrap; }
44
+ .fs-status-table th:first-child, .fs-status-table td:first-child { padding-left:0; }
45
+ .fs-status-table th:last-child, .fs-status-table td:last-child { padding-right:0; }
46
+ .fs-status-table .fs-num { text-align:right; font-variant-numeric:tabular-nums; }
47
+ .fs-status-swatch { display:inline-block; width:12px; height:12px; margin-right:8px; vertical-align:-1px; border-radius:2px; }
48
+ .fs-status-pill { display:inline-block; font-size:11px; line-height:16px; padding:0 8px; border-radius:8px; margin-right:4px;
49
+ border:1px solid #202124; background:#202124; color:#fff; }
50
+ .fs-status-pill.fs-quiet { background:#fff; color:#202124; border-color:#9e9e9e; }
51
+ .fs-status-foot { font-size:12px; color:var(--fs-muted); margin:8px 0 0; line-height:1.4; }
52
+ /* the background tasks (stats.taskDetails), in the table's style; its State says what the
53
+ row colour did */
54
+ .fs-status-tasks table.grid { border:0; border-collapse:collapse; margin:0; font-size:12px; }
55
+ .fs-status-tasks table.grid th, .fs-status-tasks table.grid td { border:0; border-bottom:1px solid var(--fs-rule);
56
+ padding:4px 12px 4px 0; font-size:12px; line-height:1.4; background:transparent; vertical-align:top;
57
+ font-family:"Helvetica Neue", Helvetica, Arial, sans-serif; }
58
+ .fs-status-tasks table.grid th { font-weight:600; color:var(--fs-muted); text-align:left; border-bottom-color:#9e9e9e; }
59
+ .fs-status-tasks table.grid tr { background:transparent !important; }
60
+ /* by the panel's own width (the page's column, not the window): six tiles a row, then 3, 2, 1 */
61
+ @container (max-width:700px) { .fs-status-grid { grid-template-columns:repeat(3, minmax(0, 1fr)); } }
62
+ @container (max-width:460px) { .fs-status-grid { grid-template-columns:repeat(2, minmax(0, 1fr)); } }
63
+ @container (max-width:280px) { .fs-status-grid { grid-template-columns:minmax(0, 1fr); } }
64
+ </style>
65
+ <section class="fs-status">
66
+ <h{{ level | default: 3 }} class="fs-status-head">{{ view.heading | escape }}{% for m in view.meta %} <span class="fs-status-meta">· {{ m | escape }}</span>{% endfor %}</h{{ level | default: 3 }}>
67
+ {%- if view.tiles.size > 0 %}
68
+ <div class="fs-status-grid"{% if view.columns %} style="--fs-cols:{{ view.columns }}"{% endif %}>
69
+ {%- for t in view.tiles %}
70
+ <div class="fs-status-tile">
71
+ <div class="fs-status-label">{{ t.label | escape }}</div>
72
+ <div class="fs-status-value">{{ t.value | escape }}{% if t.unit %}<span class="fs-status-unit">{{ t.unit | escape }}</span>{% endif %}</div>
73
+ {%- if t.sub %}<div class="fs-status-sub">{{ t.sub | escape }}</div>{% endif %}
74
+ </div>
75
+ {%- endfor %}
76
+ </div>
77
+ {%- endif %}
78
+ {%- if view.memory %}
79
+ <div class="fs-status-meter">
80
+ <div class="fs-status-meter-head"><span><strong>Memory</strong> · {{ view.memory.scope | escape }}</span>
81
+ <span class="fs-status-meter-value">{% if view.memory.unknown %}not known{% else %}{{ view.memory.value | escape }}{% endif %}</span></div>
82
+ {%- unless view.memory.unknown %}
83
+ <div class="fs-status-bar" role="img" aria-label="Memory {{ view.memory.value | escape }}">
84
+ {%- for s in view.memory.segments %}<div class="fs-status-seg{% if s.striped %} fs-status-striped{% endif %}" style="width:{{ s.width }}%;background-color:{{ s.color }}" title="{{ s.title | escape }}"></div>{% endfor %}
85
+ {%- for t in view.memory.ticks %}<div class="fs-status-tick" style="left:{{ t.at }}%"></div>{% endfor %}
86
+ </div>
87
+ <div class="fs-status-ticks">
88
+ {%- for t in view.memory.ticks %}<span class="fs-status-tick-{{ forloop.index }}" style="right:{{ t.right }}%">{{ t.label | escape }}</span>{% endfor %}
89
+ </div>
90
+ {%- endunless %}
91
+ {%- for note in view.memory.notes %}<p class="fs-status-notes">{{ note | escape }}</p>{% endfor %}
92
+ </div>
93
+ {%- endif %}
94
+ {%- if view.table %}
95
+ <div class="fs-status-scroll">
96
+ <table class="fs-status-table">
97
+ <thead><tr>{% for c in view.table.columns %}<th{% if c.num %} class="fs-num"{% endif %}>{{ c.label | escape }}</th>{% endfor %}</tr></thead>
98
+ <tbody>
99
+ {%- for r in view.table.rows %}
100
+ <tr>{% for c in r.cells %}<td{% if c.num %} class="fs-num"{% endif %}>
101
+ {%- if forloop.first %}<span class="fs-status-swatch{% if r.swatch.striped %} fs-status-striped{% endif %}" style="background-color:{{ r.swatch.color }}"></span>{% endif -%}
102
+ {%- if c.pills %}{% for p in c.pills %}<span class="fs-status-pill{% if p.quiet %} fs-quiet{% endif %}">{{ p.text | escape }}</span>{% endfor %}{% else %}{{ c.text | escape }}{% endif -%}
103
+ </td>{% endfor %}</tr>
104
+ {%- endfor %}
105
+ </tbody>
106
+ </table>
107
+ </div>
108
+ {%- endif %}
109
+ {%- for f in view.footnotes %}<p class="fs-status-foot">{{ f | escape }}</p>{% endfor %}
110
+ </section>
111
+ {%- if view.memory and view.memory.ticks %}
112
+ <script>
113
+ // Each threshold label ends at its own tick. When the refuse label would run into the abort
114
+ // label, it ends just left of it instead; if there isn't room for that, the abort label takes
115
+ // a second line, each under its own tick again. Done on load and whenever the panel's width changes.
116
+ (function() {
117
+ const panel = document.currentScript.previousElementSibling;
118
+ const line = panel && panel.querySelector('.fs-status-ticks');
119
+ if (!line || line.children.length !== 2) return;
120
+ const [refuse, abort] = line.children;
121
+ const atRefuse = refuse.style.right;
122
+ function placeTicks() {
123
+ line.classList.remove('fs-status-stacked');
124
+ refuse.style.right = atRefuse;
125
+ const gap = 8;
126
+ const overlap = refuse.getBoundingClientRect().right + gap - abort.getBoundingClientRect().left;
127
+ if (overlap <= 0) return;
128
+ const width = line.getBoundingClientRect().width;
129
+ const right = width - (abort.getBoundingClientRect().left - line.getBoundingClientRect().left) + gap;
130
+ if (right + refuse.getBoundingClientRect().width <= width) {
131
+ refuse.style.right = right + 'px';
132
+ } else {
133
+ line.classList.add('fs-status-stacked');
134
+ }
135
+ }
136
+ placeTicks();
137
+ if (window.ResizeObserver) new ResizeObserver(placeTicks).observe(line);
138
+ })();
139
+ </script>
140
+ {%- endif %}