Add standalone backup script and admin update-check banner
ops/backup.sh snapshots Redis (BGSAVE, dynamic RDB path lookup) and ./config for standalone deployments, with retention. A background service polls GitHub releases every 24h and surfaces an admin-only banner in the UI when a newer version is published.
This commit is contained in:
@@ -34,6 +34,7 @@ app.contoller = require('./controller');
|
||||
require('./services/host_lookup');
|
||||
require('./services/host_scheduler');
|
||||
require('./services/dynamic_dns');
|
||||
require('./services/update_check');
|
||||
|
||||
// Push pubsub over the socket and back.
|
||||
app.onListen.push(function(){
|
||||
|
||||
+6
-1
@@ -66,7 +66,12 @@ module.exports = {
|
||||
enabled: true,
|
||||
initial: 15000, // first refresh 15s after start
|
||||
interval: 14400000, // then every 4 hours
|
||||
}
|
||||
},
|
||||
updateCheck:{
|
||||
enabled: true,
|
||||
initial: 30000, // first check 30s after start
|
||||
interval: 86400000, // then every 24h
|
||||
},
|
||||
},
|
||||
|
||||
// Dynamic DNS: services queried (in order) to learn this box's public IP.
|
||||
|
||||
@@ -31,4 +31,6 @@ router.use('/group', middleware.auth, authz.requireAdmin, require('./group'));
|
||||
// Self-service API tokens (PATs) — owner-scoped, no admin gate required.
|
||||
router.use('/api-token', middleware.auth, require('./api_token'));
|
||||
|
||||
router.use('/update-check', middleware.auth, require('./update_check'));
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
const router = require('express').Router();
|
||||
const updateCheck = require('../utils/update_check');
|
||||
|
||||
// Any authenticated user can read this (it's just "is a newer version
|
||||
// published on GitHub", not sensitive) -- the UI only shows the banner to
|
||||
// admins (see views/top.ejs), but the endpoint itself doesn't need to be
|
||||
// admin-gated.
|
||||
router.get('/', async function(req, res, next){
|
||||
try{
|
||||
return res.json(updateCheck.getState());
|
||||
}catch(error){
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
const conf = require('@simpleworkjs/conf');
|
||||
const updateCheck = require('../utils/update_check');
|
||||
|
||||
function updateCheckService(){
|
||||
/**
|
||||
* Update Check Service
|
||||
*
|
||||
* Periodically asks GitHub for the latest published release of this repo
|
||||
* and compares it to the running version (nodejs/package.json). Never
|
||||
* auto-updates anything -- just makes the result available via
|
||||
* GET /api/update-check (see routes/update_check.js) so the admin UI can
|
||||
* show a banner when a newer release exists.
|
||||
*/
|
||||
|
||||
setTimeout(updateCheck.checkNow, conf.service.updateCheck.initial);
|
||||
setInterval(updateCheck.checkNow, conf.service.updateCheck.interval);
|
||||
|
||||
console.log('Update check service initialized');
|
||||
console.log(`- Checking ${updateCheck.REPO} releases: 30s after start, then every 24h`);
|
||||
}
|
||||
|
||||
if(conf.service.updateCheck.enabled !== false) updateCheckService();
|
||||
|
||||
module.exports = {};
|
||||
@@ -0,0 +1,66 @@
|
||||
'use strict';
|
||||
|
||||
// Periodic "is a newer release available" check against GitHub releases.
|
||||
// Nothing auto-updates -- this only surfaces a notice (an admin-only banner,
|
||||
// see routes/update_check.js + views/top.ejs) so operators know to
|
||||
// `git pull` + rebuild on their own schedule. State lives in memory only
|
||||
// (single-process app); a restart just re-checks on the next interval.
|
||||
|
||||
const { buildVersion } = require('./build_info');
|
||||
|
||||
const REPO = 'theta42/proxy';
|
||||
const API_URL = `https://api.github.com/repos/${REPO}/releases/latest`;
|
||||
|
||||
let state = {
|
||||
currentVersion: buildVersion,
|
||||
latestVersion: null,
|
||||
updateAvailable: false,
|
||||
releaseUrl: null,
|
||||
checkedAt: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
// Basic semver compare (major.minor.patch, ignoring any -prerelease/+build
|
||||
// suffix) -- good enough for comparing release tags like "v1.2.0" against
|
||||
// package.json's "1.1.0". Returns true if `a` is strictly newer than `b`.
|
||||
function isNewer(a, b) {
|
||||
const pa = a.replace(/^v/i, '').split('.').map(n => parseInt(n, 10) || 0);
|
||||
const pb = b.replace(/^v/i, '').split('.').map(n => parseInt(n, 10) || 0);
|
||||
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||
const na = pa[i] || 0, nb = pb[i] || 0;
|
||||
if (na !== nb) return na > nb;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function checkNow() {
|
||||
try {
|
||||
const res = await fetch(API_URL, {
|
||||
headers: { 'Accept': 'application/vnd.github+json', 'User-Agent': 'theta42-proxy-update-check' },
|
||||
});
|
||||
if (!res.ok) throw new Error(`GitHub API returned ${res.status}`);
|
||||
const data = await res.json();
|
||||
const latestVersion = String(data.tag_name || '').replace(/^v/i, '');
|
||||
|
||||
state = {
|
||||
currentVersion: buildVersion,
|
||||
latestVersion: latestVersion || null,
|
||||
updateAvailable: latestVersion ? isNewer(latestVersion, buildVersion) : false,
|
||||
releaseUrl: data.html_url || `https://github.com/${REPO}/releases/latest`,
|
||||
checkedAt: Date.now(),
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
// Network hiccup, rate limit, no releases published yet, etc. -- keep
|
||||
// the previous state and just note the failure; never throw, this
|
||||
// runs unattended on a timer.
|
||||
state = { ...state, checkedAt: Date.now(), error: error.message };
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
module.exports = { checkNow, getState, isNewer, REPO };
|
||||
+39
-1
@@ -83,7 +83,31 @@
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Admin-only "a newer release is available" notice (services/update_check.js).
|
||||
Dismissal is per-browser-session only (sessionStorage), not persisted server-side.
|
||||
Fixed-positioned below the fixed navbar (a plain in-flow div here would render
|
||||
UNDER the nav, since fixed elements are taken out of document flow) -- shown/hidden
|
||||
dynamically, so #spa-shell's margin-top is adjusted in JS to make room for it. -->
|
||||
<div id="update-banner" class="alert alert-info alert-dismissible mb-0 rounded-0 text-center" style="display:none; position:fixed; left:0; right:0; z-index:1029;">
|
||||
<span id="update-banner-text"></span>
|
||||
<button type="button" class="btn-close" onclick="dismissUpdateBanner()"></button>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
function showUpdateBanner(){
|
||||
let $nav = $('nav.fixed-top');
|
||||
let $banner = $('#update-banner');
|
||||
$banner.css('top', $nav.outerHeight() + 'px').show();
|
||||
$('#spa-shell').css('margin-top', ($nav.outerHeight() + $banner.outerHeight()) + 'px');
|
||||
}
|
||||
|
||||
function dismissUpdateBanner(){
|
||||
$('#update-banner').hide();
|
||||
$('#spa-shell').css('margin-top', '');
|
||||
sessionStorage.setItem('update-banner-dismissed', '1');
|
||||
}
|
||||
|
||||
$(document).ready(function(){
|
||||
|
||||
// Set the correct link to active in the top nav bar
|
||||
@@ -108,7 +132,21 @@
|
||||
$('#cl-login-button').show();
|
||||
}
|
||||
|
||||
if(data && data.isAdmin) $('.nav-admin').css('display', '');
|
||||
if(data && data.isAdmin){
|
||||
$('.nav-admin').css('display', '');
|
||||
|
||||
if(!sessionStorage.getItem('update-banner-dismissed')){
|
||||
app.api.get('update-check', function(error, info){
|
||||
if(error || !info || !info.updateAvailable) return;
|
||||
$('#update-banner-text').html(
|
||||
'A newer version of the proxy is available: <b>v' + info.latestVersion + '</b> ' +
|
||||
'(running v' + info.currentVersion + ') — ' +
|
||||
'<a href="' + info.releaseUrl + '" target="_blank" class="alert-link">see what changed</a>.'
|
||||
);
|
||||
showUpdateBanner();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user