about summary refs log tree commit diff
path: root/modules
diff options
context:
space:
mode:
authorAzat Bahawi <azat@bahawi.net>2023-04-01 04:39:59 +0300
committerAzat Bahawi <azat@bahawi.net>2023-04-01 04:39:59 +0300
commitf4145939712b0046e5d57906d4b157b8a150614d (patch)
tree3c8d24db6410692e0fa59570ff698d446ae5a96e /modules
parent2023-03-23 (diff)
2023-04-01
Diffstat (limited to 'modules')
-rw-r--r--modules/common/emacs/default.nix298
-rw-r--r--modules/common/qutebrowser.nix4
-rw-r--r--modules/nixos/firefox/userContent.css9
-rw-r--r--modules/nixos/monitoring/dashboards/ntfy.json2362
-rw-r--r--modules/nixos/monitoring/default.nix47
-rw-r--r--modules/nixos/ntfy.nix31
-rw-r--r--modules/nixos/postgresql.nix5
-rw-r--r--modules/nixos/promtail.nix24
-rw-r--r--modules/nixos/soju.nix15
9 files changed, 2632 insertions, 163 deletions
diff --git a/modules/common/emacs/default.nix b/modules/common/emacs/default.nix
index 3be560e..5499d48 100644
--- a/modules/common/emacs/default.nix
+++ b/modules/common/emacs/default.nix
@@ -25,136 +25,165 @@ in {
     };
 
     hm = {
-      xdg.configFile = {
+      xdg.configFile = mapAttrs (_: value:
+        value
+        // {
+          onChange = with config.hm.programs; ''
+            export EMACSDIR="''${XDG_CONFIG_HOME:-$HOME/.config}/emacs"
+            export DOOMDIR="''${XDG_CONFIG_HOME:-$HOME/.config}/doom"
+
+            if [[ ! -d "$EMACSDIR/.git" ]]; then
+              ${git.package}/bin/git clone --depth=1 --branch=master \
+                "https://github.com/doomemacs/doomemacs" "$EMACSDIR"
+            fi
+
+            if [[ ! -d "$DOOMDIR" ]]; then
+              mkdir -p "$DOOMDIR/snippets"
+            fi
+
+            if [[ -x "$EMACSDIR/bin/doom" ]]; then
+              if [[ ! -d "$EMACSDIR/.local" ]]; then
+                PATH="''${PATH:-/bin:/usr/bin:/usr/local/bin}:${emacs.package}/bin:${git.package}/bin" \
+                  "$EMACSDIR/bin/doom" install --force --verbose
+              fi
+
+              PATH="''${PATH:-/bin:/usr/bin:/usr/local/bin}:${emacs.package}/bin:${git.package}/bin" \
+                "$EMACSDIR/bin/doom" sync -e -p --force --verbose
+            fi
+          '';
+        }) {
         "doom/init.el".source = ./doom/init.el;
         "doom/packages.el".source = ./doom/packages.el;
         "doom/config.el" = {
-          text = concatStringsSep "\n" [
-            (let
-              # NOTE gopls will require the "go" executable which must be provided
-              # by the project's flake/shell.
-              extraBins = with pkgs;
-                [
-                  (aspellWithDicts (p: with p; [en ru])) # :checkers (spell +aspell)
-                  (python3.withPackages (p:
-                    with p; [
-                      black # :lang python :editor format
-                      isort # :lang python
-                      pyflakes # :lang python
-                      python-lsp-server # :lang (python +lsp)
-                    ]))
-                  asmfmt # :editor format
-                  bash-language-server # :lang (sh +lsp)
-                  clang-tools # :lang (cc +lsp) :editor format
-                  cmake # :term vterm
-                  cmake-format # :lang cc :editor format
-                  cmigemo # :lang japanese
-                  css-language-server # :lang (web +lsp)
-                  dhall-language-server # :lang (dhall +lsp)
-                  dockerfile-language-server # :tools (docker +lsp)
-                  editorconfig # :tools editorconfig
-                  fd # doom!
-                  gcc # :tools magit :term vterm
-                  gnuplot # :lang (org +gnuplot)
-                  gnutls # doom!
-                  go-language-server # :lang (go +lsp)
-                  gomodifytags # :lang go
-                  gore # :lang go
-                  gotests # :lang go
-                  gotools # :lang go
-                  graphviz # :lang (org +roam2) :lang plantuml
-                  html-language-server # :lang (web +lsp)
-                  html-tidy # :lang web
-                  jre # :lang plantuml
-                  json-language-server # :lang (json +lsp)
-                  libtool # :term vterm
-                  nix-language-server # :lang (nix +lsp)
-                  nodePackages.eslint # :lang (json +lsp)
-                  nodePackages.js-beautify # :lang web
-                  nodePackages.prettier # :editor format
-                  nodePackages.stylelint # :lang web
-                  nodejs # :tools debugger
-                  pandoc # :lang org markdown latex
-                  perl # term vterm
-                  pinentry-emacs # doom!
-                  pre-commit # :tools magit
-                  ripgrep # doom!
-                  rust-analyzer # :lang (rust +lsp)
-                  rustfmt # :lang rust
-                  shellcheck # :lang sh
-                  shfmt # :lang sh :editor format
-                  sqlite # :lang (org +roam2) :tools lookup
-                  texlab # lang (tex +lsp)
-                  texlive.combined.scheme-full # :lang org tex
-                  unzip # :tools debugger
-                  wordnet # :tools (lookup +dictionary +offline)
-                  yaml-language-server # :lang (yaml +lsp)
-                  zls # :lang (zig +lsp)
-                  zstd # :emacs undo
-                ]
-                ++ (
-                  #
-                  # GDB doesn't support[1] Apple Silicon on MacOS.
-                  #
-                  # [1]: https://inbox.sourceware.org/gdb/6b48224b-9e2e-518d-793b-df4fc5514884@arm.com/
-                  if (this.system != "aarch64-darwin")
-                  then [gdb] # :tools debugger
-                  else [lldb] # :tools debugger
-                )
-                ++ optionals (!pkgs.stdenv.isDarwin)
-                [
-                  # NOTE Haskell is pretty much broken every couple of days on
-                  # MacOS and I usually don't write anything in Haskell while
-                  # I'm on my work laptop, so... ShellCheck seems to be working,
-                  # though.
-                  haskellPackages.ormolu # :lang haskell :editor format
-                  haskellPackages.haskell-language-server # :lang (haskell +lsp)
-                  haskellPackages.cabal-fmt # :lang haskell :editor format
-                  haskellPackages.cabal-install # :lang haskell
-                  haskellPackages.hoogle # :lang haskell
-                ];
-            in ''
-              ;; This will integrate packages which are required by various
-              ;; modules without polluting the user's profile.
-              (setq exec-path (append exec-path '(${
-                concatMapStringsSep " " (x: ''"${x}/bin"'') extraBins
-              })))
-              (setenv "PATH" (concat (getenv "PATH") ":${
-                concatMapStringsSep ":" (x: "${x}/bin") extraBins
-              }"))
-
-              (appendq! auth-sources '("${config.secrets.authinfo.path}"))
-
-              ;; Font must be set to N+2 because otherwise it looks too small.
-              (setq doom-font (font-spec
-                                :family "${config.fontScheme.monospaceFont.family}"
-                                :size ${toString (config.fontScheme.monospaceFont.size + 2)})
-                    doom-unicode-font doom-font)
-
-              ;; :app irc
-              (setq circe-default-nick "${my.username}"
-                    circe-default-realname "${my.email}"
-                    circe-default-user circe-default-nick)
-
-              ;; :lang plantuml
-              (setq org-plantuml-jar-path "${pkgs.plantuml}/lib/plantuml.jar")
-
-              ;; :input japanese
-              (setq migemo-dictionary "${pkgs.cmigemo}/share/migemo/utf-8/migemo-dict")
-
-              ;; :input japanese
-              (setq skk-large-jisyo "${pkgs.skk-dicts}/share/skk/SKK-JISYO.L")
-
-              ;; :lang nix
-              (setq nix-nixfmt-bin "${pkgs.writeShellScript "nixfmt" ''
-                ${pkgs.alejandra}/bin/alejandra --quiet "$@"
-              ''}")
-            '')
+          text = concatLines [
+            (
+              let
+                # NOTE gopls will require the "go" executable which must be provided
+                # by the project's flake/shell.
+                extraBins = with pkgs;
+                  [
+                    (aspellWithDicts (p: with p; [en ru])) # :checkers (spell +aspell)
+                    (python3.withPackages (p:
+                      with p; [
+                        black # :lang python :editor format
+                        isort # :lang python
+                        pyflakes # :lang python
+                        python-lsp-server # :lang (python +lsp)
+                      ]))
+                    asmfmt # :editor format
+                    bash-language-server # :lang (sh +lsp)
+                    clang-tools # :lang (cc +lsp) :editor format
+                    cmake # :term vterm
+                    cmake-format # :lang cc :editor format
+                    cmigemo # :lang japanese
+                    css-language-server # :lang (web +lsp)
+                    dhall-language-server # :lang (dhall +lsp)
+                    dockerfile-language-server # :tools (docker +lsp)
+                    editorconfig # :tools editorconfig
+                    fd # doom!
+                    gcc # :tools magit :term vterm
+                    gnumake # :term vterm
+                    gnuplot # :lang (org +gnuplot)
+                    gnutls # doom!
+                    go-language-server # :lang (go +lsp)
+                    gomodifytags # :lang go
+                    gore # :lang go
+                    gotests # :lang go
+                    gotools # :lang go
+                    graphviz # :lang (org +roam2) :lang plantuml
+                    html-language-server # :lang (web +lsp)
+                    html-tidy # :lang web
+                    jre # :lang plantuml
+                    json-language-server # :lang (json +lsp)
+                    libtool # :term vterm
+                    nix-language-server # :lang (nix +lsp)
+                    nodePackages.eslint # :lang (json +lsp)
+                    nodePackages.js-beautify # :lang web
+                    nodePackages.prettier # :editor format
+                    nodePackages.stylelint # :lang web
+                    nodejs # :tools debugger
+                    pandoc # :lang org markdown latex
+                    perl # term vterm
+                    pinentry-emacs # doom!
+                    pre-commit # :tools magit
+                    ripgrep # doom!
+                    rust-analyzer # :lang (rust +lsp)
+                    rustfmt # :lang rust
+                    shellcheck # :lang sh
+                    shfmt # :lang sh :editor format
+                    sqlite # :lang (org +roam2) :tools lookup
+                    texlab # lang (tex +lsp)
+                    texlive.combined.scheme-full # :lang org tex
+                    unzip # :tools debugger
+                    wordnet # :tools (lookup +dictionary +offline)
+                    yaml-language-server # :lang (yaml +lsp)
+                    zls # :lang (zig +lsp)
+                    zstd # :emacs undo
+                  ]
+                  ++ (
+                    # GDB doesn't support[1] Apple Silicon.
+                    #
+                    # [1]: https://inbox.sourceware.org/gdb/6b48224b-9e2e-518d-793b-df4fc5514884@arm.com/
+                    if (this.system != "aarch64-darwin")
+                    then [gdb] # :tools debugger
+                    else [lldb] # :tools debugger
+                  )
+                  ++ optionals (!pkgs.stdenv.isDarwin)
+                  [
+                    # NOTE Haskell is pretty much broken every couple of days on
+                    # MacOS and I usually don't write anything in Haskell while
+                    # I'm on my work laptop, so... ShellCheck seems to be working,
+                    # though.
+                    haskellPackages.ormolu # :lang haskell :editor format
+                    haskellPackages.haskell-language-server # :lang (haskell +lsp)
+                    haskellPackages.cabal-fmt # :lang haskell :editor format
+                    haskellPackages.cabal-install # :lang haskell
+                    haskellPackages.hoogle # :lang haskell
+                  ];
+              in ''
+                ;; This will integrate packages which are required by various
+                ;; modules without polluting the user's profile.
+                (setq exec-path (append exec-path '(${
+                  concatMapStringsSep " " (x: ''"${x}/bin"'') extraBins
+                })))
+                (setenv "PATH" (concat (getenv "PATH") ":${
+                  concatMapStringsSep ":" (x: "${x}/bin") extraBins
+                }"))
+
+                (appendq! auth-sources '("${config.secrets.authinfo.path}"))
+
+                ;; Font must be set to N+2 because otherwise it looks too small.
+                (setq doom-font (font-spec
+                                  :family "${config.fontScheme.monospaceFont.family}"
+                                  :size ${toString (config.fontScheme.monospaceFont.size + 2)})
+                      doom-unicode-font doom-font)
+
+                ;; :app irc
+                (setq circe-default-nick "${my.username}"
+                      circe-default-realname "${my.email}"
+                      circe-default-user circe-default-nick)
+
+                ;; :lang plantuml
+                (setq org-plantuml-jar-path "${pkgs.plantuml}/lib/plantuml.jar")
+
+                ;; :input japanese
+                (setq migemo-dictionary "${pkgs.cmigemo}/share/migemo/utf-8/migemo-dict")
+
+                ;; :input japanese
+                ;; (setq skk-large-jisyo "${pkgs.skk-dicts}/share/skk/SKK-JISYO.L")
+
+                ;; :lang nix
+                (setq nix-nixfmt-bin "${pkgs.writeShellScript "nixfmt" ''
+                  ${pkgs.alejandra}/bin/alejandra --quiet "$@"
+                ''}")
+              ''
+            )
             (with config.hm.accounts.email; let
               mu4eAccounts = let
                 muAccounts = filter (a: a.mu.enable) (attrValues accounts);
               in
-                concatMapStringsSep "\n" (a:
+                concatMapStringsSep "\n"
+                (a:
                   with a; let
                     personalAddresses = concatMapStringsSep " " (v: ''"${v}"'') aliases;
                   in ''
@@ -177,29 +206,6 @@ in {
             '')
             (builtins.readFile ./doom/config.el)
           ];
-          onChange = with config.hm.programs; ''
-            export DOOMDIR="$HOME/.config/doom"
-            export EMACSDIR="$HOME/.config/emacs"
-
-            if [[ ! -d "$EMACSDIR/.git" ]]; then
-              ${git.package}/bin/git clone --depth=1 --branch=master \
-                "https://github.com/doomemacs/doomemacs" "$EMACSDIR"
-            fi
-
-            if [[ ! -d "$DOOMDIR" ]]; then
-              mkdir -p "$DOOMDIR/snippets"
-            fi
-
-            if [[ -x "$EMACSDIR/bin/doom" ]]; then
-              oldpath="$PATH"
-              export PATH="''${PATH:-/bin}:${emacs.package}/bin:${git.package}/bin"
-
-              "$EMACSDIR/bin/doom" sync -e -p --force --verbose
-
-              export PATH="$oldpath"
-              unset oldpath
-            fi
-          '';
         };
       };
 
diff --git a/modules/common/qutebrowser.nix b/modules/common/qutebrowser.nix
index 68a41a5..7913001 100644
--- a/modules/common/qutebrowser.nix
+++ b/modules/common/qutebrowser.nix
@@ -509,7 +509,7 @@ in {
             }
           ];
         in
-          concatStringsSep "\n" final + "\n")
+          concatLines final + "\n")
         + (let
           allowSetting = setting: url: "config.set('content.${setting}', True, '${url}')";
 
@@ -530,7 +530,7 @@ in {
 
           final = allowedMediaCapture ++ allowedNotifications;
         in
-          concatStringsSep "\n" final + "\n");
+          concatLines final + "\n");
     };
   };
 }
diff --git a/modules/nixos/firefox/userContent.css b/modules/nixos/firefox/userContent.css
index 2de8cde..2b515e3 100644
--- a/modules/nixos/firefox/userContent.css
+++ b/modules/nixos/firefox/userContent.css
@@ -158,6 +158,15 @@
     }
 }
 
+@-moz-document regexp("https?://grafana\.com/docs/.*")
+{
+    .ads__content,
+    .scroll,
+    .sticky-footer {
+        display: none !important;
+    }
+}
+
 @-moz-document regexp("https?://habr\.com/(ru|en)/(article|company/.*/news|blog|post)/.*")
 {
     .Vue-Toastification__container,
diff --git a/modules/nixos/monitoring/dashboards/ntfy.json b/modules/nixos/monitoring/dashboards/ntfy.json
new file mode 100644
index 0000000..fd02a2e
--- /dev/null
+++ b/modules/nixos/monitoring/dashboards/ntfy.json
@@ -0,0 +1,2362 @@
+{
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": {
+          "type": "grafana",
+          "uid": "-- Grafana --"
+        },
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "target": {
+          "limit": 100,
+          "matchAny": false,
+          "tags": [],
+          "type": "dashboard"
+        },
+        "type": "dashboard"
+      }
+    ]
+  },
+  "editable": true,
+  "fiscalYearStartMonth": 0,
+  "graphTooltip": 0,
+  "id": 75,
+  "links": [],
+  "liveNow": false,
+  "panels": [
+    {
+      "collapsed": false,
+      "gridPos": {
+        "h": 1,
+        "w": 24,
+        "x": 0,
+        "y": 0
+      },
+      "id": 38,
+      "panels": [],
+      "title": "Overview",
+      "type": "row"
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "PBFA97CFB590B2093"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "thresholds"
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "light-green",
+                "value": null
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 3,
+        "w": 4,
+        "x": 0,
+        "y": 1
+      },
+      "id": 36,
+      "options": {
+        "colorMode": "value",
+        "graphMode": "none",
+        "justifyMode": "auto",
+        "orientation": "auto",
+        "reduceOptions": {
+          "calcs": [
+            "last"
+          ],
+          "fields": "",
+          "values": false
+        },
+        "textMode": "auto"
+      },
+      "pluginVersion": "9.4.7",
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "code",
+          "expr": "ntfy_messages_published_success{job=\"$job\"}",
+          "legendFormat": "Messages cached",
+          "range": true,
+          "refId": "A"
+        }
+      ],
+      "title": "Published",
+      "type": "stat"
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "PBFA97CFB590B2093"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "thresholds"
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "orange",
+                "value": null
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 3,
+        "w": 4,
+        "x": 4,
+        "y": 1
+      },
+      "id": 33,
+      "options": {
+        "colorMode": "value",
+        "graphMode": "none",
+        "justifyMode": "auto",
+        "orientation": "auto",
+        "reduceOptions": {
+          "calcs": [
+            "last"
+          ],
+          "fields": "",
+          "values": false
+        },
+        "textMode": "auto"
+      },
+      "pluginVersion": "9.4.7",
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "builder",
+          "expr": "ntfy_messages_cached_total{job=\"$job\"}",
+          "legendFormat": "Messages cached",
+          "range": true,
+          "refId": "A"
+        }
+      ],
+      "title": "Cached",
+      "type": "stat"
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "PBFA97CFB590B2093"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "thresholds"
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "#69bfb5",
+                "value": null
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 3,
+        "w": 4,
+        "x": 8,
+        "y": 1
+      },
+      "id": 31,
+      "options": {
+        "colorMode": "value",
+        "graphMode": "none",
+        "justifyMode": "auto",
+        "orientation": "auto",
+        "reduceOptions": {
+          "calcs": [
+            "last"
+          ],
+          "fields": "",
+          "values": false
+        },
+        "textMode": "auto"
+      },
+      "pluginVersion": "9.4.7",
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "builder",
+          "expr": "ntfy_visitors_total{job=\"$job\"}",
+          "legendFormat": "Visitors",
+          "range": true,
+          "refId": "A"
+        }
+      ],
+      "title": "Visitors",
+      "type": "stat"
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "PBFA97CFB590B2093"
+      },
+      "description": "",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "thresholds"
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "red",
+                "value": 80
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 3,
+        "w": 4,
+        "x": 12,
+        "y": 1
+      },
+      "id": 32,
+      "options": {
+        "colorMode": "value",
+        "graphMode": "none",
+        "justifyMode": "auto",
+        "orientation": "auto",
+        "reduceOptions": {
+          "calcs": [
+            "last"
+          ],
+          "fields": "",
+          "values": false
+        },
+        "textMode": "auto"
+      },
+      "pluginVersion": "9.4.7",
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "builder",
+          "expr": "ntfy_users_total{job=\"$job\"}",
+          "legendFormat": "Visitors",
+          "range": true,
+          "refId": "A"
+        }
+      ],
+      "title": "Users",
+      "type": "stat"
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "PBFA97CFB590B2093"
+      },
+      "description": "",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "thresholds"
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "blue",
+                "value": null
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 3,
+        "w": 4,
+        "x": 16,
+        "y": 1
+      },
+      "id": 34,
+      "options": {
+        "colorMode": "value",
+        "graphMode": "none",
+        "justifyMode": "auto",
+        "orientation": "auto",
+        "reduceOptions": {
+          "calcs": [
+            "last"
+          ],
+          "fields": "",
+          "values": false
+        },
+        "textMode": "auto"
+      },
+      "pluginVersion": "9.4.7",
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "builder",
+          "expr": "ntfy_topics_total{job=\"$job\"}",
+          "legendFormat": "Topics",
+          "range": true,
+          "refId": "A"
+        }
+      ],
+      "title": "Topics",
+      "type": "stat"
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "PBFA97CFB590B2093"
+      },
+      "description": "",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "thresholds"
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "purple",
+                "value": null
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 3,
+        "w": 4,
+        "x": 20,
+        "y": 1
+      },
+      "id": 35,
+      "options": {
+        "colorMode": "value",
+        "graphMode": "none",
+        "justifyMode": "auto",
+        "orientation": "auto",
+        "reduceOptions": {
+          "calcs": [
+            "last"
+          ],
+          "fields": "",
+          "values": false
+        },
+        "textMode": "auto"
+      },
+      "pluginVersion": "9.4.7",
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "builder",
+          "expr": "ntfy_subscribers_total",
+          "legendFormat": "Subscribers",
+          "range": true,
+          "refId": "A"
+        }
+      ],
+      "title": "Subscribers",
+      "type": "stat"
+    },
+    {
+      "gridPos": {
+        "h": 1,
+        "w": 24,
+        "x": 0,
+        "y": 4
+      },
+      "id": 10,
+      "title": "Metrics",
+      "type": "row"
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "PBFA97CFB590B2093"
+      },
+      "description": "Number of successfully published messages, and messages that could not be published (due to rate limiting, bad formatting, etc.)",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisCenteredZero": false,
+            "axisColorMode": "text",
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              }
+            ]
+          }
+        },
+        "overrides": [
+          {
+            "matcher": {
+              "id": "byName",
+              "options": "Failed"
+            },
+            "properties": [
+              {
+                "id": "custom.axisColorMode",
+                "value": "text"
+              },
+              {
+                "id": "color",
+                "value": {
+                  "fixedColor": "red",
+                  "mode": "fixed"
+                }
+              }
+            ]
+          }
+        ]
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 6,
+        "x": 0,
+        "y": 5
+      },
+      "id": 42,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom",
+          "showLegend": true
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "code",
+          "expr": "rate(ntfy_messages_published_success{job=\"$job\"}[$rate])",
+          "legendFormat": "Success",
+          "range": true,
+          "refId": "A"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "code",
+          "expr": "rate(ntfy_messages_published_failure{job=\"$job\"}[$rate])",
+          "hide": false,
+          "legendFormat": "Failed",
+          "range": true,
+          "refId": "B"
+        }
+      ],
+      "title": "Messages published (per second)",
+      "type": "timeseries"
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "PBFA97CFB590B2093"
+      },
+      "description": "Number of messages published since last ntfy server restart",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisCenteredZero": false,
+            "axisColorMode": "text",
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "red",
+                "value": 80
+              }
+            ]
+          }
+        },
+        "overrides": [
+          {
+            "matcher": {
+              "id": "byName",
+              "options": "Failed"
+            },
+            "properties": [
+              {
+                "id": "color",
+                "value": {
+                  "fixedColor": "red",
+                  "mode": "fixed"
+                }
+              }
+            ]
+          }
+        ]
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 6,
+        "x": 6,
+        "y": 5
+      },
+      "id": 4,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom",
+          "showLegend": true
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "builder",
+          "expr": "ntfy_messages_published_success{job=\"$job\"}",
+          "legendFormat": "Successful",
+          "range": true,
+          "refId": "A"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "builder",
+          "expr": "ntfy_messages_published_failure{job=\"$job\"}",
+          "hide": false,
+          "legendFormat": "Failed",
+          "range": true,
+          "refId": "B"
+        }
+      ],
+      "title": "Messages published",
+      "type": "timeseries"
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "PBFA97CFB590B2093"
+      },
+      "description": "Number of messages currently stored in message cache",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisCenteredZero": false,
+            "axisColorMode": "text",
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "red",
+                "value": 80
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 6,
+        "x": 12,
+        "y": 5
+      },
+      "id": 2,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom",
+          "showLegend": true
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "builder",
+          "expr": "ntfy_messages_cached_total{job=\"$job\"}",
+          "legendFormat": "Messages in database",
+          "range": true,
+          "refId": "A"
+        }
+      ],
+      "title": "Messages cached",
+      "type": "timeseries"
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "PBFA97CFB590B2093"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisCenteredZero": false,
+            "axisColorMode": "text",
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "red",
+                "value": 80
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 6,
+        "x": 18,
+        "y": 5
+      },
+      "id": 14,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom",
+          "showLegend": true
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "builder",
+          "expr": "ntfy_visitors_total{job=\"$job\"}",
+          "legendFormat": "Visitors",
+          "range": true,
+          "refId": "A"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "builder",
+          "expr": "ntfy_topics_total{job=\"$job\"}",
+          "hide": false,
+          "legendFormat": "Topics",
+          "range": true,
+          "refId": "B"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "builder",
+          "expr": "ntfy_subscribers_total{job=\"$job\"}",
+          "hide": false,
+          "legendFormat": "Subscribers",
+          "range": true,
+          "refId": "C"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "builder",
+          "expr": "ntfy_users_total{job=\"$job\"}",
+          "hide": false,
+          "legendFormat": "Users",
+          "range": true,
+          "refId": "D"
+        }
+      ],
+      "title": "Visitors, subscribers, topics",
+      "type": "timeseries"
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "PBFA97CFB590B2093"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisCenteredZero": false,
+            "axisColorMode": "text",
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "red",
+                "value": 80
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 6,
+        "x": 0,
+        "y": 12
+      },
+      "id": 43,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom",
+          "showLegend": true
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "code",
+          "expr": "sum by(job) (rate(ntfy_http_requests_total{job=\"$job\"}[$rate]))",
+          "legendFormat": "Requests per second",
+          "range": true,
+          "refId": "A"
+        }
+      ],
+      "title": "HTTP requests (per second)",
+      "type": "timeseries"
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "PBFA97CFB590B2093"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisCenteredZero": false,
+            "axisColorMode": "text",
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "red",
+                "value": 80
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 9,
+        "x": 6,
+        "y": 12
+      },
+      "id": 41,
+      "options": {
+        "legend": {
+          "calcs": [
+            "mean"
+          ],
+          "displayMode": "table",
+          "placement": "right",
+          "showLegend": true,
+          "sortBy": "Mean",
+          "sortDesc": true
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "code",
+          "expr": "sum by(http_code) (rate(ntfy_http_requests_total{job=\"$job\", http_code!=\"200\", http_code!=\"429\", http_code!=\"507\"}[$rate]))",
+          "legendFormat": "{{http_code}}",
+          "range": true,
+          "refId": "A"
+        }
+      ],
+      "title": "HTTP errors (per second, excl. 429/507)",
+      "type": "timeseries"
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "PBFA97CFB590B2093"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisCenteredZero": false,
+            "axisColorMode": "text",
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "red",
+                "value": 80
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 9,
+        "x": 15,
+        "y": 12
+      },
+      "id": 16,
+      "options": {
+        "legend": {
+          "calcs": [
+            "mean"
+          ],
+          "displayMode": "table",
+          "placement": "right",
+          "showLegend": true,
+          "sortBy": "Mean",
+          "sortDesc": true
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "code",
+          "expr": "sum by(ntfy_code) (rate(ntfy_http_requests_total{http_code!=\"200\", job=\"$job\"}[$rate]))",
+          "legendFormat": "{{http_method}} {{http_code}} {{ntfy_code}}",
+          "range": true,
+          "refId": "A"
+        }
+      ],
+      "title": "HTTP errors (per second, ntfy code)",
+      "type": "timeseries"
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "PBFA97CFB590B2093"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisCenteredZero": false,
+            "axisColorMode": "text",
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "red",
+                "value": 80
+              }
+            ]
+          },
+          "unit": "decbytes"
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 6,
+        "x": 0,
+        "y": 19
+      },
+      "id": 20,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom",
+          "showLegend": true
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "builder",
+          "expr": "ntfy_attachments_total_size{job=\"$job\"}",
+          "legendFormat": "Total size in MB",
+          "range": true,
+          "refId": "A"
+        }
+      ],
+      "title": "Attachments: Total cache size",
+      "transformations": [],
+      "type": "timeseries"
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "PBFA97CFB590B2093"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisCenteredZero": false,
+            "axisColorMode": "text",
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": -1,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "red",
+                "value": 80
+              }
+            ]
+          }
+        },
+        "overrides": [
+          {
+            "matcher": {
+              "id": "byName",
+              "options": "Failure"
+            },
+            "properties": [
+              {
+                "id": "color",
+                "value": {
+                  "fixedColor": "red",
+                  "mode": "fixed"
+                }
+              }
+            ]
+          }
+        ]
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 6,
+        "x": 6,
+        "y": 19
+      },
+      "id": 27,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom",
+          "showLegend": true
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "code",
+          "expr": "rate(ntfy_firebase_published_success{job=\"$job\"}[$rate])",
+          "legendFormat": "Success",
+          "range": true,
+          "refId": "A"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "code",
+          "expr": "rate(ntfy_firebase_published_failure{job=\"$job\"}[$rate])",
+          "hide": false,
+          "legendFormat": "Failure",
+          "range": true,
+          "refId": "B"
+        }
+      ],
+      "title": "Firebase messages sent",
+      "type": "timeseries"
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "PBFA97CFB590B2093"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisCenteredZero": false,
+            "axisColorMode": "text",
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              }
+            ]
+          }
+        },
+        "overrides": [
+          {
+            "matcher": {
+              "id": "byName",
+              "options": "Rejected (HTTP 507)"
+            },
+            "properties": [
+              {
+                "id": "color",
+                "value": {
+                  "fixedColor": "red",
+                  "mode": "fixed"
+                }
+              }
+            ]
+          }
+        ]
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 6,
+        "x": 12,
+        "y": 19
+      },
+      "id": 26,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom",
+          "showLegend": true
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "code",
+          "expr": "rate(ntfy_unifiedpush_published_success{job=\"$job\"}[$rate])",
+          "legendFormat": "Success",
+          "range": true,
+          "refId": "A"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "code",
+          "expr": "rate(ntfy_http_requests_total{job=\"$job\",http_code=\"507\"}[$rate])",
+          "hide": false,
+          "legendFormat": "Rejected (HTTP 507)",
+          "range": true,
+          "refId": "B"
+        }
+      ],
+      "title": "UnifiedPush messages",
+      "type": "timeseries"
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "PBFA97CFB590B2093"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisCenteredZero": false,
+            "axisColorMode": "text",
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "red",
+                "value": 80
+              }
+            ]
+          }
+        },
+        "overrides": [
+          {
+            "matcher": {
+              "id": "byName",
+              "options": "Failure"
+            },
+            "properties": [
+              {
+                "id": "color",
+                "value": {
+                  "fixedColor": "red",
+                  "mode": "fixed"
+                }
+              }
+            ]
+          }
+        ]
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 6,
+        "x": 18,
+        "y": 19
+      },
+      "id": 24,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom",
+          "showLegend": true
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "code",
+          "expr": "rate(ntfy_matrix_published_success{job=\"$job\"}[$rate])",
+          "legendFormat": "Success",
+          "range": true,
+          "refId": "A"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "code",
+          "expr": "rate(ntfy_matrix_published_failure{job=\"$job\"}[$rate])",
+          "hide": false,
+          "legendFormat": "Failure",
+          "range": true,
+          "refId": "B"
+        }
+      ],
+      "title": "Matrix messages published",
+      "type": "timeseries"
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "PBFA97CFB590B2093"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisCenteredZero": false,
+            "axisColorMode": "text",
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green"
+              },
+              {
+                "color": "red",
+                "value": 80
+              }
+            ]
+          }
+        },
+        "overrides": [
+          {
+            "matcher": {
+              "id": "byName",
+              "options": "Failure"
+            },
+            "properties": [
+              {
+                "id": "color",
+                "value": {
+                  "fixedColor": "red",
+                  "mode": "fixed"
+                }
+              }
+            ]
+          }
+        ]
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 6,
+        "x": 0,
+        "y": 26
+      },
+      "id": 12,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom",
+          "showLegend": true
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "builder",
+          "expr": "ntfy_emails_sent_success{job=\"$job\"}",
+          "legendFormat": "Success",
+          "range": true,
+          "refId": "A"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "builder",
+          "expr": "ntfy_emails_sent_failure{job=\"$job\"}",
+          "hide": false,
+          "legendFormat": "Failure",
+          "range": true,
+          "refId": "B"
+        }
+      ],
+      "title": "Emails sent",
+      "type": "timeseries"
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "PBFA97CFB590B2093"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisCenteredZero": false,
+            "axisColorMode": "text",
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green"
+              },
+              {
+                "color": "red",
+                "value": 80
+              }
+            ]
+          }
+        },
+        "overrides": [
+          {
+            "matcher": {
+              "id": "byName",
+              "options": "Failure"
+            },
+            "properties": [
+              {
+                "id": "color",
+                "value": {
+                  "fixedColor": "red",
+                  "mode": "fixed"
+                }
+              }
+            ]
+          }
+        ]
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 6,
+        "x": 6,
+        "y": 26
+      },
+      "id": 22,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom",
+          "showLegend": true
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "builder",
+          "expr": "ntfy_emails_received_success{job=\"$job\"}",
+          "legendFormat": "Success",
+          "range": true,
+          "refId": "A"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "builder",
+          "expr": "ntfy_emails_received_failure{job=\"$job\"}",
+          "hide": false,
+          "legendFormat": "Failure",
+          "range": true,
+          "refId": "B"
+        }
+      ],
+      "title": "Emails received",
+      "type": "timeseries"
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "PBFA97CFB590B2093"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisCenteredZero": false,
+            "axisColorMode": "text",
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green"
+              },
+              {
+                "color": "red",
+                "value": 80
+              }
+            ]
+          },
+          "unit": "ms"
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 6,
+        "x": 12,
+        "y": 26
+      },
+      "id": 29,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom",
+          "showLegend": true
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "builder",
+          "expr": "ntfy_message_publish_duration_ms{job=\"$job\"}",
+          "legendFormat": "Duration",
+          "range": true,
+          "refId": "A"
+        }
+      ],
+      "title": "Message publish duration",
+      "type": "timeseries"
+    },
+    {
+      "collapsed": false,
+      "gridPos": {
+        "h": 1,
+        "w": 24,
+        "x": 0,
+        "y": 33
+      },
+      "id": 8,
+      "panels": [],
+      "title": "Internals",
+      "type": "row"
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "PBFA97CFB590B2093"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisCenteredZero": false,
+            "axisColorMode": "text",
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green"
+              },
+              {
+                "color": "red",
+                "value": 80
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 6,
+        "x": 0,
+        "y": 34
+      },
+      "id": 6,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom",
+          "showLegend": false
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "builder",
+          "expr": "go_goroutines{job=\"$job\"}",
+          "legendFormat": "Go routines",
+          "range": true,
+          "refId": "A"
+        }
+      ],
+      "title": "Go routines",
+      "type": "timeseries"
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "PBFA97CFB590B2093"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisCenteredZero": false,
+            "axisColorMode": "text",
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "log": 10,
+              "type": "symlog"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green"
+              },
+              {
+                "color": "red",
+                "value": 80
+              }
+            ]
+          },
+          "unit": "none"
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 6,
+        "x": 6,
+        "y": 34
+      },
+      "id": 44,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom",
+          "showLegend": true
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "builder",
+          "expr": "process_open_fds{job=\"$job\"}",
+          "legendFormat": "Open",
+          "range": true,
+          "refId": "A"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "builder",
+          "expr": "process_max_fds{job=\"$job\"}",
+          "hide": false,
+          "legendFormat": "Max",
+          "range": true,
+          "refId": "B"
+        }
+      ],
+      "title": "File descriptors",
+      "type": "timeseries"
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "PBFA97CFB590B2093"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisCenteredZero": false,
+            "axisColorMode": "text",
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green"
+              },
+              {
+                "color": "red",
+                "value": 80
+              }
+            ]
+          },
+          "unit": "decbytes"
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 6,
+        "x": 12,
+        "y": 34
+      },
+      "id": 45,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom",
+          "showLegend": true
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "builder",
+          "expr": "process_resident_memory_bytes{job=\"$job\"}",
+          "legendFormat": "Resident memory used by ntfy (RSS)",
+          "range": true,
+          "refId": "A"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "PBFA97CFB590B2093"
+          },
+          "editorMode": "builder",
+          "expr": "process_virtual_memory_bytes{job=\"$job\"}",
+          "hide": false,
+          "legendFormat": "Virtual memory used by ntfy (VSS)",
+          "range": true,
+          "refId": "B"
+        }
+      ],
+      "title": "Resident/virtual memory",
+      "type": "timeseries"
+    }
+  ],
+  "refresh": "10s",
+  "revision": 1,
+  "schemaVersion": 38,
+  "style": "dark",
+  "tags": [],
+  "templating": {
+    "list": [
+      {
+        "current": {
+          "isNone": true,
+          "selected": false,
+          "text": "None",
+          "value": ""
+        },
+        "datasource": {
+          "type": "prometheus",
+          "uid": "PBFA97CFB590B2093"
+        },
+        "definition": "label_values(ntfy_visitors_total, job)",
+        "hide": 0,
+        "includeAll": false,
+        "label": "Job",
+        "multi": false,
+        "name": "job",
+        "options": [],
+        "query": {
+          "query": "label_values(ntfy_visitors_total, job)",
+          "refId": "StandardVariableQuery"
+        },
+        "refresh": 1,
+        "regex": "",
+        "skipUrlSync": false,
+        "sort": 0,
+        "type": "query"
+      },
+      {
+        "auto": false,
+        "auto_count": 30,
+        "auto_min": "10s",
+        "current": {
+          "selected": false,
+          "text": "30m",
+          "value": "30m"
+        },
+        "description": "Average per-second rates over values from this time span",
+        "hide": 0,
+        "label": "Rate",
+        "name": "rate",
+        "options": [
+          {
+            "selected": false,
+            "text": "1m",
+            "value": "1m"
+          },
+          {
+            "selected": false,
+            "text": "5m",
+            "value": "5m"
+          },
+          {
+            "selected": false,
+            "text": "10m",
+            "value": "10m"
+          },
+          {
+            "selected": true,
+            "text": "30m",
+            "value": "30m"
+          },
+          {
+            "selected": false,
+            "text": "1h",
+            "value": "1h"
+          }
+        ],
+        "query": "1m,5m,10m,30m,1h",
+        "queryValue": "",
+        "refresh": 2,
+        "skipUrlSync": false,
+        "type": "interval"
+      }
+    ]
+  },
+  "time": {
+    "from": "now-5m",
+    "to": "now"
+  },
+  "timepicker": {},
+  "timezone": "",
+  "title": "ntfy",
+  "uid": "TO6HgexVz",
+  "version": 2,
+  "weekStart": ""
+}
\ No newline at end of file
diff --git a/modules/nixos/monitoring/default.nix b/modules/nixos/monitoring/default.nix
index 94fff67..6f74e9f 100644
--- a/modules/nixos/monitoring/default.nix
+++ b/modules/nixos/monitoring/default.nix
@@ -60,6 +60,12 @@ in {
               jsonData.client = "standalone";
             })
           ];
+          datasources.settings.deleteDatasources = [
+            {
+              name = "PostgreSQL";
+              orgId = 1;
+            }
+          ];
 
           # https://grafana.com/docs/grafana/latest/administration/provisioning/#dashboards
           dashboards.settings.providers = [
@@ -68,6 +74,10 @@ in {
               options.path = ./dashboards/node.json;
             }
             {
+              name = "ntfy";
+              options.path = ./dashboards/ntfy.json;
+            }
+            {
               name = "endlessh";
               options.path = ./dashboards/endlessh.json;
             }
@@ -120,6 +130,43 @@ in {
           with my.configurations;
           with config.services.prometheus.exporters; [
             {
+              job_name = "promtail";
+              static_configs = [
+                {
+                  targets =
+                    mkTargets [
+                      manwe
+                      varda
+                      yavanna
+                    ]
+                    config.nixfiles.modules.promtail.port;
+                }
+              ];
+            }
+            {
+              job_name = "ntfy";
+              static_configs = [
+                {
+                  targets =
+                    mkTargets
+                    [
+                      manwe
+                    ]
+                    config.nixfiles.modules.ntfy.port;
+                }
+              ];
+            }
+            {
+              job_name = "soju";
+              static_configs = [
+                {
+                  targets = [
+                    "127.0.0.1:${toString config.nixfiles.modules.soju.prometheus.port}"
+                  ];
+                }
+              ];
+            }
+            {
               job_name = "endlessh-go";
               static_configs = [
                 {
diff --git a/modules/nixos/ntfy.nix b/modules/nixos/ntfy.nix
index 80b6247..f8510d5 100644
--- a/modules/nixos/ntfy.nix
+++ b/modules/nixos/ntfy.nix
@@ -1,6 +1,7 @@
 {
   config,
   lib,
+  this,
   ...
 }:
 with lib; let
@@ -20,6 +21,22 @@ in {
       type = with types; str;
       default = "ntfy.${config.networking.domain}";
     };
+
+    prometheus = {
+      enable = mkEnableOption "Prometheus exporter." // {default = true;};
+
+      address = mkOption {
+        description = "Address.";
+        type = with types; str;
+        default = this.wireguard.ipv4.address;
+      };
+
+      port = mkOption {
+        description = "Port.";
+        type = with types; port;
+        default = 9289;
+      };
+    };
   };
 
   config = mkIf cfg.enable {
@@ -27,9 +44,14 @@ in {
       enable = true;
       upstreams.ntfy.servers.${config.services.ntfy-sh.settings.listen-http} = {};
       virtualHosts.${cfg.domain} = {
-        locations."/" = {
-          proxyPass = "http://ntfy";
-          proxyWebsockets = true;
+        locations = {
+          "/" = {
+            proxyPass = "http://ntfy";
+            proxyWebsockets = true;
+          };
+          "/metrics".extraConfig = ''
+            deny all;
+          '';
         };
         extraConfig = nginxInternalOnly;
       };
@@ -44,6 +66,9 @@ in {
         behind-proxy = true;
         attachment-cache-dir = "/var/cache/ntfy/attachments";
         auth-file = "/var/lib/ntfy/user.db";
+        enable-metrics = cfg.prometheus.enable;
+        metrics-listen-http = with cfg.prometheus;
+          optionalString cfg.prometheus.enable "${address}:${toString port}";
       };
     };
 
diff --git a/modules/nixos/postgresql.nix b/modules/nixos/postgresql.nix
index 79515e8..c7085ce 100644
--- a/modules/nixos/postgresql.nix
+++ b/modules/nixos/postgresql.nix
@@ -70,10 +70,7 @@ in {
       };
     };
 
-    systemd.services.postgresql.postStart =
-      optionalString (cfg.extraPostStart != [])
-      concatStringsSep "\n"
-      cfg.extraPostStart;
+    systemd.services.postgresql.postStart = optionalString (cfg.extraPostStart != []) concatLines cfg.extraPostStart;
 
     environment.sessionVariables.PSQLRC = toString (pkgs.writeText "psqlrc" ''
       \set QUIET 1
diff --git a/modules/nixos/promtail.nix b/modules/nixos/promtail.nix
index a3a6fe9..d52384a 100644
--- a/modules/nixos/promtail.nix
+++ b/modules/nixos/promtail.nix
@@ -10,12 +10,16 @@ in {
   options.nixfiles.modules.promtail = {
     enable = mkEnableOption "Promtail";
 
-    loki = {
-      url = mkOption {
-        description = "Address of a listening Loki service.";
-        type = with types; str;
-        default = "https://${config.nixfiles.modules.loki.domain}";
-      };
+    port = mkOption {
+      description = "Port.";
+      type = with types; port;
+      default = 30181;
+    };
+
+    loki.url = mkOption {
+      description = "Address of a listening Loki service.";
+      type = with types; str;
+      default = "https://${config.nixfiles.modules.loki.domain}";
     };
   };
 
@@ -26,7 +30,7 @@ in {
       configuration = {
         server = rec {
           http_listen_address = this.wireguard.ipv4.address;
-          http_listen_port = 30181;
+          http_listen_port = cfg.port;
 
           grpc_listen_address = this.wireguard.ipv4.address;
           grpc_listen_port = http_listen_port + 1;
@@ -43,7 +47,11 @@ in {
           }
         ];
 
-        positions.filename = "/tmp/positions.yaml";
+        positions = {
+          filename = "/tmp/positions.yaml";
+          sync_period = "15s";
+          ignore_invalid_yaml = true;
+        };
 
         scrape_configs = [
           {
diff --git a/modules/nixos/soju.nix b/modules/nixos/soju.nix
index 14faf00..3cfe015 100644
--- a/modules/nixos/soju.nix
+++ b/modules/nixos/soju.nix
@@ -34,6 +34,16 @@ in {
       type = with types; str;
       default = config.networking.fqdn;
     };
+
+    prometheus = {
+      enable = mkEnableOption "Prometheus exporter." // {default = true;};
+
+      port = mkOption {
+        description = "Port.";
+        type = with types; port;
+        default = 9259;
+      };
+    };
   };
 
   config = let
@@ -68,6 +78,11 @@ in {
             # https://soju.im/doc/soju.1.html
             configFile = pkgs.writeText "soju.conf" ''
               listen ${cfg.protocol}://${cfg.address}:${toString cfg.port}
+              ${
+                with cfg.prometheus;
+                  optionalString enable
+                  "listen http+prometheus://localhost:${toString port}"
+              }
               db postgres ${
                 concatStringsSep " " [
                   "host=/run/postgresql"

Consider giving Nix/NixOS a try! <3