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:
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
<% } %>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user