feat: cloud backup destinations + long-term resource history
Cloud backups (Dropbox / WebDAV / SFTP):
- backup-manager.js: save + load handlers per provider, credential
resolution via credentialManager, destination probe.
- routes/backups.js: /credentials/{provider} (masked GET, POST, DELETE),
/test-destination, scheduling endpoints.
- status/js/backup-restore.js: destination picker, provider-specific
credential forms, test button wired to backend probe.
- npm deps already present (dropbox 10.34.0, webdav 5.7.1,
ssh2-sftp-client 11.0.0).
Resource history:
- resource-monitor.js: three-tier rollup storage — raw 10s samples
(7-day retention), hourly rollups (30-day), daily rollups
(365-day). getHistoryByRange() auto-selects the appropriate tier.
- routes/monitoring.js: /monitoring/history/:containerId now supports
startTime/endTime range mode (legacy ?hours=N still works).
- status/js/resource-monitor.js + dashboard.css: "History" tab with
range buttons (1h/24h/7d/30d/1y), SVG sparklines for
CPU / memory / network. Renderer handles raw and rolled-up shapes.
status/dist/features.js rebuilt from source via build.js.
Lifted out of wip/cloud-backups-and-history; the half-finished
app-deps feature from that branch (frontend calls /api/v1/apps/
check-dependencies but the endpoint doesn't exist) is preserved
separately on wip/app-deps for later.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -42,5 +42,115 @@ module.exports = function({ backupManager, asyncHandler }) {
|
||||
success(res, { result });
|
||||
}, 'backups-restore'));
|
||||
|
||||
// ==================== CLOUD DESTINATIONS ====================
|
||||
|
||||
// Test a destination (write+read+delete probe)
|
||||
router.post('/backups/test-destination', asyncHandler(async (req, res) => {
|
||||
const destination = req.body;
|
||||
if (!destination || !destination.type) {
|
||||
const { ValidationError } = require('../errors');
|
||||
throw new ValidationError('destination.type is required');
|
||||
}
|
||||
const result = await backupManager.testDestination(destination);
|
||||
success(res, result);
|
||||
}, 'backups-test-destination'));
|
||||
|
||||
// Get cloud credentials (masked) for a provider
|
||||
// Provider: dropbox | webdav | sftp
|
||||
router.get('/backups/credentials/:provider', asyncHandler(async (req, res) => {
|
||||
const credentialManager = require('../credential-manager');
|
||||
const provider = req.params.provider;
|
||||
if (!['dropbox', 'webdav', 'sftp'].includes(provider)) {
|
||||
const { ValidationError } = require('../errors');
|
||||
throw new ValidationError('Invalid provider');
|
||||
}
|
||||
|
||||
const mask = (val) => val ? '***' : null;
|
||||
let creds = {};
|
||||
if (provider === 'dropbox') {
|
||||
const token = await credentialManager.retrieve('backup.dropbox.token');
|
||||
creds = { token: mask(token) };
|
||||
} else if (provider === 'webdav') {
|
||||
creds = {
|
||||
url: (await credentialManager.retrieve('backup.webdav.url')) || null,
|
||||
username: (await credentialManager.retrieve('backup.webdav.username')) || null,
|
||||
password: mask(await credentialManager.retrieve('backup.webdav.password'))
|
||||
};
|
||||
} else if (provider === 'sftp') {
|
||||
creds = {
|
||||
host: (await credentialManager.retrieve('backup.sftp.host')) || null,
|
||||
port: (await credentialManager.retrieve('backup.sftp.port')) || '22',
|
||||
username: (await credentialManager.retrieve('backup.sftp.username')) || null,
|
||||
password: mask(await credentialManager.retrieve('backup.sftp.password')),
|
||||
privateKey: mask(await credentialManager.retrieve('backup.sftp.privateKey'))
|
||||
};
|
||||
}
|
||||
success(res, { provider, credentials: creds });
|
||||
}, 'backups-credentials-get'));
|
||||
|
||||
// Save cloud credentials for a provider
|
||||
router.post('/backups/credentials/:provider', asyncHandler(async (req, res) => {
|
||||
const credentialManager = require('../credential-manager');
|
||||
const { ValidationError } = require('../errors');
|
||||
const provider = req.params.provider;
|
||||
|
||||
if (!['dropbox', 'webdav', 'sftp'].includes(provider)) {
|
||||
throw new ValidationError('Invalid provider');
|
||||
}
|
||||
|
||||
const body = req.body || {};
|
||||
const storeIfPresent = async (key, val) => {
|
||||
if (val !== undefined && val !== null && val !== '' && val !== '***') {
|
||||
await credentialManager.store(key, String(val));
|
||||
}
|
||||
};
|
||||
|
||||
if (provider === 'dropbox') {
|
||||
if (!body.token || body.token === '***') {
|
||||
const existing = await credentialManager.retrieve('backup.dropbox.token');
|
||||
if (!existing) {
|
||||
throw new ValidationError('Dropbox token required');
|
||||
}
|
||||
} else {
|
||||
await credentialManager.store('backup.dropbox.token', body.token);
|
||||
}
|
||||
} else if (provider === 'webdav') {
|
||||
await storeIfPresent('backup.webdav.url', body.url);
|
||||
await storeIfPresent('backup.webdav.username', body.username);
|
||||
await storeIfPresent('backup.webdav.password', body.password);
|
||||
} else if (provider === 'sftp') {
|
||||
await storeIfPresent('backup.sftp.host', body.host);
|
||||
await storeIfPresent('backup.sftp.port', body.port);
|
||||
await storeIfPresent('backup.sftp.username', body.username);
|
||||
await storeIfPresent('backup.sftp.password', body.password);
|
||||
await storeIfPresent('backup.sftp.privateKey', body.privateKey);
|
||||
}
|
||||
|
||||
success(res, { message: `${provider} credentials saved` });
|
||||
}, 'backups-credentials-set'));
|
||||
|
||||
// Delete cloud credentials for a provider
|
||||
router.delete('/backups/credentials/:provider', asyncHandler(async (req, res) => {
|
||||
const credentialManager = require('../credential-manager');
|
||||
const { ValidationError } = require('../errors');
|
||||
const provider = req.params.provider;
|
||||
|
||||
if (!['dropbox', 'webdav', 'sftp'].includes(provider)) {
|
||||
throw new ValidationError('Invalid provider');
|
||||
}
|
||||
|
||||
const keys = {
|
||||
dropbox: ['backup.dropbox.token'],
|
||||
webdav: ['backup.webdav.url', 'backup.webdav.username', 'backup.webdav.password'],
|
||||
sftp: ['backup.sftp.host', 'backup.sftp.port', 'backup.sftp.username', 'backup.sftp.password', 'backup.sftp.privateKey']
|
||||
};
|
||||
|
||||
for (const k of keys[provider]) {
|
||||
try { await credentialManager.delete(k); } catch (_) {}
|
||||
}
|
||||
|
||||
success(res, { message: `${provider} credentials deleted` });
|
||||
}, 'backups-credentials-delete'));
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user