G2.3: Windy Git branding — and get it out of one host's disk into the repo
All checks were successful
check / gate (push) Successful in 19s
canary / probe (push) Successful in 8s

The front end was 100% stock Gitea: green teacup, "Gitea: Git with a cup of
tea", "A painless, self-hosted Git service". G2.3 was specified in the plan with
an acceptance test and never executed, and nothing enforced it.

Now: Windy Git name, wind-mark logo, brand-blue accent, and a landing page that
says what this actually is. Uses Gitea's SUPPORTED surface (custom templates +
public assets) so upstream upgrades keep arriving — no source modified (D-2/I-1).

Two traps this cost, both now documented and tested:

1. GITEA__DEFAULT__APP_NAME does not work. Gitea reads APP_NAME from the TOP
   LEVEL of app.ini; the env var created a literal [default] section that Gitea
   ignores, so the installer's stock APP_NAME kept winning while the config
   looked correct. The env-to-ini pass also APPENDED a second APP_NAME rather
   than replacing the first — a new variant of the documented G4A.3 trap.

2. Cloudflare caches /assets/* for 6h and no token in this stack can purge, so
   the new logo and CSS were invisible while being correct at origin. Brand
   assets now carry a VERSION IN THE FILENAME; bump it on every change.

Committed with an idempotent apply.sh, because applying it straight to Veron's
disk first was itself the config-drift trap this project documents: a rebuild
would have silently reverted to stock Gitea.

85 tests green.

Co-Authored-By: Claude (Fable 5) <noreply@anthropic.com>
This commit is contained in:
Grant Whitmer
2026-08-14 09:08:48 -04:00
parent e7dee39151
commit eae1bff50b
8 changed files with 153 additions and 0 deletions

28
deploy/branding/README.md Normal file
View File

@@ -0,0 +1,28 @@
# Windy Git branding (G2.3)
Gitea's **supported** customisation surface: custom templates and public assets.
No Gitea source is modified, so upstream upgrades keep arriving (D-2, I-1).
deploy/branding/ → $GITEA_CUSTOM (/data/gitea) in the container
→ /srv/windygit/git/gitea/ on Veron 1
Apply with `./deploy/branding/apply.sh`.
## Two traps this cost, both worth knowing
**1. `GITEA__DEFAULT__APP_NAME` does not work.** Gitea reads `APP_NAME` from the
*top level* of `app.ini` (before any `[section]`). The env var instead created a
literal `[default]` section, which Gitea ignores — and the installer's stock
`APP_NAME` kept winning, so the site said "Gitea: Git with a cup of tea" while
the config looked correct. Worse, the env-to-ini pass **appended** a second
`APP_NAME` rather than replacing the first. `apply.sh` sets it at the top level
directly.
**2. Cloudflare caches `/assets/*` for 6 hours and no token in this stack can
purge.** Editing a fixed filename leaves the old bytes live for hours — the new
logo and CSS were both invisible while being correct at origin. **Version the
filename** (`theme-windy.v2.css`) on every brand change; a `?query` is not
enough because some caches ignore it.
The nav logo is swapped in CSS rather than by overriding Gitea's navbar
template — a one-line rule instead of a forked template that would drift.

23
deploy/branding/apply.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Apply Windy Git branding to the Gitea custom tree. Idempotent.
set -euo pipefail
CUSTOM="${GITEA_CUSTOM_HOST:-/srv/windygit/git/gitea}"
HERE="$(cd "$(dirname "$0")" && pwd)"
sudo mkdir -p "$CUSTOM"/templates/custom "$CUSTOM"/public/assets/img "$CUSTOM"/public/assets/css
sudo cp -r "$HERE"/templates/. "$CUSTOM"/templates/
sudo cp -r "$HERE"/public/. "$CUSTOM"/public/
sudo chown -R 1000:1000 "$CUSTOM"/templates "$CUSTOM"/public
# APP_NAME must sit at the TOP LEVEL. See README trap #1.
INI="$CUSTOM/conf/app.ini"
sudo cp "$INI" "$INI.bak-brand-$(date +%s)"
sudo python3 - "$INI" <<'PY'
import sys
p = sys.argv[1]
lines = [l for l in open(p).read().splitlines()
if not l.strip().startswith(("APP_NAME", "APP_SLOGAN"))]
lines.insert(0, "APP_NAME = Windy Git")
open(p, "w").write("\n".join(lines) + "\n")
PY
echo "applied. restart gitea to pick it up."

View File

@@ -0,0 +1,27 @@
/* Windy Git brand accent. Layered on top of Gitea's theme rather than
replacing it, so upstream theme fixes keep arriving. */
:root {
--color-primary: #0ea5e9;
--color-primary-dark-1: #0284c7;
--color-primary-dark-2: #0369a1;
--color-primary-light-1: #38bdf8;
--color-primary-light-2: #7dd3fc;
}
.wg-hero { max-width: 780px; margin: 4rem auto 2rem; padding: 0 1.5rem; text-align: center; }
.wg-hero h1 { font-size: 2.6rem; margin: 1.2rem 0 .4rem; letter-spacing: -.02em; }
.wg-hero .wg-sub { font-size: 1.15rem; opacity: .78; margin-bottom: 2.2rem; }
.wg-grid { display: grid; gap: 1.1rem; grid-template-columns: repeat(auto-fit,minmax(230px,1fr));
max-width: 900px; margin: 0 auto 2.5rem; padding: 0 1.5rem; text-align: left; }
.wg-card { border: 1px solid var(--color-secondary); border-radius: 8px; padding: 1.1rem 1.2rem; }
.wg-card h3 { margin: 0 0 .35rem; font-size: 1.02rem; }
.wg-card p { margin: 0; opacity: .74; font-size: .9rem; line-height: 1.5; }
.wg-cta { margin-bottom: 3rem; }
/* Logo swap via CSS.
Cloudflare cached the stock /assets/img/logo.svg for 6h and no available API
token can purge. Pointing at a NEW filename sidesteps the stale object
without touching Gitea's own templates — the supported customisation surface,
per D-2 (membrane, not merge). */
img[src$="/assets/img/logo.svg"] {
content: url("/assets/img/wg-mark.svg");
}

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
<defs><linearGradient id="w" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#38bdf8"/><stop offset="100%" stop-color="#0ea5e9"/>
</linearGradient></defs>
<circle cx="16" cy="16" r="15" fill="#0b1220"/>
<path d="M5 11h13a3.2 3.2 0 1 0-3.1-4" fill="none" stroke="url(#w)" stroke-width="2.4" stroke-linecap="round"/>
<path d="M5 16h17a3.6 3.6 0 1 1-3.5 4.5" fill="none" stroke="url(#w)" stroke-width="2.4" stroke-linecap="round"/>
<path d="M5 21h9a2.8 2.8 0 1 1-2.7 3.5" fill="none" stroke="url(#w)" stroke-width="2.4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 660 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
<defs><linearGradient id="w" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#38bdf8"/><stop offset="100%" stop-color="#0ea5e9"/>
</linearGradient></defs>
<circle cx="16" cy="16" r="15" fill="#0b1220"/>
<path d="M5 11h13a3.2 3.2 0 1 0-3.1-4" fill="none" stroke="url(#w)" stroke-width="2.4" stroke-linecap="round"/>
<path d="M5 16h17a3.6 3.6 0 1 1-3.5 4.5" fill="none" stroke="url(#w)" stroke-width="2.4" stroke-linecap="round"/>
<path d="M5 21h9a2.8 2.8 0 1 1-2.7 3.5" fill="none" stroke="url(#w)" stroke-width="2.4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 660 B

View File

@@ -0,0 +1,5 @@
{{/* Windy Git brand layer.
The filename carries a version: Cloudflare caches /assets/* for 6h with no
purge token available to this stack, so editing a fixed filename leaves the
old bytes live for hours. Bump the suffix on every brand change. */}}
<link rel="stylesheet" href="{{AssetUrlPrefix}}/css/theme-windy.v2.css">

View File

@@ -0,0 +1,27 @@
{{template "base/head" .}}
<div role="main" aria-label="{{ctx.Locale.Tr "home"}}" class="page-content home">
<div class="wg-hero">
<img src="{{AssetUrlPrefix}}/img/logo.svg" alt="Windy Git" width="72" height="72">
<h1>Windy Git</h1>
<p class="wg-sub">Your work, every version — and agents as citizens.</p>
<div class="wg-cta">
{{if not .IsSigned}}
<a class="ui primary button" href="{{AppSubUrl}}/user/login">Sign in with Windy</a>
{{else}}
<a class="ui primary button" href="{{AppSubUrl}}/{{.SignedUser.Name}}?tab=repositories">Your repositories</a>
{{end}}
<a class="ui button" href="{{AppSubUrl}}/explore/repos">Explore</a>
</div>
</div>
<div class="wg-grid">
<div class="wg-card"><h3>Code and models, one host</h3>
<p>Git and LFS over Windy Cloud storage. Source, weights and adapters live side by side.</p></div>
<div class="wg-card"><h3>Agents are first-class</h3>
<p>An agent signs in with its own Eternitas passport — not a human's borrowed token — and its work is attributable to it.</p></div>
<div class="wg-card"><h3>Your account, no second password</h3>
<p>Sign in with the Windy account you already have. Nothing new to remember.</p></div>
<div class="wg-card"><h3>Kept, and kept elsewhere</h3>
<p>Every repository is bundled nightly to off-site storage, and the restore is rehearsed rather than assumed.</p></div>
</div>
</div>
{{template "base/footer" .}}