Skip to content

Commit c55ccc4

Browse files
authored
Merge pull request #211 from keyxmakerx/claude/chronicle-launch-fixes-LcWMJ
Claude/chronicle launch fixes lc wmj
2 parents 9612f86 + a86fcc0 commit c55ccc4

File tree

8 files changed

+177
-51
lines changed

8 files changed

+177
-51
lines changed

internal/app/routes.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,6 +1192,11 @@ func (a *App) RegisterRoutes() {
11921192
pkgRepo := packages.NewPackageRepository(a.DB)
11931193
pkgGitHub := packages.NewGitHubClient()
11941194
pkgService := packages.NewPackageService(pkgRepo, pkgGitHub, a.Config.Upload.MediaPath)
1195+
// Rescan system registry when a new system package is installed so it
1196+
// appears in the campaign Settings > Game System dropdown immediately.
1197+
packages.SetOnSystemInstall(pkgService, func() {
1198+
systems.ScanPackageDir(filepath.Join(a.Config.Upload.MediaPath, "packages", "systems"))
1199+
})
11951200
packages.ConfigureSettings(pkgService, settingsRepo)
11961201
pkgHandler := packages.NewHandler(pkgService)
11971202
pkgOwnerHandler := packages.NewOwnerHandler(pkgService)

internal/plugins/campaigns/handler.go

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,75 @@ func (h *Handler) RemoveBackdrop(c echo.Context) error {
465465
return c.Redirect(http.StatusSeeOther, "/campaigns/"+cc.Campaign.ID+"/settings")
466466
}
467467

468+
// UploadTopbarImage handles POST /campaigns/:id/topbar-image. Accepts an image
469+
// file, stores it via the media service, and sets the topbar style to image mode.
470+
func (h *Handler) UploadTopbarImage(c echo.Context) error {
471+
cc := GetCampaignContext(c)
472+
if cc == nil {
473+
return apperror.NewMissingContext()
474+
}
475+
if cc.MemberRole < RoleOwner {
476+
return apperror.NewForbidden("only campaign owners can change the topbar")
477+
}
478+
if h.mediaUploader == nil {
479+
return apperror.NewInternal(nil)
480+
}
481+
482+
file, err := c.FormFile("file")
483+
if err != nil {
484+
return apperror.NewBadRequest("no file provided")
485+
}
486+
src, err := file.Open()
487+
if err != nil {
488+
return apperror.NewInternal(err)
489+
}
490+
defer func() { _ = src.Close() }()
491+
492+
fileBytes, err := io.ReadAll(src)
493+
if err != nil {
494+
return apperror.NewBadRequest("failed to read file")
495+
}
496+
mimeType := http.DetectContentType(fileBytes)
497+
498+
// Reuse backdrop upload logic for media storage.
499+
filename, err := h.mediaUploader.UploadBackdrop(
500+
c.Request().Context(), cc.Campaign.ID,
501+
auth.GetUserID(c), fileBytes, file.Filename, mimeType,
502+
)
503+
if err != nil {
504+
return err
505+
}
506+
507+
// Update topbar style to image mode with the uploaded file.
508+
style := TopbarStyle{Mode: "image", ImagePath: filename}
509+
if err := h.service.UpdateTopbarStyle(c.Request().Context(), cc.Campaign.ID, &style); err != nil {
510+
return err
511+
}
512+
513+
h.logAudit(c, cc.Campaign.ID, "campaign.topbar_image.uploaded", nil)
514+
return c.JSON(http.StatusOK, map[string]string{"status": "ok", "image_path": filename})
515+
}
516+
517+
// RemoveTopbarImage handles DELETE /campaigns/:id/topbar-image. Resets topbar
518+
// style to default (no image).
519+
func (h *Handler) RemoveTopbarImage(c echo.Context) error {
520+
cc := GetCampaignContext(c)
521+
if cc == nil {
522+
return apperror.NewMissingContext()
523+
}
524+
if cc.MemberRole < RoleOwner {
525+
return apperror.NewForbidden("only campaign owners can change the topbar")
526+
}
527+
528+
style := TopbarStyle{Mode: ""}
529+
if err := h.service.UpdateTopbarStyle(c.Request().Context(), cc.Campaign.ID, &style); err != nil {
530+
return err
531+
}
532+
533+
h.logAudit(c, cc.Campaign.ID, "campaign.topbar_image.removed", nil)
534+
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
535+
}
536+
468537
// UpdateAccentColorAPI handles PUT /campaigns/:id/accent-color. Sets the
469538
// campaign's accent color for branding customization.
470539
func (h *Handler) UpdateAccentColorAPI(c echo.Context) error {
@@ -775,25 +844,13 @@ func (h *Handler) Settings(c echo.Context) error {
775844

776845
// PluginHub renders the campaign plugin hub page, showing all enabled
777846
// addons with quick links to their main pages.
778-
// GET /campaigns/:id/plugins
847+
// GET /campaigns/:id/plugins — redirects to Settings > Features tab.
779848
func (h *Handler) PluginHub(c echo.Context) error {
780849
cc := GetCampaignContext(c)
781850
if cc == nil {
782851
return apperror.NewMissingContext()
783852
}
784-
785-
var addons []PluginHubAddon
786-
if h.addonLister != nil {
787-
var err error
788-
addons, err = h.addonLister.ListForPluginHub(c.Request().Context(), cc.Campaign.ID)
789-
if err != nil {
790-
slog.Warn("plugin hub: list addons failed", slog.Any("error", err))
791-
}
792-
}
793-
794-
isOwner := cc.MemberRole >= RoleOwner
795-
csrfToken := middleware.GetCSRFToken(c)
796-
return middleware.Render(c, http.StatusOK, PluginHubPage(cc, addons, isOwner, csrfToken))
853+
return c.Redirect(http.StatusSeeOther, "/campaigns/"+cc.Campaign.ID+"/settings?tab=features")
797854
}
798855

799856
// PluginHubFragment returns the plugin hub list content as an HTMX fragment.

internal/plugins/campaigns/routes.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ func RegisterRoutes(e *echo.Echo, h *Handler, svc CampaignService, authSvc auth.
7373
cg.PUT("/accent-color", h.UpdateAccentColorAPI, RequireRole(RoleOwner))
7474
cg.PUT("/branding", h.UpdateBrandingAPI, RequireRole(RoleOwner))
7575
cg.PUT("/topbar-style", h.UpdateTopbarStyleAPI, RequireRole(RoleOwner))
76+
cg.POST("/topbar-image", h.UploadTopbarImage, RequireRole(RoleOwner))
77+
cg.DELETE("/topbar-image", h.RemoveTopbarImage, RequireRole(RoleOwner))
7678
cg.PUT("/topbar-content", h.UpdateTopbarContentAPI, RequireRole(RoleOwner))
7779
cg.PUT("/font-family", h.UpdateFontFamilyAPI, RequireRole(RoleOwner))
7880
cg.PUT("/welcome-message", h.UpdateWelcomeMessageAPI, RequireRole(RoleOwner))

internal/plugins/campaigns/settings.templ

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -667,19 +667,6 @@ templ settingsPeopleTab(cc *CampaignContext, members []CampaignMember, transfer
667667
// settingsIntegrationsTab renders Foundry VTT, API keys, export/import, and quick links.
668668
templ settingsIntegrationsTab(cc *CampaignContext, csrfToken, baseURL string) {
669669
<div class="space-y-6">
670-
// Quick links.
671-
<div class="flex flex-wrap gap-3">
672-
<a href={ templ.SafeURL(fmt.Sprintf("/campaigns/%s/customize", cc.Campaign.ID)) } class="btn-secondary text-sm inline-flex items-center gap-1.5">
673-
<i class="fa-solid fa-paintbrush text-xs"></i> Customization Hub
674-
</a>
675-
<a href={ templ.SafeURL(fmt.Sprintf("/campaigns/%s/plugins", cc.Campaign.ID)) } class="btn-secondary text-sm inline-flex items-center gap-1.5">
676-
<i class="fa-solid fa-puzzle-piece text-xs"></i> Feature Hub
677-
</a>
678-
<a href={ templ.SafeURL(fmt.Sprintf("/campaigns/%s/api-keys", cc.Campaign.ID)) } class="btn-secondary text-sm inline-flex items-center gap-1.5">
679-
<i class="fa-solid fa-key text-xs"></i> API Keys
680-
</a>
681-
</div>
682-
683670
// Foundry VTT.
684671
if baseURL != "" {
685672
<div class="card p-6">

internal/plugins/packages/service.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ type packageService struct {
112112
settings SettingsReader
113113
settingsWriter SettingsWriter
114114
mediaDir string // Root media directory (e.g., ./media).
115+
onSystemInstall func() // Called after a system package is installed.
115116
}
116117

117118
// NewPackageService creates a new package service with the given dependencies.
@@ -123,6 +124,14 @@ func NewPackageService(repo PackageRepository, github *GitHubClient, mediaDir st
123124
}
124125
}
125126

127+
// SetOnSystemInstall wires a callback invoked after a system package is installed.
128+
// Used to rescan the system registry so newly installed systems appear immediately.
129+
func SetOnSystemInstall(svc PackageService, fn func()) {
130+
if s, ok := svc.(*packageService); ok {
131+
s.onSystemInstall = fn
132+
}
133+
}
134+
126135
// ConfigureSettings wires settings reader/writer into the package service.
127136
// Called from routes.go after both services are initialized.
128137
func ConfigureSettings(svc PackageService, settings SettingsReader) {
@@ -396,6 +405,12 @@ func (s *packageService) InstallVersion(ctx context.Context, packageID, version
396405
slog.String("version", version),
397406
slog.String("path", destDir),
398407
)
408+
409+
// Notify system registry to rescan after a system package install.
410+
if pkg.Type != PackageTypeFoundryModule && s.onSystemInstall != nil {
411+
s.onSystemInstall()
412+
}
413+
399414
return nil
400415
}
401416

internal/templates/layouts/app.templ

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -423,29 +423,7 @@ templ Sidebar() {
423423
</svg>
424424
Members
425425
</a>
426-
<a
427-
href={ templ.SafeURL(fmt.Sprintf("/campaigns/%s/plugins", GetCampaignID(ctx))) }
428-
class={ sidebarNavLink,
429-
templ.KV(sidebarNavActive, isPathPrefix(ctx, fmt.Sprintf("/campaigns/%s/plugins", GetCampaignID(ctx)))),
430-
templ.KV(sidebarNavInactive, !isPathPrefix(ctx, fmt.Sprintf("/campaigns/%s/plugins", GetCampaignID(ctx)))) }
431-
>
432-
<span class="w-4 h-4 mr-3 shrink-0 flex items-center justify-center">
433-
<i class="fa-solid fa-puzzle-piece text-xs"></i>
434-
</span>
435-
Features
436-
</a>
437426
if GetCampaignRole(ctx) >= 3 {
438-
<a
439-
href={ templ.SafeURL(fmt.Sprintf("/campaigns/%s/activity", GetCampaignID(ctx))) }
440-
class={ sidebarNavLink,
441-
templ.KV(sidebarNavActive, isPathPrefix(ctx, fmt.Sprintf("/campaigns/%s/activity", GetCampaignID(ctx)))),
442-
templ.KV(sidebarNavInactive, !isPathPrefix(ctx, fmt.Sprintf("/campaigns/%s/activity", GetCampaignID(ctx)))) }
443-
>
444-
<svg class="w-4 h-4 mr-3 shrink-0" width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
445-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"></path>
446-
</svg>
447-
Activity Log
448-
</a>
449427
if IsAddonEnabled(ctx, "media-gallery") {
450428
<a
451429
href={ templ.SafeURL(fmt.Sprintf("/campaigns/%s/media", GetCampaignID(ctx))) }

static/js/widgets/appearance_editor.js

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
brandName: config.brandName || '',
4141
accentColor: config.accentColor || '',
4242
fontFamily: config.fontFamily || '',
43-
topbarStyle: { mode: '', color: '', gradient_from: '', gradient_to: '', gradient_dir: 'to-r' },
43+
topbarStyle: { mode: '', color: '', gradient_from: '', gradient_to: '', gradient_dir: 'to-r', image_path: '' },
4444
topbarContent: { mode: 'none', links: [], quote: '' }
4545
};
4646

@@ -91,6 +91,75 @@
9191
var modeContainer = el.querySelector('#appearance-topbar-mode');
9292
var solidPanel = el.querySelector('#appearance-topbar-solid');
9393
var gradientPanel = el.querySelector('#appearance-topbar-gradient');
94+
var imagePanel = el.querySelector('#appearance-topbar-image');
95+
96+
// Dynamically add "Image" button and upload panel if not in template.
97+
if (modeContainer && !modeContainer.querySelector('[data-mode="image"]')) {
98+
var imgBtn = document.createElement('button');
99+
imgBtn.type = 'button';
100+
imgBtn.className = 'btn-secondary text-xs px-3 py-1.5';
101+
imgBtn.setAttribute('data-mode', 'image');
102+
imgBtn.textContent = 'Image';
103+
modeContainer.appendChild(imgBtn);
104+
}
105+
if (!imagePanel && modeContainer) {
106+
imagePanel = document.createElement('div');
107+
imagePanel.id = 'appearance-topbar-image';
108+
imagePanel.className = 'hidden space-y-2';
109+
imagePanel.innerHTML =
110+
'<p class="text-xs text-fg-secondary">Upload a background image for the topbar.</p>' +
111+
'<div class="flex items-center gap-3">' +
112+
' <label class="btn-secondary text-xs cursor-pointer">' +
113+
' <i class="fa-solid fa-upload mr-1.5"></i>Choose Image' +
114+
' <input type="file" accept="image/*" class="hidden" id="topbar-image-input"/>' +
115+
' </label>' +
116+
' <button type="button" id="topbar-image-remove" class="text-xs text-fg-muted hover:text-rose-400 transition-colors">' +
117+
' <i class="fa-solid fa-trash mr-1"></i>Remove' +
118+
' </button>' +
119+
'</div>';
120+
// Insert after gradient panel.
121+
if (gradientPanel) {
122+
gradientPanel.parentNode.insertBefore(imagePanel, gradientPanel.nextSibling);
123+
}
124+
125+
// Wire upload handler.
126+
var imgInput = imagePanel.querySelector('#topbar-image-input');
127+
if (imgInput) {
128+
imgInput.addEventListener('change', function () {
129+
var file = imgInput.files[0];
130+
if (!file) return;
131+
var form = new FormData();
132+
form.append('file', file);
133+
fetch('/campaigns/' + config.campaignId + '/topbar-image', {
134+
method: 'POST', body: form, credentials: 'same-origin',
135+
headers: { 'X-CSRF-Token': Chronicle.getCsrf() }
136+
}).then(function (res) {
137+
if (res.ok) {
138+
Chronicle.notify('Topbar image uploaded — reloading...', 'success');
139+
setTimeout(function () { window.location.reload(); }, 600);
140+
} else {
141+
Chronicle.notify('Failed to upload image', 'error');
142+
}
143+
});
144+
});
145+
}
146+
var imgRemove = imagePanel.querySelector('#topbar-image-remove');
147+
if (imgRemove) {
148+
imgRemove.addEventListener('click', function () {
149+
Chronicle.apiFetch('/campaigns/' + config.campaignId + '/topbar-image', { method: 'DELETE' })
150+
.then(function (res) {
151+
if (res.ok) {
152+
draft.topbarStyle.mode = '';
153+
draft.topbarStyle.image_path = '';
154+
setActiveMode('');
155+
updateTopbarPreview();
156+
Chronicle.notify('Topbar image removed', 'success');
157+
setTimeout(function () { window.location.reload(); }, 600);
158+
}
159+
});
160+
});
161+
}
162+
}
94163
var solidColorInput = el.querySelector('#appearance-topbar-color');
95164
var gradFromInput = el.querySelector('#appearance-topbar-gradient-from');
96165
var gradToInput = el.querySelector('#appearance-topbar-gradient-to');
@@ -609,6 +678,9 @@
609678
if (gradientPanel) {
610679
gradientPanel.classList.toggle('hidden', mode !== 'gradient');
611680
}
681+
if (imagePanel) {
682+
imagePanel.classList.toggle('hidden', mode !== 'image');
683+
}
612684
}
613685

614686
/**
@@ -626,6 +698,9 @@
626698
var dir = GRADIENT_DIR_CSS[draft.topbarStyle.gradient_dir] || 'to right';
627699
previewTopbar.style.background = 'linear-gradient(' + dir + ', ' + draft.topbarStyle.gradient_from + ', ' + draft.topbarStyle.gradient_to + ')';
628700
previewTopbar.style.color = isLightColor(draft.topbarStyle.gradient_from) ? '' : '#f9fafb';
701+
} else if (mode === 'image' && draft.topbarStyle.image_path) {
702+
previewTopbar.style.background = 'url(/media/' + draft.topbarStyle.image_path + ') center/cover no-repeat';
703+
previewTopbar.style.color = '#f9fafb';
629704
} else {
630705
previewTopbar.style.background = '';
631706
previewTopbar.style.color = '';

static/js/widgets/template_editor.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
Chronicle.register('template-editor', {
1717
init(el) {
18+
console.log('[template-editor] init called, endpoint:', el.dataset.endpoint);
1819
this.el = el;
1920
this.endpoint = el.dataset.endpoint;
2021
this.campaignId = el.dataset.campaignId;
@@ -1516,8 +1517,14 @@ Chronicle.register('template-editor', {
15161517

15171518
bindSave() {
15181519
const btn = this.findSaveBtn();
1520+
console.log('[template-editor] bindSave: btn found =', !!btn, btn);
15191521
if (btn) {
1520-
btn.addEventListener('click', () => this.save());
1522+
btn.addEventListener('click', () => {
1523+
console.log('[template-editor] Save button clicked');
1524+
this.save();
1525+
});
1526+
} else {
1527+
console.warn('[template-editor] Save button #te-save-btn NOT FOUND — save will not work');
15211528
}
15221529

15231530
// Ctrl+S / Cmd+S shortcut. Store reference for cleanup in destroy().

0 commit comments

Comments
 (0)