Skip to content

Commit 711a1ca

Browse files
authored
Merge pull request #121 from keyxmakerx/claude/fix-backdrop-customizer-uAn3g
Claude/fix backdrop customizer u an3g
2 parents 919b72c + f379632 commit 711a1ca

15 files changed

Lines changed: 397 additions & 37 deletions

File tree

.ai/status.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,16 @@
88
<!-- ====================================================================== -->
99

1010
## Last Updated
11-
2026-03-11 -- **Sprint V-5: Journal fixes, session edit, @mentions, audio attachments.**
11+
2026-03-11 -- **Session fix + Owner Dashboard (two-dashboard architecture).**
1212

13+
30. **Session completion fix + two-dashboard architecture.** Two changes:
14+
- **Session "Mark Complete" bug fix**: `hx-vals` always sends form-encoded data regardless of `hx-headers` Content-Type. `UpdateSessionAPI` expects JSON, so the decode failed → 400 error → red notification bar. Replaced with `Chronicle.apiFetch()` onclick handler using `data-url` and `data-name` attributes. Also fixes XSS risk from session name interpolation into JSON template string.
15+
- **Two-dashboard architecture**: Split campaign dashboard into two independently customizable dashboards:
16+
- **Campaign Page** (`GET /campaigns/:id`) — visible to all members and public visitors. The "front page" of the campaign.
17+
- **Owner Dashboard** (`GET /campaigns/:id/dashboard`) — visible only to campaign owner. Campaign management with quick links (Settings, Customize, Members, Plugins), category grid, and recent entities.
18+
New migration (000006) adds `owner_dashboard_layout` JSON column. Full CRUD: model field + parser, repository query updates, service methods with shared `validateDashboardLayout()` helper, handler + routes, `OwnerDashboardPage` templ component, sidebar "Dashboard" link for owners, and second dashboard editor section in Customization Hub.
19+
20+
### Previous Update
1321
29. **Journal + Sessions + Audio Attachments sprint.** Four changes:
1422
- **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.
1523
- **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.

.ai/todo.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Known broken or missing things, ordered by severity.
5858
- [x] **Timeline event creation from timeline page** — Already implemented: "Create Event" button in header opens modal with full form (name, date, description, category, visibility, color, multi-day, recurrence). POST to standalone-events API.
5959
- [x] **Calendar shows as pending plugin in admin** — Fixed: set calendar and API to PluginActive, added 5 missing active plugins to registry.
6060
- [x] **False "confirm before leaving" dialog** — Fixed: added htmx:beforeRedirect listener to clear dirty form state before HTMX redirects.
61+
- [x] **Session "Mark Complete" button fails with red error bar**`hx-vals` sends form-encoded data but `UpdateSessionAPI` expects JSON. Replaced with `Chronicle.apiFetch()` onclick. Also fixed XSS risk from session name interpolation into JSON string.
6162
- [x] **Image upload 500 error (ClamAV)** — Removed ClamAV entirely, added structured error logging to upload pipeline.
6263
- [x] **Nav panel not showing new entities** — Fixed: sidebar drill panel refreshes content on navigation instead of closing.
6364
- [x] **Shop inventory "entity type not found" in template editor** — Fixed: added missing data-campaign-id to layout editor fragment widget mount.
@@ -188,7 +189,7 @@ _Fix orphaned data, cascade gaps, and admin DB visibility. See `.ai/phases.md`._
188189

189190
### Phase U: Collaboration & Platform Maturity
190191

191-
- [ ] **Sprint U-1: Role-Aware Dashboards**Role-keyed dashboard layouts. Dashboard editor gains role selector. Players see role-specific dashboard or default fallback.
192+
- [~] **Sprint U-1: Role-Aware Dashboards**Two-dashboard architecture implemented: Campaign Page (public, `/campaigns/:id`) and Owner Dashboard (owner-only, `/campaigns/:id/dashboard`). Both independently customizable via Customization Hub. Migration 000006 adds `owner_dashboard_layout` column. Remaining: role selector in dashboard editor so Players/Scribes can see role-specific campaign page layouts.
192193
- [ ] **Sprint U-2: Invite System** — Migration: `campaign_invites` table. Email invitations with one-click accept link. Non-public campaigns require invitation. Invite management UI.
193194
- [ ] **Sprint U-3: 2FA/TOTP Support** — TOTP enrollment with QR code (`pquerna/otp`). Login redirect to TOTP input. Recovery codes (8 hashed). Admin force-disable.
194195
- [ ] **Sprint U-4: Accessibility Audit (WCAG 2.1 AA)** — ARIA labels, focus traps, skip-to-content, color contrast 4.5:1, keyboard nav, screen reader announcements, axe-core scanning.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE campaigns DROP COLUMN owner_dashboard_layout;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE campaigns ADD COLUMN owner_dashboard_layout JSON DEFAULT NULL;

internal/plugins/campaigns/.ai.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ Plugin (Core -- always enabled)
5151
| GET | /campaigns | Index | Auth only | List user's campaigns |
5252
| GET | /campaigns/new | NewForm | Auth only | Create campaign form |
5353
| POST | /campaigns | Create | Auth only | Create campaign |
54-
| GET | /campaigns/:id | Show | Player | Campaign dashboard |
54+
| GET | /campaigns/:id | Show | Player | Campaign page (public dashboard) |
55+
| GET | /campaigns/:id/dashboard | OwnerDashboard | Owner | Owner-only management dashboard |
5556
| GET | /campaigns/:id/edit | EditForm | Owner | Edit form |
5657
| PUT | /campaigns/:id | Update | Owner | Update campaign |
5758
| DELETE | /campaigns/:id | Delete | Owner | Delete campaign |
@@ -65,6 +66,9 @@ Plugin (Core -- always enabled)
6566
| GET | /campaigns/:id/accept-transfer | AcceptTransfer | Auth only | Accept (token) |
6667
| POST | /campaigns/:id/cancel-transfer | CancelTransfer | Owner | Cancel transfer |
6768
| GET | /campaigns/:id/customize | Customize | Owner | Customization Hub |
69+
| GET | /campaigns/:id/owner-dashboard-layout | GetOwnerDashboardLayout | Owner | Get owner dashboard layout JSON |
70+
| PUT | /campaigns/:id/owner-dashboard-layout | UpdateOwnerDashboardLayout | Owner | Save owner dashboard layout |
71+
| DELETE | /campaigns/:id/owner-dashboard-layout | ResetOwnerDashboardLayout | Owner | Reset owner dashboard to defaults |
6872
| GET | /campaigns/:id/plugins | PluginHub | Player | Features page |
6973
| GET | /campaigns/:id/sidebar-config | GetSidebarConfig | Player | Get sidebar config |
7074
| PUT | /campaigns/:id/sidebar-config | UpdateSidebarConfig | Owner | Save sidebar order/visibility |
@@ -89,6 +93,15 @@ Plugin (Core -- always enabled)
8993
- **Accent color**: `accent_color` column — CSS `--color-accent` override injected via layout
9094
- Campaign settings page has image upload and color picker sections
9195

96+
## Two-Dashboard Architecture
97+
98+
Campaigns have two independently customizable dashboards:
99+
100+
- **Campaign Page** (`GET /campaigns/:id`) — visible to all members and public visitors. The "front page" of the campaign. Layout stored in `dashboard_layout` column.
101+
- **Owner Dashboard** (`GET /campaigns/:id/dashboard`) — visible only to campaign owner. Management-focused dashboard with quick links (Settings, Customize, Members, Plugins), category grid, and recent entities. Layout stored in `owner_dashboard_layout` column (migration 000006).
102+
103+
Both dashboards use the same `DashboardBlockSwitch` dispatcher and are editable via the Customization Hub (Dashboard tab shows both editors side-by-side). The dashboard editor widget mounts with different `data-endpoint` values pointing to the respective layout APIs.
104+
92105
## Dashboard Block Types
93106

94107
The campaigns plugin defines the central `DashboardBlockSwitch` dispatcher. Supported block types:

internal/plugins/campaigns/customize.templ

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,9 @@ templ dashboardEditorTab(cc *CampaignContext, csrfToken string) {
146146
<div class="h-full flex flex-col">
147147
<div class="px-6 pt-4 pb-2 shrink-0">
148148
<div class="max-w-3xl mx-auto">
149-
<h2 class="text-base font-semibold text-fg mb-0.5">Campaign Dashboard</h2>
149+
<h2 class="text-base font-semibold text-fg mb-0.5">Campaign Page</h2>
150150
<p class="text-xs text-fg-secondary">
151-
Customize the layout of your campaign's main dashboard page.
151+
Customize the layout of your campaign's main page visible to all members.
152152
</p>
153153
</div>
154154
</div>
@@ -160,6 +160,22 @@ templ dashboardEditorTab(cc *CampaignContext, csrfToken string) {
160160
data-csrf-token={ csrfToken }
161161
></div>
162162
</div>
163+
<div class="px-6 pt-6 pb-2 shrink-0 border-t border-edge mt-4">
164+
<div class="max-w-3xl mx-auto">
165+
<h2 class="text-base font-semibold text-fg mb-0.5">Owner Dashboard</h2>
166+
<p class="text-xs text-fg-secondary">
167+
Customize your private management dashboard (visible to owner and co-DMs only).
168+
</p>
169+
</div>
170+
</div>
171+
<div class="flex-1 overflow-y-auto px-6 py-2">
172+
<div
173+
data-widget="dashboard-editor"
174+
data-endpoint={ fmt.Sprintf("/campaigns/%s/owner-dashboard-layout", cc.Campaign.ID) }
175+
data-campaign-id={ cc.Campaign.ID }
176+
data-csrf-token={ csrfToken }
177+
></div>
178+
</div>
163179
</div>
164180
}
165181

internal/plugins/campaigns/handler.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,76 @@ func (h *Handler) ResetDashboardLayout(c echo.Context) error {
812812
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
813813
}
814814

815+
// --- Owner Dashboard ---
816+
817+
// OwnerDashboard renders the owner-only management dashboard (GET /campaigns/:id/dashboard).
818+
func (h *Handler) OwnerDashboard(c echo.Context) error {
819+
cc := GetCampaignContext(c)
820+
if cc == nil {
821+
return apperror.NewMissingContext()
822+
}
823+
824+
var recentEntities []RecentEntity
825+
if h.recentLister != nil {
826+
recentEntities, _ = h.recentLister.ListRecentForDashboard(
827+
c.Request().Context(), cc.Campaign.ID, int(cc.MemberRole), auth.GetUserID(c), 8,
828+
)
829+
}
830+
831+
csrfToken := middleware.GetCSRFToken(c)
832+
return middleware.Render(c, http.StatusOK, OwnerDashboardPage(cc, recentEntities, csrfToken))
833+
}
834+
835+
// GetOwnerDashboardLayout returns the owner dashboard layout JSON (GET /campaigns/:id/owner-dashboard-layout).
836+
func (h *Handler) GetOwnerDashboardLayout(c echo.Context) error {
837+
cc := GetCampaignContext(c)
838+
if cc == nil {
839+
return apperror.NewMissingContext()
840+
}
841+
842+
layout, err := h.service.GetOwnerDashboardLayout(c.Request().Context(), cc.Campaign.ID)
843+
if err != nil {
844+
return err
845+
}
846+
847+
return c.JSON(http.StatusOK, layout)
848+
}
849+
850+
// UpdateOwnerDashboardLayout saves the owner dashboard layout (PUT /campaigns/:id/owner-dashboard-layout).
851+
func (h *Handler) UpdateOwnerDashboardLayout(c echo.Context) error {
852+
cc := GetCampaignContext(c)
853+
if cc == nil {
854+
return apperror.NewMissingContext()
855+
}
856+
857+
var layout DashboardLayout
858+
if err := json.NewDecoder(c.Request().Body).Decode(&layout); err != nil {
859+
return apperror.NewBadRequest("invalid JSON body")
860+
}
861+
862+
if err := h.service.UpdateOwnerDashboardLayout(c.Request().Context(), cc.Campaign.ID, &layout); err != nil {
863+
return err
864+
}
865+
866+
h.logAudit(c, cc.Campaign.ID, "owner_dashboard_layout_updated", nil)
867+
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
868+
}
869+
870+
// ResetOwnerDashboardLayout removes the custom owner dashboard layout (DELETE /campaigns/:id/owner-dashboard-layout).
871+
func (h *Handler) ResetOwnerDashboardLayout(c echo.Context) error {
872+
cc := GetCampaignContext(c)
873+
if cc == nil {
874+
return apperror.NewMissingContext()
875+
}
876+
877+
if err := h.service.ResetOwnerDashboardLayout(c.Request().Context(), cc.Campaign.ID); err != nil {
878+
return err
879+
}
880+
881+
h.logAudit(c, cc.Campaign.ID, "owner_dashboard_layout_reset", nil)
882+
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
883+
}
884+
815885
// --- View As Player Toggle ---
816886

817887
// viewAsPlayerCookie is the cookie name for the "view as player" display toggle.

internal/plugins/campaigns/model.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,9 @@ type Campaign struct {
9898
Settings string `json:"settings"`
9999
BackdropPath *string `json:"backdrop_path,omitempty"`
100100
SidebarConfig string `json:"sidebar_config"`
101-
DashboardLayout *string `json:"dashboard_layout,omitempty"` // JSON layout; nil = use hardcoded default.
102-
CreatedBy string `json:"created_by"`
101+
DashboardLayout *string `json:"dashboard_layout,omitempty"` // JSON layout; nil = use hardcoded default.
102+
OwnerDashboardLayout *string `json:"owner_dashboard_layout,omitempty"` // Owner-only dashboard layout; nil = use default.
103+
CreatedBy string `json:"created_by"`
103104
CreatedAt time.Time `json:"created_at"`
104105
UpdatedAt time.Time `json:"updated_at"`
105106
}
@@ -277,6 +278,23 @@ func (c *Campaign) ParseDashboardLayout() *DashboardLayout {
277278
return &layout
278279
}
279280

281+
// ParseOwnerDashboardLayout parses the campaign's owner_dashboard_layout JSON
282+
// into a DashboardLayout struct. Returns nil if the column is NULL (use default).
283+
func (c *Campaign) ParseOwnerDashboardLayout() *DashboardLayout {
284+
if c.OwnerDashboardLayout == nil || *c.OwnerDashboardLayout == "" {
285+
return nil
286+
}
287+
var layout DashboardLayout
288+
if err := json.Unmarshal([]byte(*c.OwnerDashboardLayout), &layout); err != nil {
289+
slog.Warn("failed to parse owner dashboard layout, using default",
290+
slog.String("campaign_id", c.ID),
291+
slog.String("error", err.Error()),
292+
)
293+
return nil
294+
}
295+
return &layout
296+
}
297+
280298
// CampaignSettings holds campaign-level configuration stored as JSON in
281299
// the campaigns.settings column. Accent color, display preferences, etc.
282300
type CampaignSettings struct {

0 commit comments

Comments
 (0)