feat(page): temporary notes — auto-trash after X hours unless made permanent (#201)

"Temporary notes" with a death timer: created via a dedicated hourglass button
in the space-tree header, a note auto-moves to Trash after a configurable X
hours (default 24) unless explicitly made permanent ("structure or die").

Reuses existing mechanisms, mirroring is_template and the trash-cleanup job:
- New nullable column pages.temporary_expires_at (NULL = permanent; non-NULL =
  frozen deadline) + partial index for the sweep; workspace column
  temporary_note_hours (default via DEFAULT_TEMPORARY_NOTE_HOURS = 24).
- create-page DTO `temporary` flag; the deadline is frozen at creation so later
  setting changes never reschedule existing notes.
- POST /pages/toggle-temporary (mirror of toggle-template): arm/clear the timer,
  CASL-guarded via validateCanEdit, cross-workspace NotFound defense-in-depth.
- TemporaryNoteCleanupService: hourly @Interval sweep that soft-deletes expired
  notes through the exact PageRepo.removePage path (recursive over children,
  emits PAGE_SOFT_DELETED), attributed to the creator; idempotent via
  deletedAt IS NULL filters.
- restorePage clears temporary_expires_at so a restored note can't be re-trashed.
- Workspace setting temporary_note_hours (audit-tracked) + a hours editor in
  workspace General settings.
- Client: second create button, orange tree icon, tree + page-header menu toggle
  ("Make temporary"/"Make permanent"), an open-note banner with a rescue action,
  and en/ru i18n.

Tests (unit): toggle-temporary controller (toggle/explicit/permission/cross-ws +
DTO validation), cleanup-job sweep (selection filters, per-note removePage,
error isolation), and a migration up/down sanity. Server tsc, client tsc -b,
and the page+workspace jest suites are green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
claude code agent 227
2026-06-26 06:34:22 +03:00
parent fad1aa0501
commit eb5b696431
35 changed files with 1063 additions and 17 deletions
@@ -13,6 +13,7 @@ import {
IconEye,
IconEyeOff,
IconFileExport,
IconHourglass,
IconPlus,
IconSettings,
IconStar,
@@ -71,6 +72,10 @@ export function SpaceSidebar() {
handleCreate(null);
}
function handleCreateTemporaryPage() {
handleCreate(null, { temporary: true });
}
return (
<>
<div className={classes.navbar}>
@@ -111,16 +116,39 @@ export function SpaceSidebar() {
SpaceCaslAction.Manage,
SpaceCaslSubject.Page,
) && (
<Tooltip label={t("Create page")} withArrow position="right">
<ActionIcon
variant="default"
size={18}
onClick={handleCreatePage}
aria-label={t("Create page")}
<>
<Tooltip
label={t("Create page")}
withArrow
position="right"
>
<IconPlus />
</ActionIcon>
</Tooltip>
<ActionIcon
variant="default"
size={18}
onClick={handleCreatePage}
aria-label={t("Create page")}
>
<IconPlus />
</ActionIcon>
</Tooltip>
{/* Standalone second button: a "temporary note" auto-moves to
trash after the workspace lifetime unless made permanent. */}
<Tooltip
label={t("New temporary note")}
withArrow
position="right"
>
<ActionIcon
variant="default"
size={18}
onClick={handleCreateTemporaryPage}
aria-label={t("New temporary note")}
>
<IconHourglass />
</ActionIcon>
</Tooltip>
</>
)}
</Group>
</Group>