@@ -26,6 +26,14 @@ be shared with all campaign members via the `is_shared` flag.
2626- FK cascade delete from ` notes.id `
2727- Max 50 versions per note, oldest auto-pruned
2828
29+ ### note_attachments table (migration 000005)
30+ - Per-note audio file attachments (voice memos, session recordings)
31+ - ` id ` (CHAR(26) ULID), ` note_id ` (FK CASCADE), ` campaign_id `
32+ - ` file_path ` , ` original_name ` , ` mime_type ` , ` file_size `
33+ - ` duration_secs ` (INT nullable — for future metadata extraction)
34+ - ` transcript ` (LONGTEXT nullable — manual or AI-generated transcript text)
35+ - ` created_at ` , ` updated_at `
36+
2937## Key Features
3038
3139### Core
@@ -87,6 +95,10 @@ All under `/campaigns/:id/notes`:
8795| GET | ` /:noteId/versions ` | ListVersions | Version history |
8896| GET | ` /:noteId/versions/:vid ` | GetVersion | Get specific version |
8997| POST | ` /:noteId/versions/:vid/restore ` | RestoreVersion | Restore to version |
98+ | GET | ` /:nid/attachments ` | ListAttachments | List audio attachments for note |
99+ | POST | ` /:nid/attachments ` | UploadAttachment | Upload audio file (multipart) |
100+ | DELETE | ` /:nid/attachments/:aid ` | DeleteAttachment | Remove attachment |
101+ | PUT | ` /:nid/attachments/:aid/transcript ` | UpdateTranscript | Save transcript text |
90102
91103## Frontend Widget
92104Registered as ` Chronicle.register('notes', ...) ` . Mounted via
@@ -106,19 +118,53 @@ app layout. The widget renders a fixed-position panel in the bottom-right.
106118- ` .note-entry-html ` — rich text display container
107119- ` .notes-versions-* ` — version history sub-panel
108120
121+ ## Audio Attachments (Sprint V-5)
122+
123+ Audio file attachments for journal notes. Foundation for future AI transcription.
124+
125+ ### Backend
126+ - ** ` MediaUploader ` interface** in ` handler.go ` — adapter wrapping media plugin's ` UploadRaw ` method
127+ for multipart file uploads. Wired in ` app/routes.go ` via ` mediaUploadAdapter ` .
128+ - ** ` AttachmentRepository ` ** interface in ` repository.go ` — CRUD for ` note_attachments ` table.
129+ Methods: ` CreateAttachment ` , ` ListByNote ` , ` FindAttachmentByID ` , ` DeleteAttachment ` , ` UpdateTranscript ` .
130+ - ** ` AttachmentService ` ** interface in ` service.go ` — business logic layer.
131+ Methods: ` ListAttachments ` , ` GetAttachment ` , ` CreateAttachment ` , ` DeleteAttachment ` , ` UpdateTranscript ` .
132+ - Upload handler reads multipart file, pipes through ` MediaUploader ` (inherits media plugin's
133+ MIME validation, magic bytes check, quota enforcement), then creates ` NoteAttachment ` record.
134+ - Allowed audio MIME types: ` audio/mpeg ` , ` audio/ogg ` , ` audio/wav ` , ` audio/webm ` .
135+
136+ ### Journal UI (` journal.js ` )
137+ - ** Upload button** : Microphone icon (` <label> ` wrapping hidden file input) in note toolbar.
138+ Accepts audio MIME types only.
139+ - ** Attachments area** : ` #journal-attachments ` div between editor and status bar in ` journal.templ ` .
140+ Hidden when no attachments.
141+ - ** Audio player** : Inline ` <audio> ` element with native controls per attachment.
142+ - ** Transcript** : Collapsible section per attachment with editable textarea and "Save transcript" button.
143+ Submits via ` PUT /:nid/attachments/:aid/transcript ` .
144+ - ** Delete** : Trash icon per attachment with confirmation dialog.
145+ - Attachments auto-load when a note is selected (` loadAttachments ` in ` selectNote ` ).
146+
147+ ### Journal @Mentions
148+ The journal's TipTap editor now includes ` MentionLink ` mark (preserves ` data-mention-id `
149+ and ` data-entity-preview ` attributes) and ` MentionExtension ` lifecycle hooks (onCreate,
150+ onUpdate, onKeyDown, onDestroy). Users can type ` @ ` to search entities and insert mention
151+ links with tooltip cards, matching the main entity editor's behavior.
152+
109153## Dependencies
110154- ** Uses:** ` internal/apperror ` , ` internal/plugins/auth ` (GetUserID, RequireAuth),
111- ` internal/plugins/campaigns ` (RequireCampaignAccess, RequireRole, GetCampaignContext)
112- - ** Used by:** App layout (` NotesWidget() ` templ component)
155+ ` internal/plugins/campaigns ` (RequireCampaignAccess, RequireRole, GetCampaignContext),
156+ ` internal/plugins/media ` (via ` MediaUploader ` adapter for audio uploads)
157+ - ** Used by:** App layout (` NotesWidget() ` templ component), journal page
113158
114159## File Map
115160| File | Purpose |
116161| ------| ---------|
117- | model.go | Note, NoteVersion, Block, ChecklistItem structs; request DTOs; IsLocked()/IsLockedByUser() methods |
118- | repository.go | NoteRepository interface + MariaDB impl (CRUD, list queries, lock ops, version ops) |
119- | service.go | NoteService interface + impl (business logic, version snapshots, lock management) |
120- | handler.go | HTTP handlers (thin: bind, call service, render); canAccessNote helper |
121- | routes.go | Route registration on Echo campaign group |
162+ | model.go | Note, NoteVersion, NoteAttachment, Block, ChecklistItem structs; request DTOs; IsLocked()/IsLockedByUser() methods |
163+ | repository.go | NoteRepository + AttachmentRepository interfaces + MariaDB impl |
164+ | service.go | NoteService + AttachmentService interfaces + impl (business logic, version snapshots, lock management, attachment CRUD) |
165+ | handler.go | HTTP handlers (thin: bind, call service, render); canAccessNote helper; MediaUploader interface; attachment handlers |
166+ | routes.go | Route registration on Echo campaign group (notes + attachments) |
167+ | journal.templ | Full-page journal template with audio upload button and attachments area |
122168| service_test.go | 28 unit tests with mock repository |
123169| .ai.md | This file |
124170
0 commit comments