A fork of mtelver's day10 project
1(** Package list and detail pages *)
2
3let list_page ~html_dir =
4 let packages = Day10_web_data.Package_data.list_packages ~html_dir in
5 let rows = packages |> List.map (fun (name, version) ->
6 Printf.sprintf {|
7 <tr>
8 <td><a href="/packages/%s/%s">%s</a></td>
9 <td>%s</td>
10 <td>%s</td>
11 <td><a href="/docs/p/%s/%s/doc/index.html">View Docs</a></td>
12 </tr>
13 |} name version name version (Layout.badge `Success) name version
14 ) |> String.concat "\n" in
15
16 let content = Printf.sprintf {|
17 <h1>Packages</h1>
18 <div class="card">
19 <input type="search" id="pkg-search" placeholder="Search packages..." onkeyup="filterTable()">
20 <table id="pkg-table">
21 <thead>
22 <tr>
23 <th>Package</th>
24 <th>Version</th>
25 <th>Docs Status</th>
26 <th>Links</th>
27 </tr>
28 </thead>
29 <tbody>
30 %s
31 </tbody>
32 </table>
33 </div>
34 <script>
35 function filterTable() {
36 const filter = document.getElementById('pkg-search').value.toLowerCase();
37 const rows = document.querySelectorAll('#pkg-table tbody tr');
38 rows.forEach(row => {
39 const text = row.textContent.toLowerCase();
40 row.style.display = text.includes(filter) ? '' : 'none';
41 });
42 }
43 </script>
44 |} rows
45 in
46 Layout.page ~title:"Packages" ~content
47
48let detail_page ~html_dir ~cache_dir ~platform ~log_dir ~name ~version =
49 let package = name ^ "." ^ version in
50 if not (Day10_web_data.Package_data.package_has_docs ~html_dir ~name ~version) then
51 Layout.page ~title:"Package Not Found" ~content:(Printf.sprintf {|
52 <h1>Package Not Found</h1>
53 <p class="card">No documentation found for %s</p>
54 <p><a href="/packages">← Back to packages</a></p>
55 |} package)
56 else
57 let all_versions = Day10_web_data.Package_data.list_package_versions ~html_dir ~name in
58 let versions_list = all_versions |> List.map (fun v ->
59 if v = version then
60 Printf.sprintf "<li><strong>%s</strong> (current)</li>" v
61 else
62 Printf.sprintf {|<li><a href="/packages/%s/%s">%s</a></li>|} name v v
63 ) |> String.concat "\n" in
64
65 (* Get layer info for dependencies and build timestamp *)
66 let layer_info = Day10_web_data.Layer_data.get_package_layer
67 ~cache_dir ~platform ~package in
68
69 (* Get latest run ID for log links *)
70 let latest_run = Day10_web_data.Run_data.get_latest_run_id ~log_dir in
71
72 (* Determine build status from multiple sources *)
73 let build_status, build_time = match layer_info with
74 | Some info ->
75 let timestamp = Unix.gmtime info.created in
76 let time_str = Printf.sprintf "%04d-%02d-%02d %02d:%02d:%02d UTC"
77 (timestamp.Unix.tm_year + 1900) (timestamp.Unix.tm_mon + 1)
78 timestamp.Unix.tm_mday timestamp.Unix.tm_hour
79 timestamp.Unix.tm_min timestamp.Unix.tm_sec in
80 let status = if info.exit_status = 0 then `Success else `Failed in
81 (status, Some time_str)
82 | None ->
83 (* No layer info - check logs and summary *)
84 match latest_run with
85 | Some run_id ->
86 (* Check if run is still in progress *)
87 if Day10_web_data.Run_data.is_run_in_progress ~log_dir ~run_id then
88 (* Check if we have logs for this package *)
89 if Day10_web_data.Run_data.has_build_log ~log_dir ~run_id ~package then
90 (`In_progress, None)
91 else
92 (`Pending, None)
93 else
94 (* Run finished - check summary for status *)
95 begin match Day10_web_data.Run_data.get_package_status_from_summary
96 ~log_dir ~run_id ~package with
97 | Some `Success -> (`Success, None)
98 | Some (`Failed _) -> (`Failed, None)
99 | Some `Not_in_run -> (`Unknown, None)
100 | None -> (`Unknown, None)
101 end
102 | None -> (`Unknown, None)
103 in
104
105 (* Build info section *)
106 let status_badge = match build_status with
107 | `Success -> Layout.badge `Success
108 | `Failed -> Layout.badge `Failed
109 | `In_progress ->
110 {|<span class="badge badge-warning" style="animation: pulse-glow 1s ease-in-out infinite;">building</span>|}
111 | `Pending ->
112 {|<span class="badge badge-warning">pending</span>|}
113 | `Unknown -> Layout.badge `Skipped
114 in
115
116 let build_status_content =
117 let time_line = match build_time with
118 | Some t -> Printf.sprintf {|<p><strong>Built:</strong> %s</p>|} t
119 | None -> ""
120 in
121 Printf.sprintf {|
122 <p><strong>Status:</strong> %s</p>
123 %s
124 |} status_badge time_line
125 in
126
127 (* Log links - show if logs exist *)
128 let log_links = match latest_run with
129 | Some run_id ->
130 let has_build = Day10_web_data.Run_data.has_build_log ~log_dir ~run_id ~package in
131 let has_docs = Day10_web_data.Run_data.has_doc_log ~log_dir ~run_id ~package in
132 if has_build || has_docs then
133 let build_link = if has_build then
134 Printf.sprintf {|<a href="/runs/%s/build/%s">Build Log →</a>|} run_id package
135 else
136 {|<span style="color: var(--text-dim);">No build log</span>|}
137 in
138 let doc_link = if has_docs then
139 Printf.sprintf {|<a href="/runs/%s/docs/%s">Doc Log →</a>|} run_id package
140 else
141 {|<span style="color: var(--text-dim);">No doc log</span>|}
142 in
143 Printf.sprintf {|
144 <p style="margin-top: 1rem;">
145 %s
146 <span style="margin: 0 0.5rem; color: var(--text-dim);">|</span>
147 %s
148 </p>
149 |} build_link doc_link
150 else
151 {|<p style="margin-top: 1rem; color: var(--text-dim);">No logs in latest run.</p>|}
152 | None ->
153 {|<p style="margin-top: 1rem; color: var(--text-dim);">No runs recorded yet.</p>|}
154 in
155
156 let build_info = Printf.sprintf {|
157 <div class="card">
158 <h2>Build & Logs</h2>
159 %s
160 %s
161 </div>
162 |} build_status_content log_links
163 in
164
165 (* Parse "name.version" format - version starts at first .digit or .v followed by digit *)
166 let parse_package_str s =
167 let len = String.length s in
168 let rec find_version_start i =
169 if i >= len - 1 then None
170 else if s.[i] = '.' then
171 let next = s.[i + 1] in
172 if next >= '0' && next <= '9' then Some i
173 else if next = 'v' && i + 2 < len && s.[i + 2] >= '0' && s.[i + 2] <= '9' then Some i
174 else find_version_start (i + 1)
175 else find_version_start (i + 1)
176 in
177 match find_version_start 0 with
178 | Some i -> Some (String.sub s 0 i, String.sub s (i + 1) (len - i - 1))
179 | None -> None
180 in
181
182 (* Dependencies section - always show *)
183 let deps_section =
184 let deps_content = match layer_info with
185 | Some info when info.deps <> [] ->
186 let deps_list = info.deps
187 |> List.map (fun dep ->
188 match parse_package_str dep with
189 | Some (dep_name, dep_version) ->
190 Printf.sprintf {|<li><a href="/packages/%s/%s">%s</a></li>|} dep_name dep_version dep
191 | None ->
192 Printf.sprintf "<li>%s</li>" dep)
193 |> String.concat "\n" in
194 Printf.sprintf {|<ul>%s</ul>|} deps_list
195 | Some _ ->
196 {|<p style="color: var(--text-dim);">No dependencies.</p>|}
197 | None ->
198 {|<p style="color: var(--text-dim);">Dependency information not available.</p>|}
199 in
200 let deps_count = match layer_info with
201 | Some info -> List.length info.deps
202 | None -> 0
203 in
204 Printf.sprintf {|
205 <div class="card">
206 <h2>Dependencies (%d)</h2>
207 %s
208 </div>
209 |} deps_count deps_content
210 in
211
212 (* Reverse dependencies section - always show *)
213 let reverse_deps = Day10_web_data.Layer_data.get_reverse_deps
214 ~cache_dir ~platform ~package in
215 let rev_deps_section =
216 let rev_deps_content = if reverse_deps <> [] then
217 let rev_deps_list = reverse_deps
218 |> List.map (fun dep ->
219 match parse_package_str dep with
220 | Some (dep_name, dep_version) ->
221 Printf.sprintf {|<li><a href="/packages/%s/%s">%s</a></li>|} dep_name dep_version dep
222 | None ->
223 Printf.sprintf "<li>%s</li>" dep)
224 |> String.concat "\n" in
225 Printf.sprintf {|<ul>%s</ul>|} rev_deps_list
226 else
227 {|<p style="color: var(--text-dim);">No packages depend on this one.</p>|}
228 in
229 Printf.sprintf {|
230 <div class="card">
231 <h2>Reverse Dependencies (%d)</h2>
232 %s
233 </div>
234 |} (List.length reverse_deps) rev_deps_content
235 in
236
237 let content = Printf.sprintf {|
238 <h1>%s</h1>
239 <p><a href="/packages">← Back to packages</a></p>
240
241 <div class="card">
242 <h2>Documentation</h2>
243 <p>%s</p>
244 <p><a href="/docs/p/%s/%s/doc/index.html">View Documentation →</a></p>
245 </div>
246
247 %s
248 %s
249 %s
250
251 <div class="card">
252 <h2>Other Versions</h2>
253 <ul>%s</ul>
254 </div>
255 |} package (Layout.badge `Success) name version build_info deps_section rev_deps_section versions_list
256 in
257 Layout.page ~title:package ~content
258
259(** Combined build and doc logs page for a package *)
260let logs_page ~log_dir ~name ~version =
261 let package = name ^ "." ^ version in
262 let latest_run = Day10_web_data.Run_data.get_latest_run_id ~log_dir in
263 match latest_run with
264 | None ->
265 Layout.page ~title:(package ^ " Logs") ~content:(Printf.sprintf {|
266 <h1>%s Logs</h1>
267 <p><a href="/packages/%s/%s">← Back to package</a></p>
268 <div class="card">
269 <p>No run data available.</p>
270 </div>
271 |} package name version)
272 | Some run_id ->
273 let build_log = Day10_web_data.Run_data.read_build_log ~log_dir ~run_id ~package in
274 let doc_log = Day10_web_data.Run_data.read_doc_log ~log_dir ~run_id ~package in
275
276 let build_section = match build_log with
277 | Some log ->
278 Printf.sprintf {|
279 <div class="card">
280 <h2>Build Log</h2>
281 <p><em>From run %s</em></p>
282 <pre>%s</pre>
283 </div>
284 |} run_id log
285 | None ->
286 {|<div class="card"><h2>Build Log</h2><p>No build log available for this package.</p></div>|}
287 in
288
289 let doc_section = match doc_log with
290 | Some log ->
291 Printf.sprintf {|
292 <div class="card">
293 <h2>Documentation Log</h2>
294 <p><em>From run %s</em></p>
295 <pre>%s</pre>
296 </div>
297 |} run_id log
298 | None ->
299 {|<div class="card"><h2>Documentation Log</h2><p>No doc log available for this package.</p></div>|}
300 in
301
302 let content = Printf.sprintf {|
303 <h1>%s Logs</h1>
304 <p><a href="/packages/%s/%s">← Back to package</a></p>
305 %s
306 %s
307 |} package name version build_section doc_section
308 in
309 Layout.page ~title:(package ^ " Logs") ~content