dcc 0.2.0 → 0.2.1

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 (167) 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 +8 -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 +36 -18
  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/ring.rb +1 -1
  49. data/lib/dcc/extract.rb +1 -1
  50. data/lib/dcc/i18n/text_lookup.rb +8 -4
  51. data/lib/dcc/i18n.rb +1 -1
  52. data/lib/dcc/inspect/report.rb +51 -14
  53. data/lib/dcc/inspect.rb +1 -1
  54. data/lib/dcc/migrate/route.rb +44 -0
  55. data/lib/dcc/migrate/v2_to_v3.rb +445 -0
  56. data/lib/dcc/migrate.rb +130 -26
  57. data/lib/dcc/plugin/base.rb +64 -0
  58. data/lib/dcc/plugin/registry.rb +7 -2
  59. data/lib/dcc/plugin.rb +6 -2
  60. data/lib/dcc/quantity_format/formatter.rb +16 -10
  61. data/lib/dcc/quantity_format.rb +1 -1
  62. data/lib/dcc/quantity_math/quantity.rb +1 -1
  63. data/lib/dcc/quantity_math/real.rb +6 -6
  64. data/lib/dcc/quantity_math.rb +1 -1
  65. data/lib/dcc/schema/version.rb +2 -2
  66. data/lib/dcc/server/app.html +282 -0
  67. data/lib/dcc/server/app.rb +171 -87
  68. data/lib/dcc/server/demo_cert.xml +244 -0
  69. data/lib/dcc/server/storage/entry.rb +1 -1
  70. data/lib/dcc/server/storage/memory.rb +3 -2
  71. data/lib/dcc/server/storage.rb +1 -1
  72. data/lib/dcc/server.rb +1 -1
  73. data/lib/dcc/si/base/complex.rb +1 -1
  74. data/lib/dcc/si/base/complex_list_xml_list.rb +1 -1
  75. data/lib/dcc/si/base/constant.rb +1 -1
  76. data/lib/dcc/si/base/coverage_interval.rb +36 -0
  77. data/lib/dcc/si/base/coverage_interval_mu.rb +1 -1
  78. data/lib/dcc/si/base/expanded_mu.rb +1 -1
  79. data/lib/dcc/si/base/expanded_unc.rb +34 -0
  80. data/lib/dcc/si/base/expanded_unc_xml_list.rb +3 -2
  81. data/lib/dcc/si/base/hybrid.rb +3 -2
  82. data/lib/dcc/si/base/measurement_uncertainty_univariate.rb +1 -1
  83. data/lib/dcc/si/base/real.rb +13 -3
  84. data/lib/dcc/si/base/real_list_xml_list.rb +5 -3
  85. data/lib/dcc/si/base/si_list.rb +3 -2
  86. data/lib/dcc/si/base/standard_mu.rb +1 -1
  87. data/lib/dcc/si/base.rb +5 -2
  88. data/lib/dcc/si/v1/coverage_interval.rb +8 -0
  89. data/lib/dcc/si/v1/expanded_unc.rb +1 -16
  90. data/lib/dcc/si/v1/measurement_uncertainty_univariate.rb +2 -1
  91. data/lib/dcc/si/v1.rb +5 -2
  92. data/lib/dcc/si/v2/coverage_interval.rb +8 -0
  93. data/lib/dcc/si/v2/coverage_interval_mu_xml_list.rb +4 -2
  94. data/lib/dcc/si/v2/expanded_mu_xml_list.rb +2 -1
  95. data/lib/dcc/si/v2/expanded_unc.rb +8 -0
  96. data/lib/dcc/si/v2/measurement_uncertainty_univariate.rb +2 -1
  97. data/lib/dcc/si/v2.rb +8 -3
  98. data/lib/dcc/signature/result.rb +2 -2
  99. data/lib/dcc/signature/signer.rb +31 -18
  100. data/lib/dcc/signature/verifier.rb +23 -21
  101. data/lib/dcc/signature.rb +1 -1
  102. data/lib/dcc/transform/result.rb +1 -1
  103. data/lib/dcc/transform/xslt.rb +22 -12
  104. data/lib/dcc/transform.rb +5 -4
  105. data/lib/dcc/type/base64_binary.rb +5 -3
  106. data/lib/dcc/type/decimal_xml_list.rb +99 -13
  107. data/lib/dcc/type/iso_country_code.rb +3 -2
  108. data/lib/dcc/type/iso_language_code.rb +3 -2
  109. data/lib/dcc/type/schema_version.rb +3 -2
  110. data/lib/dcc/type/si_unit.rb +3 -2
  111. data/lib/dcc/type_guards.rb +3 -3
  112. data/lib/dcc/v2/calibration_laboratory.rb +2 -1
  113. data/lib/dcc/v2/condition.rb +22 -1
  114. data/lib/dcc/v2/configuration.rb +1 -1
  115. data/lib/dcc/v2/digital_calibration_certificate.rb +2 -1
  116. data/lib/dcc/v2/identification.rb +14 -1
  117. data/lib/dcc/v2/mathml.rb +8 -0
  118. data/lib/dcc/v2/measurement_meta_data_list.rb +2 -1
  119. data/lib/dcc/v2/quantity.rb +4 -2
  120. data/lib/dcc/v2.rb +4 -2
  121. data/lib/dcc/v3/calibration_laboratory.rb +2 -1
  122. data/lib/dcc/v3/configuration.rb +1 -1
  123. data/lib/dcc/v3/digital_calibration_certificate.rb +2 -1
  124. data/lib/dcc/v3/mathml.rb +8 -0
  125. data/lib/dcc/v3/measurement_meta_data_list.rb +2 -1
  126. data/lib/dcc/v3/quantity.rb +4 -2
  127. data/lib/dcc/v3.rb +12 -2
  128. data/lib/dcc/validate/business_rules.rb +23 -9
  129. data/lib/dcc/validate/issue.rb +2 -1
  130. data/lib/dcc/validate/result.rb +6 -4
  131. data/lib/dcc/validate/schematron/profile.rb +73 -18
  132. data/lib/dcc/validate/schematron/rule.rb +13 -3
  133. data/lib/dcc/validate/schematron/rules/administrative_data_completeness.rb +11 -4
  134. data/lib/dcc/validate/schematron/rules/base.rb +1 -2
  135. data/lib/dcc/validate/schematron/rules/date_range_check.rb +11 -5
  136. data/lib/dcc/validate/schematron/rules/id_refid_linking.rb +0 -3
  137. data/lib/dcc/validate/schematron/rules/influence_conditions_placement.rb +22 -12
  138. data/lib/dcc/validate/schematron/rules/iso_code_validation.rb +31 -14
  139. data/lib/dcc/validate/schematron/rules/language_code_dedup.rb +12 -5
  140. data/lib/dcc/validate/schematron/rules/non_si_declaration.rb +7 -4
  141. data/lib/dcc/validate/schematron/rules/release_format_check.rb +7 -4
  142. data/lib/dcc/validate/schematron/rules/schema_version_check.rb +13 -9
  143. data/lib/dcc/validate/schematron/rules/uncertainty_consistency.rb +39 -10
  144. data/lib/dcc/validate/schematron/rules/unit_format_check.rb +2 -3
  145. data/lib/dcc/validate/schematron/rules/used_methods_placement.rb +15 -10
  146. data/lib/dcc/validate/schematron/rules/used_software_placement.rb +15 -10
  147. data/lib/dcc/validate/schematron/rules/xml_list_spacing.rb +4 -3
  148. data/lib/dcc/validate/schematron/rules.rb +28 -14
  149. data/lib/dcc/validate/schematron.rb +1 -1
  150. data/lib/dcc/validate/xsd.rb +9 -3
  151. data/lib/dcc/validate.rb +1 -1
  152. data/lib/dcc/version.rb +1 -1
  153. data/lib/dcc/versioned_parser.rb +1 -1
  154. data/lib/dcc.rb +55 -7
  155. data/sample_dcc.xml +140 -0
  156. data/web/index.html +12 -0
  157. data/web/package-lock.json +1599 -0
  158. data/web/package.json +22 -0
  159. data/web/src/App.vue +349 -0
  160. data/web/src/lib/builder.ts +171 -0
  161. data/web/src/lib/dcc.ts +338 -0
  162. data/web/src/lib/units.json +5581 -0
  163. data/web/src/main.ts +5 -0
  164. data/web/src/style.css +27 -0
  165. data/web/tsconfig.json +15 -0
  166. data/web/vite.config.ts +14 -0
  167. metadata +125 -9
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dcc
4
+ module Plugin
5
+ # Included by a plugin class so it can declare what it adds to Dcc.
6
+ #
7
+ # @example
8
+ # class MyPlugin
9
+ # include Dcc::Plugin::Base
10
+ # register_validator MyRule
11
+ # end
12
+ module Base
13
+ # @param base [Class] the including class.
14
+ def self.included(base)
15
+ base.extend(ClassMethods)
16
+ end
17
+
18
+ # `Profile#call` does `rule_class.new.check_on(dcc)`, so a validator
19
+ # has to be a class. An instance responds to `#check_on` and so looks
20
+ # right, but would fail deep inside validation with a message naming
21
+ # neither the plugin nor the author's line.
22
+ #
23
+ # @param entry [Object] the candidate rule.
24
+ # @raise [ArgumentError] unless entry is a class defining #check_on.
25
+ # @return [Class] the entry.
26
+ def self.rule_class!(entry)
27
+ return entry if rule_class?(entry)
28
+
29
+ raise ::ArgumentError,
30
+ "expected a rule class responding to #check_on, " \
31
+ "got #{describe(entry)}"
32
+ end
33
+
34
+ # @param entry [Object]
35
+ # @return [Boolean]
36
+ def self.rule_class?(entry)
37
+ entry.is_a?(::Class) && entry.method_defined?(:check_on)
38
+ end
39
+ private_class_method :rule_class?
40
+
41
+ # @param entry [Object]
42
+ # @return [String]
43
+ def self.describe(entry)
44
+ return entry.inspect if entry.is_a?(::Class)
45
+
46
+ "an instance of #{entry.class}"
47
+ end
48
+ private_class_method :describe
49
+
50
+ # Declaration helpers available on the including class.
51
+ module ClassMethods
52
+ # Add a Schematron rule to the active validation profile.
53
+ #
54
+ # @param rule_class [Class] a rule class defining `#check_on(dcc)`.
55
+ # @raise [ArgumentError] unless rule_class is such a class.
56
+ # @return [Class] the rule class.
57
+ def register_validator(rule_class)
58
+ checked = ::Dcc::Plugin::Base.rule_class!(rule_class)
59
+ ::Dcc::Plugin.register(:validators, checked)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -9,12 +9,17 @@ module Dcc
9
9
  @mutex = ::Mutex.new
10
10
 
11
11
  class << self
12
+ # Idempotent per (category, entry): registering the same entry in
13
+ # the same category twice is a no-op, so a doubly-loaded plugin does
14
+ # not run its validators twice.
15
+ #
12
16
  # @param category [Symbol]
13
17
  # @param entry [Class, Object]
14
18
  # @return [Class, Object]
15
19
  def register(category, entry)
16
20
  @mutex.synchronize do
17
- @categories[category.to_sym] << entry
21
+ entries = @categories[category.to_sym]
22
+ entries << entry unless entries.include?(entry)
18
23
  entry
19
24
  end
20
25
  end
@@ -42,4 +47,4 @@ module Dcc
42
47
  end
43
48
  end
44
49
  end
45
- end
50
+ end
data/lib/dcc/plugin.rb CHANGED
@@ -5,13 +5,17 @@
5
5
  # without modifying the core codebase (Open/Closed Principle).
6
6
  #
7
7
  # @example Register a custom validator
8
- # Dcc::Plugin.register(:validators, MyCustomRule)
8
+ # class MyPlugin
9
+ # include Dcc::Plugin::Base
10
+ # register_validator MyCustomRule
11
+ # end
9
12
  #
10
13
  # @example List all registered validators
11
14
  # Dcc::Plugin.all(:validators)
12
15
  module Dcc
13
16
  module Plugin
14
17
  autoload :Registry, "dcc/plugin/registry"
18
+ autoload :Base, "dcc/plugin/base"
15
19
 
16
20
  class << self
17
21
  # @param category [Symbol] e.g. :validators, :converters, :cli_commands.
@@ -39,4 +43,4 @@ module Dcc
39
43
  end
40
44
  end
41
45
  end
42
- end
46
+ end
@@ -45,25 +45,24 @@ module Dcc
45
45
  private
46
46
 
47
47
  def format_value
48
- # Two decimals by default; tightened to uncertainty precision when known.
49
- return @value.round(2).to_s("F") if uncertainty.nil?
48
+ # Two decimals by default, tightened to the uncertainty's precision
49
+ # when it has any. Neither a nil nor a NaN uncertainty has one.
50
+ return @value.round(2).to_s("F") if uncertainty.nil? || uncertainty.nan?
50
51
 
51
- # Decimal places = 1 - floor(log10(uncertainty))
52
- decimal_places = [0, 1 - Integer(Math.log10(uncertainty.to_f).floor)].max
53
52
  @value.round(decimal_places).to_s("F")
54
53
  end
55
54
 
56
55
  def format_uncertainty
57
56
  return "" if uncertainty.nil?
57
+ return "NaN" if uncertainty.nan?
58
58
 
59
- decimal_places = [0, 1 - Integer(Math.log10(uncertainty.to_f).floor)].max
60
59
  uncertainty.round(decimal_places).to_s("F")
61
60
  end
62
61
 
63
62
  def align_value_and_uncertainty
64
63
  return [@value.to_s("F"), uncertainty.to_s("F")] if uncertainty.nil?
64
+ return [format_value, "NaN"] if uncertainty.nan?
65
65
 
66
- decimal_places = [0, 1 - Integer(Math.log10(uncertainty.to_f).floor)].max
67
66
  v_str = @value.round(decimal_places).to_s("F")
68
67
  u_str = uncertainty.round(decimal_places).to_s("F")
69
68
  # Short form: digits of uncertainty after the last common digit
@@ -71,6 +70,13 @@ module Dcc
71
70
  [v_str, last_digits]
72
71
  end
73
72
 
73
+ # Decimal places = 1 - floor(log10(uncertainty)). D-SI writes `NaN` for a
74
+ # not-measured entry (SI_Format.xsd:1195) and `Math.log10` cannot answer
75
+ # for it, so every caller excludes nil and NaN first.
76
+ def decimal_places
77
+ [0, 1 - Integer(Math.log10(uncertainty.to_f).floor)].max
78
+ end
79
+
74
80
  def unit_suffix
75
81
  return "" if unit.nil? || unit.empty?
76
82
 
@@ -81,10 +87,10 @@ module Dcc
81
87
  unit
82
88
  .to_s
83
89
  .gsub(/\\([a-z]+)/, '\\1') # \kelvin → kelvin
84
- .gsub(/\\per/, '/')
85
- .gsub(/\\cubic/, '^3')
86
- .gsub(/\\square/, '^2')
90
+ .gsub("\\per", "/")
91
+ .gsub("\\cubic", "^3")
92
+ .gsub("\\square", "^2")
87
93
  end
88
94
  end
89
95
  end
90
- end
96
+ end
@@ -7,4 +7,4 @@ module Dcc
7
7
  module QuantityFormat
8
8
  autoload :Formatter, "dcc/quantity_format/formatter"
9
9
  end
10
- end
10
+ end
@@ -43,4 +43,4 @@ module Dcc
43
43
  end
44
44
  end
45
45
  end
46
- end
46
+ end
@@ -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
@@ -11,4 +11,4 @@ module Dcc
11
11
  autoload :Quantity, "dcc/quantity_math/quantity"
12
12
  autoload :Real, "dcc/quantity_math/real"
13
13
  end
14
- end
14
+ 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>