From 7cdfb35e554890a86a3f022aefbf17f307925edc Mon Sep 17 00:00:00 2001 From: Vladimir Bryzgalov Date: Mon, 17 Aug 2026 19:31:51 +0500 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=B2=D0=BE=D0=B4=D0=BA=D0=B0=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D0=B5=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B7=D0=B1=D0=BE=D1=80=D0=B0=20=D0=B8=20=D0=B4?= =?UTF-8?q?=D0=BB=D0=B8=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B2=20=D0=B8=D1=81=D1=82=D0=BE=D1=80=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit При выносе общих стилей правила для сводки потерялись: подпись слипалась со значением, и строки шли столбиком вместо ряда. Длительность у задач, обработанных до 0.21.0, оставалась нулевой. Значение есть в сохранённом результате, поэтому история чинится при открытии базы. Co-Authored-By: Claude Opus 5 (1M context) --- app/store.py | 20 ++++++++++++++++++++ app/view.py | 7 ++++++- tests/test_api.py | 31 +++++++++++++++++++++++++++++++ tests/test_view.py | 11 +++++++++++ 4 files changed, 68 insertions(+), 1 deletion(-) diff --git a/app/store.py b/app/store.py index cb99066..7c6c8b2 100644 --- a/app/store.py +++ b/app/store.py @@ -50,6 +50,26 @@ class JobStore: self._conn.executescript(_SCHEMA) self._conn.commit() self._recover_stuck() + self._backfill_duration() + + def _backfill_duration(self) -> None: + """Проставляет длительность задачам, обработанным до её сохранения. + + Раньше поле оставалось нулевым: при загрузке файл ещё не декодирован, + а после обработки никто не возвращал значение обратно. В сохранённом + результате оно есть, поэтому историю можно починить. + """ + with self._lock: + rows = self._conn.execute( + "SELECT id, result FROM jobs " + "WHERE duration_sec <= 0 AND result IS NOT NULL").fetchall() + for row in rows: + try: + value = float(json.loads(row["result"]).get("duration_sec") or 0) + except (ValueError, TypeError, json.JSONDecodeError): + continue + if value > 0: + self._update(row["id"], duration_sec=value) def _recover_stuck(self) -> None: """После аварийного перезапуска задачи «в работе» никто не доделает.""" diff --git a/app/view.py b/app/view.py index 4420883..fa486c9 100644 --- a/app/view.py +++ b/app/view.py @@ -51,7 +51,12 @@ _CSS = """ :root { --bg:#fbfaf7; --fg:#22201d; --dim:#8a8580; --line:#e6e2db; h1 { font-size:19px; margin:0 0 14px; font-weight:600; } h2 { font-size:15px; margin:26px 0 8px; font-weight:600; } .hint { color:var(--dim); font-size:13px; margin:6px 0 14px; } - .bad { color:var(--warn); }""" + .bad { color:var(--warn); } + .stats { display:flex; flex-wrap:wrap; gap:6px 22px; padding:12px 0; + border-top:1px solid var(--line); border-bottom:1px solid var(--line); + margin-bottom:18px; } + .stat .k { color:var(--dim); font-size:13px; margin-right:6px; } + .stat .v { font-variant-numeric:tabular-nums; }""" STATUS_RU = {"done": "готово", "failed": "ошибка", "running": "в работе", "queued": "в очереди"} diff --git a/tests/test_api.py b/tests/test_api.py index 5f192ca..d814a0b 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -395,3 +395,34 @@ class TestRetention: job_id = store.create("call.mp3", 0.0) store.mark_done(job_id, {"duration_sec": 903.0, "turns": []}) assert store.recent()[0]["duration_sec"] == 903.0 + + +class TestDurationBackfill: + """История, записанная до сохранения длительности, показывала 00:00.""" + + def test_old_rows_get_duration_from_result(self, tmp_path): + import json as _json + import sqlite3 + + from app.store import JobStore + + path = tmp_path / "jobs.db" + store = JobStore(path) + job_id = store.create("call.mp3", 0.0) + # Так выглядела запись раньше: результат есть, длительность нулевая. + with sqlite3.connect(path) as conn: + conn.execute("UPDATE jobs SET status='done', result=?, duration_sec=0 WHERE id=?", + (_json.dumps({"duration_sec": 1596.0, "turns": []}), job_id)) + assert JobStore(path).recent()[0]["duration_sec"] == 1596.0 + + def test_broken_result_does_not_break_startup(self, tmp_path): + import sqlite3 + + from app.store import JobStore + + path = tmp_path / "jobs.db" + store = JobStore(path) + job_id = store.create("call.mp3", 0.0) + with sqlite3.connect(path) as conn: + conn.execute("UPDATE jobs SET result='не json' WHERE id=?", (job_id,)) + assert JobStore(path).recent()[0]["duration_sec"] == 0.0 diff --git a/tests/test_view.py b/tests/test_view.py index 07c70c6..19cb2e6 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -146,3 +146,14 @@ class TestCopyButton: """Word берёт разметку, простые редакторы - текст.""" page = render_job(job()) assert "text/html" in page and "text/plain" in page + + +class TestStatsLayout: + """Подпись и значение слипались: правила выносили в общий блок и потеряли.""" + + def test_label_and_value_are_separated(self): + page = render_job(job()) + assert ".stat .k" in page and "margin-right" in page + + def test_stats_are_laid_out_in_a_row(self): + assert ".stats { display:flex" in render_job(job())