Skip to content

Commit 21b7c96

Browse files
committed
cleanup: remove orphaned addon settings page, fix JSON injection risk
- Fix potential JSON injection in HX-Trigger header by using json.Marshal instead of string concatenation for error messages - Remove orphaned /addons/settings route, handler, and full-page template (superseded by unified Plugin Hub at /plugins) - Update ToggleCampaignAddon fallback redirect to /plugins - Remove dead hx-target/hx-swap on Plugin Hub toggle forms (handler always responds with HX-Redirect, making swap targets unused) - Record ADR-029 for features page consolidation decision https://claude.ai/code/session_01QJLkgjQDu5qohzJKGV4hj9
1 parent 4388237 commit 21b7c96

7 files changed

Lines changed: 54 additions & 66 deletions

File tree

.ai/decisions.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,3 +1019,34 @@ is skipped for trusted built-in plugins but enforced for user extensions via
10191019
(calendar before sessions/timeline).
10201020
- Removed migrate_preflight.go and bandaid lint tests from migrate_test.go.
10211021
- Fresh DB only — no backward compatibility with the old 63-migration sequence.
1022+
1023+
---
1024+
1025+
## ADR-029: Features Page Consolidation (Plugin Hub + Addon Settings → Single Page)
1026+
1027+
**Date:** 2026-03-10
1028+
**Status:** Accepted
1029+
1030+
**Context:** Campaign feature management was split across two pages:
1031+
1. **Plugin Hub** (`/campaigns/:id/plugins`) — read-only card grid visible to all members.
1032+
2. **Addon Settings** (`/campaigns/:id/addons/settings`) — owner-only toggle list.
1033+
1034+
This created confusion: owners had two "features" pages with different layouts and
1035+
capabilities. Non-owners could see features but couldn't tell which were enabled.
1036+
1037+
**Decision:** Consolidate into a single Features page at `/campaigns/:id/plugins`.
1038+
- All members see the card grid with enable/disable status.
1039+
- Owners see inline toggle buttons on each card.
1040+
- The old `/addons/settings` route, handler, and full-page template are removed.
1041+
- The addons fragment route (`/addons/fragment`) remains for the Customization Hub.
1042+
- Toggle forms include `redirect_to=plugins` so the handler redirects back to the
1043+
unified page after toggling.
1044+
1045+
**Alternatives considered:**
1046+
- Keep both pages with cross-links: still confusing, maintenance burden.
1047+
- Merge into the Customization Hub: too buried, features deserve top-level access.
1048+
1049+
**Consequences:**
1050+
- Single source of truth for feature management.
1051+
- Owners can manage features directly from the same page all members see.
1052+
- Future enhancements (per-addon entity usage, "offline" banners) have one target page.

.ai/status.md

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

1010
## Last Updated
11+
2026-03-10 -- **Cleanup & consolidation pass after bug fixes.**
12+
13+
7. **JSON injection fix**: HX-Trigger header in `CreateEntityType` error path used string concatenation to build JSON. Replaced with `json.Marshal()` to prevent malformed JSON from error messages containing special characters.
14+
15+
8. **Orphaned addon settings page removed**: Deleted `/addons/settings` route, `CampaignAddonsPage` handler, and `CampaignAddonsPageTempl` template — all superseded by the unified Plugin Hub at `/plugins`. Fragment route (`/addons/fragment`) kept for Customization Hub.
16+
17+
9. **Fallback redirect updated**: `ToggleCampaignAddon` non-HTMX fallback now redirects to `/plugins` instead of the removed `/addons/settings`.
18+
19+
10. **Dead HTMX attributes cleaned**: Removed `hx-target`/`hx-swap` from Plugin Hub toggle forms since the handler always responds with `HX-Redirect` (the swap targets were never used).
20+
21+
11. **ADR-029 recorded**: Features page consolidation decision documented in `.ai/decisions.md`.
22+
23+
### Previous Update
1124
2026-03-10 -- **Bug fixes & UX improvements (navbar, features page, entity templates).**
1225

1326
1. **Drag-and-drop CSRF fix**: `sidebar_tree.js` reorderEntity() was using raw `fetch()` without CSRF token, causing 403 on drag-drop reorder. Added `Chronicle.getCsrf()` header.

internal/plugins/addons/campaign_addons.templ

Lines changed: 3 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,13 @@
1-
// campaign_addons.templ renders the per-campaign features & content packs page.
2-
// Campaign owners use this to enable/disable features for their campaign.
1+
// campaign_addons.templ renders per-campaign addon list fragments.
2+
// The full features page is now handled by the campaigns plugin's Plugin Hub
3+
// at /campaigns/:id/plugins. This file provides fragments for the Customization Hub.
34

45
package addons
56

67
import (
78
"fmt"
8-
"github.com/keyxmakerx/chronicle/internal/templates/layouts"
99
)
1010

11-
// CampaignAddonsPageTempl renders the full campaign addons settings page.
12-
templ CampaignAddonsPageTempl(campaignID string, addons []CampaignAddon, csrfToken string) {
13-
@layouts.App("Features & Content Packs") {
14-
<div class="max-w-3xl mx-auto space-y-6">
15-
<div class="flex items-center justify-between">
16-
<div>
17-
<h1 class="text-2xl font-bold text-fg">Features & Content Packs</h1>
18-
<p class="text-sm text-fg-secondary mt-1">
19-
Enable or disable features and content packs for this campaign.
20-
</p>
21-
</div>
22-
<a href={ templ.SafeURL(fmt.Sprintf("/campaigns/%s/edit", campaignID)) } class="btn-secondary text-sm">
23-
Back to Settings
24-
</a>
25-
</div>
26-
27-
<div id="addons-list">
28-
@CampaignAddonsListFragment(campaignID, addons, csrfToken)
29-
</div>
30-
31-
// Info panel.
32-
<div class="card p-6">
33-
<div class="flex items-start gap-4">
34-
<span class="w-10 h-10 rounded-lg bg-accent/10 flex items-center justify-center shrink-0">
35-
<i class="fa-solid fa-circle-info text-accent"></i>
36-
</span>
37-
<div>
38-
<h3 class="text-sm font-semibold text-fg mb-1">About Features & Content Packs</h3>
39-
<p class="text-sm text-fg-secondary leading-relaxed">
40-
Features add optional capabilities to your campaign like calendars, maps, and timelines.
41-
Content packs provide pre-made entity types, tag sets, and reference data.
42-
Toggle them on or off at any time — disabling does not delete your data.
43-
</p>
44-
</div>
45-
</div>
46-
</div>
47-
</div>
48-
}
49-
}
50-
5111
// CampaignAddonsListFragment renders just the addon list (for HTMX swaps).
5212
templ CampaignAddonsListFragment(campaignID string, addons []CampaignAddon, csrfToken string) {
5313
// Group by category.

internal/plugins/addons/handler.go

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -130,22 +130,6 @@ func (h *Handler) CampaignAddonsAPI(c echo.Context) error {
130130
return c.JSON(http.StatusOK, addons)
131131
}
132132

133-
// CampaignAddonsPage renders the campaign addons settings section (GET /campaigns/:id/addons/settings).
134-
func (h *Handler) CampaignAddonsPage(c echo.Context) error {
135-
cc := campaigns.GetCampaignContext(c)
136-
if cc == nil {
137-
return apperror.NewForbidden("campaign context required")
138-
}
139-
140-
addons, err := h.service.ListForCampaign(c.Request().Context(), cc.Campaign.ID)
141-
if err != nil {
142-
return err
143-
}
144-
145-
csrfToken := middleware.GetCSRFToken(c)
146-
return middleware.Render(c, http.StatusOK, CampaignAddonsPageTempl(cc.Campaign.ID, addons, csrfToken))
147-
}
148-
149133
// CampaignAddonsFragment returns the addons list fragment for embedding in the
150134
// Customization Hub Extensions tab (GET /campaigns/:id/addons/fragment).
151135
func (h *Handler) CampaignAddonsFragment(c echo.Context) error {
@@ -210,5 +194,5 @@ func (h *Handler) ToggleCampaignAddon(c echo.Context) error {
210194
return middleware.Render(c, http.StatusOK, CampaignAddonsListFragment(cc.Campaign.ID, addons, csrfToken))
211195
}
212196

213-
return c.Redirect(http.StatusSeeOther, "/campaigns/"+cc.Campaign.ID+"/addons/settings")
197+
return c.Redirect(http.StatusSeeOther, "/campaigns/"+cc.Campaign.ID+"/plugins")
214198
}

internal/plugins/addons/routes.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@ func RegisterCampaignRoutes(e *echo.Echo, h *Handler, campaignSvc campaigns.Camp
2424
campaigns.RequireCampaignAccess(campaignSvc),
2525
)
2626

27-
// Addon settings page (campaign owner only).
28-
cg.GET("/addons/settings", h.CampaignAddonsPage, campaigns.RequireRole(campaigns.RoleOwner))
29-
3027
// Addon list fragment for Customization Hub Extensions tab (HTMX).
3128
cg.GET("/addons/fragment", h.CampaignAddonsFragment, campaigns.RequireRole(campaigns.RoleOwner))
3229

internal/plugins/campaigns/settings.templ

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -516,8 +516,6 @@ templ pluginHubCard(cc *CampaignContext, addon PluginHubAddon, isOwner bool, csr
516516
<form
517517
method="POST"
518518
hx-put={ fmt.Sprintf("/campaigns/%s/addons/%s/toggle", cc.Campaign.ID, strconv.Itoa(addon.AddonID)) }
519-
hx-target="#plugin-hub-list"
520-
hx-swap="innerHTML"
521519
class="shrink-0"
522520
>
523521
<input type="hidden" name="csrf_token" value={ csrfToken }/>

internal/plugins/entities/handler.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1527,7 +1527,12 @@ func (h *Handler) CreateEntityType(c echo.Context) error {
15271527
if middleware.IsHTMX(c) {
15281528
c.Response().Header().Set("HX-Retarget", "#entity-type-list")
15291529
c.Response().Header().Set("HX-Reswap", "innerHTML")
1530-
c.Response().Header().Set("HX-Trigger", `{"chronicle:notify":{"message":"`+errMsg+`","type":"error"}}`)
1530+
triggerData := map[string]any{
1531+
"chronicle:notify": map[string]string{"message": errMsg, "type": "error"},
1532+
}
1533+
if triggerJSON, err := json.Marshal(triggerData); err == nil {
1534+
c.Response().Header().Set("HX-Trigger", string(triggerJSON))
1535+
}
15311536
return middleware.Render(c, http.StatusOK,
15321537
EntityTypeListContent(cc, entityTypes, counts, csrfToken))
15331538
}

0 commit comments

Comments
 (0)