pediatric-ai-scribe-v3/migrations/1777090000000_notes-trash.js
Daniel 355c2a999b feat(notes): trash + restore + DOMPurify sanitizer + 9 contract tests
Soft-delete for notes — Daniel asked for "deleted notes go to trash"
so a slip of the finger doesn't lose work.

Schema: migrations/1777090000000_notes-trash.js adds a deleted_at
timestamptz column to personal_notes (NULL = active) plus an index
on (user_id, deleted_at).

Server (src/routes/notes.js):
  GET    /api/notes              now filters deleted_at IS NULL
  GET    /api/notes/trash        new — list trashed items, newest-
                                  deleted first
  DELETE /api/notes/:id          now soft-deletes (sets deleted_at)
  DELETE /api/notes/:id?hard=1   hard-delete, only allowed on items
                                  already in trash (UI bug can't
                                  erase an active note)
  POST   /api/notes/:id/restore  pull a note out of trash
  POST   /api/notes/trash/empty  hard-delete every trashed note for
                                  the user

Frontend (public/components/notes.html + public/js/notes.js +
public/css/styles.css):
  • Sidebar gets two tabs — "Notes" / "Trash (n)" with live count
  • Trash tab shows deleted-at timestamps, Restore + delete-forever
    per row, Empty-trash button at the bottom
  • Active list and trash count refresh in parallel after every
    save / delete / restore
  • Delete button in the editor now says "Move to trash" and uses
    the showConfirm helper (no native dialogs)

Sanitizer swap (public/js/notes.js):
  Replaced the homegrown allowlist walker with DOMPurify (already
  loaded from cdnjs in index.html, used by learningHub.js too).
  Custom HTML sanitizers historically have bypasses; DOMPurify is
  the right primitive.

Tests (test/notes-sanitize.test.js — node:test + jsdom + dompurify
       as new dev deps):
  9 contract tests covering script-tag stripping, inline event
  handlers, img onerror, iframe/object, style attributes, every
  preserved tag in the allowlist, javascript: URI rejection,
  null/undefined input, and nested-script-inside-paragraph. Total
  test count: 37 → 46 passing.

SW cache bumped to pedscribe-v12-notes5.
2026-04-25 01:02:49 +02:00

21 lines
746 B
JavaScript

/**
* Soft-delete for personal_notes — Daniel asked for "deleted notes go to
* trash" so a slip of the finger doesn't lose work. Adds a deleted_at
* timestamp; NULL means active. Trash listing filters by NOT NULL,
* regular listing filters by NULL.
*
* Restore = clear deleted_at. Empty Trash = real DELETE. No retention
* policy yet — items stay in trash until the user empties it.
*/
exports.up = (pgm) => {
pgm.addColumn('personal_notes', {
deleted_at: { type: 'timestamptz', notNull: false, default: null },
});
pgm.createIndex('personal_notes', ['user_id', 'deleted_at']);
};
exports.down = (pgm) => {
pgm.dropIndex('personal_notes', ['user_id', 'deleted_at']);
pgm.dropColumn('personal_notes', 'deleted_at');
};