Files
dashcaddy/dashcaddy-api/routes/updates.js
Sami ffa6966fd3 Add auto-update system for DashCaddy instances
- self-updater.js: polls for new versions, downloads/verifies tarballs,
  triggers host-side rebuild via systemd path unit
- dashcaddy-update.sh + systemd units: host-side container rebuild with
  automatic rollback on health check failure
- 7 new /api/v1/system/* endpoints for version info, update check/apply,
  rollback, and update history
- Frontend: DashCaddy tab in Updates modal with version display,
  changelog, update button, rollback, and notification dot
- install.sh: updater service installation, volume mounts, env vars
- build-release.sh + webhook-handler.js: release server pipeline
  (Gitea webhook → build tarball → deploy to get.dashcaddy.net)
- Dockerfile: DASHCADDY_COMMIT build arg → VERSION file
- Version bump to 1.1.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 03:11:35 -08:00

128 lines
5.1 KiB
JavaScript

const express = require('express');
const { paginate, parsePaginationParams } = require('../pagination');
module.exports = function(ctx) {
const router = express.Router();
// ===== UPDATE MANAGEMENT ENDPOINTS =====
// Check for updates
router.post('/updates/check', ctx.asyncHandler(async (req, res) => {
await ctx.updateManager.checkForUpdates();
const updates = ctx.updateManager.getAvailableUpdates();
res.json({ success: true, updates, count: updates.length });
}, 'updates-check'));
// Get available updates
router.get('/updates/available', ctx.asyncHandler(async (req, res) => {
const updates = ctx.updateManager.getAvailableUpdates();
const paginationParams = parsePaginationParams(req.query);
const result = paginate(updates, paginationParams);
res.json({ success: true, updates: result.data, count: updates.length, ...(result.pagination && { pagination: result.pagination }) });
}, 'updates-available'));
// Update a container
router.post('/updates/update/:containerId', ctx.asyncHandler(async (req, res) => {
const result = await ctx.updateManager.updateContainer(req.params.containerId, req.body);
res.json({ success: true, result });
}, 'updates-update'));
// Rollback update
router.post('/updates/rollback/:containerId', ctx.asyncHandler(async (req, res) => {
await ctx.updateManager.rollbackUpdate(req.params.containerId);
res.json({ success: true, message: 'Rollback completed' });
}, 'updates-rollback'));
// Get update history
router.get('/updates/history', ctx.asyncHandler(async (req, res) => {
const paginationParams = parsePaginationParams(req.query);
// When paginating, fetch all history so pagination can slice correctly
const fetchLimit = paginationParams ? Number.MAX_SAFE_INTEGER : (parseInt(req.query.limit) || 50);
const history = ctx.updateManager.getHistory(fetchLimit);
const result = paginate(history, paginationParams);
res.json({ success: true, history: result.data, ...(result.pagination && { pagination: result.pagination }) });
}, 'updates-history'));
// Configure auto-update
router.post('/updates/auto-update/:containerId', ctx.asyncHandler(async (req, res) => {
ctx.updateManager.configureAutoUpdate(req.params.containerId, req.body);
res.json({ success: true, message: 'Auto-update configured' });
}, 'updates-auto-update'));
// Schedule update
router.post('/updates/schedule/:containerId', ctx.asyncHandler(async (req, res) => {
const { scheduledTime } = req.body;
if (!scheduledTime) {
return ctx.errorResponse(res, 400, 'scheduledTime is required');
}
ctx.updateManager.scheduleUpdate(req.params.containerId, scheduledTime);
res.json({ success: true, message: 'Update scheduled', scheduledTime });
}, 'updates-schedule'));
// ===== DASHCADDY SELF-UPDATE ENDPOINTS =====
// Get current version
router.get('/system/version', ctx.asyncHandler(async (req, res) => {
const local = ctx.selfUpdater.getLocalVersion();
res.json({ success: true, name: 'DashCaddy', version: local.version, commit: local.commit });
}, 'system-version'));
// Check for DashCaddy update
router.get('/system/update-check', ctx.asyncHandler(async (req, res) => {
const result = await ctx.selfUpdater.checkForUpdate();
res.json({ success: true, ...result });
}, 'system-update-check'));
// Apply available update
router.post('/system/update-apply', ctx.asyncHandler(async (req, res) => {
const check = await ctx.selfUpdater.checkForUpdate();
if (!check.available) {
return res.json({ success: true, message: 'Already up to date' });
}
// Start async — container may restart
ctx.selfUpdater.applyUpdate(check.remote).catch(err => {
ctx.logError('self-update', err);
});
res.json({
success: true,
message: 'Update initiated',
fromVersion: check.local.version,
toVersion: check.remote.version,
});
}, 'system-update-apply'));
// Get update status
router.get('/system/update-status', ctx.asyncHandler(async (req, res) => {
res.json({
success: true,
status: ctx.selfUpdater.getStatus(),
lastCheck: ctx.selfUpdater.lastCheckTime,
lastResult: ctx.selfUpdater.lastCheckResult,
});
}, 'system-update-status'));
// Get self-update history
router.get('/system/update-history', ctx.asyncHandler(async (req, res) => {
const history = ctx.selfUpdater.getUpdateHistory();
res.json({ success: true, history });
}, 'system-update-history'));
// List rollback versions
router.get('/system/rollback-versions', ctx.asyncHandler(async (req, res) => {
const versions = ctx.selfUpdater.getAvailableRollbacks();
res.json({ success: true, versions });
}, 'system-rollback-versions'));
// Rollback to a previous version
router.post('/system/rollback', ctx.asyncHandler(async (req, res) => {
const { version } = req.body;
if (!version) return ctx.errorResponse(res, 400, 'version is required');
ctx.selfUpdater.rollbackToVersion(version).catch(err => {
ctx.logError('self-rollback', err);
});
res.json({ success: true, message: `Rollback to ${version} initiated` });
}, 'system-rollback'));
return router;
};