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.
121 lines
6.3 KiB
HTML
121 lines
6.3 KiB
HTML
<div class="module-header">
|
|
<h2><i class="fas fa-note-sticky" style="color:#f59e0b;"></i> Notes</h2>
|
|
</div>
|
|
|
|
<!-- data-view controls which pane is visible on mobile (<900px):
|
|
"list" — sidebar only, "reader" — read pane only,
|
|
"editor" — edit pane only. On desktop the sidebar is always
|
|
visible and the right pane flips between reader/editor. -->
|
|
<div class="notes-layout" id="notes-layout" data-view="list">
|
|
|
|
<!-- Left pane: list + new-note button + search + trash toggle -->
|
|
<aside class="notes-sidebar">
|
|
<div class="notes-sidebar-head">
|
|
<button id="btn-notes-new" class="btn-primary notes-new-btn" type="button">
|
|
<i class="fas fa-plus"></i> New note
|
|
</button>
|
|
<div class="notes-search">
|
|
<i class="fas fa-search"></i>
|
|
<input type="text" id="notes-search" placeholder="Search" autocomplete="off">
|
|
</div>
|
|
<div class="notes-tabs">
|
|
<button id="btn-notes-tab-active" class="notes-tab-btn active" type="button" data-view="active">
|
|
Notes
|
|
</button>
|
|
<button id="btn-notes-tab-trash" class="notes-tab-btn" type="button" data-view="trash">
|
|
<i class="fas fa-trash"></i> Trash <span class="notes-trash-count" id="notes-trash-count"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="notes-list" class="notes-list">
|
|
<div class="notes-empty">Loading…</div>
|
|
</div>
|
|
<div id="notes-trash-foot" class="notes-trash-foot hidden">
|
|
<button id="btn-notes-trash-empty" class="btn-sm" type="button"
|
|
style="background:var(--red-light);color:var(--red);border:1px solid var(--red);">
|
|
<i class="fas fa-fire"></i> Empty trash
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Reader pane — shown by default after opening any saved note -->
|
|
<section id="notes-reader" class="notes-reader hidden">
|
|
<div class="notes-reader-head">
|
|
<button type="button" class="btn-sm btn-ghost notes-back" id="btn-notes-reader-back" title="Back to list">
|
|
<i class="fas fa-arrow-left"></i>
|
|
</button>
|
|
<h2 id="notes-reader-title" class="notes-reader-title">Note title</h2>
|
|
<div class="notes-reader-actions">
|
|
<button id="btn-note-edit" class="btn-sm btn-primary" type="button">
|
|
<i class="fas fa-pen"></i> Edit
|
|
</button>
|
|
<button id="btn-note-delete-read" class="btn-sm" type="button"
|
|
style="background:var(--red-light);color:var(--red);border:1px solid var(--red);">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="notes-reader-body" class="notes-reader-body"></div>
|
|
<div class="notes-reader-foot">
|
|
<span id="notes-reader-meta" class="notes-meta"></span>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Editor pane — shown when creating a new note or editing an existing one -->
|
|
<section id="notes-editor" class="notes-editor hidden">
|
|
<div class="notes-editor-head">
|
|
<button type="button" class="btn-sm btn-ghost notes-back" id="btn-notes-editor-back" title="Back">
|
|
<i class="fas fa-arrow-left"></i>
|
|
</button>
|
|
<input type="text" id="note-title"
|
|
class="notes-title-input"
|
|
placeholder="Title"
|
|
maxlength="200"
|
|
autocomplete="off">
|
|
<div class="notes-editor-actions">
|
|
<!-- Voice → AI note: transcribes via the admin-configured
|
|
STT provider, then asks the AI to produce a clean note.
|
|
Pause/Resume available natively through MediaRecorder. -->
|
|
<div class="notes-voice-bar" id="notes-voice-bar">
|
|
<button id="btn-note-rec-start" class="btn-sm btn-ghost" type="button" title="Record voice → AI note">
|
|
<i class="fas fa-microphone" style="color:var(--red);"></i> Dictate
|
|
</button>
|
|
<button id="btn-note-rec-pause" class="btn-sm btn-ghost hidden" type="button">
|
|
<i class="fas fa-pause"></i> Pause
|
|
</button>
|
|
<button id="btn-note-rec-stop" class="btn-sm hidden" type="button" style="background:var(--red);color:white;border:none;">
|
|
<i class="fas fa-stop"></i> Stop
|
|
</button>
|
|
<span id="notes-rec-indicator" class="notes-rec-indicator hidden">
|
|
<span class="pulse-dot"></span>
|
|
<span id="notes-rec-state">Recording</span>
|
|
<span class="notes-rec-timer" id="notes-rec-timer">00:00</span>
|
|
</span>
|
|
</div>
|
|
<span id="notes-status" class="notes-status"></span>
|
|
<button id="btn-note-save" class="btn-sm btn-primary" type="button">
|
|
<i class="fas fa-floppy-disk"></i> Save
|
|
</button>
|
|
<button id="btn-note-delete" class="btn-sm" type="button"
|
|
style="background:var(--red-light);color:var(--red);border:1px solid var(--red);">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="note-body-editor" class="notes-body-editor"></div>
|
|
<div class="notes-editor-foot">
|
|
<span id="note-meta" class="notes-meta"></span>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Empty placeholder — shown on desktop when nothing's picked -->
|
|
<section id="notes-empty-state" class="notes-empty-state">
|
|
<div class="notes-empty-card">
|
|
<div class="notes-empty-icon"><i class="fas fa-note-sticky"></i></div>
|
|
<button id="btn-notes-new-empty" type="button" class="btn-primary notes-empty-cta">
|
|
<i class="fas fa-plus"></i> New note
|
|
</button>
|
|
</div>
|
|
</section>
|
|
|
|
</div>
|