dcc 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +19 -0
  3. data/.github/workflows/release.yml +28 -0
  4. data/.gitignore +4 -0
  5. data/.rubocop_todo.yml +560 -10
  6. data/CHANGELOG.adoc +34 -1
  7. data/CONTRIBUTING.adoc +8 -1
  8. data/Gemfile +5 -7
  9. data/README.adoc +162 -7
  10. data/dcc.gemspec +9 -2
  11. data/docs/schema-versions.adoc +20 -7
  12. data/examples/build_minimal_dcc.rb +0 -0
  13. data/examples/convert_all_formats.rb +2 -1
  14. data/examples/parse_and_inspect.rb +0 -0
  15. data/examples/quantity_math_demo.rb +7 -7
  16. data/examples/validate_full.rb +1 -1
  17. data/lib/dcc/base/calibration_laboratory.rb +2 -1
  18. data/lib/dcc/base/condition.rb +5 -2
  19. data/lib/dcc/base/core_data.rb +8 -3
  20. data/lib/dcc/base/formula.rb +3 -3
  21. data/lib/dcc/base/identification.rb +6 -3
  22. data/lib/dcc/base/mathml.rb +33 -0
  23. data/lib/dcc/base/quantity.rb +2 -1
  24. data/lib/dcc/base/resp_person.rb +4 -2
  25. data/lib/dcc/base/text.rb +1 -1
  26. data/lib/dcc/base.rb +2 -1
  27. data/lib/dcc/builder/dsl.rb +1 -1
  28. data/lib/dcc/builder/session.rb +16 -14
  29. data/lib/dcc/builder.rb +3 -3
  30. data/lib/dcc/cli/cli.rb +152 -26
  31. data/lib/dcc/cli/formatters.rb +35 -7
  32. data/lib/dcc/cli.rb +1 -1
  33. data/lib/dcc/context_configuration.rb +2 -1
  34. data/lib/dcc/context_options.rb +4 -2
  35. data/lib/dcc/convert/csv.rb +27 -9
  36. data/lib/dcc/convert/html.rb +26 -110
  37. data/lib/dcc/convert/json.rb +10 -7
  38. data/lib/dcc/convert/liquid_filters.rb +58 -0
  39. data/lib/dcc/convert/result.rb +1 -1
  40. data/lib/dcc/convert/templates/report.liquid +250 -0
  41. data/lib/dcc/convert/yaml.rb +1 -1
  42. data/lib/dcc/convert.rb +1 -0
  43. data/lib/dcc/diff/change.rb +1 -1
  44. data/lib/dcc/diff/result.rb +8 -4
  45. data/lib/dcc/diff.rb +43 -11
  46. data/lib/dcc/error.rb +7 -0
  47. data/lib/dcc/extract/file.rb +3 -4
  48. data/lib/dcc/extract/formula/ast.rb +137 -0
  49. data/lib/dcc/extract/formula/evaluator.rb +357 -0
  50. data/lib/dcc/extract/formula/parser.rb +305 -0
  51. data/lib/dcc/extract/formula/quantity.rb +89 -0
  52. data/lib/dcc/extract/formula.rb +110 -0
  53. data/lib/dcc/extract/ring.rb +1 -1
  54. data/lib/dcc/extract.rb +1 -1
  55. data/lib/dcc/i18n/text_lookup.rb +8 -4
  56. data/lib/dcc/i18n.rb +1 -1
  57. data/lib/dcc/inspect/report.rb +51 -14
  58. data/lib/dcc/inspect.rb +1 -1
  59. data/lib/dcc/migrate/route.rb +44 -0
  60. data/lib/dcc/migrate/v2_to_v3.rb +445 -0
  61. data/lib/dcc/migrate.rb +130 -26
  62. data/lib/dcc/plugin/base.rb +64 -0
  63. data/lib/dcc/plugin/registry.rb +7 -2
  64. data/lib/dcc/plugin.rb +6 -2
  65. data/lib/dcc/quantity_format/formatter.rb +16 -10
  66. data/lib/dcc/quantity_format.rb +1 -1
  67. data/lib/dcc/quantity_math/complex.rb +252 -0
  68. data/lib/dcc/quantity_math/matrix.rb +217 -0
  69. data/lib/dcc/quantity_math/quantity.rb +1 -1
  70. data/lib/dcc/quantity_math/real.rb +6 -6
  71. data/lib/dcc/quantity_math.rb +31 -3
  72. data/lib/dcc/schema/version.rb +2 -2
  73. data/lib/dcc/server/app.html +282 -0
  74. data/lib/dcc/server/app.rb +171 -87
  75. data/lib/dcc/server/demo_cert.xml +244 -0
  76. data/lib/dcc/server/storage/entry.rb +1 -1
  77. data/lib/dcc/server/storage/memory.rb +3 -2
  78. data/lib/dcc/server/storage.rb +1 -1
  79. data/lib/dcc/server.rb +1 -1
  80. data/lib/dcc/si/base/complex.rb +1 -1
  81. data/lib/dcc/si/base/complex_list_xml_list.rb +1 -1
  82. data/lib/dcc/si/base/constant.rb +1 -1
  83. data/lib/dcc/si/base/coverage_interval.rb +36 -0
  84. data/lib/dcc/si/base/coverage_interval_mu.rb +1 -1
  85. data/lib/dcc/si/base/expanded_mu.rb +1 -1
  86. data/lib/dcc/si/base/expanded_unc.rb +34 -0
  87. data/lib/dcc/si/base/expanded_unc_xml_list.rb +3 -2
  88. data/lib/dcc/si/base/hybrid.rb +3 -2
  89. data/lib/dcc/si/base/measurement_uncertainty_univariate.rb +1 -1
  90. data/lib/dcc/si/base/real.rb +13 -3
  91. data/lib/dcc/si/base/real_list_xml_list.rb +5 -3
  92. data/lib/dcc/si/base/si_list.rb +3 -2
  93. data/lib/dcc/si/base/standard_mu.rb +1 -1
  94. data/lib/dcc/si/base.rb +5 -2
  95. data/lib/dcc/si/v1/coverage_interval.rb +8 -0
  96. data/lib/dcc/si/v1/expanded_unc.rb +1 -16
  97. data/lib/dcc/si/v1/measurement_uncertainty_univariate.rb +2 -1
  98. data/lib/dcc/si/v1.rb +5 -2
  99. data/lib/dcc/si/v2/coverage_interval.rb +8 -0
  100. data/lib/dcc/si/v2/coverage_interval_mu_xml_list.rb +4 -2
  101. data/lib/dcc/si/v2/expanded_mu_xml_list.rb +2 -1
  102. data/lib/dcc/si/v2/expanded_unc.rb +8 -0
  103. data/lib/dcc/si/v2/measurement_uncertainty_univariate.rb +2 -1
  104. data/lib/dcc/si/v2.rb +8 -3
  105. data/lib/dcc/signature/result.rb +2 -2
  106. data/lib/dcc/signature/signer.rb +31 -18
  107. data/lib/dcc/signature/verifier.rb +23 -21
  108. data/lib/dcc/signature.rb +1 -1
  109. data/lib/dcc/streaming/reader.rb +246 -0
  110. data/lib/dcc/streaming.rb +79 -0
  111. data/lib/dcc/transform/result.rb +1 -1
  112. data/lib/dcc/transform/xslt.rb +22 -12
  113. data/lib/dcc/transform.rb +5 -4
  114. data/lib/dcc/type/base64_binary.rb +5 -3
  115. data/lib/dcc/type/decimal_xml_list.rb +99 -13
  116. data/lib/dcc/type/iso_country_code.rb +3 -2
  117. data/lib/dcc/type/iso_language_code.rb +3 -2
  118. data/lib/dcc/type/schema_version.rb +3 -2
  119. data/lib/dcc/type/si_unit.rb +3 -2
  120. data/lib/dcc/type_guards.rb +3 -3
  121. data/lib/dcc/v2/calibration_laboratory.rb +2 -1
  122. data/lib/dcc/v2/condition.rb +22 -1
  123. data/lib/dcc/v2/configuration.rb +1 -1
  124. data/lib/dcc/v2/digital_calibration_certificate.rb +2 -1
  125. data/lib/dcc/v2/identification.rb +14 -1
  126. data/lib/dcc/v2/mathml.rb +8 -0
  127. data/lib/dcc/v2/measurement_meta_data_list.rb +2 -1
  128. data/lib/dcc/v2/quantity.rb +4 -2
  129. data/lib/dcc/v2.rb +4 -2
  130. data/lib/dcc/v3/calibration_laboratory.rb +2 -1
  131. data/lib/dcc/v3/configuration.rb +1 -1
  132. data/lib/dcc/v3/digital_calibration_certificate.rb +2 -1
  133. data/lib/dcc/v3/mathml.rb +8 -0
  134. data/lib/dcc/v3/measurement_meta_data_list.rb +2 -1
  135. data/lib/dcc/v3/quantity.rb +4 -2
  136. data/lib/dcc/v3.rb +12 -2
  137. data/lib/dcc/validate/business_rules.rb +23 -9
  138. data/lib/dcc/validate/issue.rb +2 -1
  139. data/lib/dcc/validate/result.rb +6 -4
  140. data/lib/dcc/validate/schematron/profile.rb +73 -18
  141. data/lib/dcc/validate/schematron/rule.rb +13 -3
  142. data/lib/dcc/validate/schematron/rules/administrative_data_completeness.rb +11 -4
  143. data/lib/dcc/validate/schematron/rules/base.rb +1 -2
  144. data/lib/dcc/validate/schematron/rules/date_range_check.rb +11 -5
  145. data/lib/dcc/validate/schematron/rules/id_refid_linking.rb +0 -3
  146. data/lib/dcc/validate/schematron/rules/influence_conditions_placement.rb +22 -12
  147. data/lib/dcc/validate/schematron/rules/iso_code_validation.rb +31 -14
  148. data/lib/dcc/validate/schematron/rules/language_code_dedup.rb +12 -5
  149. data/lib/dcc/validate/schematron/rules/non_si_declaration.rb +7 -4
  150. data/lib/dcc/validate/schematron/rules/release_format_check.rb +7 -4
  151. data/lib/dcc/validate/schematron/rules/schema_version_check.rb +13 -9
  152. data/lib/dcc/validate/schematron/rules/uncertainty_consistency.rb +39 -10
  153. data/lib/dcc/validate/schematron/rules/unit_format_check.rb +2 -3
  154. data/lib/dcc/validate/schematron/rules/used_methods_placement.rb +15 -10
  155. data/lib/dcc/validate/schematron/rules/used_software_placement.rb +15 -10
  156. data/lib/dcc/validate/schematron/rules/xml_list_spacing.rb +4 -3
  157. data/lib/dcc/validate/schematron/rules.rb +28 -14
  158. data/lib/dcc/validate/schematron.rb +1 -1
  159. data/lib/dcc/validate/xsd.rb +9 -3
  160. data/lib/dcc/validate.rb +1 -1
  161. data/lib/dcc/version.rb +1 -1
  162. data/lib/dcc/versioned_parser.rb +1 -1
  163. data/lib/dcc.rb +68 -10
  164. data/sample_dcc.xml +140 -0
  165. data/web/index.html +12 -0
  166. data/web/package-lock.json +1599 -0
  167. data/web/package.json +22 -0
  168. data/web/src/App.vue +349 -0
  169. data/web/src/lib/builder.ts +171 -0
  170. data/web/src/lib/dcc.ts +338 -0
  171. data/web/src/lib/units.json +5581 -0
  172. data/web/src/main.ts +5 -0
  173. data/web/src/style.css +27 -0
  174. data/web/tsconfig.json +15 -0
  175. data/web/vite.config.ts +14 -0
  176. metadata +148 -9
@@ -52,7 +52,7 @@ module Dcc
52
52
  # @param exponent [Integer]
53
53
  # @return [Real]
54
54
  def **(exponent)
55
- new_value = value ** exponent
55
+ new_value = value**exponent
56
56
  Real.new(
57
57
  value: new_value,
58
58
  unit: unit && "^#{exponent}",
@@ -75,7 +75,7 @@ module Dcc
75
75
  return other.uncertainty if uncertainty.nil?
76
76
  return uncertainty if other.uncertainty.nil?
77
77
 
78
- sum_of_squares = (uncertainty ** 2) + (other.uncertainty ** 2)
78
+ sum_of_squares = (uncertainty**2) + (other.uncertainty**2)
79
79
  BigDecimal(Math.sqrt(sum_of_squares.to_f).to_s)
80
80
  end
81
81
 
@@ -86,15 +86,15 @@ module Dcc
86
86
 
87
87
  a_rel = uncertainty / value
88
88
  b_rel = other.uncertainty / other.value
89
- sum_of_squares = (a_rel ** 2) + (b_rel ** 2)
89
+ sum_of_squares = (a_rel**2) + (b_rel**2)
90
90
  BigDecimal(Math.sqrt(sum_of_squares.to_f).to_s) * new_value.abs
91
91
  end
92
92
 
93
93
  # Power rule: σ_y = |n * x^(n-1)| * σ_x
94
- def power_uncertainty(exponent, new_value)
94
+ def power_uncertainty(exponent, _new_value)
95
95
  return nil unless uncertain?
96
96
 
97
- derivative = exponent * (value ** (exponent - 1))
97
+ derivative = exponent * (value**(exponent - 1))
98
98
  (derivative.abs * uncertainty)
99
99
  end
100
100
 
@@ -113,4 +113,4 @@ module Dcc
113
113
  end
114
114
  end
115
115
  end
116
- end
116
+ end
@@ -5,10 +5,38 @@ require "bigdecimal/math"
5
5
 
6
6
  module Dcc
7
7
  # `Dcc::QuantityMath` provides BigDecimal-based arithmetic on D-SI
8
- # quantities with proper uncertainty propagation. RSS (root-sum-square)
9
- # for addition/subtraction, fractional for multiplication/division.
8
+ # quantities with proper uncertainty propagation.
9
+ #
10
+ # `Real` is scalar: RSS (root-sum-square) for addition and subtraction,
11
+ # fractional for multiplication and division. `Matrix` is multivariate and
12
+ # carries a full covariance matrix, propagating by `C_y = J C J^T`.
13
+ # `Complex` is a Cartesian complex quantity built on a two-element
14
+ # `Matrix`, so the correlation between its real and imaginary parts
15
+ # survives every operation.
10
16
  module QuantityMath
17
+ # Significant digits requested when taking the square root of a
18
+ # BigDecimal variance. Well past double precision; metrology
19
+ # uncertainties never carry this many digits. Ordinary arithmetic
20
+ # follows the caller's ambient BigDecimal context.
21
+ SQRT_PRECISION = 30
22
+
11
23
  autoload :Quantity, "dcc/quantity_math/quantity"
12
24
  autoload :Real, "dcc/quantity_math/real"
25
+ autoload :Matrix, "dcc/quantity_math/matrix"
26
+ autoload :Complex, "dcc/quantity_math/complex"
27
+
28
+ # Runs `block` with an unlimited BigDecimal precision, restoring the
29
+ # caller's `BigDecimal.limit` on the way out (including when the block
30
+ # raises). Everything feeding a covariance matrix goes through here: a
31
+ # rounded result is no longer exactly symmetric, and `Matrix`'s
32
+ # validation would reject our own output.
33
+ #
34
+ # @return [Object] whatever `block` returns
35
+ def self.exact_covariance
36
+ BigDecimal.save_limit do
37
+ BigDecimal.limit(0)
38
+ yield
39
+ end
40
+ end
13
41
  end
14
- end
42
+ end
@@ -30,7 +30,7 @@ module Dcc
30
30
  def normalize(v)
31
31
  return nil if v.nil?
32
32
 
33
- s = v.to_s.sub(/\Av/, "").gsub(/_/, ".")
33
+ s = v.to_s.sub(/\Av/, "").gsub("_", ".")
34
34
  s.empty? ? nil : s
35
35
  end
36
36
 
@@ -56,7 +56,7 @@ module Dcc
56
56
  # from the XML body.
57
57
  def resolve_dcc(version, xml: nil)
58
58
  v = normalize(version)
59
- return detect_from_xml(xml) if v.nil? || v == "auto"
59
+ return detect_from_xml(xml) || DCC_LATEST if v.nil? || v == "auto"
60
60
  return DCC_LATEST if v == "latest"
61
61
 
62
62
  unless dcc?(v)
@@ -0,0 +1,282 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1.0">
6
+ <title>DCC Validator</title>
7
+ <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
8
+ <script src="https://unpkg.com/@tailwindcss/browser@4"></script>
9
+ <style>
10
+ [v-cloak] { display: none; }
11
+ @keyframes slideUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
12
+ .slide-up { animation: slideUp 0.4s ease-out both; }
13
+ .slide-up:nth-child(2) { animation-delay: .06s; }
14
+ .slide-up:nth-child(3) { animation-delay: .12s; }
15
+ .slide-up:nth-child(4) { animation-delay: .18s; }
16
+ .slide-up:nth-child(5) { animation-delay: .24s; }
17
+ .pulse-dot { animation: pd 2s ease-in-out infinite; }
18
+ @keyframes pd { 0%,100% { opacity:1; } 50% { opacity:.3; } }
19
+ </style>
20
+ </head>
21
+ <body class="bg-slate-950 text-slate-200 font-sans antialiased min-h-screen" style="font-family: 'Inter', system-ui, sans-serif;">
22
+ <div id="app" v-cloak>
23
+
24
+ <!-- Header bar -->
25
+ <div class="border-b border-slate-800 bg-slate-900/50 backdrop-blur sticky top-0 z-50">
26
+ <div class="max-w-5xl mx-auto px-6 py-3 flex items-center gap-4">
27
+ <div class="flex items-center gap-2">
28
+ <div class="w-8 h-8 rounded-lg bg-cyan-500/10 border border-cyan-500/30 flex items-center justify-center">
29
+ <span class="text-cyan-400 text-lg font-bold">D</span>
30
+ </div>
31
+ <span class="font-semibold text-slate-100">DCC Validator</span>
32
+ <span class="text-xs text-slate-500 hidden sm:inline">v{{ version }}</span>
33
+ </div>
34
+ <div class="flex-1"></div>
35
+ <button @click="loadDemo" class="px-3 py-1.5 text-xs font-medium rounded-lg border border-slate-700 hover:border-cyan-500/50 hover:text-cyan-400 transition-colors">
36
+ Load Demo Certificate
37
+ </button>
38
+ <label class="cursor-pointer px-3 py-1.5 text-xs font-medium rounded-lg bg-cyan-500/10 border border-cyan-500/30 text-cyan-400 hover:bg-cyan-500/20 transition-colors">
39
+ Upload DCC XML
40
+ <input type="file" accept=".xml,.dcc" class="hidden" @change="onUpload($event)">
41
+ </label>
42
+ </div>
43
+ </div>
44
+
45
+ <!-- Drop zone (when no cert loaded) -->
46
+ <div v-if="!cert && !loading" class="max-w-2xl mx-auto px-6 pt-24">
47
+ <div
48
+ @dragover.prevent="dragActive = true"
49
+ @dragleave.prevent="dragActive = false"
50
+ @drop.prevent="onDrop($event)"
51
+ :class="['border-2 border-dashed rounded-2xl p-16 text-center transition-all', dragActive ? 'border-cyan-500 bg-cyan-500/5' : 'border-slate-800']"
52
+ >
53
+ <div class="text-5xl mb-4 opacity-30">📋</div>
54
+ <p class="text-slate-400 mb-1">Drag a DCC XML file here</p>
55
+ <p class="text-slate-600 text-sm">or click upload above</p>
56
+ <button @click="loadDemo" class="mt-6 px-4 py-2 text-sm font-medium rounded-lg bg-cyan-500/10 border border-cyan-500/30 text-cyan-400 hover:bg-cyan-500/20 transition-colors">
57
+ Try a demo certificate →
58
+ </button>
59
+ </div>
60
+ </div>
61
+
62
+ <!-- Loading -->
63
+ <div v-if="loading" class="max-w-2xl mx-auto px-6 pt-32 text-center">
64
+ <div class="inline-block w-8 h-8 border-2 border-cyan-500/30 border-t-cyan-500 rounded-full animate-spin mb-4"></div>
65
+ <p class="text-slate-400 text-sm">Validating certificate...</p>
66
+ </div>
67
+
68
+ <!-- Certificate view -->
69
+ <div v-if="cert && !loading" class="max-w-5xl mx-auto px-6 py-8 space-y-4">
70
+
71
+ <!-- Validity banner -->
72
+ <div :class="['rounded-2xl p-5 border slide-up flex items-center gap-4',
73
+ validation.ok ? 'bg-green-500/5 border-green-500/20' : 'bg-red-500/5 border-red-500/20']">
74
+ <div :class="['w-12 h-12 rounded-xl flex items-center justify-center text-2xl',
75
+ validation.ok ? 'bg-green-500/10' : 'bg-red-500/10']">
76
+ {{ validation.ok ? '✓' : '✕' }}
77
+ </div>
78
+ <div class="flex-1">
79
+ <div :class="['font-semibold text-lg', validation.ok ? 'text-green-400' : 'text-red-400']">
80
+ {{ validation.ok ? 'Certificate Valid' : 'Validation Failed' }}
81
+ </div>
82
+ <div class="text-sm text-slate-500">
83
+ {{ validation.errors.length }} error{{ validation.errors.length === 1 ? '' : 's' }},
84
+ {{ validation.warnings.length }} warning{{ validation.warnings.length === 1 ? '' : 's' }}
85
+ <span v-if="cert.schemaVersion"> · Schema v{{ cert.schemaVersion }}</span>
86
+ </div>
87
+ </div>
88
+ <div class="flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium"
89
+ :class="validation.ok ? 'bg-green-500/10 text-green-400 border border-green-500/20' : 'bg-red-500/10 text-red-400 border border-red-500/20'">
90
+ <span class="w-1.5 h-1.5 rounded-full pulse-dot" :class="validation.ok ? 'bg-green-400' : 'bg-red-400'"></span>
91
+ {{ validation.ok ? 'VALID' : 'INVALID' }}
92
+ </div>
93
+ </div>
94
+
95
+ <!-- Errors list -->
96
+ <div v-if="validation.errors.length" class="slide-up rounded-xl border border-red-500/15 bg-red-500/[0.03] overflow-hidden">
97
+ <div class="px-5 py-3 border-b border-red-500/10 text-xs font-semibold uppercase tracking-wide text-red-400/80">Validation Errors</div>
98
+ <div class="divide-y divide-red-500/5">
99
+ <div v-for="e in validation.errors" :key="e.line" class="px-5 py-3 flex gap-3">
100
+ <span class="text-red-500/50 text-xs font-mono pt-0.5">{{ e.line ? 'L'+e.line : '—' }}</span>
101
+ <span class="text-sm text-slate-300">{{ e.message }}</span>
102
+ </div>
103
+ </div>
104
+ </div>
105
+
106
+ <!-- Certificate identity -->
107
+ <div class="slide-up rounded-2xl border border-slate-800 bg-slate-900 overflow-hidden">
108
+ <div class="bg-gradient-to-br from-slate-900 to-slate-800/50 p-6 relative overflow-hidden">
109
+ <div class="absolute top-0 right-0 w-64 h-64 bg-cyan-500/5 rounded-full blur-3xl"></div>
110
+ <div class="relative flex justify-between items-start gap-4">
111
+ <div>
112
+ <div class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[10px] font-semibold uppercase tracking-wider border border-cyan-500/30 text-cyan-400 bg-cyan-500/5 mb-3">
113
+ <span class="w-1 h-1 rounded-full bg-cyan-400 pulse-dot"></span>
114
+ Digital Calibration Certificate
115
+ </div>
116
+ <h1 class="text-2xl font-bold text-slate-100 mb-1">{{ cert.itemName || 'Calibration Certificate' }}</h1>
117
+ <p class="text-slate-400">{{ cert.labName || '' }}</p>
118
+ <p class="font-mono text-xs text-slate-600 mt-1.5">ID: {{ cert.uniqueId || '—' }}</p>
119
+ </div>
120
+ <div class="text-right border-l border-slate-700 pl-6">
121
+ <div class="text-[10px] uppercase tracking-wider text-slate-500">Schema</div>
122
+ <div class="font-mono text-green-400 font-medium">v{{ cert.schemaVersion }}</div>
123
+ <div class="mt-2 text-[10px] uppercase tracking-wider text-slate-500">Country</div>
124
+ <div class="font-mono text-lg">{{ cert.country || '—' }}</div>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ </div>
129
+
130
+ <!-- Core data -->
131
+ <div v-if="cert.coreData" class="slide-up rounded-xl border border-slate-800 bg-slate-900 overflow-hidden">
132
+ <div class="px-5 py-3 border-b border-slate-800 flex items-center gap-2">
133
+ <span class="text-slate-500">⚙</span>
134
+ <span class="text-xs font-semibold uppercase tracking-wide text-slate-500">Core Data</span>
135
+ </div>
136
+ <div class="p-5 grid grid-cols-2 sm:grid-cols-3 gap-px bg-slate-800/50">
137
+ <div class="bg-slate-900 p-3" v-for="f in cert.coreData">
138
+ <div class="text-[10px] uppercase tracking-wider text-slate-600 mb-0.5">{{ f.label }}</div>
139
+ <div class="font-mono text-sm text-slate-200">{{ f.value }}</div>
140
+ </div>
141
+ </div>
142
+ </div>
143
+
144
+ <!-- Measurement results -->
145
+ <div v-if="cert.measurements && cert.measurements.length" class="slide-up rounded-xl border border-slate-800 bg-slate-900 overflow-hidden">
146
+ <div class="px-5 py-3 border-b border-slate-800 flex items-center gap-2">
147
+ <span class="text-slate-500">⚡</span>
148
+ <span class="text-xs font-semibold uppercase tracking-wide text-slate-500">Measurement Results</span>
149
+ </div>
150
+ <div class="p-5 space-y-3">
151
+ <div v-for="mr in cert.measurements" :key="mr.name" class="rounded-lg border border-slate-800 bg-slate-800/40 p-4">
152
+ <div class="font-medium text-slate-200 mb-3">{{ mr.name }}</div>
153
+ <div v-if="mr.results && mr.results.length" class="space-y-2">
154
+ <div v-for="r in mr.results" :key="r.name" class="flex items-center justify-between py-2 border-l-2 border-cyan-500/30 pl-3 bg-cyan-500/[0.03] rounded-r-lg">
155
+ <span class="text-sm text-slate-400">{{ r.name }}</span>
156
+ <span v-if="r.values" class="font-mono text-sm">
157
+ <span v-for="(v, i) in r.values" :key="i" class="ml-2">
158
+ <span class="text-green-400">{{ v.value }}</span>
159
+ <span class="text-slate-600"> {{ v.unit }}</span>
160
+ <span v-if="v.uncertainty" class="text-amber-400/60 text-xs"> ±{{ v.uncertainty }}</span>
161
+ </span>
162
+ </span>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ </div>
168
+
169
+ <!-- Lab / Signer -->
170
+ <div class="slide-up grid sm:grid-cols-2 gap-4">
171
+ <div v-if="cert.lab" class="rounded-xl border border-slate-800 bg-slate-900 overflow-hidden">
172
+ <div class="px-5 py-3 border-b border-slate-800 text-xs font-semibold uppercase tracking-wide text-slate-500">Calibration Laboratory</div>
173
+ <div class="p-5">
174
+ <div class="font-medium text-slate-200 mb-1">{{ cert.lab.name }}</div>
175
+ <div v-if="cert.lab.email" class="font-mono text-xs text-slate-500">{{ cert.lab.email }}</div>
176
+ <div v-if="cert.lab.address" class="text-sm text-slate-400 mt-2">{{ cert.lab.address }}</div>
177
+ </div>
178
+ </div>
179
+ <div v-if="cert.signers && cert.signers.length" class="rounded-xl border border-slate-800 bg-slate-900 overflow-hidden">
180
+ <div class="px-5 py-3 border-b border-slate-800 text-xs font-semibold uppercase tracking-wide text-slate-500">Responsible Persons</div>
181
+ <div class="p-5 space-y-2">
182
+ <div v-for="s in cert.signers" :key="s.name" class="flex items-center gap-3 rounded-lg border border-slate-800 bg-slate-800/40 px-3 py-2">
183
+ <div class="w-8 h-8 rounded-full bg-cyan-500 text-slate-900 flex items-center justify-center text-xs font-bold">{{ initials(s.name) }}</div>
184
+ <div class="flex-1">
185
+ <div class="text-sm font-medium text-slate-200">{{ s.name }}</div>
186
+ <div v-if="s.email" class="font-mono text-xs text-slate-500">{{ s.email }}</div>
187
+ </div>
188
+ <span v-if="s.main" class="px-2 py-0.5 rounded-full text-[10px] font-semibold uppercase tracking-wider text-green-400 border border-green-500/20 bg-green-500/10">Main</span>
189
+ </div>
190
+ </div>
191
+ </div>
192
+ </div>
193
+
194
+ <!-- XML source -->
195
+ <div class="slide-up rounded-xl border border-slate-800 bg-slate-900 overflow-hidden">
196
+ <button @click="showXml = !showXml" class="w-full px-5 py-3 flex items-center justify-between text-xs font-semibold uppercase tracking-wide text-slate-500 hover:bg-slate-800/50 transition-colors">
197
+ <span>XML Source</span>
198
+ <span>{{ showXml ? '▲' : '▼' }}</span>
199
+ </button>
200
+ <div v-if="showXml" class="p-5 overflow-auto max-h-96">
201
+ <pre class="text-xs text-slate-400 font-mono whitespace-pre-wrap">{{ rawXml }}</pre>
202
+ </div>
203
+ </div>
204
+
205
+ </div>
206
+ </div>
207
+
208
+ <script>
209
+ (function() {
210
+ const { createApp, ref, computed } = Vue;
211
+
212
+ createApp({
213
+ setup() {
214
+ const cert = ref(null);
215
+ const loading = ref(false);
216
+ const dragActive = ref(false);
217
+ const showXml = ref(false);
218
+ const rawXml = ref('');
219
+ const validation = ref({ ok: true, errors: [], warnings: [] });
220
+ const version = ref('0.2.0');
221
+
222
+ async function validate(xml) {
223
+ loading.value = true;
224
+ try {
225
+ const res = await fetch('/validate', {
226
+ method: 'POST',
227
+ headers: { 'Content-Type': 'application/xml' },
228
+ body: xml,
229
+ });
230
+ const data = await res.json();
231
+ cert.value = data.cert;
232
+ validation.value = data.validation;
233
+ rawXml.value = xml;
234
+ } catch (e) {
235
+ validation.value = { ok: false, errors: [{ line: null, message: 'Server error: ' + e.message }], warnings: [] };
236
+ }
237
+ loading.value = false;
238
+ }
239
+
240
+ async function loadDemo() {
241
+ loading.value = true;
242
+ try {
243
+ const res = await fetch('/demo.xml');
244
+ const xml = await res.text();
245
+ await validate(xml);
246
+ } catch (e) {
247
+ loading.value = false;
248
+ }
249
+ }
250
+
251
+ function onUpload(e) {
252
+ const file = e.target.files[0];
253
+ if (!file) return;
254
+ const reader = new FileReader();
255
+ reader.onload = (ev) => validate(ev.target.result);
256
+ reader.readAsText(file);
257
+ }
258
+
259
+ function onDrop(e) {
260
+ dragActive.value = false;
261
+ const file = e.dataTransfer.files[0];
262
+ if (!file) return;
263
+ const reader = new FileReader();
264
+ reader.onload = (ev) => validate(ev.target.result);
265
+ reader.readAsText(file);
266
+ }
267
+
268
+ function initials(name) {
269
+ if (!name) return '?';
270
+ const parts = name.trim().split(/\s+/);
271
+ if (parts.length === 1) return parts[0][0].toUpperCase();
272
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
273
+ }
274
+
275
+ return { cert, loading, dragActive, showXml, rawXml, validation, version,
276
+ loadDemo, onUpload, onDrop, initials };
277
+ }
278
+ }).mount('#app');
279
+ })();
280
+ </script>
281
+ </body>
282
+ </html>