@jaspermayone.com's dotfiles
1# Status monitoring module - serves /status endpoints for shields.io badges
2{
3 config,
4 lib,
5 pkgs,
6 ...
7}:
8
9with lib;
10
11let
12 cfg = config.atelier.services.status;
13
14 # Script to check services and write status JSON
15 statusScript = pkgs.writeShellScript "status-check" ''
16 set -euo pipefail
17 STATUS_DIR="/var/lib/status"
18 mkdir -p "$STATUS_DIR"
19
20 # Check each configured service
21 ${concatStringsSep "\n" (
22 map (svc: ''
23 if systemctl is-active --quiet ${escapeShellArg svc}; then
24 echo "ok" > "$STATUS_DIR/${svc}"
25 else
26 rm -f "$STATUS_DIR/${svc}"
27 fi
28 '') cfg.services
29 )}
30
31 # Always write host status (if this runs, host is up)
32 echo "ok" > "$STATUS_DIR/${cfg.hostname}"
33
34 # Check remote hosts via ping (Tailscale)
35 ${concatStringsSep "\n" (
36 map (host: ''
37 if ${pkgs.iputils}/bin/ping -c 1 -W 2 ${escapeShellArg host} >/dev/null 2>&1; then
38 echo "ok" > "$STATUS_DIR/${host}"
39 else
40 rm -f "$STATUS_DIR/${host}"
41 fi
42 '') cfg.remoteHosts
43 )}
44
45 # Build services JSON
46 SERVICES_JSON="{"
47 ${concatStringsSep "\n" (
48 imap0 (i: svc: ''
49 if systemctl is-active --quiet ${escapeShellArg svc}; then
50 SERVICES_JSON="$SERVICES_JSON${if i > 0 then "," else ""}\"${svc}\":true"
51 else
52 SERVICES_JSON="$SERVICES_JSON${if i > 0 then "," else ""}\"${svc}\":false"
53 fi
54 '') cfg.services
55 )}
56 SERVICES_JSON="$SERVICES_JSON}"
57
58 # Write full status JSON
59 cat > "$STATUS_DIR/status.json" << EOF
60 {
61 "hostname": "${cfg.hostname}",
62 "timestamp": "$(date -Iseconds)",
63 "services": $SERVICES_JSON
64 }
65 EOF
66 '';
67in
68{
69 options.atelier.services.status = {
70 enable = mkEnableOption "status monitoring endpoints";
71
72 hostname = mkOption {
73 type = types.str;
74 description = "Hostname for this machine's status endpoint";
75 };
76
77 domain = mkOption {
78 type = types.str;
79 description = "Domain to serve status on";
80 };
81
82 services = mkOption {
83 type = types.listOf types.str;
84 default = [ ];
85 description = "List of systemd services to monitor";
86 };
87
88 remoteHosts = mkOption {
89 type = types.listOf types.str;
90 default = [ ];
91 description = "List of remote hosts to check via ping (e.g. Tailscale hosts)";
92 };
93
94 cloudflareCredentialsFile = mkOption {
95 type = types.nullOr types.path;
96 default = null;
97 description = "Path to Cloudflare credentials file for DNS challenge";
98 };
99 };
100
101 config = mkIf cfg.enable {
102 # Timer to update status every minute
103 systemd.services.status-check = {
104 description = "Update status endpoints";
105 serviceConfig = {
106 Type = "oneshot";
107 ExecStart = statusScript;
108 };
109 };
110
111 systemd.timers.status-check = {
112 description = "Run status check every minute";
113 wantedBy = [ "timers.target" ];
114 timerConfig = {
115 OnBootSec = "30s";
116 OnUnitActiveSec = "1min";
117 };
118 };
119
120 # Ensure status directory exists
121 systemd.tmpfiles.rules = [
122 "d /var/lib/status 0755 root root -"
123 ];
124
125 # Caddy virtual host for status
126 services.caddy.virtualHosts."${cfg.domain}".extraConfig = ''
127 ${optionalString (cfg.cloudflareCredentialsFile != null) ''
128 tls {
129 dns cloudflare {env.CLOUDFLARE_API_TOKEN}
130 }
131 ''}
132
133 # Individual host status (returns 200 if file exists)
134 @status_host path /status/${cfg.hostname}
135 handle @status_host {
136 @online file /var/lib/status/${cfg.hostname}
137 handle @online {
138 respond "ok" 200
139 }
140 handle {
141 respond "offline" 503
142 }
143 }
144
145 # Service status endpoints
146 ${concatStringsSep "\n" (
147 map (svc: ''
148 @status_${svc} path /status/service/${svc}
149 handle @status_${svc} {
150 @online_${svc} file /var/lib/status/${svc}
151 handle @online_${svc} {
152 respond "ok" 200
153 }
154 handle {
155 respond "offline" 503
156 }
157 }
158 '') cfg.services
159 )}
160
161 # Remote host status endpoints (Tailscale)
162 ${concatStringsSep "\n" (
163 map (host: ''
164 @status_${host} path /status/${host}
165 handle @status_${host} {
166 @online_${host} file /var/lib/status/${host}
167 handle @online_${host} {
168 respond "ok" 200
169 }
170 handle {
171 respond "offline" 503
172 }
173 }
174 '') cfg.remoteHosts
175 )}
176
177 # Full status JSON
178 @status_json path /status
179 handle @status_json {
180 root * /var/lib/status
181 rewrite * /status.json
182 file_server
183 header Content-Type application/json
184 }
185
186 # Root redirect to status
187 handle {
188 respond "alastor.hogwarts.channel - see /status" 200
189 }
190 '';
191 };
192}