mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-09-04 06:35:29 +00:00
feat: make image-update check cadence configurable and visible (#1377)
* feat: make image-update check cadence configurable and visible The background image-update scanner polled registries on a hardcoded 6-hour interval, with no way to see when it last ran or when the next run was due. Operators testing updates read this as auto-update being unreliable: a manual update checks the registry immediately and applies, so the slow background scan rarely raised the "update available" notification before the stack was already current. Backend: - ImageUpdateService reads image_update_check_interval_minutes (15-1440, default 120) and drives a single generation-guarded self-rescheduling timer with 10% per-run jitter so fleet nodes do not poll in lockstep. restartPolling() applies a new interval live, with no restart, and cannot leave a duplicate timer when a save lands mid-scan. - GET /api/image-updates/status now returns checking, intervalMinutes, lastCheckedAt, nextCheckAt, and the manual-cooldown fields. New admin-only PUT /api/image-updates/interval persists the setting and reschedules. Frontend: - New Settings > Automation > Image update checks section to choose the interval (read-only for non-admins; admin enforced on the backend). - The Auto-Update readiness view shows last-checked, next-check, and a ticking manual-recheck cooldown, and the copy distinguishes registry detection from scheduled auto-update execution. Adds backend unit and route tests and frontend component tests, and updates the auto-update documentation. * fix: drop stale image-update status response in the readiness strip loadCadence() ran on mount and again after a Recheck with no request token, so a slow initial /image-updates/status response could resolve after the recheck-triggered one and overwrite the fresh cooldown with stale data, or set state after the view unmounted. Guard setCadence with a monotonic token mirroring loadReadiness, and bump it on unmount. Adds a regression test for the out-of-order resolution.
This commit is contained in:
@@ -72,10 +72,55 @@ describe('GET /api/image-updates/status', () => {
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it('returns a checking flag', async () => {
|
||||
it('returns the enriched status payload', async () => {
|
||||
const res = await request(app).get('/api/image-updates/status').set('Cookie', adminCookie);
|
||||
expect(res.status).toBe(200);
|
||||
expect(typeof res.body.checking).toBe('boolean');
|
||||
// start() never runs in route tests, so the interval reflects the seeded
|
||||
// default (120) via the field initializer rather than NaN.
|
||||
expect(res.body.intervalMinutes).toBe(120);
|
||||
expect(res.body.manualCooldownMinutes).toBe(2);
|
||||
expect(typeof res.body.manualCooldownRemainingMs).toBe('number');
|
||||
expect('lastCheckedAt' in res.body).toBe(true);
|
||||
expect('nextCheckAt' in res.body).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /api/image-updates/interval', () => {
|
||||
it('rejects unauthenticated requests with 401', async () => {
|
||||
const res = await request(app).put('/api/image-updates/interval').send({ minutes: 30 });
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it('rejects non-admin users with 403', async () => {
|
||||
const res = await request(app).put('/api/image-updates/interval').set('Cookie', viewerCookie).send({ minutes: 30 });
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
|
||||
it('rejects an interval below the minimum', async () => {
|
||||
const res = await request(app).put('/api/image-updates/interval').set('Cookie', adminCookie).send({ minutes: 5 });
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it('rejects an interval above the maximum', async () => {
|
||||
const res = await request(app).put('/api/image-updates/interval').set('Cookie', adminCookie).send({ minutes: 5000 });
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it('rejects a non-integer interval', async () => {
|
||||
const res = await request(app).put('/api/image-updates/interval').set('Cookie', adminCookie).send({ minutes: 'soon' });
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it('persists a valid interval and returns the enriched status', async () => {
|
||||
const res = await request(app).put('/api/image-updates/interval').set('Cookie', adminCookie).send({ minutes: 30 });
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.intervalMinutes).toBe(30);
|
||||
// The value is persisted to global_settings...
|
||||
expect(DatabaseService.getInstance().getGlobalSettings().image_update_check_interval_minutes).toBe('30');
|
||||
// ...and a follow-up status read reflects the rescheduled cadence.
|
||||
const statusRes = await request(app).get('/api/image-updates/status').set('Cookie', adminCookie);
|
||||
expect(statusRes.body.intervalMinutes).toBe(30);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user