11<?php
2- $ avgDur = '-- ' ;
3- if ($ avgSec > 0 ) {
4- if ($ avgSec < 60 ) $ avgDur = $ avgSec . 's ' ;
5- elseif ($ avgSec < 3600 ) $ avgDur = round ($ avgSec / 60 ) . 'm ' ;
6- else $ avgDur = round ($ avgSec / 3600 , 1 ) . 'h ' ;
2+ function formatDurationLabel (int $ s ): string {
3+ return $ s >= 3600 ? floor ($ s / 3600 ) . 'h ' . floor (($ s % 3600 ) / 60 ) . 'm ' . ($ s % 60 ) . 's ' : ($ s >= 60 ? floor ($ s / 60 ) . 'm ' . ($ s % 60 ) . 's ' : $ s . 's ' );
4+ }
5+
6+ $ avgDur = $ avgSec > 0 ? formatDurationLabel ((int ) $ avgSec ) : '-- ' ;
7+ $ maxCompletedDuration = max (array_map (fn ($ j ) => (int ) ($ j ['duration_seconds ' ] ?? 0 ), $ completed ?: [['duration_seconds ' => 0 ]]));
8+
9+ function durationBarHtml (int $ duration , int $ maxDuration ): string {
10+ $ pct = $ maxDuration > 0 ? min (100 , round (($ duration / $ maxDuration ) * 100 )) : 0 ;
11+ $ hue = 120 - round ($ pct * 1.2 );
12+ $ label = formatDurationLabel ($ duration );
13+ return '<div class="progress position-relative" style="height:20px;min-width:120px;" title=" ' . htmlspecialchars ($ label ) . '"> '
14+ . '<div class="progress-bar" style="width: ' . $ pct . '%;background-color:hsl( ' . $ hue . ',70%,45%);"></div> '
15+ . '<span class="position-absolute top-50 start-50 translate-middle small fw-semibold px-2 rounded bg-body text-body border text-nowrap"> ' . htmlspecialchars ($ label ) . '</span> '
16+ . '</div> ' ;
717 }
818
919 // Job type icons mapping
@@ -196,7 +206,7 @@ function jobTypeIcon(string $type): string {
196206 <td class="d-none d-md-table-cell">
197207 <?php
198208 $ d = $ job ['duration_seconds ' ] ?? 0 ;
199- echo $ d >= 60 ? floor ( $ d / 60 ) . ' m ' . ( $ d % 60 ) . ' s ' : $ d . ' s ' ;
209+ echo durationBarHtml (( int ) $ d , $ maxCompletedDuration ) ;
200210 ?>
201211 </td>
202212 <td class="text-center">
@@ -253,7 +263,18 @@ function formatDate(d) {
253263
254264 function formatDuration(s) {
255265 s = parseInt(s) || 0;
256- return s >= 60 ? Math.floor(s / 60) + 'm ' + (s % 60) + 's' : s + 's';
266+ return s >= 3600 ? Math.floor(s / 3600) + 'h ' + Math.floor((s % 3600) / 60) + 'm ' + (s % 60) + 's' : (s >= 60 ? Math.floor(s / 60) + 'm ' + (s % 60) + 's' : s + 's');
267+ }
268+
269+ function durationBar(s, maxS) {
270+ s = parseInt(s) || 0;
271+ maxS = parseInt(maxS) || 0;
272+ const pct = maxS > 0 ? Math.min(100, Math.round((s / maxS) * 100)) : 0;
273+ const hue = 120 - Math.round(pct * 1.2);
274+ const label = formatDuration(s);
275+ return '<div class="progress position-relative" style="height:20px;min-width:120px;" title="' + esc(label) + '">' +
276+ '<div class="progress-bar" style="width:' + pct + '%;background-color:hsl(' + hue + ',70%,45%);"></div>' +
277+ '<span class="position-absolute top-50 start-50 translate-middle small fw-semibold px-2 rounded bg-body text-body border text-nowrap">' + esc(label) + '</span></div>';
257278 }
258279
259280 function statusBadge(status) {
@@ -313,7 +334,7 @@ function buildInProgressRow(job) {
313334 '<td class="text-end" onclick="event.stopPropagation()">' + actions + '</td></tr>';
314335 }
315336
316- function buildCompletedRow(job) {
337+ function buildCompletedRow(job, maxDuration ) {
317338 let statusHtml;
318339 if (job.status === 'completed' && job.had_warnings) {
319340 statusHtml = '<i class="bi bi-exclamation-triangle-fill text-warning" data-bs-toggle="tooltip" title="' + esc('Completed with warnings: ' + String(job.error_log || '').substring(0, 200)) + '"></i>';
@@ -339,7 +360,7 @@ function buildCompletedRow(job) {
339360 '<td class="text-nowrap">' + jobTypeIcon(job.task_type) + esc(job.task_type) + '</td>' +
340361 '<td class="d-none d-md-table-cell">' + Number(job.files_total || 0).toLocaleString() + '</td>' +
341362 '<td class="d-none d-md-table-cell">' + esc(job.repo_name || '--') + '</td>' +
342- '<td class="d-none d-md-table-cell">' + formatDuration (job.duration_seconds) + '</td>' +
363+ '<td class="d-none d-md-table-cell">' + durationBar (job.duration_seconds, maxDuration ) + '</td>' +
343364 '<td class="text-center">' + statusHtml + '</td>' +
344365 '<td class="text-end" onclick="event.stopPropagation()">' + actions + '</td></tr>';
345366 }
@@ -369,7 +390,8 @@ function refreshQueue() {
369390 let html = '<div class="table-responsive"><table class="table table-hover align-middle mb-0"><thead class="table-light"><tr>' +
370391 '<th>Date</th><th>Client</th><th>Task</th><th class="d-none d-md-table-cell">Files</th><th class="d-none d-md-table-cell">Repo</th><th class="d-none d-md-table-cell">Duration</th><th class="text-center">Status</th><th style="width:80px;"></th>' +
371392 '</tr></thead><tbody>';
372- data.completed.forEach(j => html += buildCompletedRow(j));
393+ const maxDuration = Math.max(0, ...data.completed.map(j => parseInt(j.duration_seconds) || 0));
394+ data.completed.forEach(j => html += buildCompletedRow(j, maxDuration));
373395 html += '</tbody></table></div>';
374396 cCard.innerHTML = html;
375397 }
0 commit comments