Files
jmiller 81d065b9bb
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Successful in 9s
Generic: Project CI / Lint & Validate (pull_request) Successful in 32s
Generic: Standards Compliance / Secret Scanning (push) Failing after 9s
Universal: PR Check / Secret Scan (pull_request) Successful in 51s
Generic: Standards Compliance / License Header Validation (push) Successful in 9s
Generic: Standards Compliance / Repository Structure Validation (push) Successful in 8s
Generic: Standards Compliance / Coding Standards Check (push) Successful in 10s
Generic: Standards Compliance / Workflow Configuration Check (push) Failing after 9s
Generic: Standards Compliance / Documentation Quality Check (push) Successful in 8s
Generic: Standards Compliance / README Completeness Check (push) Failing after 7s
Generic: Standards Compliance / Version Consistency Check (push) Successful in 1m2s
Generic: Standards Compliance / Script Integrity Validation (push) Successful in 26s
Generic: Standards Compliance / Git Repository Hygiene (push) Successful in 1m5s
Generic: Standards Compliance / File Naming Standards (push) Successful in 10s
Generic: Standards Compliance / Line Length Check (push) Successful in 23s
Generic: Standards Compliance / Insecure Code Pattern Detection (push) Successful in 10s
Generic: Standards Compliance / Code Complexity Analysis (push) Successful in 1m2s
Generic: Standards Compliance / Code Duplication Detection (push) Successful in 1m8s
Generic: Standards Compliance / Dead Code Detection (push) Successful in 16s
Generic: Standards Compliance / File Size Limits (push) Successful in 16s
Generic: Standards Compliance / TODO/FIXME Tracking (push) Successful in 14s
Generic: Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 1m3s
Deploy (Prod) / Build & Deploy to Prod (push) Successful in 5m9s
Generic: Standards Compliance / Broken Link Detection (push) Successful in 12s
Generic: Standards Compliance / API Documentation Coverage (push) Successful in 7s
Generic: Standards Compliance / Accessibility Check (push) Successful in 7s
Generic: Standards Compliance / Performance Metrics (push) Successful in 8s
Generic: Standards Compliance / Binary File Detection (push) Successful in 2m12s
Generic: Standards Compliance / Unused Dependencies Check (push) Successful in 55s
Generic: Standards Compliance / Terraform Configuration Validation (push) Successful in 12s
Generic: Standards Compliance / Enterprise Readiness Check (push) Successful in 55s
Generic: Standards Compliance / Repository Health Check (push) Successful in 54s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Standards Compliance / Compliance Summary (push) Has been cancelled
feat: MokoGIT branding -- mokoconsulting.tech reskin, Font Awesome, toggle, a11y menu, PWA
Comprehensive MokoGIT theme/branding built from the authoritative mokoconsulting.tech
design tokens (MokoOnyx *.custom.css):

- Theme: replace gitea themes entirely with theme-mokogit-{light,dark,auto} (default
  mokogit-auto); brand tokens (primary #010156 / accent #3f8ff0, navy nav+footer #112855,
  semantic palette); mokogit-brand.css component reskin; runtime custom-stylesheet override
  hook (templates/custom/header.tmpl -> mokogit-custom.css, re-skin live with no rebuild).
- Font Awesome 7 Free 7.1.0: vendored unmodified (CSS headers intact + webfonts + verbatim
  LICENSE.txt), license-compliant for distribution/white-label (Icons CC BY 4.0, Fonts SIL
  OFL 1.1, Code MIT); attributed in THIRD-PARTY-NOTICES.md, README, and wiki.
- Light/dark toggle: navbar sun/moon control, server-persisted via the existing
  /user/settings/appearance/theme endpoint (anonymous falls back to localStorage).
- Accessibility menu: 6 options (text resize, color invert, high contrast, link highlight,
  readable font, pause animations); localStorage-persisted, applied on <html>, ARIA/keyboard.
- PWA: service worker (network-first navigation + cached offline fallback) + branded offline
  page + enhanced manifest (display standalone, navy theme_color, app shortcuts); manifest
  icon sourced from the site-admin branding icon if set, else the bundled MokoGIT logo.

Verified: `go build ./...` and `vite build` both green; only mokogit themes emit.
2026-07-16 14:31:52 -05:00

209 lines
8.8 KiB
Go

// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package misc
import (
"net/http"
"path"
"strconv"
"strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/git"
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/httpcache"
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/httplib"
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/json"
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/setting"
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/util"
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/web/middleware"
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/services/context"
)
func SiteManifest(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/manifest+json")
if httpcache.HandleGenericETagPublicCache(req, w, "", &setting.AppStartTime) {
return
}
if req.Method == http.MethodHead {
return
}
ctx := req.Context()
absoluteAssetURL := strings.TrimSuffix(httplib.MakeAbsoluteURL(ctx, setting.StaticURLPrefix), "/")
appURL := strings.TrimSuffix(httplib.GuessCurrentAppURL(ctx), "/")
// Icons resolve to admin-uploaded branding (nav-icon writes logo.png, favicon
// upload writes favicon.*) when set, otherwise to the bundled MokoGIT branded
// defaults at the same /assets/img/ paths. See routers/web/admin/branding.go.
manifest := map[string]any{
"name": setting.AppName,
"short_name": setting.AppName,
"description": setting.AppName + " — self-hosted Git service",
"id": appURL + "/",
"start_url": appURL + "/",
"scope": appURL + "/",
"display": "standalone",
"display_override": []string{"standalone", "minimal-ui", "browser"},
"orientation": "any",
"theme_color": "#112855", // MokoGIT signature navy
"background_color": "#0e1318",
"categories": []string{"developer", "productivity"},
"icons": []map[string]string{
{"src": absoluteAssetURL + "/assets/img/favicon.svg", "type": "image/svg+xml", "sizes": "any", "purpose": "any"},
{"src": absoluteAssetURL + "/assets/img/favicon.png", "type": "image/png", "sizes": "256x256", "purpose": "any"},
{"src": absoluteAssetURL + "/assets/img/logo-small.png", "type": "image/png", "sizes": "512x512", "purpose": "any"},
{"src": absoluteAssetURL + "/assets/img/logo-small.png", "type": "image/png", "sizes": "512x512", "purpose": "maskable"},
{"src": absoluteAssetURL + "/assets/img/logo.png", "type": "image/png", "sizes": "512x512", "purpose": "any"},
},
"shortcuts": []map[string]any{
{"name": "Dashboard", "url": appURL + "/", "icons": []map[string]string{{"src": absoluteAssetURL + "/assets/img/logo-small.png", "sizes": "512x512"}}},
{"name": "Issues", "url": appURL + "/issues"},
{"name": "Pull Requests", "url": appURL + "/pulls"},
{"name": "Explore", "url": appURL + "/explore/repos"},
},
}
_ = json.NewEncoder(w).Encode(manifest)
}
// serviceWorkerJS is the MokoGIT PWA service worker. Tokens are replaced per
// request so the scope and offline/asset URLs honor AppSubURL. Strategy:
// - navigation requests: network-first, fall back to the cached offline page
// - same-origin static assets under the asset prefix: cache-first
// - everything else: passthrough
const serviceWorkerJS = `
const CACHE = '__CACHE__';
const OFFLINE_URL = '__OFFLINE__';
const ASSET_PREFIX = '__ASSETS__';
self.addEventListener('install', (e) => {
e.waitUntil(caches.open(CACHE).then((c) => c.add(new Request(OFFLINE_URL, {cache: 'reload'}))).then(() => self.skipWaiting()));
});
self.addEventListener('activate', (e) => {
e.waitUntil(caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))).then(() => self.clients.claim()));
});
self.addEventListener('fetch', (e) => {
const req = e.request;
if (req.method !== 'GET') return;
const url = new URL(req.url);
if (url.origin !== self.location.origin) return;
if (req.mode === 'navigate') {
e.respondWith(fetch(req).catch(() => caches.match(OFFLINE_URL)));
return;
}
if (ASSET_PREFIX && url.pathname.startsWith(ASSET_PREFIX)) {
e.respondWith(caches.match(req).then((cached) => cached || fetch(req).then((resp) => {
if (resp && resp.ok && resp.type === 'basic') { const clone = resp.clone(); caches.open(CACHE).then((c) => c.put(req, clone)); }
return resp;
}).catch(() => cached)));
}
});
`
// ServiceWorker serves the PWA service worker script with a root-scoped
// Service-Worker-Allowed header so it can control navigations under AppSubURL.
func ServiceWorker(w http.ResponseWriter, req *http.Request) {
scope := setting.AppSubURL + "/"
assetPrefix := strings.TrimSuffix(setting.StaticURLPrefix, "/") + "/assets/"
js := serviceWorkerJS
js = strings.ReplaceAll(js, "__CACHE__", "mokogit-pwa-"+setting.AppStartTime.Format("20060102150405"))
js = strings.ReplaceAll(js, "__OFFLINE__", setting.AppSubURL+"/-/offline")
js = strings.ReplaceAll(js, "__ASSETS__", assetPrefix)
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
w.Header().Set("Service-Worker-Allowed", scope)
if httpcache.HandleGenericETagPublicCache(req, w, "", &setting.AppStartTime) {
return
}
if req.Method == http.MethodHead {
return
}
_, _ = w.Write([]byte(js))
}
// Offline serves a self-contained, branded offline fallback page. It is
// precached by the service worker and shown when a navigation fails offline.
func Offline(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if req.Method == http.MethodHead {
return
}
page := strings.ReplaceAll(offlineHTML, "__NAME__", setting.AppName)
page = strings.ReplaceAll(page, "__ICON__", strings.TrimSuffix(setting.StaticURLPrefix, "/")+"/assets/img/logo.png")
_, _ = w.Write([]byte(page))
}
// offlineHTML is a dependency-light offline page (inline styles, MokoGIT navy).
const offlineHTML = `<!DOCTYPE html>
<html lang="en" data-theme="mokogit-auto">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Offline &middot; __NAME__</title>
<style>
:root { color-scheme: light dark; }
html,body { height:100%; margin:0; }
body { display:flex; align-items:center; justify-content:center; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif; background:#0e1318; color:#e6ebf1; text-align:center; padding:2rem; }
.card { max-width:28rem; }
img { width:72px; height:72px; object-fit:contain; margin-bottom:1.25rem; }
h1 { font-size:1.5rem; margin:0 0 .5rem; color:#f1f5f9; }
p { color:#c7ccd4; line-height:1.5; margin:0 0 1.5rem; }
button { background:#010156; color:#fff; border:0; border-radius:.25rem; padding:.6rem 1.25rem; font-size:1rem; cursor:pointer; }
button:hover { background:#010149; }
.bar { height:4px; background:#112855; position:fixed; top:0; left:0; right:0; }
</style>
</head>
<body>
<div class="bar"></div>
<div class="card">
<img src="__ICON__" alt="" onerror="this.style.display='none'">
<h1>You're offline</h1>
<p>__NAME__ can't be reached right now. Check your connection and try again &mdash; recently visited pages may still be available.</p>
<button onclick="location.reload()">Retry</button>
</div>
</body>
</html>`
func SSHInfo(rw http.ResponseWriter, req *http.Request) {
if !git.DefaultFeatures().SupportProcReceive {
rw.WriteHeader(http.StatusNotFound)
return
}
rw.Header().Set("content-type", "text/json;charset=UTF-8")
_, err := rw.Write([]byte(`{"type":"agit","version":1}`))
if err != nil {
log.Error("fail to write result: err: %v", err)
rw.WriteHeader(http.StatusInternalServerError)
return
}
rw.WriteHeader(http.StatusOK)
}
func DummyOK(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusOK)
}
func RobotsTxt(w http.ResponseWriter, req *http.Request) {
robotsTxt := util.FilePathJoinAbs(setting.CustomPath, "public/robots.txt")
if ok, _ := util.IsExist(robotsTxt); !ok {
robotsTxt = util.FilePathJoinAbs(setting.CustomPath, "robots.txt") // the legacy "robots.txt"
}
httpcache.SetCacheControlInHeader(w.Header(), httpcache.CacheControlForPublicStatic())
http.ServeFile(w, req, robotsTxt)
}
func StaticRedirect(target string) func(w http.ResponseWriter, req *http.Request) {
return func(w http.ResponseWriter, req *http.Request) {
http.Redirect(w, req, path.Join(setting.StaticURLPrefix, target), http.StatusMovedPermanently)
}
}
func LocationRedirect(target string) func(w http.ResponseWriter, req *http.Request) {
return func(w http.ResponseWriter, req *http.Request) {
http.Redirect(w, req, target, http.StatusSeeOther)
}
}
func WebBannerDismiss(ctx *context.Context) {
_, rev, _ := setting.Config().Instance.WebBanner.ValueRevision(ctx)
middleware.SetSiteCookie(ctx.Resp, middleware.CookieWebBannerDismissed, strconv.Itoa(rev), 48*3600)
ctx.JSONOK()
}