Compare commits

...

3 Commits

3 changed files with 442 additions and 367 deletions
+95 -83
View File
@@ -10,6 +10,11 @@ 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);
ensureAttrSet = field: value:
if builtins.isAttrs value then
value
else
throw "favicon `${field}` must be an attribute set";
resolveColor = value: resolveColor = value:
if value == null then null if value == null then null
else if !builtins.isString value then else if !builtins.isString value then
@@ -17,48 +22,62 @@ let
else if lib.hasPrefix "#" value then else if lib.hasPrefix "#" value then
value value
else else
lib.attrByPath [ value ] (throw "Unknown favicon color reference `${value}`") palette; let
paletteValue = lib.attrByPath [ value ] (throw "Unknown favicon color reference `${value}`") palette;
in
if builtins.isString paletteValue then
"#${paletteValue}"
else
throw "favicon palette reference `${value}` must resolve to a string";
normalizeProfile = profile: normalizeProfile = profile:
let let
normalizedProfile = ensureAttrSet "profile" profile;
bg = bg =
if profile ? bg then resolveColor profile.bg if normalizedProfile ? bg then resolveColor normalizedProfile.bg
else if profile ? background then resolveColor profile.background else if normalizedProfile ? background then resolveColor normalizedProfile.background
else null; else null;
fg = fg =
if profile ? fg then resolveColor profile.fg if normalizedProfile ? fg then resolveColor normalizedProfile.fg
else if profile ? foreground then resolveColor profile.foreground else if normalizedProfile ? foreground then resolveColor normalizedProfile.foreground
else null; else null;
in in
(lib.filterAttrs (name: _: !(builtins.elem name [ "bg" "background" "fg" "foreground" ])) profile) (lib.filterAttrs (name: _: !(builtins.elem name [ "bg" "background" "fg" "foreground" ])) normalizedProfile)
// lib.optionalAttrs (bg != null) { bg = bg; } // lib.optionalAttrs (bg != null) { bg = bg; }
// lib.optionalAttrs (fg != null) { fg = fg; }; // lib.optionalAttrs (fg != null) { fg = fg; };
hostMappings = lib.mapAttrs' (mapping: profile: hostMappings = lib.mapAttrs (_: profile: normalizeProfile profile) (
lib.nameValuePair mapping (normalizeProfile profile) ensureAttrSet "mappings" (containerCfg.extra.mappings or { })
) (containerCfg.extra.mappings or {}); );
defaultProfile =
if containerCfg.extra ? default then
normalizeProfile containerCfg.extra.default
else
null;
faviconPathPatterns = [
"favicon(-[0-9]+x[0-9]+)?(\\.(ico|png|svg))?"
"fav(icon)?(-[0-9]+x[0-9]+)?\\.(ico|png|svg)"
"apple-icon(-[0-9]+)?(\\.(ico|png))?"
"apple-touch-icon(-precomposed)?\\.png"
"android-chrome-[0-9]+x[0-9]+\\.png"
"mstile-[0-9]+x[0-9]+\\.png"
"logo\\.ico"
];
traefikAssetPathRegexp = traefikAssetPathRegexp =
"^/(.*/)?" "^/(.*/)?"
+ "(fav(icon)?(-[0-9]+x[0-9]+)?\\.(ico|png|svg)" + "("
+ "|(favicon|apple-icon)(-[0-9]+)?(\\.(ico|png))?" + lib.concatStringsSep "|" faviconPathPatterns
+ "|logo\\.(ico)" + ")$";
+ "|fav([0-9]+)?\\.(ico|png)"
+ "|apple-touch-icon(-precomposed)?\\.png"
+ "|android-chrome-[0-9]+x[0-9]+\\.png"
+ "|mstile-[0-9]+x[0-9]+\\.png)$";
configFile = pkgs.writeText "favicon-config.json" (builtins.toJSON { configFile = pkgs.writeText "favicon-config.json" (builtins.toJSON {
inherit cacheControl; inherit cacheControl;
borderRadius = borderRadius; inherit borderRadius;
domain = serverCfg.domain; domain = serverCfg.domain;
mappings = hostMappings; mappings = hostMappings;
default = default = defaultProfile;
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
pillow pillow
]); ]);
serverScript = pkgs.writeText "favicon-server.py" '' serverScript = pkgs.writeText "favicon-server.py" ''
import base64
import hashlib import hashlib
from io import BytesIO from io import BytesIO
import json import json
@@ -68,7 +87,7 @@ let
from pathlib import Path from pathlib import Path
import cairosvg import cairosvg
from PIL import Image from PIL import Image, ImageDraw
CONFIG_PATH = os.environ.get("FAVICON_CONFIG", "/config/config.json") CONFIG_PATH = os.environ.get("FAVICON_CONFIG", "/config/config.json")
LOGO_PATH = os.environ.get("FAVICON_LOGO", "/assets/logo.svg") LOGO_PATH = os.environ.get("FAVICON_LOGO", "/assets/logo.svg")
@@ -87,31 +106,19 @@ let
APP_DOMAIN = (APP_CONFIG.get("domain", "") or "").strip().lower() APP_DOMAIN = (APP_CONFIG.get("domain", "") or "").strip().lower()
CACHE_CONTROL = APP_CONFIG.get("cacheControl", DEFAULT_CACHE_CONTROL) CACHE_CONTROL = APP_CONFIG.get("cacheControl", DEFAULT_CACHE_CONTROL)
LOGO_HASH = hashlib.sha256(LOGO_BYTES).hexdigest() LOGO_HASH = hashlib.sha256(LOGO_BYTES).hexdigest()
DEFAULT_COLORS = {"bg": "#111827", "fg": "#f8fafc"}
def _normalize_host(host): def _request_host(headers):
host = (
headers.get("X-Forwarded-Host")
or headers.get("X-Original-Host")
or headers.get("Host", "")
)
host = (host or "").split(",", 1)[0].split(":", 1)[0].strip().lower() host = (host or "").split(",", 1)[0].split(":", 1)[0].strip().lower()
if APP_DOMAIN and host.endswith(f".{APP_DOMAIN}"): if APP_DOMAIN and host.endswith(f".{APP_DOMAIN}"):
return host[:-(len(APP_DOMAIN) + 1)] return host[:-(len(APP_DOMAIN) + 1)]
return host return host
def _request_host(headers):
forwarded = headers.get("X-Forwarded-Host", "")
original = headers.get("X-Original-Host", "")
host = forwarded or original or headers.get("Host", "")
return _normalize_host(host)
def _pick_profile(host):
return MAPPINGS.get(host) or DEFAULT_PROFILE
def _color(value, fallback):
return value if isinstance(value, str) and value else fallback
def _resolved_profile(profile):
return {
"bg": _color(profile.get("bg") or profile.get("background"), "#111827"),
"fg": _color(profile.get("fg") or profile.get("foreground"), "#f8fafc"),
}
def _replace_svg_color(svg, attribute, color): def _replace_svg_color(svg, attribute, color):
if attribute in {"fill", "stroke"}: if attribute in {"fill", "stroke"}:
svg = re.sub( svg = re.sub(
@@ -133,67 +140,72 @@ let
flags=re.IGNORECASE, flags=re.IGNORECASE,
) )
def _tinted_logo_data_uri(color):
svg = LOGO_BYTES.decode("utf-8")
svg = _replace_svg_color(svg, "fill", color)
svg = _replace_svg_color(svg, "stroke", color)
return "data:image/svg+xml;base64," + base64.b64encode(svg.encode("utf-8")).decode("ascii")
border_radius = str(APP_CONFIG.get("borderRadius", "8")).strip() border_radius = str(APP_CONFIG.get("borderRadius", "8")).strip()
if not border_radius.endswith("px"): if not border_radius.endswith("px"):
border_radius = f"{border_radius}px" border_radius = f"{border_radius}px"
border_radius_px = max(0, int(float(border_radius[:-2])))
def _render_svg(colors): def _colors(profile):
logo_data_uri = _tinted_logo_data_uri(colors["fg"]) profile = profile or {}
return {
canvas = 256 "bg": profile.get("bg") or profile.get("background") or DEFAULT_COLORS["bg"],
return f"""<svg xmlns="http://www.w3.org/2000/svg" width="{canvas}" height="{canvas}" viewBox="0 0 {canvas} {canvas}"> "fg": profile.get("fg") or profile.get("foreground") or DEFAULT_COLORS["fg"],
<rect x="0" y="0" width="{canvas}" height="{canvas}" rx="{border_radius}" ry="{border_radius}" fill="{colors["bg"]}" />
<image href="{logo_data_uri}" x="0" y="0" width="{canvas}" height="{canvas}" preserveAspectRatio="xMidYMid meet" />
</svg>"""
def _cache_key(host, colors):
cache_inputs = {
"asset_size": ASSET_SIZE,
"bg": colors["bg"],
"border_radius": border_radius,
"fg": colors["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, colors): def _asset_path(host, colors):
payload = json.dumps(
{
"asset_size": ASSET_SIZE,
"bg": colors["bg"],
"border_radius": border_radius,
"fg": colors["fg"],
"host": host,
"logo_hash": LOGO_HASH,
},
sort_keys=True,
separators=(",", ":"),
)
safe_host = re.sub(r"[^a-z0-9.-]+", "_", host or "default") safe_host = re.sub(r"[^a-z0-9.-]+", "_", host or "default")
return f"{safe_host}-{_cache_key(host, colors)}.ico" digest = hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16]
return CACHE_DIR / f"{safe_host}-{digest}.ico"
def _generate_asset(host, profile): def _render_icon(colors, target):
colors = _resolved_profile(profile) svg = LOGO_BYTES.decode("utf-8")
cache_name = _cache_name(host, colors) for attribute in ("fill", "stroke"):
target = CACHE_DIR / cache_name svg = _replace_svg_color(svg, attribute, colors["fg"])
if target.exists():
return target
CACHE_DIR.mkdir(parents=True, exist_ok=True) logo_png = cairosvg.svg2png(
svg = _render_svg(colors).encode("utf-8") bytestring=svg.encode("utf-8"),
png_bytes = cairosvg.svg2png(bytestring=svg, output_width=ASSET_SIZE, output_height=ASSET_SIZE) output_width=ASSET_SIZE,
image = Image.open(BytesIO(png_bytes)) output_height=ASSET_SIZE,
image.save(target, format="ICO", sizes=[(ASSET_SIZE, ASSET_SIZE)]) )
image.close() tmp_target = target.with_suffix(".tmp")
return target with Image.new("RGBA", (ASSET_SIZE, ASSET_SIZE), (0, 0, 0, 0)) as canvas:
ImageDraw.Draw(canvas).rounded_rectangle(
(0, 0, ASSET_SIZE, ASSET_SIZE),
radius=min(border_radius_px, ASSET_SIZE // 2),
fill=colors["bg"],
)
with Image.open(BytesIO(logo_png)) as logo:
canvas.alpha_composite(logo.convert("RGBA"))
canvas.save(tmp_target, format="ICO", sizes=[(ASSET_SIZE, ASSET_SIZE)])
os.replace(tmp_target, target)
class Handler(BaseHTTPRequestHandler): class Handler(BaseHTTPRequestHandler):
server_version = "favicon-router/1.0" server_version = "favicon-router/1.0"
def _serve(self, include_body): def _serve(self, include_body):
host = _request_host(self.headers) host = _request_host(self.headers)
profile = _pick_profile(host) profile = MAPPINGS.get(host) or DEFAULT_PROFILE
if not profile: if not profile:
self.send_error(404, "No favicon mapping for host") self.send_error(404, "No favicon mapping for host")
return return
asset_path = _generate_asset(host, profile) colors = _colors(profile)
asset_path = _asset_path(host, colors)
if not asset_path.exists():
CACHE_DIR.mkdir(parents=True, exist_ok=True)
_render_icon(colors, asset_path)
etag = f'"{asset_path.stem.rsplit("-", 1)[-1]}"' etag = f'"{asset_path.stem.rsplit("-", 1)[-1]}"'
if self.headers.get("If-None-Match") == etag: if self.headers.get("If-None-Match") == etag:
self.send_response(304) self.send_response(304)
+338 -281
View File
@@ -1,293 +1,350 @@
{ config, containerCfg, pkgs, lib, builder, name,... }: { config, containerCfg, pkgs, lib, builder, name, ... }:
let let
version = "latest"; version = "latest";
serverCfg = config.syscfg.server; serverCfg = config.syscfg.server;
mediaCfg = config.syscfg.media; mediaCfg = config.syscfg.media;
backgroundImage = if mediaCfg.banner.png != null then mediaCfg.banner.png else mediaCfg.bg; homepageExtra = containerCfg.extra or {};
backgroundFileName = builtins.baseNameOf (toString backgroundImage); backgroundImage = if mediaCfg.banner.png != null then mediaCfg.banner.png else mediaCfg.bg;
backgroundMount = "/app/public/media/${backgroundFileName}"; backgroundFileName = builtins.baseNameOf (toString backgroundImage);
backgroundMount = "/app/public/media/${backgroundFileName}";
latitude =
if homepageExtra ? latitude then homepageExtra.latitude
else if homepageExtra ? lat then homepageExtra.lat
else 47.3769;
longitude =
if homepageExtra ? longitude then homepageExtra.longitude
else if homepageExtra ? lon then homepageExtra.lon
else 8.5417;
extraBookmarks = homepageExtra.bookmarks or [];
extraServices = homepageExtra.services or [];
settings = pkgs.writers.writeYAML "settings.yaml" { settings = pkgs.writers.writeYAML "settings.yaml" {
title = "Home"; title = "Home";
description = ""; description = "";
startUrl = "https://${containerCfg.subdomain}.${serverCfg.domain}"; startUrl = "https://${containerCfg.subdomain}.${serverCfg.domain}";
background = { background = {
image = "/media/${backgroundFileName}"; image = "/media/${backgroundFileName}";
blur = "xs"; # "", sm, md, xl,... brightness = 50;
# saturate = ""; };
brightness = 50; cardBlur = "md";
# opacity = 40; favicon = config.syscfg.media.logo.ico;
}; theme = "dark";
cardBlur = "md"; color = "slate";
# favicon = ""; fullWidth = true;
theme = "dark"; useEqualHeights = true;
color = "slate"; pwa = { };
fullWidth = true; layout = {
useEqualHeights = true; Admin = {
pwa = { style = "row";
columns = 4;
};
};
providers = {
finnhub = "{{HOMEPAGE_VAR_FINNHUB}}";
};
headerStyle = "clean";
hideVersion = true;
disableUpdateCheck = true;
showStats = false;
statusStyle = "dot";
hideErrors = true;
};
}; widgets = pkgs.writers.writeYAML "widgets.yaml" [
layout = { { openmeteo = {
Admin = { latitude = toString latitude;
style = "row"; longitude = toString longitude;
columns = 4; timezone = config.time.timeZone;
units = "metric";
cache = "15";
};
}
{ search = {
provider = "custom";
focus = true;
showSearchSuggestions = true;
target = "_blank";
} // (lib.optionalAttrs (serverCfg.containers ? searxng) {
url = "https://${serverCfg.containers.searxng.subdomain}.${serverCfg.domain}/search?q=";
suggestionUrl = "https://${serverCfg.containers.searxng.subdomain}.${serverCfg.domain}/autocompleter?q=";
});
}
{ stocks = {
provider = "finnhub";
color = true;
cache = 15;
watchlist = homepageExtra.stocks or [];
};
}
];
bookmarks = pkgs.writers.writeYAML "bookmarks.yaml" (extraBookmarks);
services = pkgs.writers.writeYAML "services.yaml" ([
{ Media = lib.flatten [
(lib.optional (serverCfg.containers ? jellyfin) {
Jellyfin = {
icon = "jellyfin.png";
href = "https://${serverCfg.containers.jellyfin.subdomain}.${serverCfg.domain}";
widget = {
type = "jellyfin";
url = "http://jellyfin-server:8096";
key = "{{HOMEPAGE_VAR_JELLYFIN_API}}";
}; };
}; };
providers = { })
finnhub = "{{HOMEPAGE_VAR_FINNHUB}}"; (lib.optional (serverCfg.containers ? invidious) {
}; Invidious = {
headerStyle = "clean"; icon = "invidious.png";
hideVersion = true; href = "https://${serverCfg.containers.invidious.subdomain}.${serverCfg.domain}";
disableUpdateCheck = true; };
showStats = false; })
statusStyle = "dot"; (lib.optional (serverCfg.containers ? calibre) {
hideErrors = true; Calibre = {
}; icon = "calibre.png";
widgets = pkgs.writers.writeYAML "widgets.yaml" [ href = "https://${serverCfg.containers.calibre.subdomain}.${serverCfg.domain}";
{openmeteo = { };
latitude = "47.3769"; })
longitude = "8.5417"; (lib.optional (serverCfg.containers ? freshrss) {
timezone = "Europe/Zurich"; FreshRSS = {
units = "metric"; icon = "freshrss.png";
cache = "15"; href = "https://${serverCfg.containers.freshrss.subdomain}.${serverCfg.domain}";
};} };
{search = { })
provider = "custom"; (lib.optional (serverCfg.containers ? miniflux) {
focus = true; Miniflux = {
showSearchSuggestions = true; icon = "miniflux.png";
target = "_blank"; href = "https://${serverCfg.containers.miniflux.subdomain}.${serverCfg.domain}";
} // (lib.optionalAttrs (serverCfg.containers?searxng) { widget = {
url = "https://${serverCfg.containers.searxng.subdomain}.${serverCfg.domain}/search?q="; type = "miniflux";
suggestionUrl = "https://${serverCfg.containers.searxng.subdomain}.${serverCfg.domain}/autocompleter?q="; url = "http://miniflux-server";
}); key = "{{HOMEPAGE_VAR_MINIFLUX_API}}";
} };
{stocks = { };
provider = "finnhub"; })
color = true; (lib.optional (serverCfg.containers ? suwayomi) {
cache = 15; Suwayomi = {
watchlist = containerCfg.extra.stocks or []; icon = "suwayomi.png";
href = "https://${serverCfg.containers.suwayomi.subdomain}.${serverCfg.domain}";
};} };
]; })
];
bookmarks = pkgs.writers.writeYAML "bookmarks.yaml" [ }
{ Cloud = lib.flatten [
]; (lib.optional (serverCfg.containers ? nextcloud) {
services = pkgs.writers.writeYAML "services.yaml" [ Nextcloud = {
{Media = lib.flatten [ icon = "nextcloud.png";
(lib.optional (serverCfg.containers?jellyfin) { href = "https://${serverCfg.containers.nextcloud.subdomain}.${serverCfg.domain}";
Jellyfin={ widget = {
icon = "jellyfin.png"; type = "nextcloud";
href = "https://${serverCfg.containers.jellyfin.subdomain}.${serverCfg.domain}"; url = "http://nextcloud-server:80";
widget = { key = "{{HOMEPAGE_VAR_NEXTCLOUD_API}}";
type="jellyfin"; };
url = "http://jellyfin-server:8096"; };
key = "{{HOMEPAGE_VAR_JELLYFIN_API}}"; })
}; (lib.optional (serverCfg.containers ? ethercalc) {
}; Ethercalc = {
}) icon = "ethercalc.png";
(lib.optional (serverCfg.containers?invidious) { href = "https://${serverCfg.containers.ethercalc.subdomain}.${serverCfg.domain}";
Invidious={ };
icon = "invidious.png"; })
href = "https://${serverCfg.containers.invidious.subdomain}.${serverCfg.domain}"; (lib.optional (serverCfg.containers ? etherpad) {
}; Etherpad = {
}) icon = "etherpad.png";
(lib.optional (serverCfg.containers?miniflux) { href = "https://${serverCfg.containers.etherpad.subdomain}.${serverCfg.domain}";
Miniflux={ };
icon = "miniflux.png"; })
href = "https://${serverCfg.containers.miniflux.subdomain}.${serverCfg.domain}"; (lib.optional (serverCfg.containers ? collabora && false) {
widget = { Collabora = {
type="miniflux"; icon = "microsoft-office.png";
url = "http://miniflux-server"; href = "https://${serverCfg.containers.collabora.subdomain}.${serverCfg.domain}";
key = "{{HOMEPAGE_VAR_MINIFLUX_API}}"; };
}; })
}; (lib.optional (serverCfg.containers ? immich) {
}) Immich = {
];} icon = "immich.png";
{Cloud = lib.flatten [ href = "https://${serverCfg.containers.immich.subdomain}.${serverCfg.domain}";
(lib.optional (serverCfg.containers?nextcloud) { widget = {
Nextcloud={ type = "immich";
icon = "nextcloud.png"; url = "http://immich-server:80";
href = "https://${serverCfg.containers.nextcloud.subdomain}.${serverCfg.domain}"; key = "{{HOMEPAGE_VAR_IMMICH_API}}";
widget = { version = "2";
type="nextcloud"; };
url = "http://nextcloud-server:80"; };
key = "{{HOMEPAGE_VAR_NEXTCLOUD_API}}"; })
}; ];
}; }
}) { Home = lib.flatten [
(lib.optional (serverCfg.containers?ethercalc) { (lib.optional (serverCfg.containers ? homeassistant) {
Ethercalc={ "Home Assistant" = {
icon = "ethercalc.png"; icon = "home-assistant.png";
href = "https://${serverCfg.containers.ethercalc.subdomain}.${serverCfg.domain}"; href = "https://${serverCfg.containers.homeassistant.subdomain}.${serverCfg.domain}";
}; };
}) })
(lib.optional (serverCfg.containers?etherpad) { (lib.optional (serverCfg.containers ? openhab) {
Etherpad={ openHAB = {
icon = "etherpad.png"; icon = "openhab.png";
href = "https://${serverCfg.containers.etherpad.subdomain}.${serverCfg.domain}"; href = "https://${serverCfg.containers.openhab.subdomain}.${serverCfg.domain}";
}; };
}) })
(lib.optional (serverCfg.containers?immich) { (lib.optional (serverCfg.containers ? frigate) {
immich={ Frigate = {
icon = "immich.png"; icon = "frigate.png";
href = "https://${serverCfg.containers.immich.subdomain}.${serverCfg.domain}"; href = "https://${serverCfg.containers.frigate.subdomain}.${serverCfg.domain}";
widget = { };
type="immich"; })
url = "http://immich-server:80"; ];
key = "{{HOMEPAGE_VAR_IMMICH_API}}"; }
version = "2"; { Dev = lib.flatten [
}; (lib.optional (serverCfg.containers ? gitea) {
}; Gitea = {
}) icon = "gitea.png";
];} href = "https://${serverCfg.containers.gitea.subdomain}.${serverCfg.domain}";
{Dev = lib.flatten [ };
(lib.optional (serverCfg.containers?gitea) { })
Gitea={ (lib.optional (serverCfg.containers ? trmnl) {
icon = "gitea.png"; TRMNL = {
href = "https://${serverCfg.containers.gitea.subdomain}.${serverCfg.domain}"; icon = "terminal.png";
# widget = { href = "https://${serverCfg.containers.trmnl.subdomain}.${serverCfg.domain}";
# type="gitea"; };
# url = "http://gitea-server:8080"; })
# key = "{{HOMEPAGE_VAR_GITEA_API}}"; ];
# }; }
}; { Admin = lib.flatten [
}) (lib.optional (serverCfg.containers ? traefik) {
];} Traefik = {
{Admin = #lib.flatten [ icon = "traefik.png";
#({permissions.groups = ["admin"];}) href = "https://${serverCfg.containers.traefik.subdomain}.${serverCfg.domain}";
#({services = widget = {
lib.flatten [ type = "traefik";
(lib.optional (serverCfg.containers?traefik) { url = "http://traefik-server:8080";
Traefik={ };
icon = "traefik.png"; };
href = "https://${serverCfg.containers.traefik.subdomain}.${serverCfg.domain}"; })
widget = { (lib.optional (serverCfg.containers ? authentik) {
type = "traefik"; Authentik = {
url = "http://traefik-server:8080"; icon = "authentik.png";
}; href = "https://${serverCfg.containers.authentik.subdomain}.${serverCfg.domain}";
}; widget = {
}) type = "authentik";
(lib.optional (serverCfg.containers?authentik) { url = "http://authentik-server:9000";
Authentik={ key = "{{HOMEPAGE_VAR_AUTHENTIK_API}}";
icon = "authentik.png"; version = "2";
href = "https://${serverCfg.containers.authentik.subdomain}.${serverCfg.domain}"; };
widget = { };
type = "authentik"; })
url = "http://authentik-server:9000"; (lib.optional (serverCfg.containers ? umami) {
key = "{{HOMEPAGE_VAR_AUTHENTIK_API}}"; Umami = {
version = "2"; icon = "umami.png";
}; href = "https://${serverCfg.containers.umami.subdomain}.${serverCfg.domain}";
}; };
}) })
(lib.optional (serverCfg.containers?umami) { (lib.optional (serverCfg.containers ? influx) {
Umami={ Influx = {
icon = "umami.png"; icon = "grafana.png";
href = "https://${serverCfg.containers.umami.subdomain}.${serverCfg.domain}"; href = "https://${serverCfg.containers.influx.subdomain}.${serverCfg.domain}";
}; };
}) })
(lib.optional (serverCfg.containers?influx) { (lib.optional (serverCfg.containers ? handbrake) {
Influx={ Handbrake = {
icon = "grafana.png"; icon = "handbrake.png";
href = "https://${serverCfg.containers.influx.subdomain}.${serverCfg.domain}"; href = "https://${serverCfg.containers.handbrake.subdomain}.${serverCfg.domain}";
}; };
}) })
(lib.optional (serverCfg.containers?handbrake) { (lib.optional (serverCfg.containers ? transmission) {
Handbrake={ Transmission = {
icon = "handbrake.png"; icon = "transmission.png";
href = "https://${serverCfg.containers.handbrake.subdomain}.${serverCfg.domain}"; href = "https://${serverCfg.containers.transmission.subdomain}.${serverCfg.domain}/transmission";
}; widget = {
}) type = "transmission";
(lib.optional (serverCfg.containers?transmission) { url = "http://transmission-server:9091";
Transmission={ rpcUrl = "/transmission/";
icon = "transmission.png"; };
href = "https://${serverCfg.containers.transmission.subdomain}.${serverCfg.domain}/transmission"; };
widget = { })
type = "transmission"; (lib.optional (serverCfg.containers ? selfmark) {
url = "http://transmission-server:9091"; Selfmark = {
rpcUrl = "/transmission/"; icon = "link.png";
}; href = "https://${serverCfg.containers.selfmark.subdomain}.${serverCfg.domain}";
}; };
}) })
(lib.optional (serverCfg.containers?servarr) ( (lib.optional (serverCfg.containers ? servarr) (
let let
modules = serverCfg.containers.servarr.extra.modules or ["prowlarr" "sonarr" "radarr" "flaresolverr" ]; modules = serverCfg.containers.servarr.extra.modules or [ "prowlarr" "sonarr" "radarr" "flaresolverr" ];
in in
(lib.optional (builtins.elem "sonarr" modules) { (lib.optional (builtins.elem "sonarr" modules) {
Sonarr={ Sonarr = {
icon = "sonarr.png"; icon = "sonarr.png";
href = "https://${serverCfg.containers.servarr.subdomain}.${serverCfg.domain}/sonarr"; href = "https://${serverCfg.containers.servarr.subdomain}.${serverCfg.domain}/sonarr";
widget = { widget = {
type = "sonarr"; type = "sonarr";
url = "http://servarr-sonarr:8989"; url = "http://servarr-sonarr:8989";
key = "{{HOMEPAGE_VAR_SONARR_API}}"; key = "{{HOMEPAGE_VAR_SONARR_API}}";
};
};
}) ++ (lib.optional (builtins.elem "radarr" modules) {
Radarr={
icon = "radarr.png";
href = "https://${serverCfg.containers.servarr.subdomain}.${serverCfg.domain}/radarr";
widget = {
type = "radarr";
url = "http://servarr-radarr:8989";
key = "{{HOMEPAGE_VAR_RADARR_API}}";
};
};
}) ++ (lib.optional (builtins.elem "lidarr" modules) {
Lidarr={
icon = "lidarr.png";
href = "https://${serverCfg.containers.servarr.subdomain}.${serverCfg.domain}/lidarr";
widget = {
type = "lidarr";
url = "http://servarr-lidarr:8989";
key = "{{HOMEPAGE_VAR_LIDARR_API}}";
};
};
}) ++ (lib.optional (builtins.elem "prowlarr" modules) {
Prowlarr={
icon = "prowlarr.png";
href = "https://${serverCfg.containers.servarr.subdomain}.${serverCfg.domain}/prowlarr";
widget = {
type = "prowlarr";
url = "http://servarr-prowlarr:8989";
key = "{{HOMEPAGE_VAR_PROWLARR_API}}";
};
}; };
};
}) })
# Bazarr ++ (lib.optional (builtins.elem "radarr" modules) {
Radarr = {
)) icon = "radarr.png";
];}#)];} href = "https://${serverCfg.containers.servarr.subdomain}.${serverCfg.domain}/radarr";
]; widget = {
type = "radarr";
url = "http://servarr-radarr:8989";
key = "{{HOMEPAGE_VAR_RADARR_API}}";
};
};
})
++ (lib.optional (builtins.elem "lidarr" modules) {
Lidarr = {
icon = "lidarr.png";
href = "https://${serverCfg.containers.servarr.subdomain}.${serverCfg.domain}/lidarr";
widget = {
type = "lidarr";
url = "http://servarr-lidarr:8989";
key = "{{HOMEPAGE_VAR_LIDARR_API}}";
};
};
})
++ (lib.optional (builtins.elem "prowlarr" modules) {
Prowlarr = {
icon = "prowlarr.png";
href = "https://${serverCfg.containers.servarr.subdomain}.${serverCfg.domain}/prowlarr";
widget = {
type = "prowlarr";
url = "http://servarr-prowlarr:8989";
key = "{{HOMEPAGE_VAR_PROWLARR_API}}";
};
};
})
))
];
}
] ++ extraServices);
in { in {
runtime = { runtime = {
containers = { containers = {
server = builder.mkContainer { server = builder.mkContainer {
subdomain = containerCfg.subdomain; subdomain = containerCfg.subdomain;
image = "ghcr.io/gethomepage/homepage:${version}"; image = "ghcr.io/gethomepage/homepage:${version}";
port = 3000; port = 3000;
extraEnv = { extraEnv = {
HOMEPAGE_VAR_TITLE="${serverCfg.domain}"; HOMEPAGE_VAR_TITLE = "${serverCfg.domain}";
HOMEPAGE_ALLOWED_HOSTS = "${containerCfg.subdomain}.${serverCfg.domain},${builder.host}"; HOMEPAGE_ALLOWED_HOSTS = "${containerCfg.subdomain}.${serverCfg.domain},${builder.host}";
}; };
extraLabels = { extraLabels = {
"traefik.http.routers.${containerCfg.subdomain}.service" = "${containerCfg.subdomain}"; "traefik.http.routers.${containerCfg.subdomain}.service" = "${containerCfg.subdomain}";
}; };
overrides = { overrides = {
environmentFiles = [ config.sops.secrets."CUSTOM".path ]; environmentFiles = [ config.sops.secrets."CUSTOM".path ];
volumes = [ volumes = [
"${settings}:/app/config/settings.yaml:ro" "${settings}:/app/config/settings.yaml:ro"
"${services}:/app/config/services.yaml:ro" "${services}:/app/config/services.yaml:ro"
"${widgets}:/app/config/widgets.yaml:ro" "${widgets}:/app/config/widgets.yaml:ro"
"${bookmarks}:/app/config/bookmarks.yaml:ro" "${bookmarks}:/app/config/bookmarks.yaml:ro"
"${backgroundImage}:${backgroundMount}:ro" "${backgroundImage}:${backgroundMount}:ro"
]; ];
}; };
};
}; };
}; };
};
} }
+9 -3
View File
@@ -15,17 +15,23 @@ in {
sops.age.generateKey = true; sops.age.generateKey = true;
sops.secrets = lib.mkMerge [ sops.secrets = lib.mkMerge [
{ {
wifi = { };
"${config.syscfg.hostname}_ssh_priv" = { "${config.syscfg.hostname}_ssh_priv" = {
mode = "0400"; mode = "0400";
owner = config.users.users.${config.syscfg.defaultUser}.name; owner = config.users.users.${config.syscfg.defaultUser}.name;
group = config.users.users.${config.syscfg.defaultUser}.group; group = config.users.users.${config.syscfg.defaultUser}.group;
}; };
}
(lib.mkIf config.syscfg.net.wlp.enable {
wifi = { };
})
(lib.mkIf config.syscfg.net.wg.enable {
"${config.syscfg.hostname}_wg_priv" = { }; "${config.syscfg.hostname}_wg_priv" = { };
})
(lib.mkIf config.syscfg.monitoring.telegraf.enable {
telegraf = { telegraf = {
mode = "0400"; mode = "0400";
}; };
} })];
];
} }