Compare commits
3 Commits
e855ea26c0
...
59dd62b9c9
| Author | SHA1 | Date | |
|---|---|---|---|
| 59dd62b9c9 | |||
| a899fe4c6f | |||
| 75914fb975 |
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
};
|
};
|
||||||
}
|
})];
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user