Compare commits

...

2 Commits

Author SHA1 Message Date
soraefir 4218bb8344 fix 2026-06-07 14:43:00 +02:00
soraefir 6b2fd299e0 colors 2026-06-07 14:40:46 +02:00
2 changed files with 87 additions and 32 deletions
+57 -5
View File
@@ -2,6 +2,7 @@
let let
serverCfg = config.syscfg.server; serverCfg = config.syscfg.server;
mediaCfg = config.syscfg.media; mediaCfg = config.syscfg.media;
palette = serverCfg.colorScheme.palette or { };
port = 8080; port = 8080;
assetSize = 64; assetSize = 64;
cacheControl = containerCfg.extra.cacheControl or "public, max-age=86400"; cacheControl = containerCfg.extra.cacheControl or "public, max-age=86400";
@@ -9,10 +10,32 @@ let
logoSvgFileName = builtins.baseNameOf (toString mediaCfg.logo.svg); logoSvgFileName = builtins.baseNameOf (toString mediaCfg.logo.svg);
logoSvgMount = "/assets/${logoSvgFileName}"; logoSvgMount = "/assets/${logoSvgFileName}";
borderRadius = toString (containerCfg.extra.borderRadius or 32); borderRadius = toString (containerCfg.extra.borderRadius or 32);
resolveColor = value:
if value == null then null
else if !builtins.isString value then
throw "favicon color values must be strings"
else if lib.hasPrefix "#" value then
value
else
lib.attrByPath [ value ] (throw "Unknown favicon color reference `${value}`") palette;
normalizeProfile = profile:
let
bg =
if profile ? bg then resolveColor profile.bg
else if profile ? background then resolveColor profile.background
else null;
fg =
if profile ? fg then resolveColor profile.fg
else if profile ? foreground then resolveColor profile.foreground
else null;
in
(lib.filterAttrs (name: _: !(builtins.elem name [ "bg" "background" "fg" "foreground" ])) profile)
// lib.optionalAttrs (bg != null) { bg = bg; }
// lib.optionalAttrs (fg != null) { fg = fg; };
hostMappings = lib.mapAttrs' (mapping: profile: hostMappings = lib.mapAttrs' (mapping: profile:
lib.nameValuePair lib.nameValuePair
(if lib.hasInfix "." mapping then mapping else "${mapping}.${serverCfg.domain}") (if lib.hasInfix "." mapping then mapping else "${mapping}.${serverCfg.domain}")
profile (normalizeProfile profile)
) (containerCfg.extra.mappings or {}); ) (containerCfg.extra.mappings or {});
traefikAssetPathRegexp = traefikAssetPathRegexp =
"^/(.*/)?" "^/(.*/)?"
@@ -26,7 +49,9 @@ let
inherit cacheControl; inherit cacheControl;
borderRadius = borderRadius; borderRadius = borderRadius;
mappings = hostMappings; mappings = hostMappings;
default = containerCfg.extra.default or null; default =
if containerCfg.extra ? default then normalizeProfile containerCfg.extra.default
else null;
}); });
pythonEnv = pkgs.python3.withPackages (ps: with ps; [ pythonEnv = pkgs.python3.withPackages (ps: with ps; [
cairosvg cairosvg
@@ -104,8 +129,8 @@ let
return f"{text}px" return f"{text}px"
def _render_svg(profile): def _render_svg(profile):
bg = _color(profile.get("background"), "#111827") bg = _color(profile.get("bg") or profile.get("background"), "#111827")
fg = _color(profile.get("foreground"), "#f8fafc") fg = _color(profile.get("fg") or profile.get("foreground"), "#f8fafc")
border_radius = _border_radius() border_radius = _border_radius()
logo_data_uri = _tinted_logo_data_uri(fg) logo_data_uri = _tinted_logo_data_uri(fg)
@@ -115,8 +140,26 @@ let
<image href="{logo_data_uri}" x="0" y="0" width="{canvas}" height="{canvas}" preserveAspectRatio="xMidYMid meet" /> <image href="{logo_data_uri}" x="0" y="0" width="{canvas}" height="{canvas}" preserveAspectRatio="xMidYMid meet" />
</svg>""" </svg>"""
def _cache_key(host, profile):
bg = _color(profile.get("bg") or profile.get("background"), "#111827")
fg = _color(profile.get("fg") or profile.get("foreground"), "#f8fafc")
cache_inputs = {
"asset_size": ASSET_SIZE,
"bg": bg,
"border_radius": _border_radius(),
"fg": fg,
"host": host,
"logo_hash": LOGO_HASH,
}
payload = json.dumps(cache_inputs, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16]
def _cache_name(host, profile):
safe_host = re.sub(r"[^a-z0-9.-]+", "_", host or "default")
return f"{safe_host}-{_cache_key(host, profile)}.ico"
def _generate_asset(host, profile): def _generate_asset(host, profile):
cache_name = f"{host}.ico" cache_name = _cache_name(host, profile)
target = CACHE_DIR / cache_name target = CACHE_DIR / cache_name
if target.exists(): if target.exists():
return target return target
@@ -140,11 +183,20 @@ let
return return
asset_path = _generate_asset(host, profile) asset_path = _generate_asset(host, profile)
etag = f'"{asset_path.stem.rsplit("-", 1)[-1]}"'
if self.headers.get("If-None-Match") == etag:
self.send_response(304)
self.send_header("ETag", etag)
self.send_header("Cache-Control", APP_CONFIG.get("cacheControl", DEFAULT_CACHE_CONTROL))
self.end_headers()
return
payload = asset_path.read_bytes() payload = asset_path.read_bytes()
self.send_response(200) self.send_response(200)
self.send_header("Content-Type", "image/x-icon") self.send_header("Content-Type", "image/x-icon")
self.send_header("Content-Length", str(len(payload))) self.send_header("Content-Length", str(len(payload)))
self.send_header("Cache-Control", APP_CONFIG.get("cacheControl", DEFAULT_CACHE_CONTROL)) self.send_header("Cache-Control", APP_CONFIG.get("cacheControl", DEFAULT_CACHE_CONTROL))
self.send_header("ETag", etag)
self.end_headers() self.end_headers()
if include_body: if include_body:
self.wfile.write(payload) self.wfile.write(payload)
+30 -27
View File
@@ -38,11 +38,11 @@
homepage.subdomain = "home"; homepage.subdomain = "home";
homepage.extra.stocks = [ "GLD" "VOO" "QUIK" "AMD" "DPRO" "KTOS" "NNE" "NVO" ]; homepage.extra.stocks = [ "GLD" "VOO" "QUIK" "AMD" "DPRO" "KTOS" "NNE" "NVO" ];
# ===== CLOUD ===== # ===== CLOUD =====
# nextcloud.subdomain = "cloud"; nextcloud.subdomain = "cloud";
# collabora.subdomain = "office"; collabora.subdomain = "office";
# etherpad.subdomain = "pad"; etherpad.subdomain = "pad";
# ethercalc.subdomain = "calc"; # ethercalc.subdomain = "calc";
# immich.subdomain = "pic"; immich.subdomain = "pic";
# ===== FLIX ===== # ===== FLIX =====
# invidious.subdomain = "yt"; # invidious.subdomain = "yt";
# jellyfin.subdomain = "flix"; # jellyfin.subdomain = "flix";
@@ -51,39 +51,42 @@
# transmission = { subdomain = "arr"; subpath = "transmission"; }; # transmission = { subdomain = "arr"; subpath = "transmission"; };
# handbrake = { subdomain = "arr"; subpath = "hb"; }; # handbrake = { subdomain = "arr"; subpath = "hb"; };
# ===== DEV ===== # ===== DEV =====
# gitea.subdomain = "git"; gitea.subdomain = "git";
# ===== HOME ===== # ===== HOME =====
# openhab.subdomain = "hab"; # openhab.subdomain = "hab";
# trmnl = { subdomain = "hass"; subpath = "trmnl"; }; # trmnl = { subdomain = "hass"; subpath = "trmnl"; };
influx.subdomain = "metrum"; influx.subdomain = "metrum";
# freshrss.subdomain = "rss"; freshrss.subdomain = "rss";
# suwayomi.subdomain = "manga"; suwayomi.subdomain = "manga";
# calibre.subdomain = "books"; calibre.subdomain = "books";
# selfmark = { subdomain = "arr"; subpath = "selfmark"; }; selfmark = { subdomain = "arr"; subpath = "selfmark"; };
favicon.extra = { favicon.extra = {
mappings = { mappings = {
"home" = { traefik={bg="base01";fg="low0B";};
background = "#0f172a"; umami={bg="base01";fg="low0B";};
foreground = "#22c55e"; sso={bg="base01";fg="high08";};
}; searx={bg="base01";fg="alt_high0D";};
"cloud" = { home={bg="base01";fg="base07";};
background = "#0b1220"; cloud={bg="base01";fg="high0C";};
foreground = "#38bdf8"; office={bg="base01";fg="alt_high0C";};
}; pad={bg="base01";fg="alt_high0C";};
"sso" = { #calc={bg="base01";fg="alt_high0C";};
background = "#1f1630"; pic={bg="base01";fg="high09";};
foreground = "#f59e0b"; yt={bg="base01";fg="high0F";};
}; flix={bg="base01";fg="high0D";};
"metrum" = { arr={bg="base01";fg="low0D";};
background = "#1a1d29"; git={bg="base01";fg="high0A";};
foreground = "#a78bfa"; hab={bg="base01";fg="high08";};
}; metrum={bg="base01";fg="low0B";};
rss={bg="base01";fg="high0A";};
manga={bg="base01";fg="high0E";};
books={bg="base01";fg="high0E";};
}; };
default = { default = {
background = "#111827"; bg = "base01";
foreground = "#ffffff"; fg = "base07";
}; };
}; };
}; };