145 lines
6.6 KiB
HTML
145 lines
6.6 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Job {{ job.job_id }} - Feedmill Recounter{% endblock %}
|
|
{% block content %}
|
|
|
|
<div class="status-page">
|
|
<div class="status-header">
|
|
<div class="status-header-top">
|
|
<span class="status-badge badge badge-{{ 'info' if job.status.name == 'RUNNING' else 'warning' if job.status.name == 'PENDING' else 'success' if job.status.name == 'COMPLETED' else 'error' if job.status.name == 'FAILED' else 'neutral' }}" id="status-badge">
|
|
{{ job.status.name }}
|
|
</span>
|
|
<button class="btn btn-danger btn-sm" id="cancel-btn" style="display:{{ 'inline-flex' if job.status.name in ('PENDING', 'RUNNING') else 'none' }}" aria-label="Abort this job">Abort</button>
|
|
<button class="btn btn-ghost btn-sm" id="delete-btn" style="display:{{ 'inline-flex' if job.status.name in ('COMPLETED', 'FAILED', 'CANCELLED') else 'none' }}" aria-label="Delete this job">Delete</button>
|
|
<time class="status-time" id="created-time" datetime="{{ job.created_at }}">{{ job.created_at | int }}</time>
|
|
</div>
|
|
<h2 class="job-id" id="job-title">Job {{ job.job_id[:8] }}...</h2>
|
|
</div>
|
|
|
|
<div id="status-announcer" class="sr-only" aria-live="assertive" aria-atomic="true"></div>
|
|
|
|
<div class="progress-section card" id="progress-section" aria-live="polite" aria-busy="false">
|
|
<div class="progress-header">
|
|
<span class="progress-label">Processing Progress</span>
|
|
<span class="progress-pct" id="progress-pct">{{ "%.0f"|format(job.progress * 100) }}%</span>
|
|
</div>
|
|
<div class="progress-track" role="progressbar" aria-valuenow="{{ "%.0f"|format(job.progress * 100) }}" aria-valuemin="0" aria-valuemax="100" aria-label="Processing progress">
|
|
<div class="progress-fill" id="progress-fill" style="width: {{ "%.0f"|format(job.progress * 100) }}%"></div>
|
|
</div>
|
|
{% if job.current_model %}
|
|
<div class="current-model" id="current-model">
|
|
<span class="model-label">Current model:</span>
|
|
<span class="model-name" id="model-name">{{ job.current_model }}</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="live-preview-section card" id="live-preview-section" style="display: block">
|
|
<div class="live-preview-header">
|
|
<h3 class="section-title">Live Preview</h3>
|
|
<label class="toggle-label" for="preview-toggle">
|
|
<input type="checkbox" id="preview-toggle" checked>
|
|
<span class="toggle-switch"></span>
|
|
<span class="toggle-text" id="toggle-text">ON</span>
|
|
</label>
|
|
<span class="live-fps" id="live-fps">-- FPS</span>
|
|
</div>
|
|
<div class="live-preview-wrap">
|
|
<img id="live-preview-img" class="live-preview-img" src="/api/preview/{{ job.job_id }}" alt="Annotated frame from video processing" />
|
|
<div class="preview-placeholder" id="preview-placeholder">Waiting for first frame...</div>
|
|
</div>
|
|
<div class="live-counts" id="live-counts" style="display:none">
|
|
<div class="live-count-item">
|
|
<span class="live-count-label">Sack In</span>
|
|
<span class="live-count-value stat-loading" id="live-loading">0</span>
|
|
</div>
|
|
<div class="live-count-item">
|
|
<span class="live-count-label">Sack Out</span>
|
|
<span class="live-count-value stat-unloading" id="live-unloading">0</span>
|
|
</div>
|
|
<div class="live-count-item">
|
|
<span class="live-count-label">Net</span>
|
|
<span class="live-count-value stat-net" id="live-net">0</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="video-thumb-section card" id="video-thumb-section">
|
|
<h3 class="section-title">Source Video</h3>
|
|
<div class="video-thumb-wrap">
|
|
<video id="video-thumb" class="video-thumb" muted preload="metadata" aria-label="Source video preview"></video>
|
|
<div class="video-fallback" id="video-fallback">
|
|
<span class="video-fallback-icon">🎬</span>
|
|
<span>Video preview unavailable</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="results-section" id="results-section" style="display: {{ 'block' if job.results else 'none' }}">
|
|
<h3 class="section-title">Results</h3>
|
|
<div class="results-grid" id="results-grid">
|
|
{% for r in job.results %}
|
|
<div class="result-card card">
|
|
<div class="result-model">{{ r.model_name }}</div>
|
|
<div class="result-stats">
|
|
<div class="stat">
|
|
<span class="stat-value stat-loading">{{ r.loading_count }}</span>
|
|
<span class="stat-label">Sack In</span>
|
|
</div>
|
|
<div class="stat">
|
|
<span class="stat-value stat-unloading">{{ r.unloading_count }}</span>
|
|
<span class="stat-label">Sack Out</span>
|
|
</div>
|
|
<div class="stat">
|
|
<span class="stat-value stat-loading">{{ r.box_loading_count }}</span>
|
|
<span class="stat-label">Box In</span>
|
|
</div>
|
|
<div class="stat">
|
|
<span class="stat-value stat-unloading">{{ r.box_unloading_count }}</span>
|
|
<span class="stat-label">Box Out</span>
|
|
</div>
|
|
<div class="stat">
|
|
<span class="stat-value stat-net">{{ r.net_count }}</span>
|
|
<span class="stat-label">Sack Net</span>
|
|
</div>
|
|
</div>
|
|
<div class="result-meta">
|
|
<span class="meta-item"><strong>Batches:</strong> {{ r.batch_count }}</span>
|
|
<span class="meta-item"><strong>Frames:</strong> {{ r.frame_count }}</span>
|
|
<span class="meta-item"><strong>Duration:</strong> {{ "%.1f"|format(r.duration_seconds) }}s</span>
|
|
</div>
|
|
{% if r.output_path %}
|
|
<a href="/download/{{ job.job_id }}/{{ r.output_path | basename }}" class="btn btn-primary result-download" aria-label="Download result for {{ r.model_name }}">Download</a>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="error-alert card" id="error-alert" style="display: {{ 'block' if job.error else 'none' }}">
|
|
<span class="error-icon">⚠</span>
|
|
<div class="error-content">
|
|
<strong>Job Failed</strong>
|
|
<p id="error-message">{{ job.error }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script src="{{ url_for('static', filename='app.js') }}"></script>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
var el = document.getElementById('created-time');
|
|
if (el) {
|
|
var ts = parseInt(el.getAttribute('datetime'), 10) * 1000;
|
|
var d = new Date(ts);
|
|
el.textContent = d.toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true });
|
|
}
|
|
if (typeof initStatusPage === 'function') {
|
|
initStatusPage('{{ job.job_id }}', '{{ job.status.name }}');
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|