Skip to content

Commit 5f70511

Browse files
committed
Enh(viewer): Improvements to batch monitor
1 parent 53b5a65 commit 5f70511

3 files changed

Lines changed: 171 additions & 8 deletions

File tree

.cursor/rules/batch_monitor.mdc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ There are the following statuses:
3333
- SUBMITTED (grey)
3434
- PENDING (grey)
3535
- RUNNABLE (grey)
36-
- STARTING (yellow)
37-
- RUNNING (faded green)
36+
- STARTING (cyan)
37+
- RUNNING (yellow)
3838
- SUCCEEDED (green)
3939
- FAILED (red)
4040

codeclash/viewer/app_aws.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def __init__(
3737
self.region = region
3838
self.logs_base_dir = logs_base_dir or Path("logs")
3939
self._job_id_to_folder: dict[str, str] | None = None
40+
self._job_id_to_round_info: dict[str, tuple[int, int] | None] | None = None
4041

4142
def list_jobs(self, *, limit: int | None = None) -> list[dict[str, Any]]:
4243
"""List all jobs from AWS Batch in the last 24h"""
@@ -86,6 +87,7 @@ def format_job_for_display(self, job: dict[str, Any]) -> dict[str, Any]:
8687
aws_link = self._generate_aws_console_link(job_id)
8788
emagedoc_link = self._generate_emagedoc_link(job_id)
8889
s3_link = self._generate_s3_link(job_id)
90+
round_info = self._get_round_info(job_id)
8991

9092
return {
9193
"job_id": job_id,
@@ -98,6 +100,7 @@ def format_job_for_display(self, job: dict[str, Any]) -> dict[str, Any]:
98100
"aws_link": aws_link,
99101
"emagedoc_link": emagedoc_link,
100102
"s3_link": s3_link,
103+
"round_info": round_info,
101104
}
102105

103106
def _calculate_time_running(
@@ -140,14 +143,16 @@ def _calculate_time_running(
140143
return f"{hours}h {minutes}m", duration_seconds
141144

142145
def _build_job_id_to_folder_mapping(self) -> dict[str, str]:
143-
"""Build mapping from AWS Batch job ID to log folder path"""
146+
"""Build mapping from AWS Batch job ID to log folder path and round info"""
144147
if self._job_id_to_folder is not None:
145148
return self._job_id_to_folder
146149

147150
mapping = {}
151+
round_info_mapping = {}
148152
if not self.logs_base_dir.exists():
149153
logger.warning(f"Logs directory does not exist: {self.logs_base_dir}")
150154
self._job_id_to_folder = mapping
155+
self._job_id_to_round_info = round_info_mapping
151156
return mapping
152157

153158
for metadata_file in self.logs_base_dir.rglob("metadata.json"):
@@ -157,13 +162,29 @@ def _build_job_id_to_folder_mapping(self) -> dict[str, str]:
157162
if job_id:
158163
folder_path = metadata_file.parent.relative_to(self.logs_base_dir)
159164
mapping[job_id] = str(folder_path)
160-
except (json.JSONDecodeError, OSError, KeyError) as e:
165+
166+
total_rounds = metadata.get("config", {}).get("tournament", {}).get("rounds")
167+
round_stats = metadata.get("round_stats", {})
168+
completed_rounds = sum(1 for round_key in round_stats.keys() if int(round_key) > 0)
169+
170+
if total_rounds is not None:
171+
round_info_mapping[job_id] = (completed_rounds, total_rounds)
172+
else:
173+
round_info_mapping[job_id] = None
174+
except (json.JSONDecodeError, OSError, KeyError, ValueError) as e:
161175
logger.debug(f"Failed to read metadata from {metadata_file}: {e}")
162176

163177
self._job_id_to_folder = mapping
178+
self._job_id_to_round_info = round_info_mapping
164179
logger.info(f"Built job ID mapping with {len(mapping)} entries")
165180
return mapping
166181

182+
def _get_round_info(self, job_id: str) -> tuple[int, int] | None:
183+
"""Get round info for a job"""
184+
if self._job_id_to_round_info is None:
185+
self._build_job_id_to_folder_mapping()
186+
return self._job_id_to_round_info.get(job_id) if self._job_id_to_round_info else None
187+
167188
def _generate_aws_console_link(self, job_id: str) -> str:
168189
"""Generate AWS console link for a job"""
169190
account_id = "039984708918"

codeclash/viewer/templates/batch.html

Lines changed: 146 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,13 @@
146146
}
147147

148148
.status-STARTING {
149-
background: #ffc107;
150-
color: #000;
149+
background: #17a2b8;
150+
color: #fff;
151151
}
152152

153153
.status-RUNNING {
154-
background: rgba(40, 167, 69, 0.3);
155-
color: #28a745;
154+
background: #ffc107;
155+
color: #000;
156156
}
157157

158158
.status-SUCCEEDED {
@@ -315,6 +315,94 @@
315315
font-size: 0.85rem;
316316
margin-left: auto;
317317
}
318+
319+
.status-counter {
320+
background: var(--bg-secondary);
321+
border: 1px solid var(--border-color);
322+
border-radius: 0.5rem;
323+
margin-bottom: 1.5rem;
324+
padding: 1rem;
325+
}
326+
327+
.status-counter-title {
328+
font-weight: 600;
329+
color: var(--text-primary);
330+
display: flex;
331+
align-items: center;
332+
gap: 0.5rem;
333+
margin-bottom: 1rem;
334+
}
335+
336+
.status-counter-grid {
337+
display: grid;
338+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
339+
gap: 0.75rem;
340+
}
341+
342+
.status-count-item {
343+
background: var(--bg-tertiary);
344+
padding: 0.75rem;
345+
border-radius: 0.375rem;
346+
border-left: 3px solid;
347+
display: flex;
348+
flex-direction: column;
349+
gap: 0.25rem;
350+
}
351+
352+
.status-count-item.SUBMITTED,
353+
.status-count-item.PENDING,
354+
.status-count-item.RUNNABLE {
355+
border-left-color: #6c757d;
356+
}
357+
358+
.status-count-item.STARTING {
359+
border-left-color: #17a2b8;
360+
}
361+
362+
.status-count-item.RUNNING {
363+
border-left-color: #ffc107;
364+
}
365+
366+
.status-count-item.SUCCEEDED {
367+
border-left-color: #28a745;
368+
}
369+
370+
.status-count-item.FAILED {
371+
border-left-color: #dc3545;
372+
}
373+
374+
.status-count-label {
375+
font-size: 0.75rem;
376+
text-transform: uppercase;
377+
color: var(--text-secondary);
378+
font-weight: 600;
379+
letter-spacing: 0.05em;
380+
}
381+
382+
.status-count-value {
383+
font-size: 1.5rem;
384+
font-weight: 700;
385+
color: var(--text-primary);
386+
}
387+
388+
.rounds-count {
389+
display: inline-block;
390+
background-color: var(--accent-color);
391+
color: #000000;
392+
padding: 0.25rem 0.5rem;
393+
border-radius: 0.25rem;
394+
font-size: 0.85rem;
395+
font-weight: 700;
396+
}
397+
398+
.rounds-count-warning {
399+
background-color: #ffc107;
400+
}
401+
402+
.rounds-unknown {
403+
color: var(--text-muted);
404+
font-style: italic;
405+
}
318406
</style>
319407
</head>
320408
<body>
@@ -326,6 +414,16 @@ <h1><i class="bi bi-cloud"></i> AWS Batch Job Monitor</h1>
326414
</a>
327415
</div>
328416

417+
<div class="status-counter">
418+
<div class="status-counter-title">
419+
<i class="bi bi-bar-chart"></i>
420+
<span>Job Status Summary (Last 24h)</span>
421+
</div>
422+
<div class="status-counter-grid" id="status-counter-grid">
423+
<!-- Will be populated by JavaScript -->
424+
</div>
425+
</div>
426+
329427
<div class="filter-container">
330428
<label for="status-filter">Filter by status:</label>
331429
<select id="status-filter" class="filter-select" onchange="applyStatusFilter()">
@@ -355,13 +453,47 @@ <h1><i class="bi bi-cloud"></i> AWS Batch Job Monitor</h1>
355453
let statusFilter = '';
356454
let lastRefreshTime = null;
357455

456+
function updateStatusCounter() {
457+
const statusCounts = {
458+
'SUBMITTED': 0,
459+
'PENDING': 0,
460+
'RUNNABLE': 0,
461+
'STARTING': 0,
462+
'RUNNING': 0,
463+
'SUCCEEDED': 0,
464+
'FAILED': 0
465+
};
466+
467+
allJobs.forEach(job => {
468+
if (statusCounts.hasOwnProperty(job.status)) {
469+
statusCounts[job.status]++;
470+
}
471+
});
472+
473+
const grid = document.getElementById('status-counter-grid');
474+
let html = '';
475+
476+
Object.keys(statusCounts).forEach(status => {
477+
const count = statusCounts[status];
478+
html += `
479+
<div class="status-count-item ${status}">
480+
<div class="status-count-label">${status}</div>
481+
<div class="status-count-value">${count}</div>
482+
</div>
483+
`;
484+
});
485+
486+
grid.innerHTML = html;
487+
}
488+
358489
async function fetchJobs() {
359490
try {
360491
const response = await fetch('/batch/api/jobs');
361492
const data = await response.json();
362493

363494
if (data.success) {
364495
allJobs = data.jobs;
496+
updateStatusCounter();
365497
renderJobs();
366498
clearError();
367499
lastRefreshTime = Date.now();
@@ -434,6 +566,7 @@ <h1><i class="bi bi-cloud"></i> AWS Batch Job Monitor</h1>
434566
<th class="sortable" onclick="sortBy('time_running_seconds')">
435567
Time Running <i class="bi bi-arrow-down-up sort-icon"></i>
436568
</th>
569+
<th>Rounds</th>
437570
<th class="sortable" onclick="sortBy('job_name')">
438571
Job Name <i class="bi bi-arrow-down-up sort-icon"></i>
439572
</th>
@@ -450,11 +583,20 @@ <h1><i class="bi bi-cloud"></i> AWS Batch Job Monitor</h1>
450583
const escapedJobName = job.job_name.replace(/'/g, "\\'");
451584
const escapedJobId = job.job_id.replace(/'/g, "\\'");
452585

586+
let roundsHtml = '<span class="rounds-unknown">-</span>';
587+
if (job.round_info && Array.isArray(job.round_info) && job.round_info.length === 2) {
588+
const completed = job.round_info[0];
589+
const total = job.round_info[1];
590+
const warningClass = completed < total ? ' rounds-count-warning' : '';
591+
roundsHtml = `<span class="rounds-count${warningClass}">${completed}/${total}</span>`;
592+
}
593+
453594
html += `
454595
<tr>
455596
<td>${job.created_at || '-'}</td>
456597
<td><span class="status-tag status-${job.status}">${job.status}</span></td>
457598
<td>${job.time_running || '-'}</td>
599+
<td>${roundsHtml}</td>
458600
<td>
459601
<div class="copy-cell">
460602
<span class="job-name">${job.job_name}</span>

0 commit comments

Comments
 (0)