Skip to content

Commit 7156b6b

Browse files
authored
Merge pull request #119 from keyxmakerx/claude/fix-backdrop-customizer-uAn3g
Claude/fix backdrop customizer u an3g
2 parents a46f378 + d605665 commit 7156b6b

26 files changed

Lines changed: 1351 additions & 334 deletions

.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

.ai/status.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,25 @@
88
<!-- ====================================================================== -->
99

1010
## Last Updated
11+
2026-03-11 -- **Sprint V-5: Journal fixes, session edit, @mentions, audio attachments.**
12+
13+
29. **Journal + Sessions + Audio Attachments sprint.** Four changes:
14+
- **Journal save bug fix**: `journal.js` referenced `window.Chronicle._tiptapBundle` which doesn't exist — the TipTap bundle is `window.TipTap`. Fixed the reference so TipTap editor loads correctly and notes save.
15+
- **Session edit UI**: Added edit button + modal on session detail page. Pre-populates all fields (name, date, summary, status, recurrence). Submits JSON PUT to existing `UpdateSessionAPI` endpoint. Visible to Scribe+ users.
16+
- **Journal @mentions**: Added `MentionLink` mark and `MentionExtension` lifecycle wiring to journal's TipTap editor. Users can type `@` to search and link entities with tooltip cards, matching the main entity editor's behavior.
17+
- **Audio attachments (Sprint V-5)**: New `note_attachments` table (migration 000005). Full backend: `AttachmentRepository` + `AttachmentService` + REST handlers (list/upload/delete/transcript). Media service extended with audio MIME types (mp3/ogg/wav/webm) and magic bytes validation, with `sanitizeImage()` guarded to skip audio files. Journal UI: microphone upload button, inline `<audio>` players, collapsible transcript textarea per attachment, delete support.
18+
19+
### Previous Update
20+
2026-03-11 -- **Sprint W-0.5: Visual Customization + Admin DB Explorer (IN PROGRESS).**
21+
22+
28. **Customization Hub & Features page bug fixes.** Five issues resolved:
23+
- **Settings page deduplication**: Removed backdrop upload and accent color picker from campaign settings page — appearance customization now lives exclusively in the Customization Hub's Appearance tab.
24+
- **Accent color page refresh fix**: `UpdateAccentColorAPI` no longer sends `HX-Refresh: true`, preventing the full page reload that navigated users away from the Appearance tab.
25+
- **Draft+Save model for appearance**: Appearance tab now uses a local draft model — brand name, accent color, and topbar style changes preview instantly but are only persisted when the user clicks "Save Changes". Backdrop upload remains immediate (file upload requires server storage).
26+
- **Features tab removed from Customization Hub**: The "Features & Packs" tab was removed; features are managed exclusively on the dedicated Plugin Hub page (`/plugins`). The per-category Attributes editor was moved into the Categories tab where it fits naturally.
27+
- **Plugin Hub in-place toggle**: Feature enable/disable on the Plugin Hub now uses `HX-Trigger: plugin-hub-refresh` instead of `HX-Redirect`, enabling instant in-place list refresh via a new `/plugins/fragment` endpoint.
28+
29+
### Previous Update
1130
2026-03-11 -- **Sprint W-0.5: Visual Customization + Admin DB Explorer (IN PROGRESS).**
1231

1332
27. **Fix: Embed plugin migrations in binary (ADR-030).** Root cause of entity page errors and DB Explorer showing 0/0: plugin migrations used relative filesystem paths (`internal/plugins/*/migrations/`) that only resolve when the binary's CWD is the project root. In Docker, the binary runs from `/app` so migration directories were never found, tables were never created, and entity pages crashed. Fix: each plugin now embeds its `migrations/*.sql` via Go's `embed.FS`. `PluginSchema.MigrationsDir` (string) replaced with `MigrationsFS` (`fs.FS`). `RegisteredPlugins()` moved from `database` package to `cmd/server/main.go` to avoid import cycles (database can't import plugin packages). `PluginSchemas` stored on `App` struct and passed to `DatabaseExplorer` for on-demand re-migration. New `embed.go` files in calendar, maps, sessions, timeline, syncapi plugins.

.ai/todo.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ _Quick capture, backlinks, enhanced graph, editor power-ups. See `.ai/obsidian-n
204204
- [x] **Full-Page Journal & Category Nav Redesign** — Full-page journal view at `/campaigns/:id/journal` with two-panel Obsidian-like layout (note tree sidebar + TipTap editor, search, folder tabs, autosave). Journal link in sidebar nav (gated on notes addon). Notes FAB hidden on journal page to avoid sync conflicts. Removed "Session Journal" topbar button. Redesigned sidebar drill panel: merged header + action bar into compact clickable row (category name opens full page, replacing "View All"), count as pill badge, inline "+" button. Entity tree: folder/page icons, guide lines, smooth transitions, distinct reorder vs reparent D&D feedback.
205205
- [x] **Sprint V-3: Content Templates**`content_templates` table, ContentTemplateService with CRUD + seeding, REST API, template picker on entity create form (loads by entity type), "Insert Template" slash command in editor, 4 default templates (Session Recap, NPC Profile, Location, Quest Log) seeded on campaign creation, Customization Hub "Content Templates" tab for management.
206206
- [ ] **Sprint V-4: Enhanced Graph View & Cover Images**@mention links in graph, entity type/tag filtering, local graph (N hops), clustering, orphan detection. Cover/banner image layout block type for entity pages.
207-
- [ ] **Sprint V-5: Session Journal Audio Attachments**Audio file upload support in Session Journal notes. Users can attach audio recordings (voice memos, session recordings, ambient tracks) to session notes. Privacy controls: share audio with session participants (public to group) or keep private (visible only to uploader). Media plugin integration for storage/serving. Allowed MIME types: audio/mpeg, audio/ogg, audio/wav, audio/webm. Inline audio player in note view. Migration for audio attachment metadata.
207+
- [x] **Sprint V-5: Session Journal Audio Attachments**`note_attachments` table (migration 000005), `AttachmentRepository` + `AttachmentService` + REST handlers (list/upload/delete/transcript). Media service extended with audio MIME types (mp3/ogg/wav/webm) and magic bytes validation; `sanitizeImage()` guarded to skip audio. Journal UI: microphone upload button in toolbar, inline `<audio>` players per attachment, collapsible editable transcript textarea, delete support. Also fixed: journal save bug (`_tiptapBundle``window.TipTap`), added @mentions (MentionLink + MentionExtension) to journal editor, added session edit modal UI.
208208

209209
### Phase W: Polish, Ecosystem & Delight
210210

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DROP TABLE IF EXISTS note_attachments;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-- Note attachments for audio files and transcripts (Sprint V-5).
2+
CREATE TABLE IF NOT EXISTS note_attachments (
3+
id CHAR(26) NOT NULL,
4+
note_id CHAR(26) NOT NULL,
5+
campaign_id CHAR(26) NOT NULL,
6+
file_path VARCHAR(512) NOT NULL,
7+
original_name VARCHAR(255) NOT NULL,
8+
mime_type VARCHAR(100) NOT NULL,
9+
file_size BIGINT NOT NULL DEFAULT 0,
10+
duration_secs INT DEFAULT NULL,
11+
transcript LONGTEXT DEFAULT NULL,
12+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
13+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
14+
15+
PRIMARY KEY (id),
16+
INDEX idx_note_attachments_note (note_id),
17+
INDEX idx_note_attachments_campaign (campaign_id),
18+
19+
CONSTRAINT fk_note_attachments_note FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE
20+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

internal/app/routes.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -983,8 +983,11 @@ func (a *App) RegisterRoutes() {
983983

984984
// Notes widget: personal floating note-taking panel (Google Keep-style).
985985
noteRepo := notes.NewNoteRepository(a.DB)
986-
noteService := notes.NewNoteService(noteRepo)
987-
noteHandler := notes.NewHandler(noteService)
986+
attRepo := notes.NewAttachmentRepository(a.DB)
987+
noteSvc := notes.NewNoteServiceWithAttachments(noteRepo, attRepo)
988+
noteHandler := notes.NewHandler(noteSvc)
989+
noteHandler.SetAttachmentService(noteSvc)
990+
noteHandler.SetMediaUploader(&mediaUploadAdapter{svc: mediaService})
988991
noteHandler.SetMemberLister(campaignService)
989992
notes.RegisterRoutes(e, noteHandler, campaignService, authService)
990993

@@ -1467,3 +1470,25 @@ func (a *App) RegisterRoutes() {
14671470
// REST API v1 is registered above via syncapi.RegisterAPIRoutes().
14681471
// Endpoints: /api/v1/campaigns/:id/{entity-types,entities,sync}
14691472
}
1473+
1474+
// mediaUploadAdapter adapts MediaService to the notes.MediaUploader interface.
1475+
type mediaUploadAdapter struct {
1476+
svc media.MediaService
1477+
}
1478+
1479+
// UploadRaw stores a file via the media service and returns the relative path.
1480+
func (a *mediaUploadAdapter) UploadRaw(ctx context.Context, campaignID, userID string, fileBytes []byte, originalName, mimeType string) (string, error) {
1481+
file, err := a.svc.Upload(ctx, media.UploadInput{
1482+
CampaignID: campaignID,
1483+
UploadedBy: userID,
1484+
OriginalName: originalName,
1485+
MimeType: mimeType,
1486+
FileSize: int64(len(fileBytes)),
1487+
UsageType: "attachment",
1488+
FileBytes: fileBytes,
1489+
})
1490+
if err != nil {
1491+
return "", err
1492+
}
1493+
return file.Filename, nil
1494+
}

internal/plugins/addons/handler.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -173,15 +173,14 @@ func (h *Handler) ToggleCampaignAddon(c echo.Context) error {
173173
}
174174
}
175175

176-
// If the toggle came from the Plugin Hub page, redirect back there
177-
// so it re-renders with updated state.
176+
// If the toggle came from the Plugin Hub page, trigger an in-place
177+
// refresh of the addon list instead of a full-page redirect.
178178
if c.FormValue("redirect_to") == "plugins" {
179-
redirectURL := "/campaigns/" + cc.Campaign.ID + "/plugins"
180179
if middleware.IsHTMX(c) {
181-
c.Response().Header().Set("HX-Redirect", redirectURL)
180+
c.Response().Header().Set("HX-Trigger", "plugin-hub-refresh")
182181
return c.NoContent(http.StatusNoContent)
183182
}
184-
return c.Redirect(http.StatusSeeOther, redirectURL)
183+
return c.Redirect(http.StatusSeeOther, "/campaigns/"+cc.Campaign.ID+"/plugins")
185184
}
186185

187186
// Return updated addon list for HTMX swap (addons settings page).

internal/plugins/campaigns/branding.templ

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,21 @@ templ AccentColorPicker(campaignID string, currentColor string, csrfToken string
128128
}
129129

130130
// appearanceTab renders the visual customization editor in the Customization Hub.
131-
// Contains: faux site outline, brand name/logo, accent color, and topbar styling.
131+
// Contains: faux site outline, brand name/logo, accent color, topbar styling,
132+
// and backdrop image. Changes preview locally; a Save button persists them.
132133
templ appearanceTab(cc *CampaignContext, csrfToken string) {
133134
<h2 class="text-base font-semibold text-fg mb-1">Appearance</h2>
134-
<p class="text-xs text-fg-secondary mb-6">Customize the visual style of your campaign.</p>
135+
<p class="text-xs text-fg-secondary mb-4">Customize the visual style of your campaign. Changes preview instantly — click Save when you're happy.</p>
136+
137+
<!-- Save bar (shown when there are unsaved changes) -->
138+
<div id="appearance-save-bar" class="hidden sticky top-0 z-10 mb-4 p-3 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 flex items-center justify-between">
139+
<span class="text-sm text-amber-800 dark:text-amber-200">
140+
<i class="fa-solid fa-circle-exclamation text-xs mr-1.5"></i> You have unsaved changes
141+
</span>
142+
<button type="button" id="appearance-save-btn" class="btn-primary text-sm px-4 py-1.5">
143+
<i class="fa-solid fa-check text-xs mr-1"></i> Save Changes
144+
</button>
145+
</div>
135146

136147
<div
137148
data-widget="appearance-editor"
@@ -179,6 +190,15 @@ templ appearanceTab(cc *CampaignContext, csrfToken string) {
179190
</div>
180191
</div>
181192

193+
<!-- Backdrop image (saves immediately since file upload requires server) -->
194+
<div class="card p-4 mb-4">
195+
<h3 class="text-sm font-semibold text-fg mb-2">
196+
<i class="fa-solid fa-image text-xs mr-1.5 text-fg-muted"></i> Backdrop Image
197+
</h3>
198+
<p class="text-xs text-fg-secondary mb-3">Header banner displayed on your campaign dashboard.</p>
199+
@BackdropUploadSection(cc.Campaign.ID, cc.Campaign.BackdropPath, csrfToken)
200+
</div>
201+
182202
<!-- Brand name -->
183203
<div class="card p-4 mb-4">
184204
<h3 class="text-sm font-semibold text-fg mb-2">
@@ -205,13 +225,49 @@ templ appearanceTab(cc *CampaignContext, csrfToken string) {
205225
</div>
206226
</div>
207227

208-
<!-- Accent color -->
228+
<!-- Accent color (JS-driven, no server calls until Save) -->
209229
<div class="card p-4 mb-4">
210230
<h3 class="text-sm font-semibold text-fg mb-2">
211231
<i class="fa-solid fa-droplet text-xs mr-1.5 text-fg-muted"></i> Accent Color
212232
</h3>
213233
<p class="text-xs text-fg-secondary mb-3">Theme color used for buttons, links, and highlights.</p>
214-
@AccentColorPicker(cc.Campaign.ID, cc.Campaign.ParseSettings().AccentColor, csrfToken)
234+
<div id="appearance-accent-colors" class="space-y-3">
235+
<div class="flex flex-wrap gap-2">
236+
for _, preset := range accentColorPresets {
237+
<button
238+
type="button"
239+
data-accent-color={ preset.Color }
240+
class="w-8 h-8 rounded-full border-2 transition-transform hover:scale-110 shrink-0"
241+
style={ fmt.Sprintf("background-color: %s", preset.Color) }
242+
if cc.Campaign.ParseSettings().AccentColor == preset.Color {
243+
class="w-8 h-8 rounded-full border-2 border-white ring-2 ring-offset-2 ring-offset-surface ring-fg transition-transform hover:scale-110 shrink-0"
244+
} else {
245+
class="w-8 h-8 rounded-full border-2 border-transparent hover:border-white/50 transition-transform hover:scale-110 shrink-0"
246+
}
247+
title={ preset.Name }
248+
></button>
249+
}
250+
// Reset to default button.
251+
<button
252+
type="button"
253+
data-accent-color=""
254+
class="w-8 h-8 rounded-full border-2 border-dashed border-edge flex items-center justify-center hover:border-fg-muted transition-colors shrink-0"
255+
if cc.Campaign.ParseSettings().AccentColor == "" {
256+
class="w-8 h-8 rounded-full border-2 border-dashed border-fg ring-2 ring-offset-2 ring-offset-surface ring-fg flex items-center justify-center transition-colors shrink-0"
257+
}
258+
title="Reset to default"
259+
>
260+
<i class="fa-solid fa-xmark text-[10px] text-fg-muted"></i>
261+
</button>
262+
</div>
263+
<p class="text-[10px] text-fg-muted" id="appearance-accent-label">
264+
if cc.Campaign.ParseSettings().AccentColor != "" {
265+
Current: { cc.Campaign.ParseSettings().AccentColor }
266+
} else {
267+
Using default theme color
268+
}
269+
</p>
270+
</div>
215271
</div>
216272

217273
<!-- Topbar style -->

0 commit comments

Comments
 (0)