From 531275e4ce4a391cc12c07363be2c719a6d006b4 Mon Sep 17 00:00:00 2001 From: Gros Frumos Date: Mon, 16 Mar 2026 17:44:49 +0200 Subject: [PATCH] =?UTF-8?q?kin:=20KIN-UI-003=20=D0=9A=D0=BE=D0=BD=D1=81?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D0=B5=D0=BD=D1=82=D0=BD=D0=B0=D1=8F=20=D0=BE?= =?UTF-8?q?=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B0=20=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=B1=D0=BE=D0=BA=20=D0=B2=20del()=20=E2=80=94=20=D0=B8?= =?UTF-8?q?=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20throwApiError?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/models.py | 4 +- tests/test_api.py | 19 +++++ web/api.py | 15 +++- web/frontend/src/api.ts | 5 +- web/frontend/src/views/Dashboard.vue | 104 +++++++++++++++++++-------- 5 files changed, 112 insertions(+), 35 deletions(-) diff --git a/core/models.py b/core/models.py index cbdeb72..d3187c4 100644 --- a/core/models.py +++ b/core/models.py @@ -100,9 +100,9 @@ def get_project(conn: sqlite3.Connection, id: str) -> dict | None: def delete_project(conn: sqlite3.Connection, id: str) -> None: - """Delete a project and all its related data (modules, decisions, tasks).""" + """Delete a project and all its related data (modules, decisions, tasks, phases).""" # Delete tables that have FK references to tasks BEFORE deleting tasks - for table in ("modules", "agent_logs", "decisions", "pipelines", "tasks"): + for table in ("modules", "agent_logs", "decisions", "pipelines", "project_phases", "tasks"): conn.execute(f"DELETE FROM {table} WHERE project_id = ?", (id,)) conn.execute("DELETE FROM projects WHERE id = ?", (id,)) conn.commit() diff --git a/tests/test_api.py b/tests/test_api.py index 3503e9a..844e159 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1694,4 +1694,23 @@ def test_bootstrap_endpoint_success(bootstrap_client, tmp_path): assert data["project"]["id"] == "goodproj" assert "modules_count" in data assert "decisions_count" in data + + +def test_delete_project_ok(client): + # Create a separate project to delete + r = client.post("/api/projects", json={"id": "del1", "name": "Del1", "path": "/del1"}) + assert r.status_code == 200 + + r = client.delete("/api/projects/del1") + assert r.status_code == 204 + assert r.content == b"" + + # Verify project is gone + r = client.get("/api/projects/del1") + assert r.status_code == 404 + + +def test_delete_project_not_found(client): + r = client.delete("/api/projects/99999") + assert r.status_code == 404 assert "tasks_count" in data diff --git a/web/api.py b/web/api.py index 10c8d28..6475518 100644 --- a/web/api.py +++ b/web/api.py @@ -14,7 +14,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent)) from fastapi import FastAPI, HTTPException, Query from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, FileResponse +from fastapi.responses import JSONResponse, FileResponse, Response from fastapi.staticfiles import StaticFiles from pydantic import BaseModel, model_validator @@ -247,6 +247,19 @@ def patch_project(project_id: str, body: ProjectPatch): return p +@app.delete("/api/projects/{project_id}", status_code=204) +def delete_project(project_id: str): + """Delete a project and all its related data (tasks, decisions, phases, logs).""" + conn = get_conn() + p = models.get_project(conn, project_id) + if not p: + conn.close() + raise HTTPException(404, f"Project '{project_id}' not found") + models.delete_project(conn, project_id) + conn.close() + return Response(status_code=204) + + @app.post("/api/projects/{project_id}/sync/obsidian") def sync_obsidian_endpoint(project_id: str): """Запускает двусторонний Obsidian sync для проекта.""" diff --git a/web/frontend/src/api.ts b/web/frontend/src/api.ts index 85994ea..8db543e 100644 --- a/web/frontend/src/api.ts +++ b/web/frontend/src/api.ts @@ -48,7 +48,8 @@ async function post(path: string, body: unknown): Promise { async function del(path: string): Promise { const res = await fetch(`${BASE}${path}`, { method: 'DELETE' }) - if (!res.ok) throw new Error(`${res.status} ${res.statusText}`) + if (!res.ok) await throwApiError(res) + if (res.status === 204) return undefined as T return res.json() } @@ -270,6 +271,8 @@ export const api = { post(`/projects/${projectId}/deploy`, {}), syncObsidian: (projectId: string) => post(`/projects/${projectId}/sync/obsidian`, {}), + deleteProject: (id: string) => + del(`/projects/${id}`), deleteDecision: (projectId: string, decisionId: number) => del<{ deleted: number }>(`/projects/${projectId}/decisions/${decisionId}`), createDecision: (data: { project_id: string; type: string; title: string; description: string; category?: string; tags?: string[] }) => diff --git a/web/frontend/src/views/Dashboard.vue b/web/frontend/src/views/Dashboard.vue index 4a15de9..c998ccc 100644 --- a/web/frontend/src/views/Dashboard.vue +++ b/web/frontend/src/views/Dashboard.vue @@ -139,6 +139,21 @@ function toggleNpRole(key: string) { else npRoles.value.push(key) } +// Delete project +const confirmDeleteId = ref(null) +const deleteError = ref('') + +async function deleteProject(id: string) { + deleteError.value = '' + try { + await api.deleteProject(id) + projects.value = projects.value.filter(p => p.id !== id) + confirmDeleteId.value = null + } catch (e: any) { + deleteError.value = e.message + } +} + async function createNewProject() { npError.value = '' if (!npRoles.value.length) { @@ -197,39 +212,66 @@ async function createNewProject() {

{{ error }}

- -
-
- {{ p.id }} - - - {{ p.name }} +
+ +
+

Удалить проект «{{ p.name }}»? Это действие необратимо.

+
+ +
-
- ${{ costMap[p.id]?.toFixed(2) }}/wk - pri {{ p.priority }} +

{{ deleteError }}

+
+ + +
+
+ {{ p.id }} + + + {{ p.name }} +
+
+ ${{ costMap[p.id]?.toFixed(2) }}/wk + pri {{ p.priority }} + +
-
-
- {{ p.total_tasks }} tasks - - - {{ p.active_tasks }} active - - {{ p.review_tasks }} awaiting review - {{ p.blocked_tasks }} blocked - {{ p.done_tasks }} done - - {{ p.total_tasks - p.done_tasks - p.active_tasks - p.blocked_tasks - (p.review_tasks || 0) }} pending - -
- +
+ {{ p.total_tasks }} tasks + + + {{ p.active_tasks }} active + + {{ p.review_tasks }} awaiting review + {{ p.blocked_tasks }} blocked + {{ p.done_tasks }} done + + {{ p.total_tasks - p.done_tasks - p.active_tasks - p.blocked_tasks - (p.review_tasks || 0) }} pending + +
+ +