From 34415a4b8a655cb7a4a1181284750366e9f88336 Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 17 Aug 2026 11:40:14 +0300 Subject: [PATCH] fix: keep disconnect dates in the clients metadata snapshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Даты «Задать дату» (lastDisconnectedAt) и отключение по расписанию (scheduledTunnelDisconnectAt) тоже живут в clientsTable и терялись при затирании. Снимок хранит и их, но возвращает только когда запись реально затёрли (пропал creationDate) — иначе воскресало бы снятое в панели расписание. Co-Authored-By: Claude Opus 5 --- scripts/clients-meta.mjs | 18 +++++++++++++++--- scripts/test-clients-meta.mjs | 21 +++++++++++++++++++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/scripts/clients-meta.mjs b/scripts/clients-meta.mjs index daec11b..fd28f5d 100644 --- a/scripts/clients-meta.mjs +++ b/scripts/clients-meta.mjs @@ -11,8 +11,17 @@ import fs from "fs"; 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» — дефолтное имя от приложения, оно не ценнее сохранённого. */ export function isPlaceholderName(name) { @@ -58,7 +67,9 @@ export function healClientsMeta(clients, meta) { if (!id) return row; const userData = { ...(row.userData || {}) }; 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])) { userData[field] = saved[field]; restored++; @@ -69,6 +80,7 @@ export function healClientsMeta(clients, meta) { if (isMeaningful(field, userData[field])) keep[field] = userData[field]; } if (Object.keys(keep).length) nextMeta[id] = keep; + else delete nextMeta[id]; return { ...row, userData }; }); return { clients: healed, restored, meta: nextMeta, metaChanged: JSON.stringify(nextMeta) !== before }; diff --git a/scripts/test-clients-meta.mjs b/scripts/test-clients-meta.mjs index 20d192e..2995331 100644 --- a/scripts/test-clients-meta.mjs +++ b/scripts/test-clients-meta.mjs @@ -3,7 +3,15 @@ import assert from "assert"; import { healClientsMeta, isPlaceholderName } from "./clients-meta.mjs"; 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" } }, ]; @@ -22,9 +30,18 @@ const wiped = [ const fixed = healClientsMeta(wiped, snap.meta); assert.equal(fixed.clients[0].userData.clientName, "Mama"); 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[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. Повторный прогон уже целой таблицы ничего не чинит и не переписывает снимок. const again = healClientsMeta(fixed.clients, fixed.meta);