@geotechcli/core 0.4.39 → 0.4.40

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.
@@ -68,6 +68,15 @@ function sourceModeLabel(sourceHint) {
68
68
  return 'Extraction evidence retained';
69
69
  }
70
70
  }
71
+ function reviewFilterValue(item) {
72
+ if (/not extracted|missing/i.test(item.value) || /required/i.test(item.review)) {
73
+ return 'missing';
74
+ }
75
+ if (/recommended|verify|manual|visual|review/i.test(item.review) && !/ready/i.test(item.review)) {
76
+ return 'needs_review';
77
+ }
78
+ return 'verified';
79
+ }
71
80
  function compactSvgText(value, maxLength = 28) {
72
81
  const normalized = (value ?? '').replace(/\s+/g, ' ').trim();
73
82
  if (normalized.length <= maxLength) {
@@ -90,6 +99,21 @@ function renderMetric(metric) {
90
99
  </article>
91
100
  `;
92
101
  }
102
+ function renderStatusBadges(dossier) {
103
+ if (dossier.badges.length === 0) {
104
+ return '';
105
+ }
106
+ return `
107
+ <div class="status-badge-row" aria-label="Document status">
108
+ ${dossier.badges.map((badge) => `
109
+ <span class="status-badge ${toneClass(badge.tone)}">
110
+ <span>${escapeHtml(badge.label)}</span>
111
+ <strong>${escapeHtml(badge.value)}</strong>
112
+ </span>
113
+ `).join('')}
114
+ </div>
115
+ `;
116
+ }
93
117
  function renderTable(table, className = 'data-section') {
94
118
  const tableId = escapeHtml(idFromTitle(table.title));
95
119
  const body = table.rows.length === 0
@@ -119,8 +143,18 @@ function renderTable(table, className = 'data-section') {
119
143
  function renderTrustTable(dossier) {
120
144
  const rows = dossier.trustItems.length === 0
121
145
  ? '<tr><td colspan="6">No trust-layer rows were retained.</td></tr>'
122
- : dossier.trustItems.map((item) => `
123
- <tr>
146
+ : dossier.trustItems.map((item) => {
147
+ const reviewFilter = reviewFilterValue(item);
148
+ const searchText = [
149
+ item.item,
150
+ item.value,
151
+ item.sourcePage,
152
+ item.confidence,
153
+ item.review,
154
+ item.evidence,
155
+ ].join(' ').toLowerCase();
156
+ return `
157
+ <tr data-trust-row data-review="${escapeHtml(reviewFilter)}" data-search="${escapeHtml(searchText)}">
124
158
  <td><strong>${escapeHtml(item.item)}</strong></td>
125
159
  <td>${escapeHtml(item.value)}</td>
126
160
  <td>${escapeHtml(item.sourcePage)}</td>
@@ -128,13 +162,26 @@ function renderTrustTable(dossier) {
128
162
  <td>${escapeHtml(item.review)}</td>
129
163
  <td>${escapeHtml(item.evidence)}</td>
130
164
  </tr>
131
- `).join('');
165
+ `;
166
+ }).join('');
132
167
  return `
133
168
  <section class="data-section" id="parameters">
134
169
  <div class="section-heading">
135
170
  <h2>Engineering Parameters</h2>
136
171
  <p>Evidence-first review table. Every retained or missing item is shown with source, confidence, review posture, and an evidence snippet.</p>
137
172
  </div>
173
+ <div class="trust-controls" aria-label="Parameter table controls">
174
+ <label>
175
+ <span>Search parameters</span>
176
+ <input id="trust-search" type="search" placeholder="Filter by parameter, value, source, or evidence" />
177
+ </label>
178
+ <div class="filter-row" aria-label="Review status filters">
179
+ <button type="button" class="filter-button active" data-trust-filter="all">All</button>
180
+ <button type="button" class="filter-button" data-trust-filter="needs_review">Needs review</button>
181
+ <button type="button" class="filter-button" data-trust-filter="missing">Missing</button>
182
+ <button type="button" class="filter-button" data-trust-filter="verified">Verified</button>
183
+ </div>
184
+ </div>
138
185
  <div class="table-shell trust-table">
139
186
  <table>
140
187
  <thead>
@@ -153,6 +200,97 @@ function renderTrustTable(dossier) {
153
200
  </section>
154
201
  `;
155
202
  }
203
+ function renderGroundModelCrossSection(profile) {
204
+ if (!profile || profile.columns.length < 2 || profile.maxDepth <= 0) {
205
+ return `
206
+ <section class="data-section" id="ground-cross-section">
207
+ <div class="section-heading">
208
+ <h2>Ground Model Cross-Section</h2>
209
+ <p>A schematic cross-section needs at least two boreholes with retained depth evidence.</p>
210
+ </div>
211
+ <div class="empty-state">Ground-model bands were not drawn because borehole depth and layer evidence were incomplete.</div>
212
+ </section>
213
+ `;
214
+ }
215
+ const width = 940;
216
+ const height = 310;
217
+ const plotTop = 58;
218
+ const plotHeight = 182;
219
+ const left = 84;
220
+ const right = width - 44;
221
+ const usableWidth = right - left;
222
+ const referenceLayers = profile.columns
223
+ .flatMap((column) => column.layers)
224
+ .sort((leftLayer, rightLayer) => leftLayer.depthFrom - rightLayer.depthFrom)
225
+ .slice(0, 8);
226
+ const layers = referenceLayers.length > 0
227
+ ? referenceLayers
228
+ : [{
229
+ depthFrom: 0,
230
+ depthTo: profile.maxDepth,
231
+ label: 'Ground profile',
232
+ description: 'Layer boundaries were not available in structured form.',
233
+ tone: 'neutral',
234
+ uncertain: true,
235
+ }];
236
+ const yForDepth = (depth) => plotTop + (Math.max(0, Math.min(profile.maxDepth, depth)) / profile.maxDepth) * plotHeight;
237
+ const ticks = Array.from({ length: 6 }, (_value, index) => Number((profile.maxDepth * index / 5).toFixed(2)));
238
+ const columnX = (index) => profile.columns.length === 1
239
+ ? left + usableWidth / 2
240
+ : left + (usableWidth * index / (profile.columns.length - 1));
241
+ return `
242
+ <section class="data-section" id="ground-cross-section">
243
+ <div class="section-heading">
244
+ <h2>Ground Model Cross-Section</h2>
245
+ <p>Conceptual cross-section connecting retained borehole evidence. Bands are schematic and should be verified against source logs before design use.</p>
246
+ </div>
247
+ <div class="profile-shell cross-section-shell">
248
+ <svg class="ground-cross-section" viewBox="0 0 ${width} ${height}" role="img" aria-label="AI-assisted ground model cross-section">
249
+ <rect x="0" y="0" width="${width}" height="${height}" rx="18" fill="#ffffff" />
250
+ ${ticks.map((tick) => {
251
+ const y = yForDepth(tick);
252
+ return `
253
+ <line x1="${left - 34}" y1="${y.toFixed(2)}" x2="${right + 12}" y2="${y.toFixed(2)}" stroke="#d8e0ea" stroke-width="1" />
254
+ <text x="14" y="${(y + 4).toFixed(2)}" class="profile-axis">${escapeHtml(tick.toFixed(tick % 1 === 0 ? 0 : 1))} ${escapeHtml(profile.depthUnit)}</text>
255
+ `;
256
+ }).join('')}
257
+ ${layers.map((layer) => {
258
+ const y1 = yForDepth(layer.depthFrom);
259
+ const y2 = Math.max(y1 + 18, yForDepth(layer.depthTo));
260
+ return `
261
+ <g>
262
+ <title>${escapeHtml(`${layer.label}: ${layer.description}`)}</title>
263
+ <path d="M ${left} ${y1.toFixed(2)} L ${right} ${y1.toFixed(2)} L ${right} ${y2.toFixed(2)} L ${left} ${y2.toFixed(2)} Z"
264
+ fill="${toneColor(layer.tone)}" stroke="#7a8aa0" stroke-width="1.2" ${layer.uncertain ? 'stroke-dasharray="8 6"' : ''} opacity="0.88" />
265
+ <text x="${left + 18}" y="${(y1 + Math.min(34, (y2 - y1) / 2 + 5)).toFixed(2)}" class="profile-label">${escapeHtml(compactSvgText(layer.label, 32))}</text>
266
+ <text x="${left + 18}" y="${(y1 + Math.min(52, (y2 - y1) / 2 + 23)).toFixed(2)}" class="profile-small">${escapeHtml(compactSvgText(layer.description, 64))}</text>
267
+ </g>
268
+ `;
269
+ }).join('')}
270
+ ${profile.columns.map((column, index) => {
271
+ const x = columnX(index);
272
+ const depth = column.totalDepth ?? profile.maxDepth;
273
+ const bottomY = yForDepth(depth);
274
+ const waterY = column.waterTableDepth != null ? yForDepth(column.waterTableDepth) : null;
275
+ return `
276
+ <g>
277
+ <line x1="${x.toFixed(2)}" y1="${plotTop - 10}" x2="${x.toFixed(2)}" y2="${bottomY.toFixed(2)}" stroke="#0b4f8a" stroke-width="3" />
278
+ <circle cx="${x.toFixed(2)}" cy="${plotTop - 12}" r="6" fill="#0b4f8a" />
279
+ <text x="${(x - 24).toFixed(2)}" y="28" class="profile-title">${escapeHtml(column.boreholeId)}</text>
280
+ <text x="${(x - 36).toFixed(2)}" y="${height - 32}" class="profile-small">TD ${escapeHtml(depth.toFixed(2))} ${escapeHtml(profile.depthUnit)}</text>
281
+ ${waterY != null ? `
282
+ <line x1="${(x - 34).toFixed(2)}" y1="${waterY.toFixed(2)}" x2="${(x + 34).toFixed(2)}" y2="${waterY.toFixed(2)}" stroke="#0891b2" stroke-width="2" />
283
+ <text x="${(x - 40).toFixed(2)}" y="${(waterY - 7).toFixed(2)}" class="profile-water">GW</text>
284
+ ` : ''}
285
+ </g>
286
+ `;
287
+ }).join('')}
288
+ </svg>
289
+ </div>
290
+ <p class="verification-note">AI-assisted ground model. Use source logs and engineering judgment before adopting layer continuity, groundwater, or design parameters.</p>
291
+ </section>
292
+ `;
293
+ }
156
294
  function renderBoreholeProfile(profile) {
157
295
  if (!profile || profile.columns.length === 0 || profile.maxDepth <= 0) {
158
296
  return `
@@ -278,6 +416,11 @@ function renderSourceEvidence(dossier) {
278
416
  ].filter(Boolean).join(' | '))}</p>
279
417
  ${card.highlights.length > 0 ? `<ul>${card.highlights.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul>` : '<p>No strong structured highlight was retained for this page.</p>'}
280
418
  ${card.warnings.length > 0 ? `<small>Human verification recommended: ${escapeHtml(String(card.warnings.length))} retained warning(s).</small>` : ''}
419
+ <div class="evidence-actions" aria-label="${escapeHtml(`${card.pageLabel} review actions`)}">
420
+ <a href="#processing-audit">Open source page</a>
421
+ <button type="button" data-dossier-action="verified">Mark verified</button>
422
+ <button type="button" data-dossier-action="flagged">Flag issue</button>
423
+ </div>
281
424
  </article>
282
425
  `).join('')}
283
426
  </div>
@@ -336,6 +479,28 @@ function renderProcessingAudit(dossier, auditTables) {
336
479
  </details>
337
480
  `;
338
481
  }
482
+ function renderReviewBar(dossier) {
483
+ const missingCount = dossier.trustItems.filter((item) => reviewFilterValue(item) === 'missing').length;
484
+ const needsReviewCount = dossier.trustItems.filter((item) => reviewFilterValue(item) === 'needs_review').length;
485
+ const reviewBadgeValue = dossier.badges.find((badge) => /review/i.test(badge.label))?.value ?? 'Yes';
486
+ const reviewRequired = /^no$/i.test(reviewBadgeValue) ? 'Review ready' : 'Review required';
487
+ return `
488
+ <div class="review-bar" role="region" aria-label="Human review workflow">
489
+ <div>
490
+ <strong>${escapeHtml(reviewRequired)}</strong>
491
+ <span>${escapeHtml(`${dossier.pageCards.length} page(s), ${dossier.trustItems.length} review item(s), ${needsReviewCount} need review, ${missingCount} missing`)}</span>
492
+ </div>
493
+ <div class="review-actions">
494
+ <a href="#parameters" class="primary-action">Approve Extraction</a>
495
+ <a href="#risks">Flag for Review</a>
496
+ <button type="button" onclick="window.print()">Export Dossier</button>
497
+ <a href="#ground-cross-section">Generate Ground Model</a>
498
+ <a href="#source-evidence">Ask Geotech Agent</a>
499
+ <a href="#processing-audit">Open Audit Trail</a>
500
+ </div>
501
+ </div>
502
+ `;
503
+ }
339
504
  function renderActionBar() {
340
505
  return `
341
506
  <div class="action-bar" aria-label="Review actions">
@@ -398,6 +563,7 @@ export function renderIngestDossierAsHtml(dossier) {
398
563
  background: var(--bg);
399
564
  font-variant-numeric: tabular-nums;
400
565
  overflow-x: hidden;
566
+ padding-bottom: 98px;
401
567
  }
402
568
 
403
569
  .layout {
@@ -538,6 +704,37 @@ export function renderIngestDossierAsHtml(dossier) {
538
704
 
539
705
  .hero-meta strong { color: var(--text); }
540
706
 
707
+ .status-badge-row {
708
+ display: flex;
709
+ flex-wrap: wrap;
710
+ gap: 9px;
711
+ margin-top: 16px;
712
+ }
713
+
714
+ .status-badge {
715
+ display: inline-grid;
716
+ gap: 4px;
717
+ min-width: 126px;
718
+ padding: 10px 12px;
719
+ border: 1px solid rgba(102, 112, 133, 0.18);
720
+ border-radius: 14px;
721
+ line-height: 1.2;
722
+ }
723
+
724
+ .status-badge span {
725
+ color: currentColor;
726
+ opacity: 0.72;
727
+ font-size: 0.68rem;
728
+ text-transform: uppercase;
729
+ font-weight: 900;
730
+ }
731
+
732
+ .status-badge strong {
733
+ color: var(--text);
734
+ font-size: 0.86rem;
735
+ overflow-wrap: anywhere;
736
+ }
737
+
541
738
  .executive-grid, .metric-grid, .card-grid, .evidence-grid, .audit-page-grid, .footer-grid {
542
739
  display: grid;
543
740
  gap: 16px;
@@ -640,6 +837,67 @@ export function renderIngestDossierAsHtml(dossier) {
640
837
  color: #ffffff;
641
838
  }
642
839
 
840
+ .review-bar {
841
+ position: fixed;
842
+ left: 50%;
843
+ bottom: 0;
844
+ z-index: 45;
845
+ display: flex;
846
+ align-items: center;
847
+ justify-content: space-between;
848
+ gap: 18px;
849
+ width: min(1180px, calc(100vw - 36px));
850
+ margin: 10px auto 0;
851
+ padding: 13px 14px;
852
+ border: 1px solid #263244;
853
+ border-radius: 18px 18px 0 0;
854
+ background: rgba(11, 18, 32, 0.94);
855
+ color: #f8fafc;
856
+ box-shadow: 0 -18px 38px rgba(11, 18, 32, 0.2);
857
+ backdrop-filter: blur(14px);
858
+ transform: translate(-50%, 120%);
859
+ transition: transform 180ms ease;
860
+ }
861
+
862
+ .review-bar.visible {
863
+ transform: translate(-50%, 0);
864
+ }
865
+
866
+ .review-bar > div:first-child {
867
+ display: grid;
868
+ gap: 3px;
869
+ min-width: 220px;
870
+ }
871
+
872
+ .review-bar strong { font-size: 0.95rem; }
873
+ .review-bar span { color: #94a3b8; font-size: 0.84rem; line-height: 1.4; }
874
+
875
+ .review-actions {
876
+ display: flex;
877
+ flex-wrap: wrap;
878
+ justify-content: flex-end;
879
+ gap: 8px;
880
+ }
881
+
882
+ .review-actions a, .review-actions button {
883
+ border: 1px solid #334155;
884
+ border-radius: 10px;
885
+ background: #111827;
886
+ color: #dbeafe;
887
+ padding: 8px 10px;
888
+ font: inherit;
889
+ font-size: 0.78rem;
890
+ font-weight: 850;
891
+ text-decoration: none;
892
+ cursor: pointer;
893
+ }
894
+
895
+ .review-actions .primary-action {
896
+ background: #38bdf8;
897
+ border-color: #38bdf8;
898
+ color: #0b1220;
899
+ }
900
+
643
901
  .data-section {
644
902
  display: grid;
645
903
  gap: 16px;
@@ -705,6 +963,27 @@ export function renderIngestDossierAsHtml(dossier) {
705
963
  height: auto;
706
964
  }
707
965
 
966
+ .cross-section-shell {
967
+ background:
968
+ linear-gradient(180deg, rgba(11, 79, 138, 0.04), rgba(22, 135, 93, 0.04)),
969
+ var(--surface);
970
+ }
971
+
972
+ .ground-cross-section {
973
+ display: block;
974
+ min-width: 840px;
975
+ width: 100%;
976
+ height: auto;
977
+ }
978
+
979
+ .verification-note {
980
+ color: var(--muted);
981
+ font-weight: 700;
982
+ line-height: 1.55;
983
+ padding-left: 14px;
984
+ border-left: 3px solid var(--warning);
985
+ }
986
+
708
987
  .profile-title { font: 800 15px Inter, Segoe UI, sans-serif; fill: #101828; }
709
988
  .profile-label { font: 800 13px Inter, Segoe UI, sans-serif; fill: #101828; }
710
989
  .profile-small { font: 11px Inter, Segoe UI, sans-serif; fill: #475467; }
@@ -757,6 +1036,71 @@ export function renderIngestDossierAsHtml(dossier) {
757
1036
  line-height: 1;
758
1037
  }
759
1038
 
1039
+ .trust-controls {
1040
+ display: flex;
1041
+ align-items: end;
1042
+ justify-content: space-between;
1043
+ gap: 12px;
1044
+ flex-wrap: wrap;
1045
+ padding: 14px;
1046
+ border: 1px solid var(--border);
1047
+ border-radius: var(--radius);
1048
+ background: var(--surface);
1049
+ }
1050
+
1051
+ .trust-controls label {
1052
+ display: grid;
1053
+ gap: 7px;
1054
+ flex: 1 1 320px;
1055
+ color: var(--muted);
1056
+ font-size: 0.75rem;
1057
+ text-transform: uppercase;
1058
+ font-weight: 900;
1059
+ }
1060
+
1061
+ .trust-controls input {
1062
+ width: 100%;
1063
+ border: 1px solid var(--border);
1064
+ border-radius: 12px;
1065
+ background: #ffffff;
1066
+ color: var(--text);
1067
+ padding: 11px 12px;
1068
+ font: inherit;
1069
+ font-size: 0.92rem;
1070
+ outline: none;
1071
+ text-transform: none;
1072
+ font-weight: 600;
1073
+ }
1074
+
1075
+ .trust-controls input:focus {
1076
+ border-color: #93c5fd;
1077
+ box-shadow: 0 0 0 4px rgba(11, 79, 138, 0.08);
1078
+ }
1079
+
1080
+ .filter-row {
1081
+ display: flex;
1082
+ gap: 8px;
1083
+ flex-wrap: wrap;
1084
+ }
1085
+
1086
+ .filter-button {
1087
+ border: 1px solid var(--border);
1088
+ border-radius: 999px;
1089
+ background: var(--surface-soft);
1090
+ color: var(--muted);
1091
+ padding: 9px 12px;
1092
+ font: inherit;
1093
+ font-size: 0.8rem;
1094
+ font-weight: 850;
1095
+ cursor: pointer;
1096
+ }
1097
+
1098
+ .filter-button.active {
1099
+ background: var(--primary);
1100
+ color: #ffffff;
1101
+ border-color: var(--primary);
1102
+ }
1103
+
760
1104
  .evidence-grid {
761
1105
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
762
1106
  }
@@ -785,6 +1129,38 @@ export function renderIngestDossierAsHtml(dossier) {
785
1129
  line-height: 1.45;
786
1130
  }
787
1131
 
1132
+ .evidence-actions {
1133
+ display: flex;
1134
+ flex-wrap: wrap;
1135
+ gap: 8px;
1136
+ padding-top: 4px;
1137
+ }
1138
+
1139
+ .evidence-actions a, .evidence-actions button {
1140
+ border: 1px solid var(--border);
1141
+ border-radius: 10px;
1142
+ background: rgba(255, 255, 255, 0.62);
1143
+ color: var(--primary);
1144
+ padding: 8px 9px;
1145
+ font: inherit;
1146
+ font-size: 0.78rem;
1147
+ font-weight: 850;
1148
+ text-decoration: none;
1149
+ cursor: pointer;
1150
+ }
1151
+
1152
+ .evidence-actions button[data-state="verified"] {
1153
+ color: var(--success);
1154
+ border-color: rgba(22, 135, 93, 0.28);
1155
+ background: var(--success-soft);
1156
+ }
1157
+
1158
+ .evidence-actions button[data-state="flagged"] {
1159
+ color: var(--warning);
1160
+ border-color: rgba(183, 121, 31, 0.28);
1161
+ background: var(--warning-soft);
1162
+ }
1163
+
788
1164
  .empty-state {
789
1165
  padding: 18px;
790
1166
  border: 1px dashed var(--border);
@@ -898,6 +1274,30 @@ export function renderIngestDossierAsHtml(dossier) {
898
1274
  line-height: 1.58;
899
1275
  }
900
1276
 
1277
+ tr[hidden] { display: none; }
1278
+
1279
+ .toast-region {
1280
+ position: fixed;
1281
+ top: 18px;
1282
+ right: 18px;
1283
+ z-index: 60;
1284
+ display: grid;
1285
+ gap: 8px;
1286
+ pointer-events: none;
1287
+ }
1288
+
1289
+ .toast {
1290
+ max-width: 320px;
1291
+ padding: 11px 14px;
1292
+ border: 1px solid #334155;
1293
+ border-radius: 12px;
1294
+ background: #0b1220;
1295
+ color: #f8fafc;
1296
+ box-shadow: 0 18px 38px rgba(11, 18, 32, 0.22);
1297
+ font-size: 0.86rem;
1298
+ font-weight: 700;
1299
+ }
1300
+
901
1301
  @media (max-width: 980px) {
902
1302
  .layout { grid-template-columns: minmax(0, 1fr); padding: 18px; max-width: 100%; }
903
1303
  .sidebar { position: static; min-height: auto; }
@@ -906,15 +1306,23 @@ export function renderIngestDossierAsHtml(dossier) {
906
1306
  }
907
1307
 
908
1308
  @media (max-width: 620px) {
1309
+ body { padding-bottom: 0; }
909
1310
  .layout { display: block; width: 100%; max-width: 390px; margin: 0; padding: 12px; }
910
1311
  .sidebar, .content, .hero, .data-section, .audit-drawer, .footer-grid {
911
1312
  width: 100%;
912
1313
  max-width: 100%;
913
1314
  }
1315
+ .sidebar { padding: 16px; min-height: auto; border-radius: 14px; }
1316
+ .brand { gap: 5px; padding-bottom: 12px; margin-bottom: 10px; }
1317
+ .brand span { display: none; }
1318
+ .nav-list { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; }
1319
+ .nav-list a { padding: 8px 9px; font-size: 0.82rem; }
914
1320
  .content { margin-top: 18px; }
915
1321
  .hero { padding: 22px; }
916
1322
  .executive-grid, .metric-grid, .card-grid, .evidence-grid, .footer-grid { grid-template-columns: 1fr; }
917
- .nav-list { grid-template-columns: 1fr; }
1323
+ .review-bar { position: static; transform: none; width: 100%; margin-top: 18px; border-radius: 18px; align-items: stretch; }
1324
+ .review-bar.visible { transform: none; }
1325
+ .review-actions { justify-content: flex-start; }
918
1326
  h1 { font-size: 1.72rem; max-width: 100%; }
919
1327
  }
920
1328
  </style>
@@ -929,6 +1337,7 @@ export function renderIngestDossierAsHtml(dossier) {
929
1337
  <nav class="nav-list">
930
1338
  <a href="#overview">Overview</a>
931
1339
  <a href="#ground-model">Ground Model</a>
1340
+ <a href="#ground-cross-section">Cross-Section</a>
932
1341
  <a href="#boreholes">Boreholes</a>
933
1342
  <a href="#parameters">Engineering Parameters</a>
934
1343
  <a href="#risks">Risks and Limitations</a>
@@ -945,6 +1354,7 @@ export function renderIngestDossierAsHtml(dossier) {
945
1354
  <h1>${escapeHtml(dossier.title)}</h1>
946
1355
  <p class="hero-subtitle">${escapeHtml(subtitle)}</p>
947
1356
  <p class="hero-summary">${escapeHtml(dossier.summary)}</p>
1357
+ ${renderStatusBadges(dossier)}
948
1358
  </div>
949
1359
  <div class="hero-meta">
950
1360
  <strong>${escapeHtml(dossier.sourceLabel)}</strong>
@@ -984,6 +1394,7 @@ export function renderIngestDossierAsHtml(dossier) {
984
1394
  </div>
985
1395
  </section>
986
1396
 
1397
+ ${renderGroundModelCrossSection(dossier.boreholeProfile)}
987
1398
  ${renderBoreholeProfile(dossier.boreholeProfile)}
988
1399
  ${renderTrustTable(dossier)}
989
1400
  ${mainTables.map((table) => renderTable(table)).join('')}
@@ -1017,6 +1428,67 @@ export function renderIngestDossierAsHtml(dossier) {
1017
1428
  </section>
1018
1429
  </main>
1019
1430
  </div>
1431
+ ${renderReviewBar(dossier)}
1432
+ <div class="toast-region" aria-live="polite" aria-atomic="true"></div>
1433
+ <script>
1434
+ (() => {
1435
+ const search = document.getElementById('trust-search');
1436
+ const rows = Array.from(document.querySelectorAll('[data-trust-row]'));
1437
+ const buttons = Array.from(document.querySelectorAll('[data-trust-filter]'));
1438
+ const toastRegion = document.querySelector('.toast-region');
1439
+ const reviewBar = document.querySelector('.review-bar');
1440
+ let activeFilter = 'all';
1441
+
1442
+ const showToast = (message) => {
1443
+ if (!toastRegion) return;
1444
+ const toast = document.createElement('div');
1445
+ toast.className = 'toast';
1446
+ toast.textContent = message;
1447
+ toastRegion.appendChild(toast);
1448
+ window.setTimeout(() => toast.remove(), 2600);
1449
+ };
1450
+
1451
+ const applyTrustFilter = () => {
1452
+ const query = String(search?.value ?? '').trim().toLowerCase();
1453
+ rows.forEach((row) => {
1454
+ const text = row.getAttribute('data-search') ?? '';
1455
+ const review = row.getAttribute('data-review') ?? '';
1456
+ const queryMatch = !query || text.includes(query);
1457
+ const filterMatch = activeFilter === 'all' || review === activeFilter;
1458
+ row.hidden = !(queryMatch && filterMatch);
1459
+ });
1460
+ };
1461
+
1462
+ const syncReviewBar = () => {
1463
+ if (!reviewBar) return;
1464
+ reviewBar.classList.toggle('visible', window.innerWidth > 620 && window.scrollY > 420);
1465
+ };
1466
+
1467
+ syncReviewBar();
1468
+ window.addEventListener('scroll', syncReviewBar, { passive: true });
1469
+ window.addEventListener('resize', syncReviewBar);
1470
+
1471
+ search?.addEventListener('input', applyTrustFilter);
1472
+ buttons.forEach((button) => {
1473
+ button.addEventListener('click', () => {
1474
+ activeFilter = button.getAttribute('data-trust-filter') ?? 'all';
1475
+ buttons.forEach((candidate) => candidate.classList.toggle('active', candidate === button));
1476
+ applyTrustFilter();
1477
+ });
1478
+ });
1479
+
1480
+ document.querySelectorAll('[data-dossier-action]').forEach((control) => {
1481
+ control.addEventListener('click', () => {
1482
+ const state = control.getAttribute('data-dossier-action') ?? '';
1483
+ control.setAttribute('data-state', state);
1484
+ control.textContent = state === 'verified' ? 'Verified' : 'Issue flagged';
1485
+ showToast(state === 'verified'
1486
+ ? 'Evidence item marked verified in this local dossier view.'
1487
+ : 'Evidence item flagged for engineering review in this local dossier view.');
1488
+ });
1489
+ });
1490
+ })();
1491
+ </script>
1020
1492
  </body>
1021
1493
  </html>`;
1022
1494
  }