consolidate css/, badges into styles/ directory at project root
Rename css/ → styles/ and add coverage badge SVG. CI updates styles/coverage.svg in the repo via Gitea contents API so the README badge renders without external URL dependencies. All compose.yml mounts, .dockerignore, README, and docs refs updated.
@@ -19,6 +19,6 @@ trino/
|
|||||||
traefik/
|
traefik/
|
||||||
nginx/
|
nginx/
|
||||||
gitea/
|
gitea/
|
||||||
css/
|
styles/
|
||||||
.woodpecker/
|
.woodpecker/
|
||||||
.claude/
|
.claude/
|
||||||
|
|||||||
28
README.md
@@ -1,6 +1,6 @@
|
|||||||
# Homelab Stack
|
# Homelab Stack
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Self-hosted infrastructure stack with GPU support, S3-compatible storage, Git server with container registry, CI/CD, data lakehouse, observability, documentation, and research tools. All services share a unified retro "throwback" theme injected via Traefik middleware.
|
Self-hosted infrastructure stack with GPU support, S3-compatible storage, Git server with container registry, CI/CD, data lakehouse, observability, documentation, and research tools. All services share a unified retro "throwback" theme injected via Traefik middleware.
|
||||||
|
|
||||||
@@ -100,18 +100,18 @@ Per-service CSS files handle service-specific overrides:
|
|||||||
|
|
||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `css/inject.css` | Global throwback palette + logo hiding (served as `throwback.css`) |
|
| `styles/inject.css` | Global throwback palette + logo hiding (served as `throwback.css`) |
|
||||||
| `css/dashboard.css` | Dashboard tile grid layout |
|
| `styles/dashboard.css` | Dashboard tile grid layout |
|
||||||
| `css/gitea.css` | Gitea theme (registered as `throwback` in Gitea UI settings) |
|
| `styles/gitea.css` | Gitea theme (registered as `throwback` in Gitea UI settings) |
|
||||||
| `css/woodpecker.css` | Woodpecker custom CSS (via `WOODPECKER_CUSTOM_CSS_FILE`) |
|
| `styles/woodpecker.css` | Woodpecker custom CSS (via `WOODPECKER_CUSTOM_CSS_FILE`) |
|
||||||
| `css/marimo.css` | Marimo notebook theme |
|
| `styles/marimo.css` | Marimo notebook theme |
|
||||||
| `css/fav32.png` | 32x32 PNG favicon (canonical, injected by Traefik) |
|
| `styles/fav32.png` | 32x32 PNG favicon (canonical, injected by Traefik) |
|
||||||
| `css/favicon.png` | 64x64 PNG favicon |
|
| `styles/favicon.png` | 64x64 PNG favicon |
|
||||||
| `css/fav16.png` | 16x16 PNG favicon |
|
| `styles/fav16.png` | 16x16 PNG favicon |
|
||||||
| `css/apple-touch-icon.png` | 180x180 Apple touch icon |
|
| `styles/apple-touch-icon.png` | 180x180 Apple touch icon |
|
||||||
| `css/favicon.svg` | SVG favicon (legacy) |
|
| `styles/favicon.svg` | SVG favicon (legacy) |
|
||||||
| `css/logo.svg` | Navbar logo for Gitea |
|
| `styles/logo.svg` | Navbar logo for Gitea |
|
||||||
| `css/logo.png` | 256x256 PNG logo |
|
| `styles/logo.png` | 256x256 PNG logo |
|
||||||
|
|
||||||
Grafana favicons are replaced via volume mounts over the default Grafana icons.
|
Grafana favicons are replaced via volume mounts over the default Grafana icons.
|
||||||
|
|
||||||
@@ -467,7 +467,7 @@ stack/
|
|||||||
├── compose.yml # Docker Compose (all services)
|
├── compose.yml # Docker Compose (all services)
|
||||||
├── .env # Environment variables (not in git)
|
├── .env # Environment variables (not in git)
|
||||||
├── README.md
|
├── README.md
|
||||||
├── css/ # Centralized theme assets
|
├── styles/ # Centralized theme assets
|
||||||
│ ├── inject.css # Global throwback CSS (served as throwback.css)
|
│ ├── inject.css # Global throwback CSS (served as throwback.css)
|
||||||
│ ├── dashboard.css # Dashboard tile grid
|
│ ├── dashboard.css # Dashboard tile grid
|
||||||
│ ├── gitea.css # Gitea throwback theme
|
│ ├── gitea.css # Gitea throwback theme
|
||||||
|
|||||||
18
compose.yml
@@ -116,7 +116,7 @@ services:
|
|||||||
- gitea_data:/var/lib/gitea
|
- gitea_data:/var/lib/gitea
|
||||||
- gitea_config:/etc/gitea
|
- gitea_config:/etc/gitea
|
||||||
- ./gitea/custom:/var/lib/gitea/custom
|
- ./gitea/custom:/var/lib/gitea/custom
|
||||||
- ./css/gitea.css:/var/lib/gitea/custom/public/assets/css/theme-throwback.css:ro
|
- ./styles/gitea.css:/var/lib/gitea/custom/public/assets/css/theme-throwback.css:ro
|
||||||
ports:
|
ports:
|
||||||
- "2222:2222"
|
- "2222:2222"
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -149,7 +149,7 @@ services:
|
|||||||
- WOODPECKER_CUSTOM_CSS_FILE=/etc/woodpecker/custom.css
|
- WOODPECKER_CUSTOM_CSS_FILE=/etc/woodpecker/custom.css
|
||||||
volumes:
|
volumes:
|
||||||
- woodpecker_data:/var/lib/woodpecker
|
- woodpecker_data:/var/lib/woodpecker
|
||||||
- ./css/woodpecker.css:/etc/woodpecker/custom.css:ro
|
- ./styles/woodpecker.css:/etc/woodpecker/custom.css:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
- gitea
|
- gitea
|
||||||
- postgres
|
- postgres
|
||||||
@@ -185,7 +185,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./notebooks:/home/kert/notebooks
|
- ./notebooks:/home/kert/notebooks
|
||||||
- ./notebooks/.marimo-config:/home/kert/.config/marimo
|
- ./notebooks/.marimo-config:/home/kert/.config/marimo
|
||||||
- ./css/marimo.css:/home/kert/.config/marimo/retro-arcade.css:ro
|
- ./styles/marimo.css:/home/kert/.config/marimo/retro-arcade.css:ro
|
||||||
- ./notebooks/home-page-DUMF8ZY4.js:/home/kert/workspace/.venv/lib/python3.13/site-packages/marimo/_static/assets/home-page-DUMF8ZY4.js:ro
|
- ./notebooks/home-page-DUMF8ZY4.js:/home/kert/workspace/.venv/lib/python3.13/site-packages/marimo/_static/assets/home-page-DUMF8ZY4.js:ro
|
||||||
- ./data:/home/kert/data
|
- ./data:/home/kert/data
|
||||||
- ./zotero/data:/home/kert/zotero:ro
|
- ./zotero/data:/home/kert/zotero:ro
|
||||||
@@ -334,9 +334,9 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
- ./nginx/index.html:/usr/share/nginx/html/index.html:ro
|
- ./nginx/index.html:/usr/share/nginx/html/index.html:ro
|
||||||
- ./css/dashboard.css:/usr/share/nginx/html/dashboard.css:ro
|
- ./styles/dashboard.css:/usr/share/nginx/html/dashboard.css:ro
|
||||||
- ./css/inject.css:/usr/share/nginx/html/throwback.css:ro
|
- ./styles/inject.css:/usr/share/nginx/html/throwback.css:ro
|
||||||
- ./css/fav32.png:/usr/share/nginx/html/fav32.png:ro
|
- ./styles/fav32.png:/usr/share/nginx/html/fav32.png:ro
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -452,9 +452,9 @@ services:
|
|||||||
- ./grafana/provisioning:/etc/grafana/provisioning:ro
|
- ./grafana/provisioning:/etc/grafana/provisioning:ro
|
||||||
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
|
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
|
||||||
- grafana_data:/var/lib/grafana
|
- grafana_data:/var/lib/grafana
|
||||||
- ./css/favicon.svg:/usr/share/grafana/public/img/grafana_icon.svg:ro
|
- ./styles/favicon.svg:/usr/share/grafana/public/img/grafana_icon.svg:ro
|
||||||
- ./css/fav32.png:/usr/share/grafana/public/img/fav32.png:ro
|
- ./styles/fav32.png:/usr/share/grafana/public/img/fav32.png:ro
|
||||||
- ./css/apple-touch-icon.png:/usr/share/grafana/public/img/apple-touch-icon.png:ro
|
- ./styles/apple-touch-icon.png:/usr/share/grafana/public/img/apple-touch-icon.png:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
- loki
|
- loki
|
||||||
- jaeger
|
- jaeger
|
||||||
|
|||||||
@@ -7,14 +7,19 @@ Usage:
|
|||||||
# Post coverage as a Gitea commit status (reads GITEA_TOKEN, GITEA_URL,
|
# Post coverage as a Gitea commit status (reads GITEA_TOKEN, GITEA_URL,
|
||||||
# CI_COMMIT_SHA from environment):
|
# CI_COMMIT_SHA from environment):
|
||||||
uv run python dev/scripts/coverage_badge.py --post pytest.out
|
uv run python dev/scripts/coverage_badge.py --post pytest.out
|
||||||
|
|
||||||
|
# Update styles/coverage.svg in the repo via Gitea API:
|
||||||
|
uv run python dev/scripts/coverage_badge.py --upload coverage.svg
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
TEMPLATE = """\
|
TEMPLATE = """\
|
||||||
@@ -39,6 +44,8 @@ TEMPLATE = """\
|
|||||||
</g>
|
</g>
|
||||||
</svg>"""
|
</svg>"""
|
||||||
|
|
||||||
|
FILE_PATH = "styles/coverage.svg"
|
||||||
|
|
||||||
|
|
||||||
def color_for(pct: int) -> str:
|
def color_for(pct: int) -> str:
|
||||||
if pct >= 95:
|
if pct >= 95:
|
||||||
@@ -85,30 +92,40 @@ def post_status(pct: int) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def upload_badge(svg_path: str) -> None:
|
def upload_badge(svg_path: str) -> None:
|
||||||
|
"""Update styles/coverage.svg in the repo via Gitea contents API."""
|
||||||
token, url = _gitea_env()
|
token, url = _gitea_env()
|
||||||
endpoint = f"{url}/api/packages/homelab/generic/coverage/latest/badge.svg"
|
endpoint = f"{url}/api/v1/repos/homelab/stack/contents/{FILE_PATH}"
|
||||||
data = open(svg_path, "rb").read()
|
svg_data = open(svg_path, "rb").read()
|
||||||
# Delete existing package version first (ignore 404)
|
content_b64 = base64.b64encode(svg_data).decode()
|
||||||
|
|
||||||
|
# Get current file SHA (needed for update)
|
||||||
|
sha = None
|
||||||
try:
|
try:
|
||||||
req = urllib.request.Request(
|
|
||||||
f"{url}/api/packages/homelab/generic/coverage/latest",
|
|
||||||
headers={"Authorization": f"token {token}"},
|
|
||||||
method="DELETE",
|
|
||||||
)
|
|
||||||
urllib.request.urlopen(req)
|
|
||||||
except urllib.error.HTTPError:
|
|
||||||
pass
|
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
endpoint,
|
endpoint,
|
||||||
data=data,
|
headers={"Authorization": f"token {token}"},
|
||||||
headers={
|
|
||||||
"Authorization": f"token {token}",
|
|
||||||
"Content-Type": "image/svg+xml",
|
|
||||||
},
|
|
||||||
method="PUT",
|
|
||||||
)
|
)
|
||||||
with urllib.request.urlopen(req) as resp:
|
with urllib.request.urlopen(req) as resp:
|
||||||
print(f"Uploaded badge SVG (HTTP {resp.status})")
|
sha = json.loads(resp.read())["sha"]
|
||||||
|
except urllib.error.HTTPError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
body = {"content": content_b64, "message": "update coverage badge [skip ci]"}
|
||||||
|
if sha:
|
||||||
|
body["sha"] = sha
|
||||||
|
|
||||||
|
method = "PUT" if sha else "POST"
|
||||||
|
req = urllib.request.Request(
|
||||||
|
endpoint,
|
||||||
|
data=json.dumps(body).encode(),
|
||||||
|
headers={
|
||||||
|
"Authorization": f"token {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
method=method,
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req) as resp:
|
||||||
|
print(f"Updated {FILE_PATH} in repo (HTTP {resp.status})")
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* Throwback palette — matches css/inject.css */
|
/* Throwback palette — matches styles/inject.css */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--ifm-color-primary: #3366cc;
|
--ifm-color-primary: #3366cc;
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
20
styles/coverage.svg
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="116" height="20">
|
||||||
|
<linearGradient id="b" x2="0" y2="100%">
|
||||||
|
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||||
|
<stop offset="1" stop-opacity=".1"/>
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath id="a">
|
||||||
|
<rect width="116" height="20" rx="3" fill="#fff"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#a)">
|
||||||
|
<path fill="#555" d="M0 0h65v20H0z"/>
|
||||||
|
<path fill="#4c1" d="M65 0h51v20H65z"/>
|
||||||
|
<path fill="url(#b)" d="M0 0h116v20H0z"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
|
||||||
|
<text x="32.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
|
||||||
|
<text x="32.5" y="14">coverage</text>
|
||||||
|
<text x="90" y="15" fill="#010101" fill-opacity=".3">99%</text>
|
||||||
|
<text x="90" y="14">99%</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 822 B |
|
Before Width: | Height: | Size: 741 B After Width: | Height: | Size: 741 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |