19 Commits

Author SHA1 Message Date
120b61a423 Merge pull request 'v1.3.0' (#35) from dev into main
All checks were successful
CI / test (push) Successful in 14s
Release / release (push) Successful in 40s
CI / build-dev (push) Has been skipped
Reviewed-on: #35
2026-03-28 15:31:57 -04:00
074f0600af Merge pull request 'chore: release v1.3.0' (#34) from chore/bump-v1.3.0 into dev
All checks were successful
CI / test (push) Successful in 14s
CI / build-dev (push) Successful in 21s
CI / test (pull_request) Successful in 14s
CI / build-dev (pull_request) Has been skipped
Reviewed-on: #34
2026-03-28 15:30:02 -04:00
e4f9407827 Merge branch 'dev' into chore/bump-v1.3.0
All checks were successful
CI / test (pull_request) Successful in 13s
CI / build-dev (pull_request) Has been skipped
2026-03-28 15:29:09 -04:00
fde5ce7dc1 Merge pull request 'chore: release v1.3.0' (#33) from chore/bump-v1.3.0 into dev
All checks were successful
CI / test (push) Successful in 13s
CI / build-dev (push) Successful in 28s
Reviewed-on: #33
2026-03-28 15:28:44 -04:00
20df10b333 chore: release v1.3.0
All checks were successful
CI / test (pull_request) Successful in 13s
CI / build-dev (pull_request) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 15:28:39 -04:00
c906511bfc chore: release v1.3.0
All checks were successful
CI / test (pull_request) Successful in 13s
CI / build-dev (pull_request) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 15:27:30 -04:00
745e5920ad Merge pull request 'fix: set html zoom 1.1 so default scale matches browser 110%' (#32) from fix/base-zoom into dev
All checks were successful
CI / test (push) Successful in 12s
CI / build-dev (push) Successful in 20s
Reviewed-on: #32
2026-03-28 15:25:32 -04:00
90e0a98914 fix: set html zoom 1.1 so default scale matches browser 110%
All checks were successful
CI / test (pull_request) Successful in 13s
CI / build-dev (pull_request) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 15:24:58 -04:00
cba4b73798 Merge pull request 'fix: use badge for stack on detail overview, consistent across all views' (#31) from fix/detail-stack-badge into dev
All checks were successful
CI / test (push) Successful in 14s
CI / build-dev (push) Successful in 21s
Reviewed-on: #31
2026-03-28 15:22:13 -04:00
0d567472a9 fix: use badge for stack on detail overview, consistent across all views
All checks were successful
CI / test (pull_request) Successful in 12s
CI / build-dev (pull_request) Has been skipped
stack was plain highlighted text on the detail page but a coloured badge
on the home cards and in the history timeline. Now all three use the same
badge component.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 15:21:25 -04:00
9f6b2ece52 Merge pull request 'fix: parse SQLite timestamps as UTC, not local time' (#30) from fix/sqlite-utc-timestamps into dev
All checks were successful
CI / test (push) Successful in 12s
CI / build-dev (push) Successful in 20s
Reviewed-on: #30
2026-03-28 15:20:15 -04:00
e3911157e9 fix: parse SQLite timestamps as UTC, not local time
All checks were successful
CI / test (pull_request) Successful in 14s
CI / build-dev (pull_request) Has been skipped
SQLite datetime('now') returns 'YYYY-MM-DD HH:MM:SS' with no timezone
marker. JS was treating this as local time, so timestamps showed the
correct UTC digits but with the local TZ abbreviation attached (e.g.
'7:15 PM EDT' when the real local time was '3:15 PM EDT').

Add parseUtc() which appends 'Z' before parsing any string that has no
existing timezone marker, ensuring JS always treats them as UTC before
the display-timezone conversion is applied.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 15:19:35 -04:00
0589288dfe Merge pull request 'fix: populate nav instance count on direct detail page load/refresh' (#29) from fix/detail-nav-count into dev
All checks were successful
CI / test (push) Successful in 13s
CI / build-dev (push) Successful in 21s
Reviewed-on: #29
2026-03-28 15:16:32 -04:00
8ead7687e5 fix: populate nav instance count on direct detail page load/refresh
All checks were successful
CI / test (pull_request) Successful in 13s
CI / build-dev (pull_request) Has been skipped
nav-count was only set in renderDashboard, so loading /instance/:vmid
directly left it showing "—". Add getInstances() to the parallel fetch
in renderDetailPage and set the count there too.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 15:15:55 -04:00
0e1e9b6699 Merge pull request 'fix: show stack badge in history timeline, matching state treatment' (#28) from fix/history-stack-badge into dev
All checks were successful
CI / test (push) Successful in 13s
CI / build-dev (push) Successful in 22s
Reviewed-on: #28
2026-03-28 15:14:02 -04:00
3c008c5bce fix: show stack badge in history timeline, matching state treatment
All checks were successful
CI / test (pull_request) Successful in 13s
CI / build-dev (pull_request) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 15:13:27 -04:00
cd16b7ea28 Merge pull request 'v1.2.2' (#16) from dev into main
All checks were successful
CI / test (push) Successful in 13s
Release / release (push) Successful in 34s
CI / build-dev (push) Has been skipped
Reviewed-on: #16
2026-03-28 14:01:33 -04:00
afbdefa549 Merge pull request 'v1.2.1' (#13) from dev into main
All checks were successful
CI / test (push) Successful in 14s
CI / build-dev (push) Has been skipped
Reviewed-on: #13
2026-03-28 13:55:34 -04:00
f1e192c5d4 Merge pull request 'v1.2.0' (#10) from dev into main
Some checks failed
CI / test (push) Successful in 13s
Release / release (push) Failing after 5m14s
CI / build-dev (push) Has been skipped
Reviewed-on: #10
2026-03-28 13:24:34 -04:00
6 changed files with 36 additions and 11 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,4 @@
node_modules/
js/version.js
data/*.db
data/*.db-shm
data/*.db-wal

View File

@@ -25,6 +25,10 @@
--mono: 'JetBrains Mono', 'IBM Plex Mono', monospace;
}
html {
zoom: 1.1;
}
html, body {
height: 100%;
background: var(--bg);

View File

@@ -39,17 +39,25 @@ function esc(str) {
return d.innerHTML;
}
// SQLite datetime('now') → 'YYYY-MM-DD HH:MM:SS' (UTC, no timezone marker).
// Appending 'Z' tells JS to parse it as UTC rather than local time.
function parseUtc(d) {
if (typeof d !== 'string') return new Date(d);
const hasZone = d.endsWith('Z') || /[+-]\d{2}:\d{2}$/.test(d);
return new Date(hasZone ? d : d.replace(' ', 'T') + 'Z');
}
function fmtDate(d) {
if (!d) return '—';
try {
return new Date(d).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', timeZone: getTimezone() });
return parseUtc(d).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', timeZone: getTimezone() });
} catch (e) { return d; }
}
function fmtDateFull(d) {
if (!d) return '—';
try {
return new Date(d).toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: getTimezone(), timeZoneName: 'short' });
return parseUtc(d).toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: getTimezone(), timeZoneName: 'short' });
} catch (e) { return d; }
}
@@ -156,9 +164,10 @@ function fmtHistVal(field, val) {
}
async function renderDetailPage(vmid) {
const [inst, history] = await Promise.all([getInstance(vmid), getInstanceHistory(vmid)]);
const [inst, history, all] = await Promise.all([getInstance(vmid), getInstanceHistory(vmid), getInstances()]);
if (!inst) { navigate('dashboard'); return; }
currentVmid = vmid;
document.getElementById('nav-count').textContent = `${all.length} instance${all.length !== 1 ? 's' : ''}`;
document.getElementById('detail-vmid-crumb').textContent = vmid;
document.getElementById('detail-name').textContent = inst.name;
@@ -169,7 +178,7 @@ async function renderDetailPage(vmid) {
document.getElementById('detail-identity').innerHTML = `
<div class="kv-row"><span class="kv-key">name</span><span class="kv-val highlight">${esc(inst.name)}</span></div>
<div class="kv-row"><span class="kv-key">state</span><span class="kv-val"><span class="badge ${esc(inst.state)}">${esc(inst.state)}</span></span></div>
<div class="kv-row"><span class="kv-key">stack</span><span class="kv-val highlight">${esc(inst.stack) || '—'}</span></div>
<div class="kv-row"><span class="kv-key">stack</span><span class="kv-val"><span class="badge ${esc(inst.stack)}">${esc(inst.stack) || '—'}</span></span></div>
<div class="kv-row"><span class="kv-key">vmid</span><span class="kv-val highlight">${inst.vmid}</span></div>
<div class="kv-row"><span class="kv-key">internal id</span><span class="kv-val">${inst.id}</span></div>
`;
@@ -197,7 +206,7 @@ async function renderDetailPage(vmid) {
<span class="tl-time">${fmtDateFull(e.changed_at)}</span>
</div>`;
const label = FIELD_LABELS[e.field] ?? esc(e.field);
const newCls = e.field === 'state'
const newCls = (e.field === 'state' || e.field === 'stack')
? `badge ${esc(e.new_value)}`
: `tl-new ${stateClass(e.field, e.new_value)}`;
return `

1
js/version.js Normal file
View File

@@ -0,0 +1 @@
const VERSION = "1.3.0";

View File

@@ -1,6 +1,6 @@
{
"name": "catalyst",
"version": "1.2.2",
"version": "1.3.0",
"type": "module",
"scripts": {
"start": "node server/server.js",

View File

@@ -58,16 +58,22 @@ describe('esc', () => {
// ── fmtDate() ─────────────────────────────────────────────────────────────────
function fmtDate(d) {
function parseUtc(d) {
if (typeof d !== 'string') return new Date(d)
const hasZone = d.endsWith('Z') || /[+-]\d{2}:\d{2}$/.test(d)
return new Date(hasZone ? d : d.replace(' ', 'T') + 'Z')
}
function fmtDate(d, tz = 'UTC') {
if (!d) return '—'
try {
return new Date(d).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })
return parseUtc(d).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', timeZone: tz })
} catch (e) { return d }
}
describe('fmtDate', () => {
it('formats a valid ISO date string', () => {
const result = fmtDate('2024-03-15T00:00:00')
const result = fmtDate('2024-03-15T12:00:00Z')
expect(result).toMatch(/Mar/)
expect(result).toMatch(/15/)
expect(result).toMatch(/2024/)
@@ -91,7 +97,7 @@ describe('fmtDate', () => {
function fmtDateFull(d, tz = 'UTC') {
if (!d) return '—'
try {
return new Date(d).toLocaleString('en-US', {
return parseUtc(d).toLocaleString('en-US', {
year: 'numeric', month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit',
timeZone: tz, timeZoneName: 'short',
@@ -118,6 +124,12 @@ describe('fmtDateFull', () => {
expect(result).toMatch(/EDT/)
})
it('treats SQLite-format timestamps (space, no Z) as UTC', () => {
// SQLite datetime('now') → 'YYYY-MM-DD HH:MM:SS', no timezone marker.
// Must parse identically to the same moment expressed as ISO UTC.
expect(fmtDateFull('2024-03-15 18:30:00', 'UTC')).toBe(fmtDateFull('2024-03-15T18:30:00Z', 'UTC'))
})
it('returns — for null', () => {
expect(fmtDateFull(null)).toBe('—')
})