feat: Add estimated time remaining display and duration formatting to job progress cards

This commit is contained in:
JB
2025-12-01 05:26:50 -08:00
parent c7dd8cf13a
commit b3aaa94831
4 changed files with 52 additions and 10 deletions
+15
View File
@@ -35,6 +35,21 @@ def datetimeformat(value: float, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
from datetime import datetime from datetime import datetime
return datetime.fromtimestamp(value).strftime(fmt) return datetime.fromtimestamp(value).strftime(fmt)
@main_bp.app_template_filter("durationformat")
def durationformat(value: Optional[float]) -> str:
if value is None:
return ""
seconds = int(value)
if seconds < 60:
return f"{seconds}s"
minutes = seconds // 60
seconds = seconds % 60
if minutes < 60:
return f"{minutes}m {seconds}s"
hours = minutes // 60
minutes = minutes % 60
return f"{hours}h {minutes}m"
@main_bp.route("/") @main_bp.route("/")
def index(): def index():
pending_id = request.args.get("pending_id") pending_id = request.args.get("pending_id")
+20 -9
View File
@@ -148,13 +148,24 @@ class Job:
generate_epub3: bool = False generate_epub3: bool = False
speaker_analysis: Dict[str, Any] = field(default_factory=dict) speaker_analysis: Dict[str, Any] = field(default_factory=dict)
speaker_analysis_threshold: int = 3 speaker_analysis_threshold: int = 3
analysis_requested: bool = False
speaker_voice_languages: List[str] = field(default_factory=list) @property
applied_speaker_config: Optional[str] = None def estimated_time_remaining(self) -> Optional[float]:
entity_summary: Dict[str, Any] = field(default_factory=dict) """
manual_overrides: List[Dict[str, Any]] = field(default_factory=list) Returns the estimated seconds remaining based on current progress and elapsed time.
pronunciation_overrides: List[Dict[str, Any]] = field(default_factory=list) Returns None if the job hasn't started, is finished, or progress is 0.
normalization_overrides: Dict[str, Any] = field(default_factory=dict) """
if self.status != JobStatus.RUNNING or not self.started_at or self.progress <= 0:
return None
elapsed = time.time() - self.started_at
if elapsed <= 0:
return None
# Estimate total time based on current progress
total_estimated = elapsed / self.progress
remaining = total_estimated - elapsed
return max(0.0, remaining)
def add_log(self, message: str, level: str = "info") -> None: def add_log(self, message: str, level: str = "info") -> None:
entry = JobLog(timestamp=time.time(), message=message, level=level) entry = JobLog(timestamp=time.time(), message=message, level=level)
@@ -1589,8 +1600,8 @@ def default_storage_root() -> Path:
def build_service( def build_service(
runner: Callable[[Job], None], runner: Callable[[Job], None],
*, *,
output_root: Optional[Path] = None, output_root: Optional<Path] = None,
uploads_root: Optional[Path] = None, uploads_root: Optional<Path] = None,
) -> ConversionService: ) -> ConversionService:
output_root = output_root or default_storage_root() output_root = output_root or default_storage_root()
service = ConversionService( service = ConversionService(
+11
View File
@@ -1910,6 +1910,17 @@ button.step-indicator__item:focus-visible {
color: var(--muted); color: var(--muted);
} }
.job-card__progress-meta {
display: flex;
justify-content: space-between;
align-items: center;
}
.job-card__eta {
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.job-card__footer { .job-card__footer {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
+5
View File
@@ -26,7 +26,12 @@
<div class="progress-bar" style="--progress: {{ progress_value }}%"> <div class="progress-bar" style="--progress: {{ progress_value }}%">
<div class="progress-bar__fill"></div> <div class="progress-bar__fill"></div>
</div> </div>
<div class="job-card__progress-meta">
<small>{{ progress_value }}% · {{ job.processed_characters }} / {{ job.total_characters or '—' }}</small> <small>{{ progress_value }}% · {{ job.processed_characters }} / {{ job.total_characters or '—' }}</small>
{% if job.estimated_time_remaining %}
<small class="job-card__eta">~{{ job.estimated_time_remaining | durationformat }} remaining</small>
{% endif %}
</div>
</div> </div>
<div class="job-card__footer"> <div class="job-card__footer">
<a class="button button--ghost" href="{{ url_for('jobs.job_detail', job_id=job.id) }}">Details</a> <a class="button button--ghost" href="{{ url_for('jobs.job_detail', job_id=job.id) }}">Details</a>