Compare commits

...

14 Commits

Author SHA1 Message Date
Jonathan Miller d541a07263 fix(ui): left-align admin sidebar — fix justify-content and text-align
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Changed details.toggleable-item summary from justify-content:
space-between to gap + margin-left: auto on the chevron. Added
.flex-container-nav .item rule to force left alignment on all
menu items including standalone links.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 23:29:52 -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
Jonathan Miller d97955394f feat(ci): auto-update updates.xml on production deploy
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 2s
After a successful production deployment, the deploy workflow now
automatically updates updates.xml on main with the new version,
release URL, and docker image tag for the stable channel.

Dev deployments skip this step — only production releases update
the stable channel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 19:49:04 -05:00
jmiller 592a71968f Merge pull request 'feat(ci): enable maintenance mode during deployments' (#177) from feat/deploy-maintenance-mode into dev
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 23s
2026-05-26 00:35:09 +00:00
Jonathan Miller d55b79a9ff feat(ci): enable maintenance mode during deployments
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
The deploy workflow now:
1. Enables maintenance mode before building (users see maintenance page)
2. Builds, pushes, and restarts the container
3. Disables maintenance mode after health check passes (if: always)

Uses Gitea's built-in maintenance mode via admin config API.
If the instance is already down, the enable step gracefully warns
instead of failing. The disable step runs even if deploy fails
to avoid leaving the instance in maintenance mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 19:33:59 -05:00
jmiller 64e1e37e20 Merge pull request 'fix: generate checksums on API asset upload' (#175) from feat/release-sha-checksums into dev
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 18s
2026-05-26 00:15:52 +00:00
Jonathan Miller a847129f9c fix: generate checksums on API asset upload, not just CreateRelease
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
The API endpoint POST /releases/{id}/assets bypasses CreateRelease
and UpdateRelease, so checksums were not generated for API uploads.
Added GenerateReleaseChecksums call after successful asset upload.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 19:15:12 -05:00
jmiller 30e16cccc1 Merge pull request 'feat: auto-generate SHA256 checksums for release attachments' (#174) from feat/release-sha-checksums into dev 2026-05-26 00:08:48 +00:00
jmiller b74cf800ef Merge pull request 'feat: update checker channels, email + ntfy notifications' (#173) from feat/update-checker-channels into dev 2026-05-26 00:08:09 +00:00
Jonathan Miller 90f612f211 feat: auto-generate SHA256 checksums for release attachments
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
When a release is created or updated with attachments, automatically
compute SHA256 checksums for every file and attach a checksums.sha256
manifest file. The manifest follows the standard sha256sum format:
  <hash>  <filename>

Existing checksums.sha256 files are replaced when attachments change.
Checksums are generated for both CreateRelease and UpdateRelease flows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 19:05:13 -05:00
14 changed files with 432 additions and 7 deletions
+104 -2
View File
@@ -8,7 +8,7 @@ on:
workflow_dispatch:
inputs:
version:
description: 'Version tag (e.g. v1.26.1-moko.04.00.00)'
description: 'Version tag (e.g. v1.26.1-moko.05.01.00)'
required: true
default: 'latest'
environment:
@@ -30,6 +30,7 @@ env:
DEPLOY_HOST: git.mokoconsulting.tech
DEPLOY_PORT: 2918
DEPLOY_USER: mokoconsulting
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
deploy:
@@ -47,15 +48,30 @@ jobs:
echo "source_dir=/opt/gitea/source" >> $GITHUB_OUTPUT
echo "branch=main" >> $GITHUB_OUTPUT
echo "tag=${VERSION}" >> $GITHUB_OUTPUT
echo "instance_url=https://git.mokoconsulting.tech" >> $GITHUB_OUTPUT
else
echo "compose_dir=/opt/gitea-dev" >> $GITHUB_OUTPUT
echo "container=mokogitea-dev" >> $GITHUB_OUTPUT
echo "source_dir=/opt/gitea-dev/source" >> $GITHUB_OUTPUT
echo "branch=dev" >> $GITHUB_OUTPUT
echo "tag=${VERSION}-dev" >> $GITHUB_OUTPUT
echo "instance_url=https://git.dev.mokoconsulting.tech" >> $GITHUB_OUTPUT
fi
- name: Build, push, and deploy via SSH
- name: Enable maintenance mode
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
run: |
echo "Enabling maintenance mode on ${INSTANCE_URL}..."
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/x-www-form-urlencoded" \
"${INSTANCE_URL}/-/admin/config" \
-d 'key=instance.maintenance_mode&value={"AdminWebAccessOnly":true}' \
|| echo "WARNING: Could not enable maintenance mode (instance may be down)"
- name: Build and deploy via SSH
env:
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
TAG: ${{ steps.config.outputs.tag }}
@@ -124,6 +140,92 @@ jobs:
exit 1
"
- name: Update updates.xml
if: success()
env:
GITEA_TOKEN: ${{ secrets.GA_TOKEN }}
TAG: ${{ steps.config.outputs.tag }}
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
DEPLOY_ENV: ${{ github.event.inputs.environment }}
run: |
# Only update updates.xml for production stable releases
if [ "$DEPLOY_ENV" != "production" ]; then
echo "Skipping updates.xml — dev deployments don't update stable channel"
exit 0
fi
# Extract moko version from tag (e.g. v1.26.1-moko.05.01.01 -> 05.01.01)
MOKO_VER=$(echo "$TAG" | sed -n 's/.*-moko\.\(.*\)/\1/p')
if [ -z "$MOKO_VER" ]; then
echo "Could not extract moko version from tag: $TAG"
exit 0
fi
RELEASE_URL="https://${REGISTRY}/MokoConsulting/MokoGitea/releases/tag/${TAG}"
DOCKER_IMG="${REGISTRY}/${IMAGE}:${TAG}"
python3 << PYEOF
import json, os, re, base64, urllib.request
token = os.environ["GITEA_TOKEN"]
registry = os.environ["REGISTRY"]
tag = os.environ["TAG"]
moko_ver = os.environ["MOKO_VER"]
release_url = os.environ["RELEASE_URL"]
docker_img = os.environ["DOCKER_IMG"]
api = f"https://{registry}/api/v1/repos/MokoConsulting/MokoGitea"
# Fetch current updates.xml
req = urllib.request.Request(f"{api}/contents/updates.xml?ref=main",
headers={"Authorization": f"token {token}"})
with urllib.request.urlopen(req) as resp:
data = json.loads(resp.read())
sha = data["sha"]
content = base64.b64decode(data["content"]).decode("utf-8")
# Update stable channel version, infourl, and docker tag
content = re.sub(
r"(<tags><tag>stable</tag></tags>[\s\S]*?<version>)[^<]*(</version>)",
rf"\g<1>{moko_ver}\2", content)
content = re.sub(
r"(<tags><tag>stable</tag></tags>[\s\S]*?<infourl[^>]*>)[^<]*(</infourl>)",
rf"\g<1>{release_url}\2", content)
content = re.sub(
r"(<tags><tag>stable</tag></tags>[\s\S]*?<downloadurl[^>]*>)[^<]*(</downloadurl>)",
rf"\g<1>{docker_img}\2", content)
# Also update VERSION comment at top
content = re.sub(r"VERSION: [^\n]*", f"VERSION: {moko_ver}", content)
# Push updated file
encoded = base64.b64encode(content.encode()).decode()
payload = json.dumps({
"message": f"chore(ci): update updates.xml to {moko_ver}",
"content": encoded,
"sha": sha,
"branch": "main",
}).encode()
req = urllib.request.Request(f"{api}/contents/updates.xml",
data=payload, method="PUT",
headers={"Authorization": f"token {token}", "Content-Type": "application/json"})
with urllib.request.urlopen(req) as resp:
print(f"updates.xml updated to {moko_ver}")
PYEOF
- name: Disable maintenance mode
if: always()
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
INSTANCE_URL: ${{ steps.config.outputs.instance_url }}
run: |
echo "Disabling maintenance mode on ${INSTANCE_URL}..."
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/x-www-form-urlencoded" \
"${INSTANCE_URL}/-/admin/config" \
-d 'key=instance.maintenance_mode&value={"AdminWebAccessOnly":false}' \
|| echo "WARNING: Could not disable maintenance mode"
- name: Verify
run: |
sleep 5
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

@@ -18,6 +18,7 @@ import (
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context/upload"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/convert"
release_service "git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/release"
)
func checkReleaseMatchRepo(ctx *context.APIContext, releaseID int64) bool {
@@ -263,6 +264,14 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
return
}
// Regenerate checksums after new attachment
rel, relErr := repo_model.GetReleaseByID(ctx, releaseID)
if relErr == nil {
if checksumErr := release_service.GenerateReleaseChecksums(ctx, rel); checksumErr != nil {
log.Error("GenerateReleaseChecksums after upload: %v", checksumErr)
}
}
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attach))
}
+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)
+82
View File
@@ -0,0 +1,82 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package release
import (
"bytes"
"context"
"crypto/sha256"
"fmt"
"io"
repo_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/storage"
attachment_service "git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/attachment"
)
// GenerateReleaseChecksums computes SHA256 checksums for all attachments
// on a release and adds a checksums.sha256 manifest file as an attachment.
func GenerateReleaseChecksums(ctx context.Context, rel *repo_model.Release) error {
// Load attachments into rel.Attachments
if err := repo_model.GetReleaseAttachments(ctx, rel); err != nil {
return fmt.Errorf("GetReleaseAttachments: %w", err)
}
if len(rel.Attachments) == 0 {
return nil
}
// Remove existing checksums file if present
for _, a := range rel.Attachments {
if a.Name == "checksums.sha256" {
if err := repo_model.DeleteAttachment(ctx, a, true); err != nil {
log.Warn("Failed to delete old checksums.sha256: %v", err)
}
break
}
}
// Compute SHA256 for each attachment
var manifest bytes.Buffer
for _, a := range rel.Attachments {
if a.Name == "checksums.sha256" {
continue
}
fr, err := storage.Attachments.Open(a.RelativePath())
if err != nil {
log.Warn("Cannot open attachment %s for checksumming: %v", a.Name, err)
continue
}
h := sha256.New()
if _, err := io.Copy(h, fr); err != nil {
fr.Close()
log.Warn("Cannot read attachment %s for checksumming: %v", a.Name, err)
continue
}
fr.Close()
fmt.Fprintf(&manifest, "%x %s\n", h.Sum(nil), a.Name)
}
if manifest.Len() == 0 {
return nil
}
// Create the checksums.sha256 attachment
checksumAttach := &repo_model.Attachment{
RepoID: rel.RepoID,
ReleaseID: rel.ID,
Name: "checksums.sha256",
}
if _, err := attachment_service.NewAttachment(ctx, checksumAttach, &manifest, int64(manifest.Len())); err != nil {
return fmt.Errorf("create checksums.sha256 attachment: %w", err)
}
log.Info("Generated checksums.sha256 for release %s (repo %d)", rel.TagName, rel.RepoID)
return nil
}
+14
View File
@@ -190,6 +190,13 @@ func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, attachmentU
return err
}
// Generate SHA256 checksums for all release attachments
if len(attachmentUUIDs) > 0 {
if err := GenerateReleaseChecksums(gitRepo.Ctx, rel); err != nil {
log.Error("GenerateReleaseChecksums for %s: %v", rel.TagName, err)
}
}
if !rel.IsDraft {
notify_service.NewRelease(gitRepo.Ctx, rel)
}
@@ -344,6 +351,13 @@ func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repo
}
}
// Regenerate checksums when attachments change
if len(addAttachmentUUIDs) > 0 || len(delAttachmentUUIDs) > 0 {
if err := GenerateReleaseChecksums(ctx, rel); err != nil {
log.Error("GenerateReleaseChecksums for %s: %v", rel.TagName, err)
}
}
if !rel.IsDraft {
if !isTagCreated && !isConvertedFromTag {
notify_service.UpdateRelease(gitRepo.Ctx, doer, rel)
+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">
+7 -1
View File
@@ -9,8 +9,8 @@ details.toggleable-item .menu {
details.toggleable-item summary {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.5em;
padding: 0.92857143em 1.14285714em;
}
@@ -20,6 +20,7 @@ details.toggleable-item summary::-webkit-details-marker /* Safari */ {
}
details.toggleable-item summary::after {
margin-left: auto;
transition: transform 0.25s ease;
content: "";
width: 14px;
@@ -35,3 +36,8 @@ details.toggleable-item summary::after {
details.toggleable-item[open] summary::after {
transform: rotate(90deg);
}
.flex-container-nav .ui.menu .item {
text-align: left !important;
justify-content: flex-start !important;
}