main
1{
2 lib,
3 stdenv,
4 globals,
5}:
6let
7 # Service category definitions with metadata
8 serviceCategories = {
9 media = {
10 title = "Media Services";
11 services = [
12 "jellyfin"
13 "jellyseerr"
14 "immich"
15 ];
16 descriptions = {
17 jellyfin = "Media server for movies, TV shows, and music";
18 jellyseerr = "Request management for media";
19 immich = "Photo and video backup";
20 };
21 };
22
23 downloads = {
24 title = "Download & Management";
25 services = [
26 "transmission"
27 "sonarr"
28 "radarr"
29 "lidarr"
30 "bazarr"
31 ];
32 descriptions = {
33 transmission = "BitTorrent client";
34 sonarr = "TV show management";
35 radarr = "Movie management";
36 lidarr = "Music management";
37 bazarr = "Subtitle management";
38 };
39 };
40
41 utilities = {
42 title = "Utilities";
43 services = [
44 "kiwix"
45 "n8n"
46 "paperless"
47 "grafana"
48 ];
49 descriptions = {
50 kiwix = "Offline Wikipedia and content";
51 n8n = "Workflow automation";
52 paperless = "Document management";
53 grafana = "Monitoring and metrics";
54 };
55 };
56 };
57
58 # Extract syncthing machines from globals
59 syncthingMachines = lib.filterAttrs (
60 _name: machine: machine ? syncthing && machine.syncthing ? folders
61 ) globals.machines;
62
63 # Generate syncthing service entries
64 syncthingServices = [
65 {
66 name = "Syncthing (Overview)";
67 url = "https://syncthing.sbr.pm";
68 description = "File synchronization overview";
69 }
70 ]
71 ++ (lib.mapAttrsToList (name: _machine: {
72 name = "Syncthing (${name})";
73 url = "https://syncthing.sbr.pm/${name}";
74 description = "Syncthing on ${name}";
75 }) syncthingMachines);
76
77 # Generate service entries from service list
78 mkServiceEntry =
79 category: serviceName:
80 let
81 # Capitalize first letter
82 capitalize = str: (lib.toUpper (lib.substring 0 1 str)) + (lib.substring 1 (-1) str);
83 displayName = capitalize serviceName;
84 description =
85 category.descriptions.${serviceName} or "Service on ${globals.services.${serviceName}.host}";
86 in
87 {
88 name = displayName;
89 url = "https://${serviceName}.sbr.pm";
90 inherit description;
91 };
92
93 # Generate all service entries for a category
94 mkCategoryServices =
95 category: map (serviceName: mkServiceEntry category serviceName) category.services;
96
97 # Generate service list HTML
98 mkServiceList =
99 services:
100 lib.concatMapStringsSep "\n" (service: ''
101 <div class="service-card">
102 <h3><a href="${service.url}">${service.name}</a></h3>
103 <p>${service.description}</p>
104 </div>
105 '') services;
106
107 # Generate category section HTML
108 mkCategorySection =
109 categoryId: category:
110 let
111 services = if categoryId == "sync" then syncthingServices else mkCategoryServices category;
112 in
113 ''
114 <section class="category">
115 <h2>${category.title}</h2>
116 <div class="service-grid">
117 ${mkServiceList services}
118 </div>
119 </section>
120 '';
121
122 # Generate the complete HTML page
123 htmlContent = ''
124 <!DOCTYPE html>
125 <html lang="en">
126 <head>
127 <meta charset="UTF-8">
128 <meta name="viewport" content="width=device-width, initial-scale=1.0">
129 <title>Services Dashboard</title>
130 <style>
131 * {
132 margin: 0;
133 padding: 0;
134 box-sizing: border-box;
135 }
136
137 body {
138 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
139 line-height: 1.6;
140 color: #333;
141 background: #f5f5f5;
142 padding: 2rem;
143 }
144
145 .container {
146 max-width: 1200px;
147 margin: 0 auto;
148 background: white;
149 padding: 2rem;
150 border-radius: 8px;
151 box-shadow: 0 2px 8px rgba(0,0,0,0.1);
152 }
153
154 h1 {
155 color: #2c3e50;
156 margin-bottom: 2rem;
157 padding-bottom: 1rem;
158 border-bottom: 2px solid #3498db;
159 }
160
161 .category {
162 margin-bottom: 3rem;
163 }
164
165 h2 {
166 color: #34495e;
167 margin-bottom: 1rem;
168 font-size: 1.5rem;
169 }
170
171 .service-grid {
172 display: grid;
173 grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
174 gap: 1rem;
175 }
176
177 .service-card {
178 background: #f8f9fa;
179 padding: 1.5rem;
180 border-radius: 6px;
181 border-left: 4px solid #3498db;
182 transition: transform 0.2s, box-shadow 0.2s;
183 }
184
185 .service-card:hover {
186 transform: translateY(-2px);
187 box-shadow: 0 4px 12px rgba(0,0,0,0.1);
188 }
189
190 .service-card h3 {
191 font-size: 1.1rem;
192 margin-bottom: 0.5rem;
193 }
194
195 .service-card a {
196 color: #2980b9;
197 text-decoration: none;
198 font-weight: 500;
199 }
200
201 .service-card a:hover {
202 color: #3498db;
203 text-decoration: underline;
204 }
205
206 .service-card p {
207 color: #7f8c8d;
208 font-size: 0.9rem;
209 }
210
211 @media (max-width: 768px) {
212 body {
213 padding: 1rem;
214 }
215
216 .container {
217 padding: 1rem;
218 }
219
220 .service-grid {
221 grid-template-columns: 1fr;
222 }
223 }
224 </style>
225 </head>
226 <body>
227 <div class="container">
228 <h1>Services Dashboard</h1>
229
230 ${mkCategorySection "media" serviceCategories.media}
231 ${mkCategorySection "downloads" serviceCategories.downloads}
232 ${mkCategorySection "sync" {
233 title = "File Synchronization";
234 }}
235 ${mkCategorySection "utilities" serviceCategories.utilities}
236 </div>
237 </body>
238 </html>
239 '';
240in
241stdenv.mkDerivation {
242 pname = "homepage";
243 version = "1.0.0";
244
245 dontUnpack = true;
246 dontBuild = true;
247
248 installPhase = ''
249 runHook preInstall
250
251 mkdir -p $out
252 cat > $out/index.html << 'EOF'
253 ${htmlContent}
254 EOF
255
256 runHook postInstall
257 '';
258
259 meta = {
260 description = "Simple HTML homepage listing all services from globals.nix";
261 license = lib.licenses.mit;
262 platforms = lib.platforms.all;
263 };
264}