26 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
1582c28b28 Merge pull request 'fix: clean up instance detail subtitle — dividers, readable values' (#27) from feat/timezone-settings into dev
All checks were successful
CI / test (push) Successful in 12s
CI / build-dev (push) Successful in 21s
Reviewed-on: #27
2026-03-28 15:10:54 -04:00
bcd934f5b1 Merge branch 'dev' into feat/timezone-settings
All checks were successful
CI / test (pull_request) Successful in 12s
CI / build-dev (pull_request) Has been skipped
2026-03-28 15:10:26 -04:00
4c9acd20c7 fix: clean up instance detail subtitle — dividers, readable values
All checks were successful
CI / test (pull_request) Successful in 13s
CI / build-dev (pull_request) Has been skipped
Separate vmid / id / created with a subtle vertical border so they
don't run together. Bump font to 13px. Labels drop to 11px muted,
values use full --text colour so the actual data stands out clearly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 15:10:05 -04:00
520fb98d96 Merge pull request 'feat: redesign history timeline — single-line, timestamp right-aligned' (#26) from feat/timezone-settings into dev
All checks were successful
CI / test (push) Successful in 12s
CI / build-dev (push) Successful in 19s
Reviewed-on: #26
2026-03-28 15:07:01 -04:00
800184d2be Merge branch 'dev' into feat/timezone-settings
All checks were successful
CI / test (pull_request) Successful in 12s
CI / build-dev (pull_request) Has been skipped
2026-03-28 15:06:34 -04:00
82c314f85c feat: redesign history timeline — single-line, timestamp right-aligned
All checks were successful
CI / test (pull_request) Successful in 13s
CI / build-dev (pull_request) Has been skipped
Each event is now one row: label · old → new on the left, timestamp
right-aligned. Nothing is far from anything else. State changes use the
existing badge component for immediate visual recognition. The created
event reads 'instance created' in accent. Middle-dot separator keeps
field label and change value clearly associated without forced spacing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 15:06:09 -04:00
2fba532ec7 Merge pull request 'feat: rework history timeline for clarity' (#25) from feat/timezone-settings into dev
All checks were successful
CI / test (push) Successful in 13s
CI / build-dev (push) Successful in 20s
Reviewed-on: #25
2026-03-28 15:01:39 -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 73 additions and 33 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);
@@ -374,16 +378,25 @@ select:focus { border-color: var(--accent); }
}
.detail-sub {
font-size: 12px;
color: var(--text3);
margin-top: 6px;
font-size: 13px;
margin-top: 8px;
display: flex;
gap: 16px;
align-items: center;
gap: 0;
}
.detail-sub span { display: flex; gap: 4px; }
.detail-sub .lbl { color: var(--text3); }
.detail-sub .val { color: var(--text2); }
.detail-sub > span {
display: flex;
align-items: center;
gap: 6px;
}
.detail-sub > span + span {
margin-left: 12px;
padding-left: 12px;
border-left: 1px solid var(--border);
}
.detail-sub .lbl { color: var(--text3); font-size: 11px; }
.detail-sub .val { color: var(--text); }
.detail-actions { display: flex; gap: 8px; }
@@ -630,21 +643,25 @@ select:focus { border-color: var(--accent); }
/* ── HISTORY TIMELINE ── */
.tl-item {
padding: 12px 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
padding: 9px 0;
border-bottom: 1px solid var(--border);
}
.tl-item:last-child { border-bottom: none; }
.tl-time { display: block; color: var(--text3); font-size: 11px; margin-bottom: 5px; }
.tl-desc { display: flex; align-items: baseline; gap: 16px; font-size: 13px; }
.tl-label { color: var(--text2); min-width: 130px; }
.tl-change { display: flex; align-items: baseline; gap: 8px; }
.tl-old { color: var(--text3); text-decoration: line-through; }
.tl-arrow { color: var(--text3); }
.tl-event { display: flex; align-items: center; gap: 7px; font-size: 13px; min-width: 0; }
.tl-label { color: var(--text2); }
.tl-sep { color: var(--text3); user-select: none; }
.tl-old { color: var(--text3); text-decoration: line-through; font-size: 12px; }
.tl-arrow { color: var(--text3); font-size: 11px; }
.tl-new { color: var(--text); font-weight: 500; }
.tl-time { color: var(--text3); font-size: 11px; white-space: nowrap; flex-shrink: 0; }
.tl-deployed { color: var(--accent); }
.tl-testing { color: var(--amber); }
.tl-degraded { color: var(--red); }
.tl-created .tl-label { color: var(--accent); font-weight: 500; }
.tl-created .tl-event { color: var(--accent); font-weight: 500; }
.tl-empty { color: var(--text3); font-size: 12px; padding: 8px 0; }
/* ── SETTINGS MODAL ── */

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>
`;
@@ -193,21 +202,23 @@ async function renderDetailPage(vmid) {
? history.map(e => {
if (e.field === 'created') return `
<div class="tl-item tl-created">
<span class="tl-event">instance created</span>
<span class="tl-time">${fmtDateFull(e.changed_at)}</span>
<div class="tl-desc"><span class="tl-label">instance created</span></div>
</div>`;
const label = FIELD_LABELS[e.field] ?? esc(e.field);
const newCls = (e.field === 'state' || e.field === 'stack')
? `badge ${esc(e.new_value)}`
: `tl-new ${stateClass(e.field, e.new_value)}`;
return `
<div class="tl-item">
<span class="tl-time">${fmtDateFull(e.changed_at)}</span>
<div class="tl-desc">
<div class="tl-event">
<span class="tl-label">${label}</span>
<span class="tl-change">
<span class="tl-old">${fmtHistVal(e.field, e.old_value)}</span>
<span class="tl-arrow">→</span>
<span class="tl-new ${stateClass(e.field, e.new_value)}">${fmtHistVal(e.field, e.new_value)}</span>
</span>
<span class="tl-sep">·</span>
<span class="tl-old">${fmtHistVal(e.field, e.old_value)}</span>
<span class="tl-arrow">→</span>
<span class="${newCls}">${fmtHistVal(e.field, e.new_value)}</span>
</div>
<span class="tl-time">${fmtDateFull(e.changed_at)}</span>
</div>`;
}).join('')
: '<div class="tl-empty">no history yet</div>';

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('—')
})