Persist traffic settings and support multi-device traffic views
Build and Deploy Gateway / build-and-push (push) Successful in 32s
Build and Deploy Gateway / deploy (push) Successful in 7s

This commit is contained in:
2026-08-31 06:46:59 +03:00
parent 6777422a27
commit 3c2eefe108
18 changed files with 653 additions and 124 deletions
+45
View File
@@ -206,3 +206,48 @@ test('route is unavailable when no traffic collector exists', async () => {
(error) => error.code === 'ENDPOINT_NOT_FOUND',
);
});
test('PUT persists validated traffic settings with revision protection and returns canonical state', async () => {
let state = { revision: 4, traffic: { grouping: 'site', sort: 'popular', retentionSeconds: 10 } };
let sent = false;
const route = createLiveTrafficRoute({
traffic: null,
settingsState: {
read: () => state,
update: (mutator) => { state = { ...mutator(state), revision: state.revision + 1 }; },
},
readBody: async () => ({
settings: { grouping: 'device', sort: 'recent', retentionSeconds: 30 },
expectedRevision: 4,
}),
sendState: async (res) => { sent = true; res.end(JSON.stringify(state)); },
});
const res = response();
assert.equal(await route.handle({ method: 'PUT', url: '/api/traffic/settings' }, res), true);
assert.equal(sent, true);
assert.deepEqual(state.traffic, { grouping: 'device', sort: 'recent', retentionSeconds: 30 });
assert.equal(state.revision, 5);
const conflict = createLiveTrafficRoute({
traffic: null,
settingsState: { read: () => state, update: () => { throw new Error('must not update'); } },
readBody: async () => ({ settings: state.traffic, expectedRevision: 4 }),
sendState: async () => {},
});
await assert.rejects(
conflict.handle({ method: 'PUT', url: '/api/traffic/settings' }, response()),
(error) => error.code === 'STATE_CONFLICT',
);
const invalid = createLiveTrafficRoute({
traffic: null,
settingsState: { read: () => state, update: () => { throw new Error('must not update'); } },
readBody: async () => ({ settings: { grouping: 'guess' }, expectedRevision: 5 }),
sendState: async () => {},
});
await assert.rejects(
invalid.handle({ method: 'PUT', url: '/api/traffic/settings' }, response()),
(error) => error.code === 'REQUEST_INVALID',
);
});
+1
View File
@@ -339,6 +339,7 @@ setInterval(() => {}, 60_000);
'singboxStartedAt',
'subscription',
'subscriptionHost',
'traffic',
'userInfo',
]);
let revision = initial.revision;
+2 -1
View File
@@ -137,13 +137,14 @@ test('schema v7 migrates failover disabled without losing canonical state', (t)
});
const migrated = store.read();
assert.equal(migrated.schemaVersion, 8);
assert.equal(migrated.schemaVersion, STATE_SCHEMA_VERSION);
assert.equal(migrated.revision, 19);
assert.equal(migrated.routeRulesRevision, 4);
assert.equal(migrated.failoverPolicy.enabled, false);
assert.equal(migrated.failoverRuntimeState.lastSwitchAt, null);
assert.equal(migrated.appliedFailoverPolicy, null);
assert.deepEqual(migrated.diagnostics.hiddenServiceIds, ['google']);
assert.deepEqual(migrated.traffic, { grouping: 'site', sort: 'popular', retentionSeconds: 10 });
assert.equal(store.migration.fromVersion, 7);
assert.deepEqual(JSON.parse(fs.readFileSync(store.migration.backupPath, 'utf8')), legacy);
});