@@ -6,178 +6,125 @@
< title > Dataforth · TestDataDB< / title >
< style >
: root {
--bg : #f6 f8fa ; --surface : #ffffff ; --border : #e3 e8ee ; --border-strong : #cbd5e1 ;
--ink : #0f172a ; --ink-2 : #475569 ; --ink-3 : #94a3b8 ; --accent : #1e40af ; --accent-soft : #eef4 ff ;
--pass-bg : #dcfce7 ; --pass-ink : #15803d ; --fail-bg : #fee2e2 ; --fail-ink : #b 91c1c ;
--hover : #f1f5f9 ; --sel : #e0e7ff ; --desk : #e7ecf1 ;
--mono : ui-monospace , "SFMono-Regular" , Consolas , "Liberation Mono" , monospace ;
--bg : #f8fafc ; --surface : #ffffff ; --border : #e2 e8f0 ; --border-strong : #cbd5e1 ;
--ink : #0f172a ; --ink-2 : #475569 ; --ink-3 : #94a3b8 ; --accent : #1e40af ; --accent-soft : #eff6 ff ;
--pass-bg : #dcfce7 ; --pass-ink : #166534 ; --fail-bg : #fee2e2 ; --fail-ink : #9 91b1b ;
--hover : #f1f5f9 ; --sel : #e0e7ff ; --mono : ui-monospace , "SFMono-Regular" , Consolas , "Liberation Mono" , monospace ;
--sans : Inter , system-ui , - apple-system , "Segoe UI" , Roboto , sans-serif ;
--r : 6 px ; --insp : 480 px ;
}
* { box-sizing : border-box }
html , body { height : 100 % ; margin : 0 }
body { font-family : var ( - - sans ) ; font-size : 13.5 px ; color : var ( - - ink ) ; background : var ( - - bg ) ;
display : grid ; grid-template-rows : auto 1 fr ; overflow : hidden ; -webkit- font-smoothing : antialiased }
button , input , select { font-family : inherit }
: focus-visible { outline : none ; box-shadow : 0 0 0 2 px var ( - - surface ) , 0 0 0 4 px var ( - - accent ) }
body { font-family : var ( - - sans ) ; font-size : 14 px ; color : var ( - - ink ) ; background : var ( - - bg ) ;
display : grid ; grid-template-rows : auto 1 fr ; overflow : hidden }
/* ---------- header ---------- */
header { display : flex ; align-items : center ; gap : 14 px ; padding : 0 14 px ; height : 50 px ;
background : var ( - - surface ) ; border-bottom : 1 px solid var ( - - border ) ; position : relative ; z-index : 30 }
. brand { font-weight : 700 ; letter-spacing : -.01 em ; white-space : nowrap ; font-size : 14.5 px }
. brand b { color : var ( - - accent ) ; font-weight : 700 }
. menubtn { display : none ; border : 1 px solid var ( - - border ) ; background : none ; border-radius : var ( - - r ) ; height : 32 px ; width : 34 px ; cursor : pointer ; color : var ( - - ink -2 ) }
. omni { flex : 1 ; position : relative ; max-width : 780 px }
. omni . field { display : flex ; align-items : center ; height : 38 px ; border : 1 px solid var ( - - border - strong ) ; border-radius : 8 px ; background : var ( - - bg ) ; padding : 0 8 px 0 30 px ; position : relative }
. omni . field : focus-with in { border-color : var ( - - accent ) ; box-shadow : 0 0 0 3 px var ( - - accent - soft ) ; background : #fff }
. omni . ic { position : absolute ; left : 9 px ; color : var ( - - ink -3 ) ; font-size : 15 px ; line-height : 1 }
. omni inp ut{ flex : 1 ; he ight: 100 % ; border : 0 ; background : none ; font-size : 14 px ; color : var ( - - ink ) ; outline : none }
. mode { font-size : 10.5 px ; font-weight : 600 ; letter-spacing : .04 em ; text-transform : uppercase ; color : var ( - - accent ) ;
background : var ( - - accent - soft ) ; border : 1 px solid #dbeafe ; border-radius : 5 px ; padding : 2 px 7 px ; cursor : pointer ; white-space : nowrap }
. mode : hover { background : #dbeafe }
. history { position : absolute ; top : 44 px ; left : 0 ; right : 0 ; background : #fff ; border : 1 px solid var ( - - border ) ; border-radius : 8 px ;
box-shadow : 0 8 px 24 px rgba ( 15 , 23 , 42 , .12 ) ; padding : 5 px ; display : none ; z-index : 40 }
. history . show { display : block }
. history . hl { font-size : 10.5 px ; text-transform : uppercase ; letter-spacing : .05 em ; color : var ( - - ink -3 ) ; padding : 5 px 8 px }
. history button { display : flex ; align-items : center ; gap : 8 px ; width : 100 % ; text-align : left ; border : 0 ; background : none ;
border-radius : 5 px ; padding : 6 px 8 px ; font-size : 13 px ; cursor : pointer ; color : var ( - - ink ) ; font-family : var ( - - mono ) }
. history button : hover { background : var ( - - hover ) }
header { display : flex ; align-items : center ; gap : 16 px ; padding : 0 16 px ; height : 52 px ;
background : var ( - - surface ) ; border-bottom : 1 px solid var ( - - border ) }
. brand { font-weight : 700 ; letter-spacing : -.01 em ; white-space : nowrap }
. brand span { color : var ( - - accent ) }
. omni { flex : 1 ; position : relative }
. omni input { width : 100 % ; height : 36 px ; padding : 0 12 px 0 32 px ; font-size : 14 px ; font-family : var ( - - sans ) ;
border : 1 px solid var ( - - border - strong ) ; border-radius : 6 px ; background : var ( - - bg ) ; color : var ( - - ink ) }
. omni input : focus{ outl ine : none ; border-color : var ( - - accent ) ; box-shadow : 0 0 0 2 px var ( - - accent - soft ) ; background : #fff }
. omni . ic { position : absolute ; left : 10 px ; top : 9px ; color : var ( - - ink -3 ) ; font-size : 15 px }
. omni . ro ute { position : absolute ; r ight: 10 px ; top : 9 px ; font-size : 11 px ; color : var ( - - ink -3 ) ; font-family : var ( - - mono ) }
. badge { display : flex ; align-items : center ; gap : 6 px ; font-size : 12 px ; color : var ( - - ink -2 ) ; white-space : nowrap }
. dot { width : 8 px ; height : 8 px ; border-radius : 50 % ; background : #22c55e ; box-shadow : 0 0 0 3 px #22c55e 22 }
. hbtn { font-size : 12.5 px ; color : var ( - - ink -2 ) ; background : none ; border : 1 px solid var ( - - border ) ; border-radius : var ( - - r ) ; height : 32 px ; padding : 0 11 px ; cursor : pointer }
. hbtn { font: inherit ; font -size : 12px ; color : var ( - - ink -2 ) ; background : none ; border : 1 px solid var ( - - border ) ;
border-radius : 6 px ; height : 30 px ; padding : 0 10 px ; cursor : pointer }
. hbtn : hover { background : var ( - - hover ) }
/* ---------- stats popover ---------- */
. pop { position : absolute ; top : 46 px ; right : 12 px ; width : 340 px ; background : #fff ; border : 1 px solid var ( - - border ) ;
border-radius : 10 px ; box-shadow : 0 12 px 34 px rgba ( 15 , 23 , 42 , .16 ) ; padding : 16 px ; display : none ; z-index : 50 }
. pop . show { display : block }
. pop h4 { margin : 0 0 2 px ; font-size : 13 px }
. pop . big { font-size : 26 px ; font-weight : 700 ; letter-spacing : -.02 em ; font-variant-numeric : tabular-nums }
. pop . sub { font-size : 11.5 px ; color : var ( - - ink -3 ) }
. pop . bar { display : flex ; height : 10 px ; border-radius : 5 px ; overflow : hidden ; margin : 12 px 0 6 px ; background : #eef2f6 }
. pop . bar i { display : block }
. pop . legend { display : flex ; gap : 14 px ; font-size : 11.5 px ; color : var ( - - ink -2 ) }
. pop . legend span :: before { content : "" ; display : inline-block ; width : 8 px ; height : 8 px ; border-radius : 2 px ; margin-right : 5 px ; vertical-align : middle }
. pop . lt { display : flex ; justify-content : space-between ; font-size : 12 px ; padding : 3 px 0 ; border-top : 1 px solid var ( - - border ) ; font-family : var ( - - mono ) }
. pop . lt : first-of-type { border-top : 0 ; margin-top : 8 px }
/* ---------- layout ---------- */
main { display : grid ; grid-template-columns : 236 px 1 fr var ( - - insp ) ; min-height : 0 ; height : 100 % }
main { display : grid ; grid-template-columns : 232 px 1 fr var ( - - insp , 460 px ); min-height : 0 ; height : 100 % }
. pane { min-height : 0 ; overflow : auto }
/* ---------- filter rail ---------- */
. rail { border-right : 1 px solid var ( - - border ) ; background : var ( - - surface ) ; padding : 14 px ; overflow-y : auto }
. rail h3 { font-size : 10.5 px ; text-transform : uppercase ; letter-spacing : .06 em ; color : var ( - - ink -3 ) ; margin : 18 px 0 8 px ; font-weight : 600 }
. rail h3 : first-child { margin-top : 0 }
/* ---------- presets ---------- */
. presets { display : flex ; flex-direction : column ; gap : 5 px }
. preset { display : flex ; align-items : center ; gap : 8 px ; width : 100 % ; text-align : left ; font-size : 12.5px ; height : 31 px ; padding : 0 9 px ;
border : 1 px solid var ( - - border ) ; border-radius : var ( - - r ) ; background : #fff ; color : var ( - - ink ) ; cursor : pointer ; transition : background .08 s }
. preset : hover : not ( : disabled ) {background : var ( - - accent - soft ) ; border-color : #bfdbfe }
. preset . pi { width : 14 px ; text-align : center ; color : var ( - - accent ) ; font-size : 12 px }
. preset : disabled { opacity : .45 ; cursor : not-allowed }
. famrow { display : f lex ; flex-wrap : wrap ; gap : 5 px ; margin-top : 7 px }
. fam { font-family : var ( - - mono ) ; font-size : 11.5 px ; height : 26 px ; padding : 0 9 px ; border : 1 px solid var ( - - border ) ; border-radius : 5 px ; background : #fff ; color : var ( - - ink -2 ) ; cursor : pointer }
. fam : hover { background : var ( - - accent - soft ) ; border-color : #bfdbfe ; color : var ( - - accent ) }
. preset { display : flex ; align-items : center ; gap : 7 px ; width : 100 % ; text-align : left ; font: inherit ; font -size : 12.5 px ;
height : 30 px ; padding : 0 9 px ; border: 1 px solid var ( - - border ) ; border-radius : 6 px ; background : #fff ; color : var ( - - ink ) ; cursor : pointer }
. preset : hover { background : var ( - - accent - soft ) ; border-color : #bfdbfe }
. preset . pi { width : 15 px ; text-align : center ; color : var ( - - ink -3 ) }
. preset . fam { display : inline-flex ; width : auto ; height : 26 px ; font-family : var ( - - mono ) ; font-size : 11.5 px ; padding : 0 8 px }
. preset . fam : disabled , . preset : disab led { opacity : .5 ; cursor : not-allowed ; background : #fff }
. famrow { display : flex ; flex-wrap : wrap ; gap : 5 px ; margin-top : 6 px }
/* ---------- filter rail ---------- */
. rail { border-right : 1 px solid var ( - - border ) ; background : var ( - - surface ) ; padding : 14 px }
. rail h3 { font-size : 11 px ; text-transform : uppercase ; letter-spacing : .06 em ; color : var ( - - ink -3 ) ; margin : 18 px 0 8 px ; font-weight : 600 }
. rail h3 : first-child { margin-top : 0 }
. seg { display : flex ; border : 1 px solid var ( - - border - strong ) ; border-radius : 6 px ; overflow : hidden }
. seg button { flex : 1 ; font : inherit ; font-size : 12 px ; height : 30 px ; border : 0 ; background : #fff ; color : var ( - - ink -2 ) ; cursor : pointer }
. seg button + button { border-left : 1 px solid var ( - - border ) }
. seg button . on { background : var ( - - accent ) ; color : #fff }
. rail label { display : block ; font-size : 11 px ; color : var ( - - ink -3 ) ; margin : 8 px 0 3 px }
. rail input , . rail select { width : 100 % ; height : 31 px ; font-size : 13 px ; padding : 0 8 px ; border : 1 px 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 3 px var ( - - accent - soft ) }
. chips { display : flex ; gap : 5 px ; margin-top : 7 px }
. chip { font-size : 11 px ; height : 24 px ; padding : 0 8 px ; border : 1 px solid var ( - - border ) ; border-radius : 20 px ; background : #fff ; color : var ( - - ink -2 ) ; cursor : pointer }
. chip : hover { background : var ( - - hover ) }
. reset { margin-top : 18 px ; width : 100 % ; height : 31 px ; font-size : 12 px ; background : none ; border : 1 px solid var ( - - border ) ; border-radius : var ( - - r ) ; color : var ( - - ink -2 ) ; cursor : pointer }
. rail input , . rail select { width : 100 % ; height : 30 px ; font: inherit ; font -size : 13 px ; padding : 0 8 px ;
border : 1 px solid var ( - - border - strong ) ; border-radius : 6 px ; background : #fff ; color : var ( - - ink ) }
. rail input : focus , . rail select : focus { outline : none ; border-color : var ( - - accent ) }
. reset { margin-top : 18 px ; width : 100 % ; height : 3 0px ; font : inherit ; font-size : 1 2px ; background : none ;
border : 1 px solid var ( - - border ) ; border-radius : 6 px ; color : var ( - - ink -2 ) ; cursor : pointer }
. reset : hover { background : var ( - - hover ) }
/* ---------- results ---------- */
. results { display : grid ; grid-template-rows : auto auto 1 fr auto ; min-height : 0 ; background : var ( - - surface ) }
. results { display : grid ; grid-template-rows : auto 1 fr auto ; min-height : 0 ; background : var ( - - surface ) }
. rtoolbar { display : flex ; align-items : center ; gap : 12 px ; padding : 8 px 12 px ; border-bottom : 1 px solid var ( - - border ) ; font-size : 12 px ; color : var ( - - ink -2 ) }
. rtoolbar . count { font-weight : 7 00; color : var ( - - ink ) ; font-variant-numeric : tabular-nums }
. rtoolbar . count { font-weight : 6 00; color : var ( - - ink ) }
. rtoolbar . sp { flex : 1 }
. rtoolbar a { font-size : 12 px ; color : var ( - - accent ) ; text-decoration : none }
. rtoolbar a : hover { text-decoration : underline }
. rtoolbar select { font-size : 12 px ; color : var ( - - ink -2 ) ; border : 1 px solid var ( - - border ) ; border-radius : 5 px ; height : 27 px ; background : #fff }
. selbar { display : none ; align-items : center ; gap : 10 px ; padding : 6 px 12 px ; background : var ( - - accent - soft ) ; border-bottom : 1 px solid #dbeafe ; font-size : 12 px ; color : var ( - - accent ) }
. selbar . show { display : flex }
. selbar button { font-size : 12 px ; height : 26 px ; padding : 0 9 px ; border : 1 px solid #bfdbfe ; border-radius : var ( - - r ) ; background : #fff ; color : var ( - - accent ) ; cursor : pointer }
. selbar button : disabled { opacity : .5 ; cursor : not-allowed }
. rtoolbar a , . rtoolbar select { font : inherit ; font-size : 12 px ; color : var ( - - accent ) ; text-decoration : none }
. rtoolbar select { color : var ( - - ink -2 ) ; border : 1 px solid var ( - - border ) ; border-radius : 5 px ; height : 26 px }
. twrap { overflow : auto ; min-height : 0 }
table { width : 100 % ; border-collapse : collapse ; font-size : 13 px }
thead th { position : sticky ; top : 0 ; background : #f7f9fb ; border-bottom : 1 px solid var ( - - border - strong ) ; text-align : left ;
font-size : 10.5 px ; text-transform : uppercase ; letter-spacing : .04 em ; color : var ( - - ink -3 ) ; font-weight : 600 ; padding : 7 px 10 px ; 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 : 9 px ; margin-left : 3 px }
thead th . ck { width : 30 px ; padding-left : 12 px }
thead th { position : sticky ; top : 0 ; background : #f8fafc ; border-bottom : 1 px solid var ( - - border - strong ) ;
text-align : left ; font-size: 11 px ; text-transform : uppercase ; letter-spacing : .04 em ; color : var ( - - ink -3 ) ;
font-weight : 600 ; padding : 7 px 10 px ; white-space : nowrap ; z-index : 1 }
tbody td { padding : 5 px 10 px ; border-bottom : 1 px solid var ( - - border ) ; white-space : nowrap }
tbody td . ck { padding-left : 12 px }
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 3 px 0 0 var ( - - accent ) }
. mono { font-family : var ( - - mono ) ; font-size : 12.5 px ; font-variant-numeric : tabular-nums }
. pill { display : inline-block ; font-size : 10.5 px ; font-weight : 7 00; padding : 1 px 8 px ; border-radius : 4 px ; font-family : var ( - - mono ) }
. mono { font-family : var ( - - mono ) ; font-size : 12.5 px }
. pill { display : inline-block ; font-size : 11 px ; font-weight : 6 00; padding : 1 px 8 px ; border-radius : 4 px ; font-family : var ( - - mono ) }
. pill . PASS { background : var ( - - pass - bg ) ; color : var ( - - pass - ink ) }
. pill . FAIL { background : var ( - - fail - bg ) ; color : var ( - - fail - ink ) }
. web { font-size : 13 px }
. pager { display : flex ; align-items : center ; gap : 10 px ; padding : 7 px 12 px ; border-top : 1 px solid var ( - - border ) ; font-size : 12 px ; color : var ( - - ink -2 ) }
. pager button { font-size : 12 px ; height : 28 px ; padding : 0 11 px ; border : 1 px solid var ( - - border - strong ) ; border-radius : var ( - - r ) ; background : #fff ; cursor : pointer ; color : var ( - - ink ) }
. pager { display : flex ; align-items : center ; gap : 10 px ; padding : 8 px 12 px ; border-top : 1 px solid var ( - - border ) ; font-size : 12 px ; color : var ( - - ink -2 ) }
. pager button { font : inherit ; font-size : 12 px ; height : 28 px ; padding : 0 10 px ; border : 1 px solid var ( - - border - strong ) ;
border-radius : 6 px ; 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 : 8 px ; padding : 40 px 20 px ; color : var ( - - ink -3 ) ; text-align : center }
. state . em { font-size : 26 px ; opacity : .6 }
. state . err { color : var ( - - fail - ink ) }
. state button { margin-top : 4 px ; font-size : 12 px ; height : 28 px ; padding : 0 12 px ; border : 1 px solid var ( - - border - strong ) ; border-radius : var ( - - r ) ; background : #fff ; cursor : pointer ; color : var ( - - ink ) }
. skel { height : 13 px ; margin : 8 px 10 px ; border-radius : 4 px ; background : linear-gradient ( 90 deg , #eef2f6 , #e2e8f0 , #eef2f6 ) ; background-size : 200 % 100 % ; animation : sh 1.1 s infinite }
@ keyframes sh { 0 % { background-position : 200 % 0 } 100 % { background-position : -200 % 0 } }
/* ---------- inspector ---------- */
. insp { position : relative ; border-left : 1 px solid var ( - - border ) ; background : var ( - - surface ) ; display : grid ; grid-template-rows : auto auto 1 fr ; min-height : 0 }
. resizer { position : absolute ; left : -3 px ; top : 0 ; bottom : 0 ; width : 7 px ; cursor : col-resize ; z-index : 5 }
. resizer : hover , . resizer . drag { background : linear-gradient ( 90 deg , transparent , var ( - - accent ) 45 % , var ( - - accent ) 55 % , transparent ) }
body . resizing { cursor : col-resize ; user-select : none }
. meta { padding : 12 px 14 px ; border-bottom : 1 px solid var ( - - border ) }
. meta . sn { font-family : var ( - - mono ) ; font-size : 16 px ; font-weight : 700 ; letter-spacing : -.01 em }
. meta dl { display : grid ; grid-template-columns : auto 1 fr ; gap : 3 px 12 px ; margin : 10 px 0 0 ; font-size : 12.5 px }
. meta dt { color : var ( - - ink -3 ) }
. meta dd { margin : 0 ; font-family : var ( - - mono ) }
. acts { display : flex ; gap : 6 px ; flex-wrap : wrap ; padding : 10 px 14 px ; border-bottom : 1 px solid var ( - - border ) }
. acts a , . acts button { font-size : 12 px ; height : 29 px ; padding : 0 11 px ; border : 1 px solid var ( - - border - strong ) ; border-radius : var ( - - r ) ;
background : #fff ; color : var ( - - ink ) ; text-decoration : none ; display : inline-flex ; align-items : center ; gap : 5 px ; 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 : 14 px ; position : relative }
. viewer iframe { display : block ; width : 100 % ; min-height : 100 % ; border : 1 px solid var ( - - border ) ; border-radius : 5 px ; background : #fff ;
box-shadow : 0 1 px 5 px rgba ( 15 , 23 , 42 , .10 ) }
kbd { font-family : var ( - - mono ) ; font-size : 11 px ; background : #f1f5f9 ; border : 1 px solid var ( - - border - strong ) ; border-bottom-width : 2 px ; border-radius : 4 px ; padding : 0 5 px ; color : var ( - - ink -2 ) }
/* ---------- responsive ---------- */
@ media ( max-width : 1180px ) {
main { grid-template-columns : 1 fr var ( - - insp ) }
. rail { position : absolute ; top : 50 px ; bottom : 0 ; left : 0 ; width : 250 px ; z-index : 20 ; box-shadow : 6 px 0 20 px rgba ( 15 , 23 , 42 , .12 ) ; transform : translateX ( -104 % ) ; transition : transform .18 s }
body . rail-open . rail { transform : none }
. menubtn { display : inline-flex ; align-items : center ; justify-content : center }
}
@ media ( max-width : 820px ) { main { grid-template-columns : 1 fr } . insp { position : absolute ; top : 50 px ; bottom : 0 ; right : 0 ; width : min ( 94 vw , 520 px ) ; z-index : 18 ; box-shadow : -8 px 0 24 px rgba ( 15 , 23 , 42 , .14 ) } . insp : not ( . open ) { display : none } . resizer { display : none } }
. insp . meta{ padding : 12 px 14 px ; border-bottom : 1 px solid var ( - - border ) }
. insp . meta . sn { font-family : var ( - - mono ) ; font-size : 16 px ; font-weight : 700 }
. insp . meta dl { display : grid ; grid-template-columns : auto 1 fr ; gap : 3 px 12 px ; margin : 10 px 0 0 ; font-size : 12.5 px }
. insp . meta dt { color : var ( - - ink -3 ) }
. insp . meta dd { margin : 0 ; font-family : var ( - - mono ) }
. insp . acts{ display : flex ; gap : 6 px ; flex-wrap : wrap ; padding : 10 px 14 px ; border-bottom : 1 px solid var ( - - border ) }
. insp . acts a , . insp . acts button { font: inherit ; font -size : 12 px ; height : 28 px ; padding : 0 10 px ; border : 1 px solid var ( - - border - strong ) ;
border-radius : 6 px ; 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 : 8 px ; font-size : 13 px ; text-align : center ; padding : 20 px }
. skel { height : 24 px ; margin : 6 px 10 px ; border-radius : 4 px ; background : linear-gradient ( 90 deg , #f1f5f9 , #e8edf3 , #f1f5f9 ) ; background-size : 200 % 100 % ; animation : sh 1.1 s infinite }
@ keyframes sh { 0 % { background-position : 200 % 0 } 100 % { background- position: -200 % 0 } }
kbd { font-family : var ( - - mono ) ; font-size : 11 px ; background : #f1f5f9 ; border : 1 px solid var ( - - border - strong ) ; border-bottom-width : 2 px ; border- radius : 4 px ; padding : 0 5 px }
< / style >
< / head >
< body >
< header >
< button class = "menubtn" id = "menu" title = "Filters" > ☰ < / button >
< div class = "brand" > Dataforth < b > ·< / b > TestDataDB< / div >
< div class = "brand" > Dataforth < span > · < / span > TestDataDB< / div >
< div class = "omni" >
< 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 )" >
< butto n class = "mod e" id = "mode" title = "Click to force the search mod e" > auto < / butto n>
< / div >
< div class = "history" id = "history" > < / div >
< span class = "ic " > 🔍< / span >
< input id = "omni" autocomplete = "off" spellcheck = "false"
placeholder = "Search serial, model, or text… (press / to focus)" >
< spa n class = "rout e" id = "rout e" > < / spa n>
< / div >
< div class = "badge" > < span class = "dot" > < / span > < span id = "ingest" > loading…< / span > < / div >
< button class = "hbtn" id = "statsBtn" > Stats ▾ < / button >
< div class = "pop" id = "statsPop" > < / div >
< button class = "hbtn" id = "statsBtn" title = "Database statistics" > Stats< / button >
< / header >
< main >
< aside class = "rail pane" id = "rail" >
<!-- filter rail -- >
< aside class = "rail pane" >
< 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 >
@@ -187,195 +134,130 @@
< 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 all < / button >
< button class = "reset" id = "reset" > Reset filters < / 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 = "#" 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 >
< label > rows
< select id = "pageSize" > < option > 25< / option > < option selected > 50< / option > < option > 100< / option > < / select >
< / label >
< a id = "exportCsv" href = "#" > Export CSV ↓< / a >
< / div >
< div class = "twrap" id = "twrap" >
< table >
< thead > < tr >
< 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 >
< th > Serial< / th > < th > Model< / th > < th > Test Date< / th > < th > Stn< / th > < th > Log< / th > < th > Result< / th > < th > 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 class = "hint " > < kbd > /< / kbd > search · < kbd > ↑< / kbd > < kbd > ↓< / kbd > rows · < kbd > ↵< / kbd > open · < kbd > Esc< / kbd > back < / span >
< span style = "color:var(--ink-3) " > < kbd > /< / kbd > search · < kbd > ↑< / kbd > < kbd > ↓< / kbd > rows · < kbd > ↵< / kbd > open · < kbd > Esc< / kbd > clear < / span >
< / div >
< / section >
< aside class = "insp" id = "insp" >
<!-- inspector -- >
< aside class = "insp" >
< div class = "resizer" id = "resizer" title = "Drag to resize" > < / div >
< div class = "meta" id = "meta" > < / div >
< div class = "meta" id = "meta" > < div class = "empty" style = "height:auto;padding:8px 0" > No record selected< / div > < / div >
< div class = "acts" id = "acts" style = "display:none" > < / div >
< div class = "viewer" id = "viewer" > < / div >
< div id = "viewer" > < div class = "empty" > Search a serial number, then select a row.< br > The calibration certificate renders here.< / div > < / div >
< / aside >
< / main >
< script >
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 => ( { '&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' } [ c ] ) ) ;
const fmtDate = d => d ? String ( d ) . slice ( 0 , 10 ) : '' ;
const isoD = d => d . toISOString ( ) . slice ( 0 , 10 ) ;
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 ;
/* ------ ---- 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 ) { $ ( 'mod e' ) . 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 ;
// ---- 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 ; $ ( 'rout e' ) . 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 '; }
}
$ ( '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 ) {
function params ( extra ) {
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 ( state . s ort) { p . set ( 'sor t' , 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 ) ; }
if ( extra !== 'exp ort' ) { p . set ( 'limi t' , 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 showState ( html , cls ) { $ ( ' state' ) . className = 'state ' + ( cls || '' ) ; $ ( 'sta te' ) . innerHTML = html ; $ ( 'rows' ) . innerHTML = '' ; }
async function search ( ) {
$ ( '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 ( ) ) ;
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 syncUrl ( ) {
const p = params ( ) ; if ( state. selected ) p . set ( 'selec ted ' , state . selected ) ;
history . replaceState ( null , '' , '?' + p . toString ( ) ) ;
}
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 ) { 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="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"> ${ 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 = 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 activeFilters ( ) { return [ 'serial' , 'model' , 'q' , 'result' , 'station' , 'logtype' , 'from' , 'to' ] . some ( k => state [ k ] ) ; }
function fmtDate ( d ) { return d ? String ( d ) . slice ( 0 , 10 ) : '' ; }
/* ---------- 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> ${ 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' ;
$ ( '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 ) ;
async function search ( ) {
$ ( 'rows' ) . innerHTML = Array . from ( { length : 8 } ) . map ( ( ) => '<tr><td colspan="7"><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 ( ) ;
}
function loadCert ( ds ) {
$ ( 'viewer ' ) . innerHTML = '<iframe id="dsframe" title="datasheet"></iframe>' ;
const f = $ ( 'dsframe' ) ; f . onload = ( ) => { styleCert ( ) ; fitCert ( ) ; } ; f . src = ds + '?format=html' ;
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 ? '●' : '○' ;
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="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>
</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' ) ; }
}
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>
<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>Web</dt><dd> ${ r . api _uploaded _at ? 'published ' + fmtDate ( r . api _uploaded _at ) : 'not published' } </dd></dl> ` ;
$ ( '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' ;
syncUrl ( ) ;
}
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 ( ) ; } ) ;
/* ---------- 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 => s tate . 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 ) ) ;
// ---- filters wiring ----
function debouncedSearch ( ) { clearTimeout ( timer ) ; timer = setTimeout ( ( ) => { state . page = 0 ; search ( ) ; } , 280 ) ; }
$ ( 'omni ' ) . addEventListener ( 'input' , e => { routeOmni ( e . targe t . value ) ; debouncedSearch ( ) ; } ) ;
$ ( '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 ( ) ; } ;
@@ -385,102 +267,93 @@ $('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 ] = '' ) ;
state . sort = '' ; state . checks . clear ( ) ; updateSel ( ) ; $ ( 'omni ') . value = '' ; $ ( 'mode ' ) . textContent = state . forc e|| 'auto ' ;
$ ( 'fFrom' ) . value = $ ( 'fTo' ) . value = $ ( 'fStation' ) . value = $ ( 'fLog' ) . value = $ ( 'fModel' ) . value = '' ; state. page = 0 ; search ( ) ; } ;
$ ( 'menu' ) . onclick = ( ) => document . body . classList . toggle ( 'rail-open' ) ;
$ ( 'omni' ) . value = '' ; $ ( 'route' ) . textContent = '' ; $ ( 'fFrom' ) . value = $ ( 'fTo' ) . value = $ ( 'fStation ') . value = $ ( 'fLog ' ) . value = $ ( 'fModel' ) . valu e= '' ;
state . page = 0 ; search ( ) ; } ;
/* ---------- presets ---------- */
// ---- 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 ----
function clearAll ( ) { [ 'serial' , 'model' , 'q' , 'result' , 'station' , 'logtype' , 'from' , 'to' ] . forEach ( k => state [ k ] = '' ) ;
$ ( 'omni' ) . value = '' ; $ ( 'mod e' ) . 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 ( ) ; }
$ ( 'omni' ) . value = '' ; $ ( 'rout e' ) . 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 ) ;
const PRESETS = [
{ ic : '◷' , label : 'Recent' , fn : ( ) => { } } ,
{ ic : '✕' , label : 'Failures' , fn : ( ) => { state . result = 'FAIL' ; } } ,
{ 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 ) ; } } ,
{ 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' ; } } ,
] ;
const SOON = [
{ label : 'Latest upload batch' , why : 'needs sort= uploaded in /api/search' } ,
{ label : 'Latest upload batch' , why : 'needs an upload-time sort param 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 ( ) {
$ ( '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 ) ; } ) ;
const host = $ ( 'presets' ) ; host .innerHTML = '' ;
PRESETS . forEach ( p => { const b = document . createElement ( 'button' ) ; b . className = 'preset' ;
b . innerHTML = '<span class="pi">' + p . i c+ '</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 ) ; } ) ;
}
/* ---------- 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 . unshif t( 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' ) ; }
// ---- keyboard ----
document . addEventListener ( 'keydown' , e => {
if ( e . key === '/' && document . activeElement !== $ ( 'omni' ) ) { e . preventDefaul t( ) ; $ ( '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' ) ; }
} ) ;
/* ------ ---- 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 ) ;
if ( u . get ( 'sort' ) ) { state . sort = u . get ( 'sort' ) ; state . dir = u . get ( 'dir' ) || 'desc' ; }
for ( const k of [ 'result' , 'station' , 'logtype' , 'from' , 'to' ] ) if ( u . get ( k ) ) state [ k ] = u . get ( k ) ;
state . selected = u . get ( 'selected' ) ;
renderPresets ( ) ;
// filters + stats
try { const f = await ( await fetch ( API + '/api/filters' ) ) . json ( ) ;
$ ( '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 ( '' ) ;
$ ( '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 ( '' ) ;
} 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' ; }
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 ( ) ;
$ ( ' 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 ( ) ;
}
boot ( ) ;
< / script >