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.
62 lines
2.1 KiB
JSON
62 lines
2.1 KiB
JSON
{
|
|
"name": "pediatric-ai-scribe",
|
|
"version": "6.52.1",
|
|
"description": "AI-powered pediatric clinical documentation platform",
|
|
"main": "server.js",
|
|
"scripts": {
|
|
"start": "node server.js",
|
|
"test": "node --test test/",
|
|
"e2e": "./scripts/e2e.sh",
|
|
"maint:check": "node scripts/maintenance.js check",
|
|
"maint:reindex": "node scripts/maintenance.js reindex",
|
|
"migrate": "node-pg-migrate",
|
|
"migrate:up": "node-pg-migrate up",
|
|
"migrate:down": "node-pg-migrate down 1",
|
|
"migrate:status": "node -e \"const{Pool}=require('pg');const p=new Pool({connectionString:process.env.DATABASE_URL});p.query('SELECT id,name,run_on FROM pgmigrations ORDER BY id').then(r=>{console.table(r.rows);p.end();}).catch(e=>{console.error(e.message);process.exit(1);})\"",
|
|
"migrate:new": "node-pg-migrate create"
|
|
},
|
|
"dependencies": {
|
|
"@marp-team/marp-cli": "^4.3.1",
|
|
"@marp-team/marp-core": "^4.3.0",
|
|
"@tiptap/core": "^3.20.4",
|
|
"@tiptap/extension-color": "^3.20.4",
|
|
"@tiptap/extension-link": "^3.20.4",
|
|
"@tiptap/extension-text-align": "^3.20.4",
|
|
"@tiptap/extension-text-style": "^3.20.4",
|
|
"@tiptap/extension-underline": "^3.20.4",
|
|
"@tiptap/starter-kit": "^3.20.4",
|
|
"argon2": "^0.41.1",
|
|
"axios": "^1.7.7",
|
|
"bcryptjs": "^2.4.3",
|
|
"cookie-parser": "^1.4.7",
|
|
"cors": "^2.8.5",
|
|
"dotenv": "^16.4.5",
|
|
"express": "^4.21.0",
|
|
"express-rate-limit": "^7.4.0",
|
|
"helmet": "^8.0.0",
|
|
"jsonwebtoken": "^9.0.2",
|
|
"mammoth": "^1.8.0",
|
|
"multer": "^1.4.5-lts.1",
|
|
"node-pg-migrate": "^7.7.0",
|
|
"nodemailer": "^8.0.5",
|
|
"openai": "^4.73.0",
|
|
"openid-client": "^6.8.2",
|
|
"pdf-parse": "^1.1.1",
|
|
"pg": "^8.13.0",
|
|
"pptxgenjs": "^4.0.1",
|
|
"qrcode": "^1.5.4",
|
|
"speakeasy": "^2.0.0"
|
|
},
|
|
"optionalDependencies": {
|
|
"@aws-sdk/client-bedrock": "^3.700.0",
|
|
"@aws-sdk/client-bedrock-runtime": "^3.700.0",
|
|
"@aws-sdk/client-s3": "^3.700.0",
|
|
"@aws-sdk/client-transcribe-streaming": "^3.1017.0",
|
|
"@aws-sdk/s3-request-presigner": "^3.700.0",
|
|
"@google-cloud/vertexai": "^1.9.0"
|
|
},
|
|
"devDependencies": {
|
|
"dompurify": "^3.4.1",
|
|
"jsdom": "^29.0.2"
|
|
}
|
|
}
|