Compare commits

...

6 Commits

Author SHA1 Message Date
Jonathan Miller f19fd9683f fix(ui): left-align all admin sidebar menu items
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Fomantic UI's vertical menu centers text by default. Added
text-align: left to .flex-container-nav menu items so all
admin sidebar entries align consistently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 22:47:36 -05:00
jmiller 4ef4aeb04a Merge pull request 'feat: admin branding page with uploadable images (#181)' (#182) from feat/181-admin-branding into dev
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 20s
2026-05-26 02:13:37 +00:00
jmiller 4ec61ec260 Merge pull request 'fix: org sidebar not listing members or teams (#183)' (#184) from fix/183-org-sidebar into dev
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 21s
2026-05-26 01:57:26 +00:00
Jonathan Miller b5defc2a4a fix: populate org home sidebar with members, teams, and public member status (#183)
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
The org home page template expects .Members, .NumMembers, .Teams,
.IsOrganizationMember, .IsOrganizationOwner, and .IsPublicMember
but the handler only set OrgOverviewMembers and OrgOverviewTeams
(different key names). The sidebar rendered empty because the
template variables were undefined.

Fixed by adding all required data bindings and capturing the
membersIsPublic map (previously discarded) as a callable function.

Closes #183

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 20:56:44 -05:00
Jonathan Miller d77713dd77 feat: admin branding page with uploadable nav icon, logo, and favicon (#181)
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Add a Branding section to Site Administration where admins can upload
custom images for three separate slots:

- Nav icon (logo-small.png) — top-left corner, 30x30px
- Login logo (logo.png) — login page and homepage
- Favicon (favicon.png) — browser tab icon

Changes:
- New admin route: /-/admin/branding with upload forms
- Templates use AssetUrlPrefix instead of hardcoded external URLs
- Nav bar uses logo-small.png with fallback to logo.png
- Uploads save to custom/public/assets/img/ (persists across restarts)
- SVG overrides auto-removed when PNG is uploaded
- Added logo-small.png as default built-in asset

Closes #181

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 20:23:34 -05:00
jmiller a45be34139 Merge pull request 'feat(ci): auto-update updates.xml on production deploy' (#179) from feat/auto-update-xml into dev
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 25s
2026-05-26 00:56:19 +00:00
10 changed files with 220 additions and 4 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+118
View File
@@ -0,0 +1,118 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package admin
import (
"io"
"net/http"
"os"
"path/filepath"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
const tplBranding templates.TplName = "admin/branding"
// brandingImageDir returns the path to the custom branding images directory.
func brandingImageDir() string {
return filepath.Join(setting.CustomPath, "public", "assets", "img")
}
// Branding shows the admin branding page.
func Branding(ctx *context.Context) {
ctx.Data["Title"] = "Branding"
ctx.Data["PageIsAdminBranding"] = true
imgDir := brandingImageDir()
ctx.Data["HasNavIcon"] = fileExists(filepath.Join(imgDir, "logo-small.png"))
ctx.Data["HasLogo"] = fileExists(filepath.Join(imgDir, "logo.png"))
ctx.Data["HasFavicon"] = fileExists(filepath.Join(imgDir, "favicon.png"))
ctx.HTML(http.StatusOK, tplBranding)
}
// BrandingUpload handles branding image uploads.
func BrandingUpload(ctx *context.Context) {
imageType := ctx.FormString("type")
if imageType == "" {
ctx.Flash.Error("No image type specified")
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
return
}
var filename string
switch imageType {
case "nav-icon":
filename = "logo-small.png"
case "logo":
filename = "logo.png"
case "favicon":
filename = "favicon.png"
default:
ctx.Flash.Error("Invalid image type: " + imageType)
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
return
}
file, header, err := ctx.Req.FormFile("file")
if err != nil {
ctx.Flash.Error("Upload failed: " + err.Error())
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
return
}
defer file.Close()
// Validate file size (max 2MB)
if header.Size > 2*1024*1024 {
ctx.Flash.Error("File too large (max 2MB)")
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
return
}
// Ensure the custom image directory exists
imgDir := brandingImageDir()
if err := os.MkdirAll(imgDir, 0o755); err != nil {
ctx.Flash.Error("Failed to create image directory")
log.Error("MkdirAll %s: %v", imgDir, err)
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
return
}
// Write the file
destPath := filepath.Join(imgDir, filename)
dest, err := os.Create(destPath)
if err != nil {
ctx.Flash.Error("Failed to save image")
log.Error("Create %s: %v", destPath, err)
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
return
}
defer dest.Close()
if _, err := io.Copy(dest, file); err != nil {
ctx.Flash.Error("Failed to write image")
log.Error("Copy to %s: %v", destPath, err)
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
return
}
// Also remove SVG override if present (PNG should take priority)
svgPath := filepath.Join(imgDir, filename[:len(filename)-4]+".svg")
if fileExists(svgPath) {
os.Remove(svgPath)
log.Info("Removed SVG override: %s", svgPath)
}
ctx.Flash.Success("Branding image updated: " + imageType)
log.Info("Branding image uploaded: %s (%d bytes)", filename, header.Size)
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
}
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
+9 -1
View File
@@ -93,7 +93,7 @@ func home(ctx *context.Context, viewRepositories bool) {
ListOptions: db.ListOptions{Page: 1, PageSize: 25},
}
members, _, err := organization.FindOrgMembers(ctx, opts)
members, membersIsPublic, err := organization.FindOrgMembers(ctx, opts)
if err != nil {
ctx.ServerError("FindOrgMembers", err)
return
@@ -102,6 +102,14 @@ func home(ctx *context.Context, viewRepositories bool) {
const orgOverviewTeamsLimit = 5
ctx.Data["OrgOverviewMembers"] = members
ctx.Data["OrgOverviewTeams"] = ctx.Org.Teams[:min(len(ctx.Org.Teams), orgOverviewTeamsLimit)]
ctx.Data["Members"] = members
ctx.Data["NumMembers"] = len(members)
ctx.Data["Teams"] = ctx.Org.Teams
ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember
ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner
ctx.Data["IsPublicMember"] = func(uid int64) bool {
return membersIsPublic[uid]
}
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0
+5
View File
@@ -765,6 +765,11 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Get("/settings", admin.ConfigSettings)
})
m.Group("/branding", func() {
m.Get("", admin.Branding)
m.Post("/upload", admin.BrandingUpload)
})
m.Group("/monitor", func() {
m.Get("/stats", admin.MonitorStats)
m.Get("/cron", admin.CronTasks)
+78
View File
@@ -0,0 +1,78 @@
{{template "admin/layout_head" (dict "pageClass" "admin branding")}}
<div class="admin-setting-content">
<h4 class="ui top attached header">
{{svg "octicon-paintbrush" 16}} Branding
</h4>
<div class="ui attached segment">
<p>Upload custom branding images. Changes take effect immediately — no restart required.</p>
<div class="ui three stackable cards">
<!-- Nav Icon -->
<div class="card">
<div class="content">
<div class="header">Nav Icon</div>
<div class="meta">Top-left corner (30x30px recommended)</div>
</div>
<div class="image tw-p-4 tw-text-center" style="background: var(--color-body);">
<img src="{{AssetUrlPrefix}}/img/logo-small.png?v={{ctx.CspScriptNonce}}" style="max-height: 64px;" onerror="this.src='{{AssetUrlPrefix}}/img/logo.png'">
</div>
<div class="extra content">
<form method="post" action="{{AppSubUrl}}/-/admin/branding/upload" enctype="multipart/form-data">
{{.CsrfTokenHtml}}
<input type="hidden" name="type" value="nav-icon">
<div class="ui action input tw-w-full">
<input type="file" name="file" accept="image/png,image/svg+xml" required>
<button type="submit" class="ui primary button">{{svg "octicon-upload" 14}} Upload</button>
</div>
</form>
{{if .HasNavIcon}}<span class="ui green label tw-mt-2">Custom</span>{{else}}<span class="ui grey label tw-mt-2">Default</span>{{end}}
</div>
</div>
<!-- Login Logo -->
<div class="card">
<div class="content">
<div class="header">Login Logo</div>
<div class="meta">Login page and homepage (wide format recommended)</div>
</div>
<div class="image tw-p-4 tw-text-center" style="background: var(--color-body);">
<img src="{{AssetUrlPrefix}}/img/logo.png?v={{ctx.CspScriptNonce}}" style="max-height: 64px;">
</div>
<div class="extra content">
<form method="post" action="{{AppSubUrl}}/-/admin/branding/upload" enctype="multipart/form-data">
{{.CsrfTokenHtml}}
<input type="hidden" name="type" value="logo">
<div class="ui action input tw-w-full">
<input type="file" name="file" accept="image/png,image/svg+xml" required>
<button type="submit" class="ui primary button">{{svg "octicon-upload" 14}} Upload</button>
</div>
</form>
{{if .HasLogo}}<span class="ui green label tw-mt-2">Custom</span>{{else}}<span class="ui grey label tw-mt-2">Default</span>{{end}}
</div>
</div>
<!-- Favicon -->
<div class="card">
<div class="content">
<div class="header">Favicon</div>
<div class="meta">Browser tab icon (256x256px recommended)</div>
</div>
<div class="image tw-p-4 tw-text-center" style="background: var(--color-body);">
<img src="{{AssetUrlPrefix}}/img/favicon.png?v={{ctx.CspScriptNonce}}" style="max-height: 64px;">
</div>
<div class="extra content">
<form method="post" action="{{AppSubUrl}}/-/admin/branding/upload" enctype="multipart/form-data">
{{.CsrfTokenHtml}}
<input type="hidden" name="type" value="favicon">
<div class="ui action input tw-w-full">
<input type="file" name="file" accept="image/png,image/svg+xml,image/x-icon" required>
<button type="submit" class="ui primary button">{{svg "octicon-upload" 14}} Upload</button>
</div>
</form>
{{if .HasFavicon}}<span class="ui green label tw-mt-2">Custom</span>{{else}}<span class="ui grey label tw-mt-2">Default</span>{{end}}
</div>
</div>
</div>
</div>
</div>
{{template "admin/layout_tail" .}}
+3
View File
@@ -84,6 +84,9 @@
</div>
</details>
{{end}}
<a class="{{if .PageIsAdminBranding}}active {{end}}item" href="{{AppSubUrl}}/-/admin/branding">
{{svg "octicon-paintbrush" 16}} Branding
</a>
<details class="item toggleable-item" {{if or .PageIsAdminConfig}}open{{end}}>
<summary>{{ctx.Locale.Tr "admin.config"}}</summary>
<div class="menu">
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="navbar-left">
<!-- the logo -->
<a class="item" id="navbar-logo" href="{{AppSubUrl}}/" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home_title"}}{{end}}">
<img width="30" height="30" src="https://mokoconsulting.tech/images/branding/logo.png" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
<img width="30" height="30" src="{{AssetUrlPrefix}}/img/logo-small.png" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true" onerror="this.src='{{AssetUrlPrefix}}/img/logo.png'">
</a>
<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column -->
+1 -1
View File
@@ -2,7 +2,7 @@
<div role="main" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home_title"}}{{end}}" class="page-content home">
<div class="tw-mb-8 tw-px-8">
<div class="center">
<img class="logo" width="220" height="220" src="https://mokoconsulting.tech/images/branding/logo.png" alt="{{ctx.Locale.Tr "logo"}}">
<img class="logo" width="220" height="220" src="{{AssetUrlPrefix}}/img/logo.png" alt="{{ctx.Locale.Tr "logo"}}">
<div class="hero">
<h1 class="ui icon header title tw-text-balance">
{{AppName}}
+1 -1
View File
@@ -21,7 +21,7 @@
<div class="ui container tw-flex">
<div class="item tw-flex-1">
<a href="{{AppSubUrl}}/" aria-label="{{ctx.Locale.Tr "home_title"}}">
<img width="30" height="30" src="https://mokoconsulting.tech/images/branding/logo.png" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
<img width="30" height="30" src="{{AssetUrlPrefix}}/img/logo-small.png" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
</a>
</div>
<div class="item">
+4
View File
@@ -11,6 +11,10 @@
width: 240px;
}
.flex-container-nav .ui.menu .item {
text-align: left;
}
/* wide sidebar on the right, used in frontpage */
.flex-container-sidebar {
width: 35%;