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