From b3a330589c160f5c138575a348b3dc7ec7827274 Mon Sep 17 00:00:00 2001 From: MattMRE <160173637+mattmre@users.noreply.github.com> Date: Thu, 24 Sep 2026 08:47:28 -0400 Subject: [PATCH 1/2] fix(dashboard): parse ISO timestamps in the inline fallback page The fallback document multiplied every timestamp by 1000. Unix numbers still do. Naive ISO strings are treated as UTC, and an unparseable value renders as a dash instead of Invalid Date. --- dashboard_server.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/dashboard_server.py b/dashboard_server.py index 8ebb8cc..a0e13ef 100644 --- a/dashboard_server.py +++ b/dashboard_server.py @@ -592,9 +592,17 @@ def get_inline_dashboard_html(): }; function formatTimestamp(timestamp) { - if (!timestamp) return '-'; - const date = new Date(timestamp * 1000); - return date.toLocaleString(); + if (timestamp === null || timestamp === undefined || timestamp === '') return '-'; + var date; + if (typeof timestamp === 'number' && isFinite(timestamp)) { + date = new Date(timestamp * 1000); + } else if (typeof timestamp === 'string') { + var zoned = /(?:Z|[+-]\d{2}:?\d{2})$/.test(timestamp) ? timestamp : (timestamp + 'Z'); + date = new Date(zoned); + } else { + return '-'; + } + return isNaN(date.getTime()) ? '-' : date.toLocaleString(); } function formatNumber(num) { From 89fd7ffaa330b4b4401838f183fa734e3c2dba74 Mon Sep 17 00:00:00 2001 From: MattMRE <160173637+mattmre@users.noreply.github.com> Date: Thu, 24 Sep 2026 10:44:59 -0400 Subject: [PATCH 2/2] fix(dashboard): escape the inline timestamp regex --- dashboard_server.py | 2 +- test_dashboard_ui_contract.py | 141 ++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 1 deletion(-) diff --git a/dashboard_server.py b/dashboard_server.py index a0e13ef..0f69086 100644 --- a/dashboard_server.py +++ b/dashboard_server.py @@ -597,7 +597,7 @@ def get_inline_dashboard_html(): if (typeof timestamp === 'number' && isFinite(timestamp)) { date = new Date(timestamp * 1000); } else if (typeof timestamp === 'string') { - var zoned = /(?:Z|[+-]\d{2}:?\d{2})$/.test(timestamp) ? timestamp : (timestamp + 'Z'); + var zoned = /(?:Z|[+-]\\d{2}:?\\d{2})$/.test(timestamp) ? timestamp : (timestamp + 'Z'); date = new Date(zoned); } else { return '-'; diff --git a/test_dashboard_ui_contract.py b/test_dashboard_ui_contract.py index f598c2c..a7904fd 100644 --- a/test_dashboard_ui_contract.py +++ b/test_dashboard_ui_contract.py @@ -1,6 +1,9 @@ """Markup and evidence-index contracts for the dashboard control page.""" +import json import os +import shutil +import subprocess import tempfile import unittest @@ -88,5 +91,143 @@ def test_existing_chain_file_is_counted(self): self.assertEqual(result["summary"]["evidence_chain_files_missing"], 0) +def _format_timestamp_source(text): + start = text.index("function formatTimestamp") + end = text.index("function formatNumber") + return text[start:end] + + +_NODE_DRIVER = r""" +const fs = require('fs'); +const input = JSON.parse(fs.readFileSync(0, 'utf8')); +const formatTimestamp = new Function(`${input.fn} +return formatTimestamp;`)(); + +function run(value) { + try { + return {ok: true, value: formatTimestamp(value)}; + } catch (err) { + return {ok: false, error: String(err && err.stack ? err.stack : err)}; + } +} + +const doubled = 1700000000 * 1000 * 1000; +const payload = { + zero: run(0), + seconds: run(1700000000), + naive: run('2026-09-24T00:00:00'), + zulu: run('2026-09-24T00:00:00Z'), + offset: run('2026-09-24T00:00:00-05:00'), + bad: run('not-a-time'), + meta: { + zeroLocale: new Date(0).toLocaleString(), + zeroUtcYear: new Date(0).getUTCFullYear(), + secondsScaledLocale: new Date(1700000000 * 1000).toLocaleString(), + secondsScaledUtcYear: new Date(1700000000 * 1000).getUTCFullYear(), + secondsRawLocale: new Date(1700000000).toLocaleString(), + secondsRawUtcYear: new Date(1700000000).getUTCFullYear(), + secondsDoubledLocale: new Date(doubled).toLocaleString(), + secondsDoubledUtcYear: new Date(doubled).getUTCFullYear(), + naiveUtcLocale: new Date('2026-09-24T00:00:00Z').toLocaleString(), + naiveLocalLocale: new Date('2026-09-24T00:00:00').toLocaleString(), + zuluLocale: new Date('2026-09-24T00:00:00Z').toLocaleString(), + offsetLocale: new Date('2026-09-24T00:00:00-05:00').toLocaleString(), + offsetMinutes: new Date('2026-09-24T00:00:00Z').getTimezoneOffset(), + }, +}; +process.stdout.write(JSON.stringify(payload)); +""" + + +class TestInlineDashboardTimestamp(unittest.TestCase): + def test_format_timestamp_scales_numbers_and_keeps_regex_backslash_d(self): + source_path = os.path.splitext(dashboard_server.__file__)[0] + ".py" + with open(source_path, encoding="utf-8") as handle: + source_fn = _format_timestamp_source(handle.read()) + html_fn = _format_timestamp_source(dashboard_server.get_inline_dashboard_html()) + + # Python source writes \\d; the runtime HTML string contains \d, not \\d. + self.assertEqual(source_fn.count("\\\\d"), 2) + self.assertIn("/(?:Z|[+-]\\\\d{2}:?\\\\d{2})$/", source_fn) + self.assertEqual(html_fn.count("\\\\d"), 0) + self.assertEqual(html_fn.count("\\d"), 2) + self.assertIn("/(?:Z|[+-]\\d{2}:?\\d{2})$/", html_fn) + + number_at = html_fn.index("typeof timestamp === 'number'") + string_at = html_fn.index("typeof timestamp === 'string'") + else_at = html_fn.index("} else {", string_at) + number_branch = html_fn[number_at:string_at] + string_branch = html_fn[string_at:else_at] + self.assertIn("timestamp * 1000", number_branch) + self.assertNotIn("timestamp * 1000", string_branch) + self.assertNotIn("* 1000", string_branch) + self.assertIn("+ 'Z'", string_branch) + self.assertEqual(html_fn.count("timestamp * 1000"), 1) + + node = shutil.which("node") + if node is None: + print("node was missing") + return + + utc = self._run_node(node, html_fn, "UTC") + self._assert_common(utc) + self.assertIn("1970", utc["zero"]["value"]) + self.assertEqual(utc["zero"]["value"], utc["meta"]["zeroLocale"]) + self.assertEqual(utc["naive"]["value"], utc["meta"]["naiveUtcLocale"]) + + zoned = self._run_node(node, html_fn, "America/New_York") + self._assert_common(zoned) + self.assertNotEqual(zoned["meta"]["offsetMinutes"], 0) + self.assertEqual(zoned["naive"]["value"], zoned["meta"]["naiveUtcLocale"]) + self.assertNotEqual(zoned["naive"]["value"], zoned["meta"]["naiveLocalLocale"]) + self.assertNotIn("2001", zoned["zero"]["value"]) + + def _assert_common(self, payload): + zero = payload["zero"] + seconds = payload["seconds"] + naive = payload["naive"] + bad = payload["bad"] + meta = payload["meta"] + self.assertTrue(zero["ok"]) + self.assertTrue(seconds["ok"]) + self.assertTrue(naive["ok"]) + self.assertTrue(payload["zulu"]["ok"]) + self.assertTrue(payload["offset"]["ok"]) + self.assertTrue(bad["ok"]) + self.assertEqual(bad["value"], "-") + self.assertEqual(meta["zeroUtcYear"], 1970) + self.assertNotIn("2001", zero["value"]) + self.assertEqual(seconds["value"], meta["secondsScaledLocale"]) + self.assertNotEqual(seconds["value"], meta["secondsRawLocale"]) + self.assertNotEqual(seconds["value"], meta["secondsDoubledLocale"]) + self.assertEqual(meta["secondsScaledUtcYear"], 2023) + self.assertEqual(meta["secondsRawUtcYear"], 1970) + self.assertGreater(meta["secondsDoubledUtcYear"], 50000) + self.assertIn("2023", seconds["value"]) + self.assertNotIn(str(meta["secondsDoubledUtcYear"]), seconds["value"]) + self.assertNotEqual(naive["value"], "-") + self.assertEqual(payload["zulu"]["value"], meta["zuluLocale"]) + self.assertNotEqual(payload["zulu"]["value"], "-") + self.assertEqual(payload["offset"]["value"], meta["offsetLocale"]) + self.assertNotEqual(payload["offset"]["value"], "-") + + def _run_node(self, node, fn_source, tz_name): + env = os.environ.copy() + env["TZ"] = tz_name + env["LANG"] = "en_US.UTF-8" + env["LC_ALL"] = "en_US.UTF-8" + completed = subprocess.run( + [node, "-e", _NODE_DRIVER], + input=json.dumps({"fn": fn_source}), + capture_output=True, + text=True, + env=env, + timeout=30, + check=False, + ) + self.assertEqual(completed.returncode, 0, completed.stderr) + return json.loads(completed.stdout) + + if __name__ == "__main__": unittest.main()