diff --git a/internal/app/routes.go b/internal/app/routes.go index 06b9b523..de77de60 100644 --- a/internal/app/routes.go +++ b/internal/app/routes.go @@ -1192,6 +1192,11 @@ func (a *App) RegisterRoutes() { pkgRepo := packages.NewPackageRepository(a.DB) pkgGitHub := packages.NewGitHubClient() pkgService := packages.NewPackageService(pkgRepo, pkgGitHub, a.Config.Upload.MediaPath) + // Rescan system registry when a new system package is installed so it + // appears in the campaign Settings > Game System dropdown immediately. + packages.SetOnSystemInstall(pkgService, func() { + systems.ScanPackageDir(filepath.Join(a.Config.Upload.MediaPath, "packages", "systems")) + }) packages.ConfigureSettings(pkgService, settingsRepo) pkgHandler := packages.NewHandler(pkgService) pkgOwnerHandler := packages.NewOwnerHandler(pkgService) diff --git a/internal/plugins/campaigns/handler.go b/internal/plugins/campaigns/handler.go index 1014da17..a9e2d3b0 100644 --- a/internal/plugins/campaigns/handler.go +++ b/internal/plugins/campaigns/handler.go @@ -465,6 +465,75 @@ func (h *Handler) RemoveBackdrop(c echo.Context) error { return c.Redirect(http.StatusSeeOther, "/campaigns/"+cc.Campaign.ID+"/settings") } +// UploadTopbarImage handles POST /campaigns/:id/topbar-image. Accepts an image +// file, stores it via the media service, and sets the topbar style to image mode. +func (h *Handler) UploadTopbarImage(c echo.Context) error { + cc := GetCampaignContext(c) + if cc == nil { + return apperror.NewMissingContext() + } + if cc.MemberRole < RoleOwner { + return apperror.NewForbidden("only campaign owners can change the topbar") + } + if h.mediaUploader == nil { + return apperror.NewInternal(nil) + } + + file, err := c.FormFile("file") + if err != nil { + return apperror.NewBadRequest("no file provided") + } + src, err := file.Open() + if err != nil { + return apperror.NewInternal(err) + } + defer func() { _ = src.Close() }() + + fileBytes, err := io.ReadAll(src) + if err != nil { + return apperror.NewBadRequest("failed to read file") + } + mimeType := http.DetectContentType(fileBytes) + + // Reuse backdrop upload logic for media storage. + filename, err := h.mediaUploader.UploadBackdrop( + c.Request().Context(), cc.Campaign.ID, + auth.GetUserID(c), fileBytes, file.Filename, mimeType, + ) + if err != nil { + return err + } + + // Update topbar style to image mode with the uploaded file. + style := TopbarStyle{Mode: "image", ImagePath: filename} + if err := h.service.UpdateTopbarStyle(c.Request().Context(), cc.Campaign.ID, &style); err != nil { + return err + } + + h.logAudit(c, cc.Campaign.ID, "campaign.topbar_image.uploaded", nil) + return c.JSON(http.StatusOK, map[string]string{"status": "ok", "image_path": filename}) +} + +// RemoveTopbarImage handles DELETE /campaigns/:id/topbar-image. Resets topbar +// style to default (no image). +func (h *Handler) RemoveTopbarImage(c echo.Context) error { + cc := GetCampaignContext(c) + if cc == nil { + return apperror.NewMissingContext() + } + if cc.MemberRole < RoleOwner { + return apperror.NewForbidden("only campaign owners can change the topbar") + } + + style := TopbarStyle{Mode: ""} + if err := h.service.UpdateTopbarStyle(c.Request().Context(), cc.Campaign.ID, &style); err != nil { + return err + } + + h.logAudit(c, cc.Campaign.ID, "campaign.topbar_image.removed", nil) + return c.JSON(http.StatusOK, map[string]string{"status": "ok"}) +} + // UpdateAccentColorAPI handles PUT /campaigns/:id/accent-color. Sets the // campaign's accent color for branding customization. func (h *Handler) UpdateAccentColorAPI(c echo.Context) error { @@ -775,25 +844,13 @@ func (h *Handler) Settings(c echo.Context) error { // PluginHub renders the campaign plugin hub page, showing all enabled // addons with quick links to their main pages. -// GET /campaigns/:id/plugins +// GET /campaigns/:id/plugins — redirects to Settings > Features tab. func (h *Handler) PluginHub(c echo.Context) error { cc := GetCampaignContext(c) if cc == nil { return apperror.NewMissingContext() } - - var addons []PluginHubAddon - if h.addonLister != nil { - var err error - addons, err = h.addonLister.ListForPluginHub(c.Request().Context(), cc.Campaign.ID) - if err != nil { - slog.Warn("plugin hub: list addons failed", slog.Any("error", err)) - } - } - - isOwner := cc.MemberRole >= RoleOwner - csrfToken := middleware.GetCSRFToken(c) - return middleware.Render(c, http.StatusOK, PluginHubPage(cc, addons, isOwner, csrfToken)) + return c.Redirect(http.StatusSeeOther, "/campaigns/"+cc.Campaign.ID+"/settings?tab=features") } // PluginHubFragment returns the plugin hub list content as an HTMX fragment. diff --git a/internal/plugins/campaigns/routes.go b/internal/plugins/campaigns/routes.go index fb4d6f96..3edf7cd5 100644 --- a/internal/plugins/campaigns/routes.go +++ b/internal/plugins/campaigns/routes.go @@ -73,6 +73,8 @@ func RegisterRoutes(e *echo.Echo, h *Handler, svc CampaignService, authSvc auth. cg.PUT("/accent-color", h.UpdateAccentColorAPI, RequireRole(RoleOwner)) cg.PUT("/branding", h.UpdateBrandingAPI, RequireRole(RoleOwner)) cg.PUT("/topbar-style", h.UpdateTopbarStyleAPI, RequireRole(RoleOwner)) + cg.POST("/topbar-image", h.UploadTopbarImage, RequireRole(RoleOwner)) + cg.DELETE("/topbar-image", h.RemoveTopbarImage, RequireRole(RoleOwner)) cg.PUT("/topbar-content", h.UpdateTopbarContentAPI, RequireRole(RoleOwner)) cg.PUT("/font-family", h.UpdateFontFamilyAPI, RequireRole(RoleOwner)) cg.PUT("/welcome-message", h.UpdateWelcomeMessageAPI, RequireRole(RoleOwner)) diff --git a/internal/plugins/campaigns/settings.templ b/internal/plugins/campaigns/settings.templ index a483fee9..e618b4b4 100644 --- a/internal/plugins/campaigns/settings.templ +++ b/internal/plugins/campaigns/settings.templ @@ -667,19 +667,6 @@ templ settingsPeopleTab(cc *CampaignContext, members []CampaignMember, transfer // settingsIntegrationsTab renders Foundry VTT, API keys, export/import, and quick links. templ settingsIntegrationsTab(cc *CampaignContext, csrfToken, baseURL string) {