[feature] «Временные заметки»: автоперенос в корзину через X часов (структурируй или умри) #201
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Концепция
«Временная заметка» — заметка с «таймером смерти». Создаётся отдельной кнопкой; через настраиваемые X часов (по умолчанию 24) она автоматически уезжает в корзину тем же мягким удалением, что и при ручном «Удалить». Чтобы заметка выжила, нужно осознанное действие — «Сделать постоянной». Не сделал — умерла. Девиз: структурируй или умри.
Переиспользование существующих механизмов:
pages→ иконка в дереве → пункт меню-переключатель → toggle-эндпоинт), см.is_template;@Interval-сервис по образцуTrashCleanupService;PageRepo.removePage(через корзину, не напрямую).Жизненный цикл (две стадии смерти)
Удаление двухстадийное: временная → (через X ч) корзина → (через
trashRetentionDays) навсегда. Это даёт сетку безопасности — даже «умершую» заметку ещё можно восстановить из корзины.Зафиксированные решения
ActionIconс иконкой песочных часов рядом с обычным «+»), а не split-меню.removePage; восстановление возвращает всё поддерево). Поведение задокументировать в подсказке.Модель данных
Решение: одна nullable-колонка-таймстемп вместо булева флага
Добавляем
pages.temporary_expires_at timestamptz NULL:NULL→ заметка обычная;NULL→ заметка временная, значение = точный момент «смерти».Признак «временная» =
temporary_expires_at IS NOT NULL— он управляет и символом, и пунктом меню. Дедлайн замораживается при создании (now + X ч), поэтому изменение глобальной настройки X не «переигрывает» дедлайны уже созданных заметок.Новая миграция (по образцу
20260620T130000-page-is-template.ts):Настройка X часов — на workspace (по образцу
trash_retention_days,20260228T223532-audit.ts):Дефолт при
NULL— константа в коде:DEFAULT_TEMPORARY_NOTE_HOURS = 24(аналогDEFAULT_RETENTION_DAYS = 30).Сопутствующие правки типов:
apps/server/src/database/types/db.d.ts:Pages.temporaryExpiresAt: Timestamp | null;Workspaces.temporaryNoteHours: Generated<number>.apps/server/src/database/repos/page/page.repo.ts→baseFields: добавить'temporaryExpiresAt'.IPageиSpaceTreeNode—temporaryExpiresAt?: string | null.Серверная часть
1. Создание временной заметки
apps/server/src/core/page/dto/create-page.dto.ts:page.service.tscreate()(передinsertPage):2. Переключатель «временная ⇄ постоянная» (toggle)
Зеркало механизма шаблонов. Новый эндпоинт
POST /pages/toggle-temporary(рядом сtoggle-templateвpage-template.controller.ts), DTO по образцуtoggle-template.dto.ts:Логика (зеркало
toggleTemplate): проверки существования /workspaceId/validateCanEdit, затем:Даёт сразу обе операции: «Сделать постоянной» (
temporary:false) и симметричную «Сделать временной» для существующей заметки (как «Make template»).3. Фоновый сборщик —
TemporaryNoteCleanupServiceКалька
trash-cleanup.service.ts; регистрируется вpage.module.tsрядом сTrashCleanupService.@nestjs/scheduleуже глобально включён (TelemetryModule).Используем
removePage, а не новый код: он уже рекурсивно проставляетdeletedAtдетям, удаляетsharesв транзакции и эмититPAGE_SOFT_DELETED(инвалидация дерева, watcher-уведомления) —page.repo.ts:296-378.4. Восстановление из корзины
В
restorePage(page.repo.ts:380-439) при восстановлении обнулятьtemporary_expires_at— иначе дедлайн в прошлом приведёт к мгновенному повторному удалению в ближайший час:5. Настройка X часов (workspace)
По образцу
trashRetentionDays: поле вupdate-workspace.dto.ts(@IsOptional() @IsInt() @Min(1)), audit-tracking вworkspace.service.tsupdate(), эндпоинтPOST /workspace/updateуже обрабатывает любые поля DTO.temporaryNoteHoursдобавить вbaseFieldsworkspace-репозитория.Клиентская часть
1. Отдельная вторая кнопка создания
В шапке дерева
apps/client/src/features/space/components/sidebar/space-sidebar.tsx:114-123рядом с обычнымIconPlusдобавить вторую самостоятельнуюActionIconсIconHourglass(tooltip «Новая временная заметка»), вызывающуюhandleCreate(null, { temporary: true }).Проброс флага в
use-tree-mutation.ts:121-182:create-page.dto/IPageInputна клиенте — добавить опциональныйtemporary?: boolean.2. Символ-пометка в дереве
В
space-tree-row.tsx:175-192рядом с индикатором шаблона добавить аналог для временной заметки (@tabler/icons-reactv3.40 доступен; напр.IconClockHour4/IconHourglass, оранжевый цвет):Проброс поля с сервера в дерево: добавить
temporaryExpiresAtвSidebarPageRow/shapeSidebarPagesTree(sidebar-pages-tree.util.ts) и в SELECT запроса sidebar-страниц; на клиенте — в маппингbuildTree(apps/client/src/features/page/tree/utils/utils.ts), какisTemplate.3. Пункт меню «Сделать постоянной»
Зеркало пункта «Make template» в
space-tree-node-menu.tsx:240-249(хукuseToggleTemporaryMutationпо образцуuseToggleTemplateMutation):Тот же пункт продублировать в меню открытой страницы
page-header-menu.tsx(PageActionMenu, рядом с «Move to trash»).4. Баннер на открытой временной заметке
По образцу
trash-banner.tsx: если у открытой страницы естьtemporaryExpiresAt, показывать сверху предупреждение «⏳ Эта заметка переедет в корзину через N ч. Сделать постоянной?» с кнопкой действия. Усиливает философию «структурируй или умри» и даёт второй явный путь спасения.5. UI-редактор времени жизни (в часах)
Новый компонент в настройках workspace
apps/client/src/pages/settings/workspace/(раздел General или новый «Retention»):NumberInput«Время жизни временной заметки (часы)»,min=1, целое, сохранение черезPOST /workspace/update(temporaryNoteHours). ПолеtemporaryNoteHoursдобавить в клиентскийIWorkspace.6. i18n
Строки в
apps/client/public/locales/en-US/translation.jsonиru-RU/translation.json(плоский «ключ → значение»):Temporary note,Make temporary,Make permanent,New temporary note,Temporary · expires {{when}}, текст баннера, подпись настройки времени жизни.Краевые случаи и решения
removePage). Восстановление возвращает поддерево. Решение зафиксировано (см. выше). Задокументировать в подсказке.restorePageобнуляемtemporary_expires_at→ заметка становится постоянной.creatorIdвremovePage— без изменения схемы; в корзине видно «удалил <автор>».deletedAt IS NULL,removePageтоже фильтруетdeletedAt IS NULLвWHERE→ идемпотентно.validateCanEdit(как у шаблонов); создание — существующими проверками create.Карта точек интеграции
apps/server/src/database/migrations/pages.temporary_expires_at,workspaces.temporary_note_hours+ partial indexapps/server/src/database/types/db.d.tsPagesиWorkspacesapps/server/src/database/repos/page/page.repo.tstemporaryExpiresAtвbaseFields; обнуление вrestorePageapps/server/src/core/page/dto/create-page.dto.tstemporary?: booleanapps/server/src/core/page/services/page.service.tstemporaryExpiresAtapps/server/src/core/page/transclusion/page-template.controller.ts+ новыйToggleTemporaryDtoPOST /pages/toggle-temporarytemporary-note-cleanup.service.ts+apps/server/src/core/page/page.module.ts@Interval-сервисapps/server/src/core/page/services/sidebar-pages-tree.util.tstemporaryExpiresAtapps/server/src/core/workspace/dto/update-workspace.dto.ts+workspace.service.tstemporaryNoteHoursapps/client/src/features/page/types/page.types.ts,apps/client/src/features/page/tree/types.ts,IWorkspacetemporaryExpiresAt?,temporaryNoteHours?space-sidebar.tsx,use-tree-mutation.tstemporaryspace-tree-row.tsxspace-tree-node-menu.tsx,page-header-menu.tsxapps/client/src/features/page/tree/utils/utils.ts(buildTree)SpaceTreeNodeapps/client/src/pages/settings/workspace/NumberInput«время жизни (часы)»apps/client/public/locales/{en-US,ru-RU}/translation.jsonПоэтапный план реализации
db.d.ts→baseFields.create-page.dto+ расчёт дедлайна вcreate()→ToggleTemporaryDto+ эндпоинтtoggle-temporary→ обнуление дедлайна вrestorePage.TemporaryNoteCleanupService+ регистрация вpage.module.sidebar-pages-tree.util(сервер) →IPage/SpaceTreeNode+buildTree(клиент).update-workspace.dto+ audit-tracking + UI-редактор (часы).page-template.controller.spec.tsиtrash-cleanup).Issue является проектной проработкой (design-only). Исходный код в рамках этой задачи не изменялся.