diff --git a/dashboard_server.py b/dashboard_server.py index 8ebb8cc..0f69086 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) { 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()