From 41061afe5cd38173d5fbdf087db2aba56f0e46b1 Mon Sep 17 00:00:00 2001 From: ibizaman Date: Mon, 23 Feb 2026 22:36:27 +0100 Subject: [PATCH] homepage: init with dashboard contract --- docs/contracts.md | 14 +- docs/recipes/exposeService.md | 26 + docs/redirects.json | 574 +++++++++++++- docs/services.md | 12 +- flake.nix | 15 + lib/default.nix | 743 +++++++++--------- lib/homepage.nix | 92 +++ modules/blocks/authelia.nix | 14 + modules/blocks/authelia/docs/default.md | 24 +- modules/blocks/lldap.nix | 14 + modules/blocks/lldap/docs/default.md | 28 +- modules/blocks/monitoring.nix | 15 + modules/blocks/monitoring/docs/default.md | 19 + modules/contracts/backup/docs/default.md | 4 +- modules/contracts/dashboard.nix | 77 ++ modules/contracts/dashboard/docs/default.md | 65 ++ modules/contracts/dashboard/dummyModule.nix | 30 + modules/contracts/default.nix | 33 +- modules/services/arr.nix | 14 + modules/services/arr/docs/default.md | 232 +++++- modules/services/audiobookshelf.nix | 14 + modules/services/deluge.nix | 16 +- modules/services/firefly-iii.nix | 19 + modules/services/firefly-iii/docs/default.md | 38 + modules/services/forgejo.nix | 15 + modules/services/forgejo/docs/default.md | 17 + modules/services/grocy.nix | 15 + modules/services/hledger.nix | 15 + modules/services/home-assistant.nix | 15 + .../services/home-assistant/docs/default.md | 52 ++ modules/services/homepage.nix | 286 +++++++ modules/services/homepage/docs/Screenshot.png | Bin 0 -> 130526 bytes modules/services/homepage/docs/default.md | 191 +++++ modules/services/immich.nix | 14 + modules/services/jellyfin.nix | 14 + modules/services/jellyfin/docs/default.md | 32 + modules/services/karakeep.nix | 14 + modules/services/karakeep/docs/default.md | 32 + modules/services/nextcloud-server.nix | 15 + .../services/nextcloud-server/docs/default.md | 22 +- modules/services/open-webui.nix | 14 + modules/services/open-webui/docs/default.md | 22 + modules/services/paperless.nix | 16 +- modules/services/pinchflat.nix | 15 +- modules/services/pinchflat/docs/default.md | 24 +- modules/services/vaultwarden.nix | 14 + modules/services/vaultwarden/docs/default.md | 18 +- test/modules/homepage.nix | 269 +++++++ test/modules/lib.nix | 1 + test/services/homepage.nix | 209 +++++ 50 files changed, 3072 insertions(+), 411 deletions(-) create mode 100644 lib/homepage.nix create mode 100644 modules/contracts/dashboard.nix create mode 100644 modules/contracts/dashboard/docs/default.md create mode 100644 modules/contracts/dashboard/dummyModule.nix create mode 100644 modules/services/homepage.nix create mode 100644 modules/services/homepage/docs/Screenshot.png create mode 100644 modules/services/homepage/docs/default.md create mode 100644 test/modules/homepage.nix create mode 100644 test/services/homepage.nix diff --git a/docs/contracts.md b/docs/contracts.md index dc7a2e9..dbb4b0f 100644 --- a/docs/contracts.md +++ b/docs/contracts.md @@ -37,13 +37,17 @@ Provided contracts are: - [SSL generator contract](contracts-ssl.html) to generate SSL certificates. Two providers are implemented: self-signed and Let's Encrypt. - [Backup contract][] to backup directories. - One provider is implemented: [Restic][]. + Two providers are implemented: [BorgBackup][] and [Restic][]. - [Database Backup contract](contracts-databasebackup.html) to backup database dumps. - One provider is implemented: [Restic][]. -- [Secret contract](contracts-secret.html) to provide secrets that are deployed outside of the Nix store. + One provider is implemented: [BorgBackup][] and [Restic][]. +- [Contract for Secrets](contracts-secret.html) to provide secrets that are deployed outside of the Nix store. One provider is implemented: [SOPS][]. +- [Dashboard contract](contracts-dashboard.html) to show services in a nice user-facing dashboard. + One provider is implemented: [Homepage][]. [backup contract]: contracts-backup.html +[borgbackup]: blocks-borgbackup.html +[homepage]: services-homepage.html [restic]: blocks-restic.html [sops]: blocks-sops.html @@ -63,6 +67,10 @@ modules/contracts/databasebackup/docs/default.md modules/contracts/secret/docs/default.md ``` +```{=include=} chapters html:into-file=//contracts-dashboard.html +modules/contracts/dashboard/docs/default.md +``` + ## Problem Statement {#contracts-why} Currently in nixpkgs, every module accessing a shared resource diff --git a/docs/recipes/exposeService.md b/docs/recipes/exposeService.md index d4724c1..7f0f6be 100644 --- a/docs/recipes/exposeService.md +++ b/docs/recipes/exposeService.md @@ -128,3 +128,29 @@ shb.sops.secret."awesome" = { request = config.shb.restic.instances.awesome.settings.passphrase.request; }; ``` + +## Impermanence {#recipes-exposeService-impermanence} + +To save the data folder in an impermanence setup, add: + +```nix +{ + shb.zfs.datasets."safe/awesome".path = config.services.awesome.dataDir; +} +``` + +## Application Dashboard {#recipes-exposeService-applicationdashboard} + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.MyServices.services.Awesome = { + sortOrder = 1; + dashboard.request = { + externalUrl = "https://${fqdn}"; + internalUrl = "http://127.0.0.1:${toString listenPort}"; + }; + }; +} +``` diff --git a/docs/redirects.json b/docs/redirects.json index 0e6e87f..10e5030 100644 --- a/docs/redirects.json +++ b/docs/redirects.json @@ -53,9 +53,6 @@ "blocks-authelia-forward-auth": [ "blocks-authelia.html#blocks-authelia-forward-auth" ], - "blocks-authelia-global-setup": [ - "blocks-authelia.html#blocks-authelia-global-setup" - ], "blocks-authelia-oidc": [ "blocks-authelia.html#blocks-authelia-oidc" ], @@ -65,6 +62,21 @@ "blocks-authelia-options-shb.authelia.autheliaUser": [ "blocks-authelia.html#blocks-authelia-options-shb.authelia.autheliaUser" ], + "blocks-authelia-options-shb.authelia.dashboard": [ + "blocks-authelia.html#blocks-authelia-options-shb.authelia.dashboard" + ], + "blocks-authelia-options-shb.authelia.dashboard.request": [ + "blocks-authelia.html#blocks-authelia-options-shb.authelia.dashboard.request" + ], + "blocks-authelia-options-shb.authelia.dashboard.request.externalUrl": [ + "blocks-authelia.html#blocks-authelia-options-shb.authelia.dashboard.request.externalUrl" + ], + "blocks-authelia-options-shb.authelia.dashboard.request.internalUrl": [ + "blocks-authelia.html#blocks-authelia-options-shb.authelia.dashboard.request.internalUrl" + ], + "blocks-authelia-options-shb.authelia.dashboard.result": [ + "blocks-authelia.html#blocks-authelia-options-shb.authelia.dashboard.result" + ], "blocks-authelia-options-shb.authelia.dcdomain": [ "blocks-authelia.html#blocks-authelia-options-shb.authelia.dcdomain" ], @@ -323,6 +335,9 @@ "blocks-authelia-troubleshooting": [ "blocks-authelia.html#blocks-authelia-troubleshooting" ], + "blocks-authelia-usage-configuration": [ + "blocks-authelia.html#blocks-authelia-usage-configuration" + ], "blocks-borgbackup": [ "blocks-borgbackup.html#blocks-borgbackup" ], @@ -578,8 +593,8 @@ "blocks-lldap": [ "blocks-lldap.html#blocks-lldap" ], - "blocks-lldap-global-setup": [ - "blocks-lldap.html#blocks-lldap-global-setup" + "blocks-lldap-features": [ + "blocks-lldap.html#blocks-lldap-features" ], "blocks-lldap-manage-groups": [ "blocks-lldap.html#blocks-lldap-manage-groups" @@ -623,6 +638,21 @@ "blocks-lldap-options-shb.lldap.backup.result.restoreScript": [ "blocks-lldap.html#blocks-lldap-options-shb.lldap.backup.result.restoreScript" ], + "blocks-lldap-options-shb.lldap.dashboard": [ + "blocks-lldap.html#blocks-lldap-options-shb.lldap.dashboard" + ], + "blocks-lldap-options-shb.lldap.dashboard.request": [ + "blocks-lldap.html#blocks-lldap-options-shb.lldap.dashboard.request" + ], + "blocks-lldap-options-shb.lldap.dashboard.request.externalUrl": [ + "blocks-lldap.html#blocks-lldap-options-shb.lldap.dashboard.request.externalUrl" + ], + "blocks-lldap-options-shb.lldap.dashboard.request.internalUrl": [ + "blocks-lldap.html#blocks-lldap-options-shb.lldap.dashboard.request.internalUrl" + ], + "blocks-lldap-options-shb.lldap.dashboard.result": [ + "blocks-lldap.html#blocks-lldap-options-shb.lldap.dashboard.result" + ], "blocks-lldap-options-shb.lldap.dcdomain": [ "blocks-lldap.html#blocks-lldap-options-shb.lldap.dcdomain" ], @@ -824,18 +854,27 @@ "blocks-lldap-options-shb.lldap.webUIListenPort": [ "blocks-lldap.html#blocks-lldap-options-shb.lldap.webUIListenPort" ], - "blocks-lldap-restrict-access-by-ip": [ - "blocks-lldap.html#blocks-lldap-restrict-access-by-ip" - ], - "blocks-lldap-ssl": [ - "blocks-lldap.html#blocks-lldap-ssl" - ], "blocks-lldap-tests": [ "blocks-lldap.html#blocks-lldap-tests" ], "blocks-lldap-troubleshooting": [ "blocks-lldap.html#blocks-lldap-troubleshooting" ], + "blocks-lldap-usage": [ + "blocks-lldap.html#blocks-lldap-usage" + ], + "blocks-lldap-usage-applicationdashboard": [ + "blocks-lldap.html#blocks-lldap-usage-applicationdashboard" + ], + "blocks-lldap-usage-configuration": [ + "blocks-lldap.html#blocks-lldap-usage-configuration" + ], + "blocks-lldap-usage-restrict-access-by-ip": [ + "blocks-lldap.html#blocks-lldap-usage-restrict-access-by-ip" + ], + "blocks-lldap-usage-ssl": [ + "blocks-lldap.html#blocks-lldap-usage-ssl" + ], "blocks-mitmdump": [ "blocks-mitmdump.html#blocks-mitmdump" ], @@ -953,6 +992,21 @@ "blocks-monitoring-options-shb.monitoring.contactPoints": [ "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.contactPoints" ], + "blocks-monitoring-options-shb.monitoring.dashboard": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.dashboard" + ], + "blocks-monitoring-options-shb.monitoring.dashboard.request": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.dashboard.request" + ], + "blocks-monitoring-options-shb.monitoring.dashboard.request.externalUrl": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.dashboard.request.externalUrl" + ], + "blocks-monitoring-options-shb.monitoring.dashboard.request.internalUrl": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.dashboard.request.internalUrl" + ], + "blocks-monitoring-options-shb.monitoring.dashboard.result": [ + "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.dashboard.result" + ], "blocks-monitoring-options-shb.monitoring.dashboards": [ "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.dashboards" ], @@ -1133,6 +1187,12 @@ "blocks-monitoring-usage": [ "blocks-monitoring.html#blocks-monitoring-usage" ], + "blocks-monitoring-usage-applicationdashboard": [ + "blocks-monitoring.html#blocks-monitoring-usage-applicationdashboard" + ], + "blocks-monitoring-usage-configuration": [ + "blocks-monitoring.html#blocks-monitoring-usage-configuration" + ], "blocks-monitoring-usage-log-optimization": [ "blocks-monitoring.html#blocks-monitoring-usage-log-optimization" ], @@ -1712,6 +1772,15 @@ "contract-backup-usage": [ "contracts-backup.html#contract-backup-usage" ], + "contract-dashboard": [ + "contracts-dashboard.html#contract-dashboard" + ], + "contract-dashboard-options": [ + "contracts-dashboard.html#contract-dashboard-options" + ], + "contract-dashboard-providers": [ + "contracts-dashboard.html#contract-dashboard-providers" + ], "contract-databasebackup": [ "contracts-databasebackup.html#contract-databasebackup" ], @@ -1805,6 +1874,27 @@ "contracts-concept": [ "contracts.html#contracts-concept" ], + "contracts-dashboard-options-shb.contracts.dashboard": [ + "contracts-dashboard.html#contracts-dashboard-options-shb.contracts.dashboard" + ], + "contracts-dashboard-options-shb.contracts.dashboard.request": [ + "contracts-dashboard.html#contracts-dashboard-options-shb.contracts.dashboard.request" + ], + "contracts-dashboard-options-shb.contracts.dashboard.request.externalUrl": [ + "contracts-dashboard.html#contracts-dashboard-options-shb.contracts.dashboard.request.externalUrl" + ], + "contracts-dashboard-options-shb.contracts.dashboard.request.internalUrl": [ + "contracts-dashboard.html#contracts-dashboard-options-shb.contracts.dashboard.request.internalUrl" + ], + "contracts-dashboard-options-shb.contracts.dashboard.result": [ + "contracts-dashboard.html#contracts-dashboard-options-shb.contracts.dashboard.result" + ], + "contracts-dashboard-options-shb.contracts.dashboard.settings": [ + "contracts-dashboard.html#contracts-dashboard-options-shb.contracts.dashboard.settings" + ], + "contracts-dashboard-usage": [ + "contracts-dashboard.html#contracts-dashboard-usage" + ], "contracts-databasebackup-options-shb.contracts.databasebackup": [ "contracts-databasebackup.html#contracts-databasebackup-options-shb.contracts.databasebackup" ], @@ -2156,12 +2246,18 @@ "recipes-exposeService": [ "recipes-exposeService.html#recipes-exposeService" ], + "recipes-exposeService-applicationdashboard": [ + "recipes-exposeService.html#recipes-exposeService-applicationdashboard" + ], "recipes-exposeService-backup": [ "recipes-exposeService.html#recipes-exposeService-backup" ], "recipes-exposeService-debug": [ "recipes-exposeService.html#recipes-exposeService-debug" ], + "recipes-exposeService-impermanence": [ + "recipes-exposeService.html#recipes-exposeService-impermanence" + ], "recipes-exposeService-ldap": [ "recipes-exposeService.html#recipes-exposeService-ldap" ], @@ -2216,6 +2312,9 @@ "services-arr": [ "services-arr.html#services-arr" ], + "services-arr-features": [ + "services-arr.html#services-arr-features" + ], "services-arr-options": [ "services-arr.html#services-arr-options" ], @@ -2258,6 +2357,21 @@ "services-arr-options-shb.arr.bazarr.backup.result.restoreScript": [ "services-arr.html#services-arr-options-shb.arr.bazarr.backup.result.restoreScript" ], + "services-arr-options-shb.arr.bazarr.dashboard": [ + "services-arr.html#services-arr-options-shb.arr.bazarr.dashboard" + ], + "services-arr-options-shb.arr.bazarr.dashboard.request": [ + "services-arr.html#services-arr-options-shb.arr.bazarr.dashboard.request" + ], + "services-arr-options-shb.arr.bazarr.dashboard.request.externalUrl": [ + "services-arr.html#services-arr-options-shb.arr.bazarr.dashboard.request.externalUrl" + ], + "services-arr-options-shb.arr.bazarr.dashboard.request.internalUrl": [ + "services-arr.html#services-arr-options-shb.arr.bazarr.dashboard.request.internalUrl" + ], + "services-arr-options-shb.arr.bazarr.dashboard.result": [ + "services-arr.html#services-arr-options-shb.arr.bazarr.dashboard.result" + ], "services-arr-options-shb.arr.bazarr.dataDir": [ "services-arr.html#services-arr-options-shb.arr.bazarr.dataDir" ], @@ -2273,6 +2387,15 @@ "services-arr-options-shb.arr.bazarr.settings": [ "services-arr.html#services-arr-options-shb.arr.bazarr.settings" ], + "services-arr-options-shb.arr.bazarr.settings.ApiKey": [ + "services-arr.html#services-arr-options-shb.arr.bazarr.settings.ApiKey" + ], + "services-arr-options-shb.arr.bazarr.settings.ApiKey.source": [ + "services-arr.html#services-arr-options-shb.arr.bazarr.settings.ApiKey.source" + ], + "services-arr-options-shb.arr.bazarr.settings.ApiKey.transform": [ + "services-arr.html#services-arr-options-shb.arr.bazarr.settings.ApiKey.transform" + ], "services-arr-options-shb.arr.bazarr.settings.LogLevel": [ "services-arr.html#services-arr-options-shb.arr.bazarr.settings.LogLevel" ], @@ -2336,6 +2459,21 @@ "services-arr-options-shb.arr.jackett.backup.result.restoreScript": [ "services-arr.html#services-arr-options-shb.arr.jackett.backup.result.restoreScript" ], + "services-arr-options-shb.arr.jackett.dashboard": [ + "services-arr.html#services-arr-options-shb.arr.jackett.dashboard" + ], + "services-arr-options-shb.arr.jackett.dashboard.request": [ + "services-arr.html#services-arr-options-shb.arr.jackett.dashboard.request" + ], + "services-arr-options-shb.arr.jackett.dashboard.request.externalUrl": [ + "services-arr.html#services-arr-options-shb.arr.jackett.dashboard.request.externalUrl" + ], + "services-arr-options-shb.arr.jackett.dashboard.request.internalUrl": [ + "services-arr.html#services-arr-options-shb.arr.jackett.dashboard.request.internalUrl" + ], + "services-arr-options-shb.arr.jackett.dashboard.result": [ + "services-arr.html#services-arr-options-shb.arr.jackett.dashboard.result" + ], "services-arr-options-shb.arr.jackett.dataDir": [ "services-arr.html#services-arr-options-shb.arr.jackett.dataDir" ], @@ -2441,6 +2579,21 @@ "services-arr-options-shb.arr.lidarr.backup.result.restoreScript": [ "services-arr.html#services-arr-options-shb.arr.lidarr.backup.result.restoreScript" ], + "services-arr-options-shb.arr.lidarr.dashboard": [ + "services-arr.html#services-arr-options-shb.arr.lidarr.dashboard" + ], + "services-arr-options-shb.arr.lidarr.dashboard.request": [ + "services-arr.html#services-arr-options-shb.arr.lidarr.dashboard.request" + ], + "services-arr-options-shb.arr.lidarr.dashboard.request.externalUrl": [ + "services-arr.html#services-arr-options-shb.arr.lidarr.dashboard.request.externalUrl" + ], + "services-arr-options-shb.arr.lidarr.dashboard.request.internalUrl": [ + "services-arr.html#services-arr-options-shb.arr.lidarr.dashboard.request.internalUrl" + ], + "services-arr-options-shb.arr.lidarr.dashboard.result": [ + "services-arr.html#services-arr-options-shb.arr.lidarr.dashboard.result" + ], "services-arr-options-shb.arr.lidarr.dataDir": [ "services-arr.html#services-arr-options-shb.arr.lidarr.dataDir" ], @@ -2456,6 +2609,15 @@ "services-arr-options-shb.arr.lidarr.settings": [ "services-arr.html#services-arr-options-shb.arr.lidarr.settings" ], + "services-arr-options-shb.arr.lidarr.settings.ApiKey": [ + "services-arr.html#services-arr-options-shb.arr.lidarr.settings.ApiKey" + ], + "services-arr-options-shb.arr.lidarr.settings.ApiKey.source": [ + "services-arr.html#services-arr-options-shb.arr.lidarr.settings.ApiKey.source" + ], + "services-arr-options-shb.arr.lidarr.settings.ApiKey.transform": [ + "services-arr.html#services-arr-options-shb.arr.lidarr.settings.ApiKey.transform" + ], "services-arr-options-shb.arr.lidarr.settings.LogLevel": [ "services-arr.html#services-arr-options-shb.arr.lidarr.settings.LogLevel" ], @@ -2519,6 +2681,21 @@ "services-arr-options-shb.arr.radarr.backup.result.restoreScript": [ "services-arr.html#services-arr-options-shb.arr.radarr.backup.result.restoreScript" ], + "services-arr-options-shb.arr.radarr.dashboard": [ + "services-arr.html#services-arr-options-shb.arr.radarr.dashboard" + ], + "services-arr-options-shb.arr.radarr.dashboard.request": [ + "services-arr.html#services-arr-options-shb.arr.radarr.dashboard.request" + ], + "services-arr-options-shb.arr.radarr.dashboard.request.externalUrl": [ + "services-arr.html#services-arr-options-shb.arr.radarr.dashboard.request.externalUrl" + ], + "services-arr-options-shb.arr.radarr.dashboard.request.internalUrl": [ + "services-arr.html#services-arr-options-shb.arr.radarr.dashboard.request.internalUrl" + ], + "services-arr-options-shb.arr.radarr.dashboard.result": [ + "services-arr.html#services-arr-options-shb.arr.radarr.dashboard.result" + ], "services-arr-options-shb.arr.radarr.dataDir": [ "services-arr.html#services-arr-options-shb.arr.radarr.dataDir" ], @@ -2609,6 +2786,21 @@ "services-arr-options-shb.arr.readarr.backup.result.restoreScript": [ "services-arr.html#services-arr-options-shb.arr.readarr.backup.result.restoreScript" ], + "services-arr-options-shb.arr.readarr.dashboard": [ + "services-arr.html#services-arr-options-shb.arr.readarr.dashboard" + ], + "services-arr-options-shb.arr.readarr.dashboard.request": [ + "services-arr.html#services-arr-options-shb.arr.readarr.dashboard.request" + ], + "services-arr-options-shb.arr.readarr.dashboard.request.externalUrl": [ + "services-arr.html#services-arr-options-shb.arr.readarr.dashboard.request.externalUrl" + ], + "services-arr-options-shb.arr.readarr.dashboard.request.internalUrl": [ + "services-arr.html#services-arr-options-shb.arr.readarr.dashboard.request.internalUrl" + ], + "services-arr-options-shb.arr.readarr.dashboard.result": [ + "services-arr.html#services-arr-options-shb.arr.readarr.dashboard.result" + ], "services-arr-options-shb.arr.readarr.dataDir": [ "services-arr.html#services-arr-options-shb.arr.readarr.dataDir" ], @@ -2624,6 +2816,15 @@ "services-arr-options-shb.arr.readarr.settings": [ "services-arr.html#services-arr-options-shb.arr.readarr.settings" ], + "services-arr-options-shb.arr.readarr.settings.ApiKey": [ + "services-arr.html#services-arr-options-shb.arr.readarr.settings.ApiKey" + ], + "services-arr-options-shb.arr.readarr.settings.ApiKey.source": [ + "services-arr.html#services-arr-options-shb.arr.readarr.settings.ApiKey.source" + ], + "services-arr-options-shb.arr.readarr.settings.ApiKey.transform": [ + "services-arr.html#services-arr-options-shb.arr.readarr.settings.ApiKey.transform" + ], "services-arr-options-shb.arr.readarr.settings.LogLevel": [ "services-arr.html#services-arr-options-shb.arr.readarr.settings.LogLevel" ], @@ -2687,6 +2888,21 @@ "services-arr-options-shb.arr.sonarr.backup.result.restoreScript": [ "services-arr.html#services-arr-options-shb.arr.sonarr.backup.result.restoreScript" ], + "services-arr-options-shb.arr.sonarr.dashboard": [ + "services-arr.html#services-arr-options-shb.arr.sonarr.dashboard" + ], + "services-arr-options-shb.arr.sonarr.dashboard.request": [ + "services-arr.html#services-arr-options-shb.arr.sonarr.dashboard.request" + ], + "services-arr-options-shb.arr.sonarr.dashboard.request.externalUrl": [ + "services-arr.html#services-arr-options-shb.arr.sonarr.dashboard.request.externalUrl" + ], + "services-arr-options-shb.arr.sonarr.dashboard.request.internalUrl": [ + "services-arr.html#services-arr-options-shb.arr.sonarr.dashboard.request.internalUrl" + ], + "services-arr-options-shb.arr.sonarr.dashboard.result": [ + "services-arr.html#services-arr-options-shb.arr.sonarr.dashboard.result" + ], "services-arr-options-shb.arr.sonarr.dataDir": [ "services-arr.html#services-arr-options-shb.arr.sonarr.dataDir" ], @@ -2735,6 +2951,30 @@ "services-arr-options-shb.arr.sonarr.subdomain": [ "services-arr.html#services-arr-options-shb.arr.sonarr.subdomain" ], + "services-arr-usage": [ + "services-arr.html#services-arr-usage" + ], + "services-arr-usage-apikeys": [ + "services-arr.html#services-arr-usage-apikeys" + ], + "services-arr-usage-applicationdashboard": [ + "services-arr.html#services-arr-usage-applicationdashboard" + ], + "services-arr-usage-configuration": [ + "services-arr.html#services-arr-usage-configuration" + ], + "services-arr-usage-jackett-proxy": [ + "services-arr.html#services-arr-usage-jackett-proxy" + ], + "services-authelia-features": [ + "blocks-authelia.html#services-authelia-features" + ], + "services-authelia-usage": [ + "blocks-authelia.html#services-authelia-usage" + ], + "services-authelia-usage-applicationdashboard": [ + "blocks-authelia.html#services-authelia-usage-applicationdashboard" + ], "services-category-ai": [ "services.html#services-category-ai" ], @@ -2744,6 +2984,9 @@ "services-category-code": [ "services.html#services-category-code" ], + "services-category-dashboard": [ + "services.html#services-category-dashboard" + ], "services-category-documents": [ "services.html#services-category-documents" ], @@ -2771,6 +3014,9 @@ "services-firefly-iii-declarative-ldap": [ "services-firefly-iii.html#services-firefly-iii-declarative-ldap" ], + "services-firefly-iii-features": [ + "services-firefly-iii.html#services-firefly-iii-features" + ], "services-firefly-iii-impermanence": [ "services-firefly-iii.html#services-firefly-iii-impermanence" ], @@ -2837,6 +3083,21 @@ "services-firefly-iii-options-shb.firefly-iii.backup.result.restoreScript": [ "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.backup.result.restoreScript" ], + "services-firefly-iii-options-shb.firefly-iii.dashboard": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dashboard" + ], + "services-firefly-iii-options-shb.firefly-iii.dashboard.request": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dashboard.request" + ], + "services-firefly-iii-options-shb.firefly-iii.dashboard.request.externalUrl": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dashboard.request.externalUrl" + ], + "services-firefly-iii-options-shb.firefly-iii.dashboard.request.internalUrl": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dashboard.request.internalUrl" + ], + "services-firefly-iii-options-shb.firefly-iii.dashboard.result": [ + "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dashboard.result" + ], "services-firefly-iii-options-shb.firefly-iii.dbPassword": [ "services-firefly-iii.html#services-firefly-iii-options-shb.firefly-iii.dbPassword" ], @@ -3050,9 +3311,15 @@ "services-firefly-iii-usage": [ "services-firefly-iii.html#services-firefly-iii-usage" ], + "services-firefly-iii-usage-applicationdashboard": [ + "services-firefly-iii.html#services-firefly-iii-usage-applicationdashboard" + ], "services-firefly-iii-usage-backup": [ "services-firefly-iii.html#services-firefly-iii-usage-backup" ], + "services-firefly-iii-usage-configuration": [ + "services-firefly-iii.html#services-firefly-iii-usage-configuration" + ], "services-forgejo": [ "services-forgejo.html#services-forgejo" ], @@ -3098,6 +3365,21 @@ "services-forgejo-options-shb.forgejo.backup.result.restoreScript": [ "services-forgejo.html#services-forgejo-options-shb.forgejo.backup.result.restoreScript" ], + "services-forgejo-options-shb.forgejo.dashboard": [ + "services-forgejo.html#services-forgejo-options-shb.forgejo.dashboard" + ], + "services-forgejo-options-shb.forgejo.dashboard.request": [ + "services-forgejo.html#services-forgejo-options-shb.forgejo.dashboard.request" + ], + "services-forgejo-options-shb.forgejo.dashboard.request.externalUrl": [ + "services-forgejo.html#services-forgejo-options-shb.forgejo.dashboard.request.externalUrl" + ], + "services-forgejo-options-shb.forgejo.dashboard.request.internalUrl": [ + "services-forgejo.html#services-forgejo-options-shb.forgejo.dashboard.request.internalUrl" + ], + "services-forgejo-options-shb.forgejo.dashboard.result": [ + "services-forgejo.html#services-forgejo-options-shb.forgejo.dashboard.result" + ], "services-forgejo-options-shb.forgejo.databasePassword": [ "services-forgejo.html#services-forgejo-options-shb.forgejo.databasePassword" ], @@ -3359,6 +3641,9 @@ "services-forgejo-usage": [ "services-forgejo.html#services-forgejo-usage" ], + "services-forgejo-usage-applicationdashboard": [ + "services-forgejo.html#services-forgejo-usage-applicationdashboard" + ], "services-forgejo-usage-backup": [ "services-forgejo.html#services-forgejo-usage-backup" ], @@ -3446,6 +3731,21 @@ "services-home-assistant-options-shb.home-assistant.config.unit_system": [ "services-home-assistant.html#services-home-assistant-options-shb.home-assistant.config.unit_system" ], + "services-home-assistant-options-shb.home-assistant.dashboard": [ + "services-home-assistant.html#services-home-assistant-options-shb.home-assistant.dashboard" + ], + "services-home-assistant-options-shb.home-assistant.dashboard.request": [ + "services-home-assistant.html#services-home-assistant-options-shb.home-assistant.dashboard.request" + ], + "services-home-assistant-options-shb.home-assistant.dashboard.request.externalUrl": [ + "services-home-assistant.html#services-home-assistant-options-shb.home-assistant.dashboard.request.externalUrl" + ], + "services-home-assistant-options-shb.home-assistant.dashboard.request.internalUrl": [ + "services-home-assistant.html#services-home-assistant-options-shb.home-assistant.dashboard.request.internalUrl" + ], + "services-home-assistant-options-shb.home-assistant.dashboard.result": [ + "services-home-assistant.html#services-home-assistant-options-shb.home-assistant.dashboard.result" + ], "services-home-assistant-options-shb.home-assistant.domain": [ "services-home-assistant.html#services-home-assistant-options-shb.home-assistant.domain" ], @@ -3503,6 +3803,9 @@ "services-home-assistant-usage": [ "services-home-assistant.html#services-home-assistant-usage" ], + "services-home-assistant-usage-applicationdashboard": [ + "services-home-assistant.html#services-home-assistant-usage-applicationdashboard" + ], "services-home-assistant-usage-backup": [ "services-home-assistant.html#services-home-assistant-usage-backup" ], @@ -3539,6 +3842,132 @@ "services-home-assistant-usage-voice": [ "services-home-assistant.html#services-home-assistant-usage-voice" ], + "services-homepage": [ + "services-homepage.html#services-homepage" + ], + "services-homepage-features": [ + "services-homepage.html#services-homepage-features" + ], + "services-homepage-options": [ + "services-homepage.html#services-homepage-options" + ], + "services-homepage-options-shb.homepage.domain": [ + "services-homepage.html#services-homepage-options-shb.homepage.domain" + ], + "services-homepage-options-shb.homepage.enable": [ + "services-homepage.html#services-homepage-options-shb.homepage.enable" + ], + "services-homepage-options-shb.homepage.ldap": [ + "services-homepage.html#services-homepage-options-shb.homepage.ldap" + ], + "services-homepage-options-shb.homepage.ldap.userGroup": [ + "services-homepage.html#services-homepage-options-shb.homepage.ldap.userGroup" + ], + "services-homepage-options-shb.homepage.servicesGroups": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.name": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.name" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request.group": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request.group" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request.mode": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request.mode" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request.owner": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request.owner" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request.restartUnits": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.request.restartUnits" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.result": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.result" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.result.path": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.apiKey.result.path" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard.request": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard.request" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard.request.externalUrl": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard.request.externalUrl" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard.request.internalUrl": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard.request.internalUrl" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard.result": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard.result" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.name": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.name" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.settings": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.settings" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.sortOrder": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.sortOrder" + ], + "services-homepage-options-shb.homepage.servicesGroups._name_.sortOrder": [ + "services-homepage.html#services-homepage-options-shb.homepage.servicesGroups._name_.sortOrder" + ], + "services-homepage-options-shb.homepage.ssl": [ + "services-homepage.html#services-homepage-options-shb.homepage.ssl" + ], + "services-homepage-options-shb.homepage.ssl.paths": [ + "services-homepage.html#services-homepage-options-shb.homepage.ssl.paths" + ], + "services-homepage-options-shb.homepage.ssl.paths.cert": [ + "services-homepage.html#services-homepage-options-shb.homepage.ssl.paths.cert" + ], + "services-homepage-options-shb.homepage.ssl.paths.key": [ + "services-homepage.html#services-homepage-options-shb.homepage.ssl.paths.key" + ], + "services-homepage-options-shb.homepage.ssl.systemdService": [ + "services-homepage.html#services-homepage-options-shb.homepage.ssl.systemdService" + ], + "services-homepage-options-shb.homepage.sso": [ + "services-homepage.html#services-homepage-options-shb.homepage.sso" + ], + "services-homepage-options-shb.homepage.sso.authEndpoint": [ + "services-homepage.html#services-homepage-options-shb.homepage.sso.authEndpoint" + ], + "services-homepage-options-shb.homepage.sso.authorization_policy": [ + "services-homepage.html#services-homepage-options-shb.homepage.sso.authorization_policy" + ], + "services-homepage-options-shb.homepage.sso.enable": [ + "services-homepage.html#services-homepage-options-shb.homepage.sso.enable" + ], + "services-homepage-options-shb.homepage.subdomain": [ + "services-homepage.html#services-homepage-options-shb.homepage.subdomain" + ], + "services-homepage-usage": [ + "services-homepage.html#services-homepage-usage" + ], + "services-homepage-usage-custom": [ + "services-homepage.html#services-homepage-usage-custom" + ], + "services-homepage-usage-main": [ + "services-homepage.html#services-homepage-usage-main" + ], + "services-homepage-usage-service": [ + "services-homepage.html#services-homepage-usage-service" + ], + "services-homepage-usage-widget": [ + "services-homepage.html#services-homepage-usage-widget" + ], "services-jellyfin": [ "services-jellyfin.html#services-jellyfin" ], @@ -3623,6 +4052,21 @@ "services-jellyfin-options-shb.jellyfin.backup.result.restoreScript": [ "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.backup.result.restoreScript" ], + "services-jellyfin-options-shb.jellyfin.dashboard": [ + "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.dashboard" + ], + "services-jellyfin-options-shb.jellyfin.dashboard.request": [ + "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.dashboard.request" + ], + "services-jellyfin-options-shb.jellyfin.dashboard.request.externalUrl": [ + "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.dashboard.request.externalUrl" + ], + "services-jellyfin-options-shb.jellyfin.dashboard.request.internalUrl": [ + "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.dashboard.request.internalUrl" + ], + "services-jellyfin-options-shb.jellyfin.dashboard.result": [ + "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.dashboard.result" + ], "services-jellyfin-options-shb.jellyfin.debug": [ "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.debug" ], @@ -3776,9 +4220,15 @@ "services-jellyfin-usage": [ "services-jellyfin.html#services-jellyfin-usage" ], + "services-jellyfin-usage-applicationdashboard": [ + "services-jellyfin.html#services-jellyfin-usage-applicationdashboard" + ], "services-jellyfin-usage-backup": [ "services-jellyfin.html#services-jellyfin-usage-backup" ], + "services-jellyfin-usage-configuration": [ + "services-jellyfin.html#services-jellyfin-usage-configuration" + ], "services-karakeep": [ "services-karakeep.html#services-karakeep" ], @@ -3824,6 +4274,21 @@ "services-karakeep-options-shb.karakeep.backup.result.restoreScript": [ "services-karakeep.html#services-karakeep-options-shb.karakeep.backup.result.restoreScript" ], + "services-karakeep-options-shb.karakeep.dashboard": [ + "services-karakeep.html#services-karakeep-options-shb.karakeep.dashboard" + ], + "services-karakeep-options-shb.karakeep.dashboard.request": [ + "services-karakeep.html#services-karakeep-options-shb.karakeep.dashboard.request" + ], + "services-karakeep-options-shb.karakeep.dashboard.request.externalUrl": [ + "services-karakeep.html#services-karakeep-options-shb.karakeep.dashboard.request.externalUrl" + ], + "services-karakeep-options-shb.karakeep.dashboard.request.internalUrl": [ + "services-karakeep.html#services-karakeep-options-shb.karakeep.dashboard.request.internalUrl" + ], + "services-karakeep-options-shb.karakeep.dashboard.result": [ + "services-karakeep.html#services-karakeep-options-shb.karakeep.dashboard.result" + ], "services-karakeep-options-shb.karakeep.domain": [ "services-karakeep.html#services-karakeep-options-shb.karakeep.domain" ], @@ -3974,6 +4439,12 @@ "services-karakeep-usage": [ "services-karakeep.html#services-karakeep-usage" ], + "services-karakeep-usage-applicationdashboard": [ + "services-karakeep.html#services-karakeep-usage-applicationdashboard" + ], + "services-karakeep-usage-configuration": [ + "services-karakeep.html#services-karakeep-usage-configuration" + ], "services-mailserver": [ "services-mailserver.html#services-mailserver" ], @@ -4583,6 +5054,21 @@ "services-nextcloudserver-options-shb.nextcloud.backup.result.restoreScript": [ "services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.backup.result.restoreScript" ], + "services-nextcloudserver-options-shb.nextcloud.dashboard": [ + "services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.dashboard" + ], + "services-nextcloudserver-options-shb.nextcloud.dashboard.request": [ + "services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.dashboard.request" + ], + "services-nextcloudserver-options-shb.nextcloud.dashboard.request.externalUrl": [ + "services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.dashboard.request.externalUrl" + ], + "services-nextcloudserver-options-shb.nextcloud.dashboard.request.internalUrl": [ + "services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.dashboard.request.internalUrl" + ], + "services-nextcloudserver-options-shb.nextcloud.dashboard.result": [ + "services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.dashboard.result" + ], "services-nextcloudserver-options-shb.nextcloud.dataDir": [ "services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.dataDir" ], @@ -4667,6 +5153,9 @@ "services-nextcloudserver-usage": [ "services-nextcloud.html#services-nextcloudserver-usage" ], + "services-nextcloudserver-usage-applicationdashboard": [ + "services-nextcloud.html#services-nextcloudserver-usage-applicationdashboard" + ], "services-nextcloudserver-usage-backup": [ "services-nextcloud.html#services-nextcloudserver-usage-backup" ], @@ -4754,6 +5243,21 @@ "services-open-webui-options-shb.open-webui.backup.result.restoreScript": [ "services-open-webui.html#services-open-webui-options-shb.open-webui.backup.result.restoreScript" ], + "services-open-webui-options-shb.open-webui.dashboard": [ + "services-open-webui.html#services-open-webui-options-shb.open-webui.dashboard" + ], + "services-open-webui-options-shb.open-webui.dashboard.request": [ + "services-open-webui.html#services-open-webui-options-shb.open-webui.dashboard.request" + ], + "services-open-webui-options-shb.open-webui.dashboard.request.externalUrl": [ + "services-open-webui.html#services-open-webui-options-shb.open-webui.dashboard.request.externalUrl" + ], + "services-open-webui-options-shb.open-webui.dashboard.request.internalUrl": [ + "services-open-webui.html#services-open-webui-options-shb.open-webui.dashboard.request.internalUrl" + ], + "services-open-webui-options-shb.open-webui.dashboard.result": [ + "services-open-webui.html#services-open-webui-options-shb.open-webui.dashboard.result" + ], "services-open-webui-options-shb.open-webui.domain": [ "services-open-webui.html#services-open-webui-options-shb.open-webui.domain" ], @@ -4859,12 +5363,21 @@ "services-open-webui-usage": [ "services-open-webui.html#services-open-webui-usage" ], + "services-open-webui-usage-applicationdashboard": [ + "services-open-webui.html#services-open-webui-usage-applicationdashboard" + ], "services-open-webui-usage-backup": [ "services-open-webui.html#services-open-webui-usage-backup" ], + "services-open-webui-usage-configuration": [ + "services-open-webui.html#services-open-webui-usage-configuration" + ], "services-pinchflat": [ "services-pinchflat.html#services-pinchflat" ], + "services-pinchflat-features": [ + "services-pinchflat.html#services-pinchflat-features" + ], "services-pinchflat-options": [ "services-pinchflat.html#services-pinchflat-options" ], @@ -4901,6 +5414,21 @@ "services-pinchflat-options-shb.pinchflat.backup.result.restoreScript": [ "services-pinchflat.html#services-pinchflat-options-shb.pinchflat.backup.result.restoreScript" ], + "services-pinchflat-options-shb.pinchflat.dashboard": [ + "services-pinchflat.html#services-pinchflat-options-shb.pinchflat.dashboard" + ], + "services-pinchflat-options-shb.pinchflat.dashboard.request": [ + "services-pinchflat.html#services-pinchflat-options-shb.pinchflat.dashboard.request" + ], + "services-pinchflat-options-shb.pinchflat.dashboard.request.externalUrl": [ + "services-pinchflat.html#services-pinchflat-options-shb.pinchflat.dashboard.request.externalUrl" + ], + "services-pinchflat-options-shb.pinchflat.dashboard.request.internalUrl": [ + "services-pinchflat.html#services-pinchflat-options-shb.pinchflat.dashboard.request.internalUrl" + ], + "services-pinchflat-options-shb.pinchflat.dashboard.result": [ + "services-pinchflat.html#services-pinchflat-options-shb.pinchflat.dashboard.result" + ], "services-pinchflat-options-shb.pinchflat.domain": [ "services-pinchflat.html#services-pinchflat-options-shb.pinchflat.domain" ], @@ -4982,9 +5510,15 @@ "services-pinchflat-usage": [ "services-pinchflat.html#services-pinchflat-usage" ], + "services-pinchflat-usage-applicationdashboard": [ + "services-pinchflat.html#services-pinchflat-usage-applicationdashboard" + ], "services-pinchflat-usage-backup": [ "services-pinchflat.html#services-pinchflat-usage-backup" ], + "services-pinchflat-usage-configuration": [ + "services-pinchflat.html#services-pinchflat-usage-configuration" + ], "services-vaultwarden": [ "services-vaultwarden.html#services-vaultwarden" ], @@ -5039,6 +5573,21 @@ "services-vaultwarden-options-shb.vaultwarden.backup.result.restoreScript": [ "services-vaultwarden.html#services-vaultwarden-options-shb.vaultwarden.backup.result.restoreScript" ], + "services-vaultwarden-options-shb.vaultwarden.dashboard": [ + "services-vaultwarden.html#services-vaultwarden-options-shb.vaultwarden.dashboard" + ], + "services-vaultwarden-options-shb.vaultwarden.dashboard.request": [ + "services-vaultwarden.html#services-vaultwarden-options-shb.vaultwarden.dashboard.request" + ], + "services-vaultwarden-options-shb.vaultwarden.dashboard.request.externalUrl": [ + "services-vaultwarden.html#services-vaultwarden-options-shb.vaultwarden.dashboard.request.externalUrl" + ], + "services-vaultwarden-options-shb.vaultwarden.dashboard.request.internalUrl": [ + "services-vaultwarden.html#services-vaultwarden-options-shb.vaultwarden.dashboard.request.internalUrl" + ], + "services-vaultwarden-options-shb.vaultwarden.dashboard.result": [ + "services-vaultwarden.html#services-vaultwarden-options-shb.vaultwarden.dashboard.result" + ], "services-vaultwarden-options-shb.vaultwarden.databasePassword": [ "services-vaultwarden.html#services-vaultwarden-options-shb.vaultwarden.databasePassword" ], @@ -5150,6 +5699,9 @@ "services-vaultwarden-usage": [ "services-vaultwarden.html#services-vaultwarden-usage" ], + "services-vaultwarden-usage-applicationdashboard": [ + "services-vaultwarden.html#services-vaultwarden-usage-applicationdashboard" + ], "services-vaultwarden-usage-configuration": [ "services-vaultwarden.html#services-vaultwarden-usage-configuration" ], diff --git a/docs/services.md b/docs/services.md index c4e6481..8dbf8f5 100644 --- a/docs/services.md +++ b/docs/services.md @@ -1,9 +1,9 @@ # Services {#services} -Services are usually web applications that SHB help you self-host. +Services are usually web applications that SHB help you self-host some of your data. Configuration of those is purposely made more opinionated than the upstream nixpkgs modules -in exchange for requiring less options to define. +in exchange for an uniformized configuration experience. That is possible thanks to the extensive use of blocks provided by SHB. ::: {.note} @@ -19,6 +19,7 @@ information is provided in the respective manual sections. | [Firefly-iii][] | Y (1) | Y | Y | Y | Y (2) | N | | [Forgejo][] | Y (1) | Y | Y | Y | Y (2) | N | | [Home-Assistant][] | Y (1) | Y | N | Y | Y (2) | N | +| [Homepage][] | Y (1) | Y | N | Y | Y (2) | N | | [Jellyfin][] | Y (1) | Y | Y | Y | Y (2) | N | | [Karakeep][] | Y (1) | Y | Y | Y | Y (2) | N | | [Nextcloud Server][] | Y (1) | Y | Y | Y | Y (2) | P (3) | @@ -40,6 +41,7 @@ Legend: **N**: no but WIP; **P**: partial; **Y**: yes [Firefly-iii]: services-firefly-iii.html [Forgejo]: services-forgejo.html [Home-Assistant]: services-home-assistant.html +[Homepage]: services-homepage.html [Jellyfin]: services-jellyfin.html [Karakeep]: services-karakeep.html [Nextcloud Server]: services-nextcloud.html @@ -48,6 +50,12 @@ Legend: **N**: no but WIP; **P**: partial; **Y**: yes [Simple NixOS Mailserver]: services-mailserver.html [Vaultwarden]: services-vaultwarden.html +## Dashboard {#services-category-dashboard} + +```{=include=} chapters html:into-file=//services-homepage.html +modules/services/homepage/docs/default.md +``` + ## Documents {#services-category-documents} ```{=include=} chapters html:into-file=//services-nextcloud.html diff --git a/flake.nix b/flake.nix index ec8cc6c..7ad9110 100644 --- a/flake.nix +++ b/flake.nix @@ -66,6 +66,9 @@ # The contract dummies are used to show options for contracts. contractDummyModules = [ modules/contracts/backup/dummyModule.nix + modules/contracts/dashboard/dummyModule.nix + modules/contracts/databasebackup/dummyModule.nix + modules/contracts/secret/dummyModule.nix modules/contracts/ssl/dummyModule.nix ]; in @@ -110,6 +113,7 @@ (pkgs.path + "/nixos/modules/services/misc/forgejo.nix") ]; "services/home-assistant" = ./modules/services/home-assistant.nix; + "services/homepage" = ./modules/services/homepage.nix; "services/jellyfin" = ./modules/services/jellyfin.nix; "services/karakeep" = ./modules/services/karakeep.nix; "services/mailserver" = ./modules/services/mailserver.nix; @@ -131,6 +135,14 @@ "backup" ]; }; + "contracts/dashboard" = { + module = ./modules/contracts/dashboard/dummyModule.nix; + optionRoot = [ + "shb" + "contracts" + "dashboard" + ]; + }; "contracts/databasebackup" = { module = ./modules/contracts/databasebackup/dummyModule.nix; optionRoot = [ @@ -298,6 +310,7 @@ // (vm_test "hledger" ./test/services/hledger.nix) // (vm_test "immich" ./test/services/immich.nix) // (vm_test "homeassistant" ./test/services/home-assistant.nix) + // (vm_test "homepage" ./test/services/homepage.nix) // (vm_test "jellyfin" ./test/services/jellyfin.nix) // (vm_test "karakeep" ./test/services/karakeep.nix) // (vm_test "nextcloud" ./test/services/nextcloud.nix) @@ -407,6 +420,7 @@ self.nixosModules.hledger self.nixosModules.immich self.nixosModules.home-assistant + self.nixosModules.homepage self.nixosModules.jellyfin self.nixosModules.karakeep self.nixosModules.mailserver @@ -445,6 +459,7 @@ nixosModules.hledger = modules/services/hledger.nix; nixosModules.immich = modules/services/immich.nix; nixosModules.home-assistant = modules/services/home-assistant.nix; + nixosModules.homepage = modules/services/homepage.nix; nixosModules.jellyfin = modules/services/jellyfin.nix; nixosModules.karakeep = modules/services/karakeep.nix; nixosModules.mailserver = modules/services/mailserver.nix; diff --git a/lib/default.nix b/lib/default.nix index 130856e..293f9e1 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -2,411 +2,420 @@ let inherit (builtins) isAttrs hasAttr; inherit (lib) any concatMapStringsSep concatStringsSep; -in -rec { - # Replace secrets in a file. - # - userConfig is an attrset that will produce a config file. - # - resultPath is the location the config file should have on the filesystem. - # - generator is a function taking two arguments name and value and returning path in the nix - # nix store where the - replaceSecrets = - { - userConfig, - resultPath, - generator, - user ? null, - permissions ? "u=r,g=r,o=", - }: - let - configWithTemplates = withReplacements userConfig; + shb = rec { + # Replace secrets in a file. + # - userConfig is an attrset that will produce a config file. + # - resultPath is the location the config file should have on the filesystem. + # - generator is a function taking two arguments name and value and returning path in the nix + # nix store where the + replaceSecrets = + { + userConfig, + resultPath, + generator, + user ? null, + permissions ? "u=r,g=r,o=", + }: + let + configWithTemplates = withReplacements userConfig; - nonSecretConfigFile = generator "template" configWithTemplates; + nonSecretConfigFile = generator "template" configWithTemplates; - replacements = getReplacements userConfig; - in - replaceSecretsScript { - file = nonSecretConfigFile; - inherit resultPath replacements; - inherit user permissions; - }; - - replaceSecretsFormatAdapter = format: format.generate; - replaceSecretsGeneratorAdapter = - generator: name: value: - pkgs.writeText "generator " (generator value); - toEnvVar = replaceSecretsGeneratorAdapter ( - v: (lib.generators.toINIWithGlobalSection { } { globalSection = v; }) - ); - - template = - file: newPath: replacements: - replaceSecretsScript { - inherit file replacements; - resultPath = newPath; - }; - - genReplacement = - secret: - let - t = - { - transform ? null, - ... - }: - if isNull transform then x: x else transform; - in - lib.attrsets.nameValuePair (secretName secret.name) ((t secret) "$(cat ${toString secret.source})"); - - replaceSecretsScript = - { - file, - resultPath, - replacements, - user ? null, - permissions ? "u=r,g=r,o=", - }: - let - templatePath = resultPath + ".template"; - - # We check that the files containing the secrets have the - # correct permissions for us to read them in this separate - # step. Otherwise, the $(cat ...) commands inside the sed - # replacements could fail but not fail individually but - # not fail the whole script. - checkPermissions = concatMapStringsSep "\n" ( - pattern: "cat ${pattern.source} > /dev/null" - ) replacements; - - sedPatterns = concatMapStringsSep " " (pattern: "-e \"s|${pattern.name}|${pattern.value}|\"") ( - map genReplacement replacements - ); - - sedCmd = if replacements == [ ] then "cat" else "${pkgs.gnused}/bin/sed ${sedPatterns}"; - in - '' - set -euo pipefail - - ${checkPermissions} - - mkdir -p $(dirname ${templatePath}) - ln -fs ${file} ${templatePath} - rm -f ${resultPath} - touch ${resultPath} - '' - + (lib.optionalString (user != null) '' - chown ${user} ${resultPath} - '') - + '' - ${sedCmd} ${templatePath} > ${resultPath} - chmod ${permissions} ${resultPath} - ''; - - secretFileType = lib.types.submodule { - options = { - source = lib.mkOption { - type = lib.types.path; - description = "File containing the value."; + replacements = getReplacements userConfig; + in + replaceSecretsScript { + file = nonSecretConfigFile; + inherit resultPath replacements; + inherit user permissions; }; - transform = lib.mkOption { - type = lib.types.raw; - description = "An optional function to transform the secret."; - default = null; - example = lib.literalExpression '' - v: "prefix-$${v}-suffix" - ''; + replaceSecretsFormatAdapter = format: format.generate; + replaceSecretsGeneratorAdapter = + generator: name: value: + pkgs.writeText "generator " (generator value); + toEnvVar = replaceSecretsGeneratorAdapter ( + v: (lib.generators.toINIWithGlobalSection { } { globalSection = v; }) + ); + + template = + file: newPath: replacements: + replaceSecretsScript { + inherit file replacements; + resultPath = newPath; + }; + + genReplacement = + secret: + let + t = + { + transform ? null, + ... + }: + if isNull transform then x: x else transform; + in + lib.attrsets.nameValuePair (secretName secret.name) ((t secret) "$(cat ${toString secret.source})"); + + replaceSecretsScript = + { + file, + resultPath, + replacements, + user ? null, + permissions ? "u=r,g=r,o=", + }: + let + templatePath = resultPath + ".template"; + + # We check that the files containing the secrets have the + # correct permissions for us to read them in this separate + # step. Otherwise, the $(cat ...) commands inside the sed + # replacements could fail but not fail individually but + # not fail the whole script. + checkPermissions = concatMapStringsSep "\n" ( + pattern: "cat ${pattern.source} > /dev/null" + ) replacements; + + sedPatterns = concatMapStringsSep " " (pattern: "-e \"s|${pattern.name}|${pattern.value}|\"") ( + map genReplacement replacements + ); + + sedCmd = if replacements == [ ] then "cat" else "${pkgs.gnused}/bin/sed ${sedPatterns}"; + in + '' + set -euo pipefail + + ${checkPermissions} + + mkdir -p $(dirname ${templatePath}) + ln -fs ${file} ${templatePath} + rm -f ${resultPath} + touch ${resultPath} + '' + + (lib.optionalString (user != null) '' + chown ${user} ${resultPath} + '') + + '' + ${sedCmd} ${templatePath} > ${resultPath} + chmod ${permissions} ${resultPath} + ''; + + secretFileType = lib.types.submodule { + options = { + source = lib.mkOption { + type = lib.types.path; + description = "File containing the value."; + }; + + transform = lib.mkOption { + type = lib.types.raw; + description = "An optional function to transform the secret."; + default = null; + example = lib.literalExpression '' + v: "prefix-$${v}-suffix" + ''; + }; }; }; - }; - secretName = - names: "%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) names)}%"; + secretName = + names: "%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) names)}%"; - withReplacements = - attrs: - let - valueOrReplacement = - name: value: if !(builtins.isAttrs value && value ? "source") then value else secretName name; - in - mapAttrsRecursiveCond (v: !v ? "source") valueOrReplacement attrs; + withReplacements = + attrs: + let + valueOrReplacement = + name: value: if !(builtins.isAttrs value && value ? "source") then value else secretName name; + in + mapAttrsRecursiveCond (v: !v ? "source") valueOrReplacement attrs; - getReplacements = - attrs: - let - addNameField = - name: value: - if !(builtins.isAttrs value && value ? "source") then value else value // { name = name; }; + getReplacements = + attrs: + let + addNameField = + name: value: + if !(builtins.isAttrs value && value ? "source") then value else value // { name = name; }; - secretsWithName = mapAttrsRecursiveCond (v: !v ? "source") addNameField attrs; - in - collect (v: builtins.isAttrs v && v ? "source") secretsWithName; + secretsWithName = mapAttrsRecursiveCond (v: !v ? "source") addNameField attrs; + in + collect (v: builtins.isAttrs v && v ? "source") secretsWithName; - # Inspired lib.attrsets.mapAttrsRecursiveCond but also recurses on lists. - mapAttrsRecursiveCond = - # A function, given the attribute set the recursion is currently at, determine if to recurse deeper into that attribute set. - cond: - # A function, given a list of attribute names and a value, returns a new value. - f: - # Attribute set or list to recursively map over. - set: - let - recurse = - path: val: - if builtins.isAttrs val && cond val then - lib.attrsets.mapAttrs (n: v: recurse (path ++ [ n ]) v) val - else if builtins.isList val && cond val then - lib.lists.imap0 (i: v: recurse (path ++ [ (builtins.toString i) ]) v) val - else - f path val; - in - recurse [ ] set; + # Inspired lib.attrsets.mapAttrsRecursiveCond but also recurses on lists. + mapAttrsRecursiveCond = + # A function, given the attribute set the recursion is currently at, determine if to recurse deeper into that attribute set. + cond: + # A function, given a list of attribute names and a value, returns a new value. + f: + # Attribute set or list to recursively map over. + set: + let + recurse = + path: val: + if builtins.isAttrs val && cond val then + lib.attrsets.mapAttrs (n: v: recurse (path ++ [ n ]) v) val + else if builtins.isList val && cond val then + lib.lists.imap0 (i: v: recurse (path ++ [ (builtins.toString i) ]) v) val + else + f path val; + in + recurse [ ] set; - # Like lib.attrsets.collect but also recurses on lists. - collect = - # Given an attribute's value, determine if recursion should stop. - pred: - # The attribute set to recursively collect. - attrs: - if pred attrs then - [ attrs ] - else if builtins.isAttrs attrs then - lib.lists.concatMap (collect pred) (lib.attrsets.attrValues attrs) - else if builtins.isList attrs then - lib.lists.concatMap (collect pred) attrs - else - [ ]; + # Like lib.attrsets.collect but also recurses on lists. + collect = + # Given an attribute's value, determine if recursion should stop. + pred: + # The attribute set to recursively collect. + attrs: + if pred attrs then + [ attrs ] + else if builtins.isAttrs attrs then + lib.lists.concatMap (collect pred) (lib.attrsets.attrValues attrs) + else if builtins.isList attrs then + lib.lists.concatMap (collect pred) attrs + else + [ ]; - indent = - i: str: - lib.concatMapStringsSep "\n" (x: (lib.strings.replicate i " ") + x) (lib.splitString "\n" str); + indent = + i: str: + lib.concatMapStringsSep "\n" (x: (lib.strings.replicate i " ") + x) (lib.splitString "\n" str); - # Generator for XML - formatXML = - { - enclosingRoot ? null, - }: - { - type = - with lib.types; - let - valueType = - nullOr (oneOf [ - bool - int - float - str - path - (attrsOf valueType) - (listOf valueType) - ]) - // { - description = "XML value"; - }; - in - valueType; + # Generator for XML + formatXML = + { + enclosingRoot ? null, + }: + { + type = + with lib.types; + let + valueType = + nullOr (oneOf [ + bool + int + float + str + path + (attrsOf valueType) + (listOf valueType) + ]) + // { + description = "XML value"; + }; + in + valueType; - generate = - name: value: - pkgs.callPackage ( + generate = + name: value: + pkgs.callPackage ( + { runCommand, python3 }: + runCommand "config" + { + value = builtins.toJSON (if enclosingRoot == null then value else { ${enclosingRoot} = value; }); + passAsFile = [ "value" ]; + } + ( + pkgs.writers.writePython3 "dict2xml" + { + libraries = with python3.pkgs; [ + python + dict2xml + ]; + } + '' + import os + import json + from dict2xml import dict2xml + + with open(os.environ["valuePath"]) as f: + content = json.loads(f.read()) + if content is None: + print("Could not parse env var valuePath as json") + os.exit(2) + with open(os.environ["out"], "w") as out: + out.write(dict2xml(content)) + '' + ) + ) { }; + + }; + + parseXML = + xml: + let + xmlToJsonFile = pkgs.callPackage ( { runCommand, python3 }: runCommand "config" { - value = builtins.toJSON (if enclosingRoot == null then value else { ${enclosingRoot} = value; }); - passAsFile = [ "value" ]; + inherit xml; + passAsFile = [ "xml" ]; } ( - pkgs.writers.writePython3 "dict2xml" + pkgs.writers.writePython3 "xml2json" { - libraries = with python3.pkgs; [ - python - dict2xml - ]; + libraries = with python3.pkgs; [ python ]; } '' import os import json - from dict2xml import dict2xml + from collections import ChainMap + from xml.etree import ElementTree + + + def xml_to_dict_recursive(root): + all_descendants = list(root) + if len(all_descendants) == 0: + return {root.tag: root.text} + else: + merged_dict = ChainMap(*map(xml_to_dict_recursive, all_descendants)) + return {root.tag: dict(merged_dict)} + + + with open(os.environ["xmlPath"]) as f: + root = ElementTree.XML(f.read()) + xml = xml_to_dict_recursive(root) + j = json.dumps(xml) - with open(os.environ["valuePath"]) as f: - content = json.loads(f.read()) - if content is None: - print("Could not parse env var valuePath as json") - os.exit(2) with open(os.environ["out"], "w") as out: - out.write(dict2xml(content)) + out.write(j) '' ) ) { }; + in + builtins.fromJSON (builtins.readFile xmlToJsonFile); - }; + renameAttrName = + attrset: from: to: + (lib.attrsets.filterAttrs (name: v: name == from) attrset) + // { + ${to} = attrset.${from}; + }; - parseXML = - xml: - let - xmlToJsonFile = pkgs.callPackage ( - { runCommand, python3 }: - runCommand "config" + # Taken from https://github.com/antifuchs/nix-flake-tests/blob/main/default.nix + # with a nicer diff display function. + check = + { pkgs, tests }: + let + formatValue = + val: + if (builtins.isList val || builtins.isAttrs val) then + builtins.toJSON val + else + builtins.toString val; + + resultToString = { - inherit xml; - passAsFile = [ "xml" ]; - } - ( - pkgs.writers.writePython3 "xml2json" + name, + expected, + result, + }: + builtins.readFile ( + pkgs.runCommand "nix-flake-tests-error" { - libraries = with python3.pkgs; [ python ]; + expected = formatValue expected; + result = formatValue result; + passAsFile = [ + "expected" + "result" + ]; } '' - import os - import json - from collections import ChainMap - from xml.etree import ElementTree - - - def xml_to_dict_recursive(root): - all_descendants = list(root) - if len(all_descendants) == 0: - return {root.tag: root.text} - else: - merged_dict = ChainMap(*map(xml_to_dict_recursive, all_descendants)) - return {root.tag: dict(merged_dict)} - - - with open(os.environ["xmlPath"]) as f: - root = ElementTree.XML(f.read()) - xml = xml_to_dict_recursive(root) - j = json.dumps(xml) - - with open(os.environ["out"], "w") as out: - out.write(j) + echo "${name} failed (- expected, + result)" > $out + cp ''${expectedPath} ''${expectedPath}.json + cp ''${resultPath} ''${resultPath}.json + ${pkgs.deepdiff}/bin/deep diff ''${expectedPath}.json ''${resultPath}.json >> $out '' - ) + ); + + results = pkgs.lib.runTests tests; + in + if results != [ ] then + builtins.throw (concatStringsSep "\n" (map resultToString (lib.traceValSeq results))) + else + pkgs.runCommand "nix-flake-tests-success" { } "echo > $out"; + + genConfigOutOfBandSystemd = + { + config, + configLocation, + generator, + user ? null, + permissions ? "u=r,g=r,o=", + }: + { + loadCredentials = getLoadCredentials "source" config; + preStart = lib.mkBefore (replaceSecrets { + userConfig = updateToLoadCredentials "source" "$CREDENTIALS_DIRECTORY" config; + resultPath = configLocation; + inherit generator; + inherit user permissions; + }); + }; + + updateToLoadCredentials = + sourceField: rootDir: attrs: + let + hasPlaceholderField = v: isAttrs v && hasAttr sourceField v; + + valueOrLoadCredential = + path: value: + if !(hasPlaceholderField value) then + value + else + value // { ${sourceField} = rootDir + "/" + concatStringsSep "_" path; }; + in + mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) valueOrLoadCredential attrs; + + getLoadCredentials = + sourceField: attrs: + let + hasPlaceholderField = v: isAttrs v && hasAttr sourceField v; + + addPathField = + path: value: if !(hasPlaceholderField value) then value else value // { inherit path; }; + + secretsWithPath = mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) addPathField attrs; + + allSecrets = collect (v: hasPlaceholderField v) secretsWithPath; + + genLoadCredentials = secret: "${concatStringsSep "_" secret.path}:${secret.${sourceField}}"; + in + map genLoadCredentials allSecrets; + + anyNotNull = any (x: x != null); + + mkJellyfinPlugin = + { + pname, + version, + hash, + url, + }: + pkgs.callPackage ( + { stdenv, fetchzip }: + stdenv.mkDerivation (finalAttrs: { + inherit pname version; + + src = fetchzip { + inherit url hash; + stripRoot = false; + }; + + dontBuild = true; + + installPhase = '' + mkdir $out + cp -r . $out + ''; + }) ) { }; - in - builtins.fromJSON (builtins.readFile xmlToJsonFile); - renameAttrName = - attrset: from: to: - (lib.attrsets.filterAttrs (name: v: name == from) attrset) - // { - ${to} = attrset.${from}; - }; + update = + attr: fn: attrset: + attrset // { ${attr} = fn attrset.${attr}; }; - # Taken from https://github.com/antifuchs/nix-flake-tests/blob/main/default.nix - # with a nicer diff display function. - check = - { pkgs, tests }: - let - formatValue = - val: - if (builtins.isList val || builtins.isAttrs val) then - builtins.toJSON val - else - builtins.toString val; - - resultToString = - { - name, - expected, - result, - }: - builtins.readFile ( - pkgs.runCommand "nix-flake-tests-error" - { - expected = formatValue expected; - result = formatValue result; - passAsFile = [ - "expected" - "result" - ]; - } - '' - echo "${name} failed (- expected, + result)" > $out - cp ''${expectedPath} ''${expectedPath}.json - cp ''${resultPath} ''${resultPath}.json - ${pkgs.deepdiff}/bin/deep diff ''${expectedPath}.json ''${resultPath}.json >> $out - '' - ); - - results = pkgs.lib.runTests tests; - in - if results != [ ] then - builtins.throw (concatStringsSep "\n" (map resultToString (lib.traceValSeq results))) - else - pkgs.runCommand "nix-flake-tests-success" { } "echo > $out"; - - genConfigOutOfBandSystemd = - { - config, - configLocation, - generator, - user ? null, - permissions ? "u=r,g=r,o=", - }: - { - loadCredentials = getLoadCredentials "source" config; - preStart = lib.mkBefore (replaceSecrets { - userConfig = updateToLoadCredentials "source" "$CREDENTIALS_DIRECTORY" config; - resultPath = configLocation; - inherit generator; - inherit user permissions; - }); - }; - - updateToLoadCredentials = - sourceField: rootDir: attrs: - let - hasPlaceholderField = v: isAttrs v && hasAttr sourceField v; - - valueOrLoadCredential = - path: value: - if !(hasPlaceholderField value) then - value - else - value // { ${sourceField} = rootDir + "/" + concatStringsSep "_" path; }; - in - mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) valueOrLoadCredential attrs; - - getLoadCredentials = - sourceField: attrs: - let - hasPlaceholderField = v: isAttrs v && hasAttr sourceField v; - - addPathField = - path: value: if !(hasPlaceholderField value) then value else value // { inherit path; }; - - secretsWithPath = mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) addPathField attrs; - - allSecrets = collect (v: hasPlaceholderField v) secretsWithPath; - - genLoadCredentials = secret: "${concatStringsSep "_" secret.path}:${secret.${sourceField}}"; - in - map genLoadCredentials allSecrets; - - anyNotNull = any (x: x != null); - - mkJellyfinPlugin = - { - pname, - version, - hash, - url, - }: - pkgs.callPackage ( - { stdenv, fetchzip }: - stdenv.mkDerivation (finalAttrs: { - inherit pname version; - - src = fetchzip { - inherit url hash; - stripRoot = false; - }; - - dontBuild = true; - - installPhase = '' - mkdir $out - cp -r . $out - ''; - }) - ) { }; + }; +in +shb +// { + homepage = pkgs.callPackage ./homepage.nix { inherit shb; }; } diff --git a/lib/homepage.nix b/lib/homepage.nix new file mode 100644 index 0000000..0795be2 --- /dev/null +++ b/lib/homepage.nix @@ -0,0 +1,92 @@ +{ lib, shb }: +let + sort = + attr: vs: + map (v: { ${v.name} = v.${attr}; }) ( + lib.sortOn (v: v.sortOrder) (lib.mapAttrsToList (n: v: v // { name = n; }) vs) + ); + + slufigy = builtins.replaceStrings [ "-" ] [ "_" ]; + + mkService = + groupName: serviceName: + { + request, + ... + }: + apiKey: settings: + lib.recursiveUpdate ( + { + href = request.externalUrl; + siteMonitor = if (request.internalUrl == null) then null else request.internalUrl; + icon = "sh-${lib.toLower serviceName}"; + } + // lib.optionalAttrs (apiKey != null) { + widget = { + # Duplicating because widgets call the api key various names + # and duplicating is a hacky but easy solution. + key = "{{HOMEPAGE_FILE_${slufigy groupName}_${slufigy serviceName}}}"; + password = "{{HOMEPAGE_FILE_${slufigy groupName}_${slufigy serviceName}}}"; + type = lib.toLower serviceName; + url = if (request.internalUrl != null) then request.internalUrl else request.externalUrl; + }; + } + ) settings; + + asServiceGroup = + cfg: + sort "services" ( + lib.mapAttrs ( + groupName: groupCfg: + shb.update "services" ( + services: + sort "dashboard" ( + lib.mapAttrs ( + serviceName: serviceCfg: + shb.update "dashboard" ( + dashboard: + (mkService groupName serviceName) dashboard serviceCfg.apiKey (serviceCfg.settings or { }) + ) serviceCfg + ) services + ) + ) groupCfg + ) cfg + ); + + allKeys = + cfg: + let + flat = lib.flatten ( + lib.mapAttrsToList ( + groupName: groupCfg: + lib.mapAttrsToList ( + serviceName: serviceCfg: + lib.optionalAttrs (serviceCfg.apiKey != null) { + inherit serviceName groupName; + inherit (serviceCfg.apiKey.result) path; + } + ) groupCfg.services + ) cfg + ); + + flatWithApiKey = builtins.filter (v: v != { }) flat; + in + builtins.listToAttrs ( + map ( + { + groupName, + serviceName, + path, + }: + lib.nameValuePair "${slufigy groupName}_${slufigy serviceName}" path + ) flatWithApiKey + ); +in +{ + inherit + allKeys + asServiceGroup + mkService + sort + ; +} diff --git a/modules/blocks/authelia.nix b/modules/blocks/authelia.nix index a2af7b1..ee12ed9 100644 --- a/modules/blocks/authelia.nix +++ b/modules/blocks/authelia.nix @@ -390,6 +390,20 @@ in to see exactly what Authelia receives and sends back. ''; }; + + dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = lib.types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${cfg.subdomain}.${cfg.domain}"; + externalUrlText = "https://\${config.shb.authelia.subdomain}.\${config.shb.authelia.domain}"; + internalUrl = "http://127.0.0.1:${toString listenPort}"; + }; + }; + }; }; config = lib.mkIf cfg.enable { diff --git a/modules/blocks/authelia/docs/default.md b/modules/blocks/authelia/docs/default.md index 6d26b4d..4508a24 100644 --- a/modules/blocks/authelia/docs/default.md +++ b/modules/blocks/authelia/docs/default.md @@ -12,7 +12,13 @@ as well as some extensive [troubleshooting](#blocks-authelia-troubleshooting) fe Note that forward authentication is configured with the [nginx block](blocks-nginx.html#blocks-nginx-usage-shbforwardauth). -## Global Setup {#blocks-authelia-global-setup} +## Features {#services-authelia-features} + +- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-authelia-usage-applicationdashboard) + +## Usage {#services-authelia-usage} + +### Initial Configuration {#blocks-authelia-usage-configuration} Authelia cannot work without SSL and LDAP. So setting up the Authelia block requires to setup the [SSL block][] first @@ -80,6 +86,22 @@ Crucially, the `shb.authelia.secrets.ldapAdminPasswordFile` must be the same as the `shb.lldap.ldapUserPassword` defined for the [LLDAP block][]. This is done using Sops' `key` option. +### Application Dashboard {#services-authelia-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the [dashboard option](#blocks-authelia-options-shb.authelia.dashboard). + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Admin.services.Authelia = { + sortOrder = 2; + dashboard.request = config.shb.authelia.dashboard.request; + }; +} +``` + ## SHB OIDC integration {#blocks-authelia-shb-oidc} For services [provided by SelfHostBlocks][services] that handle [OIDC integration][OIDC], diff --git a/modules/blocks/lldap.nix b/modules/blocks/lldap.nix index 15170d5..9d66a6d 100644 --- a/modules/blocks/lldap.nix +++ b/modules/blocks/lldap.nix @@ -333,6 +333,20 @@ in type = types.bool; default = true; }; + + dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = lib.types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${cfg.subdomain}.${cfg.domain}"; + externalUrlText = "https://\${config.shb.lldap.subdomain}.\${config.shb.lldap.domain}"; + internalUrl = "http://127.0.0.1:${toString cfg.webUIListenPort}"; + }; + }; + }; }; config = lib.mkIf cfg.enable { diff --git a/modules/blocks/lldap/docs/default.md b/modules/blocks/lldap/docs/default.md index ec0c723..c0b9755 100644 --- a/modules/blocks/lldap/docs/default.md +++ b/modules/blocks/lldap/docs/default.md @@ -7,7 +7,13 @@ across services. [LLDAP]: https://github.com/lldap/lldap -## Global Setup {#blocks-lldap-global-setup} +## Features {#blocks-lldap-features} + +- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#blocks-lldap-usage-applicationdashboard) + +## Usage {#blocks-lldap-usage} + +### Initial Configuration {#blocks-lldap-usage-configuration} ```nix shb.lldap = { @@ -29,7 +35,7 @@ shb.sops.secret."lldap/user_password".request = config.shb.lldap.ldapUserPasswor This assumes secrets are setup with SOPS as mentioned in [the secrets setup section](usage.html#usage-secrets) of the manual. -## SSL {#blocks-lldap-ssl} +### SSL {#blocks-lldap-usage-ssl} Using SSL is an important security practice, like always. Using the [SSL block][], the configuration to add to the one above is: @@ -44,7 +50,7 @@ shb.certs.certs.letsencrypt.${domain}.extraDomains = [ shb.lldap.ssl = config.shb.certs.certs.letsencrypt.${config.shb.lldap.domain}; ``` -## Restrict Access By IP {#blocks-lldap-restrict-access-by-ip} +### Restrict Access By IP {#blocks-lldap-usage-restrict-access-by-ip} For added security, you can restrict access to the LLDAP UI by adding the following line: @@ -53,6 +59,22 @@ by adding the following line: shb.lldap.restrictAccessIPRange = "192.168.50.0/24"; ``` +### Application Dashboard {#blocks-lldap-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the [dashboard option](#blocks-lldap-options-shb.lldap.dashboard). + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Admin.services.LLDAP = { + sortOrder = 2; + dashboard.request = config.shb.lldap.dashboard.request; + }; +} +``` + ## Manage Groups {#blocks-lldap-manage-groups} The following snippet will create group named "family" if it does not exist yet. diff --git a/modules/blocks/monitoring.nix b/modules/blocks/monitoring.nix index 4aad94b..f9da03a 100644 --- a/modules/blocks/monitoring.nix +++ b/modules/blocks/monitoring.nix @@ -247,6 +247,21 @@ in }; }; }; + + dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = lib.types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${cfg.subdomain}.${cfg.domain}"; + externalUrlText = "https://\${config.shb.monitoring.subdomain}.\${config.shb.monitoring.domain}"; + internalUrl = "https://${cfg.subdomain}.${cfg.domain}"; + internalUrlText = "https://\${config.shb.monitoring.subdomain}.\${config.shb.monitoring.domain}"; + }; + }; + }; }; config = lib.mkMerge [ diff --git a/modules/blocks/monitoring/docs/default.md b/modules/blocks/monitoring/docs/default.md index 86e325c..cbef2ab 100644 --- a/modules/blocks/monitoring/docs/default.md +++ b/modules/blocks/monitoring/docs/default.md @@ -17,9 +17,12 @@ This block sets up the monitoring stack for Self Host Blocks. It is composed of: - Registration is enabled through SSO. - Access through [subdomain](#blocks-monitoring-options-shb.monitoring.subdomain) using reverse proxy. - Access through [HTTPS](#blocks-monitoring-options-shb.monitoring.ssl) using reverse proxy. +- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#blocks-monitoring-usage-applicationdashboard) ## Usage {#blocks-monitoring-usage} +### Initial Configuration {#blocks-monitoring-usage-configuration} + The following snippet assumes a few blocks have been setup already: - the [secrets block](usage.html#usage-secrets) with SOPS, @@ -110,6 +113,22 @@ You might for example want to update the metrics retention time with: services.prometheus.retentionTime = "60d"; ``` +### Application Dashboard {#blocks-monitoring-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the [dashboard option](#blocks-monitoring-options-shb.monitoring.dashboard). + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Admin.services.Grafana = { + sortOrder = 10; + dashboard.request = config.shb.monitoring.dashboard.request; + }; +} +``` + ## Provisioning {#blocks-monitoring-provisioning} Self Host Blocks will create automatically the following resources: diff --git a/modules/contracts/backup/docs/default.md b/modules/contracts/backup/docs/default.md index 43bcec1..9c0c1eb 100644 --- a/modules/contracts/backup/docs/default.md +++ b/modules/contracts/backup/docs/default.md @@ -33,7 +33,9 @@ Here is an example module defining such a `backup` option: { options = { myservice.backup = mkOption { - type = contracts.backup.request; + type = lib.types.submodule { + options = contracts.backup.request; + }; default = { user = "myservice"; sourceDirectories = [ diff --git a/modules/contracts/dashboard.nix b/modules/contracts/dashboard.nix new file mode 100644 index 0000000..f1d3685 --- /dev/null +++ b/modules/contracts/dashboard.nix @@ -0,0 +1,77 @@ +{ lib, ... }: +let + inherit (lib) mkOption; + inherit (lib.types) + nullOr + submodule + str + ; +in +{ + mkRequest = + { + serviceName ? "", + externalUrl ? "", + externalUrlText ? null, + internalUrl ? null, + internalUrlText ? null, + apiKey ? null, + }: + mkOption { + description = '' + Request part of the dashboard contract. + ''; + default = { }; + type = submodule { + options = { + externalUrl = + mkOption { + description = '' + URL at which the service can be accessed. + + This URL should go through the reverse proxy. + ''; + type = str; + default = externalUrl; + example = "https://jellyfin.example.com"; + } + // (lib.optionalAttrs (externalUrlText != null) { + defaultText = externalUrlText; + }); + + internalUrl = + mkOption { + description = '' + URL at which the service can be accessed directly. + + This URL should bypass the reverse proxy. + It can be used for example to ping the service + and making sure it is up and running correctly. + ''; + type = nullOr str; + default = internalUrl; + example = "http://127.0.0.1:8081"; + } + // (lib.optionalAttrs (internalUrlText != null) { + defaultText = internalUrlText; + }); + }; + }; + }; + + mkResult = + { + }: + mkOption { + description = '' + Result part of the dashboard contract. + + No option is provided here. + ''; + default = { }; + type = submodule { + options = { + }; + }; + }; +} diff --git a/modules/contracts/dashboard/docs/default.md b/modules/contracts/dashboard/docs/default.md new file mode 100644 index 0000000..a4d9528 --- /dev/null +++ b/modules/contracts/dashboard/docs/default.md @@ -0,0 +1,65 @@ +# Dashboard Contract {#contract-dashboard} + +This NixOS contract is used for user-facing services +that want to be displayed on a dashboard. + +It is a contract between a service that can be accessed through an URL +and a service that wants to show a list of those services. + +## Providers {#contract-dashboard-providers} + +The providers of this contract in SHB are: + + + +- The homepage service under its [shb.homepage.servicesGroups..services..dashboard](#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard) option. + +## Usage {#contracts-dashboard-usage} + +A service that can be shown on a dashboard will provide a `dashboard` option. + +Here is an example module defining such a requester option for this dashboard contract: + +```nix +{ + options = { + myservice.dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = lib.types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${config.myservice.subdomain}.${config.myservice.domain}"; + internalUrl = "http://127.0.0.1:${config.myservice.port}"; + }; + }; + }; + }; +}; +``` + +Then, plug both consumer and provider together in the `config`: + +```nix +{ + config = { + = { + dashboard.request = config.myservice.dashboard.request; + }; + }; +} +``` + +And that's it for the contract part. +For more specific details on each provider, go to their respective manual pages. + +## Contract Reference {#contract-dashboard-options} + +These are all the options that are expected to exist for this contract to be respected. + +```{=include=} options +id-prefix: contracts-dashboard-options- +list-id: selfhostblocks-options +source: @OPTIONS_JSON@ +``` diff --git a/modules/contracts/dashboard/dummyModule.nix b/modules/contracts/dashboard/dummyModule.nix new file mode 100644 index 0000000..691ab60 --- /dev/null +++ b/modules/contracts/dashboard/dummyModule.nix @@ -0,0 +1,30 @@ +{ lib, shb, ... }: +let + inherit (lib) mkOption; + inherit (lib.types) submodule; +in +{ + imports = [ + ../../../lib/module.nix + ]; + + options.shb.contracts.dashboard = mkOption { + description = '' + Contract for user-facing services that want to + be displayed on a dashboard. + + The requester communicates to the provider + how to access the service + through the `request` options. + + The provider reads from the `request` options + and configures what is necessary on its side + to show the service and check its availability. + It does not communicate back to the requester. + ''; + + type = submodule { + options = shb.contracts.dashboard.contract; + }; + }; +} diff --git a/modules/contracts/default.nix b/modules/contracts/default.nix index e9964a8..0f90360 100644 --- a/modules/contracts/default.nix +++ b/modules/contracts/default.nix @@ -48,21 +48,28 @@ let importContract = module: let - importedModule = pkgs.callPackage module { inherit shb; }; + importedModule = pkgs.callPackage module { + shb = shb // { + inherit contracts; + }; + }; in mkContractFunctions { inherit (importedModule) mkRequest mkResult; }; -in -{ - databasebackup = importContract ./databasebackup.nix; - backup = importContract ./backup.nix; - mount = pkgs.callPackage ./mount.nix { }; - secret = importContract ./secret.nix; - ssl = pkgs.callPackage ./ssl.nix { }; - test = { - secret = pkgs.callPackage ./secret/test.nix { inherit shb; }; - databasebackup = pkgs.callPackage ./databasebackup/test.nix { inherit shb; }; - backup = pkgs.callPackage ./backup/test.nix { inherit shb; }; + + contracts = { + databasebackup = importContract ./databasebackup.nix; + dashboard = importContract ./dashboard.nix; + backup = importContract ./backup.nix; + mount = pkgs.callPackage ./mount.nix { }; + secret = importContract ./secret.nix; + ssl = pkgs.callPackage ./ssl.nix { }; + test = { + secret = pkgs.callPackage ./secret/test.nix { inherit shb; }; + databasebackup = pkgs.callPackage ./databasebackup/test.nix { inherit shb; }; + backup = pkgs.callPackage ./backup/test.nix { inherit shb; }; + }; }; -} +in +contracts diff --git a/modules/services/arr.nix b/modules/services/arr.nix index c7bc938..3d152da 100644 --- a/modules/services/arr.nix +++ b/modules/services/arr.nix @@ -392,6 +392,20 @@ let }; }; }; + + dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = lib.types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${cfg.${name}.subdomain}.${cfg.${name}.domain}"; + externalUrlText = "https://\${config.shb.arr.${name}.subdomain}.\${config.shb.arr.${name}.domain}"; + internalUrl = "http://127.0.0.1:${toString cfg.${name}.settings.Port}"; + }; + }; + }; } // (c.moreOptions or { }); }; diff --git a/modules/services/arr/docs/default.md b/modules/services/arr/docs/default.md index a9b441d..5668cf6 100644 --- a/modules/services/arr/docs/default.md +++ b/modules/services/arr/docs/default.md @@ -3,12 +3,242 @@ Defined in [`/modules/services/arr.nix`](@REPO@/modules/services/arr.nix). This NixOS module sets up multiple [Servarr](https://wiki.servarr.com/) services. +## Features {#services-arr-features} Compared to the stock module from nixpkgs, this one sets up, in a fully declarative manner LDAP and SSO integration as well as the API key. -This manual page is under construction. +## Usage {#services-arr-usage} + +### Initial Configuration {#services-arr-usage-configuration} + +The following snippet assumes a few blocks have been setup already: + +- the [secrets block](usage.html#usage-secrets) with SOPS, +- the [`shb.ssl` block](blocks-ssl.html#usage), +- the [`shb.lldap` block](blocks-lldap.html#blocks-lldap-global-setup). +- the [`shb.authelia` block](blocks-authelia.html#blocks-sso-global-setup). + +```nix +{ + shb.certs.certs.letsencrypt.${domain}.extraDomains = [ + "moviesdl.${domain}" + "seriesdl.${domain}" + "subtitlesdl.${domain}" + "booksdl.${domain}" + "musicdl.${domain}" + "indexer.${domain}" + ]; + + shb.arr = { + radarr = { + inherit domain; + enable = true; + ssl = config.shb.certs.certs.letsencrypt.${domain}; + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + }; + sonarr = { + inherit domain; + enable = true; + ssl = config.shb.certs.certs.letsencrypt."${domain}"; + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + }; + bazarr = { + inherit domain; + enable = true; + ssl = config.shb.certs.certs.letsencrypt."${domain}"; + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + }; + readarr = { + inherit domain; + enable = true; + ssl = config.shb.certs.certs.letsencrypt."${domain}"; + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + }; + lidarr = { + inherit domain; + enable = true; + ssl = config.shb.certs.certs.letsencrypt."${domain}"; + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + }; + jackett = { + inherit domain; + enable = true; + ssl = config.shb.certs.certs.letsencrypt."${domain}"; + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + }; + }; +} +``` + +The user and admin LDAP groups are created automatically. + +### API Keys {#services-arr-usage-apikeys} + +The API keys for each arr service can be created declaratively. + +First, generate one secret for each service with `nix run nixpkgs#openssl -- rand -hex 64` +and store it in your secrets file (for example the SOPS file). + +Then, add the API key to each service: + +```nix +{ + shb.arr = { + radarr = { + settings = { + ApiKey.source = config.shb.sops.secret."radarr/apikey".result.path; + }; + }; + sonarr = { + settings = { + ApiKey.source = config.shb.sops.secret."sonarr/apikey".result.path; + }; + }; + bazarr = { + settings = { + ApiKey.source = config.shb.sops.secret."bazarr/apikey".result.path; + }; + }; + readarr = { + settings = { + ApiKey.source = config.shb.sops.secret."readarr/apikey".result.path; + }; + }; + lidarr = { + settings = { + ApiKey.source = config.shb.sops.secret."lidarr/apikey".result.path; + }; + }; + jackett = { + settings = { + ApiKey.source = config.shb.sops.secret."jackett/apikey".result.path; + }; + }; + }; + + shb.sops.secret."radarr/apikey".request = { + mode = "0440"; + owner = "radarr"; + group = "radarr"; + restartUnits = [ "radarr.service" ]; + }; + shb.sops.secret."sonarr/apikey".request = { + mode = "0440"; + owner = "sonarr"; + group = "sonarr"; + restartUnits = [ "sonarr.service" ]; + }; + shb.sops.secret."bazarr/apikey".request = { + mode = "0440"; + owner = "bazarr"; + group = "bazarr"; + restartUnits = [ "bazarr.service" ]; + }; + shb.sops.secret."readarr/apikey".request = { + mode = "0440"; + owner = "readarr"; + group = "readarr"; + restartUnits = [ "readarr.service" ]; + }; + shb.sops.secret."lidarr/apikey".request = { + mode = "0440"; + owner = "lidarr"; + group = "lidarr"; + restartUnits = [ "lidarr.service" ]; + }; + shb.sops.secret."jackett/apikey".request = { + mode = "0440"; + owner = "jackett"; + group = "jackett"; + restartUnits = [ "jackett.service" ]; + }; +} +``` + +### Application Dashboard {#services-arr-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the various dashboard options. + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Media.services.Radarr = { + sortOrder = 10; + dashboard.request = config.shb.arr.radarr.dashboard.request; + apiKey.result = config.shb.sops.secret."radarr/homepageApiKey".result; + }; + shb.sops.secret."radarr/homepageApiKey" = { + settings.key = "radarr/apikey"; + request = config.shb.homepage.servicesGroups.Media.services.Radarr.apiKey.request; + }; + shb.homepage.servicesGroups.Media.services.Sonarr = { + sortOrder = 11; + dashboard.request = config.shb.arr.sonarr.dashboard.request; + apiKey.result = config.shb.sops.secret."sonarr/homepageApiKey".result; + }; + shb.sops.secret."sonarr/homepageApiKey" = { + settings.key = "sonarr/apikey"; + request = config.shb.homepage.servicesGroups.Media.services.Sonarr.apiKey.request; + }; + shb.homepage.servicesGroups.Media.services.Bazarr = { + sortOrder = 12; + dashboard.request = config.shb.arr.bazarr.dashboard.request; + apiKey.result = config.shb.sops.secret."bazarr/homepageApiKey".result; + }; + shb.sops.secret."bazarr/homepageApiKey" = { + settings.key = "bazarr/apikey"; + request = config.shb.homepage.servicesGroups.Media.services.Bazarr.apiKey.request; + }; + shb.homepage.servicesGroups.Media.services.Readarr = { + sortOrder = 13; + dashboard.request = config.shb.arr.readarr.dashboard.request; + apiKey.result = config.shb.sops.secret."readarr/homepageApiKey".result; + }; + shb.sops.secret."readarr/homepageApiKey" = { + settings.key = "readarr/apikey"; + request = config.shb.homepage.servicesGroups.Media.services.Readarr.apiKey.request; + }; + shb.homepage.servicesGroups.Media.services.Lidarr = { + sortOrder = 14; + dashboard.request = config.shb.arr.lidarr.dashboard.request; + apiKey.result = config.shb.sops.secret."lidarr/homepageApiKey".result; + }; + shb.sops.secret."lidarr/homepageApiKey" = { + settings.key = "lidarr/apikey"; + request = config.shb.homepage.servicesGroups.Media.services.Lidarr.apiKey.request; + }; + shb.homepage.servicesGroups.Media.services.Jackett = { + sortOrder = 15; + dashboard.request = config.shb.arr.jackett.dashboard.request; + apiKey.result = config.shb.sops.secret."jackett/homepageApiKey".result; + }; + shb.sops.secret."jackett/homepageApiKey" = { + settings.key = "jackett/apikey"; + request = config.shb.homepage.servicesGroups.Media.services.Jackett.apiKey.request; + }; +} +``` + +This example reuses the API keys generated declaratively from the previous section. + +### Jackett Proxy {#services-arr-usage-jackett-proxy} + +The Jackett service can be made to use a proxy with: + +```nix +{ + shb.arr.jackett = { + settings = { + ProxyType = "0"; + ProxyUrl = "127.0.0.1:1234"; + }; + }; +}; +``` ## Options Reference {#services-arr-options} diff --git a/modules/services/audiobookshelf.nix b/modules/services/audiobookshelf.nix index bcea2af..d64a6f8 100644 --- a/modules/services/audiobookshelf.nix +++ b/modules/services/audiobookshelf.nix @@ -152,6 +152,20 @@ in default = false; example = true; }; + + dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = lib.types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${cfg.subdomain}.${cfg.domain}"; + externalUrlText = "https://\${config.shb.audiobookshelf.subdomain}.\${config.shb.audiobookshelf.domain}"; + internalUrl = "http://127.0.0.1:${toString cfg.webPort}"; + }; + }; + }; }; config = lib.mkIf cfg.enable ( diff --git a/modules/services/deluge.nix b/modules/services/deluge.nix index 53f99af..f4c3ff4 100644 --- a/modules/services/deluge.nix +++ b/modules/services/deluge.nix @@ -36,7 +36,7 @@ in options.shb.deluge = { enable = lib.mkEnableOption "the SHB Deluge service"; - enableDashboard = lib.mkEnableOption "the Torrents SHB dashboard" // { + enableDashboard = lib.mkEnableOption "the Torrents SHB monitoring dashboard" // { default = true; }; @@ -301,6 +301,20 @@ in default = null; example = "info"; }; + + dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = lib.types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${fqdn}"; + externalUrlText = "https://\${config.shb.deluge.subdomain}.\${config.shb.deluge.domain}"; + internalUrl = "http://127.0.0.1:${toString cfg.webPort}"; + }; + }; + }; }; config = lib.mkIf cfg.enable ( diff --git a/modules/services/firefly-iii.nix b/modules/services/firefly-iii.nix index 45a1122..8a1b2ba 100644 --- a/modules/services/firefly-iii.nix +++ b/modules/services/firefly-iii.nix @@ -10,6 +10,9 @@ let in { imports = [ + ../blocks/nginx.nix + ../blocks/lldap.nix + ../../lib/module.nix ]; @@ -293,6 +296,22 @@ in }; }; }; + + dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = lib.types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${cfg.subdomain}.${cfg.domain}"; + externalUrlText = "https://\${config.shb.firefly-iii.subdomain}.\${config.shb.firefly-iii.domain}"; + # This works thanks to the Personal Access Token. + internalUrl = "https://${cfg.subdomain}.${cfg.domain}"; + internalUrlText = "https://\${config.shb.firefly-iii.subdomain}.\${config.shb.firefly-iii.domain}"; + }; + }; + }; }; config = lib.mkIf cfg.enable ( diff --git a/modules/services/firefly-iii/docs/default.md b/modules/services/firefly-iii/docs/default.md index 87ea3b6..c94b972 100644 --- a/modules/services/firefly-iii/docs/default.md +++ b/modules/services/firefly-iii/docs/default.md @@ -12,8 +12,14 @@ It also sets up the Firefly-iii data importer service and nearly automatically links it to the Firefly-iii instance using a Personal Account Token. Instructions on how to do so is given in the next section. +## Features {#services-firefly-iii-features} + +- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-firefly-iii-usage-applicationdashboard) + ## Usage {#services-firefly-iii-usage} +### Initial Configuration {#services-firefly-iii-usage-configuration} + The following snippet assumes a few blocks have been setup already: - the [secrets block](usage.html#usage-secrets) with SOPS, @@ -127,6 +133,38 @@ shb.lldap.ensureUsers.USERNAME.groups = [ ]; ``` +### Application Dashboard {#services-firefly-iii-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the [dashboard option](#services-firefly-iii-options-shb.firefly-iii.dashboard). + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Finance.services.Firefly-iii = { + sortOrder = 1; + dashboard.request = config.shb.firefly-iii.dashboard.request; + settings.widget.type = "firefly"; + }; +} +``` + +The widget type needs to be set manually otherwise it is not displayed correctly. + +An API key can be set to show extra info: + +```nix +{ + shb.homepage.servicesGroups.Finance.services.Firefly-iii = { + apiKey.result = config.shb.sops.secret."firefly-iii/homepageApiKey".result; + }; + + shb.sops.secret."firefly-iii/homepageApiKey".request = + config.shb.homepage.servicesGroups.Finance.services.Firefly-iii.apiKey.request; +} +``` + ## Database Inspection {#services-firefly-iii-database-inspection} Access the database with: diff --git a/modules/services/forgejo.nix b/modules/services/forgejo.nix index 387a072..9666f89 100644 --- a/modules/services/forgejo.nix +++ b/modules/services/forgejo.nix @@ -408,6 +408,21 @@ in type = bool; default = false; }; + + dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = lib.types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${cfg.subdomain}.${cfg.domain}"; + externalUrlText = "https://\${config.shb.forgejo.subdomain}.\${config.shb.forgejo.domain}"; + internalUrl = "https://${cfg.subdomain}.${cfg.domain}"; + internalUrlText = "https://\${config.shb.forgejo.subdomain}.\${config.shb.forgejo.domain}"; + }; + }; + }; }; config = mkMerge [ diff --git a/modules/services/forgejo/docs/default.md b/modules/services/forgejo/docs/default.md index 9c39855..a2af906 100644 --- a/modules/services/forgejo/docs/default.md +++ b/modules/services/forgejo/docs/default.md @@ -17,6 +17,7 @@ LDAP and SSO integration as well as one local runner. - Access through [subdomain](#services-forgejo-options-shb.forgejo.subdomain) using reverse proxy. [Manual](#services-forgejo-usage-configuration). - Access through [HTTPS](#services-forgejo-options-shb.forgejo.ssl) using reverse proxy. [Manual](#services-forgejo-usage-configuration). - [Backup](#services-forgejo-options-shb.forgejo.sso) through the [backup block](./blocks-backup.html). [Manual](#services-forgejo-usage-backup). +- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-forgejo-usage-applicationdashboard) ## Usage {#services-forgejo-usage} @@ -206,6 +207,22 @@ The name `"forgejo"` in the `instances` can be anything. The `config.shb.forgejo.backup` option provides what directories to backup. You can define any number of Restic instances to backup Forgejo multiple times. +### Application Dashboard {#services-forgejo-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the [dashboard option](#services-forgejo-options-shb.forgejo.dashboard). + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Admin.services.Forgejo = { + sortOrder = 1; + dashboard.request = config.shb.forgejo.dashboard.request; + }; +} +``` + ### Extra Settings {#services-forgejo-usage-extra-settings} Other Forgejo settings can be accessed through the nixpkgs [stock service][]. diff --git a/modules/services/grocy.nix b/modules/services/grocy.nix index 3cc090f..a4ddf4a 100644 --- a/modules/services/grocy.nix +++ b/modules/services/grocy.nix @@ -111,6 +111,21 @@ in default = false; example = true; }; + + dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = lib.types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${cfg.subdomain}.${cfg.domain}"; + externalUrlText = "https://\${config.shb.grocy.subdomain}.\${config.shb.grocy.domain}"; + internalUrl = "https://${cfg.subdomain}.${cfg.domain}"; + internalUrlText = "https://\${config.shb.grocy.subdomain}.\${config.shb.grocy.domain}"; + }; + }; + }; }; config = lib.mkIf cfg.enable ( diff --git a/modules/services/hledger.nix b/modules/services/hledger.nix index 9fabf7a..b4d56f3 100644 --- a/modules/services/hledger.nix +++ b/modules/services/hledger.nix @@ -82,6 +82,21 @@ in default = [ "--forecast" ]; type = lib.types.listOf lib.types.str; }; + + dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = lib.types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${cfg.subdomain}.${cfg.domain}"; + externalUrlText = "https://\${config.shb.hledger.subdomain}.\${config.shb.hledger.domain}"; + internalUrl = "http://127.0.0.1:${toString config.services.hledger-web.port}"; + internalUrlText = "http://127.0.0.1:\${config.services.hledger-web.port}"; + }; + }; + }; }; config = lib.mkIf cfg.enable { diff --git a/modules/services/home-assistant.nix b/modules/services/home-assistant.nix index abfaa7f..b79e43a 100644 --- a/modules/services/home-assistant.nix +++ b/modules/services/home-assistant.nix @@ -218,6 +218,21 @@ in }; }; }; + + dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = lib.types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${cfg.subdomain}.${cfg.domain}"; + externalUrlText = "https://\${config.shb.home-assistant.subdomain}.\${config.shb.home-assistant.domain}"; + internalUrl = "http://127.0.0.1:${toString config.services.home-assistant.config.http.server_port}"; + internalUrlText = "http://127.0.0.1:\${config.services.home-assistant.config.http.server_port}"; + }; + }; + }; }; config = lib.mkIf cfg.enable { diff --git a/modules/services/home-assistant/docs/default.md b/modules/services/home-assistant/docs/default.md index 378cf24..c99c61b 100644 --- a/modules/services/home-assistant/docs/default.md +++ b/modules/services/home-assistant/docs/default.md @@ -15,6 +15,7 @@ LDAP and SSO integration. - Access through [subdomain](#services-home-assistant-options-shb.home-assistant.subdomain) using reverse proxy. [Manual](#services-home-assistant-usage-configuration). - Access through [HTTPS](#services-home-assistant-options-shb.home-assistant.ssl) using reverse proxy. [Manual](#services-home-assistant-usage-configuration). - [Backup](#services-home-assistant-options-shb.home-assistant.backup) through the [backup block](./blocks-backup.html). [Manual](#services-home-assistant-usage-backup). +- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-home-assistant-usage-applicationdashboard) - Not yet: declarative SSO. @@ -163,6 +164,57 @@ You can define any number of Restic instances to backup Home-Assistant multiple You will then need to configure more options like the `repository`, as explained in the [restic](blocks-restic.html) documentation. +### Application Dashboard {#services-home-assistant-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the [dashboard option](#services-home-assistant-options-shb.home-assistant.dashboard). + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Home.services.HomeAssistant = { + sortOrder = 1; + dashboard.request = config.shb.home-assistant.dashboard.request; + settings.icon = "si-homeassistant"; + }; +} +``` + +The icon needs to be set manually otherwise it is not displayed correctly. + +An API key can be set to show extra info: + +```nix +{ + shb.homepage.servicesGroups.Home.services.HomeAssistant = { + apiKey.result = config.shb.sops.secret."home-assistant/homepageApiKey".result; + }; + + shb.sops.secret."home-assistant/homepageApiKey".request = + config.shb.homepage.servicesGroups.Home.services.HomeAssistant.apiKey.request; +} +``` + +Custom widgets can be set using Home Assistant templating: + +```nix +{ + shb.homepage.servicesGroups.Home.services.HomeAssistant = { + settings.widget.custom = [ + { + template = "{{ states('sensor.power_consumption_power_consumption', with_unit=True, rounded=True) }}"; + label = "energy now"; + } + { + state = "sensor.power_consumption_daily_power_consumption"; + label = "energy today"; + } + ]; + }; +} +``` + ### Extra Components {#services-home-assistant-usage-extra-components} Packaged components can be found in the documentation of the corresponding option diff --git a/modules/services/homepage.nix b/modules/services/homepage.nix new file mode 100644 index 0000000..e5f26b8 --- /dev/null +++ b/modules/services/homepage.nix @@ -0,0 +1,286 @@ +{ + config, + lib, + shb, + ... +}: +let + cfg = config.shb.homepage; + + inherit (lib) types; +in +{ + imports = [ + ../../lib/module.nix + + ../blocks/lldap.nix + ../blocks/nginx.nix + ]; + + options.shb.homepage = { + enable = lib.mkEnableOption "the SHB homepage service"; + + subdomain = lib.mkOption { + type = types.str; + description = '' + Subdomain under which homepage will be served. + + ``` + . + ``` + ''; + example = "homepage"; + }; + + domain = lib.mkOption { + description = '' + Domain under which homepage is served. + + ``` + . + ``` + ''; + type = types.str; + example = "domain.com"; + }; + + ssl = lib.mkOption { + description = "Path to SSL files"; + type = types.nullOr shb.contracts.ssl.certs; + default = null; + }; + + servicesGroups = lib.mkOption { + description = "Group of services that should be showed on the dashboard."; + default = { }; + type = types.attrsOf ( + types.submodule ( + { name, ... }: + { + options = { + name = lib.mkOption { + type = types.str; + description = "Display name of the group. Defaults to the attr name."; + default = name; + }; + sortOrder = lib.mkOption { + description = '' + Order in which groups will be shown. + + The rules are: + + - Lowest number is shown first. + - Two groups having the same number are shown in a consistent (same across multiple deploys) but undefined order. + - Default is null which means at the end. + ''; + type = types.nullOr types.int; + default = null; + }; + services = lib.mkOption { + description = "Services that should be showed in the group on the dashboard."; + default = { }; + type = types.attrsOf ( + types.submodule ( + { name, ... }: + { + options = { + name = lib.mkOption { + type = types.str; + description = "Display name of the service. Defaults to the attr name."; + default = name; + }; + sortOrder = lib.mkOption { + type = types.nullOr types.int; + description = '' + Order in which groups will be shown. + + The rules are: + + - Lowest number is shown first. + - Two groups having the same number are shown in a consistent (same across multiple deploys) but undefined order. + - Default is null which means at the end. + ''; + default = null; + }; + dashboard = lib.mkOption { + description = '' + Provider of the dashboard contract. + + By default: + + - The `serviceName` option comes from the attr name. + - The `icon` option comes from applying `toLower` on the attr name. + - The `siteMonitor` option is set only if `internalUrl` is set. + ''; + type = types.submodule { + options = shb.contracts.dashboard.mkProvider { + resultCfg = { }; + }; + }; + }; + apiKey = lib.mkOption { + description = '' + API key used to access the service. + + This can be used to get data from the service. + ''; + default = null; + type = types.nullOr ( + lib.types.submodule { + options = shb.contracts.secret.mkRequester { + owner = "root"; + restartUnits = [ "homepage-dashboard.service" ]; + }; + } + ); + }; + settings = lib.mkOption { + description = '' + Extra options to pass to the homepage service. + + Check https://gethomepage.dev/configs/services/#icons + if the default icon is not correct. + + And check https://gethomepage.dev/widgets + if the default widget type is not correct. + ''; + default = { }; + type = types.attrsOf types.anything; + example = lib.literalExpression '' + { + icon = "si-homeassistant"; + widget.type = "firefly"; + widget.custom = [ + { + template = "{{ states('sensor.total_power', with_unit=True, rounded=True) }}"; + label = "energy now"; + } + { + state = "sensor.total_power_today"; + label = "energy today"; + } + ]; + } + ''; + }; + }; + } + ) + ); + }; + }; + } + ) + ); + }; + + ldap = lib.mkOption { + description = '' + Setup LDAP integration. + ''; + default = { }; + type = types.submodule { + options = { + userGroup = lib.mkOption { + type = types.str; + description = "Group users must belong to be able to login."; + default = "homepage_user"; + }; + }; + }; + }; + + sso = lib.mkOption { + description = '' + Setup SSO integration. + ''; + default = { }; + type = types.submodule { + options = { + enable = lib.mkEnableOption "SSO integration."; + + authEndpoint = lib.mkOption { + type = lib.types.str; + description = "Endpoint to the SSO provider."; + example = "https://authelia.example.com"; + }; + + authorization_policy = lib.mkOption { + type = types.enum [ + "one_factor" + "two_factor" + ]; + description = "Require one factor (password) or two factor (device) authentication."; + default = "one_factor"; + }; + }; + }; + }; + }; + + config = lib.mkIf cfg.enable { + services.homepage-dashboard = { + enable = true; + + allowedHosts = "${cfg.subdomain}.${cfg.domain}"; + + settings = { + baseUrl = "https://${cfg.subdomain}.${cfg.domain}"; + startUrl = "https://${cfg.subdomain}.${cfg.domain}"; + disableUpdateCheck = true; + }; + + bookmarks = [ ]; + + services = shb.homepage.asServiceGroup cfg.servicesGroups; + + widgets = [ ]; + }; + + systemd.services.homepage-dashboard.serviceConfig = + let + keys = shb.homepage.allKeys cfg.servicesGroups; + in + { + # LoadCredential = [ + # "Media_Jellyfin:/path" + # ]; + LoadCredential = lib.mapAttrsToList (name: path: "${name}:${path}") keys; + # Environment = [ + # "HOMEPAGE_FILE_Media_Jellyfin=%d/Media_Jellyfin" + # ]; + Environment = lib.mapAttrsToList (name: path: "HOMEPAGE_FILE_${name}=%d/${name}") keys; + }; + + # This should be using a contract instead of setting the option directly. + shb.lldap = lib.mkIf config.shb.lldap.enable { + ensureGroups = { + ${cfg.ldap.userGroup} = { }; + }; + }; + + shb.nginx.vhosts = [ + ( + { + inherit (cfg) subdomain domain ssl; + + upstream = "http://127.0.0.1:${toString config.services.homepage-dashboard.listenPort}/"; + extraConfig = '' + proxy_read_timeout 300s; + proxy_send_timeout 300s; + ''; + autheliaRules = lib.optionals (cfg.sso.enable) [ + { + domain = "${cfg.subdomain}.${cfg.domain}"; + policy = cfg.sso.authorization_policy; + subject = [ "group:${cfg.ldap.userGroup}" ]; + } + ]; + } + // lib.optionalAttrs cfg.sso.enable { + inherit (cfg.sso) authEndpoint; + } + ) + ]; + }; +} diff --git a/modules/services/homepage/docs/Screenshot.png b/modules/services/homepage/docs/Screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..5db8effd56eb718b3cadf17c7cad01f63528e09a GIT binary patch literal 130526 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

3d(72Mn zH#-93o`@%W_K24F@NzhUWH8E{eOcHN{67IF&i1Y@$IUE9C##C;@+7{6x{#x@Dyq3%^mb>Qb1x2;j zZJ%@Dx-Zqr)X7MR{GW!ne4l%EI0sA5it>ULWBM@>%HfK)oSw}a?R&QPESNjEo!IyE z^;T%Wox99j@^r8Ao=DM$jJBi0((b@bDRI3XVKU7ekX^q;L$e3h@XvM35ye#~oq z`u)1e+m$yljB#T1x;1iAcoIzH!O`Aa1?KgqF7I1d-zDQ<<~H{qszEx0)|?3Qh@1Ln zN!J)cqFn13_9l{LO&G7QKb_2C{LMzuTh`)aV4E;J){#i_7DZz zd0r%Q+Cl#K8`f$@WjU;IUhh{t0p-8nPlKF^WOq%rQH{6hc!8y!)BUGyW)S_dO$^hw zEkXN{`3MsVX#32IC+4uqIjg(Pd_s@>3qg z}{H=>}<#hC_EtBOoY9cO5`Q4=p7P3Q|(i%|TM?(B0kL zb-2UN@B82XbMJn91a{1xz4y$TnYG^cUG#%B$_8H^tsNd-RcEWDttO8a=}z{Jx{n8o znRL#c6vT&@;f{S�I~+ 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

#eqF47b#(JCt1Q)HwBbJA0O~EY7{ig zd_5s?5uI^@=AHt>>K~!^8vUD&?jta3GR)Q0kv8+U{|4*1b-bROn$KU1c!4k9zQB3$ ziy)|n`QINdhhaZ#QUL^cQ|WU_QJ(?VtV1R9_@S?`gv`>wk=^{GMjDPqL=!%&s4@F=TK}NAMt`~;e ziMu`5aIX4h?0Kw#W<2m5uZlnqO2h>ay9@X#Zbm~;t90%Zbv(NEuwTX*R3gKvnhRT1 zd-OMDwXx0*)+T)kK?|+iqob}Bol?>|IO+E2lrox7kxLI;3m}k8TR--r$a8vu1rJbJt?Pc>%I6x7rh+no8f5elFDB;~8WgXl(rGoT36J2{oC(N%@ z=f_LvlstMh;1sYU=e0WziXAos{bV0O70|3dOpp!@8kelj3dTzKZdcQ}#_OWk8IM6( zR+RvTCk3{&?H~ZAja%`t9IMkZHYTEWI9jMTc=5mAK=~Olt5Z)&VZa`RLfTB9{%L}f9RTq)hj^S+ zdvUICf>^a@S9>OWq1siTnHZ8_K)yF1rNh+iJv!r2wFS%pD5m@x)^*lxO`KqJIZOnd z=IR7yOIBy8K-?&n~eQ!0VEnQ*!DX$oTQM{t`hDqySj?)%YmS6 zN`~zI$RD35h+%qgvoXC~5#IB~cg){@p(jHbE$+Spm~mKf%;828&suMGoYLbVu}mAb zCHI=qlV{HD?k^Rr{Ki`auEYx&)>;+VTqYt07s{Ka>Ndm7Sz@^CIsc;hm6V*UCID2D z!|_CJ*L<`04csrWxg3h;-p=a08^c{k8Vk(QO+%4Lq1YMpf-}nQ&Yj=WEpY;R>4)>5 zW1`t5e_ua`Ek0yPr6MLH*I$p1kB^LuAd!F5eeJLzq`^@jzt3c6ncR}Z!Vb=o((SMt z3p$TV9Pju+Ucjyj_8I!o=AyX0PNd)m!-jm5IhR7>emsm#Zn;2!Ak2aKt=!zJ(BW!7eYI;!R zx(*>oVKoWP0xc8Y6)nAUevvHUGWnG)oOMeK#51-8T&0IcL&woHGtqo^dieR{8$zr($qs=Nsj;ef} zNIyVL8_|7E0+#Dv)Lk9#@l-ko#D2?>4nrDv#?rXS*RNlX)dji{>VXvRc}{}}%3dO|-R)BIq#kjwK zoVe4jj)S$?$XtuMXaUh@%z||`+CNUV7dxt@f4gOz*Zh(el_C53V*z-B%HyD4Aa)uEM_H(HKQ*Vn+1pnGm(oWz%hpcPRbnM;b zvLIzNb_R9b=0}osFr2oZ?8u#ELnyG#{_u})igA1A%ICn<?;R-PHPe~^Lb|&@S@ewZEe6xi=f!^mG0TCX#~2DZuwvMyqF2ggpwPzD z)RdUb6I6$}lHM5JU*;JOmVyhKzTIrt*=b$*(;h|K91SGB;oO;RGsPr| zvTTkY`BDs(FpljCOhVV`hSFzKQHfS%9vNq6um?%ur)Xt!O$3C5{7GX7MfhLwQg;LJ z`&--cYZncnJvEn|>lA&o_DL=_}$2;;%uGjPN+vn+IKADvSOO1p4N1 zi(kFEJN~gzr9D;hF|pvZrin?uSGNu#AKq7M3R=o*nl0|1Pr;;ZPo*CnteCIp94Ul! z@9JT=y9)F`xDBjgv_MANp3C=_5h{MQRyBjI(o3$OSE2V-rpnrQ6$Z zNjt{cD!r)LH}>tx_JrP24b{dOP$Cg=gY;dTUtvEn7%^1l!p_R>edyP7sHP_d*H}|D3+&;K!}y!4kEf>CGb* zZUhD{1wX~~4*cho~5+fok z@d=cxkguu&75PMpZvocuJ6p^#59o;Ayv#wyr%h|ctU_BuLq}6tzs9)zE_bDUGYccF z&%G#sY{(M(assSoJ=lkRmNx@}0Kt1Et-=aEc=)BN4NU#|s`=*t%ct;r_wH#r?yIpr zrX3rxZ#InC9BYoWA z?KbkO>U;ry{?(x}E!c7k=IDS_fpu~i;_=KlQW44Uvcyb;aaaZK*#^>c$T$P{5g)Z< zQ%=cWNs|}&c0cda(ti00^hh$Aez??j3BYIo^ZJFk%FiEQ|6_ITZoSX4-#erQr^(W< zIW!;#G!A)l1fz~}kg%8GjEK$Hw{OQ~3GA|-M2o;Y9~UzvmFb8q9>h5DfV-uoy^Kxf zs|g9FxfrKWz7MgnHHd*MIetM{?m)-vK1BxI*mNOcQ~R4%~-oW1#Y20e$A z!?-nw7cK7utPFP9mq^;TrB9~>L1v^}biaIwRM$2#8vkKp6*#PmR(o>y$EqwvQWxf0 zxhfo}(TS~AQ&T%Ee^{il#~N#kDcIl0LSqx6z$B@TXO2G;XlKP2(o<(H z5c1-_6Fy2Ed-!3nzHR(l-x4CT%ID|~h3_m?SN4A`cOr2IhDTK~>c_@HHv4PVs9eLV zY3lw9WksMW4ydy)$j0?6j;BF>BI4ZsfA2{((cQ0xTnnz2`4oYC7dN!5;wQL{2aJI< zb&Wi$JV(H0g!Gi(r)S!m!S9`xu{GHebXWVn7nOL5tl;+j5mUBFMF$Uub(!v_fbwt> z`$)+8y~&L!NO8dE8Oj-RDW^~HAu!bCvdAh%B15NjsZ^nkrsL3>2!6iW>Y)jkYnpMM zTXY}+MpHhf$^7A1ky5riKR>^eloT&-hhChQOIiL2M6j57Q4gx%{giHFbB#zEfe`H;Ol)W#o#Ej=W}_KLTQ!MMX1ePqv#w z*aj;bN*|%&XUHhOYZ^kV>`dgQcT-Iy9z|-+_+r~uDOV$cdzJB=iGTT&-2wBfy$pGo z0okJrL~~o4a94xwz{(r-8?9cH46`vBv75KDx%Ir(tu0>#p9C6*YBXzBZNGl)aiiO( zc|ImV6f109A7o)MvcI$B4Qy7=A1~=ltPy3Mb0y~cS}IN`JA7FAwWV;L;B~ihjW~Wy zp_<@rba7O$11d2+_Gvyho_vy46#LDjA{rnqdqHaD@R7n$B&W;l)&dR9KQtP!TSfyR zg%6-Ypt79~ODrEYSjB7FVLZ>a^`%AMREZNhxbsMcVJqC4q6A^hW zDCWglwJ1a}=-0Qh@EHCxV^8Kvg7Z+aZz3ZHYStPyzFcK8t#ZsmZdXqhpWvu1b(%X^ ztEvm=6Atkfz~>X@xBQ{yZssL-%G!s0>}>51+|0^cUm(vdhkQonTGjPqYTq9`#J^h| z%0@g)X-2Ng|R}3X_mTJuMApy}iZOH|r7x zF*uq{bqHi>XdY7C>JRbeH8v%gaFhet0HY}s2=S~7*jdF;g0aQRPxiV#s-{7GgQwoe z0(tndRBdw^mFYsLKkB~AvjQv^{8E{@Fo1#gW&kg1$S@0-VpJnW2iJe5jiM9?viwb} zgKU4DEqZJn2rBLQ@nVQ-?TdZs9GknWN!ITY(zaG9%EE>Hg;_j#_v&kpJXb2O#Hc7f zfYql+v|DX%iF+Cv^eB|M1#Z2;S6R9;J!QWG6tWVr3xT)teg&Nu4+*>`AnS|k=K>xq z990hTNg7i6b4MSVCe}&j^gFMMi_a>5$;^ysR`NaP&|mqn?y9iFqXEO1L^`@;Xo$W? z@XQh*=l}{Qd@Oh4tpHDXfw>^0FUOc&`jL8Fl+&8#05oKR*@~Y=ckRX@2RbT<$FM^m&BuZ46s+ixtGcE#6APDRxi+#>ia=hYtzjmnf7i{oD(UDz(#Q9z zYv~cn0y}?|O+^dvtqo!bnA+Wgamn`W?OgobI``f6Zh*P~<`}?dS$x*GNZs~i89)$3 z1F(ZFT*+G_DH%Mg9}s2a0POOvYE{Qb3~wd)|&CCEA&={D%A zA7S>m{(>9m<={W6i7t@nGFo4v@p)e}BAthGSKwCot5-T`_fOp3lX?GPM)nyR7-3*m))%u44b>(z#ni9!06evI^SmHQd+Wqw%*NENe*G6gcs< z_3VCDcvSPy4#!%^_5w!%Ces@`%i5Zxr=w*~KKKS;Dnh*{K6z!j_5RXoWm$-w-E1@$ z-Dy{|vVbEniJ+c}^+W#~q<`h*LC2J^(VhamgI!}lu9pYk6ZumHOUQ1~&HM8%!@gsr zbwi-?iyt4zu-(u7pzYL@|I(j6Q9I=Z6e51*^gwWU{BMx|*y;QWdS+~Un)d}l8O?QL_re(U2g%KL(Ha&oe=q(pPq zy~C}}m4maj?>=H@0*+RmqJ!i&(2I2v|Nd(zfT*X8e}vMiw0hFfbUX-Xe80_KWGc<8 z7<&uC9KR@3BOn^oShQtCs%j7bh*QH;D$sdzfrQJgVjT0md++TqL-Y|g!63}$eWq#A zqhCBBhfjOHXsp$4b@XItfwO0P(Z^dWzjekDCqyu2i=QKmhe26AlVVMyLk_ zR8J?37GetIkV{P1;&^62;&0dS3N4+sIOb`4?QQox4(iG8vSX zI-Au=X67Uc%Rs*o+2#wcVXtp*phG;@*O@52aO&6^PCsX-#yfwrTfe*q^Ro1nk1?Cc4G4y(I4)Kz?jqXr+J(LXm3 z#!vYZ_w+$hkd73tw4?q?SO~GyM*@nF(a~ZNmBnu@Z+-Sq;+_S#0rkm95SRsDvqU}~ zj<)Q4!}<7fwLzk-DS7EadShS_&K^uc@j2BH*Xj2zP#^su>X&H5wC@->-wy!h=-t21 zdZ61EHHB1V%U4Dc5)wwMRLyb4)35VI{7@^y!$sGy2CXw5Z`h%(J<|(euJ^zV-0fV40c@P&*PgMrfX9G_FIKzG@4qjaQa*D0^pPXeR>B8# zwo|1dU${Srls(T)t(#}++$i6HVt(;_L>aAz_YTpR$6QgR%?JAlJJZ^}bB<$U=w}N~ z9sPd}V~di_IH5kGX2qX?o?{jHIooc*fJ~2KQKe1vF1I&Ws;;*kjt)l$xo&JGd;wTj z$ua8j-^1sGpCuliQ`p$u;K}MNzm}ulSpEIhEmUT5BHy=%`<%N@p-yyWiD33V&jC#x zqnvi#`KQxe=9pM8@f~!3BFs76|D=O}a|6?DRc!k3dGNy4+=ak$@*N2N(<_4mj%8X9{cNy#W(aHH;HJmR${_s!wM zk^~SUa{o0mDpvEw6GX~x-+oT<`UT*6(37cS&t;+$(*_Nay5VjJq?zS^kGH3vaA4{N zuB%swkA*@h_TjzvIz|F>m-O0*rY4 z_S=*trZ=u&IyR1{gBfQ(`(&<*5rn!#q0nIooqs{w#Qz1nM)XJ(bS5PvtbD#iOY=cW z0%7N4SH2*ffE3&Z!rtUbMa1R8nKL5z6XCY@wpHhnuqzT?sR67F*X6{joDBL~9SM=lgKunh( zpv3UNF}j-8gYV=w(V$Sz`bpH`Mp3q1r&qs}&NXliz?j=ZXWyRrU)up7#8M!n93d!* z5iH##yr+QiGst}%EJ|3w%B3KZU`>y@0)pLtfc*}6JvJR&JR*VLdvp5wPE(tGg2|sos)vnd zxl`gPIZ$BtdT_uDbY5q0=EfF(lu(hDDx0#F$#e%moV_nY7j9Px zu=#Zq>^f7L#29t{F92jC{}~ynjL6n>wM|U@Ihva-A5TO(3UE!A9=ff6t<`&=_W1-j z4l<6Dy*oY%0Sr0fkV$2Z?V0l4mvo#r%}ouZ#5BiHr6p4#N`cWhz)FmcUtQmR1=SPJ z@4w+WW*RUCv)AqYizZsBo|Q|2n(ZS<;&Vx2M4`g3M+{D&HfQNl45BWL% z5032mACCN<4CZ6uCRLG^2sYZjeEIU?+BQIW!@JLa)=rj?mzP&Yw{TM3^TzLKVqhD| z$LF(&1L(e?<@W8{=TzB#;X!}u|EL+NAQd_V>@|zOK}Cy;)^lL=|4|wmZBsw3N`g6F zebRT`LhP95L3w`+4__2}5d}OSv9Bk2s5;>f-tNgTe1;a>iOL@t|9Vov5(w#IhGB*O zzu(d5euBZY(Xt!kuC}Jc;<{mw0#Giq=7ZH#tQ_9(S$c2K#k}zHEoQi$%pJ2SX%}V0Rqff$o?CL5J(<+bm5mnAFCsU5XWB9S{s~hc;wn zp60P7ck9;bRv8>nDWaE$gZ~$vPRrdE)Y;u=pXyL@f6S`|8w|NQHvOeJ@F~d|ra%zD(xK+Z=P;Ge@r*u!xqEux zxo#=_#cAuU{?3-xLbsi1yBRxsyD$6#T%$bfEapW+ zzlgt&=ZZ~^s;k9}P)H;krR=uaeG(eO-E5s7p9wYvl3D+>xa98L%Xx6XLjlR_JIh0* zKdc;qwK_$erf2k=Z2z7DN_V=YMFOGBbi=}xsk=0eeD$7Ygka;07|RuXzfp;gOkr5j zPexXk)7WH&If`T*_Jh(>ukC~OyWqj@)ClYN^Qo4Wp{l13};vHgkMIfe*t(zj&fQSr4at1;WLGkwQrx;pwU%3^`* z+?m_-|6aJwZ1C?XgMa8Wp7Cg?|MM>$-PO0`cV|Ab8**}@YTn$CSuJ(Pyzq;!n>$3xKgHe*bTap}G&e*zTiX91 zSg@_0I|Uv36ubUaqh!R2M*t{18k#er%)pjA$8QGv=~ZPZ<}1ZatdQD%kO}kBG&6Rc z_?m7ywOsos!)S$U33&%~e$w9sVY*>SC$umK{>l4?4jt7Tik|OSgfKIBD?Dbdws6JG zB&sTC9IyBpwm&!5q~4s_u;;LSN0XiQKv{%IGz>dK+=n^o(|Z0kRgF@NXo0*-P4sW> zFZBsTh$WdbaDD34InunRB>^1gKNyTqo|?KX%DLUJgr+ zv;Hwi-EhxtHIHAUp}8*T;PO8I-#uQKP)0N-=sP@}m}wT<_)R86@yM$gc7j>3FayZD_wlGiX{qww7AD$ZbKZ~RMa~je z=zMq0ABiY%Xkf3P-p4Q2_6`|CG_u)g31BONN58-K98_2d?Tmcsl;aZ)Jvni&Fy}$Z zr3@qT`cp{H&;wrDCPpK(rSjSR+>dH?IqutE)Z%x|7hQ%uj2BQs@03c_VLonFIBC75 zx?zG(AoHk|wd?o4w+a{k%=llgPXV2D676CxdcKd=T7=I`Aj>w8IS%Gjfd`_5{5o*& zxh38FXD?G@2qf=~H-y#`4;fn7@vl|Z7nT8Inh`cll%cYcHt`%`Nloq?Qs<~N6a zyMRt*xtxAQ*E0%PBfUI~ODrx@w)JEY0JF*&%?yO{oR=B1V#H}amUs!HP6G}umu_|S zc+K|a*cYr4scPhVg;Vq9=Y7w-;L0n?lh4Vrn?@RRs|yZv_dkv`XcZJpemkxaKV|Ya z;4ouqrmI4t*`+s;T3JcDrkf)wn1gon=&6WQ*_xmhPfG1ZnA6nc`T9q+y)}fvln7lC9Xg;g_uScUyJ$u3@WAzQQYJWm%6t7agZvypv>07n6x%N5{Ff~Rt< zr-e^mAKw#rvP$mhy{R!2DE)W*mlbl45#^s<^Zz~}P!Bv$7>USZ1)L*#U-qwre1C$$ zl}^?3_D?aa%{_!bpvWqFLu^&ZSF4*bAI7YJg!akm(MPh8UcpLS+9>OX0BeC#xZf+*sm!GC#10S8U1qB5$%@wuTY(IeA%WBg5 zKK1}54WBeGNV&qE_6YP(YPtInaFi3!G5-QhP&-A>@!h9N9ok8!^*r{E%AJ^@h_t$b zNds)(5cITYk4TY$W2VpcUZ9S4milO77P|G?7Skjh_Z4pR@$xYxF7IJPg!xR% zj<*7&r~y<4KY({bIi@!Q4fi)nS8EZD0fa&@p79_T+f-qR-8XUcn&^qT~TDSRLP(y+X-tkEo$OjqY`SAhAEg54Kl_phw@$>m3*KHTq z4?p{ieYn$oSC5`JHZsFD*D}sI*`x?{D8GfSXkN*U_@i1ev3IYWddI6-PP(TV8R-pg z(lwk?J&xA$nsa&=D+Cz#H!-Gr>+ezFl-sQ%>N42G%s6hCnigNi@rVmP`fVQh!BU(` za_Z~Q?mp&$Ce%$&&o)5S5BEK$Y=E?$lcE@Fu$p5{AS;g3LQ_eaX|&JP)S zB6Ntw2H&E0X=|tHOBGzZAringI(F3DxR=O_rSX!Adl12B@`4i*ZeldsQ~eOOgp(qR zR70!g)fkn4DUv<>8JZ6u6;|PejD{QJk%{#cnsHnFR_hxW-|B52Fa?7#d%1Yej zk>wb6;w&3c|Mf;0ukTAI{%3+Cy(wbFIhFSCwyC~8bqdfCL8f613tky+pu2F!jTY#W zB*bqgqt$jkU7G}59sGR{;MaqxFJJtLsOOPnfv?PW0!q%#PyuUR`$oBI90$IRzYZT| zKCdau_fl5h*SAg+ckgON`L9&T3UpMH(xsioxt;~)vlxO{>pUW>`}Iom9^y)Az^Vrr6|rmM)CPc zUMxeZq5Nx4vzd1O=C3)G8zrn$&0CYaz$A!2ML14MHoC30(WZ~^xcaS|m^Q8MEfhXb zqdTZzovxfu&|~#ohD#-t=pXERS!YN)pN^6-PE*I+8yjm0zg~&S(dMY7qWlHw?|afT zbK?yvJQ_pNTrn$_IdRqY(_SOwtlwZZw_|31nH@~Ix7%`{Kopr4On;!kZWkq^X&P(h zGnLM6tJ()_&n2m)2skX^!tm2O=<#E33VKZ~-vh+-9hH?@Rnz99@W7VqG>w~}<+#m}#pkSzy~=)RVPOh>fRJ-_EoqvK zRXX1lHD!P0bp56V;U8k8`VuuU_loY%zJpJ_%hGQrPEN=;*Vx2xer-Lhf|5c-1@%}> z4_>-hGUNtxoxFz~gg|&BE6c7zAO@+qJnT}1OAioSOOa_JWWnzfBt&qN`}85+_%{f+_n*B~&^m`x~?k>rEKJ&3|*yW+kkAWTpY zEos-3kW=dwm`Swtb}Ol|p!G8AzjMTW;e)Pd zFM*;6SVdR4|7sC&D6`pJE_A}D_!N4>)%Y13M_VLsVS7rJJ$8EV?&O|G+HV!xZ1AS*>y`yddTvM`-7SBAp&wyXigm*v5Czcl1v%*cfG`9aPWu4*b2Lyh(to0!kR~FlI8+FK}N%f`<)(zeTi4$;19y zP)=I#w#=6V zvOH&RE1a2Qp+fwQqPli9$D?Yep)KB@_aIm}t^j89S;*EG>JrvGnUNua{90oJ6x8bK z_{-Fyh~2;&Ku!864t&4ZB6>IiM_cX>h>>6ZkSgqm5Vsusw7#B*!3^fdO&K*6WR^Lf z^i3`ZcKPqr%c!Ra^ew3x9UrHb=_tnEF|FEspRdh9)+s%IC%Hr}t1_id?)dA{Ys)a@ z;ddz}68(ZIybv*ceTd5*b&`1Hcc0Ex9Op*brRa400`Tob>9Xg=8^lnGajLSmOy*DEdavUks|hR64` z^PX{MXzFSkF70`Oj%OSY9^GQX&=bAIg#~frSi~_S#ARwsY5-sUe{uoxrneUu6<)VU z;G1^00}j>;Cj!2B?{>su9nQ>4dxKhHTBcQDe!B?GAj%k7rB_?AIgK>N9$W91Xqwj_ z_kOdy^5H3k?HdQ&@PmGe+c75D3>WDneSOA%(TlOVQTGvxS3Uv1_SM^ptbt2D)m7EP zp`3Y4uHP!;=D(j-(6k6?54xtT6~=2Bz&MJ5?zdB5Qx7D=h8-I+=19z~2&t-|9zCmo z!{4x%iGa~~FMMFy3ngktZFAo4D}>D{qcZQA_pVE*kWJR4aj3Rn3Q4^hv^z@RWc*2XWhzch7;RC;?f<0y(mv@Sk!57_+wXF2-gu8_RFP*P@<(LAPd z*@nZ6;kw7DE)p)m4v9@n;E_TXK=pNzjgo& z9Hy=@!uFXDE=|OVF5DErNuHTcF9qoIM(i&xlvS5)(mYr>dd-hpqe z#gYSsm@RHST~IYFeCnVW@v*7h^&L=JTxYijsMz&wdVKvQ(D37*%M$I8%m^U7OQ-t5EPqz3 zr)XjXHDtyRPRjYXnZUs5x{chG9PPstP^}T)tJV~Tv2er}+Nh09v=Ww~|3nxOHOW67>O<&K~&I~2& z$Y)9&si-by+s96uzLb{J&@kcXztN39!4wz!QzSpOHCOZ=pHbB-HOB9G1}!cY8CCEJ zyDk*Cv$urHzup_KP7_n><&9Bx^K>@TwbEm!O0qRM?<{vTL^!O z8F6u~JLZUD(cDfz1sKRPFw|4Z8dh(NZuaG_7xk0|?5XB+arO16Q2n0VAvhwOyH3qo zwvWI{)n$frw_@6dhA?upI!?bq&h1TNlS#TiJ(uB%>i&xAXSiON_@xbKJk8|b%eb`k z;re_UP3|Xbd70jhdI?J`I->F4ZUs~*k37qJ`bIH|(|yz_hSXSKV=czf+LD9zF(13K?Zhe`$ zI$qa)el%36XS&sfGCmJ2tyKikjeKp|utb&c&C`8$)+6S*OP`dx6u?>xES*LD|5j@I{(9lFZ7(SY)OG^Q;b;*) z;;|GGJ$rI+>NeIjJdVj$ic(gU_$qjKOvczU)6vvL7#^-HOJ)>qVq9M@Dk+%^_#!U6 zGaz%;ti*DnstUtS00p3)TvKn37il7VNHX=>dl@Bl=hpOA4$)6Fb4yiG0X_B(%Vq81n+?yDn_>*>t(QYzEw<~kI1al?NU}lQBY_~c|YeV z<1KuHqTz_)HHg=0vw%v4s`y_23&j2NH#6*KrjO`T-mLhU_4FS3%*grKs zY7}sHg8oj-6hIy3N0G2lqgn`UD+K$%R=1^Crc{8$YW%O|10x3=jgDe z!CHx0Xh;ZlPE7?>R`ks_MTu_tKZ^hPYfO9>cqUlwgcM}H1rL%#0O0D6E{FVcBP8bnq+YVj&1gb3d-V1 zmeP?Vu);R>1@?~i5yi_+J`DgL1J14xWmT3LTH!k&2M}~9jPyKx*av(M=X+e8@CIRO z+|AE?)hjN;xar}S75GmEOp`{W4))mKq<6we_EE8|8b%O`VHm=y-4y(AS@@S*hW6;dAOV{Z*bn{dfhTGA-(w4%E`K2>)4kS zmw|)75eZo`;?LvZg_x)0^8AdGup8zbg#qe3cSp8wNOY-!tLyb?EQt&jNyv z#M)GZ0CHS6!ID?jy`iGs?N*KgBM#Xf^gQIGFO8qse-IX8Ggj(0iiC88HC?U^2zKmxUnSY4AbOUq5;~IpG}a*ITk|P?e(pY4 z_k^e@5)R@Aio|W04YJ3k0W?d-7iiTeq7dA5cu8|G1L6g(i@_kKf2ed3h;j`@q@ox| z7Wm0VnL|RPD*7*NpiI5HToiok>2@J)LTEh$wKh^s#k-U0!1ngIfXdI_znG0i`1leRl8pxyG`pbb^E&0uvNNtvi!ivu{ zc|yB0@RgJXL6R?~gb3!x&v8In>v^Ol&Rx6Yv@Zizx$yQcYsiU_^YB;;{5z@RAZP3M zb)rJ~V6K~lWS6zBy>$GZj7z}@Da+&@t*$09`2ULJE|F_apBvcbRCfH_b%kG89_3V* z!mX~H-y~Uv%UCKYa~6)G(n6#URTZxQfG3=>(yovS1VJ?ttZP$CWnhN9uPJ=`D ze;&EiShvd3r2?NUTWi7311?Bqts1?PZw8C)p{=51)h=AsQ*YRbdH2l3}Gl&cG z-?dEj|M$(Q)3R7} zAGumBEH0*vHdYob155OT`=DA##5E8s?oCREXdmyfp;1+kg;?loATzyGusZISa8g(Z zI|A8FB>hT8&Sgmp=m|pG z8j^$w00~00gxy`tvyueCJ#T;ZQ1eT8uYzFR^z_+q|Bzd}aFX%%*<^;FN|P%}ve!Wt8CPtN66=4QGd086(LEP3=Kc$K0V%A9_BQT>iq(NNtv!HP82FNEf$ z7mn7Kj$8$y9KS19j;MVOCVkhK%aU*ONu0Qf7{Ltp+w@FQ0kyVM6N2i9jEtnD#Kb`i zrparqN+H~Is==L+RUGVFlmgTJF972f^B(#KEktYLIb{)W9DpAN=0Pl1+=H-=#bPXo zWcaiS&$UO^64pfW@^ac1?{lz6vWi%p);zkwFNw$bu#T3&MrL3L=W!L&vtQO~bFUn? zewkeXSa+ZF4pM=?VfS4WyNu&9qIxI5z#bXr^giMy&d3P;fXA!hE@_#NiHa2~P;bm_jmp1>LoGcxH zD>`zuCag`PP8YTUwrT+%{qbvida7F85$Ozo1m_? z>-`>)*`jwO;JldRaEc6BpU^3@n)L22hvq#?7jjMmag9Y=mguGUBUdB_6cJ7~xmJUA zMYerK6BA5H-R?Vj($)4$CCA4>7GjVVA>7&7#*v(>J$9S_ldMO;3Otv-}pa=2cRxC)i`~5FD(F4&a4T5?mP?EZtesU= zwJ?I;>TtXr$(Ph!wI0lRf>u>cd{bRsAIrN-5MGa7X*#UbhM1-Jvg3anCvf>Cp79FZ8|UB;BVaC z%noof9}kc|>Pbj4J_V2nRgAVq3Xmu%8UI%shK>@v=Vzq>=YO!%pGD2m+Ba(dOjMj5 zCN(q9IWLg++S}MQoCK4C^L|%7!v*T zANcxs<;r9+#05k=YNT*2j7H^~7h44Ey8RbxX`^ZauYi(C$*jX@jtu{;1wC&oPzE$S zk|edaZcafjW`5YSVp(=XYwBs@q7@tR(0gWqBg31|L+>uc{%pS<@Au~tvx1`c8_=}1 z8a_eDKJfwfzaAd1Hu1Aa4w+vfbB`SYl!VY)YIB^#`a;U>j(%RGbu3xtZkH^rcXiWX zmF_!j$za9Pqoud-)`bPPM9~0H4Y$ltxX*O`{J1*6DHr4%9K5@=5quy;*DW2cEXs5= z+!Bo&tu>S%L=QetOZ$-rtgW}%g0q%N=Y(|#g^H@0ySUMAd}50CaXx^lwsl}YB%*47 zxGsh$b251!*Rn+~Z~MX%8duP{zpeU`rL^TAV$V*R)5U!go{R1V%bahFlFuwPA8&9- z?;Sb%EtS0bzO$K=IyE`_3~<^)9k8oI*mf3s+#Z-)kc4%-yEN>xkE4pAN9TG^q?;7_ z9*R~7x+&|EcUOtnO{_n|PZ$prIxn0REH2Vebap78$OczgjNnXM?Yhqn(BXBEr9m#= zW3W!5;(871%XQVlFL0$Gx4>yDaH+ZGwJk~=Ldz@KzM+0QCU45(ax-#h2plyK%GI0X zy}eS$j3691mE6(u425FkhG47lcCa1RvMqr2lqN;9o@jpjmDxqm=|gI9OYrc>INF$) zIGpuQc)8Zs{T6K*0k6Hw2kI82UF$;DC-wAobS7N~%Bc&3XlcDB(!mNnG_8FgeBRQY zEF~e@J~TWcL3OgP!(wYIr>Iybc(?1(_;+VC#Zq&{im`Rpm%Gt7zynE?XhleEiubfp zk^$%>X%QiKM}t_!l&5Q;pv=IU7(Pu<;WYq>gW>(lx3&b+LWhRpuYn^$DIhyO{=PMY zCmZ%+i!ezp$WoG9|9iTOhqFV%@@%4$sy&*M-oMqz6=l{B42FJGf_n1&9rSXtxkp2- z`5s3}oss+^DtXv-B{*6TuD34bn>#$Ikey$d{;`FkS^~7& z4g4Q!HER3xre97C4M8AD#tnCcch)P^;gGX{W~Bpb3KfkLl9;j)?B3hgS}uAI#Bcx8 zq&H`xA&V%?sTXR?l1=I86Nf#(u-A*GJ0=t4gM7C_2i z6k;bm=v$5g4K@9w40#FaV1T+kYg@8kE8Z~x)zP5zR1#43htp`QvOYZNVdJRYLs?T+uH($bD4rkIE>aq=$paYeI#iFQo$`gEK@3r=nl zKlZ*ZvTNcO`FZuZA=(ern55pc=G;Kh#suZ&j!eDukFQKgH3YghtfQvPLaUC|bG>cB z1>wI6OwkXjsVN4xn7+}(lw~Wm;ds20b;Xb94t!@nI`_4LQ2Ey8dPPVCt3*&Y1(1lQ zo@8VH2wrP7epO|sJ%u!{nrv|L{0Ss_0dL9K&wkhYcnpLmT84IrI2=JWXTGPqM$J}^ zB!w#f`+lu}N7|zm4Ds^s)NA&gGxb03KUzwsPhE+iwn$k_NHX<4-pz%rzf_D&GXbN7YL3l{q?V;w}-Ad061b`PLaS zAnT@Y_l9cX)oGvIO}YS|c9j3o=0THRYWG6jc6s0u!tXe@*O0KWkxair)gHOWek9*o zUSukLYU8@|^O(-w{%l%$Kd*Fh_e##t-iZI{x}f^lA&w@%=j2mb8c05&YD|AFI`7Pi zJ`@1Q2F$^>Q(2?=Fl)!vH5hay5$>l@*3d{5cORVC253S9^7V?CHKLCi_$0Ua7kgKr zocxAf$jfe)!|#Bb5q>?mmpj8MC3k0e$~i)i93C&u_%|7!eSj%s<>S-_3Mz!A)K|!e}Pb_g@{zy{Q5f)#6o`kpat{yQcZ7eh5a^Fdk zwob8n`qUr%*=Y*gKK=s-Y_BqQBYrWo+hF$*rZq=Z6(~`AOwUSNTcAp4C<1}-0fwbX1+FdW980k*I%`nc z)MN+Z;y3t3;j8hXz@9Z&8?UQ#ceMBP9+)NH#TiyPoOkiUA2w`GB%`4f<$}c_I@6;y zU=%?c|5=_nzDd3`_^$w&M6ilHV3RVgD->g$Ozqb5I}KA+cnv(mmluyke!{`UW?&iw!X!%J$$4STNv%;D`El%f;}+pGT8LJnD@`(G+;>x}r& zLeDHY`G`&zQwAp*lW4Dyt=U~JZS-2B0YnJio-viCD-$Fy6qB{=)=UPeJ~Tn|FLVsxQs_rGw!qSP4wEHix<#Qf3j!xyvfj_i=WdyL1HKgLCj=> z{}^`BAiN#%LkAeDZ=++|H!$soFWCO~8IHT?%gB|-Wa#-=@Ed<9F)*d(cDp~*h)xGWk$qXNdlcx#6=99M=@&<2iZ9Z@a(2jpc~B^0GTVDKYZ+!)|D2O_2wbG6{jp zcfXN>YzFi#f*gy7*AI{mp{pTb)iLaVf=x4dNQ^&qsfSXrc^KVbz3WGs zTF&f6Yl~^-q#@jJ6>k^Y{^r*|@7Kp)4f)oyZEWg*w+Q?pl6@ITIUp^#QWuL#{2FOb zT0P$NJ^pg>EA{csh9S-R)Tz*MvwPP7&Lk@vU_LusGE~8vRj=KduP8K3ttQNyVVl5F zEh(@#dAT@S)6%}|M_$|VKa1AgZe(L zNN`HR3aHRa2$^}aX2%dKep=)#^3eZ4Br(b*3(&SXAgJ(=LiK(-wKP|wvpN*L+!|%4 zH)=5ol@C9>scLAB?MnEPgt0EQ)yP!T< zP2T;SD|+!?qa1FE@U&Ql+OStq8p_{Hk{OCk67!f^j&5!upwul&KrEKT_jTHFUoGhj zqQ&PGkBzG0ymhk@TpqMK9eq&CzP|YEp`>bf@yD`0;;vC> zLaI7`^`vk-C^?fqJ9A^BB+IvXsjq2?&9XSReIy^wes04lpdcBvK}nF8^B4LWA7bAj z;Q0I-Ct$y~RIfi-y-46RTz{luf$EYd6`Ob%l;74V&10CxgdCk6niarGq~=95INAIb zZrWb2gS#Iyv^c1873+kG2t2CfrPRuf6;G$D&V|hL?7BVh%9{B! zvp=Q^tKg9f?Q;qQPXp*=Kl_&iI)kx_L2HdZZrF9eSIGELs6wUXeNX!wk)Z~&@Q4nC zEgh+uggH-(E~JZk*{&Cr@ua&s0aq_<7~=(f?&r;ct_W*HBtTSBAPEA$*4 zRFCA`2U6{m?J++Eq&vT#2|G-7AfuFJ+3EIc<~{6HL|@lXZPeX5XiUiV@&!&S3CE}Z zQ59H3zsQBg!K@0(av77uP<3W^EmT!zR#xxF)-RJsr+qRGMLw>k-HUbmQb+o6Js(>` zm@COFc`WI<&6Nbp+jU18s3Wg(wgR>kc&-46p`tAf3pxGj>8k0{AH|KnUED^wu)REu z=RZEig<{3ycNtab|k!26z)Z}|)d|Zq{roz>ySaVMRcK+j4 z1kk2G6W#sM=)H;kX_3q1bmpVx5|M@%bsPa=a%5@RUu7R& zl@RjyeP&gf8QSHI)sS9k&TSx&BvJLM6@`^t;*iS8_tUBov#Q^pQa!SdyLdF;8I;Rh-#N;>-H4BaL+^8Q4-rh|=|k3KP{-TG2Yr~H{G|TL zVD3#crDbT15iYmy_c_i-OMaa#aMu7C(onx)+L_GuOv7a;Y8)z`{Id_U9vjDaPCE7yO*I~=my`_V^W&C2M_zmknTLrvX_a`IV0+R}0R+gNa z-mR%YA*%%?ney_XQQQf4BBx%~`uSTFm)2Xpd~I)lk!xueX7`j%aoQ6cG4@-qq-Ay1 z47fpoZy;de7W<9|c|qTY+VE0v1l~2-fL)TguyH@BdoVW=$~+>q|0OJJW9Mt5cC)u$ z_wV#EYq8JXQDq5qdzFm)dc{myLiKDzRy^7~)YmIHmNeAqg(sT>iaF(W?=nO>0dyKQ z?PUMGVrN(+>Dq^%`}-be#efa{e#+dd*iP9pZ?Ha?29-ZJel_0HwZsekmGV3k6mrNx z2sA#<218jMfjjEbGM=+MSf(tdM4sWI$OOg5!JNoDTQhKG-Tj`QF_;e&~bC~GIB6TcRwvcOfGJl@mFt`00M_n znVQ$*Ia`TFijsY;&)$uny^$MbLiZ#d8&~CZ;5v!Ot_-2LRrvzvY38}YA^t(lpcYBV zGrrRKP@Ucjirr_2;3Wile#9iEK*01too#Y(hA&GzxdEM$Nx?_0z-Z!c2E3k={m0aT z%Z3+(vCHJgph50e``&y@A2Z6Jz5ZbT+j^1wFGw+z-PZ3%oacE6X+l%{MBg7ZAK~bg zO%cu~dHJ}lQ;Izne!V+DH*BqvKKen`Z!nf$67MC(al*C%rBBpK#IV54EwMN_gD$t` zn>zqxCyz=H?qzKpd5DO`Az%#BWR7kB{$+YRv?b1~gn%AI* zTb)cLcyt8(A{Pb$9ctEk8}F5&o`~JVDhyoM;jFzmeN5ZynXP$g`%ZN8sX7)zJ>t*D zB>h$HU`d+L+09?OIh7%KI6ac&2kO)$i8V_{&4dLFL?|2PZMu}}@g&@T$GX{@h@chn zH@mHm4uqQEn+PZ8gxww0vlH?2e?b~W_Krr!x{eZl`69b5NFlql_)~G?#mskkc4+L& zBpn?ioqGPUst=HRF<-Q#RNrt?u+P#QS{xBB!o68h5bH#-7U@=Td)`Mws~r9 zgM5-b)=wpgPm)-#6tsAY^r{4aD7~z z_1KCXweZM8NaAB}pm_MWBjmFs;P^AFOnQn~<8D_s-`PBwX+eG=UYrq%@cYfRl;LNk zso9j-L%3F8YP3B~YjUhHRBn!U@g%cp5$F@!8QSR+Z~QJW{e1CVN(y^v1E|4bkFoj4 z&-RxqKV5aXIKI*>$v^j1NJCNks}5t3t~r;A0M(S_W>!u%^rrPG4X=1xs+8Y+yZA4E zLoWMovK=xGrweNK)bC{d^k}Ja>?8~-_i8D29LkH-H(u;~hm;IWd@Zv+oc20y)qT&G zDt>eVpIF>s7Zwt>$CmL3vJraF>gk|p?EqJcFRq?GFXcgG_7|4&N}Nv!0x-97w^GDn zhpa_(W8UlCt2|eW?`MeKOy$=J6)O)+ae2O8FP095Fqq(g7bpzS9$-&$$dw zvgR30Pr&GpL2Xpyk7f|b|6_B3FrRjAmT-ZxAEpUK@>*rx((UFLaZrVYUsXtne~i?H zpIZZ+%+`J`10W2WCKp?@W!~!? zv+-uhW6eF;pR-ekv)^Od3x6zMkHkQ#IE!2R$Ct~3bi1MuGXu&CMNl=>1>#vhGxFG%zU`ipJ1b{S{U+-L1tfKy1^2Z2UOclZgs&Ncf;;A z`y51DP*@!84uVZ=2IWX6=kW8ONPL*Gx54U0^^{Pt9TZhv1JM7)UcTva_Y-peU0Dl@ zxDb%X+T@4rt?16J#02O>u+@=1g(MYp@g* zm(o&nR}mABrwq>3vqsi#S^#il)8v;eF8Wo%8GTzAP0QZ-SUp}eziTbvPsYxKgu6ud zphVd2)D_LkWr~rCkpuGR>_=}NstnD;X88x`z@!$g2M->lytHtrVXC8ecai z|3G@_puLCh280hxN(VskOb@~*qt+FV@Ir`*xw71cn-O5&^Pk->&>ErUqaAHZ-R zojEKaoa_-p&j`58r%vfhp64H^$0azIoz!n-e_*(0B*zg zilTLqgVTG6eRW1S)a=49Mjayt}nqE~4dL)E)urt>ON zrM>WddvvUg;5Wj*{$rwUATKHIfd<$pbybL-yq|FT%=fAq_8tU()^vzWmPU4`Jd|rgF`@ zoAi>y@{{%U#$e+SpV3BykK%<(k>P2zuoI1u0H>M0UfXvUz54UUJyaN6N8g{>+)v9$ z8rkHhVkT#nB>90wPmkplZbyvma2FQTyWw6MO%(ahl#}ZYS%11vQ-Uqk&_Jnb1X$~~ z*uzKI%LTpDoO~VBalt%#AUD2vX1l9k)v{a0ZlJ*nD_ks7*CM4Z%2zR*%WKHUH(goi z+jklmWpQ;M-(46Be#CNxsJNli>+No~R0JK_0$yFGfR0-B*=*nM7cCkgN%HP0NFAI5NQ&h&r{-Gx6(tCEZiuIM**eOI_U>jPUK|3}0OxrJJlwk&0mCpc;%^ z-*F9*V@Yy-TVMHKzyQ}Mi=O3#{d=vxQ*-l|&b!-C>+D>n3-7ouZ{Dr#N4)FLW{%3x zK{{6d+?Y`=Nosn7=2of{7K96bDqSUsM&d+^y?DeT(BxKDB~uy=21Yku?IhZ>dNtPB zWB1>mHpDKr?WrswBn}E*adJO&GJ>B(4L;MZi6#q8rMioVf?nnqY%~Yc@JbMnpxSV(&x9bg3FLLUaZG!wpwlgY#?xFb^OW3!mpjtY_W7CJ z2Z!B5;q1BOpg!5$^Pnq2*zy*YuWz8@84a#;&t#apqsUY?rk+<=s(jt~od%T1Os5^kEj#@9Aa#TLF|>9eQ(5=s|K@RLBRA> zcw&PHA2z_^civQs-qdF#UVEq&bbs4)4FfxT>uy~Wx6TWf@2N~z_$O*ULF0J?KQxx3 z)*xcd;wAW!=PyaJpO2xn~!GHtZGs0G`o(zvNgK?+CMdD%# zB(>4ZHV?i-)_a3?LtW>kMN!DVl6$ncv?kOWW5ZkB-S)M4r?t}85Cp1ejlgX~;WdGB zw_d|Bhz-RzOi>!sx{v^x)ZhxnJ9e6BQqiolaBNt51Wn^~!N%BZyU)1ZpxWVR8%9O@ z(4ZWZav*a!@`2%KPlO{w+-m6KR$h^Dwg2YZroxdD!)9kH8Eqod3v~OGBF$wxe>w~z zO)D;5B>iC+iVPMY`El{yt7^uYxtgZ>f6bJJX}1m>j&d(v^8IY+wB+pP;>*A4<=kHE zZ0xH^^%}2Xt55&dgVpxES%mFy0sGr>V^#^aQ(PEydIy?98~Fa`r8`fPaNO56e}*Ll z9GzI4NbU}>?>%lt!K#~aO!ni*tIrLp!vBA~6d3r8TNpBP7Wm;!<7cZy(vMd^43Oy* zR$)q%{EjMKDoyki2eUWSP`6)y@hQ1b0DVbZ`?7sx9JKw0MC5I%j@*Rdi?Q5>%+pQ3 zKDzpTqduJV>@?UAHn@~2m+mL+cgiJV;-m9}Y&|r{u>zTfwcV|#HT9Io2v$xYk-Jwe?{;@*uicsrwj6Lz z@apbP-G|YTJSQK9RfNv~ka!q*s`0z4v$xC(05Lfr^QT9QKp-+aQE30o!c~w=FPxTzo zful}doOGuvkuo3yuuo1N2O&L$2s$+)xCiRgeiSYBWNWXrI$HHm(7fwJec?eyp zKaG;%MS~%dW$pe_FQm?|+!yj!n!btRd@Nl_LZE~<#bI@{^OdT{q)(FduVwm#eOYtu zkKK>U%-R>@^$`*^zj(LFs;;gi(17B}X5-g8)e1|Q!KXiNF$=x=RTEcwc9xLLw7Yrl z!}0zKbvTD^u}P}_Or(l7H+O+zB%OC>;F2ZAnc157)#gQ?rF3`iyPi(&ILBuxl-IA6 zHk?vUSEKhwentOfN1MGa@zLLw&x@eqJZZ=f3ap^L2O*WjVV43ky%B5rL8s}PH9t}a@2is7{^$`)q(1MxIQ`ev9QVY%3Dz0y7D7cX6Lk< z#D5b}dayPrSn*Zg>?g{hu4+a+-whPdn^MPQoE@g9Z(yxUvuN0x>oyj(+ankr+x+lf zda@;nucbA)v55+ON0zR)=P?12Q}ue$6%QgLEQdN$Jc|@|7D-tA)@4Fp zKd$&!!E(4X0DjKzSi5C7U9&qF-rP{=3$Mt^vbn#0bjHeZww6-Kcx!d#LDSwo$Sg3A z@Pc>x)O+Y{qCbKaZ@N9h`(%Ul{1w_#>ec*@0pF8$nSUJf#l%hA%e5L_8(%@*&GM}N zK1$WO>*w%7()ViA+Y# z4`ZSkPdlwmmpzP~gi3$3qhE+Cv5Iq0+CV#Z_Z#V>u$b4i%h=?kJ<~WNckNO`b6=2*ACxOd#-qX`xY0H zy*eV5`YvNznBV1E8qA`eH-f=4|8te}l%soEg*|&KQ-RQK6|+QuTMMtAk-y5hbmR5y zZM_nc6Oe<87i>J7&B34lsY?ZhyXuE)`;NfEu+$lzIJ%MHca{K}d^4@16ZBp@pzO2} z+Q!7&o^sxUK(hFsPi=4Wk@JrEdn~?_$S#GDfrXb-4zU-FBFi($9}=Vn)Hq4zK@l_EUu( z>W-+^g+IMv{>h{$YMW?w9NREo&s;6LuFFT%ov5EwI078yY=4aNK^_@?5wZWPG?463 zqM(_-lt1B}-5l!h7sHpp<}ZZ_RZo3s*b~k8lEv%Edv#cm4|gDxWuH>;#=?qYi{c27 zNCWJ#YYrhW|5JR%OuQD-jN5#{bvA>Wb0j)9fSBx#yn2huZSm(7%2$OSX(5JYtbjg0 zXc`?Vj0W2+C{l!og2g~`OIXSr3(gC{aShUx8YI11@QEY8^(VXu^mSENSf>f-{!_Uw zJrOVO2lvB2ZMI%UWGrw3^0%n$ekQ+Qf)?n%ZLnTqEeTP(h{JGT{=qsGu zyS58`fGz)fr0L_x#eH^WdyA%^0Y%Tm;xmy`oM0EF?7}+<3Nhu)Cj(*1$)a&H7C`^P zQmgD75}NC_1W2?q)Wb;>;r*hs&WZ*ozeZBCvp*{7)=YA+yky;-&hmSaUQi4bFUUeJ z?Ykg;SQKKmQ8m*=y5Ngj$D{pmdlIIx0D7{=c5HKoMm#veu}!Cr;q82~j@1)*M$(MA2jg2MMM8B@IwOv=+Bsr$4EYciW@@3l)V?cc`mS zqx~%@U1KdoF2rQfsz=!cK z7T|kI>3!BpqE6+;zK45T^jL9uCZN=i-cL;)wh{v5Q#}Io)OFBDu+^C_7i&2kX7xv^ z3ig5`IxBfv2(vA>Mw}v*rMFF>n%d?fN~mO@J_S$pkfxzbae;=!jGG~mlB7uYQyy#6 znbPB~w+TmkuK-2udTDAr){i~y)<=vV_*a`F0NmmsZ*EtHybR0thGfedZMzImC;|C^axPZzcVA_<`L3LZw66)Z!rf_W`!2U!;0rNFn+LGmDj#TuvA3?3e|zkl$0 z&3@?}6IV~Eb9vdA8iLq$Y;y9*jYVw5&Y^W{qp%IzzQ&F@Tt*R2T@=m#q+wvh<7Gxb z?}#dED2*+>z3x48qk?Ko`-l@C#fkRO@6+{4RfD&}b8QU7%- z|Ek;a)vwj5(h8I`^L1ku6R^p51WS3PeZYSC+m5Xps55aw1Wi(Og~O0Gn*)~R!B|xX z5l9=CuLo;KkcZxWU{b%sw&@JHlr5IOyHWT0jG%*0Xv_85C0y%*Ek>u=8;%Y)nJ_cP ze9uPZ@mi4ltVwUenM=PZ_WCZn-ME7V14um$EpU1eEY@w`NLk<5e5gD@GQX$&ei}2G z7*+D9)`q8wUqd5qUO$GpC@P^VW-aW!6081Bu1T|!4ovHECJ~Hk+*!9BoT`f2rYDWX zP)p|ZoWCV^z$Q-!O?;fu#+ReCyCSJ{?46eHo{#@4E*Lg~7hDaKjyK|aFaL(?H^^X) zzZ_)0eVveQ)}}fqy_?5(;PGWD{eQut5sn$SOE2)jD^gz@ffT_>NwJ@}&#ZHirS--QW1V2U@pf3AR2seBF~uZrb;HZbFtCg4SaHs5r$S z#z!sY01>l%`y)B5#;5w@9RYUk&N>rDLH0|dhknQ3Q)n1mOp5m>!%U6e4423#$ zO}8)K;;kPZ19Fot8$}%nl9nY;UjET{!YNnR?^`Sxo1k4!JZ+{VYu8Wl18y4q$-f%%t8}WC81WZk zoZ@#Ap_=1RZ4+MNimXsZ>+n4B5#RHO#zw@t*lguq>)&xc76_)>i#f9XR%Wk1#r@QP zVqi~4p9`8@{EorEP0hY7N;PP0=z)e+!Oze4fGxnaYh^f_tNPhC-`c_RThG?WAgQY? z`9Pw_MNY-7q-R;p1<~~%H~U*;0eue$qEi1zj!(0C3t}Y#gmStZP=>8gAo!dpP@Yv3O&XR z(~gsupFr}5l&IYZilOG7YTOR$^IQQc~9!{dO5K zW30q4V-4SbG;*UHA~WE{IWrVf!`qJVh9oGwqd1Zg4Rm1O8OU__axf0x5o{*FI~+f0 z3R0T1wym9M8erF_nf}GET7zbdB&m4*=waRSRaaLGaex;B@UfU3*Q1nRGmYJ9kw}`I z1?$LMJy#|n_B`Rb6}L%ri8_2EG2gb=MMp317bil<#8;>l5KvjQZHsuaN85Sif;H>wW*QfigLM+|vHG!k%Tl8Y z#MXk&IytHa)AE4>hc*wjBoPl2P|X5^Xyn=shi_B<*(v!6NI00mfBJdC4LT*tPZ3WyDyj>4yR# z{F72&+`iDSyF4DBkdoG*{n|ejv`dOQu=sm^rT2jr*++@f>fN+SZ!8D`H9`vtOl>k# zG(%@ri2!moDz>N}K2NW%ApJ~lN~kJ#7`Xs=Er``kDXaRft)2q$ozmz8rsmLeo?28W zvIc!p#zI);{*=3t{FpL8k9h_{-)s_jr61HgGw)7rL7gS{Zun~{m)XHR~!yKqt9JbxonSp#l z37jxEoaE0#| z`cB%?Z2Iv(M@;XZSpm)Q>x@y1oaTk#$=s~U-mSbG45m!>$_J6-`V}e1rGWgE#kE7I z`RtKdggSB3-Q_C98*U6aoYwQwFQIyrv*2^7#=>`e|h|o+B*1E{y02axsn9$Yu;qwr+=I-?2df% znoKf0Je;+h39dH<@;=}outj;`>kMLD|M`!K6E6DOPn9ZAL;&2k8CC*j>6Tx|K#zOPq!t;#d>0g1Q8FzBZ%7cAnO4o22YViWQ2G?!R(MiM=MC<9JPyA=NB1E3h>7GP2_@G{9h zFw1wumTOPY^ITkhe!vc{|8n4g81rubxA-I^rei}h^EKm2I)ha_4_wskS^4tpi{g?C ze><_Vl;^4wv1ISNp$>KG|Qbr6p~I z%eHIjxgSX&F=iro5O-fB(6YuRV^Rr^-thWCCgSR}{+d#`{Kw8KY9gkfo87jnMW ze05fKcjXXG)o!HnRE#@KS>imyqWLWHEik(9Lbk#kC10EE&e}Zc*(v|-AoT|K{mY@A zjN6Ttj6W}&^fs<({10|{x-&cmpAL!LW5NK;acSv{qqnd8&MKWtUm^y!FUQ?PHTE~jTJJRm(`q+i_g>x>Rxeb&Ffcf!$fH%sB*=PEH*ZH4l;S|D$n<4Ej2FofpmbeT#6e+f_t1`LO- zB&{LFoFqXX`)`|s#5G`~F!0~f0)mg`i%M^{&Zc}@w4&`*#BY{i_gAP8z}|0gKifqg zjPqxxkBxnS&+NLt=l;f(I|+J&%!tUin6XHoELE}@y3j1vJGl(V(5YI5@9s2hL?tIr zH(o^mZm-mM_BZ_Yi`Sbb_elZ)BXI^TrtO9KLG)PwXNX?rx0LC(Jgf9qGrEh@^0iwX zZu^XO6&E*g^5o@pkBQ3u3-&b&Jb*o^xVX`Vw(FyI>bkjP`^BR_3Rxj$%G%XdM-EB5 zdh$XHb|ET3?{ke@teF{uB`+HGf&eMs0(ukXooz%xB}P~H)Y zi%6%whr=uq*l!&Fwl>3kF1)V;I3`qVh#sR-*KG*or*nHxB)e5^465Xfne?Q3ZnONb z0D|m(RM}pkvwd+AcC);=eOo4W)qkR%KLjAiDG8@h3!54a?JrOV9RGUHJyB|x5+fJw zdw!MBR3Al|;D7c}nM!2d5B3Q>E3(zO1V+evY`9 z8CqV>20U=VQh}c%;;-H0`Y-leIGTk4{&`aGzIUB_z(FH!I$Vzf)NeZEKy-h6_j7wY z+279_=P9pY|0a>f*;$x6!j!#JvsW@#b$x{D~*Wx@@g$n7=y%Dgt}4>ZizYx+sPS=+@H&{1|&lWa$Ba;mr%*_3<(5VWTVU( zkg?EpJLyfiQT0GI2CBSGu%hVSUG4w5p}Cvm@V{PR<7ips2)Mrxj}Gq0N-1qYEX0Ya zg~o<#Ovs9G?Y!&W)BMOZVZZhHs{?=6)(WOb#r!dV3(z>-X8Uc*?)Cxa=XOLUNAuzQ zJ`?3dzTDl}cqNBv{1-Cu)j@+r=RqB!yX%Vpl@zWyr&O*V!NA4N%IwP)KC{^*B4sKe z-`}x?2d3v6t3)9~={yqD5g}4-DFMD`=Tsss_dimAG*i&+&QZQY{oOVIS+;Q|kUyS) zXVG{yIrl>r`({)pKf`%6ta5`rwbt;c-GkH&Ob?RSQ%d1#(5Y;~*0R112pF^Ye*N;z$&@m&jngBJ##?V@GBPpe&imU;+N!jgt~^ z33tGy5w`p@?g3P(3(!kYjut=9=t=ivtX%Txi-#Tlpuv91>o~evc&mz-A$o{#DG_c6 z#hKej#5Q4bj5~@bOqwt(XyZz$^wBJ($cELQIUe)=WT5!VhWo*yT}DVS9#w2g$_>mS z9Tt;xcH45h#|2vHPCp&V0)kRWd)))o^vK6sxEhXt*&nyah`>Av z!KUO4Xj>kbtC^5E0GfBNgvH24g1#!|www>p%-`+60N?)A)m7I(icSPj%$<+2vKi}7 zLjT+cc>M-izqM72l3|*sEXWD8;znj1R29{`8N_H?fTM6!)tF}WioJg+5AqB)~QE90? ztY%NH=P7qxfHJSJTX%8)!tp}fTiCr}|L>)X`y-CYT6`;5%~|&IK5nmR?_9G6+-0oD ziQ4=j#J=N)(1^C(?zp<_)cE9}{^3i=`Q9w3Zo5HC?`(nXdq*{C=cx6V5k#+v- zJ8tYGe(k3K3Tf|J=|+^?@m!4jn2Tg@Mdc|{Ku%>U#OA(_HCs78hUM_O#TblqpB`cndv4BQt1i& z_3|+<50j-&il;hTg|O>tmVRikI*G5y{{k(ET$BD014~cWX*S#82UTbnFD(X3wXx6D z#3g9$y16g38qnM>?1%rM@UPc;Qdd*sJ{CU!R7f6L)Iy`p7*BERtFeQUM|4q$b3lGB zG}PKH;2O4H#)x?A9-bI1@|jG|?~+zR=IYJ*B2)MUu5W*f!*+_R`{GNqYizrI++PnB zny6e~sgz0pgQFsPMJZvAkS|!cKd2JWA5NOhp^{Z$&oc+OJiqVz{Z*;Z-ko<*E;HZ; z)a1m(1h8EOE1WF%6Egk1`zxXaN(5AwcnaRrc^KKrPQ$(9anK3H9lKuDY0kHfFD9Ty zvAwkRk00N1h?n^n8NGhgd_2DbtJ}@)Uy$a;$TW-ij&iWA>!3n2Tzdc{?rPK&`HqZU z@L&NI!wuBcU3#?!Z8Y^_b?9XMZ+{#}oLs`Z&xEs<*3%QfwMHv|>MX0fCFJsPZt(Ly z+ni{+0RmR0_7pKCi!?LJdDhlQu%56}^Eq2)fW@{+)47($@9$Pr62P}+pkE`Dpp;tM zqJYzOmm%Dc*O3x@I6TKp8N9~>q!|EyOyy33?OZdFBq#nVIu5`nen&=CCZ6~RQ5+h$ z$t3&)OW;o*8};SbkJ?3FJNS2LIWdjJ`EInQ* zYvK~BJpQ@HCd%PA;ZbHeS7XPW_P;0P$}!sn9l$mjU?FcP+UZKpawFAQXA)!R~(mkB*9QIzS2Vy1Z*RXt;@*Iavp) zAXd8MM**HY-3I%Ph_Xfu!}U}z*yU{3y~j=@;Kk(nCXfA8&~WoB5}Q`Zm21A$;H`&% z!;JUYMd|KT?FO*t*;tikO7(stoR%)^$0%V5jPGVd^bO8GTScd26@iGaZF;^!O#Wc1 zSj*D)r|iiGn+^tAqX{ zAOx6;30XdObZXvnhe$~Vcjmy~e&Hv)gQN)5Sncub98gtvn|T>6ExujlZ740S-KemWfcsXUO5zX-4Hb|; zv<;ufcJeG^=u@cs4;?E2cH ztZ`oOxMRn^#=fJwJ0l^OU4ihZvsz%`y#F4&JzF>?(CmH`D!d|EV32}Z* zrVZo|9evK)R`dActGooxg=~&Ms=4TcoqF6fdoD^Wm@2R#E`|u+>DE}44;B54(4OPd z(^{Q4zQ-yc8yk}l1~8l#Fxr3^mr>{1?a=Z;jE~i9{4`WjGZUuwJJ*kGAAC=h1uMHJHj3SP#x1P5Wp$l)`%Is{aJe~c>xQhjME!w$*eXQ`!P?`IDd3Cx{f_LFS z@?s%4E|er)2GQKwdY-ZABFae=fLncdpNUity;JSDWHH4)`5+C>if1q83MvVbL`9oQ zan)8Dgib`OQW6cc`a=SRSn}dD`oY>KY|OidT1Doh-*e)dF8@G=>iCYZGwL}^6{RB3 z^1yOw)3si6OhF(vLwR3bi=*7m)7thH zkag00pN)JiOw$T*qn}{z;wf^4eSwXAWd*qY!?P%gD6Rg} z*)h}z^@_+3aAp4IO%j*_d+tw&Mh$uGk{cFlGTHmY(a6Xij7X&YV%Fd= zvK6v)#NL(88aazCQ#n&Gf5vm@t>ltm*7QCL*q8+Ltsq6BAE)}Fan{wjmdM)5hQ54) zMk1Ny=vjGTU3d(x8&I&ceJe65XC~^wHrDK@ID;USfXaPY7E76jF_V7})!Dcq-{R*` zw-vRudmhF9-&`nCU7%>$Im#uU>P*09DP4KnIIDsdxd19bN|X+h#FNeKXUHSFjSuWB zZ2&mL5u6r^%Y?+6D#9E)w)l1mRqCqZ4;pwG1=P)sOOnCJxSJ16GsNYMD@`pu%Kh4p z`soP-8i-1Z^clsJiJJEV#T$0498HmAetYSrpm5zC*FWq3UfL}95)TPM@W>J!guIsX zyTJ5Y4dK94Q(JYYIW>sOYbkpSG<9KO&nZD=u%R&opw@$km9tPk6si|I)((Z1ZHMKJ zm$|~7D@Z_kE%QfgMD{*7yeDH^q2t1P&P``gITy2^6_t32#t^gWs?GVIoml&`ONgL7 zZyFSTLV69mHC%A1ovXX|=$!`8k;cuddT*dKI2Rh@top(_XX&+-CHW)@cBe)dt!m;= zyT4j>y-HDknAtz@T0>AK<(}+N+R1&uC~bLZ$0Z7KVmY|9!?a>i1Wr-SkF*C7`bsj{a`{W znfy@nRJ087#XW)m5hl|8-}rVEIA=R>R`b3$l+dV5yg-spomaIF;Lx{AEl+r!kunM0W}llaU1>8 zVNOEE9U{(+;fMZN8XIs65o5;f;qpT$2JC;mbaSvQf*_ilWrW`YesOJk?L8EZ^t><| z|4XYpl=(eZrs>#}Pw724LwauBq6CFZ=e008Cq+QWG18JE)Q5g z52Fw9i_cp0T*Lk}LY7p{x=J=c8S z=TS){L6TjUW`{OkV)md+te9%m3S-=r%D*+kxKxXj1tV!8pzquHNRr4bzAt5lKRr6f z3XzI<(vlBl!ljw5Dve3j-n(>pS-)MMamneR z9FqRo{uzmcAAKx&KarSuO=4{Ws%x7B&}B2Ar!jM&(3pc@5zj3$>7Qu=Ac7ywc-o=J zn4oNsv{)zzlOPn+(C)*+Ew3{ORTYZ^>9O=^D9EwPn+r4M7&HBIi3X+X7-QK^GcBb=X0Ts-@pY;B{~#CMXF%(=gB{Rov`=&>hp-Sm|+ZF z^^_+>r(+F24P=qKCiAG665rS%eFvo)`t(G8XnFUZ_P3&waft=;uSphSU0iuabUVHR z<cqn0qGTb=Ly4$$+sOsr?8il7SqG8dQGj{8X|y$ z`2sv>MGl9i-hClBc&G8GJw{_i%5&9BDq8Wz2}zM6Wq_BI6XscR31|973~>>Sh4 zJ{1dlAPcZ1jVHhee$AdN09a^Av7gSj6mYiKYIOmJjr)VK`?tAcI6byw^%F9rzE-OD z{sZBdY*5yO=Gxmr3$R{`)mYD7fIaU9U|z1U9+(Z58D5Cm@dl0~B3Zgq*&o}(|NMFW z78Yf9+m8qt8mTA-f~rtl7cUC{?%Pfuh#C;WNO44~GCO}T)+fIQS*7Y#H<4Y!(?3g^wF@YOCoiK+Ss167&u$Q%daw2LXHCo$I{= z5Dq@*mPS%EnWMf~r>=ax;@9$Yx_K$NHuj#S{YGCmP6V}UivZ`W0bVE2If z4i@UWmIhY3$-Gom9*G&ut!j{OzPuDWpU$^l#wiJ;Ol$T|4MAjBoN3@fCZ?Qkf#mqN z{+F%cl*go=%ZJZE<^$>IhJBNEJN&l0v$-ZIn(Q_9u$jwOlO?drkPvDl$L>pYkGO+B z^NVuPMD=^61&x3q;H8(tQ%Ke)^jp}FJtd#<=GQ>@MoR;kq(69(T>wmh8?p~y$uP(e zd|o*LNJ;;rN(%et{cW~p4?DV~+f|OYQ7&-27N+jor7SschG181^Nt|myp!O&*65(` zwWShpXVZ+x<%rS&qU%D8Vt%|PDutN;+Xp9WgIKylq0k9lP=OlBz;03oGN>*odtGs3 z<9gsdiE(kpcg1va&JQa-T%l@c~=$NmBSBM}W@@AoGROp)H#!-TO)Fbc~!?yds&0GY}y)H)b(u0T|Dc zS`NFkV9YP=-H#RMDNcyn3=gjoz&^{p8(=S!cX=Uo8#njf%Y{H3^rD#eA&v~N%XsmY z1QidMQ}m9$2iNT;=ZGz?XbjhrAI3l7eBAdlCqs*XZ+0@GL(@EAI&nHN^u)=n|L0V| zxxe%?2<0~FDMpWn-ALorXl|dib*0X6Qso9R8Q-#tf~1NMka5xg7}`-CrD|8WJwkBF zL58ENRyVeMHiNz(lxsdHmfQ>OqR0y0&y^yh$ZR=_)z^h@YhmMSM1_F*_; zzkDtIzgw>?YZ5v-d{Rht`Ue$7e0%~@2ozl@0z)^V9t;E!%5>H(Yf17%(xa+gP7wYH zQOYths+Htb7q^o*K7~kjF?T}AD3`qti9+zuI9>K{ub%^9{;=jBRTdZa1DIHU*jVxQ z#=M$ghx>!0`p^$wGZKF3z8j@%)%TdMf!RewU{YMY$TB>va>@s+^7S?wgEywBR4X7W z@C0H5lLy3FY7h%gD@6^Mbp5xdTFO@7G72RlLIWukGD_1S6EKm2{_3`L;}AnzEO4bh zgIK!>T984!x=ePr+qO|UyeW#BpTo%l6P}!%>n`sWg{sXs1YcJoasZKh;iD0QqIP=l zc1`xTpf6;?mlI(zl|7PhONQf-fe1`IW9lm)?s31e^8{|nx|Y1#A1;g9S}b%@YY!W7 ztd?i+oeXkH!XT7TQ|5JsEqDN7@>z|(7psLlK)s>{uf9njUj^pYQvyWwt*rs}Ro530 zrDbnO;wX0+d)6zA|BFrFJZd5ri2X51E)Zj@aRyZ^?}S>+nK>R+ZIg>oqmts6-HLmaH3-1mKpU~%R~eDx@l-gr@l^x0OnXFL1JXpuNhhf z!Fmp>zm3i?HUYF^bD7V8FN`G4r}+~gkfNgsGqPlTHcIsY3VpAt!vNC{kUw%mb8AD7 zF0|2)0mM`$a@p+bUxtO8rSx6Z{^1i7bqKO=+t2zCarZVwk5< zCAp-%P_)T_LprJDguG1Cp1^Ot$_Wa%p9<*Ca8c`M!Po%5Yi=+$Yyk_6*iM{mPuL)R z5qFaA80g6mUjJbdZp}${?RrybxMU2nNEf%ySj&%k3jcJVHJ*=cq%(QT>{BQsrrWQlcmb>>P2X zuDbIGD@|1wWLN0n0P!L60+&F+#2`Y?Dx>0Fo)+sRg7-_+AMSLL9 zAhtAR66JhU=Sq>x(r5^jr1i7MV}sc+`e0;Fi^+MvcwHNdfwY7AKtQ;f$1A3v<3bz) zRz}5}yiMb=w_z86BmB`<<%8y1vLi^; zo#qSYgNa-ekD;{5!U0CbkUWuVSF)Yd?h?FmB1Bk0}!~xU$ahz3jsl9hcc((QZZ{w0BzkT4q z-`Z!ILIMCClT|4Qrl3r8)>(&LVUkjk+#>M>P!z!52MP>@@*$HD9eYR`Y8Wfqd`5Xi zIKpWSV&Jn(g&cU5gMTYfL#H%G8|&tr^!aV)8v#J(XmMee$;L_*ntH32P)2**&Gnc0#mvjj((t=82!m5qqo=CET2%a{d#);!|@->w0CPdc#so0hxE zRrp%uJWwGXYH+`Q2YAg^YMY(5a!CWcw66BeWn@O=r%&J#qPJ6gHUhlnU#DQHzKcRc zFRzaiu1|LX>vGNa@9Rz8?gjv!^{Izu2Cp@084$dbZKCJ-RtZQ?n0D2{BNH0-)|`{L zd*%6dfXgQ{>JD=g*iIjI<=<=EdyIVE_!s*tZSWYzb~e^PN%V6Yjjjhtb=uW7cSZWv zbw09+r)?0@KAB^)E)*CFN-p_yE+du9qqv~SN+6@1whqqWNG&)U!!Lk_hNlq8+hLeG z549w@!dMbQqxAwBhth&1TbqMz3N64V} zXB!^3=ZK6j9nHz&a@uc-&BxcBGHJ7`Tl*igO^)V2WT-vZjTDjg2m@x-7iR6e$p4?s( z-s=aPa9Eo-f%_3mMhh&fgphd3Oj0>leKW*W<$wS;0P}Ao_t$=HRa(L+&2(Bw zUsxHwU^n;HV|Tu{77d{WneYVJL#6myVbdgE=mT6A3V?e!|6F`Pb9Z=ME#tN2I2h_jx z?^oQ_b_yTgY7^9elE+nZtIEMr^M<7@Y&{Bx-iT9~FavPvDcV;j+=LS7K_XP-XtfzYRt^%*=_8C5xGIJsiEm#OcKCg&WHoPaF za4m`gha@$&IQ6lHQmLXAqmq3x#WaO;fGKpLpbWVlyr$hYG(KW!xS341oZy- zeoaUywB5<2@wmV7GuJ8-rpaF>dd6Othv?@5L`Z&jUn*WJ%L^+H)P2ozf;g7RTG0T+ zN_BNya`*)6_DglWjtQ`uO7wIKkmfU*&sNR(lu(K=-i0Yy+Z1cP%%jD(k5kqVYBNqm zPMLjNa2=OC4`pO#oT?)TWd<>WLJ3yLnSfFV;fBtUHXhGX{PtHHypoj@C8re<2BJ-!$kXoIu$bab* zAG=p8H!JmY;eBo*4tu`E{(aH9(XM-L zJEvA~k>7+(ItG+^IKSR2X~Ac-1GrOrDFc4cQ$twUkJ3V~SyoV8Ts`DR8wnAo{^XZH zib!BL>Zx^{76pPC_|ikkdMLj`?sxz=%+Q3{byN|-He?BXqEPqKF_KBP5Jp?)N+SCW z)Z^Rte4TFXR7>$7h!Qe2s8vd=0DWA;M1I&MITs}jE@@NLJR6H4-5IfVNkN%|G@i}8 z_r&7oXq17vA=L9ZKB6xxMM)au$TyNjP;T^x*sw^mFI)h0v?L1f#aL^uTA{PC6#Kim zH3gA{jE$Y%E33bjo=oPIB;X751kGA!^3AVX9~(IvW?MTK6KmK0+Nip^-(B!X)iXPj zICM$(Iq0`NNLkp;yGU1ZIHe05&V7ylLc7@*<-KGsb30xOCaZyrj0_Tqnk4dL`EP{s z&lpf2Z#CK0Bv{po0x9unNw)lfyTPU&?iVNiC4w#+Ng_NR=}!kK2gRaoG~Q$?#$+{- zRlJjJb==`zC<)i63xDT7zni~6#?rOAu3lms1Z0DQKw=bx!*Y^x$li>j%pUS;Jyg!v z-1-U;M;4BSm_H$q!bG6&frKF>9yv(7ZfeNM^0DXJVoa3%dUmjp_tbydnxj+9!;$rDv@zaShCwiOmZ9_0T-C!N##g$zzrDc!(DXPo=f z%}upS;js-KfEEQhbroYnvOC{2v19I-U|vRa3c>vxwRIMxO;kbTSJpN2I;UD!MLL$X z44?6*+wSY|5|hfWU!=I5!#;K@N--qMSJo)VY1bx#D@RLWH%r=ZNjtv~mLSiDedX46 zjb9{MWyT=7c0jyWa%y`9!&1{WIe5q0lO90l0R3Ib29FuW1?RR%b=I+bfJmzp+PKaK5f1C7$KsAQW~E@3G3jPi6MG^TGg4VyEbX<*myyK$a;fN%pz)^a%^pf!OCaO6++ z2rw$M6m!K=7`pA#*7%sq!rp4}8tF~83C-pbj zi{vo084E-^dG!Bkt(FM|L?1%b9G z%krXml_Y(rk-ZWB-i(4Yn*=g1BOo?G@*AFfemPdivu^r|j1PCcqjqQ!Afq5iIyLZM zRSD!=bg07@!C#PfFPC$;E7V zk2sFIh(4ox^{=IvI9Q3syk4B^nUV2mdXy|(o>6kV74z$-m_&$ryr8JZdsZqw=_^1G zmtv*T4MjbLHWpfmUHZ)@akJbxg&ROhTz}tb$<;yf&lb$V39pa ztm0tr_g{NVbmNpy$9J;-h?f%un=$$&9$9+>BgRXF!4vVA2!knQ zS7Ez*^Dc%65*WH{dnFNihzCv(Wb3TR2iK^=wG(?OMMrN zQwzO)i!PBxU?h%sZkI-7pOhU_;ipM3tYap6UFLh!nXr%YO52JC9U<>uL=(uMFemsd zW-x@N%)JtrCoBS{1_0E**Ka1l_{rh{DcZXsL^H<%M{$kh{?5WN(&tYnnLfg!I*a;W zqq z4vv6LOLei9`$zu<;L@~>b#H28>33I=6@4V>Gh)JXQjc$)TM;ju%eB*)b^ITu-|?_O z{HfVKsjKr(DV=wFg2JA%^#7O1_vnJ!+v%il+OJGsH?2aKorgUh20qiz)@#_Nkcdp? zovyII>`G5p(G5IQ^53+j0r^o_Iy&Evtg{8=q9a}ooQiF~c%c~PD1KA~Nt%7Jh4?s- zbp~b$TLT^uJ(L+gb(BGJzBGh zWo(9A#5f4b@lD_(QJgP^ok?Qs-N2d@b6XS#p#$~8)BY*Q$IV~iVz=mb%e2~*UdJx4AHz6v>7@PbdQqsZw92CM? zu7`r;-jBlmUOwD>_=A$4LEBF;$D}PhkcZ=Jx7?x@H`U8Yl=rLr4+8Z6ZHDLsaLXlD z^<1f!{TUn_EzQ?Yi%U}DgtMwp{FXqd$bUadYKF{Shvp?5VYAjm9rxn6IO8t&;%ehn zS8`WYa;N`k2Ebuc=V&I$_Z0i1nCs`ImBSoAC535=l*CoOjMQ1PL_n_C-$C z@rji<mC8u|5gM89!VRk1u%Z(t8|@w*QUMPi$BK! z#ebwb`-@|#>tbe`z1a@Z&>b z50@5!uj8aTek8K7pY#1uB26EqeFDMg5n1cW{6AwX*>g7FiXh}0EMQV<(rF>@s@wn5 zcbQ?jesGoXWP6=js4N1h_rD+2I1|xuKf`ySqo3`gccnxQQ@%ij-(FeD8hzDy4x3Rk zhL8Wh<|7w`h;h*uH5txPU$EpkX-jdaS`%1yG+{?!N+aY*3j5dG(NlD4vUKm)x!ghy zRS0gVV)f4D#qqtgh!i@8LA8xf=?{I~hMPTUpM8Ii3%wEYg9q{8ic=Zg1ZJG9`NNXo z7xyZPzPhvMt~XOza!h{6k0pFYhha|2)^R$t=(19hLp5~r&O_@D?waVY22X9C~hc!v)* z1Zu%;(jRR0ei7Oj@nf@XS|rK%^zT{?NEgSR>zO6)9h4ID*bop5Mb`d&7fd4jj$`lZ z(%Lh0N^s{5oTA08e>X|`G?JqEVllaT&mP{T;?L_n9_mhEW4#0RgAYjDuN^GO6Vq@3 zpg$-bv60-gLk-oznLlI4;IZTr+ zmY~CBb#B8_B`0miGt*z(|JCS`D93*Hiu7z)HDw#BYprx+o221&Z13ILFBfy_M98@v z>=&B`s5J2=;X>p-6)lD->VDnBA3ux^fv^n<)ru}{j)9od*|}(L^$($9A65Nw>FX|` zn5yCxn64=iQ=%(=&q@5^1q+3+wUm>>FT0+?0pTEEIT=>~2tv?pi>XI~^IegGcZVA- zq9^kag+-qpz4yO5$w+#~cT{t*;J8<7@2-bV;{$ZI$!AP5Lqjvb#JuQ#m6&x=vQ6Oj zyuY)*(^rypR$?m(ZxcgKnju`L691_tBb!-bmbOjg<8SUnBF{%>&AQ%D(6x6i!x*F$ z6?aM169ci$9${m6x#N5b^|bN~6(%`SPUB`F#%`{3KI4+;_S;4vi^}MpR%c3wsd%Ng_c!ywc(CeXNx&-uB}K1v-qlv<`uevH z3l4W*v6-#+>rtw`^IQm4>IlIV+6wZzoCb7)q{B~(wy&?{PI?plfpyR%^qqu`(4252 zY#CjF5gMbkbo+$zPt`PMqRtJ*rfDH6QCMhzhY_6w-XhTx-mA+{kHz9G>C@G*p%VSt ztj1#3&9AShW-Rt5_<4GGQX8I$p{ zD(Wqx-)DFj@o0pZ0T4_>9qFou`t`_KP2Y0!4)mgU?{pF8d159VzyMJwO9?Kv8Xo7F zvNw2Q>tE)hMeizhaDgZ8ys;;c(e$?}pykln+DB7!-OD1)f46aF4Sp^2qasq5|9<5t zKcPhXO6}&Ug!}2xe*IpY7U})wzR#>ZjkeBFYngw|ZJGuQei!|Y0;mA!w1u`2^^}jB z40Ct-*N%aTgqGHnV^4{|qTkTH*!?nFp-~I~Qggq{0|5WT$Hi6G)|{US#EYJHW&=o< zzKA*ut4WEoi8>Cj-%;vQ4KJaYq@<@M-!_3lDG4tO zCPvBm73sh=+E&#`uIxW4d3!%mG+~!{>ITb$=a4Gk|NF8VBHX$r>QY%D-J9y zFRuY1RdqOu^)?~`aH#9)8VGo`2Z`7>9eoE13a;V-HdURAd9R(_q_I%To1q3JlcBGpn>6J=cj)nzHU)|A~hKgdh^>RYpj!JS<_bm>t|LGKike`Qy%)}t`< z<|)AzZ3Q!@GZ?STVUM-M16itdwd_PdIr5-UzEGrSeeS9%Bx39GBoe=Z+;!mGobzZ8 za|urYpg782_#%hv)wl@&pXh8u-eUt*pb`5A+qGgO)0w>c5^6ies@nb&?@ceVcj>0Y z7_bQW2Ua!Z^C>L}20Gsal5W3dEwydlN)Wb=-}10$(OWR^S?V!`aMj$E>~E2jydE{k z@gZa3Qpt6~1X!fX^$l+4!%L47RNwM1CXFQ@>h`g9t0^a&KkY2v$;%5{*{j8*#Ll*N zc9bt2EFOjwMVpzeEuV8gSI}jD|HHOTcWCD10|YVdZqEQHA!#f>-e1Fws`Y*;mBY$fbagR1p{}dL7zX45jgX4 z384{V2E)jFX*ggYz!|p8mKMfYj|%QYO~tEl-dJ7e0ib-_ch*J}IyrONOAYOFA}Xr3 zBzy8?CCkuex)iw=DH=d}y*>I+ioE!zls@Wmp{=zkyv)nBCYegqYX9@=Cj%DK#d)i% z`!Ida=L9&D2ks3aAJxo~o`GG3TDzOKV+C?e&N(Zz8e5d5tb1;c@=L5XpVTT%CrS37 zS5-lVho==Fbdr(~=Z%0hJiuu+V()i85^7Q} zp0;-**jyq;wXHQsrP9awiz?6Rd&zSsoEPtenoA#bN^z5(zn&S^WBrT>!X;+{>y+<2 z12@DP&{BA;2EK zf~hAazzG|`3j@#yIP1%so&du3oljqSXEP%yX=x60#G>6<*rPbm7N5U?LHWXjG-iav z;7mS(PIdKP4E4jVfR|13IsyC!;PsV(qu#^6ilKB6F0aSXWkt!rd~Ef|T15sYpm< zY1s_f;w0?WKt8{2vplPZKpM#TAO$$OGpn)1w`;8BQpqqbKkn{0Do zt_+jX3-RV=;f-(Qq3&33kMZyMLB7T&M8_r!+3hFtb57BvUe7OrLe~uax(8`&Ag%Me zas1PRwAvvdhfa>gLVcV#8JXO=YC46FgE)%ix)}A^%rY-)oq~${z*jGZxOy%-hnrlG zsO;ep4P9|aXtKxbT(NVD99ZDI+4yeEi0ktF?Ys3(Gi7QF%0lZ<*S#FPpeDcj>Ud;t zCR|a#N=qoJ3Wr;?bh-tvFE2$}3I-wodC=JEP0v}u+*vibB-hR09jcWQKx1|Jlao`i z(>~qxE-(n*EBd2vdODc1n}e5k1=z)LiVwfFpzCO8{?;rhYZJSyXnWs6adGYi1SDAQ|K30wjW2 z7&-4!gKz92o@GIo6>1BG*qO#!^aZyV(|$>bH6{y$Kb0#Bd_EAz44$Yu|MMnrRQtP6 z;hNvodzs-#_|w2D<0+{=mpfXSIYC=;D`+|Ux{Zq>V<|hp?^Kasw(KuI>|DIttJlzd z>y934_>vGUgN&dSya=lBUi-eZ`cC~sbiF{g54C4)4;oOMyYh5UiK1RlaXTReg!L>1 zx99pA9^(ZmiyTxs5ht#?wc%-wl8Dy)nwh;i87BfeB3DTI5_wwYzI^+3-qOcXd-e%< z8#KhW?ci?@zAd-l+=yt=(C#F*-377A(IR-v=(C!sqvo z^`u7uqU{y@KwayKnlV(1ol*3*qW3!I<>2V+=d)EM<^%&@b7*T;i|V&-AsXVA0aEFS!64yZB0mtW6H*OqO7*vF2~fJUS@i4 z^JI7Sml^W+0#W#z@@VhMj7@+L|KhmH{36!xkC?0qh;whcYz=}vGcZS;P>3L{bx~jX@=;dppuj~P>N0Ng2%OKG+ zB{e5`T8fu1Uveqt78RYB3to}<@B^((eJ(DjgC$gIhFUR}IcSqUx=W=^G&wQ(T>ooB ze;M3$36y1Wob#3AQ#Y+Pd^|YdNeQiLYv+T1WKn3&W+!lSr;|~-D6Ka<;l6h6T1AoNc|exsSYV80FWa*XVwf`i>SGEeGvh>RrGa zd}E%RE=l)jh)%GsxQJD3B2f9bof-p)#_fBE66VHf>W>feZLmot2quqYiyobf)XW#7 z2L-zZBZ6gfAj19)1rJM7e0MvKjvbvGRT&n_9vWc_h|GTm?ImgrAdt@FTZj(YN*G#aG^pH6&~$R)C_l#J;5O0n_)Xj!ShX?(Rt-W^P(DtyOn_sE5i>X7AC+1% z0}V<4C~$t3g%qulnf^MV{hFY8U%HgUI!C3mUz_7lZbv4=A_&s`rob>j2L9CPI#-?= z+m^eL;Z`yKp3tEY8EA>fY6;oeBsV^&RP8*ftxen>Tx?X-acwplNSfjar_gv3StZx; z(WU>YBP3_riS4QO>6PFVZ(-S;*A3P|bj*sJvR8>AmlYdJpMH$W6=|0G#^0PiaVN^?V=ufF}<^EV@cHNnMwEhntXg zx4SNotXFe=u*0VF>|~30dXTB9DRc!}1D90jm#2Nt1&@{`(1u%lGW)jo@ea*3A>S3Y zekY9og17J67k}!+dDUcO>X5)x%)xA`eWirw{S2%ch!{^$b-zg|riPN9(|xl%E4_1j zOJ=9DJn54P!_?GNHe z8Qq;p0`Qs+I!p`h*1L^Sy%oF3x6C`Iy6uC)ZmMp#BHV#ugD33Hn@9n3}lAwybA? zV5ZkL;m*frM4+OjW-L5%<4QxXo#<)vpR%L&)0ZcLd#fv}fIC@UG2!LnZYbN{soAUA z^TQuoXu7Vv_%NhO-?;%F4Dti@!|UM%gVkv#$@g=LjvB63og#UL$HogZxE;1zQbeAU zg$K(>Nm-y<^YPJ0*)gPoAMOwto5@?PmelS%8`F5&#qopHtiYKmo}Jk$No!lJk8cg| z`hxqeh9%4Pgy<7U&hh;xuJ7BAO;IQL;HHjHO@Dpk`a9b2cFP<*+{D-6L!z{Bte(>e+V+)-rt~}KE^9+N)^&s3j#{J=Yy_@@6yPlo<zY5 zl$A2^wj?7bhvq3xzFq_SV;o#+o;O~Ep)CM%m_eB$`r9|s@D|Fk(6$|!O77`N!bmBm zAqsxS_4z+{f%I(|ab_GmPD{w`aQeI~H<^u0o#^F_Xzi_ww_=h1+#o)_J6(^s7k0#Q z*Kuj5;KAr(h#@QMMFQL`)9B!=R!dD;#8Amh>IGRSkqX;Wc-zheAn3eGJmsn|)ZN$h z@>Tbon%)t>*07gTz^tFV$<`_*^>VmK*{@2IsjRXr(qBwYOIfzoWA^1tCBL4DK7S5@#8?t9nZfof4Tnuxva+j6fX9|S_H>TYDTTj*9&V}W42?+@fm8O=y`j_aW%*Sm;u6jOA zz!+ETjZ;QWrM-Fck`9vYB1A?iWVF2>?8GCCYI?t91*`b~-KdWiXxHPYh_wiE0Zi*f zouE3ak0dtMr`Kk#6m71$ger2NXbTX?^oOr0X&^wPVAQshl_RB`tfneradd=X9X~5* zkmz>5HUyEU{m*aTWRGre5F&%0niwFRVS6(*hL(T&C;ZxaLdr)gxJGIsj9)x|T6*WB zuPGwNQak3!1I;W$IDD%*g3>%I`O;fsDA{&v>t}^@V}5}#s--vBK@Oq*OppN}tN(DGs z?{UCo5-n;odDJWD^h-u??NI2y@c*0Pre z7-5AyB8(cjx_R^U2Y^(8%%h;2Us6j-r7UmCqt6J0x&B_|mO#(Z=f9KEE>-y7*E|Ks zTpS3lKa;w}UocdX%=T{`F=^E!dCi0GreFJ8RDNuo{Ce^Dp~dw(eU*s7Q{@gf=AX*g zvc%(n@74P+8ioR|4{0>;G~%Wh>Ktyt^;0;~Q~N&If1fm_F?7}CB@C`!Z@KYG{|j8Q zVW>RYjJhee_Irpo0$CNxaEL+^7xU1YK#n&Hf8QQ>QkpH?cR(>lM{h(=Xtbdv*>ASz z&QNMG@JaTeGzFu29^PIotr`7$vpZ(=X7tC$i1Q+pDLpX_*wApyNpFk@3S-ERu(seZTVcNmS|Hw+>Wve@j{*bG*wUH}hY2~q!9-6Oq| zrV*E!2#i%r_dneVzkwo%f3gTCesHiqLef9jKUy)3&c*ofX1K%{J}ES8PBb*rHqxe# zi|#}qzL-xTp9%Bb&KFMnfvtCu`_8NSx%r;ab^C7JG;rb%H5RlJAq$snwr@!Dhrd14 zxNXLJ-8>4V)Qz@6+H_60CN?_*nV3_gU`qi505vcc5hqdRmwd!Y#Ku{3Jg1uVv0{9! zn#zn`EVidx2_sHmXhtJjI{O*2snzd$Gn4}I{bn&mkXN|L(Ud7@*x62+moAaT zf72x#9sPX5AnXm{4v*&Y`zrJF<&7`b&v6Uu3y;bw^fd!R-;ZcW%UuWMt=}L2u06jE z-y0f&(tS48Ozy6uVUSeZn%RWY5XKvwV}6L8LIk2W28mza5ulk%@TD6lQ~uJ`#72X@0n|w|%}?9YML~Kb z`aE&}oqpcw)c0~W&HrfoU4QBWt?c3B8XrYI_`fc#U*qgvpHY0EWnAZ*kasmY5BYCG zzV#0_2wKSZ8$?Q4R5IeCs_sSJcoz}BHnWAlc`;m+LUjG!TS2mT z6U*yq-%t`6b!dx2|1(X7c1}Ac1qc;W86GT!W%u9`q3oR zX_=`D3#}6cVB0_rfZ_RMZ@$%{$0y#pF19Fw6-FO(Q;FzYyO|_%ke?u$?yqB${Of^l zt&?6toPsydwZ%}ItTu?L`Vz0(@|Z{aO({<-z7cUVfVYID-gL~Mw;0Seq;z=4J+&iH z(IJ7io}b?sfaRlqy(-o3XN2jwkF>MaHsUMI*|ik7+3ls0D}JTEgEJBO!KePYxO&%G zbZuL{yMBKSRZQC$9Nql9^WAJqK-6j_k1QM!r>2hH`dq!oO2q+TweCd=;6Rxh<0yJrp^&2@hqnOuMX1P6ok#(C93Exb8NwSc$*Udn)-^S>N+dy^1S6&+oV< zcjQgxk-R7smSP$BjAK>olHYUxAyB_1urz|<-;b6n-{=QZ6)_I4ThOa+pRT`GcDDR& z%;k5}qr3mXOnKixs0PMUg@yZaL*LZpPw|XCOB0WV|6ozNAJqbB)`k!TJvNOGK~ONO zXr#vrsS?X(y(Y~v%8FwIDO($6!XY^7`N>~QIEmJ1{2!>s^7ObD6du=)ye{{z?{Ws@ zWb3BbwT0N@{2?cFNdQenaIi3Q3|Q@;&Wi)1nPQ7g6lFD}EOCOXJT1&}tj%|(&F3zz zw7o^g+{EGRqM+O{@G`H}`#&te-0qlrSF*UI|H#b-D77o~&W&dP#xQtEFQRIS=hJ;_ z6()raQIOd|N#AzV#l^|x-0;eH<9!g3B{)(B_#u0BPA)9kX>ODrt0S&q^U}Ng&nxVl zIu)PSQBIDoZH|jr6Rq}-7eDM~V#;U>=I(sAk6`pK|az8I>|fr zZpJ6#Z#sMc=B01skk?>j^Z`&_n;wW z9$x=xH3qjnIU2}u?F(5y(L4W!;4V3;a|&(5;)Km;OW(cR7Et*xbn2%=aT$IIe#W@mhF_+L%$LzQ**fS{FW5G`U~}tq=>R{(CF= zA_7U_9H+2%QS%L39J3a^sU~=n4PCyzsQvM}i7Z{T;X&ulX`=g}aX z0MSBEPh^Z)bOrdY;xD9zu^qT?RoOd9~9NU_cr}aP>N{cg_&T4x|u3ZL8G+ARZB~ z)c@PLkJdZR%bzv|vPBa=eDSl~)Y1u#xBew6^H_MvI&j(3>v=U>sHBC&_&60iW!j-H zFIN{!w`#t_j+ND6^ZSe0@hlrPjGc77kx|-@9}FjE$tq;4=on_9JH1+LDRAg%P!N!% zoSa#%?Dc6lU(&)Bd8enhWRDOrJ*2FdJu5Moo$K$f+%Luk*d+H0&GpY3=B_@%LqbDX z1Tc9UYPoVDq2Ph_K*a=V*x^Q|ec|z*d(&8hcc*L{Di(GWnsP>>Dx4abB)FA#FB_Na z4FdFNWO#%b>a_731`~4mvTKfLH@t zls}L*LS(}>mx&^nSsCfl0X5IM{XtlY=j!=e_3KS=D&S;l>iei!IcN)kU*~kA2_CLH z$A0^UJQk$}r(?6AbQNv&YIF)Kk3aS4du)6>{W)86n$B22lTMQ2(ajci7i;=FIt7CY zh(&i@gR1rtIqAcxHB?oXJ$Fa*rwqJTkPIZ~DF;Fyeugk|pk1>A)6?Fg_cY3<`i3yx zFBloOa_4FtkSObYGvwnPGf6%|trD}DbO6TGnHxG4j&nb6ND0885Jl$aG4-{#=jG(+ zo$q=9>3&3bLOB759kaB66%z7jw-PnGhB=bk1mtj*PdbQN3#TeKunn%%t(IyhIAQKx zZjDjxS|s}XuFG(P%VRun^+MJZP!M3?rYHE74BJ!zS6{>z+D{fEml$9*6FR!8kAooq zs0Vo&S}1hdD1UNW&y=-(ewr0(M+2%VZ~|H&1s)hyyuwk|8`0+F;wjWd&l*tqc%uUq z8k&SuJpT{X8v4y>itntgbfeD1HKD7Y8oxAJgt+%oUA!BddWba{0_IjQ47@0{?2K zhyhPv%c$sV6JNxA{tqC*yKo_K|JE7aw&~-cU)yog@i;W}ieEvgE8J!o)}@L>s3fie z(m}rR@u{iJZ>v8mw(g$VcIpf~cBEDDHQu|M{lnuz>h+7JJ@vQs?J_RF?rs@)6K=@H-|J1IR^mybTF2haX77u zD*%jt-Wu`7@u}PEJ7gE>M}2zxD}6BfZxJ}3xy_E-*)yb*JqI1;E_<1wT=;c5>Sn8@ zInmjQNtfHlvAY)=tg?g+PV(|0F!wDtX*}|HGA%6?eJvH}wXm&Nqu`_{wlh8a?Lf>&|;V{`7-OvIvx7&GqhkdWe| zS4se?!MFj6J_QhMVQ#ktz>gm(rKoaSUY-FZx|*FYV}L2O;D94vh*iU4tV`6h*f_Cs zZWTuU1D64LKp@L*pIES~*WU5)PN{xr!pu@$?s{dGo257Bq-a~{bYCE2K9N`z@5aC3 zCu&^24J$17{|=vNf&a83L?hP-Z+t!rn0RD zHKNmDe5bzk}H}A!C%~9K*ri#{X zC2z))Nss~s+}%cb)>^9tI@X4ZKn$0NOK0%AXa+z^bD^9XGi<=^(C^zRXjgOKZ@eaQ zFx((|_JMho8L%S)6_XEk0Mg1$>`pbW`%Vv?X!TZdxsC~D=x&4i4iW&dSk$x>+9eBH zPii!rX@NbD@>c+D%q1WiYVYk$=Jwfqr=iQ`*!)Y-zGeeff38O{JG)8F>uujtjSG4E z<}+;cQjG=hJwR2c5+GD;OYshbe@(jvaz`}&x@1enmODR*(fiCLw(^#UWnRs;SEuu9m2*wY>gn5g)q+f zzz&-oj#j&c*v=REdJ!Z{wBQz9U@DBP#C9ih?^GB1Sk7F82Zz|NBLGUtX}~H+Kb!%G zw_ncZf(X(yz`b8%D3jRix<{~C&l~10*|`saKkpBh^i|Xqu6gEk_4M?Ru+(PF^iHi7 z#ujS29ND@NMwx17NaNO=$fuCM(K=X~vai1U$=YIzW&aPw+bJzx_xCGqg{aBYkKX|F z$9k5|?r*pE2J;NDSmqORWO^wH3;a|?vf_>6M(rn+vXC#j^!?P{x|WsC62Rqo_T2W~ zh0UYe*}tOqc-W&}7qe5GErvkxduR2i9F1A{zaMdqtOapTU%Y-vWSN#Q$C^_Vxr`4~ zyuHCc?Jb1%vlv-xUWmfx?DNf2yeww|n_)%=LOxxQ7~XF_^Z+UIY{b+_9}MlgMSm5F zOpuRZ)w==|-PMBdC`$nrsYuT|4r9<`YGu3eS~?ePed|i(M^PaT#DgZOJUesSdC}k+J$lEaxvfJZrSW>FF4OL$OO2qMSeVxluH~XrknNtx9 z8=Ki8<1yn%QI{FT3hVr~NOEDfUL3$iUzstOwe0G7?ZW2!Fz$zz;OfOM*x%CN>;6lt z;gp;J=*b?6LPd(6ckj-OgopPqYBXSW_GaqO@=YPf->eJh(@ppC9QCrFiMloSLm1H; z0H4ZoH5I5Qj%Q*{Q-8J)hT#Bd6?;o=K7>C2C2?bhf66ZXHA}a>$)m_N53rOt7@nIm0A*fviEa2 zEQj;{<;xv=8%s;Aa_^OjdM9}m#B-`AX`0yisDYpr%hz(=U9a7vl0LMYT$WO@`zUD&8N1B`(2AKktea@OuI9oGUu5x= z2>YJj^FTa)AovE5FhOv~l9EaMg4wB)&91__*AQUcI4vxI!g`cp9vDE58UBKm*ZeY0 zB|F*qvODJ;viGvI zr#T}My*0GP^n$6KoYS)zwO1&7Fzx|4zF4H&8r>Va^|thK_fODTcf5$j`;}j5e!O;* z#a77poOeX#3WeH$jxk^vA_(|Z*?QGcFTan~JMe8`NJ4?wes4AaCu%Vyf9USQ_%zMAcTZ~pcPE#h)h~KQ z7;a{22E=5)OeR7iJs$KSsl1E^!pC5?tEI1oN_9z|k}acxXPiRcB1|9bbvVF$r~kPae&eAv+pr}a{Vonw2c zCLHdMJ{?YlXsj&@Zl}zs$8UU*A1?q>xUoc)9?6inGAt8y0=IT$!y)_I+llXrJ>W~nFn zv}U)4kwztz8ML0KZpD|rd;1x7^69(;BkFZZNhR%q#=T4wK3qwu2A@LU-y2e?A$ca{YOh@1@5Bh;}7l{LZn52K1A)O%d6T5~UJ^VzRd zFCF2*&7YSh8B!)RUnmSt!+dp66Zb(IV}+5H@yHp@c)S@qJ-CIr-{#WWtPTEgq$v7N z2ZNGqHYatDL8T*dQZb>zZlW9w@Rwgq<}gsY-@Q25^cdTz@NY~*lJh7gTx|j|ACLV^ zjNEss3sg!%9{*AqLNTV<=pmS6({a=Q)fp^WbMgupJCg|j5msV2vm*g~0x0lV`DjM(oz%p!pf~nwBRNg8t-`$bMbMIkYu0(F}>O)5&8<0Im^pVtKnR!mJ#l35ZF4_V~ z2p;seAS(18D1YlX(1@1skBTqa^3WU~>_~wsVn}Ind!M_e;n=nAn4;V^Xmn9o(8mb^ z5d{--omJ8C2wZwJ}19jQTW;`kNq7ruGYrT$(xgKk2s|Xh# zf1n=ZIekLdpZ_{q_Cbs7AkME1T7QH>AlxdwsHrn-cz8GwcJNJ+xPE^)V}Z?cp^Wg{ z0|X9fS%f?eZ#_IbJQ=eYkXMz?D1(%mc9rMrxPATRLLp+)9=9Y#7^U&(-`srGCSN&pbK)EeR1tSI5ueoAVta#~muLLCOAMJu2!Op#_EwtOwUG&8<1@CGl z5QyQd&z&|N_1S5>JwplZa6DZ2*p;#sWMm@>Yrk#LSiYkiTj;r5 zgelN0dIU_vI9~I4E#cgtuclT;M%v=TTv6R2ah4xsLpBBM38Kc*mX?0@YrUGy6mttV z;c32P`hI#O-UtzwjnyMI4I!urdGyIntm>sLMR3K*Whp5y`jBA`(2!CV)zhDF=Mf*` z0)%ZUg4ah7_SEyF)o#qs(@)J|!)z!DyrBE|RPOy6d$aip*23tOF>)3P(bdW(9}-}9 zp7!R$dB{i#&lN2IA|}fq0S1rkUUyfn_*O=6^1Yx+sD%$#aqUmrT}B1*aWXE1KU(uA7YADmwQd`5ul5?qK$hCV1nidIfa7^#pb_rrr##O42Sjb4 z0qG->ox3N0SjI9I}wi7COb43f7l& z>6*0z)U<#S#X_`_=p2fFB@7$tipJATJbley6#k%12T5Qh zxa7w?_lQb>o)5iY`_Qj3SLGC|Y6bB_gfl2Vb^GICAUf@)QdTorok~TYpMljP{a;3tvlvu);)2*q=IklcVp*79CUbazK|KgwYOHictp*29P?F=|S9*-` z&Tx@1D1UC;V)ov^qCmmv5&so_#NO|icI#Wh=H}zE(kAd+e?^emk3aYcUkzWr8aCJg z**+~~%$l)0nr)#oSwFfj#Y(EV@~8?TPAgu(C4fGVk}CXE3VO!jugENilhPjL?N$M# zqY|r*1YI^ibSa9$&d*_(wWyq-WY6O(uia}e7vDEJbG9|dzxy%pVllG|&}rcW6gXf? zb*R7;%=)N-9;W%Z8rJa^TGy|D40Y6f#2V6`&x<)c0a7FjYoea>$)YokLw$4c$g7hb zLHsS|@0B$Xv$Xg5wW+UZ#V)uo9LF^oy6gBnT8LwKUP8r zUAa}5NZY@7Od)zw37BI$Ti6PtdXFaz59EOyvu{BCp>&(k!Qh-nwL4+Z=CQ>}!ZmKz znn{~G1ZKO_MNMujXtOzyy3)}|(E}za`K~e`F9VDX9;MKH;1=|)wyw}t9sh>HPgp~} z=;f6g+yV$)-o5i;A>@5KR4Av>Tz0CEIkKz|?i^P_E9iG`$fhb4MT|PG&r0ee(NE;1?)6e z2*J{4Cy@g5Wz~+(@960)gC6r*HT{9ewYx6sNc>FJ(Tf`m92?NWewjrg%E;8?1m!Ut z3un*>V-6LrDYj62=PRSMPKx~ky*@~IqB^KR%T*vho$I5nV9A^mIxVO??m43~a}0_L z`V1dLbTc<#hWv}#k~}*f-bEp8HQs7c=Ia3pE9CZdsNR~A9X~X?gtgC7p?$;UpuG~A zs9ERo!MmDV^EyFP&YJ#HpY4aqa$bM_Q#Oqyn0vY6!(hA6uztQjXthgH*=5kfVtVB+~c$y z#cWLdQh}F2!g+5Z{+hDS{y=VFt@ELOQ8YuCZDnJ;3lM#-;5(b-n#^lnbR|`AYaO=g zlw9%dJlQMXssx.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 literal 0 HcmV?d00001 diff --git a/modules/services/homepage/docs/default.md b/modules/services/homepage/docs/default.md new file mode 100644 index 0000000..f82f9c6 --- /dev/null +++ b/modules/services/homepage/docs/default.md @@ -0,0 +1,191 @@ +# Homepage Service {#services-homepage} + +Defined in [`/modules/services/homepage.nix`](@REPO@/modules/services/homepage.nix), +found in the `selfhostblocks.nixosModules.homepage` module. +See [the manual](usage.html#usage-flake) for how to import the module in your code. + +This service sets up [Homepage Dashboard][] which provides +a highly customizable homepage Docker and service API integrations. + +![](./Screenshot.png) + +[Homepage Dashboard]: https://github.com/gethomepage/homepage + +## Features {#services-homepage-features} + +- Declarative SSO login through forward authentication. + Only users of the [Homepage LDAP user group][] can access the web UI. + This is enforced using the [Authelia block][] which integrates with the LLDAP block. +- Access through [subdomain][] using the reverse proxy. + It is implemented with the [Nginx block][]. +- Access through [HTTPS][] using the reverse proxy. + It is implemented with the [SSL block][]. +- Integration with [secrets contract][] to set the API key for a widget. + +[Homepage LDAP user group]: #services-homepage-options-shb.homepage.ldap.userGroup +[Authelia block]: blocks-authelia.html +[subdomain]: #services-open-webui-options-shb.open-webui.subdomain +[HTTPS]: #services-open-webui-options-shb.open-webui.ssl +[Nginx block]: blocks-nginx.html +[SSL block]: blocks-ssl.html +[secrets contract]: contracts-secret.html + +::: {.note} +The service does not use state so no backup or impermanence integration is provided. +::: + +## Usage {#services-homepage-usage} + +The following snippet assumes a few blocks have been setup already: + +- the [secrets block](usage.html#usage-secrets) with SOPS, +- the [`shb.ssl` block](blocks-ssl.html#usage), +- the [`shb.lldap` block](blocks-lldap.html#blocks-lldap-global-setup). +- the [`shb.authelia` block](blocks-authelia.html#blocks-sso-global-setup). + +::: {.note} +Part of the configuration is done through the `shb.homepage` option described here +and the rest is done through the upstream [`services.homepage-dashboard`][] option. +::: + +[`services.homepage-dashboard`]: https://search.nixos.org/options?query=services.homepage-dashboard + +### Main service configuration {#services-homepage-usage-main} + +This part sets up the web UI and its integration with the other SHB services. +It also creates the various service groups which will hold each service. +The names are arbitrary and you can order them as you wish through the `sortOrder` option. + +```nix +{ + shb.certs.certs.letsencrypt.${domain}.extraDomains = [ + "${config.shb.homepage.subdomain}.${config.shb.homepage.domain}" + ]; + shb.homepage = { + enable = true; + subdomain = "home"; + inherit domain; + ssl = config.shb.certs.certs.letsencrypt.${domain}; + + sso = { + enable = true; + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + }; + + servicesGroups = { + Home.sortOrder = 1; + Documents.sortOrder = 2; + Finance.sortOrder = 3; + Media.sortOrder = 4; + Admin.sortOrder = 5; + }; + }; + + services.homepage-dashboard = { + settings = { + statusStyle = "dot"; + disableIndexing = true; + }; + + widgets = [ + { + locale = "fr"; + format = { + dateStyle = "long"; + timeStyle = "long"; + }; + } + ]; + }; +} +``` + +The [Homepage LDAP user group][] is created automatically and users can be added declaratively to the group with:. + +```nix +{ + shb.lldap.ensureUsers.${user}.groups = [ + config.shb.homepage.ldap.userGroup + ]; +} +``` + +### Display SHB service {#services-homepage-usage-service} + +A service consumer of the dashboard contract provides a `dashboard` option that can be used like so: + +```nix +{ + shb.homepage.servicesGroups.Media.services.Jellyfin = { + sortOrder = 2; + dashboard.request = config.shb.jellyfin.dashboard.request; + }; +} +``` + +By default: + + - The `serviceName` option comes from the attr name, here `Jellyfin`. + - The `icon` option comes from applying `toLower` on the attr name. + - The `siteMonitor` option is set only if `internalUrl` is set. + +They can be overridden by setting them in the [settings](#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.settings option) (see option documentation for examples): + +```nix +{ + shb.homepage.servicesGroups.Media.services.Jellyfin = { + sortOrder = 2; + dashboard.request = config.shb..dashboard.request; + settings = { + // custom options here. + }; + }; +} +``` + +Secrets can be randomly generated with `nix run nixpkgs#openssl -- rand -hex 64`. + +### Display custom service {#services-homepage-usage-custom} + +To display a service that does not provide a `dashboard` option like in the previous section, set the values of the request manually: + +```nix +{ + shb.homepage.servicesGroups.Media.services.Jellyfin = { + sortOrder = 2; + dashboard.request = { + externalUrl = "https://jellyfin.example.com"; + internalUrl = "http://127.0.0.1:8081"; + }; + }; +} +``` + +### Add API key for widget {#services-homepage-usage-widget} + +For services [supporting a widget](https://gethomepage.dev/widgets/), +create an API key through the service's web UI if available +then store it securely (using SOPS for example) and provide it through the +`apiKey` option: + +```nix +{ + shb.homepage.servicesGroups.Media.services.Jellyfin = { + sortOrder = 1; + dashboard.request = config.shb.jellyfin.dashboard.request; + apiKey.result = config.shb.sops.secret."jellyfin/homepageApiKey".result; + }; + shb.sops.secret."jellyfin/homepageApiKey".request = + config.shb.homepage.servicesGroups.Media.services.Jellyfin.apiKey.request; +} +``` + +Unfortunately creating API keys declaratively is rarely supported by upstream services. + +## Options Reference {#services-homepage-options} + +```{=include=} options +id-prefix: services-homepage-options- +list-id: selfhostblocks-service-homepage-options +source: @OPTIONS_JSON@ +``` diff --git a/modules/services/immich.nix b/modules/services/immich.nix index 28abfe3..7f0171b 100644 --- a/modules/services/immich.nix +++ b/modules/services/immich.nix @@ -465,6 +465,20 @@ in default = false; example = true; }; + + dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = lib.types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${fqdn}"; + externalUrlText = "https://\${config.shb.immich.subdomain}.\${config.shb.immich.domain}"; + internalUrl = "http://127.0.0.1:${toString cfg.port}"; + }; + }; + }; }; config = mkIf cfg.enable { diff --git a/modules/services/jellyfin.nix b/modules/services/jellyfin.nix index 40f8832..0e8c541 100644 --- a/modules/services/jellyfin.nix +++ b/modules/services/jellyfin.nix @@ -317,6 +317,20 @@ in }; }; }; + + dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${fqdn}"; + externalUrlText = "https://\${config.shb.jellyfin.subdomain}.\${config.shb.jellyfin.domain}"; + internalUrl = "http://127.0.0.1:${toString cfg.port}"; + }; + }; + }; }; imports = [ diff --git a/modules/services/jellyfin/docs/default.md b/modules/services/jellyfin/docs/default.md index b36b64b..c06b0eb 100644 --- a/modules/services/jellyfin/docs/default.md +++ b/modules/services/jellyfin/docs/default.md @@ -20,9 +20,12 @@ this one sets up, in a fully declarative manner: - Declarative [LDAP](#services-jellyfin-options-shb.jellyfin.ldap) configuration. - Declarative [SSO](#services-jellyfin-options-shb.jellyfin.sso) configuration. - [Backup](#services-jellyfin-options-shb.jellyfin.backup) through the [backup block](./blocks-backup.html). [Manual](#services-jellyfin-usage-backup). +- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-jellyfin-usage-applicationdashboard) ## Usage {#services-jellyfin-usage} +### Initial Configuration {#services-jellyfin-usage-configuration} + The following snippet assumes a few blocks have been setup already: - the [secrets block](usage.html#usage-secrets) with SOPS, @@ -133,6 +136,35 @@ shb.lldap.ensureUsers.USERNAME.groups = [ ]; ``` +### Application Dashboard {#services-jellyfin-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the [dashboard option](#services-jellyfin-options-shb.jellyfin.dashboard). + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Media.services.Jellyfin = { + sortOrder = 1; + dashboard.request = config.shb.jellyfin.dashboard.request; + }; +} +``` + +An API key can be set to show extra info: + +```nix +{ + shb.homepage.servicesGroups.Media.services.Jellyfin = { + apiKey.result = config.shb.sops.secret."jellyfin/homepageApiKey".result; + }; + + shb.sops.secret."jellyfin/homepageApiKey".request = + config.shb.homepage.servicesGroups.Media.services.Jellyfin.apiKey.request; +} +``` + ## Debug {#services-jellyfin-debug} In case of an issue, check the logs for systemd service `jellyfin.service`. diff --git a/modules/services/karakeep.nix b/modules/services/karakeep.nix index 20107b3..942337d 100644 --- a/modules/services/karakeep.nix +++ b/modules/services/karakeep.nix @@ -175,6 +175,20 @@ in }; }; }; + + dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = lib.types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${cfg.subdomain}.${cfg.domain}"; + externalUrlText = "https://\${config.shb.karakeep.subdomain}.\${config.shb.karakeep.domain}"; + internalUrl = "http://127.0.0.1:${toString cfg.port}"; + }; + }; + }; }; config = ( diff --git a/modules/services/karakeep/docs/default.md b/modules/services/karakeep/docs/default.md index d46cb05..cda315e 100644 --- a/modules/services/karakeep/docs/default.md +++ b/modules/services/karakeep/docs/default.md @@ -21,9 +21,12 @@ It integrates well with [Ollama][]. - Access through [subdomain](#services-karakeep-options-shb.karakeep.subdomain) using reverse proxy. - Access through [HTTPS](#services-karakeep-options-shb.karakeep.ssl) using reverse proxy. - [Backup](#services-karakeep-options-shb.karakeep.sso) through the [backup block](./blocks-backup.html). +- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-karakeep-usage-applicationdashboard) ## Usage {#services-karakeep-usage} +### Initial Configuration {#services-karakeep-usage-configuration} + The following snippet assumes a few blocks have been setup already: - the [secrets block](usage.html#usage-secrets) with SOPS, @@ -66,6 +69,35 @@ The [user](#services-open-webui-options-shb.open-webui.ldap.userGroup) and [admin](#services-open-webui-options-shb.open-webui.ldap.adminGroup) LDAP groups are created automatically. +### Application Dashboard {#services-karakeep-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the [dashboard option](#services-karakeep-options-shb.karakeep.dashboard). + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Documents.services.Karakeep = { + sortOrder = 3; + dashboard.request = config.shb.karakeep.dashboard.request; + }; +} +``` + +An API key can be set to show extra info: + +```nix +{ + shb.homepage.servicesGroups.Documents.services.Karakeep = { + apiKey.result = config.shb.sops.secret."karakeep/homepageApiKey".result; + }; + + shb.sops.secret."karakeep/homepageApiKey".request = + config.shb.homepage.servicesGroups.Documents.services.Karakeep.apiKey.request; +} +``` + ## Integration with Ollama {#services-karakeep-ollama} Assuming ollama is enabled, it will be available on port `config.services.ollama.port`. diff --git a/modules/services/nextcloud-server.nix b/modules/services/nextcloud-server.nix index 6cd6fe0..8cfae23 100644 --- a/modules/services/nextcloud-server.nix +++ b/modules/services/nextcloud-server.nix @@ -694,6 +694,21 @@ in but Self Host Blocks assumes that it will not be the case for most users. ''; }; + + dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = lib.types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${fqdn}"; + externalUrlText = "https://\${config.shb.nextcloud.subdomain}.\${config.shb.nextcloud.domain}"; + internalUrl = "https://${fqdn}"; + internalUrlText = "https://\${config.shb.nextcloud.subdomain}.\${config.shb.nextcloud.domain}"; + }; + }; + }; }; config = lib.mkMerge [ diff --git a/modules/services/nextcloud-server/docs/default.md b/modules/services/nextcloud-server/docs/default.md index 083a7c2..957e3b2 100644 --- a/modules/services/nextcloud-server/docs/default.md +++ b/modules/services/nextcloud-server/docs/default.md @@ -35,8 +35,8 @@ It is based on the nixpkgs Nextcloud server and provides opinionated defaults. - Forces PostgreSQL as the database. - Forces Redis as the cache and sets good defaults. - Backup of the [`shb.nextcloud.dataDir`][dataDir] through the [backup block](./blocks-backup.html). -- [Dashboard](#services-nextcloudserver-dashboard) for monitoring of reverse proxy, PHP-FPM, and database backups through the [monitoring - block](./blocks-monitoring.html). +- [Monitoring Dashboard](#services-nextcloudserver-dashboard) for monitoring of reverse proxy, PHP-FPM, and database backups through the [monitoring block](./blocks-monitoring.html). +- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. - [Integration Tests](@REPO@/test/services/nextcloud.nix) - Tests system cron job is setup correctly. - Tests initial admin user and password are setup correctly. @@ -344,6 +344,22 @@ Note that this will backup the whole PostgreSQL instance, not just the Nextcloud database. This limitation will be lifted in the future. +### Application Dashboard {#services-nextcloudserver-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the [dashboard option](#services-nextcloudserver-options-shb.nextcloud.dashboard). + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Documents.services.Nextcloud = { + sortOrder = 1; + dashboard.request = config.shb.nextcloud.dashboard.request; + }; +} +``` + ### Enable Preview Generator App {#services-nextcloudserver-usage-previewgenerator} The following snippet installs and enables the [Preview @@ -537,7 +553,7 @@ by issuing the command `nextcloud-occ config:system:delete instanceid`. Head over to the [Nextcloud demo](demo-nextcloud-server.html) for a demo that installs Nextcloud with or without LDAP integration on a VM with minimal manual steps. -## Dashboard {#services-nextcloudserver-dashboard} +## Monitoring Dashboard {#services-nextcloudserver-dashboard} The dashboard is added to Grafana automatically under "Self Host Blocks > Nextcloud" as long as the Nextcloud service is [enabled][] diff --git a/modules/services/open-webui.nix b/modules/services/open-webui.nix index 99a17e1..233f496 100644 --- a/modules/services/open-webui.nix +++ b/modules/services/open-webui.nix @@ -160,6 +160,20 @@ in }; }; }; + + dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = lib.types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${cfg.subdomain}.${cfg.domain}"; + externalUrlText = "https://\${config.shb.open-webui.subdomain}.\${config.shb.open-webui.domain}"; + internalUrl = "http://127.0.0.1:${toString cfg.port}"; + }; + }; + }; }; config = ( diff --git a/modules/services/open-webui/docs/default.md b/modules/services/open-webui/docs/default.md index d12ec8c..c604bde 100644 --- a/modules/services/open-webui/docs/default.md +++ b/modules/services/open-webui/docs/default.md @@ -21,9 +21,12 @@ This service sets up [Open WebUI][] which provides a frontend to various LLMs. - Access through [subdomain](#services-open-webui-options-shb.open-webui.subdomain) using reverse proxy. - Access through [HTTPS](#services-open-webui-options-shb.open-webui.ssl) using reverse proxy. - [Backup](#services-open-webui-options-shb.open-webui.sso) through the [backup block](./blocks-backup.html). +- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-open-webui-usage-applicationdashboard) ## Usage {#services-open-webui-usage} +### Initial Configuration {#services-open-webui-usage-configuration} + The following snippet assumes a few blocks have been setup already: - the [secrets block](usage.html#usage-secrets) with SOPS, @@ -63,6 +66,25 @@ The [user](#services-open-webui-options-shb.open-webui.ldap.userGroup) and [admin](#services-open-webui-options-shb.open-webui.ldap.adminGroup) LDAP groups are created automatically. +### Application Dashboard {#services-open-webui-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the [dashboard option](#services-open-webui-options-shb.open-webui.dashboard). + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Documents.services.OpenWebUI = { + sortOrder = 1; + dashboard.request = config.shb.home-assistant.dashboard.request; + settings.icon = "sh-open-webui"; + }; +} +``` + +The icon needs to be set manually otherwise it is not displayed correctly. + ## Integration with OLLAMA {#services-open-webui-ollama} Assuming ollama is enabled, it will be available on port `config.services.ollama.port`. diff --git a/modules/services/paperless.nix b/modules/services/paperless.nix index 8ab3d86..9954276 100644 --- a/modules/services/paperless.nix +++ b/modules/services/paperless.nix @@ -59,10 +59,8 @@ let mkIf lists mkOption - optionals ; inherit (lib.types) - attrs attrsOf bool enum @@ -346,6 +344,20 @@ in }; }; }; + + dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = lib.types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${cfg.subdomain}.${cfg.domain}"; + externalUrlText = "https://\${config.shb.paperless.subdomain}.\${config.shb.paperless.domain}"; + internalUrl = "http://127.0.0.1:${toString cfg.port}"; + }; + }; + }; }; config = mkIf cfg.enable { diff --git a/modules/services/pinchflat.nix b/modules/services/pinchflat.nix index 98f5a82..1ad77fd 100644 --- a/modules/services/pinchflat.nix +++ b/modules/services/pinchflat.nix @@ -1,7 +1,6 @@ { config, lib, - pkgs, shb, ... }: @@ -132,6 +131,20 @@ in }; }; }; + + dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = lib.types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${cfg.subdomain}.${cfg.domain}"; + externalUrlText = "https://\${config.shb.pinchflat.subdomain}.\${config.shb.pinchflat.domain}"; + internalUrl = "http://127.0.0.1:${toString cfg.port}"; + }; + }; + }; }; config = lib.mkIf cfg.enable { diff --git a/modules/services/pinchflat/docs/default.md b/modules/services/pinchflat/docs/default.md index b4dcf55..4688025 100644 --- a/modules/services/pinchflat/docs/default.md +++ b/modules/services/pinchflat/docs/default.md @@ -9,8 +9,14 @@ this one sets up, in a fully declarative manner, LDAP and SSO integration and has a nicer option for secrets. +## Features {#services-pinchflat-features} + +- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-pinchflat-usage-applicationdashboard) + ## Usage {#services-pinchflat-usage} +### Initial Configuration {#services-pinchflat-usage-configuration} + The following snippet assumes a few blocks have been setup already: - the [secrets block](usage.html#usage-secrets) with SOPS, @@ -46,7 +52,7 @@ Secrets can be randomly generated with `nix run nixpkgs#openssl -- rand -hex 64` The [user](#services-pinchflat-options-shb.pinchflat.ldap.userGroup) LDAP group is created automatically. -## Backup {#services-pinchflat-usage-backup} +### Backup {#services-pinchflat-usage-backup} Backing up Pinchflat using the [Restic block](blocks-restic.html) is done like so: @@ -63,6 +69,22 @@ The name `"pinchflat"` in the `instances` can be anything. The `config.shb.pinchflat.backup` option provides what directories to backup. You can define any number of Restic instances to backup Pinchflat multiple times. +### Application Dashboard {#services-pinchflat-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the [dashboard option](#services-pinchflat-options-shb.pinchflat.dashboard). + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Media.services.Pinchflat = { + sortOrder = 2; + dashboard.request = config.shb.pinchflat.dashboard.request; + }; +} +``` + ## Options Reference {#services-pinchflat-options} ```{=include=} options diff --git a/modules/services/vaultwarden.nix b/modules/services/vaultwarden.nix index 8e09a29..7adad9e 100644 --- a/modules/services/vaultwarden.nix +++ b/modules/services/vaultwarden.nix @@ -170,6 +170,20 @@ in default = false; example = true; }; + + dashboard = lib.mkOption { + description = '' + Dashboard contract consumer + ''; + default = { }; + type = lib.types.submodule { + options = shb.contracts.dashboard.mkRequester { + externalUrl = "https://${cfg.subdomain}.${cfg.domain}"; + externalUrlText = "https://\${config.shb.vaultwarden.subdomain}.\${config.shb.vaultwarden.domain}"; + internalUrl = "http://127.0.0.1:${toString cfg.port}"; + }; + }; + }; }; config = lib.mkIf cfg.enable { diff --git a/modules/services/vaultwarden/docs/default.md b/modules/services/vaultwarden/docs/default.md index 49c4454..f903454 100644 --- a/modules/services/vaultwarden/docs/default.md +++ b/modules/services/vaultwarden/docs/default.md @@ -12,7 +12,7 @@ This NixOS module is a service that sets up a [Vaultwarden Server](https://githu - Backup of the data directory through the [backup contract](./contracts-backup.html). - [Integration Tests](@REPO@/test/services/vaultwarden.nix) - Tests /admin can only be accessed when authenticated with SSO. -- Access to advanced options not exposed here thanks to how NixOS modules work. +- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. ## Usage {#services-vaultwarden-usage} @@ -120,6 +120,22 @@ The name `"vaultwarden"` in the `instances` can be anything. The `config.shb.vaultwarden.backup` option provides what directories to backup. You can define any number of Restic instances to backup Vaultwarden multiple times. +### Application Dashboard {#services-vaultwarden-usage-applicationdashboard} + +Integration with the [dashboard contract](contracts-dashboard.html) is provided +by the [dashboard option](#services-vaultwarden-options-shb.vaultwarden.dashboard). + +For example using the [Homepage](services-homepage.html) service: + +```nix +{ + shb.homepage.servicesGroups.Documents.services.Vaultwarden = { + sortOrder = 10; + dashboard.request = config.shb.vaultwarden.dashboard.request; + }; +} +``` + ## Maintenance {#services-vaultwarden-maintenance} No command-line tool is provided to administer Vaultwarden. diff --git a/test/modules/homepage.nix b/test/modules/homepage.nix new file mode 100644 index 0000000..bbe8256 --- /dev/null +++ b/test/modules/homepage.nix @@ -0,0 +1,269 @@ +{ shb }: +{ + testHomepageAsServiceGroup = { + expected = [ + { + "Media" = [ + { + "Jellyfin" = { + "href" = "https://example.com/jellyfin"; + "icon" = "sh-jellyfin"; + "siteMonitor" = "http://127.0.0.1:8096"; + }; + } + ]; + } + ]; + + expr = shb.homepage.asServiceGroup { + Media = { + services = { + Jellyfin = { + dashboard.request = { + externalUrl = "https://example.com/jellyfin"; + internalUrl = "http://127.0.0.1:8096"; + }; + apiKey = null; + }; + }; + }; + }; + }; + + testHomepageAsServiceGroupApiKey = { + expected = [ + { + "Media" = [ + { + "Jellyfin" = { + "href" = "https://example.com/jellyfin"; + "icon" = "sh-jellyfin"; + "siteMonitor" = "http://127.0.0.1:8096"; + "widget" = { + "key" = "{{HOMEPAGE_FILE_Media_Jellyfin}}"; + "password" = "{{HOMEPAGE_FILE_Media_Jellyfin}}"; + "type" = "jellyfin"; + "url" = "http://127.0.0.1:8096"; + }; + }; + } + ]; + } + ]; + + expr = shb.homepage.asServiceGroup { + Media = { + services = { + Jellyfin = { + dashboard.request = { + externalUrl = "https://example.com/jellyfin"; + internalUrl = "http://127.0.0.1:8096"; + }; + apiKey.result.path = "path_D"; + }; + }; + }; + }; + }; + + testHomepageAsServiceGroupNoServiceMonitor = { + expected = [ + { + "Media" = [ + { + "Jellyfin" = { + "href" = "https://example.com/jellyfin"; + "icon" = "sh-jellyfin"; + "siteMonitor" = null; + }; + } + ]; + } + ]; + + expr = shb.homepage.asServiceGroup { + Media = { + services = { + Jellyfin = { + dashboard.request = { + externalUrl = "https://example.com/jellyfin"; + internalUrl = null; + }; + apiKey = null; + }; + }; + }; + }; + }; + + testHomepageAsServiceGroupOverride = { + expected = [ + { + "Media" = [ + { + "Jellyfin" = { + "href" = "https://example.com/jellyfin"; + "icon" = "sh-icon"; + "siteMonitor" = "http://127.0.0.1:8096"; + }; + } + ]; + } + ]; + + expr = shb.homepage.asServiceGroup { + Media = { + services = { + Jellyfin = { + dashboard.request = { + externalUrl = "https://example.com/jellyfin"; + internalUrl = "http://127.0.0.1:8096"; + }; + settings = { + icon = "sh-icon"; + }; + apiKey = null; + }; + }; + }; + }; + }; + + testHomepageAsServiceGroupSortOrder = { + expected = [ + { "C" = [ ]; } + { "A" = [ ]; } + { "B" = [ ]; } + ]; + + expr = shb.homepage.asServiceGroup { + A = { + sortOrder = 2; + services = { }; + }; + B = { + sortOrder = 3; + services = { }; + }; + C = { + sortOrder = 1; + services = { }; + }; + }; + }; + + testHomepageAsServiceServicesSortOrder = { + expected = [ + { + "Media" = [ + { + "A" = { + "href" = "https://example.com/a"; + "icon" = "sh-a"; + "siteMonitor" = null; + }; + } + { + "C" = { + "href" = "https://example.com/c"; + "icon" = "sh-c"; + "siteMonitor" = null; + }; + } + { + "B" = { + "href" = "https://example.com/b"; + "icon" = "sh-b"; + "siteMonitor" = null; + }; + } + ]; + } + ]; + + expr = shb.homepage.asServiceGroup { + Media = { + sortOrder = null; + services = { + A = { + sortOrder = 1; + dashboard.request = { + externalUrl = "https://example.com/a"; + internalUrl = null; + }; + apiKey = null; + }; + B = { + sortOrder = 3; + dashboard.request = { + externalUrl = "https://example.com/b"; + internalUrl = null; + }; + apiKey = null; + }; + C = { + sortOrder = 2; + dashboard.request = { + externalUrl = "https://example.com/c"; + internalUrl = null; + }; + apiKey = null; + }; + }; + }; + }; + }; + + testHomepageAllKeys = { + expected = { + "A_A" = "path_A"; + "A_B" = "path_B"; + "B_D" = "path_D"; + }; + + expr = shb.homepage.allKeys { + A = { + sortOrder = 1; + services = { + A = { + sortOrder = 1; + dashboard.request = { + externalUrl = "https://example.com/a"; + internalUrl = null; + }; + apiKey.result.path = "path_A"; + }; + B = { + sortOrder = 2; + dashboard.request = { + externalUrl = "https://example.com/b"; + internalUrl = null; + }; + apiKey.result.path = "path_B"; + }; + }; + }; + B = { + sortOrder = 2; + services = { + C = { + sortOrder = 1; + dashboard.request = { + externalUrl = "https://example.com/a"; + internalUrl = null; + }; + apiKey = null; + }; + D = { + sortOrder = 2; + dashboard.request = { + externalUrl = "https://example.com/b"; + internalUrl = null; + }; + apiKey.result.path = "path_D"; + }; + }; + }; + }; + }; +} diff --git a/test/modules/lib.nix b/test/modules/lib.nix index 589c23e..9711d9a 100644 --- a/test/modules/lib.nix +++ b/test/modules/lib.nix @@ -131,3 +131,4 @@ in ''; }; } +// import ./homepage.nix { inherit shb; } diff --git a/test/services/homepage.nix b/test/services/homepage.nix new file mode 100644 index 0000000..f0cdd7f --- /dev/null +++ b/test/services/homepage.nix @@ -0,0 +1,209 @@ +{ shb, ... }: +let + commonTestScript = shb.test.mkScripts { + hasSSL = { node, ... }: !(isNull node.config.shb.homepage.ssl); + waitForServices = + { ... }: + [ + "homepage-dashboard.service" + "nginx.service" + ]; + waitForPorts = + { node, ... }: + [ + node.config.services.homepage-dashboard.listenPort + ]; + }; + + basic = + { config, ... }: + { + imports = [ + shb.test.baseModule + ../../modules/blocks/hardcodedsecret.nix + ../../modules/services/homepage.nix + ]; + + test = { + subdomain = "h"; + }; + + shb.homepage = { + enable = true; + inherit (config.test) subdomain domain; + + servicesGroups.MyHomeGroup.services.TestService.dashboard = { }; + }; + }; + + clientLogin = + { config, ... }: + { + imports = [ + shb.test.baseModule + shb.test.clientLoginModule + ]; + test = { + subdomain = "h"; + }; + + test.login = { + startUrl = "http://${config.test.fqdn}"; + # There is no login without SSO integration. + testLoginWith = [ + { + username = null; + password = null; + nextPageExpect = [ + "expect(page.get_by_text('TestService')).to_be_visible()" + ]; + } + ]; + }; + }; + + https = + { config, ... }: + { + shb.homepage = { + ssl = config.shb.certs.certs.selfsigned.n; + }; + }; + + ldap = + { config, ... }: + { + shb.homepage = { + ldap = { + userGroup = "user_group"; + }; + }; + }; + + clientLoginSso = + { config, ... }: + { + imports = [ + shb.test.baseModule + shb.test.clientLoginModule + ]; + test = { + subdomain = "h"; + }; + + test.login = { + startUrl = "https://${config.test.fqdn}"; + usernameFieldLabelRegex = "Username"; + passwordFieldLabelRegex = "Password"; + loginButtonNameRegex = "[sS]ign [iI]n"; + testLoginWith = [ + { + username = "alice"; + password = "NotAlicePassword"; + nextPageExpect = [ + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()" + ]; + } + { + username = "alice"; + password = "AlicePassword"; + nextPageExpect = [ + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()" + "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" + "expect(page.get_by_text('TestService')).to_be_visible(timeout=10000)" + ]; + } + # Bob, with its admin role only, cannot login into Karakeep because admins do not exist in Karakeep. + { + username = "charlie"; + password = "NotCharliePassword"; + nextPageExpect = [ + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()" + ]; + } + { + username = "charlie"; + password = "CharliePassword"; + nextPageExpect = [ + "expect(page).to_have_url(re.compile('.*/authenticated'))" + ]; + } + ]; + }; + }; + + sso = + { config, ... }: + { + shb.homepage = { + sso = { + enable = true; + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + }; + }; + }; +in +{ + basic = shb.test.runNixOSTest { + name = "homepage_basic"; + + nodes.client = { + imports = [ + clientLogin + ]; + }; + nodes.server = { + imports = [ + basic + ]; + }; + + testScript = commonTestScript.access; + }; + + https = shb.test.runNixOSTest { + name = "homepage_https"; + + nodes.client = { + imports = [ + clientLogin + ]; + }; + nodes.server = { + imports = [ + basic + shb.test.certs + https + ]; + }; + + testScript = commonTestScript.access; + }; + + sso = shb.test.runNixOSTest { + name = "homepage_sso"; + + nodes.client = { + imports = [ + clientLoginSso + ]; + }; + nodes.server = + { config, pkgs, ... }: + { + imports = [ + basic + shb.test.certs + https + shb.test.ldap + ldap + (shb.test.sso config.shb.certs.certs.selfsigned.n) + sso + ]; + }; + + testScript = commonTestScript.access.override { + redirectSSO = true; + }; + }; +}