Skip to content

Commit d605665

Browse files
committed
docs: update .ai.md files for Sprint V-5 changes
- notes/.ai.md: audio attachments (model, routes, backend, journal UI, @mentions) - media/.ai.md: audio MIME types, magic bytes, image-only re-encoding guard - sessions/.ai.md: edit modal documentation - phases.md: mark Sprint V-5 complete with deliverables https://claude.ai/code/session_01JmHn8AGZVkKPAvHDgR67Hu
1 parent 8c7e391 commit d605665

4 files changed

Lines changed: 99 additions & 16 deletions

File tree

.ai/phases.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,28 @@ and LegendKeeper's page headers.
103103

104104
**Key files:** `relation_graph.js`, layout block types, entity templates
105105

106+
#### Sprint V-5: Session Journal Audio Attachments & Bug Fixes ✅
107+
108+
Audio file attachments for journal notes. Foundation for future AI transcription.
109+
Also fixed journal save bug, added @mentions to journal, and session edit UI.
110+
111+
- **Journal save fix**: `journal.js` referenced nonexistent `window.Chronicle._tiptapBundle`
112+
instead of `window.TipTap`. Fixed so TipTap editor loads and notes save correctly.
113+
- **Session edit UI**: Edit button + `editSessionModal` on session detail page.
114+
Pre-populates all fields, JSON PUT to existing `UpdateSessionAPI`.
115+
- **Journal @mentions**: `MentionLink` mark + `MentionExtension` lifecycle wired
116+
into journal's TipTap editor for entity search and tooltip cards.
117+
- **Audio attachments**: `note_attachments` table (migration 000005).
118+
`AttachmentRepository` + `AttachmentService` + REST handlers (list/upload/delete/transcript).
119+
Media service extended with audio MIME types + magic bytes validation.
120+
Journal UI: microphone upload button, inline `<audio>` players, collapsible
121+
transcript textarea, delete support.
122+
123+
**Key files:** `static/js/widgets/journal.js`, `internal/widgets/notes/journal.templ`,
124+
`internal/widgets/notes/handler.go`, `internal/widgets/notes/repository.go`,
125+
`internal/widgets/notes/service.go`, `internal/plugins/media/service.go`,
126+
`internal/plugins/sessions/sessions.templ`, `db/migrations/000005_note_attachments.up.sql`
127+
106128
---
107129

108130
### Phase W: Polish, Ecosystem & Delight

internal/plugins/media/.ai.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,22 +137,23 @@ Served via `syncapi/media_api_handler.go`, authenticated with API keys.
137137

138138
1. Client sends multipart form with `file` and `usage_type` fields
139139
2. Handler extracts file, CSRF token validation via middleware
140-
3. Service validates: MIME type allowlist (JPEG, PNG, WebP, GIF)
141-
4. Service validates: magic bytes match claimed MIME type
140+
3. Service validates: MIME type allowlist (JPEG, PNG, WebP, GIF, audio/mpeg, audio/ogg, audio/wav, audio/webm)
141+
4. Service validates: magic bytes match claimed MIME type (includes audio: ID3/frame sync for MP3, OggS header, RIFF+WAVE, EBML for WebM)
142142
5. Service checks: static file size limit + dynamic quotas via `StorageLimiter`
143-
6. **Image re-encoding (CDR)**: decode + re-encode strips ALL metadata (EXIF, IPTC, XMP)
144-
and destroys polyglot payloads. WebP is re-encoded as JPEG (no Go WebP encoder).
143+
6. **Image re-encoding (CDR)**: For image MIME types only — decode + re-encode strips ALL metadata
144+
(EXIF, IPTC, XMP) and destroys polyglot payloads. WebP is re-encoded as JPEG (no Go WebP encoder).
145+
Audio files skip re-encoding (guarded by `strings.HasPrefix(mimeType, "image/")`).
145146
7. Service generates UUID filename in date-based directory: `{mediaPath}/2006/01/{uuid}.ext`
146147
8. Service writes sanitized file to disk (0640 permissions, directory 0750)
147-
9. Service generates thumbnails (300px, 800px) using Catmull-Rom interpolation
148+
9. Service generates thumbnails for images only (300px, 800px) using Catmull-Rom interpolation. Audio files skip thumbnail generation.
148149
10. Service inserts DB record with thumbnail paths as JSON
149150
11. Handler returns 201 with signed `{id, url, thumbnail_url, mime_type, file_size}`
150151

151152
## Security
152153

153154
### Upload Security
154-
- **MIME validation**: Allowlist only (JPEG, PNG, WebP, GIF)
155-
- **Magic byte validation**: Prevents Content-Type spoofing
155+
- **MIME validation**: Allowlist only (JPEG, PNG, WebP, GIF, audio/mpeg, audio/ogg, audio/wav, audio/webm)
156+
- **Magic byte validation**: Prevents Content-Type spoofing (images + audio formats)
156157
- **Image re-encoding / CDR**: Strips EXIF/metadata, destroys polyglot payloads
157158
- **Decompression bomb protection**: Max 10,000x10,000px images
158159
- **Rate limiting**: 30 uploads/min per IP
@@ -198,7 +199,7 @@ The campaign media browser (`media_browser.templ`) provides:
198199
- **Per-file progress bars**: XHR-based upload with real-time progress percentage
199200
- **Upload queue**: Alpine.js `mediaUploader` component with status tracking per file
200201
(pending → uploading → done/error), auto-refresh on completion
201-
- Client-side MIME/size validation (JPEG, PNG, WebP, GIF; 10 MB max)
202+
- Client-side MIME/size validation (JPEG, PNG, WebP, GIF; 10 MB max). Audio uploads go through journal widget, not media browser.
202203
- Pagination (24 files per page)
203204
- Storage stats header (file count, total bytes)
204205
- Sidebar link in "Manage" section (Owner-only)

internal/plugins/sessions/.ai.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ real-world play sessions (game nights) with attendee management and entity linki
1919
dependency (auto-invite campaign members). HTMX-aware responses.
2020
- **routes.go**: Route registration with role middleware.
2121
- **sessions.templ**: List page with status grouping (Upcoming/Completed/Cancelled),
22-
detail page with attendee list and RSVP buttons, create modal.
22+
detail page with attendee list and RSVP buttons, create modal, edit modal
23+
(`editSessionModal` — pre-populated fields, JSON PUT to `UpdateSessionAPI`).
2324

2425
## Data Model
2526

@@ -58,6 +59,19 @@ real-world play sessions (game nights) with attendee management and entity linki
5859
3. RSVP updates via HTMX POST, returns refreshed attendee list fragment
5960
4. Session detail shows attendee count summary (e.g., "3/5 going")
6061

62+
## Session Edit UI
63+
64+
- **Edit button**: Pencil icon in session detail header, visible to Scribe+ users.
65+
Opens `#edit-session-modal` dialog.
66+
- **Edit modal** (`editSessionModal` templ component): Pre-populated with current
67+
session values (name, scheduled_date, summary, status, recurrence settings).
68+
Uses `<dialog>` element matching `createSessionModal` pattern.
69+
- **Status select**: Dropdown with planned/completed/cancelled. Uses `data-initial`
70+
attribute + JS initialization (templ doesn't support `selected?=` syntax).
71+
- **Submit**: JS serializes form to JSON, sends `PUT` to existing `UpdateSessionAPI`
72+
endpoint via `Chronicle.apiFetch()`, reloads page on success.
73+
- **Recurrence**: AlpineJS `x-data` toggle for recurring settings (same as create modal).
74+
6175
## Key Design Decisions
6276

6377
- **Separate from calendar events**: Sessions are their own entity, not calendar

internal/widgets/notes/.ai.md

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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
92104
Registered 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

Comments
 (0)