fix: keep disconnect dates in the clients metadata snapshot

Даты «Задать дату» (lastDisconnectedAt) и отключение по расписанию
(scheduledTunnelDisconnectAt) тоже живут в clientsTable и терялись при
затирании. Снимок хранит и их, но возвращает только когда запись реально
затёрли (пропал creationDate) — иначе воскресало бы снятое в панели расписание.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 11:40:14 +03:00
parent 194ac8c742
commit 34415a4b8a
2 changed files with 34 additions and 5 deletions

View File

@@ -11,8 +11,17 @@
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
/** Поля userData, которые ведёт панель и которые нельзя терять. */ /** Поля, которые панель сама никогда не стирает: пустое значение = потеря. */
export const META_FIELDS = ["clientName", "creationDate", "last_config", "allowedIps"]; export const STABLE_FIELDS = ["clientName", "creationDate", "last_config", "allowedIps"];
/**
* Даты, которые панель штатно снимает («Задать дату», отключение по расписанию).
* Их возвращаем только когда запись действительно затёрли, иначе воскресим
* снятое расписание.
*/
export const DATE_FIELDS = ["lastDisconnectedAt", "scheduledTunnelDisconnectAt"];
export const META_FIELDS = [...STABLE_FIELDS, ...DATE_FIELDS];
/** «Client 17» — дефолтное имя от приложения, оно не ценнее сохранённого. */ /** «Client 17» — дефолтное имя от приложения, оно не ценнее сохранённого. */
export function isPlaceholderName(name) { export function isPlaceholderName(name) {
@@ -58,7 +67,9 @@ export function healClientsMeta(clients, meta) {
if (!id) return row; if (!id) return row;
const userData = { ...(row.userData || {}) }; const userData = { ...(row.userData || {}) };
const saved = nextMeta[id] || {}; const saved = nextMeta[id] || {};
for (const field of META_FIELDS) { // Приложение сносит creationDate целиком — по нему и опознаём затирание.
const wiped = !isMeaningful("creationDate", userData.creationDate) && isMeaningful("creationDate", saved.creationDate);
for (const field of wiped ? META_FIELDS : STABLE_FIELDS) {
if (!isMeaningful(field, userData[field]) && isMeaningful(field, saved[field])) { if (!isMeaningful(field, userData[field]) && isMeaningful(field, saved[field])) {
userData[field] = saved[field]; userData[field] = saved[field];
restored++; restored++;
@@ -69,6 +80,7 @@ export function healClientsMeta(clients, meta) {
if (isMeaningful(field, userData[field])) keep[field] = userData[field]; if (isMeaningful(field, userData[field])) keep[field] = userData[field];
} }
if (Object.keys(keep).length) nextMeta[id] = keep; if (Object.keys(keep).length) nextMeta[id] = keep;
else delete nextMeta[id];
return { ...row, userData }; return { ...row, userData };
}); });
return { clients: healed, restored, meta: nextMeta, metaChanged: JSON.stringify(nextMeta) !== before }; return { clients: healed, restored, meta: nextMeta, metaChanged: JSON.stringify(nextMeta) !== before };

View File

@@ -3,7 +3,15 @@ import assert from "assert";
import { healClientsMeta, isPlaceholderName } from "./clients-meta.mjs"; import { healClientsMeta, isPlaceholderName } from "./clients-meta.mjs";
const good = [ const good = [
{ clientId: "A=", userData: { clientName: "Mama", creationDate: "Sat Jun 27 2026", last_config: "{...}" } }, {
clientId: "A=",
userData: {
clientName: "Mama",
creationDate: "Sat Jun 27 2026",
last_config: "{...}",
lastDisconnectedAt: "2027-05-06T10:00:00.000Z",
},
},
{ clientId: "B=", userData: { clientName: "teplici", creationDate: "Sat Jul 04 2026" } }, { clientId: "B=", userData: { clientName: "teplici", creationDate: "Sat Jul 04 2026" } },
]; ];
@@ -22,9 +30,18 @@ const wiped = [
const fixed = healClientsMeta(wiped, snap.meta); const fixed = healClientsMeta(wiped, snap.meta);
assert.equal(fixed.clients[0].userData.clientName, "Mama"); assert.equal(fixed.clients[0].userData.clientName, "Mama");
assert.equal(fixed.clients[0].userData.last_config, "{...}"); assert.equal(fixed.clients[0].userData.last_config, "{...}");
assert.equal(fixed.clients[0].userData.lastDisconnectedAt, "2027-05-06T10:00:00.000Z");
assert.equal(fixed.clients[1].userData.creationDate, "Sat Jul 04 2026"); assert.equal(fixed.clients[1].userData.creationDate, "Sat Jul 04 2026");
assert.equal(fixed.clients[2].userData.clientName, "Client 2"); assert.equal(fixed.clients[2].userData.clientName, "Client 2");
assert.equal(fixed.restored, 5); assert.equal(fixed.restored, 6);
// 2б. Дату сняли в панели (запись целая) — снимок её не воскрешает и забывает.
const cleared = healClientsMeta(
[{ clientId: "A=", userData: { clientName: "Mama", creationDate: "Sat Jun 27 2026", last_config: "{...}" } }],
snap.meta
);
assert.equal(cleared.clients[0].userData.lastDisconnectedAt, undefined);
assert.equal(cleared.meta["A="].lastDisconnectedAt, undefined);
// 3. Повторный прогон уже целой таблицы ничего не чинит и не переписывает снимок. // 3. Повторный прогон уже целой таблицы ничего не чинит и не переписывает снимок.
const again = healClientsMeta(fixed.clients, fixed.meta); const again = healClientsMeta(fixed.clients, fixed.meta);