homepage: init with dashboard contract

This commit is contained in:
ibizaman 2026-02-23 22:36:27 +01:00 committed by Pierre Penninckx
parent 77869300ee
commit f485cc60fe
50 changed files with 3072 additions and 411 deletions

View file

@ -37,13 +37,17 @@ Provided contracts are:
- [SSL generator contract](contracts-ssl.html) to generate SSL certificates. - [SSL generator contract](contracts-ssl.html) to generate SSL certificates.
Two providers are implemented: self-signed and Let's Encrypt. Two providers are implemented: self-signed and Let's Encrypt.
- [Backup contract][] to backup directories. - [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. - [Database Backup contract](contracts-databasebackup.html) to backup database dumps.
One provider is implemented: [Restic][]. One provider is implemented: [BorgBackup][] and [Restic][].
- [Secret contract](contracts-secret.html) to provide secrets that are deployed outside of the Nix store. - [Contract for Secrets](contracts-secret.html) to provide secrets that are deployed outside of the Nix store.
One provider is implemented: [SOPS][]. 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 [backup contract]: contracts-backup.html
[borgbackup]: blocks-borgbackup.html
[homepage]: services-homepage.html
[restic]: blocks-restic.html [restic]: blocks-restic.html
[sops]: blocks-sops.html [sops]: blocks-sops.html
@ -63,6 +67,10 @@ modules/contracts/databasebackup/docs/default.md
modules/contracts/secret/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} ## Problem Statement {#contracts-why}
Currently in nixpkgs, every module accessing a shared resource Currently in nixpkgs, every module accessing a shared resource

View file

@ -128,3 +128,29 @@ shb.sops.secret."awesome" = {
request = config.shb.restic.instances.awesome.settings.passphrase.request; 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}";
};
};
}
```

View file

@ -53,9 +53,6 @@
"blocks-authelia-forward-auth": [ "blocks-authelia-forward-auth": [
"blocks-authelia.html#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-oidc": [
"blocks-authelia.html#blocks-authelia-oidc" "blocks-authelia.html#blocks-authelia-oidc"
], ],
@ -65,6 +62,21 @@
"blocks-authelia-options-shb.authelia.autheliaUser": [ "blocks-authelia-options-shb.authelia.autheliaUser": [
"blocks-authelia.html#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-options-shb.authelia.dcdomain": [
"blocks-authelia.html#blocks-authelia-options-shb.authelia.dcdomain" "blocks-authelia.html#blocks-authelia-options-shb.authelia.dcdomain"
], ],
@ -323,6 +335,9 @@
"blocks-authelia-troubleshooting": [ "blocks-authelia-troubleshooting": [
"blocks-authelia.html#blocks-authelia-troubleshooting" "blocks-authelia.html#blocks-authelia-troubleshooting"
], ],
"blocks-authelia-usage-configuration": [
"blocks-authelia.html#blocks-authelia-usage-configuration"
],
"blocks-borgbackup": [ "blocks-borgbackup": [
"blocks-borgbackup.html#blocks-borgbackup" "blocks-borgbackup.html#blocks-borgbackup"
], ],
@ -578,8 +593,8 @@
"blocks-lldap": [ "blocks-lldap": [
"blocks-lldap.html#blocks-lldap" "blocks-lldap.html#blocks-lldap"
], ],
"blocks-lldap-global-setup": [ "blocks-lldap-features": [
"blocks-lldap.html#blocks-lldap-global-setup" "blocks-lldap.html#blocks-lldap-features"
], ],
"blocks-lldap-manage-groups": [ "blocks-lldap-manage-groups": [
"blocks-lldap.html#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-options-shb.lldap.backup.result.restoreScript": [
"blocks-lldap.html#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-options-shb.lldap.dcdomain": [
"blocks-lldap.html#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-options-shb.lldap.webUIListenPort": [
"blocks-lldap.html#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-tests": [
"blocks-lldap.html#blocks-lldap-tests" "blocks-lldap.html#blocks-lldap-tests"
], ],
"blocks-lldap-troubleshooting": [ "blocks-lldap-troubleshooting": [
"blocks-lldap.html#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": [
"blocks-mitmdump.html#blocks-mitmdump" "blocks-mitmdump.html#blocks-mitmdump"
], ],
@ -953,6 +992,21 @@
"blocks-monitoring-options-shb.monitoring.contactPoints": [ "blocks-monitoring-options-shb.monitoring.contactPoints": [
"blocks-monitoring.html#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-options-shb.monitoring.dashboards": [
"blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.dashboards" "blocks-monitoring.html#blocks-monitoring-options-shb.monitoring.dashboards"
], ],
@ -1133,6 +1187,12 @@
"blocks-monitoring-usage": [ "blocks-monitoring-usage": [
"blocks-monitoring.html#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-usage-log-optimization": [
"blocks-monitoring.html#blocks-monitoring-usage-log-optimization" "blocks-monitoring.html#blocks-monitoring-usage-log-optimization"
], ],
@ -1712,6 +1772,15 @@
"contract-backup-usage": [ "contract-backup-usage": [
"contracts-backup.html#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": [ "contract-databasebackup": [
"contracts-databasebackup.html#contract-databasebackup" "contracts-databasebackup.html#contract-databasebackup"
], ],
@ -1805,6 +1874,27 @@
"contracts-concept": [ "contracts-concept": [
"contracts.html#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-options-shb.contracts.databasebackup": [
"contracts-databasebackup.html#contracts-databasebackup-options-shb.contracts.databasebackup" "contracts-databasebackup.html#contracts-databasebackup-options-shb.contracts.databasebackup"
], ],
@ -2156,12 +2246,18 @@
"recipes-exposeService": [ "recipes-exposeService": [
"recipes-exposeService.html#recipes-exposeService" "recipes-exposeService.html#recipes-exposeService"
], ],
"recipes-exposeService-applicationdashboard": [
"recipes-exposeService.html#recipes-exposeService-applicationdashboard"
],
"recipes-exposeService-backup": [ "recipes-exposeService-backup": [
"recipes-exposeService.html#recipes-exposeService-backup" "recipes-exposeService.html#recipes-exposeService-backup"
], ],
"recipes-exposeService-debug": [ "recipes-exposeService-debug": [
"recipes-exposeService.html#recipes-exposeService-debug" "recipes-exposeService.html#recipes-exposeService-debug"
], ],
"recipes-exposeService-impermanence": [
"recipes-exposeService.html#recipes-exposeService-impermanence"
],
"recipes-exposeService-ldap": [ "recipes-exposeService-ldap": [
"recipes-exposeService.html#recipes-exposeService-ldap" "recipes-exposeService.html#recipes-exposeService-ldap"
], ],
@ -2216,6 +2312,9 @@
"services-arr": [ "services-arr": [
"services-arr.html#services-arr" "services-arr.html#services-arr"
], ],
"services-arr-features": [
"services-arr.html#services-arr-features"
],
"services-arr-options": [ "services-arr-options": [
"services-arr.html#services-arr-options" "services-arr.html#services-arr-options"
], ],
@ -2258,6 +2357,21 @@
"services-arr-options-shb.arr.bazarr.backup.result.restoreScript": [ "services-arr-options-shb.arr.bazarr.backup.result.restoreScript": [
"services-arr.html#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-options-shb.arr.bazarr.dataDir": [
"services-arr.html#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-options-shb.arr.bazarr.settings": [
"services-arr.html#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-options-shb.arr.bazarr.settings.LogLevel": [
"services-arr.html#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-options-shb.arr.jackett.backup.result.restoreScript": [
"services-arr.html#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-options-shb.arr.jackett.dataDir": [
"services-arr.html#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-options-shb.arr.lidarr.backup.result.restoreScript": [
"services-arr.html#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-options-shb.arr.lidarr.dataDir": [
"services-arr.html#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-options-shb.arr.lidarr.settings": [
"services-arr.html#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-options-shb.arr.lidarr.settings.LogLevel": [
"services-arr.html#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-options-shb.arr.radarr.backup.result.restoreScript": [
"services-arr.html#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-options-shb.arr.radarr.dataDir": [
"services-arr.html#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-options-shb.arr.readarr.backup.result.restoreScript": [
"services-arr.html#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-options-shb.arr.readarr.dataDir": [
"services-arr.html#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-options-shb.arr.readarr.settings": [
"services-arr.html#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-options-shb.arr.readarr.settings.LogLevel": [
"services-arr.html#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-options-shb.arr.sonarr.backup.result.restoreScript": [
"services-arr.html#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-options-shb.arr.sonarr.dataDir": [
"services-arr.html#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-options-shb.arr.sonarr.subdomain": [
"services-arr.html#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-category-ai": [
"services.html#services-category-ai" "services.html#services-category-ai"
], ],
@ -2744,6 +2984,9 @@
"services-category-code": [ "services-category-code": [
"services.html#services-category-code" "services.html#services-category-code"
], ],
"services-category-dashboard": [
"services.html#services-category-dashboard"
],
"services-category-documents": [ "services-category-documents": [
"services.html#services-category-documents" "services.html#services-category-documents"
], ],
@ -2771,6 +3014,9 @@
"services-firefly-iii-declarative-ldap": [ "services-firefly-iii-declarative-ldap": [
"services-firefly-iii.html#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-impermanence": [
"services-firefly-iii.html#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-options-shb.firefly-iii.backup.result.restoreScript": [
"services-firefly-iii.html#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-options-shb.firefly-iii.dbPassword": [
"services-firefly-iii.html#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-usage": [
"services-firefly-iii.html#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-usage-backup": [
"services-firefly-iii.html#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": [
"services-forgejo.html#services-forgejo" "services-forgejo.html#services-forgejo"
], ],
@ -3098,6 +3365,21 @@
"services-forgejo-options-shb.forgejo.backup.result.restoreScript": [ "services-forgejo-options-shb.forgejo.backup.result.restoreScript": [
"services-forgejo.html#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-options-shb.forgejo.databasePassword": [
"services-forgejo.html#services-forgejo-options-shb.forgejo.databasePassword" "services-forgejo.html#services-forgejo-options-shb.forgejo.databasePassword"
], ],
@ -3359,6 +3641,9 @@
"services-forgejo-usage": [ "services-forgejo-usage": [
"services-forgejo.html#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-usage-backup": [
"services-forgejo.html#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-options-shb.home-assistant.config.unit_system": [
"services-home-assistant.html#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-options-shb.home-assistant.domain": [
"services-home-assistant.html#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-usage": [
"services-home-assistant.html#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-usage-backup": [
"services-home-assistant.html#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-usage-voice": [
"services-home-assistant.html#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": [
"services-jellyfin.html#services-jellyfin" "services-jellyfin.html#services-jellyfin"
], ],
@ -3623,6 +4052,21 @@
"services-jellyfin-options-shb.jellyfin.backup.result.restoreScript": [ "services-jellyfin-options-shb.jellyfin.backup.result.restoreScript": [
"services-jellyfin.html#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-options-shb.jellyfin.debug": [
"services-jellyfin.html#services-jellyfin-options-shb.jellyfin.debug" "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.debug"
], ],
@ -3776,9 +4220,15 @@
"services-jellyfin-usage": [ "services-jellyfin-usage": [
"services-jellyfin.html#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-usage-backup": [
"services-jellyfin.html#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": [
"services-karakeep.html#services-karakeep" "services-karakeep.html#services-karakeep"
], ],
@ -3824,6 +4274,21 @@
"services-karakeep-options-shb.karakeep.backup.result.restoreScript": [ "services-karakeep-options-shb.karakeep.backup.result.restoreScript": [
"services-karakeep.html#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-options-shb.karakeep.domain": [
"services-karakeep.html#services-karakeep-options-shb.karakeep.domain" "services-karakeep.html#services-karakeep-options-shb.karakeep.domain"
], ],
@ -3974,6 +4439,12 @@
"services-karakeep-usage": [ "services-karakeep-usage": [
"services-karakeep.html#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": [
"services-mailserver.html#services-mailserver" "services-mailserver.html#services-mailserver"
], ],
@ -4583,6 +5054,21 @@
"services-nextcloudserver-options-shb.nextcloud.backup.result.restoreScript": [ "services-nextcloudserver-options-shb.nextcloud.backup.result.restoreScript": [
"services-nextcloud.html#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-nextcloudserver-options-shb.nextcloud.dataDir": [
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.dataDir" "services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.dataDir"
], ],
@ -4667,6 +5153,9 @@
"services-nextcloudserver-usage": [ "services-nextcloudserver-usage": [
"services-nextcloud.html#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-nextcloudserver-usage-backup": [
"services-nextcloud.html#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-options-shb.open-webui.backup.result.restoreScript": [
"services-open-webui.html#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-options-shb.open-webui.domain": [
"services-open-webui.html#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-usage": [
"services-open-webui.html#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-usage-backup": [
"services-open-webui.html#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": [
"services-pinchflat.html#services-pinchflat" "services-pinchflat.html#services-pinchflat"
], ],
"services-pinchflat-features": [
"services-pinchflat.html#services-pinchflat-features"
],
"services-pinchflat-options": [ "services-pinchflat-options": [
"services-pinchflat.html#services-pinchflat-options" "services-pinchflat.html#services-pinchflat-options"
], ],
@ -4901,6 +5414,21 @@
"services-pinchflat-options-shb.pinchflat.backup.result.restoreScript": [ "services-pinchflat-options-shb.pinchflat.backup.result.restoreScript": [
"services-pinchflat.html#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-options-shb.pinchflat.domain": [
"services-pinchflat.html#services-pinchflat-options-shb.pinchflat.domain" "services-pinchflat.html#services-pinchflat-options-shb.pinchflat.domain"
], ],
@ -4982,9 +5510,15 @@
"services-pinchflat-usage": [ "services-pinchflat-usage": [
"services-pinchflat.html#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-usage-backup": [
"services-pinchflat.html#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": [
"services-vaultwarden.html#services-vaultwarden" "services-vaultwarden.html#services-vaultwarden"
], ],
@ -5039,6 +5573,21 @@
"services-vaultwarden-options-shb.vaultwarden.backup.result.restoreScript": [ "services-vaultwarden-options-shb.vaultwarden.backup.result.restoreScript": [
"services-vaultwarden.html#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-options-shb.vaultwarden.databasePassword": [
"services-vaultwarden.html#services-vaultwarden-options-shb.vaultwarden.databasePassword" "services-vaultwarden.html#services-vaultwarden-options-shb.vaultwarden.databasePassword"
], ],
@ -5150,6 +5699,9 @@
"services-vaultwarden-usage": [ "services-vaultwarden-usage": [
"services-vaultwarden.html#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-usage-configuration": [
"services-vaultwarden.html#services-vaultwarden-usage-configuration" "services-vaultwarden.html#services-vaultwarden-usage-configuration"
], ],

View file

@ -1,9 +1,9 @@
<!-- Read these docs at https://shb.skarabox.com --> <!-- Read these docs at https://shb.skarabox.com -->
# Services {#services} # 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 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. That is possible thanks to the extensive use of blocks provided by SHB.
::: {.note} ::: {.note}
@ -19,6 +19,7 @@ information is provided in the respective manual sections.
| [Firefly-iii][] | Y (1) | Y | Y | Y | Y (2) | N | | [Firefly-iii][] | Y (1) | Y | Y | Y | Y (2) | N |
| [Forgejo][] | 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 | | [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 | | [Jellyfin][] | Y (1) | Y | Y | Y | Y (2) | N |
| [Karakeep][] | 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) | | [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 [Firefly-iii]: services-firefly-iii.html
[Forgejo]: services-forgejo.html [Forgejo]: services-forgejo.html
[Home-Assistant]: services-home-assistant.html [Home-Assistant]: services-home-assistant.html
[Homepage]: services-homepage.html
[Jellyfin]: services-jellyfin.html [Jellyfin]: services-jellyfin.html
[Karakeep]: services-karakeep.html [Karakeep]: services-karakeep.html
[Nextcloud Server]: services-nextcloud.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 [Simple NixOS Mailserver]: services-mailserver.html
[Vaultwarden]: services-vaultwarden.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} ## Documents {#services-category-documents}
```{=include=} chapters html:into-file=//services-nextcloud.html ```{=include=} chapters html:into-file=//services-nextcloud.html

View file

@ -66,6 +66,9 @@
# The contract dummies are used to show options for contracts. # The contract dummies are used to show options for contracts.
contractDummyModules = [ contractDummyModules = [
modules/contracts/backup/dummyModule.nix 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 modules/contracts/ssl/dummyModule.nix
]; ];
in in
@ -110,6 +113,7 @@
(pkgs.path + "/nixos/modules/services/misc/forgejo.nix") (pkgs.path + "/nixos/modules/services/misc/forgejo.nix")
]; ];
"services/home-assistant" = ./modules/services/home-assistant.nix; "services/home-assistant" = ./modules/services/home-assistant.nix;
"services/homepage" = ./modules/services/homepage.nix;
"services/jellyfin" = ./modules/services/jellyfin.nix; "services/jellyfin" = ./modules/services/jellyfin.nix;
"services/karakeep" = ./modules/services/karakeep.nix; "services/karakeep" = ./modules/services/karakeep.nix;
"services/mailserver" = ./modules/services/mailserver.nix; "services/mailserver" = ./modules/services/mailserver.nix;
@ -131,6 +135,14 @@
"backup" "backup"
]; ];
}; };
"contracts/dashboard" = {
module = ./modules/contracts/dashboard/dummyModule.nix;
optionRoot = [
"shb"
"contracts"
"dashboard"
];
};
"contracts/databasebackup" = { "contracts/databasebackup" = {
module = ./modules/contracts/databasebackup/dummyModule.nix; module = ./modules/contracts/databasebackup/dummyModule.nix;
optionRoot = [ optionRoot = [
@ -298,6 +310,7 @@
// (vm_test "hledger" ./test/services/hledger.nix) // (vm_test "hledger" ./test/services/hledger.nix)
// (vm_test "immich" ./test/services/immich.nix) // (vm_test "immich" ./test/services/immich.nix)
// (vm_test "homeassistant" ./test/services/home-assistant.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 "jellyfin" ./test/services/jellyfin.nix)
// (vm_test "karakeep" ./test/services/karakeep.nix) // (vm_test "karakeep" ./test/services/karakeep.nix)
// (vm_test "nextcloud" ./test/services/nextcloud.nix) // (vm_test "nextcloud" ./test/services/nextcloud.nix)
@ -407,6 +420,7 @@
self.nixosModules.hledger self.nixosModules.hledger
self.nixosModules.immich self.nixosModules.immich
self.nixosModules.home-assistant self.nixosModules.home-assistant
self.nixosModules.homepage
self.nixosModules.jellyfin self.nixosModules.jellyfin
self.nixosModules.karakeep self.nixosModules.karakeep
self.nixosModules.mailserver self.nixosModules.mailserver
@ -445,6 +459,7 @@
nixosModules.hledger = modules/services/hledger.nix; nixosModules.hledger = modules/services/hledger.nix;
nixosModules.immich = modules/services/immich.nix; nixosModules.immich = modules/services/immich.nix;
nixosModules.home-assistant = modules/services/home-assistant.nix; nixosModules.home-assistant = modules/services/home-assistant.nix;
nixosModules.homepage = modules/services/homepage.nix;
nixosModules.jellyfin = modules/services/jellyfin.nix; nixosModules.jellyfin = modules/services/jellyfin.nix;
nixosModules.karakeep = modules/services/karakeep.nix; nixosModules.karakeep = modules/services/karakeep.nix;
nixosModules.mailserver = modules/services/mailserver.nix; nixosModules.mailserver = modules/services/mailserver.nix;

View file

@ -2,411 +2,420 @@
let let
inherit (builtins) isAttrs hasAttr; inherit (builtins) isAttrs hasAttr;
inherit (lib) any concatMapStringsSep concatStringsSep; inherit (lib) any concatMapStringsSep concatStringsSep;
in shb = rec {
rec { # Replace secrets in a file.
# Replace secrets in a file. # - userConfig is an attrset that will produce a config file.
# - userConfig is an attrset that will produce a config file. # - resultPath is the location the config file should have on the filesystem.
# - 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
# - generator is a function taking two arguments name and value and returning path in the nix # nix store where the
# nix store where the replaceSecrets =
replaceSecrets = {
{ userConfig,
userConfig, resultPath,
resultPath, generator,
generator, user ? null,
user ? null, permissions ? "u=r,g=r,o=",
permissions ? "u=r,g=r,o=", }:
}: let
let configWithTemplates = withReplacements userConfig;
configWithTemplates = withReplacements userConfig;
nonSecretConfigFile = generator "template" configWithTemplates; nonSecretConfigFile = generator "template" configWithTemplates;
replacements = getReplacements userConfig; replacements = getReplacements userConfig;
in in
replaceSecretsScript { replaceSecretsScript {
file = nonSecretConfigFile; file = nonSecretConfigFile;
inherit resultPath replacements; inherit resultPath replacements;
inherit user permissions; 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.";
}; };
transform = lib.mkOption { replaceSecretsFormatAdapter = format: format.generate;
type = lib.types.raw; replaceSecretsGeneratorAdapter =
description = "An optional function to transform the secret."; generator: name: value:
default = null; pkgs.writeText "generator " (generator value);
example = lib.literalExpression '' toEnvVar = replaceSecretsGeneratorAdapter (
v: "prefix-$${v}-suffix" 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 = secretName =
names: "%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) names)}%"; names: "%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) names)}%";
withReplacements = withReplacements =
attrs: attrs:
let let
valueOrReplacement = valueOrReplacement =
name: value: if !(builtins.isAttrs value && value ? "source") then value else secretName name; name: value: if !(builtins.isAttrs value && value ? "source") then value else secretName name;
in in
mapAttrsRecursiveCond (v: !v ? "source") valueOrReplacement attrs; mapAttrsRecursiveCond (v: !v ? "source") valueOrReplacement attrs;
getReplacements = getReplacements =
attrs: attrs:
let let
addNameField = addNameField =
name: value: name: value:
if !(builtins.isAttrs value && value ? "source") then value else value // { name = name; }; if !(builtins.isAttrs value && value ? "source") then value else value // { name = name; };
secretsWithName = mapAttrsRecursiveCond (v: !v ? "source") addNameField attrs; secretsWithName = mapAttrsRecursiveCond (v: !v ? "source") addNameField attrs;
in in
collect (v: builtins.isAttrs v && v ? "source") secretsWithName; collect (v: builtins.isAttrs v && v ? "source") secretsWithName;
# Inspired lib.attrsets.mapAttrsRecursiveCond but also recurses on lists. # Inspired lib.attrsets.mapAttrsRecursiveCond but also recurses on lists.
mapAttrsRecursiveCond = mapAttrsRecursiveCond =
# A function, given the attribute set the recursion is currently at, determine if to recurse deeper into that attribute set. # A function, given the attribute set the recursion is currently at, determine if to recurse deeper into that attribute set.
cond: cond:
# A function, given a list of attribute names and a value, returns a new value. # A function, given a list of attribute names and a value, returns a new value.
f: f:
# Attribute set or list to recursively map over. # Attribute set or list to recursively map over.
set: set:
let let
recurse = recurse =
path: val: path: val:
if builtins.isAttrs val && cond val then if builtins.isAttrs val && cond val then
lib.attrsets.mapAttrs (n: v: recurse (path ++ [ n ]) v) val lib.attrsets.mapAttrs (n: v: recurse (path ++ [ n ]) v) val
else if builtins.isList val && cond val then else if builtins.isList val && cond val then
lib.lists.imap0 (i: v: recurse (path ++ [ (builtins.toString i) ]) v) val lib.lists.imap0 (i: v: recurse (path ++ [ (builtins.toString i) ]) v) val
else else
f path val; f path val;
in in
recurse [ ] set; recurse [ ] set;
# Like lib.attrsets.collect but also recurses on lists. # Like lib.attrsets.collect but also recurses on lists.
collect = collect =
# Given an attribute's value, determine if recursion should stop. # Given an attribute's value, determine if recursion should stop.
pred: pred:
# The attribute set to recursively collect. # The attribute set to recursively collect.
attrs: attrs:
if pred attrs then if pred attrs then
[ attrs ] [ attrs ]
else if builtins.isAttrs attrs then else if builtins.isAttrs attrs then
lib.lists.concatMap (collect pred) (lib.attrsets.attrValues attrs) lib.lists.concatMap (collect pred) (lib.attrsets.attrValues attrs)
else if builtins.isList attrs then else if builtins.isList attrs then
lib.lists.concatMap (collect pred) attrs lib.lists.concatMap (collect pred) attrs
else else
[ ]; [ ];
indent = indent =
i: str: i: str:
lib.concatMapStringsSep "\n" (x: (lib.strings.replicate i " ") + x) (lib.splitString "\n" str); lib.concatMapStringsSep "\n" (x: (lib.strings.replicate i " ") + x) (lib.splitString "\n" str);
# Generator for XML # Generator for XML
formatXML = formatXML =
{ {
enclosingRoot ? null, enclosingRoot ? null,
}: }:
{ {
type = type =
with lib.types; with lib.types;
let let
valueType = valueType =
nullOr (oneOf [ nullOr (oneOf [
bool bool
int int
float float
str str
path path
(attrsOf valueType) (attrsOf valueType)
(listOf valueType) (listOf valueType)
]) ])
// { // {
description = "XML value"; description = "XML value";
}; };
in in
valueType; valueType;
generate = generate =
name: value: name: value:
pkgs.callPackage ( 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, python3 }:
runCommand "config" runCommand "config"
{ {
value = builtins.toJSON (if enclosingRoot == null then value else { ${enclosingRoot} = value; }); inherit xml;
passAsFile = [ "value" ]; passAsFile = [ "xml" ];
} }
( (
pkgs.writers.writePython3 "dict2xml" pkgs.writers.writePython3 "xml2json"
{ {
libraries = with python3.pkgs; [ libraries = with python3.pkgs; [ python ];
python
dict2xml
];
} }
'' ''
import os import os
import json 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: 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 = # Taken from https://github.com/antifuchs/nix-flake-tests/blob/main/default.nix
xml: # with a nicer diff display function.
let check =
xmlToJsonFile = pkgs.callPackage ( { pkgs, tests }:
{ runCommand, python3 }: let
runCommand "config" formatValue =
val:
if (builtins.isList val || builtins.isAttrs val) then
builtins.toJSON val
else
builtins.toString val;
resultToString =
{ {
inherit xml; name,
passAsFile = [ "xml" ]; expected,
} result,
( }:
pkgs.writers.writePython3 "xml2json" builtins.readFile (
pkgs.runCommand "nix-flake-tests-error"
{ {
libraries = with python3.pkgs; [ python ]; expected = formatValue expected;
result = formatValue result;
passAsFile = [
"expected"
"result"
];
} }
'' ''
import os echo "${name} failed (- expected, + result)" > $out
import json cp ''${expectedPath} ''${expectedPath}.json
from collections import ChainMap cp ''${resultPath} ''${resultPath}.json
from xml.etree import ElementTree ${pkgs.deepdiff}/bin/deep diff ''${expectedPath}.json ''${resultPath}.json >> $out
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)
'' ''
) );
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 = update =
attrset: from: to: attr: fn: attrset:
(lib.attrsets.filterAttrs (name: v: name == from) attrset) attrset // { ${attr} = fn attrset.${attr}; };
// {
${to} = attrset.${from};
};
# Taken from https://github.com/antifuchs/nix-flake-tests/blob/main/default.nix };
# with a nicer diff display function. in
check = shb
{ pkgs, tests }: // {
let homepage = pkgs.callPackage ./homepage.nix { inherit shb; };
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
'';
})
) { };
} }

92
lib/homepage.nix Normal file
View file

@ -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
;
}

View file

@ -390,6 +390,20 @@ in
to see exactly what Authelia receives and sends back. 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 { config = lib.mkIf cfg.enable {

View file

@ -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). 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. Authelia cannot work without SSL and LDAP.
So setting up the Authelia block requires to setup the [SSL block][] first 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][]. as the `shb.lldap.ldapUserPassword` defined for the [LLDAP block][].
This is done using Sops' `key` option. 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} ## SHB OIDC integration {#blocks-authelia-shb-oidc}
For services [provided by SelfHostBlocks][services] that handle [OIDC integration][OIDC], For services [provided by SelfHostBlocks][services] that handle [OIDC integration][OIDC],

View file

@ -333,6 +333,20 @@ in
type = types.bool; type = types.bool;
default = true; 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 { config = lib.mkIf cfg.enable {

View file

@ -7,7 +7,13 @@ across services.
[LLDAP]: https://github.com/lldap/lldap [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 ```nix
shb.lldap = { shb.lldap = {
@ -29,7 +35,7 @@ shb.sops.secret."lldap/user_password".request = config.shb.lldap.ldapUserPasswor
This assumes secrets are setup with SOPS This assumes secrets are setup with SOPS
as mentioned in [the secrets setup section](usage.html#usage-secrets) of the manual. 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 SSL is an important security practice, like always.
Using the [SSL block][], the configuration to add to the one above is: 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}; 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 For added security, you can restrict access to the LLDAP UI
by adding the following line: by adding the following line:
@ -53,6 +59,22 @@ by adding the following line:
shb.lldap.restrictAccessIPRange = "192.168.50.0/24"; 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} ## Manage Groups {#blocks-lldap-manage-groups}
The following snippet will create group named "family" if it does not exist yet. The following snippet will create group named "family" if it does not exist yet.

View file

@ -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 [ config = lib.mkMerge [

View file

@ -17,9 +17,12 @@ This block sets up the monitoring stack for Self Host Blocks. It is composed of:
- Registration is enabled through SSO. - Registration is enabled through SSO.
- Access through [subdomain](#blocks-monitoring-options-shb.monitoring.subdomain) using reverse proxy. - Access through [subdomain](#blocks-monitoring-options-shb.monitoring.subdomain) using reverse proxy.
- Access through [HTTPS](#blocks-monitoring-options-shb.monitoring.ssl) 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} ## Usage {#blocks-monitoring-usage}
### Initial Configuration {#blocks-monitoring-usage-configuration}
The following snippet assumes a few blocks have been setup already: The following snippet assumes a few blocks have been setup already:
- the [secrets block](usage.html#usage-secrets) with SOPS, - 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"; 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} ## Provisioning {#blocks-monitoring-provisioning}
Self Host Blocks will create automatically the following resources: Self Host Blocks will create automatically the following resources:

View file

@ -33,7 +33,9 @@ Here is an example module defining such a `backup` option:
{ {
options = { options = {
myservice.backup = mkOption { myservice.backup = mkOption {
type = contracts.backup.request; type = lib.types.submodule {
options = contracts.backup.request;
};
default = { default = {
user = "myservice"; user = "myservice";
sourceDirectories = [ sourceDirectories = [

View file

@ -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 = {
};
};
};
}

View file

@ -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:
<!-- Somehow generate this list -->
- The homepage service under its [shb.homepage.servicesGroups.<name>.services.<name>.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 = {
<provider-module> = {
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@
```

View file

@ -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;
};
};
}

View file

@ -48,21 +48,28 @@ let
importContract = importContract =
module: module:
let let
importedModule = pkgs.callPackage module { inherit shb; }; importedModule = pkgs.callPackage module {
shb = shb // {
inherit contracts;
};
};
in in
mkContractFunctions { mkContractFunctions {
inherit (importedModule) mkRequest mkResult; inherit (importedModule) mkRequest mkResult;
}; };
in
{ contracts = {
databasebackup = importContract ./databasebackup.nix; databasebackup = importContract ./databasebackup.nix;
backup = importContract ./backup.nix; dashboard = importContract ./dashboard.nix;
mount = pkgs.callPackage ./mount.nix { }; backup = importContract ./backup.nix;
secret = importContract ./secret.nix; mount = pkgs.callPackage ./mount.nix { };
ssl = pkgs.callPackage ./ssl.nix { }; secret = importContract ./secret.nix;
test = { ssl = pkgs.callPackage ./ssl.nix { };
secret = pkgs.callPackage ./secret/test.nix { inherit shb; }; test = {
databasebackup = pkgs.callPackage ./databasebackup/test.nix { inherit shb; }; secret = pkgs.callPackage ./secret/test.nix { inherit shb; };
backup = pkgs.callPackage ./backup/test.nix { inherit shb; }; databasebackup = pkgs.callPackage ./databasebackup/test.nix { inherit shb; };
backup = pkgs.callPackage ./backup/test.nix { inherit shb; };
};
}; };
} in
contracts

View file

@ -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 { }); // (c.moreOptions or { });
}; };

View file

@ -3,12 +3,242 @@
Defined in [`/modules/services/arr.nix`](@REPO@/modules/services/arr.nix). Defined in [`/modules/services/arr.nix`](@REPO@/modules/services/arr.nix).
This NixOS module sets up multiple [Servarr](https://wiki.servarr.com/) services. This NixOS module sets up multiple [Servarr](https://wiki.servarr.com/) services.
## Features {#services-arr-features}
Compared to the stock module from nixpkgs, Compared to the stock module from nixpkgs,
this one sets up, in a fully declarative manner this one sets up, in a fully declarative manner
LDAP and SSO integration as well as the API key. 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} ## Options Reference {#services-arr-options}

View file

@ -152,6 +152,20 @@ in
default = false; default = false;
example = true; 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 ( config = lib.mkIf cfg.enable (

View file

@ -36,7 +36,7 @@ in
options.shb.deluge = { options.shb.deluge = {
enable = lib.mkEnableOption "the SHB Deluge service"; enable = lib.mkEnableOption "the SHB Deluge service";
enableDashboard = lib.mkEnableOption "the Torrents SHB dashboard" // { enableDashboard = lib.mkEnableOption "the Torrents SHB monitoring dashboard" // {
default = true; default = true;
}; };
@ -301,6 +301,20 @@ in
default = null; default = null;
example = "info"; 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 ( config = lib.mkIf cfg.enable (

View file

@ -10,6 +10,9 @@ let
in in
{ {
imports = [ imports = [
../blocks/nginx.nix
../blocks/lldap.nix
../../lib/module.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 ( config = lib.mkIf cfg.enable (

View file

@ -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. 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. 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} ## Usage {#services-firefly-iii-usage}
### Initial Configuration {#services-firefly-iii-usage-configuration}
The following snippet assumes a few blocks have been setup already: The following snippet assumes a few blocks have been setup already:
- the [secrets block](usage.html#usage-secrets) with SOPS, - 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} ## Database Inspection {#services-firefly-iii-database-inspection}
Access the database with: Access the database with:

View file

@ -408,6 +408,21 @@ in
type = bool; type = bool;
default = false; 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 [ config = mkMerge [

View file

@ -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 [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). - 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). - [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} ## 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. The `config.shb.forgejo.backup` option provides what directories to backup.
You can define any number of Restic instances to backup Forgejo multiple times. 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} ### Extra Settings {#services-forgejo-usage-extra-settings}
Other Forgejo settings can be accessed through the nixpkgs [stock service][]. Other Forgejo settings can be accessed through the nixpkgs [stock service][].

View file

@ -111,6 +111,21 @@ in
default = false; default = false;
example = true; 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 ( config = lib.mkIf cfg.enable (

View file

@ -82,6 +82,21 @@ in
default = [ "--forecast" ]; default = [ "--forecast" ];
type = lib.types.listOf lib.types.str; 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 { config = lib.mkIf cfg.enable {

View file

@ -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 { config = lib.mkIf cfg.enable {

View file

@ -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 [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). - 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). - [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. - 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`, You will then need to configure more options like the `repository`,
as explained in the [restic](blocks-restic.html) documentation. 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} ### Extra Components {#services-home-assistant-usage-extra-components}
Packaged components can be found in the documentation of the corresponding option Packaged components can be found in the documentation of the corresponding option

View file

@ -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.
```
<subdomain>.<domain>
```
'';
example = "homepage";
};
domain = lib.mkOption {
description = ''
Domain under which homepage is served.
```
<subdomain>.<domain>
```
'';
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;
}
)
];
};
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

View file

@ -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.<service>.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@
```

View file

@ -465,6 +465,20 @@ in
default = false; default = false;
example = true; 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 { config = mkIf cfg.enable {

View file

@ -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 = [ imports = [

View file

@ -20,9 +20,12 @@ this one sets up, in a fully declarative manner:
- Declarative [LDAP](#services-jellyfin-options-shb.jellyfin.ldap) configuration. - Declarative [LDAP](#services-jellyfin-options-shb.jellyfin.ldap) configuration.
- Declarative [SSO](#services-jellyfin-options-shb.jellyfin.sso) 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). - [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} ## Usage {#services-jellyfin-usage}
### Initial Configuration {#services-jellyfin-usage-configuration}
The following snippet assumes a few blocks have been setup already: The following snippet assumes a few blocks have been setup already:
- the [secrets block](usage.html#usage-secrets) with SOPS, - 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} ## Debug {#services-jellyfin-debug}
In case of an issue, check the logs for systemd service `jellyfin.service`. In case of an issue, check the logs for systemd service `jellyfin.service`.

View file

@ -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 = ( config = (

View file

@ -21,9 +21,12 @@ It integrates well with [Ollama][].
- Access through [subdomain](#services-karakeep-options-shb.karakeep.subdomain) using reverse proxy. - Access through [subdomain](#services-karakeep-options-shb.karakeep.subdomain) using reverse proxy.
- Access through [HTTPS](#services-karakeep-options-shb.karakeep.ssl) 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). - [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} ## Usage {#services-karakeep-usage}
### Initial Configuration {#services-karakeep-usage-configuration}
The following snippet assumes a few blocks have been setup already: The following snippet assumes a few blocks have been setup already:
- the [secrets block](usage.html#usage-secrets) with SOPS, - 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) and [admin](#services-open-webui-options-shb.open-webui.ldap.adminGroup)
LDAP groups are created automatically. 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} ## Integration with Ollama {#services-karakeep-ollama}
Assuming ollama is enabled, it will be available on port `config.services.ollama.port`. Assuming ollama is enabled, it will be available on port `config.services.ollama.port`.

View file

@ -694,6 +694,21 @@ in
but Self Host Blocks assumes that it will not be the case for most users. 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 [ config = lib.mkMerge [

View file

@ -35,8 +35,8 @@ It is based on the nixpkgs Nextcloud server and provides opinionated defaults.
- Forces PostgreSQL as the database. - Forces PostgreSQL as the database.
- Forces Redis as the cache and sets good defaults. - Forces Redis as the cache and sets good defaults.
- Backup of the [`shb.nextcloud.dataDir`][dataDir] through the [backup block](./blocks-backup.html). - 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 - [Monitoring Dashboard](#services-nextcloudserver-dashboard) for monitoring of reverse proxy, PHP-FPM, and database backups through the [monitoring block](./blocks-monitoring.html).
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) - [Integration Tests](@REPO@/test/services/nextcloud.nix)
- Tests system cron job is setup correctly. - Tests system cron job is setup correctly.
- Tests initial admin user and password are 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. not just the Nextcloud database.
This limitation will be lifted in the future. 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} ### Enable Preview Generator App {#services-nextcloudserver-usage-previewgenerator}
The following snippet installs and enables the [Preview 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 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. 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" The dashboard is added to Grafana automatically under "Self Host Blocks > Nextcloud"
as long as the Nextcloud service is [enabled][] as long as the Nextcloud service is [enabled][]

View file

@ -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 = ( config = (

View file

@ -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 [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. - 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). - [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} ## Usage {#services-open-webui-usage}
### Initial Configuration {#services-open-webui-usage-configuration}
The following snippet assumes a few blocks have been setup already: The following snippet assumes a few blocks have been setup already:
- the [secrets block](usage.html#usage-secrets) with SOPS, - 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) and [admin](#services-open-webui-options-shb.open-webui.ldap.adminGroup)
LDAP groups are created automatically. 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} ## Integration with OLLAMA {#services-open-webui-ollama}
Assuming ollama is enabled, it will be available on port `config.services.ollama.port`. Assuming ollama is enabled, it will be available on port `config.services.ollama.port`.

View file

@ -59,10 +59,8 @@ let
mkIf mkIf
lists lists
mkOption mkOption
optionals
; ;
inherit (lib.types) inherit (lib.types)
attrs
attrsOf attrsOf
bool bool
enum 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 { config = mkIf cfg.enable {

View file

@ -1,7 +1,6 @@
{ {
config, config,
lib, lib,
pkgs,
shb, 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 { config = lib.mkIf cfg.enable {

View file

@ -9,8 +9,14 @@ this one sets up, in a fully declarative manner,
LDAP and SSO integration LDAP and SSO integration
and has a nicer option for secrets. 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} ## Usage {#services-pinchflat-usage}
### Initial Configuration {#services-pinchflat-usage-configuration}
The following snippet assumes a few blocks have been setup already: The following snippet assumes a few blocks have been setup already:
- the [secrets block](usage.html#usage-secrets) with SOPS, - 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) The [user](#services-pinchflat-options-shb.pinchflat.ldap.userGroup)
LDAP group is created automatically. 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: 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. The `config.shb.pinchflat.backup` option provides what directories to backup.
You can define any number of Restic instances to backup Pinchflat multiple times. 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} ## Options Reference {#services-pinchflat-options}
```{=include=} options ```{=include=} options

View file

@ -170,6 +170,20 @@ in
default = false; default = false;
example = true; 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 { config = lib.mkIf cfg.enable {

View file

@ -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). - Backup of the data directory through the [backup contract](./contracts-backup.html).
- [Integration Tests](@REPO@/test/services/vaultwarden.nix) - [Integration Tests](@REPO@/test/services/vaultwarden.nix)
- Tests /admin can only be accessed when authenticated with SSO. - 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} ## 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. The `config.shb.vaultwarden.backup` option provides what directories to backup.
You can define any number of Restic instances to backup Vaultwarden multiple times. 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} ## Maintenance {#services-vaultwarden-maintenance}
No command-line tool is provided to administer Vaultwarden. No command-line tool is provided to administer Vaultwarden.

269
test/modules/homepage.nix Normal file
View file

@ -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";
};
};
};
};
};
}

View file

@ -131,3 +131,4 @@ in
''; '';
}; };
} }
// import ./homepage.nix { inherit shb; }

209
test/services/homepage.nix Normal file
View file

@ -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;
};
};
}