{ config, lib, registry, ... }: let cfg = config.modules.services.homepage; # NOLINT(let_order) derivedAllowedHosts = lib.unique ( lib.optional (selfRoute != null) selfRoute.domain ++ lib.optionals (selfAddress != null) [ selfAddress "${selfAddress}:${toString cfg.listenPort}" ] ); selfRoute = selfEntry.route or null; selfEntry = registry.join cfg.hostName cfg.entryName; selfAddress = selfEntry.address or null; homepageServices = map mkTab (lib.filter (tab: lib.hasAttr tab entriesByTab) orderedTabs); mkTab = tab: let sections = entriesByTab.${tab}; in { ${tab} = map (section: { ${section} = sections.${section}; }) (orderedSections tab sections); }; entriesByTab = lib.foldl' insertEntry { } sortedEntries; insertEntry = acc: item: acc // { ${item.homepage.tab} = (acc.${item.homepage.tab} or { }) // { ${item.homepage.section} = (acc.${item.homepage.tab}.${item.homepage.section} or [ ]) ++ [ { ${item.homepage.name} = mkServiceAttrs item; } ]; }; }; sortedEntries = lib.sort (left: right: left.homepage.name < right.homepage.name) homepageEntries; homepageEntries = lib.concatLists ( lib.mapAttrsToList ( host: entries: lib.mapAttrsToList (entry: homepage: { inherit host entry homepage; composite = registry.join host entry; }) entries ) registry.homepage ); orderedTabs = tabOrder ++ lib.filter (tab: !lib.elem tab tabOrder) (lib.attrNames entriesByTab); tabOrder = [ "Apps" "Download" "System" ]; mkRegistryPath = registryName: host: entry: "registry.${registryName}.${host}.${entry}"; mkHref = item: if item.homepage.href != null then item.homepage.href else if (item.composite.route or null) != null then if item.composite.route.port != null then "https://${item.composite.route.domain}" else if item.composite.route.http.port != null then "http://${item.composite.route.domain}" else throw "${mkRegistryPath "homepage" item.host item.entry} needs href or a matching TCP route entry" else throw "${mkRegistryPath "homepage" item.host item.entry} needs href or a matching route entry"; mkServiceAttrs = item: lib.filterAttrs (_: value: value != null) { href = mkHref item; inherit (item.homepage) description icon; }; sectionOrderByTab = { Apps = [ "Media" "Files" "Other" ]; Download = [ "Library Managers" "Other Utilities" "Download Clients" ]; System = [ "Monitoring" "Network" "Machines" "Docker" ]; }; orderedSections = tab: sections: let sectionOrder = sectionOrderByTab.${tab} or [ ]; in sectionOrder ++ lib.filter (section: !lib.elem section sectionOrder) (lib.attrNames sections); in { options.modules.services.homepage = { enable = lib.mkEnableOption "Homepage Dashboard"; listenPort = lib.mkOption { type = lib.types.port; default = 8082; description = "Port for Homepage to bind to."; }; hostName = lib.mkOption { type = lib.types.str; description = "Host registry key that owns the Homepage guest entry."; }; entryName = lib.mkOption { type = lib.types.str; default = "homepage"; description = "Registry entry name for this Homepage service."; }; allowedHosts = lib.mkOption { type = lib.types.nullOr (lib.types.listOf lib.types.str); default = null; description = "Host headers accepted by Homepage. Defaults to matching address and route registry entries."; }; environmentFile = lib.mkOption { type = lib.types.path; default = "/run/secrets/homepage.env"; description = "Environment file passed to Homepage."; }; }; config = lib.mkIf cfg.enable { services.homepage-dashboard = { enable = true; openFirewall = true; inherit (cfg) listenPort; environmentFiles = [ cfg.environmentFile ]; allowedHosts = lib.concatStringsSep "," ( if cfg.allowedHosts == null then derivedAllowedHosts else cfg.allowedHosts ); services = homepageServices; widgets = [ { greeting = { text_size = "xl"; text = "tracion"; }; } { openmeteo = { units = "metric"; latitude = "{{HOMEPAGE_VAR_LATITUDE}}"; longitude = "{{HOMEPAGE_VAR_LONGITUDE}}"; cache = 5; }; } { datetime = { text_size = "xl"; locale = "en"; format = { dateStyle = "full"; timeStyle = "medium"; hour12 = false; }; }; } ]; settings = { title = "Tracion"; theme = "dark"; color = "slate"; showStats = false; hideVersion = true; bookmarksStyle = "icons"; useEqualHeights = true; layout = { Apps = { style = "row"; columns = 1; tab = "Apps"; Media = { style = "row"; columns = 2; }; Files = { style = "row"; columns = 2; }; Other = { style = "row"; columns = 3; iconsOnly = true; }; }; Download = { style = "row"; columns = 1; tab = "Download"; }; System = { tab = "System"; }; }; }; }; }; }