feat: modpack version tracking on server cards

- current_version column on server_config
- server_version_history table (version, who, when)
- POST /admin/servers/:id/set-version
- GET /admin/servers/:id/version-history
- Version display + edit UI on each server card

Chronicler #88 | April 14, 2026
This commit is contained in:
Claude Chronicler #88
2026-04-14 15:51:14 +00:00
parent 6dc6ce0059
commit 72c378f136
5 changed files with 175 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
-- Migration 140: Server version history tracking
-- Chronicler #88 | April 14, 2026
-- Add current_version to server_config for quick display
ALTER TABLE server_config ADD COLUMN IF NOT EXISTS current_version VARCHAR(64);
-- Version history table
CREATE TABLE IF NOT EXISTS server_version_history (
id SERIAL PRIMARY KEY,
server_identifier VARCHAR(36) NOT NULL,
version VARCHAR(64) NOT NULL,
updated_by_id VARCHAR(32) NOT NULL,
updated_by_username VARCHAR(64) NOT NULL,
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_svh_server ON server_version_history(server_identifier);
CREATE INDEX IF NOT EXISTS idx_svh_updated ON server_version_history(updated_at DESC);

View File

@@ -603,4 +603,63 @@ router.post('/:identifier/provision-subdomain', async (req, res) => {
}
});
// POST /admin/servers/:identifier/set-version
// Set the installed modpack version for a server
router.post('/:identifier/set-version', async (req, res) => {
const { identifier } = req.params;
const { version } = req.body;
if (!version || !version.trim()) {
return res.status(400).send('<span class="text-red-500 text-sm">❌ Version cannot be empty</span>');
}
const cleanVersion = version.trim();
const userId = req.user?.id || 'unknown';
const username = req.user?.username || 'Unknown';
try {
// Update current version on server_config
await db.query(
'UPDATE server_config SET current_version = $1, updated_at = NOW() WHERE server_identifier = $2',
[cleanVersion, identifier]
);
// Insert into history
await db.query(
'INSERT INTO server_version_history (server_identifier, version, updated_by_id, updated_by_username) VALUES ($1, $2, $3, $4)',
[identifier, cleanVersion, userId, username]
);
serverCache.lastFetch = 0;
console.log(`[VERSION] ${identifier}${cleanVersion} by ${username}`);
res.send(`<span class="text-green-500 text-sm">✅ Version set to ${cleanVersion}</span>`);
} catch (err) {
console.error('[set-version]', err);
res.status(500).send('<span class="text-red-500 text-sm">❌ Failed to update version</span>');
}
});
// GET /admin/servers/:identifier/version-history
// Returns version history partial for a server
router.get('/:identifier/version-history', async (req, res) => {
const { identifier } = req.params;
try {
const result = await db.query(
'SELECT version, updated_by_username, updated_at FROM server_version_history WHERE server_identifier = $1 ORDER BY updated_at DESC LIMIT 10',
[identifier]
);
res.render('admin/servers/_version_history', {
history: result.rows,
identifier,
layout: false
});
} catch (err) {
console.error('[version-history]', err);
res.status(500).send('<span class="text-red-500 text-sm">❌ Failed to load history</span>');
}
});
module.exports = router;

View File

@@ -187,4 +187,44 @@
title="Restart">🔄</button>
</div>
</div>
<!-- Modpack Version -->
<div class="px-4 pb-4" style="border-top:1px solid #333;padding-top:10px;margin-top:4px;">
<div style="font-size:10px;color:#666;text-transform:uppercase;letter-spacing:0.1em;margin-bottom:6px;">📦 Installed Version</div>
<div style="display:flex;gap:6px;align-items:center;" id="version-display-<%= server.identifier %>">
<span style="font-size:13px;font-weight:600;color:<%= (config && config.current_version) ? '#4ade80' : '#555' %>">
<%= (config && config.current_version) ? config.current_version : 'Not set' %>
</span>
<button onclick="document.getElementById('version-form-<%= server.identifier %>').style.display='block';this.style.display='none'"
style="font-size:10px;background:#333;border:1px solid #555;color:#aaa;padding:2px 8px;border-radius:4px;cursor:pointer;">
✏️ Edit
</button>
</div>
<div id="version-form-<%= server.identifier %>" style="display:none;margin-top:6px;">
<div style="display:flex;gap:6px;align-items:center;">
<input type="text" id="version-input-<%= server.identifier %>"
placeholder="e.g. 1.4.2" value="<%= (config && config.current_version) ? config.current_version : '' %>"
style="font-size:12px;background:#1a1a1a;border:1px solid #555;color:#e0e0e0;padding:4px 8px;border-radius:4px;width:140px;" />
<button onclick="saveVersion('<%= server.identifier %>')"
style="font-size:11px;background:#2563eb;color:#fff;border:none;padding:4px 10px;border-radius:4px;cursor:pointer;">
Save
</button>
<button onclick="document.getElementById('version-form-<%= server.identifier %>').style.display='none';document.getElementById('version-edit-btn-<%= server.identifier %>').style.display='inline'"
style="font-size:11px;background:#333;border:1px solid #555;color:#aaa;padding:4px 8px;border-radius:4px;cursor:pointer;">
Cancel
</button>
</div>
<div id="version-result-<%= server.identifier %>" style="margin-top:4px;font-size:11px;"></div>
<div style="margin-top:6px;">
<button hx-get="/admin/servers/<%= server.identifier %>/version-history"
hx-target="#version-history-<%= server.identifier %>"
hx-swap="innerHTML"
style="font-size:10px;background:transparent;border:none;color:#555;cursor:pointer;padding:0;text-decoration:underline;">
View history
</button>
</div>
<div id="version-history-<%= server.identifier %>" style="margin-top:4px;background:#1a1a1a;border-radius:4px;padding:4px;"></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,23 @@
<!-- Version History Partial -->
<% if (history.length === 0) { %>
<p style="color:#666;font-size:11px;margin:4px 0;">No version history yet.</p>
<% } else { %>
<table style="width:100%;font-size:11px;border-collapse:collapse;">
<thead>
<tr style="color:#666;text-align:left;">
<th style="padding:3px 6px;">Version</th>
<th style="padding:3px 6px;">By</th>
<th style="padding:3px 6px;">When</th>
</tr>
</thead>
<tbody>
<% history.forEach(function(h) { %>
<tr style="border-top:1px solid #333;">
<td style="padding:3px 6px;color:#e0e0e0;font-weight:600;"><%= h.version %></td>
<td style="padding:3px 6px;color:#aaa;"><%= h.updated_by_username %></td>
<td style="padding:3px 6px;color:#666;"><%= new Date(h.updated_at).toLocaleDateString() %></td>
</tr>
<% }); %>
</tbody>
</table>
<% } %>

View File

@@ -32,3 +32,38 @@
</div>
</div>
<script>
async function saveVersion(identifier) {
const input = document.getElementById('version-input-' + identifier);
const result = document.getElementById('version-result-' + identifier);
const version = input.value.trim();
if (!version) {
result.innerHTML = '<span style="color:#ef4444">Version cannot be empty</span>';
return;
}
result.innerHTML = '<span style="color:#888">Saving...</span>';
try {
const res = await fetch('/admin/servers/' + identifier + '/set-version', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ version })
});
const text = await res.text();
result.innerHTML = text;
// Update the display
if (res.ok) {
const display = document.getElementById('version-display-' + identifier);
const span = display.querySelector('span');
span.textContent = version;
span.style.color = '#4ade80';
}
} catch (err) {
result.innerHTML = '<span style="color:#ef4444">❌ Failed to save</span>';
}
}
</script>