Skip to content

Commit 0ed8c39

Browse files
committed
fix: address bugs found in sprint W-0.5 review
- Add appearance_editor.js script tag to base.templ (widget wasn't loading) - Add hex color and gradient direction validation in UpdateTopbarStyle - Add brandLogo length validation and include it in audit log - Add destroy handler to appearance_editor widget (timer cleanup on unmount) - Add user-facing error notifications via Chronicle.notify on API failures - Add console.warn for malformed topbar-style JSON in widget init - Add interface method docstrings for UpdateBranding/UpdateTopbarStyle - Remove unused to-bl gradient direction from topbarInlineStyle https://claude.ai/code/session_01QJLkgjQDu5qohzJKGV4hj9
1 parent 676d949 commit 0ed8c39

5 files changed

Lines changed: 63 additions & 15 deletions

File tree

internal/plugins/campaigns/handler.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,7 @@ func (h *Handler) UpdateBrandingAPI(c echo.Context) error {
492492

493493
h.logAudit(c, cc.Campaign.ID, "campaign.branding.updated", map[string]any{
494494
"brand_name": req.BrandName,
495+
"brand_logo": req.BrandLogo,
495496
})
496497

497498
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})

internal/plugins/campaigns/service.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ type CampaignService interface {
5151
// Backdrop and branding
5252
UpdateBackdropPath(ctx context.Context, campaignID string, path *string) error
5353
UpdateAccentColor(ctx context.Context, campaignID string, color string) error
54+
// UpdateBranding sets the campaign's custom brand name and logo path.
5455
UpdateBranding(ctx context.Context, campaignID, brandName, brandLogo string) error
56+
// UpdateTopbarStyle sets the campaign's topbar visual customization.
5557
UpdateTopbarStyle(ctx context.Context, campaignID string, style *TopbarStyle) error
5658
UpdateDmGrants(ctx context.Context, campaignID string, userIDs []string) error
5759

@@ -646,6 +648,9 @@ func (s *campaignService) UpdateBranding(ctx context.Context, campaignID, brandN
646648
if len(brandName) > 40 {
647649
return apperror.NewBadRequest("brand name must be 40 characters or fewer")
648650
}
651+
if len(brandLogo) > 255 {
652+
return apperror.NewBadRequest("brand logo path too long")
653+
}
649654

650655
campaign, err := s.repo.FindByID(ctx, campaignID)
651656
if err != nil {
@@ -675,6 +680,21 @@ func (s *campaignService) UpdateTopbarStyle(ctx context.Context, campaignID stri
675680
default:
676681
return apperror.NewBadRequest("invalid topbar mode, expected solid, gradient, or image")
677682
}
683+
// Validate color values are valid hex (e.g. "#6366f1") or empty.
684+
for _, color := range []string{style.Color, style.GradientFrom, style.GradientTo} {
685+
if color != "" && !isValidHexColor(color) {
686+
return apperror.NewBadRequest("invalid color format, expected #RRGGBB")
687+
}
688+
}
689+
// Validate gradient direction.
690+
if style.GradientDir != "" {
691+
switch style.GradientDir {
692+
case "to-r", "to-br", "to-b":
693+
// ok
694+
default:
695+
return apperror.NewBadRequest("invalid gradient direction")
696+
}
697+
}
678698
}
679699

680700
campaign, err := s.repo.FindByID(ctx, campaignID)
@@ -693,6 +713,19 @@ func (s *campaignService) UpdateTopbarStyle(ctx context.Context, campaignID stri
693713
return s.repo.UpdateSettings(ctx, campaignID, string(settingsJSON))
694714
}
695715

716+
// isValidHexColor checks that s is a 7-character hex color (#RRGGBB).
717+
func isValidHexColor(s string) bool {
718+
if len(s) != 7 || s[0] != '#' {
719+
return false
720+
}
721+
for _, c := range s[1:] {
722+
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
723+
return false
724+
}
725+
}
726+
return true
727+
}
728+
696729
// UpdateDmGrants sets which users are granted dm_only content visibility.
697730
// Only campaign Owners may call this. The granted users can see dm_only
698731
// content but cannot create or toggle dm_only flags.

internal/templates/layouts/app.templ

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -969,8 +969,6 @@ func topbarInlineStyle(ctx context.Context) string {
969969
dir = "to bottom right"
970970
case "to-b":
971971
dir = "to bottom"
972-
case "to-bl":
973-
dir = "to bottom left"
974972
}
975973
if style.GradientFrom != "" && style.GradientTo != "" {
976974
return fmt.Sprintf("background: linear-gradient(%s, %s, %s); border-color: transparent;", dir, style.GradientFrom, style.GradientTo)

internal/templates/layouts/base.templ

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ templ Base(title string) {
152152
<!-- Entity posts (sub-notes) -->
153153
<script src="/static/js/widgets/entity_posts.js" defer></script>
154154

155+
<!-- Appearance editor widget (campaign visual customization) -->
156+
<script src="/static/js/widgets/appearance_editor.js" defer></script>
157+
155158
<!-- Content template picker (entity create form + editor insert) -->
156159
<script src="/static/js/template_picker.js" defer></script>
157160

static/js/widgets/appearance_editor.js

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@
2525
};
2626

2727
Chronicle.register('appearance-editor', {
28+
destroy: function (el) {
29+
// Clear any pending debounce timers to prevent saves after unmount.
30+
if (el._appearanceBrandTimer) {
31+
clearTimeout(el._appearanceBrandTimer);
32+
}
33+
if (el._appearanceTopbarTimer) {
34+
clearTimeout(el._appearanceTopbarTimer);
35+
}
36+
},
2837
init: function (el, config) {
2938
var campaignId = config.campaignId;
3039
var csrfToken = config.csrf;
@@ -41,7 +50,7 @@
4150
topbarStyle = parsed;
4251
}
4352
} catch (e) {
44-
// Ignore parse errors, use defaults.
53+
console.warn('[appearance-editor] Invalid topbar-style JSON, using defaults');
4554
}
4655

4756
// DOM references.
@@ -57,9 +66,9 @@
5766
var gradToInput = el.querySelector('#appearance-topbar-gradient-to');
5867
var gradDirSelect = el.querySelector('#appearance-topbar-gradient-dir');
5968

60-
// Timers for debounced saves.
61-
var brandTimer = null;
62-
var topbarTimer = null;
69+
// Timers for debounced saves (stored on element for destroy cleanup).
70+
el._appearanceBrandTimer = null;
71+
el._appearanceTopbarTimer = null;
6372

6473
// --- Initialization ---
6574

@@ -90,8 +99,8 @@
9099
previewBrand.textContent = brandInput.value || brandInput.placeholder;
91100
}
92101
// Debounced save.
93-
clearTimeout(brandTimer);
94-
brandTimer = setTimeout(function () {
102+
clearTimeout(el._appearanceBrandTimer);
103+
el._appearanceBrandTimer = setTimeout(function () {
95104
saveBranding(brandInput.value);
96105
}, DEBOUNCE_MS);
97106
});
@@ -105,7 +114,7 @@
105114
previewBrand.textContent = brandInput.placeholder;
106115
}
107116
}
108-
clearTimeout(brandTimer);
117+
clearTimeout(el._appearanceBrandTimer);
109118
saveBranding('');
110119
});
111120
}
@@ -208,8 +217,8 @@
208217
* Debounced save for topbar style.
209218
*/
210219
function debouncedSaveTopbar() {
211-
clearTimeout(topbarTimer);
212-
topbarTimer = setTimeout(function () {
220+
clearTimeout(el._appearanceTopbarTimer);
221+
el._appearanceTopbarTimer = setTimeout(function () {
213222
saveTopbarStyle();
214223
}, DEBOUNCE_MS);
215224
}
@@ -222,8 +231,10 @@
222231
method: 'PUT',
223232
body: { brand_name: brandName },
224233
csrfToken: csrfToken
225-
}).catch(function (err) {
226-
console.error('[appearance-editor] Failed to save branding:', err);
234+
}).then(function (res) {
235+
if (!res.ok) { Chronicle.notify('Failed to save brand name', 'error'); }
236+
}).catch(function () {
237+
Chronicle.notify('Failed to save brand name', 'error');
227238
});
228239
}
229240

@@ -243,8 +254,10 @@
243254
method: 'PUT',
244255
body: body,
245256
csrfToken: csrfToken
246-
}).catch(function (err) {
247-
console.error('[appearance-editor] Failed to save topbar style:', err);
257+
}).then(function (res) {
258+
if (!res.ok) { Chronicle.notify('Failed to save topbar style', 'error'); }
259+
}).catch(function () {
260+
Chronicle.notify('Failed to save topbar style', 'error');
248261
});
249262
}
250263
}

0 commit comments

Comments
 (0)