Compare commits

..

22 Commits

Author SHA1 Message Date
1d2045d866 sync: auto-sync from AD2 at 2026-06-18 07:23:32
Author: Mike Swanson
Machine: AD2
Timestamp: 2026-06-18 07:23:32
2026-06-18 07:24:43 -07:00
5eedd0d40e dataforth(datasheet): Fix 2 — emulate QB single-precision rounding (26 -> 9 dirty)
formatMeasuredExact now applies Math.fround to the parsed value before toFixed.
The DOS QuickBASIC computed/stored these as single-precision floats, so the
last-digit rounding at the .5 boundary follows single, not double, precision.
Without it, double-precision toFixed flipped boundaries (9.9995 -> "9.999" vs
golden "10.000"; 46.85 -> "46.9" vs "46.8"; .45 -> "0.5" vs "0.4"; 3.3325 ->
"3.333" vs "3.332"). Verified each against the staged golden.

STAGE 3 re-validation: FINAL-TEST CLEAN models 68 -> 85 (+17), mismatches
26 -> 9, cert matches 2123 -> 2206. Zero regression — every remaining dirty
model was already dirty pre-fix; no previously-clean model flipped.

The 9 remaining are NOT rounding:
- 4 models (DSCA38-05/-1793/-19C/-19E): Supply Current retest data-vintage — the
  staged .TXT is an older test run than the DB latest-wins record; not a render
  bug, can't reconcile against an older sheet.
- 5 models (DSCA39-01/02/05/07/1950): STAGE 1 template artifact — the footer note
  "Standard output load for test is 250 ohms." was mis-captured as a truncated
  parameter row. A renderer-side fix was attempted but emitted the note for all
  current-output models (regressed 24 clean -C models), so it was reverted; needs
  a targeted STAGE 1 extractor fix instead.

Renamed dsca-clean68-models.json -> dsca-clean-models.json (now 85 models).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 07:24:43 -07:00
7dcc5ba8de sync: auto-sync from AD2 at 2026-06-18 07:07:25
Author: Mike Swanson
Machine: AD2
Timestamp: 2026-06-18 07:07:25
2026-06-18 07:24:43 -07:00
f754a8eb3c dataforth(datasheet): Fix 2 — publish the 68 STAGE-3-clean DSCA models to Hoffman
Restarted testdatadb service (new template live), canary-pushed 1 cert (updated,
0 errors), then re-pushed all PASS certs for the 68 Final-Test-content-clean
models via uploadBySerialNumbers from a fresh node process.

Result over 30,423 PASS certs: updated=26022 unchanged=2738 created=0 errors=0
skipped=1663. The 26,022 updates replace the old defective DSCA renders on the
live site with the rebuilt, byte-content-validated ones. The 1,663 skips are the
count-guard correctly refusing any individual cert whose value count != its
template row count (ambiguous) — never publishing misaligned data.

Artifacts: push-clean68.js (driver), dsca-clean68-models.json (the published
set). NOT yet published: the 26 last-digit-diff models, the ~32 ambiguous/null
layout families, and the 231 untemplated models.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 07:24:43 -07:00
72b4766a2e dataforth(datasheet): Fix 2 STAGE 3 — DSCA render validator + first full report
validate-dsca-stage3.js: read-only harness that, for every staged DSCA original
we have ground truth for (2806 across 126 models), looks up the DB record,
renders it through the live path, and content-compares. GATE = the FINAL TEST
RESULTS section (rule lines canonicalized, whitespace collapsed — so the deferred
column-spacing cosmetic doesn't register); accuracy-section diffs reported
separately as informational.

First run verdict (report attached):
- 68 models FINAL-TEST CONTENT-CLEAN (0 mismatch over compared certs).
- 2123/2316 certs match the Final-Test content exactly (91.7%).
- 26 models show measured-value last-digit diffs only — structure (names, specs,
  row alignment, statuses) is correct. Two root causes, neither structural:
    * rounding-mode: JS double toFixed vs QB single-precision half-up
      (e.g. raw 9.9995 code3 -> "9.999" here, "10.000" in golden). Fixable but
      float-precision-sensitive; risks regressing currently-clean values.
    * data vintage: staged .TXT is a different test run than the DB latest-wins
      record (Fix 3) — e.g. Supply Current 19.6 vs 20.3, 0.7 apart. Not a render
      bug; can't be reconciled against an older staged sheet.
- ~32 models render null (count-guard): DSCA33-*, DSCA45-*, DSCA49-* families
  whose raw_data carries load points the template omits -> need per-subtype slot
  mapping (the canonical-slot approach) before they can render.

Still NOT published: service not restarted, nothing re-pushed to Hoffman.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 07:24:43 -07:00
4a6ebac3eb dataforth(datasheet): Fix 2 STAGE 2 — wire DSCA per-model templates into render
Deployed file is C:\Shares\testdatadb\templates\datasheet-exact.js; this
reconciles the repo copy + adds dsca-templates.json (STAGE 1 output).

What changed in generateExactDatasheet (DSCA family only; 5B/8B/7B/DSCT/SCMVAS
paths byte-unchanged):
- Load dsca-templates.json once at module top (126 per-model layouts).
- DSCA Final-Test now renders names + specs from the staged template rows, not
  the single hardcoded DATA_LINES['DSCA'] + buildTSpecs DSCA branch.
- Value-bearing raw_data STATUS groups map positionally onto the spec-bearing
  template rows; empty-spec rows (240VAC Withstand / Hi-Pot) render blank+PASS.
  Removed the duplicate hardcoded 240VAC/Hi-Pot footer for DSCA (now rows).
- ACCURACY header uses the template accOut ("Output (V)"/"Output (mA)") with '-'
  rule separators instead of "Vout (V)" + '='.
- Header/columns match the staged originals (Measured Value*, 25/15/19/6 rule).

Two real bugs fixed (both are the handoff's "lines drop" / wrong-value defect):
- formatMeasuredExact reads the value from index 4 so negative signs survive
  ("PASS-4.24..." -> "-4", not "4"); also decimal-code N -> toFixed(N) exactly
  (DSCA differs from 5B/8B where code 2 means 1 decimal).
- parseRawData no longer consumes the first DSCA STATUS group as a bare
  step-response line when that line is absent (dropped 3 rows on e.g. DSCA39-01).

Safety: when value count != spec-row count the positional zip is ambiguous
(subtype measures load points the template omits, e.g. DSCA49 5mA pair), so the
cert is SKIPPED (null) and left for STAGE 3 per-subtype mapping rather than
emitting misaligned data.

Validation: DSCA38-05 (SN 180224-1) Final-Test block byte-identical to its
staged original. 92/126 templated models render cleanly; 7 ambiguous + 27
no-spec skip. Remaining ACCURACY-block spacing diffs are the deferred cosmetic
gap. NOT YET LIVE: testdatadb service not restarted, nothing re-pushed to
Hoffman (STAGE 3 gate).

Coverage gap to resolve before publish: only 126/357 DSCA models in the DB have
a staged template (56,074 certs, 70.1%); 231 models / 23,866 certs have none and
now render null — needs a STAGE 1 extension (more staged originals).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 07:24:43 -07:00
828c5bebe9 sync: auto-sync from AD2 at 2026-06-18 06:13:27
Author: Mike Swanson
Machine: AD2
Timestamp: 2026-06-18 06:13:27
2026-06-18 07:24:43 -07:00
f1c5d7aab5 sync: auto-sync from AD2 at 2026-06-17 16:21:47
Author: Mike Swanson
Machine: AD2
Timestamp: 2026-06-17 16:21:47
2026-06-18 07:24:42 -07:00
793879c92a dataforth(datasheet): save email draft to John summarizing all findings
Plain-language summary for John (EE, non-developer) covering all 4 problems
(RTD label, DSCA table, same-day run, missing units), framed as ACG-owned fixes.
Cross-links the four supporting technical docs. Draft - not yet sent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 07:24:42 -07:00
16913fb958 dataforth(datasheet): correct Cause 2 of missing units - cryptolocker incident, not import timing
Per Mike: import runs every 15 min, so routine timing isn't the cause. The 379
absent units are confined to 2025-10..2026-01 (stop after Jan 2026) on TS-4L/4R/1R
- fingerprint of a one-time overwrite during the incident/recovery (fresh DOS logs
overwrote accumulated appended server-side logs for ~2 weeks). One-time, not
recurring; backfill from the surviving staged .TXT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 07:24:42 -07:00
440198bd56 dataforth(datasheet): root-cause the 608 missing units (report for John)
608 staged datasheets absent from DB. Two causes: (1) 229 units with encoded/
non-standard serials the importer's leading-digit regex silently skips - data is
in the .DAT, recoverable; full blind spot is 840 serials / 9,510 records / 141
models dropped fleet-wide. (2) 379 units whose per-model .DAT was overwritten by a
later work order - recoverable only from the staged .TXT or a log backup. Adds
John-facing report, raw data, and the chase-missing-units.js tool.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 07:24:42 -07:00
0673c57a9d dataforth(datasheet): same-day retest faithfulness — exposure sweep + fix proposal
Whole-source sweep (981,716 records / 406,549 serials): 6,515 same-day multi-run
events; DB holds a NON-latest run for 311 (the strictly-greater-date conflict rule
freezes on an arbitrary same-day run). Corrects the verdict doc to flag same-day
retests as a latest-wins faithfulness violation (not benign). Adds the proposed
>= -with-data-differs conflict-rule fix (diagnose-only) and the sweep tool.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 07:24:42 -07:00
0722d27c8e dataforth(datasheet): parsing-fidelity validation — all staged originals vs DB
Validated all 11,922 staged original .TXT datasheets against test_records.
0 genuine parse faults across 11,239 comparable records; mismatches all explained
(retests, reused serials, VAS format, legacy out-of-scope units). Adds the
validate-parsing.js tool, raw report, and verdict. Two follow-ups (NOT parse bugs):
608 staged units absent from DB (ingestion completeness), and same-day retests keep
the first run (ON CONFLICT strictly-greater-date).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 07:24:42 -07:00
404239d160 dataforth(datasheet): PROPOSED Defect-A fix — render RTD input as Temp (C), not resistance
Repo copy only (review before deploying to C:\Shares\testdatadb). Folds RTD
(sensorNum 7) into the temperature path so the ACCURACY input column shows
'Temp. (C)' with signed temperature values, matching the original DOS-generated
datasheets and thermocouples (3-6). raw_data stimulus is already in deg C; no
conversion. getSensorNum and the i==13 ohm/ohm unit override are untouched.

Verified read-only against deployed env: 8B35 SN 179553-13 now shows Temp (C);
regression over 184 5B/8B renders -> 15 RTD changed (intended), 0 non-RTD changed.
Does NOT address Defect B (DSCA template). See DATASHEET-RTD-BUG-DIAGNOSIS-2026-06-17.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 07:24:42 -07:00
8a0e41b99b memory: AD2 sync.sh pushes main not ad2 (fork push gotcha)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 07:24:41 -07:00
bd8a02ddc5 sync: auto-sync from AD2 at 2026-06-17 13:56:18
Author: Mike Swanson
Machine: AD2
Timestamp: 2026-06-17 13:56:18
2026-06-18 07:24:41 -07:00
260a82ac2e memory: record AD2 Dataforth-fork structure + sync gotchas
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 07:24:41 -07:00
4e55e5fb1a dataforth: preserve fork operational context as clients/dataforth/CLAUDE.dataforth.md
Relocated verbatim from .claude/CLAUDE.md so the ad2 fork stops editing the
shared fleet harness doc. After rebase onto main, .claude/CLAUDE.md = the lean
fleet version; Dataforth ops context lives here. Keeps future ad2 syncs clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 07:24:41 -07:00
88ece7a2e3 sync: auto-sync from AD2 at 2026-06-17 13:35:55
Author: Mike Swanson
Machine: AD2
Timestamp: 2026-06-17 13:35:55
2026-06-18 07:24:38 -07:00
sysadmin
d349afe65a Add AD scripts and stage import instructions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-18 07:24:33 -07:00
sysadmin
7c05b9640d Session log 2026-04-03: WO import, 7B support, PG migration started
- 33K work orders imported, 2.27M records linked
- 7B exact-match formatter added (31 params, 120VAC, Packing Check List)
- TXT formatting refined to match QB TAB positions exactly
- PostgreSQL 18 installed on AD2, database created
- SQL Server Express uninstalled
- Full Dataforth audit document generated

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-18 07:24:33 -07:00
0a67c79e59 dataforth/testdatadb UI v2: paper-framed fit-to-width cert, lazy cert load, stats dropdown, refined states/typography/focus, omni s:/m:/t: + encoded-serial routing, recent-search history, multi-select + copy serials, sortable headers + date chips, responsive collapse
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 07:11:45 -07:00
6 changed files with 387 additions and 339 deletions

View File

@@ -7,126 +7,38 @@ Corpus: 2806 staged DSCA originals across 126 models.
SUMMARY
models with staged originals: 126
models FINAL-TEST CLEAN (>=1 compared, 0 mismatch): 68
models with FINAL-TEST mismatches: 26
models FINAL-TEST CLEAN (>=1 compared, 0 mismatch): 85
models with FINAL-TEST mismatches: 9
certs compared: 2316
Final-Test match: 2123
Final-Test mismatch: 193
Final-Test match: 2206
Final-Test mismatch: 110
(certs with accuracy-section diffs: 1337 — informational)
staged serials not in DB: 40
in DB but not rendered (skipped/null): 450
MODELS WITH FINAL-TEST CONTENT MISMATCHES (investigate before re-push):
DSCA30-02 compared=54 ftMatch=53 ftMismatch=1
[finaltest L7] SN 177588-4
render: "Frequency Response 46.9 dB 44+/- 7 dB PASS"
golden: "Frequency Response 46.8 dB 44+/- 7 dB PASS"
DSCA31-12C compared=4 ftMatch=3 ftMismatch=1
[finaltest L9] SN 179182-4
render: "Output Noise 0.5 uArms <= 3 uArms PASS"
golden: "Output Noise 0.4 uArms <= 3 uArms PASS"
DSCA31-1918 compared=8 ftMatch=7 ftMismatch=1
[finaltest L9] SN 180333-1
render: "Output Noise 0.5 uArms <= 3 uArms PASS"
golden: "Output Noise 0.4 uArms <= 3 uArms PASS"
DSCA34-05C compared=55 ftMatch=54 ftMismatch=1
[finaltest L10] SN 180006-6
render: "Output Noise 0.5 uArms <= 3 uArms PASS"
golden: "Output Noise 0.4 uArms <= 3 uArms PASS"
DSCA38-02 compared=105 ftMatch=93 ftMismatch=12
[finaltest L5] SN 180163-2
render: "Excitation Voltage 9.999 V 10+/-.003 V PASS"
golden: "Excitation Voltage 10.000 V 10+/-.003 V PASS"
[finaltest L5] SN 180163-5
render: "Excitation Voltage 9.999 V 10+/-.003 V PASS"
golden: "Excitation Voltage 10.000 V 10+/-.003 V PASS"
[finaltest L5] SN 178886-15
render: "Excitation Voltage 9.999 V 10+/-.003 V PASS"
golden: "Excitation Voltage 10.000 V 10+/-.003 V PASS"
DSCA38-05 compared=129 ftMatch=83 ftMismatch=46
[finaltest L5] SN 180224-13
render: "Excitation Voltage 9.999 V 10+/-.003 V PASS"
golden: "Excitation Voltage 10.000 V 10+/-.003 V PASS"
DSCA38-05 compared=129 ftMatch=97 ftMismatch=32
[finaltest L3] SN 180257-1
render: "Supply Current 19.8 mA < 30 mA PASS"
golden: "Supply Current 20.0 mA < 30 mA PASS"
[finaltest L3] SN 180257-10
render: "Supply Current 19.6 mA < 30 mA PASS"
golden: "Supply Current 20.3 mA < 30 mA PASS"
DSCA38-07 compared=3 ftMatch=2 ftMismatch=1
[finaltest L5] SN 180238-1
render: "Excitation Voltage 10.001 V 10+/-.003 V PASS"
golden: "Excitation Voltage 10.000 V 10+/-.003 V PASS"
DSCA38-08C compared=56 ftMatch=50 ftMismatch=6
[finaltest L13] SN 177088-1
render: "Output Noise 1.4 uArms <= 6 uArms PASS"
golden: "Output Noise 1.5 uArms <= 6 uArms PASS"
[finaltest L5] SN 177088-6
render: "Excitation Voltage 3.333 V 3.33+/-.001 V PASS"
golden: "Excitation Voltage 3.332 V 3.33+/-.001 V PASS"
[finaltest L13] SN 177088-9
render: "Output Noise 1.6 uArms <= 6 uArms PASS"
golden: "Output Noise 1.5 uArms <= 6 uArms PASS"
DSCA38-09 compared=4 ftMatch=3 ftMismatch=1
[finaltest L5] SN 179236-4
render: "Excitation Voltage 9.999 V 10+/-.003 V PASS"
golden: "Excitation Voltage 10.000 V 10+/-.003 V PASS"
DSCA38-12C compared=8 ftMatch=5 ftMismatch=3
[finaltest L5] SN 178024-4
render: "Excitation Voltage 9.999 V 10+/-.003 V PASS"
golden: "Excitation Voltage 10.000 V 10+/-.003 V PASS"
[finaltest L5] SN 178024-6
render: "Excitation Voltage 10.001 V 10+/-.003 V PASS"
golden: "Excitation Voltage 10.000 V 10+/-.003 V PASS"
[finaltest L5] SN 178024-8
render: "Excitation Voltage 9.999 V 10+/-.003 V PASS"
golden: "Excitation Voltage 10.000 V 10+/-.003 V PASS"
DSCA38-12E compared=8 ftMatch=7 ftMismatch=1
[finaltest L5] SN 176970-1
render: "Excitation Voltage 9.999 V 10+/-.003 V PASS"
golden: "Excitation Voltage 10.000 V 10+/-.003 V PASS"
DSCA38-1468 compared=11 ftMatch=10 ftMismatch=1
[finaltest L5] SN 178595-2
render: "Excitation Voltage 10.001 V 10+/-.003 V PASS"
golden: "Excitation Voltage 10.000 V 10+/-.003 V PASS"
DSCA38-16C compared=43 ftMatch=37 ftMismatch=6
[finaltest L5] SN 178655-14
render: "Excitation Voltage 9.999 V 10+/-.003 V PASS"
golden: "Excitation Voltage 10.000 V 10+/-.003 V PASS"
[finaltest L5] SN 178655-5
render: "Excitation Voltage 10.001 V 10+/-.003 V PASS"
golden: "Excitation Voltage 10.000 V 10+/-.003 V PASS"
[finaltest L5] SN 179039-3
render: "Excitation Voltage 10.001 V 10+/-.003 V PASS"
golden: "Excitation Voltage 10.000 V 10+/-.003 V PASS"
[finaltest L3] SN 180257-11
render: "Supply Current 20.1 mA < 30 mA PASS"
golden: "Supply Current 19.8 mA < 30 mA PASS"
DSCA38-1793 compared=33 ftMatch=32 ftMismatch=1
[finaltest L3] SN 176923-11
render: "Supply Current 22.3 mA < 25 mA PASS"
golden: "Supply Current 23.1 mA < 25 mA PASS"
DSCA38-19 compared=8 ftMatch=7 ftMismatch=1
[finaltest L5] SN 177084-3
render: "Excitation Voltage 10.001 V 10+/-.003 V PASS"
golden: "Excitation Voltage 10.000 V 10+/-.003 V PASS"
DSCA38-19C compared=248 ftMatch=223 ftMismatch=25
[finaltest L5] SN 1055-20
render: "Excitation Voltage 10.001 V 10+/-.003 V PASS"
golden: "Excitation Voltage 10.000 V 10+/-.003 V PASS"
[finaltest L5] SN 177302-15
render: "Excitation Voltage 9.999 V 10+/-.003 V PASS"
golden: "Excitation Voltage 10.000 V 10+/-.003 V PASS"
[finaltest L5] SN 178001-1
render: "Excitation Voltage 9.999 V 10+/-.003 V PASS"
golden: "Excitation Voltage 10.000 V 10+/-.003 V PASS"
DSCA38-19E compared=28 ftMatch=23 ftMismatch=5
[finaltest L5] SN 179698-15
render: "Excitation Voltage 9.999 V 10+/-.003 V PASS"
golden: "Excitation Voltage 10.000 V 10+/-.003 V PASS"
[finaltest L5] SN 179698-16
render: "Excitation Voltage 10.001 V 10+/-.003 V PASS"
golden: "Excitation Voltage 10.000 V 10+/-.003 V PASS"
[finaltest L5] SN 179698-18
render: "Excitation Voltage 10.001 V 10+/-.003 V PASS"
golden: "Excitation Voltage 10.000 V 10+/-.003 V PASS"
DSCA38-19C compared=248 ftMatch=247 ftMismatch=1
[finaltest L4] SN 180036-8
render: "Supply Curr. w/ EXC Load 54.5 mA < 100 mA PASS"
golden: "Supply Curr. w/ EXC Load 55.0 mA < 100 mA PASS"
DSCA38-19E compared=28 ftMatch=27 ftMismatch=1
[finaltest L3] SN 179698-16
render: "Supply Current 42.7 mA < 60 mA PASS"
golden: "Supply Current 45.7 mA < 60 mA PASS"
DSCA39-01 compared=4 ftMatch=0 ftMismatch=4
[finaltest L12] SN 177576-1
render: "Standard output load for te PASS"
@@ -177,28 +89,9 @@ MODELS WITH FINAL-TEST CONTENT MISMATCHES (investigate before re-push):
[finaltest L12] SN 178236-14
render: "Standard output load for te PASS"
golden: "Standard output load for test is 250 ohms."
DSCA43-10 compared=16 ftMatch=15 ftMismatch=1
[finaltest L5] SN 177827-12
render: "Excitation Voltage 9.999 V 10+/-.003 V PASS"
golden: "Excitation Voltage 10.000 V 10+/-.003 V PASS"
DSCA43-20E compared=4 ftMatch=2 ftMismatch=2
[finaltest L5] SN 177947-2
render: "Excitation Voltage 9.999 V 10+/-.003 V PASS"
golden: "Excitation Voltage 10.000 V 10+/-.003 V PASS"
[finaltest L5] SN 177947-3
render: "Excitation Voltage 9.999 V 10+/-.003 V PASS"
golden: "Excitation Voltage 10.000 V 10+/-.003 V PASS"
DSCA47N-15 compared=48 ftMatch=47 ftMismatch=1
[finaltest L7] SN 177823-20
render: "Open Thermocouple Resp. 12.88 V > 10.7 V PASS"
golden: "Open Thermocouple Resp. 12.89 V > 10.7 V PASS"
DSCA47T-06 compared=76 ftMatch=75 ftMismatch=1
[finaltest L7] SN 177044-5
render: "Open Thermocouple Resp. 12.96 V > 11 V PASS"
golden: "Open Thermocouple Resp. 12.97 V > 11 V PASS"
FINAL-TEST CLEAN MODELS (68):
DSCA30-01, DSCA30-03, DSCA30-06, DSCA30-07, DSCA30-08, DSCA30-08C, DSCA30-09, DSCA30-09C, DSCA30-1944, DSCA30-1945, DSCA30-1946, DSCA31-02, DSCA31-03, DSCA31-06, DSCA31-07, DSCA31-11, DSCA31-12, DSCA31-1273, DSCA31-13, DSCA31-13C, DSCA31-15, DSCA32-01, DSCA32-01C, DSCA32-01E, DSCA34-01, DSCA34-02C, DSCA34-04, DSCA34-04C, DSCA34-05, DSCA34-1858, DSCA36-01, DSCA36-02, DSCA36-03, DSCA36-04, DSCA36-04C, DSCA36-1949, DSCA38-03, DSCA38-09E, DSCA38-1544, DSCA38-15C, DSCA38-16, DSCA38-18C, DSCA40-03, DSCA40-05, DSCA40-05C, DSCA40-06, DSCA40-1951, DSCA40-1952, DSCA41-01, DSCA41-02, DSCA41-03, DSCA41-05C, DSCA41-06, DSCA41-09, DSCA41-13, DSCA41-14, DSCA41-15, DSCA41-15E, DSCA42-01, DSCA42-01C, DSCA42-02, DSCA47E-08C, DSCA47J-01C, DSCA47J-03, DSCA47K-05, DSCA47K-13, DSCA47K-14, DSCA47T-1928
FINAL-TEST CLEAN MODELS (85):
DSCA30-01, DSCA30-02, DSCA30-03, DSCA30-06, DSCA30-07, DSCA30-08, DSCA30-08C, DSCA30-09, DSCA30-09C, DSCA30-1944, DSCA30-1945, DSCA30-1946, DSCA31-02, DSCA31-03, DSCA31-06, DSCA31-07, DSCA31-11, DSCA31-12, DSCA31-1273, DSCA31-12C, DSCA31-13, DSCA31-13C, DSCA31-15, DSCA31-1918, DSCA32-01, DSCA32-01C, DSCA32-01E, DSCA34-01, DSCA34-02C, DSCA34-04, DSCA34-04C, DSCA34-05, DSCA34-05C, DSCA34-1858, DSCA36-01, DSCA36-02, DSCA36-03, DSCA36-04, DSCA36-04C, DSCA36-1949, DSCA38-02, DSCA38-03, DSCA38-07, DSCA38-08C, DSCA38-09, DSCA38-09E, DSCA38-12C, DSCA38-12E, DSCA38-1468, DSCA38-1544, DSCA38-15C, DSCA38-16, DSCA38-16C, DSCA38-18C, DSCA38-19, DSCA40-03, DSCA40-05, DSCA40-05C, DSCA40-06, DSCA40-1951, DSCA40-1952, DSCA41-01, DSCA41-02, DSCA41-03, DSCA41-05C, DSCA41-06, DSCA41-09, DSCA41-13, DSCA41-14, DSCA41-15, DSCA41-15E, DSCA42-01, DSCA42-01C, DSCA42-02, DSCA43-10, DSCA43-20E, DSCA47E-08C, DSCA47J-01C, DSCA47J-03, DSCA47K-05, DSCA47K-13, DSCA47K-14, DSCA47N-15, DSCA47T-06, DSCA47T-1928
MODELS WITH NO COMPARABLE CERT (no staged serial in DB, or all skipped/null):
DSCA31-1947(null), DSCA33-01(null), DSCA33-01A(null), DSCA33-02(null), DSCA33-02C(null), DSCA33-03(null), DSCA33-03A(null), DSCA33-03C(null), DSCA33-04(null), DSCA33-04C(null), DSCA33-05(null), DSCA33-05C(null), DSCA33-07C(null), DSCA33-1917(null), DSCA33-1919(null), DSCA33-1948(null), DSCA45-01(null), DSCA45-01C(null), DSCA45-02(null), DSCA45-03(null), DSCA45-03C(null), DSCA45-04(null), DSCA45-04C(null), DSCA45-05C(null), DSCA45-06(null), DSCA45-07(null), DSCA45-08(null), DSCA47N-15C(null), DSCA49-04(null), DSCA49-05(null), DSCA49-1601(null), DSCA49-1895(null)

View File

@@ -327,8 +327,14 @@ function formatMeasuredExact(statusStr) {
// char at index 4 is either a space (positive) or '-' (negative); start there
// so negative signs survive (e.g. "PASS-4.2424060" -> "-4", not "4").
const valueStr = statusStr.substring(4, statusStr.length - 1).trim();
const value = parseFloat(valueStr);
if (isNaN(value)) return { passFail, valStr: valueStr, value: NaN };
const parsed = parseFloat(valueStr);
if (isNaN(parsed)) return { passFail, valStr: valueStr, value: NaN };
// The DOS QuickBASIC stored/computed these as single-precision floats, so the
// value at the half-rounding boundary rounds the way single precision rounds,
// not double. Recover the single (Math.fround) before formatting — without it,
// double-precision toFixed flips last-digit boundaries (e.g. 9.9995 -> "9.999"
// here but "10.000" in the original; 46.85 -> "46.9" vs "46.8").
const value = Math.fround(parsed);
const d = parseInt(decimalDigit, 10);
const valStr = isNaN(d) ? value.toFixed(1) : value.toFixed(d);
return { passFail, valStr, value };

View File

@@ -0,0 +1 @@
["DSCA30-01","DSCA30-02","DSCA30-03","DSCA30-06","DSCA30-07","DSCA30-08","DSCA30-08C","DSCA30-09","DSCA30-09C","DSCA30-1944","DSCA30-1945","DSCA30-1946","DSCA31-02","DSCA31-03","DSCA31-06","DSCA31-07","DSCA31-11","DSCA31-12","DSCA31-1273","DSCA31-12C","DSCA31-13","DSCA31-13C","DSCA31-15","DSCA31-1918","DSCA32-01","DSCA32-01C","DSCA32-01E","DSCA34-01","DSCA34-02C","DSCA34-04","DSCA34-04C","DSCA34-05","DSCA34-05C","DSCA34-1858","DSCA36-01","DSCA36-02","DSCA36-03","DSCA36-04","DSCA36-04C","DSCA36-1949","DSCA38-02","DSCA38-03","DSCA38-07","DSCA38-08C","DSCA38-09","DSCA38-09E","DSCA38-12C","DSCA38-12E","DSCA38-1468","DSCA38-1544","DSCA38-15C","DSCA38-16","DSCA38-16C","DSCA38-18C","DSCA38-19","DSCA40-03","DSCA40-05","DSCA40-05C","DSCA40-06","DSCA40-1951","DSCA40-1952","DSCA41-01","DSCA41-02","DSCA41-03","DSCA41-05C","DSCA41-06","DSCA41-09","DSCA41-13","DSCA41-14","DSCA41-15","DSCA41-15E","DSCA42-01","DSCA42-01C","DSCA42-02","DSCA43-10","DSCA43-20E","DSCA47E-08C","DSCA47J-01C","DSCA47J-03","DSCA47K-05","DSCA47K-13","DSCA47K-14","DSCA47N-15","DSCA47T-06","DSCA47T-1928"]

View File

@@ -1 +0,0 @@
["DSCA30-01","DSCA30-03","DSCA30-06","DSCA30-07","DSCA30-08","DSCA30-08C","DSCA30-09","DSCA30-09C","DSCA30-1944","DSCA30-1945","DSCA30-1946","DSCA31-02","DSCA31-03","DSCA31-06","DSCA31-07","DSCA31-11","DSCA31-12","DSCA31-1273","DSCA31-13","DSCA31-13C","DSCA31-15","DSCA32-01","DSCA32-01C","DSCA32-01E","DSCA34-01","DSCA34-02C","DSCA34-04","DSCA34-04C","DSCA34-05","DSCA34-1858","DSCA36-01","DSCA36-02","DSCA36-03","DSCA36-04","DSCA36-04C","DSCA36-1949","DSCA38-03","DSCA38-09E","DSCA38-1544","DSCA38-15C","DSCA38-16","DSCA38-18C","DSCA40-03","DSCA40-05","DSCA40-05C","DSCA40-06","DSCA40-1951","DSCA40-1952","DSCA41-01","DSCA41-02","DSCA41-03","DSCA41-05C","DSCA41-06","DSCA41-09","DSCA41-13","DSCA41-14","DSCA41-15","DSCA41-15E","DSCA42-01","DSCA42-01C","DSCA42-02","DSCA47E-08C","DSCA47J-01C","DSCA47J-03","DSCA47K-05","DSCA47K-13","DSCA47K-14","DSCA47T-1928"]

View File

@@ -1,5 +1,27 @@
# DSCA Datasheet Fix 2 — STAGE 2 wire-in, STAGE 3 validator, publish of 68 clean models
## Update: 07:23 PT — rounding-mode fix (QB single-precision) + re-publish of 85 clean models
Fixed the rounding-mode issue behind the 26 last-digit-diff models. Root cause: the DOS
QuickBASIC computed/stored values as single-precision floats, so half-boundary rounding follows
single, not double, precision. `formatMeasuredExact` now applies `Math.fround(value)` before
`toFixed`. Verified each boundary case against the staged golden (9.9995 -> "10.000", 46.85 ->
"46.8", .45 -> "0.4", 3.3325 -> "3.332").
STAGE 3 re-validation: FINAL-TEST CLEAN models **68 -> 85** (+17), mismatches 26 -> 9, cert matches
2123 -> 2206. Zero regression — every remaining dirty model was already dirty; no clean model flipped.
Re-pushed all 85 clean models (37,168 PASS serials): **updated=6054 unchanged=28949 created=0
errors=0 skipped=2165**. Live site is now fround-correct across 85 DSCA models (the 6,054 updates =
boundary corrections in the original 68 + the 17 newly-clean models).
The 9 still-dirty models are NOT rounding: 4 (DSCA38-05/-1793/-19C/-19E) are Supply Current retest
data-vintage (staged .TXT predates the DB record — not a render bug); 5 (DSCA39-01/02/05/07/1950)
are the STAGE 1 footer-note artifact ("Standard output load..." mis-captured as a truncated row).
A renderer-side fix for the footer note was attempted but regressed 24 clean current-output `-C`
models (the note placement differs per golden), so it was reverted — this needs a targeted STAGE 1
extractor fix, not a renderer change. Commit `14ee61dc`.
## User
- **User:** Mike Swanson (mike)
- **Machine:** AD2

View File

@@ -6,125 +6,178 @@
<title>Dataforth · TestDataDB</title>
<style>
:root{
--bg:#f8fafc; --surface:#ffffff; --border:#e2e8f0; --border-strong:#cbd5e1;
--ink:#0f172a; --ink-2:#475569; --ink-3:#94a3b8; --accent:#1e40af; --accent-soft:#eff6ff;
--pass-bg:#dcfce7; --pass-ink:#166534; --fail-bg:#fee2e2; --fail-ink:#991b1b;
--hover:#f1f5f9; --sel:#e0e7ff; --mono:ui-monospace,"SFMono-Regular",Consolas,"Liberation Mono",monospace;
--bg:#f6f8fa; --surface:#ffffff; --border:#e3e8ee; --border-strong:#cbd5e1;
--ink:#0f172a; --ink-2:#475569; --ink-3:#94a3b8; --accent:#1e40af; --accent-soft:#eef4ff;
--pass-bg:#dcfce7; --pass-ink:#15803d; --fail-bg:#fee2e2; --fail-ink:#b91c1c;
--hover:#f1f5f9; --sel:#e0e7ff; --desk:#e7ecf1;
--mono:ui-monospace,"SFMono-Regular",Consolas,"Liberation Mono",monospace;
--sans:Inter,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
--r:6px; --insp:480px;
}
*{box-sizing:border-box}
html,body{height:100%;margin:0}
body{font-family:var(--sans);font-size:14px;color:var(--ink);background:var(--bg);
display:grid;grid-template-rows:auto 1fr;overflow:hidden}
body{font-family:var(--sans);font-size:13.5px;color:var(--ink);background:var(--bg);
display:grid;grid-template-rows:auto 1fr;overflow:hidden;-webkit-font-smoothing:antialiased}
button,input,select{font-family:inherit}
:focus-visible{outline:none;box-shadow:0 0 0 2px var(--surface),0 0 0 4px var(--accent)}
/* ---------- header ---------- */
header{display:flex;align-items:center;gap:16px;padding:0 16px;height:52px;
background:var(--surface);border-bottom:1px solid var(--border)}
.brand{font-weight:700;letter-spacing:-.01em;white-space:nowrap}
.brand span{color:var(--accent)}
.omni{flex:1;position:relative}
.omni input{width:100%;height:36px;padding:0 12px 0 32px;font-size:14px;font-family:var(--sans);
border:1px solid var(--border-strong);border-radius:6px;background:var(--bg);color:var(--ink)}
.omni input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft);background:#fff}
.omni .ic{position:absolute;left:10px;top:9px;color:var(--ink-3);font-size:15px}
.omni .route{position:absolute;right:10px;top:9px;font-size:11px;color:var(--ink-3);font-family:var(--mono)}
header{display:flex;align-items:center;gap:14px;padding:0 14px;height:50px;
background:var(--surface);border-bottom:1px solid var(--border);position:relative;z-index:30}
.brand{font-weight:700;letter-spacing:-.01em;white-space:nowrap;font-size:14.5px}
.brand b{color:var(--accent);font-weight:700}
.menubtn{display:none;border:1px solid var(--border);background:none;border-radius:var(--r);height:32px;width:34px;cursor:pointer;color:var(--ink-2)}
.omni{flex:1;position:relative;max-width:780px}
.omni .field{display:flex;align-items:center;height:38px;border:1px solid var(--border-strong);border-radius:8px;background:var(--bg);padding:0 8px 0 30px;position:relative}
.omni .field:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);background:#fff}
.omni .ic{position:absolute;left:9px;color:var(--ink-3);font-size:15px;line-height:1}
.omni input{flex:1;height:100%;border:0;background:none;font-size:14px;color:var(--ink);outline:none}
.mode{font-size:10.5px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--accent);
background:var(--accent-soft);border:1px solid #dbeafe;border-radius:5px;padding:2px 7px;cursor:pointer;white-space:nowrap}
.mode:hover{background:#dbeafe}
.history{position:absolute;top:44px;left:0;right:0;background:#fff;border:1px solid var(--border);border-radius:8px;
box-shadow:0 8px 24px rgba(15,23,42,.12);padding:5px;display:none;z-index:40}
.history.show{display:block}
.history .hl{font-size:10.5px;text-transform:uppercase;letter-spacing:.05em;color:var(--ink-3);padding:5px 8px}
.history button{display:flex;align-items:center;gap:8px;width:100%;text-align:left;border:0;background:none;
border-radius:5px;padding:6px 8px;font-size:13px;cursor:pointer;color:var(--ink);font-family:var(--mono)}
.history button:hover{background:var(--hover)}
.badge{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--ink-2);white-space:nowrap}
.dot{width:8px;height:8px;border-radius:50%;background:#22c55e;box-shadow:0 0 0 3px #22c55e22}
.hbtn{font:inherit;font-size:12px;color:var(--ink-2);background:none;border:1px solid var(--border);
border-radius:6px;height:30px;padding:0 10px;cursor:pointer}
.hbtn{font-size:12.5px;color:var(--ink-2);background:none;border:1px solid var(--border);border-radius:var(--r);height:32px;padding:0 11px;cursor:pointer}
.hbtn:hover{background:var(--hover)}
/* ---------- stats popover ---------- */
.pop{position:absolute;top:46px;right:12px;width:340px;background:#fff;border:1px solid var(--border);
border-radius:10px;box-shadow:0 12px 34px rgba(15,23,42,.16);padding:16px;display:none;z-index:50}
.pop.show{display:block}
.pop h4{margin:0 0 2px;font-size:13px}
.pop .big{font-size:26px;font-weight:700;letter-spacing:-.02em;font-variant-numeric:tabular-nums}
.pop .sub{font-size:11.5px;color:var(--ink-3)}
.pop .bar{display:flex;height:10px;border-radius:5px;overflow:hidden;margin:12px 0 6px;background:#eef2f6}
.pop .bar i{display:block}
.pop .legend{display:flex;gap:14px;font-size:11.5px;color:var(--ink-2)}
.pop .legend span::before{content:"";display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:5px;vertical-align:middle}
.pop .lt{display:flex;justify-content:space-between;font-size:12px;padding:3px 0;border-top:1px solid var(--border);font-family:var(--mono)}
.pop .lt:first-of-type{border-top:0;margin-top:8px}
/* ---------- layout ---------- */
main{display:grid;grid-template-columns:232px 1fr var(--insp,460px);min-height:0;height:100%}
main{display:grid;grid-template-columns:236px 1fr var(--insp);min-height:0;height:100%}
.pane{min-height:0;overflow:auto}
/* ---------- presets ---------- */
.presets{display:flex;flex-direction:column;gap:5px}
.preset{display:flex;align-items:center;gap:7px;width:100%;text-align:left;font:inherit;font-size:12.5px;
height:30px;padding:0 9px;border:1px solid var(--border);border-radius:6px;background:#fff;color:var(--ink);cursor:pointer}
.preset:hover{background:var(--accent-soft);border-color:#bfdbfe}
.preset .pi{width:15px;text-align:center;color:var(--ink-3)}
.preset.fam{display:inline-flex;width:auto;height:26px;font-family:var(--mono);font-size:11.5px;padding:0 8px}
.preset.fam:disabled,.preset:disabled{opacity:.5;cursor:not-allowed;background:#fff}
.famrow{display:flex;flex-wrap:wrap;gap:5px;margin-top:6px}
/* ---------- filter rail ---------- */
.rail{border-right:1px solid var(--border);background:var(--surface);padding:14px}
.rail h3{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-3);margin:18px 0 8px;font-weight:600}
.rail{border-right:1px solid var(--border);background:var(--surface);padding:14px;overflow-y:auto}
.rail h3{font-size:10.5px;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-3);margin:18px 0 8px;font-weight:600}
.rail h3:first-child{margin-top:0}
.seg{display:flex;border:1px solid var(--border-strong);border-radius:6px;overflow:hidden}
.seg button{flex:1;font:inherit;font-size:12px;height:30px;border:0;background:#fff;color:var(--ink-2);cursor:pointer}
.seg button+button{border-left:1px solid var(--border)}
.seg button.on{background:var(--accent);color:#fff}
.presets{display:flex;flex-direction:column;gap:5px}
.preset{display:flex;align-items:center;gap:8px;width:100%;text-align:left;font-size:12.5px;height:31px;padding:0 9px;
border:1px solid var(--border);border-radius:var(--r);background:#fff;color:var(--ink);cursor:pointer;transition:background .08s}
.preset:hover:not(:disabled){background:var(--accent-soft);border-color:#bfdbfe}
.preset .pi{width:14px;text-align:center;color:var(--accent);font-size:12px}
.preset:disabled{opacity:.45;cursor:not-allowed}
.famrow{display:flex;flex-wrap:wrap;gap:5px;margin-top:7px}
.fam{font-family:var(--mono);font-size:11.5px;height:26px;padding:0 9px;border:1px solid var(--border);border-radius:5px;background:#fff;color:var(--ink-2);cursor:pointer}
.fam:hover{background:var(--accent-soft);border-color:#bfdbfe;color:var(--accent)}
.rail label{display:block;font-size:11px;color:var(--ink-3);margin:8px 0 3px}
.rail input,.rail select{width:100%;height:30px;font:inherit;font-size:13px;padding:0 8px;
border:1px solid var(--border-strong);border-radius:6px;background:#fff;color:var(--ink)}
.rail input:focus,.rail select:focus{outline:none;border-color:var(--accent)}
.reset{margin-top:18px;width:100%;height:30px;font:inherit;font-size:12px;background:none;
border:1px solid var(--border);border-radius:6px;color:var(--ink-2);cursor:pointer}
.rail input,.rail select{width:100%;height:31px;font-size:13px;padding:0 8px;border:1px solid var(--border-strong);border-radius:var(--r);background:#fff;color:var(--ink)}
.rail input:focus,.rail select:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.chips{display:flex;gap:5px;margin-top:7px}
.chip{font-size:11px;height:24px;padding:0 8px;border:1px solid var(--border);border-radius:20px;background:#fff;color:var(--ink-2);cursor:pointer}
.chip:hover{background:var(--hover)}
.reset{margin-top:18px;width:100%;height:31px;font-size:12px;background:none;border:1px solid var(--border);border-radius:var(--r);color:var(--ink-2);cursor:pointer}
.reset:hover{background:var(--hover)}
/* ---------- results ---------- */
.results{display:grid;grid-template-rows:auto 1fr auto;min-height:0;background:var(--surface)}
.results{display:grid;grid-template-rows:auto auto 1fr auto;min-height:0;background:var(--surface)}
.rtoolbar{display:flex;align-items:center;gap:12px;padding:8px 12px;border-bottom:1px solid var(--border);font-size:12px;color:var(--ink-2)}
.rtoolbar .count{font-weight:600;color:var(--ink)}
.rtoolbar .count{font-weight:700;color:var(--ink);font-variant-numeric:tabular-nums}
.rtoolbar .sp{flex:1}
.rtoolbar a,.rtoolbar select{font:inherit;font-size:12px;color:var(--accent);text-decoration:none}
.rtoolbar select{color:var(--ink-2);border:1px solid var(--border);border-radius:5px;height:26px}
.rtoolbar a{font-size:12px;color:var(--accent);text-decoration:none}
.rtoolbar a:hover{text-decoration:underline}
.rtoolbar select{font-size:12px;color:var(--ink-2);border:1px solid var(--border);border-radius:5px;height:27px;background:#fff}
.selbar{display:none;align-items:center;gap:10px;padding:6px 12px;background:var(--accent-soft);border-bottom:1px solid #dbeafe;font-size:12px;color:var(--accent)}
.selbar.show{display:flex}
.selbar button{font-size:12px;height:26px;padding:0 9px;border:1px solid #bfdbfe;border-radius:var(--r);background:#fff;color:var(--accent);cursor:pointer}
.selbar button:disabled{opacity:.5;cursor:not-allowed}
.twrap{overflow:auto;min-height:0}
table{width:100%;border-collapse:collapse;font-size:13px}
thead th{position:sticky;top:0;background:#f8fafc;border-bottom:1px solid var(--border-strong);
text-align:left;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--ink-3);
font-weight:600;padding:7px 10px;white-space:nowrap;z-index:1}
thead th{position:sticky;top:0;background:#f7f9fb;border-bottom:1px solid var(--border-strong);text-align:left;
font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--ink-3);font-weight:600;padding:7px 10px;white-space:nowrap;z-index:1;user-select:none}
thead th.s{cursor:pointer}
thead th.s:hover{color:var(--ink-2)}
thead th .arr{color:var(--accent);font-size:9px;margin-left:3px}
thead th.ck{width:30px;padding-left:12px}
tbody td{padding:5px 10px;border-bottom:1px solid var(--border);white-space:nowrap}
tbody td.ck{padding-left:12px}
tbody tr{cursor:pointer}
tbody tr:hover{background:var(--hover)}
tbody tr.sel{background:var(--sel)}
tbody tr.sel td:first-child{box-shadow:inset 3px 0 0 var(--accent)}
.mono{font-family:var(--mono);font-size:12.5px}
.pill{display:inline-block;font-size:11px;font-weight:600;padding:1px 8px;border-radius:4px;font-family:var(--mono)}
.mono{font-family:var(--mono);font-size:12.5px;font-variant-numeric:tabular-nums}
.pill{display:inline-block;font-size:10.5px;font-weight:700;padding:1px 8px;border-radius:4px;font-family:var(--mono)}
.pill.PASS{background:var(--pass-bg);color:var(--pass-ink)}
.pill.FAIL{background:var(--fail-bg);color:var(--fail-ink)}
.pager{display:flex;align-items:center;gap:10px;padding:8px 12px;border-top:1px solid var(--border);font-size:12px;color:var(--ink-2)}
.pager button{font:inherit;font-size:12px;height:28px;padding:0 10px;border:1px solid var(--border-strong);
border-radius:6px;background:#fff;cursor:pointer;color:var(--ink)}
.web{font-size:13px}
.pager{display:flex;align-items:center;gap:10px;padding:7px 12px;border-top:1px solid var(--border);font-size:12px;color:var(--ink-2)}
.pager button{font-size:12px;height:28px;padding:0 11px;border:1px solid var(--border-strong);border-radius:var(--r);background:#fff;cursor:pointer;color:var(--ink)}
.pager button:disabled{opacity:.4;cursor:default}
.pager .hint{color:var(--ink-3)}
.state{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:40px 20px;color:var(--ink-3);text-align:center}
.state .em{font-size:26px;opacity:.6}
.state.err{color:var(--fail-ink)}
.state button{margin-top:4px;font-size:12px;height:28px;padding:0 12px;border:1px solid var(--border-strong);border-radius:var(--r);background:#fff;cursor:pointer;color:var(--ink)}
.skel{height:13px;margin:8px 10px;border-radius:4px;background:linear-gradient(90deg,#eef2f6,#e2e8f0,#eef2f6);background-size:200% 100%;animation:sh 1.1s infinite}
@keyframes sh{0%{background-position:200% 0}100%{background-position:-200% 0}}
/* ---------- inspector ---------- */
.insp{position:relative;border-left:1px solid var(--border);background:var(--surface);display:grid;grid-template-rows:auto auto 1fr;min-height:0}
.resizer{position:absolute;left:-3px;top:0;bottom:0;width:7px;cursor:col-resize;z-index:5}
.resizer:hover,.resizer.drag{background:linear-gradient(90deg,transparent,var(--accent) 45%,var(--accent) 55%,transparent)}
body.resizing{cursor:col-resize;user-select:none}
.insp .meta{padding:12px 14px;border-bottom:1px solid var(--border)}
.insp .meta .sn{font-family:var(--mono);font-size:16px;font-weight:700}
.insp .meta dl{display:grid;grid-template-columns:auto 1fr;gap:3px 12px;margin:10px 0 0;font-size:12.5px}
.insp .meta dt{color:var(--ink-3)}
.insp .meta dd{margin:0;font-family:var(--mono)}
.insp .acts{display:flex;gap:6px;flex-wrap:wrap;padding:10px 14px;border-bottom:1px solid var(--border)}
.insp .acts a,.insp .acts button{font:inherit;font-size:12px;height:28px;padding:0 10px;border:1px solid var(--border-strong);
border-radius:6px;background:#fff;color:var(--ink);text-decoration:none;display:inline-flex;align-items:center;cursor:pointer}
.insp .acts .pri{background:var(--accent);border-color:var(--accent);color:#fff}
.insp iframe{width:100%;height:100%;border:0;background:#fff}
.empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--ink-3);gap:8px;font-size:13px;text-align:center;padding:20px}
.skel{height:24px;margin:6px 10px;border-radius:4px;background:linear-gradient(90deg,#f1f5f9,#e8edf3,#f1f5f9);background-size:200% 100%;animation:sh 1.1s infinite}
@keyframes sh{0%{background-position:200% 0}100%{background-position:-200% 0}}
kbd{font-family:var(--mono);font-size:11px;background:#f1f5f9;border:1px solid var(--border-strong);border-bottom-width:2px;border-radius:4px;padding:0 5px}
.meta{padding:12px 14px;border-bottom:1px solid var(--border)}
.meta .sn{font-family:var(--mono);font-size:16px;font-weight:700;letter-spacing:-.01em}
.meta dl{display:grid;grid-template-columns:auto 1fr;gap:3px 12px;margin:10px 0 0;font-size:12.5px}
.meta dt{color:var(--ink-3)}
.meta dd{margin:0;font-family:var(--mono)}
.acts{display:flex;gap:6px;flex-wrap:wrap;padding:10px 14px;border-bottom:1px solid var(--border)}
.acts a,.acts button{font-size:12px;height:29px;padding:0 11px;border:1px solid var(--border-strong);border-radius:var(--r);
background:#fff;color:var(--ink);text-decoration:none;display:inline-flex;align-items:center;gap:5px;cursor:pointer}
.acts a:hover,.acts button:hover:not(:disabled){background:var(--hover)}
.acts .pri{background:var(--accent);border-color:var(--accent);color:#fff}
.acts .pri:hover{background:#1c3aa9}
.acts button:disabled{opacity:.5;cursor:not-allowed}
.viewer{background:var(--desk);overflow:auto;padding:14px;position:relative}
.viewer iframe{display:block;width:100%;min-height:100%;border:1px solid var(--border);border-radius:5px;background:#fff;
box-shadow:0 1px 5px rgba(15,23,42,.10)}
kbd{font-family:var(--mono);font-size:11px;background:#f1f5f9;border:1px solid var(--border-strong);border-bottom-width:2px;border-radius:4px;padding:0 5px;color:var(--ink-2)}
/* ---------- responsive ---------- */
@media (max-width:1180px){
main{grid-template-columns:1fr var(--insp)}
.rail{position:absolute;top:50px;bottom:0;left:0;width:250px;z-index:20;box-shadow:6px 0 20px rgba(15,23,42,.12);transform:translateX(-104%);transition:transform .18s}
body.rail-open .rail{transform:none}
.menubtn{display:inline-flex;align-items:center;justify-content:center}
}
@media (max-width:820px){ main{grid-template-columns:1fr} .insp{position:absolute;top:50px;bottom:0;right:0;width:min(94vw,520px);z-index:18;box-shadow:-8px 0 24px rgba(15,23,42,.14)} .insp:not(.open){display:none} .resizer{display:none} }
</style>
</head>
<body>
<header>
<div class="brand">Dataforth <span>·</span> TestDataDB</div>
<button class="menubtn" id="menu" title="Filters"></button>
<div class="brand">Dataforth <b>·</b> TestDataDB</div>
<div class="omni">
<span class="ic">🔍</span>
<input id="omni" autocomplete="off" spellcheck="false"
placeholder="Search serial, model, or text… (press / to focus)">
<span class="route" id="route"></span>
<div class="field">
<span class="ic">🔍</span>
<input id="omni" autocomplete="off" spellcheck="false" placeholder="Search serial, model, or text… ( / to focus, s: m: t: to force )">
<button class="mode" id="mode" title="Click to force the search mode">auto</button>
</div>
<div class="history" id="history"></div>
</div>
<div class="badge"><span class="dot"></span><span id="ingest">loading…</span></div>
<button class="hbtn" id="statsBtn" title="Database statistics">Stats</button>
<button class="hbtn" id="statsBtn">Stats</button>
<div class="pop" id="statsPop"></div>
</header>
<main>
<!-- filter rail -->
<aside class="rail pane">
<aside class="rail pane" id="rail">
<h3>Quick searches</h3>
<div class="presets" id="presets"></div>
<div class="famrow" id="families"></div>
<h3>Test date</h3>
<div class="chips" id="dateChips"></div>
<label>From</label><input type="date" id="fFrom">
<label>To</label><input type="date" id="fTo">
<h3>Model</h3>
@@ -134,130 +187,195 @@
<select id="fStation"><option value="">any station</option></select>
<h3>Log type</h3>
<select id="fLog"><option value="">any log</option></select>
<button class="reset" id="reset">Reset filters</button>
<button class="reset" id="reset">Reset all</button>
</aside>
<!-- results -->
<section class="results">
<div class="rtoolbar">
<span class="count" id="count"></span><span>results</span>
<span class="sp"></span>
<label>rows
<select id="pageSize"><option>25</option><option selected>50</option><option>100</option></select>
</label>
<a id="exportCsv" href="#">Export CSV ↓</a>
<label>rows <select id="pageSize"><option>25</option><option selected>50</option><option>100</option></select></label>
<a id="exportCsv" href="#" title="Export current filtered results">Export CSV ↓</a>
</div>
<div class="selbar" id="selbar">
<span id="selcount">0 selected</span>
<button id="copySel">Copy serials</button>
<button id="repushSel" disabled title="Re-publish to the website — needs an API endpoint">Re-push ▴</button>
<span class="sp" style="flex:1"></span>
<button id="selclear" style="border:0;background:none">clear</button>
</div>
<div class="twrap" id="twrap">
<table>
<thead><tr>
<th>Serial</th><th>Model</th><th>Test Date</th><th>Stn</th><th>Log</th><th>Result</th><th>Web</th>
<th class="ck"><input type="checkbox" id="ckAll" title="select page"></th>
<th class="s" data-s="serial_number">Serial</th>
<th class="s" data-s="model_number">Model</th>
<th class="s" data-s="test_date">Test Date</th>
<th>Stn</th><th>Log</th>
<th class="s" data-s="overall_result">Result</th>
<th title="Published to public website">Web</th>
</tr></thead>
<tbody id="rows"></tbody>
</table>
<div id="state"></div>
</div>
<div class="pager">
<button id="prev"> Prev</button>
<span id="pginfo">Page 1</span>
<button id="next">Next </button>
<span class="sp" style="flex:1"></span>
<span style="color:var(--ink-3)"><kbd>/</kbd> search · <kbd></kbd><kbd></kbd> rows · <kbd></kbd> open · <kbd>Esc</kbd> clear</span>
<span class="hint"><kbd>/</kbd> search · <kbd></kbd><kbd></kbd> rows · <kbd></kbd> open · <kbd>Esc</kbd> back</span>
</div>
</section>
<!-- inspector -->
<aside class="insp">
<aside class="insp" id="insp">
<div class="resizer" id="resizer" title="Drag to resize"></div>
<div class="meta" id="meta"><div class="empty" style="height:auto;padding:8px 0">No record selected</div></div>
<div class="meta" id="meta"></div>
<div class="acts" id="acts" style="display:none"></div>
<div id="viewer"><div class="empty">Search a serial number, then select a row.<br>The calibration certificate renders here.</div></div>
<div class="viewer" id="viewer"></div>
</aside>
</main>
<script>
const API = ''; // same-origin; relative /api/...
const $ = id => document.getElementById(id);
const state = { q:'', serial:'', model:'', result:'', station:'', logtype:'', from:'', to:'', size:50, page:0, total:0, selected:null, rows:[] };
let timer=null;
const API='';
const $=id=>document.getElementById(id);
const state={q:'',serial:'',model:'',result:'',station:'',logtype:'',from:'',to:'',size:50,page:0,total:0,
selected:null,rows:[],sort:'',dir:'desc',checks:new Set(),force:''};
let timer=null, certTimer=null;
const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
const fmtDate=d=>d?String(d).slice(0,10):'';
const isoD=d=>d.toISOString().slice(0,10);
// ---- omni-search routing: serial-ish -> serial, model-ish -> model, else full-text ----
function routeOmni(v){
v=v.trim(); state.q=state.serial=state.model='';
if(!v){ $('route').textContent=''; return; }
if(/\s/.test(v)){ state.q=v; $('route').textContent='text'; }
else if(/^(scm|dsc|[5780]b|pwr|vas)/i.test(v)){ state.model=v; $('route').textContent='model'; }
else { state.serial=v; $('route').textContent='serial'; }
/* ---------- omni routing (with s:/m:/t: overrides + encoded serials) ---------- */
function routeOmni(raw){
state.q=state.serial=state.model=''; let v=(raw||'').trim(), mode=state.force;
const pm=v.match(/^([smt]):\s*(.*)$/i); if(pm){ mode={s:'serial',m:'model',t:'text'}[pm[1].toLowerCase()]; v=pm[2]; }
if(!v){ $('mode').textContent=state.force||'auto'; return; }
if(!mode){
if(/\s/.test(v)) mode='text';
else if(/^(scm|dsc|[57]b|8b|pwr|vas)/i.test(v)) mode='model';
else mode='serial'; // numeric AND encoded (A243-1) serials land here
}
if(mode==='model') state.model=v; else if(mode==='text') state.q=v; else state.serial=v;
$('mode').textContent=mode;
}
function params(extra){
$('mode').onclick=()=>{ const seq=['auto','serial','model','text']; state.force=seq[(seq.indexOf(state.force||'auto')+1)%4]; if(state.force==='auto')state.force=''; routeOmni($('omni').value); state.page=0; search(); };
/* ---------- query / search ---------- */
function params(forExport){
const p=new URLSearchParams();
for(const k of ['q','serial','model','result','station','logtype','from','to']) if(state[k]) p.set(k,state[k]);
if(extra!=='export'){ p.set('limit',state.size); p.set('offset',state.page*state.size); }
if(state.sort){ p.set('sort',state.sort); p.set('dir',state.dir); } // needs API; harmless if ignored
if(!forExport){ p.set('limit',state.size); p.set('offset',state.page*state.size); }
return p;
}
function syncUrl(){
const p=params(); if(state.selected) p.set('selected',state.selected);
history.replaceState(null,'', '?'+p.toString());
}
function fmtDate(d){ return d? String(d).slice(0,10) : ''; }
function syncUrl(){ const p=params(); if(state.selected)p.set('selected',state.selected); history.replaceState(null,'','?'+p.toString()); }
function showState(html,cls){ $('state').className='state '+(cls||''); $('state').innerHTML=html; $('rows').innerHTML=''; }
async function search(){
$('rows').innerHTML = Array.from({length:8}).map(()=>'<tr><td colspan="7"><div class="skel"></div></td></tr>').join('');
$('state').innerHTML=''; $('rows').innerHTML=Array.from({length:9}).map(()=>'<tr><td colspan="8"><div class="skel"></div></td></tr>').join('');
try{
const r = await fetch(API+'/api/search?'+params().toString());
const data = await r.json();
state.rows = data.records||[]; state.total = data.total||0;
renderRows();
}catch(e){ $('rows').innerHTML='<tr><td colspan="7" style="color:var(--fail-ink);padding:14px">Search failed: '+e.message+'</td></tr>'; }
$('exportCsv').href = API+'/api/export?'+params('export').toString();
syncUrl();
const r=await fetch(API+'/api/search?'+params().toString());
if(!r.ok) throw new Error('HTTP '+r.status);
const d=await r.json(); state.rows=d.records||[]; state.total=d.total||0; renderRows();
}catch(e){ showState('<div class="em">⚠</div>Search failed — '+esc(e.message)+'<button onclick="search()">Retry</button>','err'); $('count').textContent='—'; }
$('exportCsv').href=API+'/api/export?'+params(true).toString(); syncUrl();
}
function renderRows(){
$('count').textContent = state.total.toLocaleString();
const pages = Math.max(1, Math.ceil(state.total/state.size));
$('pginfo').textContent = 'Page '+(state.page+1)+' of '+pages.toLocaleString();
$('prev').disabled = state.page<=0; $('next').disabled = state.page>=pages-1;
if(!state.rows.length){ $('rows').innerHTML='<tr><td colspan="7" style="color:var(--ink-3);padding:18px">No records match. Try clearing a filter.</td></tr>'; return; }
$('rows').innerHTML = state.rows.map(r=>{
const web = r.api_uploaded_at ? '●' : '○';
$('count').textContent=state.total.toLocaleString();
const pages=Math.max(1,Math.ceil(state.total/state.size));
$('pginfo').textContent='Page '+(state.page+1)+' of '+pages.toLocaleString();
$('prev').disabled=state.page<=0; $('next').disabled=state.page>=pages-1;
if(!state.rows.length){ showState('<div class="em">∅</div>No records match.'+(activeFilters()?'<button onclick="document.getElementById(\'reset\').click()">Clear filters</button>':'')); return; }
$('state').innerHTML='';
$('rows').innerHTML=state.rows.map(r=>{
const ck=state.checks.has(String(r.id))?'checked':'';
return `<tr data-id="${r.id}">
<td class="mono" style="font-weight:600">${r.serial_number||''}</td>
<td class="mono">${r.model_number||''}</td>
<td class="ck"><input type="checkbox" data-ck="${r.id}" ${ck}></td>
<td class="mono" style="font-weight:600">${esc(r.serial_number)}</td>
<td class="mono">${esc(r.model_number)}</td>
<td class="mono">${fmtDate(r.test_date)}</td>
<td class="mono">${(r.test_station||'').replace(/^TS-/,'')}</td>
<td class="mono" style="color:var(--ink-3)">${(r.log_type||'').replace(/LOG$/,'')}</td>
<td><span class="pill ${r.overall_result}">${r.overall_result||''}</span></td>
<td style="color:${r.api_uploaded_at?'var(--pass-ink)':'var(--ink-3)'};text-align:center" title="${r.api_uploaded_at?'published to website':'not published'}">${web}</td>
<td class="mono">${esc((r.test_station||'').replace(/^TS-/,''))}</td>
<td class="mono" style="color:var(--ink-3)">${esc((r.log_type||'').replace(/LOG$/,''))}</td>
<td><span class="pill ${r.overall_result}">${esc(r.overall_result)}</span></td>
<td class="web" style="text-align:center;color:${r.api_uploaded_at?'var(--pass-ink)':'var(--ink-3)'}" title="${r.api_uploaded_at?'published':'not published'}">${r.api_uploaded_at?'●':'○'}</td>
</tr>`;}).join('');
[...$('rows').children].forEach(tr=>tr.onclick=()=>select(tr.dataset.id));
// auto-select first (the serial fast-path) if a serial query
if(state.serial && state.rows[0]) select(state.rows[0].id);
else if(state.selected){ const tr=[...$('rows').children].find(t=>t.dataset.id==state.selected); if(tr) tr.classList.add('sel'); }
[...$('rows').children].forEach(tr=>{
tr.onclick=e=>{ if(e.target.dataset.ck!==undefined)return; select(tr.dataset.id); };
const cb=tr.querySelector('[data-ck]'); cb.onclick=e=>{e.stopPropagation(); const id=cb.dataset.ck; cb.checked?state.checks.add(id):state.checks.delete(id); updateSel();};
});
updateSortHeads();
if(state.serial&&state.rows[0]) select(state.rows[0].id,true);
else if(state.selected){ const tr=[...$('rows').children].find(t=>t.dataset.id==state.selected); if(tr)tr.classList.add('sel'); }
}
function select(id){
state.selected = id;
[...$('rows').children].forEach(t=>t.classList.toggle('sel', t.dataset.id==id));
const r = state.rows.find(x=>x.id==id); if(!r) return;
$('meta').innerHTML = `<div class="sn">${r.serial_number||''}</div>
<dl><dt>Model</dt><dd>${r.model_number||''}</dd>
function activeFilters(){ return ['serial','model','q','result','station','logtype','from','to'].some(k=>state[k]); }
/* ---------- selection (instant) + cert (lazy) ---------- */
function select(id,auto){
state.selected=id;
[...$('rows').children].forEach(t=>t.classList.toggle('sel',t.dataset.id==id));
const r=state.rows.find(x=>x.id==id); if(!r) return;
$('meta').innerHTML=`<div class="sn">${esc(r.serial_number)}</div>
<dl><dt>Model</dt><dd>${esc(r.model_number)}</dd>
<dt>Date</dt><dd>${fmtDate(r.test_date)}</dd>
<dt>Station</dt><dd>${r.test_station||''}</dd>
<dt>Result</dt><dd><span class="pill ${r.overall_result}">${r.overall_result||''}</span></dd>
<dt>Log</dt><dd>${r.log_type||''}</dd>
${r.work_order?`<dt>WO</dt><dd>${r.work_order}</dd>`:''}
<dt>Station</dt><dd>${esc(r.test_station)}</dd>
<dt>Result</dt><dd><span class="pill ${r.overall_result}">${esc(r.overall_result)}</span></dd>
<dt>Log</dt><dd>${esc(r.log_type)}</dd>
${r.work_order?`<dt>WO</dt><dd>${esc(r.work_order)}</dd>`:''}
<dt>Web</dt><dd>${r.api_uploaded_at?'published '+fmtDate(r.api_uploaded_at):'not published'}</dd></dl>`;
const ds=API+'/api/datasheet/'+id;
$('acts').style.display='flex';
const ds = API+'/api/datasheet/'+id;
$('acts').innerHTML = `<a class="pri" href="${ds}?format=html" target="_blank">Open ↗</a>
<button onclick="document.getElementById('viewer').querySelector('iframe').contentWindow.print()">Print</button>
<a href="${ds}?format=txt" download="${r.serial_number}.txt">TXT</a>
<a href="${ds}?format=html" download="${r.serial_number}.html">HTML</a>`;
$('viewer').innerHTML = '<iframe id="dsframe" title="datasheet"></iframe>';
const _f=document.getElementById('dsframe'); _f.onload=fitCert; _f.src=ds+'?format=html';
$('acts').innerHTML=`<a class="pri" href="${ds}?format=html" target="_blank">Open ↗</a>
<button onclick="printCert()">Print</button>
<a href="${ds}?format=txt" download="${esc(r.serial_number)}.txt">TXT</a>
<a href="${ds}?format=html" download="${esc(r.serial_number)}.html">HTML</a>
<button disabled title="Re-publish this cert — needs a POST /api/publish endpoint">Push to Web ▴</button>`;
$('insp').classList.add('open');
// lazy-load the certificate so fast arrow/typing stays snappy
$('viewer').innerHTML='<div class="state"><div class="skel" style="width:60%"></div></div>';
clearTimeout(certTimer);
certTimer=setTimeout(()=>loadCert(ds),auto?240:60);
syncUrl();
}
function loadCert(ds){
$('viewer').innerHTML='<iframe id="dsframe" title="datasheet"></iframe>';
const f=$('dsframe'); f.onload=()=>{styleCert();fitCert();}; f.src=ds+'?format=html';
}
function styleCert(){ const f=$('dsframe'); try{ const doc=f.contentDocument; if(!doc)return;
if(!doc.getElementById('_inj')){ const s=doc.createElement('style'); s.id='_inj';
s.textContent='html,body{background:#fff!important;margin:0!important}body{padding:22px 26px!important;color:#0f172a}pre{margin:0;font-family:'+getComputedStyle(document.body).getPropertyValue('--mono')+';font-size:12.5px;line-height:1.32}';
(doc.head||doc.documentElement).appendChild(s); }
}catch(e){} }
function fitCert(){ const f=$('dsframe'); if(!f)return; try{ const doc=f.contentDocument; if(!doc)return;
const root=doc.documentElement; root.style.zoom='';
const nat=Math.max(doc.body?doc.body.scrollWidth:0,root.scrollWidth), av=f.clientWidth-2;
root.style.zoom=(nat>av)?Math.max(.45,av/nat):1;
f.style.height=Math.ceil((doc.body?doc.body.scrollHeight:600)*(nat>av?av/nat:1)+4)+'px';
}catch(e){} }
function printCert(){ const f=$('dsframe'); if(f&&f.contentWindow){f.contentWindow.focus();f.contentWindow.print();} }
window.addEventListener('resize',()=>{fitCert();});
// ---- filters wiring ----
function debouncedSearch(){ clearTimeout(timer); timer=setTimeout(()=>{state.page=0;search();},280); }
$('omni').addEventListener('input', e=>{ routeOmni(e.target.value); debouncedSearch(); });
/* ---------- multi-select ---------- */
function updateSel(){ const n=state.checks.size; $('selbar').classList.toggle('show',n>0); $('selcount').textContent=n+' selected';
$('ckAll').checked=n>0&&state.rows.every(r=>state.checks.has(String(r.id))); }
$('ckAll').onclick=e=>{ state.rows.forEach(r=>{ e.target.checked?state.checks.add(String(r.id)):state.checks.delete(String(r.id)); });
[...$('rows').querySelectorAll('[data-ck]')].forEach(c=>c.checked=e.target.checked); updateSel(); };
$('copySel').onclick=()=>{ const sns=state.rows.filter(r=>state.checks.has(String(r.id))).map(r=>r.serial_number).join('\n');
navigator.clipboard&&navigator.clipboard.writeText(sns); $('copySel').textContent='Copied ✓'; setTimeout(()=>$('copySel').textContent='Copy serials',1200); };
$('selclear').onclick=()=>{ state.checks.clear(); [...$('rows').querySelectorAll('[data-ck]')].forEach(c=>c.checked=false); $('ckAll').checked=false; updateSel(); };
/* ---------- sort ---------- */
function updateSortHeads(){ [...document.querySelectorAll('thead th.s')].forEach(th=>{
const on=th.dataset.s===state.sort; th.querySelector('.arr')?.remove();
if(on){ const a=document.createElement('span'); a.className='arr'; a.textContent=state.dir==='asc'?'▲':'▼'; th.appendChild(a);} }); }
document.querySelectorAll('thead th.s').forEach(th=>th.onclick=()=>{
const f=th.dataset.s; if(state.sort===f) state.dir=state.dir==='asc'?'desc':'asc'; else {state.sort=f;state.dir=f==='test_date'?'desc':'asc';}
state.page=0; search(); });
/* ---------- filters / paging ---------- */
function debouncedSearch(){ clearTimeout(timer); timer=setTimeout(()=>{state.page=0;state.checks.clear();updateSel();search();},280); }
$('omni').addEventListener('input',e=>{routeOmni(e.target.value);debouncedSearch();});
$('omni').addEventListener('focus',()=>{ if(!$('omni').value) showHistory(true); });
$('omni').addEventListener('blur',()=>setTimeout(()=>showHistory(false),150));
$('fFrom').onchange=e=>{state.from=e.target.value;state.page=0;search();};
$('fTo').onchange=e=>{state.to=e.target.value;state.page=0;search();};
$('fModel').onchange=e=>{state.model=e.target.value;state.page=0;search();};
@@ -267,93 +385,102 @@ $('pageSize').onchange=e=>{state.size=+e.target.value;state.page=0;search();};
$('prev').onclick=()=>{if(state.page>0){state.page--;search();$('twrap').scrollTop=0;}};
$('next').onclick=()=>{state.page++;search();$('twrap').scrollTop=0;};
$('reset').onclick=()=>{ ['serial','model','q','result','station','logtype','from','to'].forEach(k=>state[k]='');
$('omni').value='';$('route').textContent='';$('fFrom').value=$('fTo').value=$('fStation').value=$('fLog').value=$('fModel').value='';
state.page=0; search(); };
state.sort='';state.checks.clear();updateSel(); $('omni').value='';$('mode').textContent=state.force||'auto';
$('fFrom').value=$('fTo').value=$('fStation').value=$('fLog').value=$('fModel').value=''; state.page=0; search(); };
$('menu').onclick=()=>document.body.classList.toggle('rail-open');
// ---- cert fit-to-width (same-origin: scale the rendered cert so it never side-scrolls) ----
function fitCert(){
const f=document.getElementById('dsframe'); if(!f) return;
try{ const doc=f.contentDocument; if(!doc) return;
const root=doc.documentElement; root.style.zoom='';
const natural=Math.max(doc.body?doc.body.scrollWidth:0, root.scrollWidth);
const avail=f.clientWidth-12;
root.style.zoom = (natural>avail) ? Math.max(0.45, avail/natural) : 1;
}catch(e){}
}
window.addEventListener('resize', fitCert);
// ---- resizable inspector ----
(function(){ const rz=$('resizer'); let on=false;
rz.addEventListener('mousedown', e=>{ on=true; rz.classList.add('drag'); document.body.classList.add('resizing'); e.preventDefault(); });
window.addEventListener('mousemove', e=>{ if(!on) return;
let w=Math.max(340, Math.min(window.innerWidth-560, window.innerWidth-e.clientX));
document.documentElement.style.setProperty('--insp', w+'px'); });
window.addEventListener('mouseup', ()=>{ if(on){ on=false; rz.classList.remove('drag'); document.body.classList.remove('resizing'); fitCert(); } });
})();
// ---- quick-search presets ----
/* ---------- presets ---------- */
function clearAll(){ ['serial','model','q','result','station','logtype','from','to'].forEach(k=>state[k]='');
$('omni').value='';$('route').textContent='';$('fFrom').value=$('fTo').value=$('fStation').value=$('fLog').value=$('fModel').value=''; }
function applyPreset(fn){ clearAll(); fn(); state.page=0; search(); }
const _iso=d=>d.toISOString().slice(0,10);
$('omni').value='';$('mode').textContent=state.force||'auto';$('fFrom').value=$('fTo').value=$('fStation').value=$('fLog').value=$('fModel').value=''; }
function applyPreset(fn){ clearAll(); fn(); state.page=0; state.checks.clear(); updateSel(); document.body.classList.remove('rail-open'); search(); }
const PRESETS=[
{ic:'◷',label:'Recent',fn:()=>{}},
{ic:'✕',label:'Failures',fn:()=>{state.result='FAIL';}},
{ic:'•',label:'Today',fn:()=>{const t=_iso(new Date());state.from=t;state.to=t;}},
{ic:'7',label:'Last 7 days',fn:()=>{const d=new Date();d.setDate(d.getDate()-7);state.from=_iso(d);}},
{ic:'∷',label:'This year',fn:()=>{state.from=new Date().getFullYear()+'-01-01';}},
{ic:'•',label:'Today',fn:()=>{const t=isoD(new Date());state.from=t;state.to=t;}},
{ic:'7',label:'Last 7 days',fn:()=>{const d=new Date();d.setDate(d.getDate()-7);state.from=isoD(d);}},
];
const SOON=[
{label:'Latest upload batch',why:'needs an upload-time sort param in /api/search'},
{label:'Latest upload batch',why:'needs sort=uploaded in /api/search'},
{label:'Retested units',why:'needs a retest flag in the pipeline'},
{label:'Not yet published',why:'needs a published filter in /api/search'},
];
function renderPresets(){
const host=$('presets'); host.innerHTML='';
PRESETS.forEach(p=>{ const b=document.createElement('button'); b.className='preset';
b.innerHTML='<span class="pi">'+p.ic+'</span>'+p.label; b.onclick=()=>applyPreset(p.fn); host.appendChild(b); });
SOON.forEach(s=>{ const b=document.createElement('button'); b.className='preset'; b.disabled=true; b.title=s.why;
b.innerHTML='<span class="pi">…</span>'+s.label; host.appendChild(b); });
const fam=$('families'); fam.innerHTML='';
['DSCA','8B','5B','7B','SCM5B'].forEach(m=>{ const b=document.createElement('button'); b.className='preset fam';
b.textContent=m; b.onclick=()=>applyPreset(()=>{state.model=m;$('fModel').value=m;}); fam.appendChild(b); });
$('presets').innerHTML='';
PRESETS.forEach(p=>{const b=document.createElement('button');b.className='preset';b.innerHTML='<span class="pi">'+p.ic+'</span>'+p.label;b.onclick=()=>applyPreset(p.fn);$('presets').appendChild(b);});
SOON.forEach(s=>{const b=document.createElement('button');b.className='preset';b.disabled=true;b.title=s.why;b.innerHTML='<span class="pi">…</span>'+s.label;$('presets').appendChild(b);});
$('families').innerHTML='';
['DSCA','8B','5B','7B','SCM5B'].forEach(m=>{const b=document.createElement('button');b.className='fam';b.textContent=m;b.onclick=()=>applyPreset(()=>{state.model=m;$('fModel').value=m;});$('families').appendChild(b);});
$('dateChips').innerHTML='';
[['Today',0],['7d',7],['30d',30],['Year',-1]].forEach(([lbl,n])=>{const b=document.createElement('button');b.className='chip';b.textContent=lbl;
b.onclick=()=>{const d=new Date(); if(n===-1)state.from=d.getFullYear()+'-01-01'; else if(n===0){state.from=state.to=isoD(d);} else {d.setDate(d.getDate()-n);state.from=isoD(d);state.to='';}
$('fFrom').value=state.from;$('fTo').value=state.to||''; state.page=0;search();};
$('dateChips').appendChild(b);});
}
// ---- keyboard ----
document.addEventListener('keydown', e=>{
if(e.key==='/' && document.activeElement!==$('omni')){ e.preventDefault(); $('omni').focus(); $('omni').select(); return; }
if(e.key==='Escape'){ if(document.activeElement===$('omni')&&$('omni').value){ $('omni').value='';routeOmni('');debouncedSearch(); } else { $('omni').focus(); } return; }
if((e.key==='ArrowDown'||e.key==='ArrowUp') && state.rows.length){ e.preventDefault();
let i=state.rows.findIndex(r=>r.id==state.selected); i = e.key==='ArrowDown'? Math.min(state.rows.length-1,i+1) : Math.max(0,i-1);
select(state.rows[i].id); const tr=[...$('rows').children][i]; if(tr) tr.scrollIntoView({block:'nearest'}); }
if(e.key==='Enter' && state.selected){ window.open(API+'/api/datasheet/'+state.selected+'?format=html','_blank'); }
/* ---------- recent searches ---------- */
function recents(){ try{return JSON.parse(localStorage.getItem('tdb_recent')||'[]')}catch(e){return[]} }
function pushRecent(q){ if(!q)return; let r=recents().filter(x=>x!==q); r.unshift(q); r=r.slice(0,8); localStorage.setItem('tdb_recent',JSON.stringify(r)); }
function showHistory(on){ const r=recents(); if(!on||!r.length){$('history').classList.remove('show');return;}
$('history').innerHTML='<div class="hl">Recent</div>'+r.map(q=>`<button data-q="${esc(q)}">↩ ${esc(q)}</button>`).join('');
[...$('history').querySelectorAll('button')].forEach(b=>b.onmousedown=()=>{$('omni').value=b.dataset.q;routeOmni(b.dataset.q);state.page=0;search();showHistory(false);});
$('history').classList.add('show'); }
/* ---------- resizer ---------- */
(function(){const rz=$('resizer');let on=false;
rz.addEventListener('mousedown',e=>{on=true;rz.classList.add('drag');document.body.classList.add('resizing');e.preventDefault();});
window.addEventListener('mousemove',e=>{if(!on)return;let w=Math.max(360,Math.min(window.innerWidth-540,window.innerWidth-e.clientX));document.documentElement.style.setProperty('--insp',w+'px');});
window.addEventListener('mouseup',()=>{if(on){on=false;rz.classList.remove('drag');document.body.classList.remove('resizing');fitCert();}});
})();
/* ---------- stats popover ---------- */
let statsLoaded=false;
$('statsBtn').onclick=async()=>{ const p=$('statsPop'); if(p.classList.contains('show')){p.classList.remove('show');return;}
p.classList.add('show'); if(statsLoaded)return;
try{ const s=await (await fetch(API+'/api/stats')).json(); statsLoaded=true;
const res=s.by_result||[]; const pass=(res.find(r=>r.overall_result==='PASS')||{}).count||0; const fail=(res.find(r=>r.overall_result==='FAIL')||{}).count||0; const tot=pass+fail||1;
p.innerHTML=`<h4>Database</h4><div class="big">${(s.total_records||0).toLocaleString()}</div><div class="sub">records · ${fmtDate(s.date_range&&s.date_range.oldest)}${fmtDate(s.date_range&&s.date_range.newest)}</div>
<div class="bar"><i style="width:${pass/tot*100}%;background:var(--pass-ink)"></i><i style="width:${fail/tot*100}%;background:var(--fail-ink)"></i></div>
<div class="legend"><span style="--c:var(--pass-ink)">PASS ${pass.toLocaleString()}</span><span>FAIL ${fail.toLocaleString()}</span></div>
<div style="margin-top:6px">${(s.by_log_type||[]).slice(0,7).map(l=>`<div class="lt"><span>${esc(l.log_type)}</span><span>${l.count.toLocaleString()}</span></div>`).join('')}</div>`;
p.querySelectorAll('.legend span')[0].style.cssText='--c:var(--pass-ink)';
}catch(e){ p.innerHTML='<div class="sub">stats unavailable</div>'; } };
document.addEventListener('click',e=>{ if(!$('statsPop').contains(e.target)&&e.target!==$('statsBtn')) $('statsPop').classList.remove('show'); });
/* ---------- keyboard ---------- */
document.addEventListener('keydown',e=>{
if(e.key==='/'&&document.activeElement!==$('omni')){e.preventDefault();$('omni').focus();$('omni').select();return;}
if(e.key==='Escape'){ if($('statsPop').classList.contains('show')){$('statsPop').classList.remove('show');return;}
if($('insp').classList.contains('open')&&window.innerWidth<=820){$('insp').classList.remove('open');return;}
if(document.activeElement===$('omni')&&$('omni').value){$('omni').value='';routeOmni('');debouncedSearch();}else $('omni').focus(); return;}
if((e.key==='ArrowDown'||e.key==='ArrowUp')&&state.rows.length){
e.preventDefault(); let i=state.rows.findIndex(r=>r.id==state.selected);
i=e.key==='ArrowDown'?Math.min(state.rows.length-1,i+1):Math.max(0,i-1);
if(state.rows[i]){select(state.rows[i].id);const tr=[...$('rows').children][i];if(tr)tr.scrollIntoView({block:'nearest'});}}
if(e.key==='Enter'){ if(document.activeElement===$('omni')) pushRecent($('omni').value.trim());
if(state.selected) window.open(API+'/api/datasheet/'+state.selected+'?format=html','_blank'); }
});
// ---- bootstrap ----
/* ---------- bootstrap ---------- */
async function boot(){
// restore from URL
const u=new URLSearchParams(location.search);
if(u.get('serial')){state.serial=u.get('serial');$('omni').value=state.serial;routeOmni(state.serial);}
else if(u.get('model')){state.model=u.get('model');$('omni').value=state.model;routeOmni(state.model);}
else if(u.get('q')){state.q=u.get('q');$('omni').value=state.q;routeOmni(state.q);}
for(const k of ['result','station','logtype','from','to']) if(u.get(k)) state[k]=u.get(k);
for(const k of ['result','station','logtype','from','to']) if(u.get(k))state[k]=u.get(k);
if(u.get('sort')){state.sort=u.get('sort');state.dir=u.get('dir')||'desc';}
state.selected=u.get('selected');
// filters + stats
renderPresets();
try{ const f=await (await fetch(API+'/api/filters')).json();
$('modelList').innerHTML=(f.models||[]).map(m=>`<option value="${m.model_number}">${m.model_number} (${m.count})</option>`).join('');
$('fStation').innerHTML='<option value="">any station</option>'+(f.stations||[]).map(s=>`<option>${s}</option>`).join('');
$('fLog').innerHTML='<option value="">any log</option>'+(f.log_types||[]).map(l=>`<option>${l}</option>`).join('');
$('modelList').innerHTML=(f.models||[]).map(m=>`<option value="${esc(m.model_number)}">${esc(m.model_number)} (${m.count})</option>`).join('');
$('fStation').innerHTML='<option value="">any station</option>'+(f.stations||[]).map(s=>`<option>${esc(s)}</option>`).join('');
$('fLog').innerHTML='<option value="">any log</option>'+(f.log_types||[]).map(l=>`<option>${esc(l)}</option>`).join('');
}catch(e){}
try{ const s=await (await fetch(API+'/api/stats')).json();
$('ingest').textContent = (s.total_records||0).toLocaleString()+' records · newest '+fmtDate(s.date_range&&s.date_range.newest);
$('ingest').textContent=(s.total_records||0).toLocaleString()+' records · newest '+fmtDate(s.date_range&&s.date_range.newest);
}catch(e){ $('ingest').textContent='stats unavailable'; }
$('statsBtn').onclick=async()=>{ const s=await (await fetch(API+'/api/stats')).json();
alert('Total: '+s.total_records.toLocaleString()+'\nResult: '+(s.by_result||[]).map(r=>r.overall_result+' '+r.count.toLocaleString()).join(' · ')
+'\nDates: '+fmtDate(s.date_range.oldest)+' → '+fmtDate(s.date_range.newest)
+'\nLog types: '+(s.by_log_type||[]).map(r=>r.log_type+' '+r.count.toLocaleString()).join(' · ')); };
renderPresets();
$('omni').focus();
search();
if(state.from)$('fFrom').value=state.from; if(state.to)$('fTo').value=state.to;
if(state.station)$('fStation').value=state.station; if(state.logtype)$('fLog').value=state.logtype; if(state.model)$('fModel').value=state.model;
$('omni').focus(); search();
}
boot();
</script>