diff --git a/core/db.py b/core/db.py index dfc7047..bf1d10c 100644 --- a/core/db.py +++ b/core/db.py @@ -773,6 +773,12 @@ def _migrate(conn: sqlite3.Connection): PRAGMA foreign_keys=ON; """) + # KIN-126: Add completed_at to tasks — set when task transitions to 'done' + task_cols_final = {r[1] for r in conn.execute("PRAGMA table_info(tasks)").fetchall()} + if "completed_at" not in task_cols_final: + conn.execute("ALTER TABLE tasks ADD COLUMN completed_at DATETIME DEFAULT NULL") + conn.commit() + def _seed_default_hooks(conn: sqlite3.Connection): """Seed default hooks for the kin project (idempotent). diff --git a/core/models.py b/core/models.py index dde8f65..6d004f3 100644 --- a/core/models.py +++ b/core/models.py @@ -304,13 +304,15 @@ def list_tasks( def update_task(conn: sqlite3.Connection, id: str, **fields) -> dict: - """Update task fields. Auto-sets updated_at.""" + """Update task fields. Auto-sets updated_at. Sets completed_at when status transitions to 'done'.""" if not fields: return get_task(conn, id) json_cols = ("brief", "spec", "review", "test_result", "security_result", "labels") for key in json_cols: if key in fields: fields[key] = _json_encode(fields[key]) + if "status" in fields and fields["status"] == "done": + fields["completed_at"] = datetime.now().isoformat() fields["updated_at"] = datetime.now().isoformat() sets = ", ".join(f"{k} = ?" for k in fields) vals = list(fields.values()) + [id] diff --git a/web/frontend/src/api.ts b/web/frontend/src/api.ts index 97c6fe5..fe44d66 100644 --- a/web/frontend/src/api.ts +++ b/web/frontend/src/api.ts @@ -122,6 +122,7 @@ export interface Task { feedback?: string | null created_at: string updated_at: string + completed_at?: string | null } export interface Decision { diff --git a/web/frontend/src/locales/en.json b/web/frontend/src/locales/en.json index 9a8724b..a3eab4f 100644 --- a/web/frontend/src/locales/en.json +++ b/web/frontend/src/locales/en.json @@ -221,7 +221,9 @@ "settings_integrations_section": "Integrations", "settings_execution_mode": "Execution mode", "settings_autocommit": "Autocommit", - "settings_autocommit_hint": "— git commit after pipeline" + "settings_autocommit_hint": "— git commit after pipeline", + "done_date_from": "From", + "done_date_to": "To" }, "escalation": { "watchdog_blocked": "Watchdog: task {task_id} blocked — {reason}", diff --git a/web/frontend/src/locales/ru.json b/web/frontend/src/locales/ru.json index 0eff34b..81c72e6 100644 --- a/web/frontend/src/locales/ru.json +++ b/web/frontend/src/locales/ru.json @@ -221,7 +221,9 @@ "settings_integrations_section": "Интеграции", "settings_execution_mode": "Режим выполнения", "settings_autocommit": "Автокоммит", - "settings_autocommit_hint": "— git commit после pipeline" + "settings_autocommit_hint": "— git commit после pipeline", + "done_date_from": "От", + "done_date_to": "До" }, "escalation": { "watchdog_blocked": "Watchdog: задача {task_id} заблокирована — {reason}", diff --git a/web/frontend/src/views/ProjectView.vue b/web/frontend/src/views/ProjectView.vue index 46051f8..5f13d48 100644 --- a/web/frontend/src/views/ProjectView.vue +++ b/web/frontend/src/views/ProjectView.vue @@ -186,6 +186,8 @@ function initStatusFilter(): string[] { const selectedStatuses = ref(initStatusFilter()) const selectedCategory = ref('') const taskSearch = ref('') +const dateFrom = ref('') +const dateTo = ref('') function toggleStatus(s: string) { const idx = selectedStatuses.value.indexOf(s) @@ -659,6 +661,16 @@ const filteredTasks = computed(() => { let tasks = searchFilteredTasks.value if (selectedStatuses.value.length > 0) tasks = tasks.filter(t => selectedStatuses.value.includes(t.status)) if (selectedCategory.value) tasks = tasks.filter(t => t.category === selectedCategory.value) + if ((dateFrom.value || dateTo.value) && selectedStatuses.value.includes('done')) { + tasks = tasks.filter(t => { + if (t.status !== 'done') return true + const dateStr = (t.completed_at || t.updated_at) ?? '' + const d = dateStr.substring(0, 10) + if (dateFrom.value && d < dateFrom.value) return false + if (dateTo.value && d > dateTo.value) return false + return true + }) + } return tasks }) @@ -1077,6 +1089,17 @@ async function addDecision() { + +
+ {{ t('projectView.done_date_from') }} + + {{ t('projectView.done_date_to') }} + + +