# ADR 0031: File Library Recycle Bin And Recent-Visit Tracking ## Status Accepted. ## Context The teacher app (`/app`) gains a left navigation rail with three entries: 文件库 / 最近打开 / 回收站. Two of them need semantics that no prior decision covers: - **回收站 (recycle bin)**: D15 defined soft delete (mark `deletedAt` on the node only; a node is invisible when any ancestor is deleted) but never defined listing, restore, or permanent deletion. - **最近打开 (recent visits)**: nothing tracks opens. ## Decision ### Recycle bin - **List**: shows nodes with `deletedAt != null` whose **ancestors are all active** (the topmost deleted node per branch; descendants of a deleted node are represented by it and not listed separately). - **Visibility/auth**: a bin entry is visible to (a) the website administrator, or (b) any actor holding an active MANAGE grant **on the deleted node itself** (grants stay live through soft delete, so this is a plain grant query — no chain walk, no inheritance; the bin is a management surface, not a browsing surface). - **Restore** clears `deletedAt` on that node only (D15 symmetry: delete marks one node, restore unmarks one node). The subtree becomes visible again immediately. Same auth as the list entry. Audited (`folder_restore` / `project_restore`). - **Permanent delete (彻底删除)** is **website-administrator only**: hard-deletes the node **and its whole subtree** (descendants enumerated via the `pathIds` materialized path, deleted deepest-first because the self-FK is `ON DELETE RESTRICT`), in one transaction, with one audit entry (`node_purge`, detail carries removed count). Grants/settings/export-jobs cascade. There is no recovery; the UI must confirm explicitly. ### Recent visits - **Model**: `FileLibRecentVisit(organizationId, userId, nodeId, filePath, openedAt)`, unique on `(organizationId, userId, nodeId, filePath)` with `filePath` defaulting to `""` (Postgres unique indexes treat NULLs as distinct). `filePath = ""` means the visit is the node itself (drill into folder/project); non-empty means a file preview inside that project. - **Recording is client-driven**: the teacher app POSTs after a successful open (folder drill, project open, file preview). The endpoint requires VIEW on the node (D8: no VIEW → 404, leaking nothing). Upsert semantics: re-opening refreshes `openedAt`. No audit entries — this is a per-user read model, not a权限-sensitive mutation. - **List**: the actor's own most recent 20, `openedAt` desc. Entries whose node is deleted **or has any deleted ancestor** are filtered out (D8/D15 visibility holds on every surface). Names are read live from `FileLibNode` (no denormalization). ## Consequences - No change to existing permission algebra; both features are additive surfaces. - The bin deliberately does not offer per-owner bins or inherited-MANAGE visibility — if real usage demands it, that is a new decision.