Format long WebUI durations with hours

Add hour-aware output to the shared duration formatter while preserving existing seconds and minutes formatting. Reuse it for the goal elapsed counter and cover both formats with a focused regression test.
This commit is contained in:
Alessandro 2026-08-12 00:58:51 +02:00
parent 37093f5888
commit d7130b8c67
3 changed files with 26 additions and 10 deletions

View file

@ -1,5 +1,8 @@
from __future__ import annotations
import base64
import shutil
import subprocess
import uuid
from pathlib import Path
from types import SimpleNamespace
@ -108,6 +111,23 @@ def test_goal_webui_uses_state_revisions_instead_of_polling():
assert "goalStore.refresh(true)" in refresh
@pytest.mark.skipif(not shutil.which("node"), reason="node is required")
def test_goal_webui_uses_shared_hour_aware_duration_formatter():
project_root = Path(__file__).resolve().parents[3]
time_utils = (project_root / "webui" / "js" / "time-utils.js").read_bytes()
module_url = "data:text/javascript;base64," + base64.b64encode(time_utils).decode("ascii")
script = f"""
import {{ formatDuration }} from {module_url!r};
if (formatDuration(3_782_000) !== "1h3m2s") throw new Error("hours");
if (formatDuration(62_000) !== "1m2s") throw new Error("minutes");
"""
subprocess.run(["node", "--input-type=module", "-e", script], check=True)
store = (project_root / "plugins" / "_goal" / "webui" / "goal-store.js").read_text()
assert 'import { formatDuration } from "/js/time-utils.js";' in store
assert "return formatDuration(this.elapsedSeconds * 1000);" in store
def test_goal_command_sets_pauses_resumes_and_deletes(context_id: str):
created = goal_command.run(_payload(context_id, "/goal Add current goal support"))
assert created["effects"][0]["message"] == "Goal set."

View file

@ -1,5 +1,6 @@
import { createStore } from "/js/AlpineStore.js";
import { callJsonApi } from "/js/api.js";
import { formatDuration } from "/js/time-utils.js";
import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
import {
toastFrontendError,
@ -68,13 +69,7 @@ const model = {
},
get elapsedLabel() {
const seconds = this.elapsedSeconds;
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = seconds % 60;
if (hours) return `${hours}h ${minutes}m`;
if (minutes) return `${minutes}m ${remainingSeconds}s`;
return `${remainingSeconds}s`;
return formatDuration(this.elapsedSeconds * 1000);
},
onMount() {

View file

@ -292,7 +292,7 @@ export function withUserTimeFormatOptions(options = {}) {
/**
* Format a duration in milliseconds to a human-readable string
* @param {number} durationMs - Duration in milliseconds
* @returns {string} Formatted duration (e.g., '45s', '2m30s')
* @returns {string} Formatted duration (e.g., '45s', '2m30s', '1h3m2s')
*/
export function formatDuration(durationMs) {
if (durationMs == null || durationMs < 0) return '0s';
@ -304,7 +304,8 @@ export function formatDuration(durationMs) {
return `${totalSecs}s`;
}
const mins = Math.floor(totalSecs / 60);
const hours = Math.floor(totalSecs / 3600);
const mins = Math.floor((totalSecs % 3600) / 60);
const secs = totalSecs % 60;
return `${mins}m${secs}s`;
return hours ? `${hours}h${mins}m${secs}s` : `${mins}m${secs}s`;
}