From 2fa151345f268013df1da309295943a9b3338b15 Mon Sep 17 00:00:00 2001 From: manoraga Date: Sun, 9 Aug 2026 13:33:55 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BD=D0=B0=D0=BB=D0=B0=D0=B6=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=BC=D0=BF=D1=82=D0=BE=D0=B2=20=D1=87=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B7=20=D0=B4=D0=B8=D0=B0=D0=BB=D0=BE=D0=B3=20=D1=81=20=D0=98?= =?UTF-8?q?=D0=98,=20=D0=BE=D1=82=D0=BB=D0=B0=D0=B6=D0=B5=D0=BD=D0=B0=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20=D1=81=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B4=D0=B0=D1=87=D0=B0=D0=BC=D0=B8,=20=D0=BD=D0=B0=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=B5=D0=BD=D0=B0=20=D0=B0=D0=B2=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B2=20=D0=B2?= =?UTF-8?q?=D0=B5=D0=B1=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9?= =?UTF-8?q?=D1=81=D0=B5.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- llm/agent.py | 126 +++++++++--------- llm/db_tools.py | 59 ++++++--- llm/schemas.py | 4 +- main.py | 204 +++++++++++++++++++++++++++-- static/index.html | 135 +++++++++++++++----- static/js/app.js | 60 ++++++--- static/js/auth.js | 312 ++++++++++++++++++++++++++++++++++++--------- static/js/chat.js | 98 ++++++++------ static/js/tasks.js | 124 ++++++++++++------ 9 files changed, 842 insertions(+), 280 deletions(-) diff --git a/llm/agent.py b/llm/agent.py index 1bd265b..fc70d3a 100644 --- a/llm/agent.py +++ b/llm/agent.py @@ -18,41 +18,22 @@ from .schemas import TOOLS_SCHEMA OLLAMA_URL = "http://192.168.11.3:11434/api/chat" MODEL_NAME = "qwen2.5:14b" +def format_rules_output(rules: List[Dict[str, Any]]) -> str: + if not rules: + return "База знаний пока пуста." + lines = [f"📚 База знаний и правила арбитража ({len(rules)}):\n"] + for idx, r in enumerate(rules, 1): + rule_text = r.get("rule_text", "").strip() + lines.append(f"{idx}. {rule_text}\n") + return "\n".join(lines).strip() + def process_chat_message(user_id: int, user_message: str, chat_history: List[Dict[str, Any]] = None) -> Tuple[str, List[Dict[str, Any]]]: if chat_history is None: chat_history = [] current_now = datetime.now().strftime("%Y-%m-%d %H:%M") - user_lower = user_message.lower().strip() - - # Быстрый прямой ответ для списка задач текущего пользователя - if any(phrase in user_lower for phrase in ["все задачи", "покажи задачи", "список задач", "реестр задач"]): - tasks = db_get_tasks(user_id) - if not tasks: - formatted_text = "Ваш список задач пуст." - else: - def get_task_num(t): - tid = str(t.get("task_id", "")) - try: - return int(tid.upper().replace("TASK-", "").strip()) - except ValueError: - return 0 - - tasks_sorted = sorted(tasks, key=get_task_num, reverse=True) - lines = [f"📋 Ваш реестр задач ({len(tasks_sorted)}):\n"] - for t in tasks_sorted: - tid = t.get('task_id', '') - title = t.get('title', 'Без названия') - lines.append(f"• {tid}: {title}") - formatted_text = "\n".join(lines) - - return formatted_text, chat_history + [{"role": "user", "content": user_message}, {"role": "assistant", "content": formatted_text}] - - if any(phrase in user_lower for phrase in ["системный промпт", "покажи промпт", "промпт системы"]): - prompt_text = db_get_active_system_prompt() - formatted_text = f"⚙️ Актуальный системный промпт ИИ:\n\n{prompt_text}" - return formatted_text, chat_history + [{"role": "user", "content": user_message}, {"role": "assistant", "content": formatted_text}] - + + # Запрос к локальной модели Qwen (полный цикл Function Calling) dynamic_prompt_text = db_get_active_system_prompt() system_prompt = { "role": "system", @@ -69,63 +50,78 @@ def process_chat_message(user_id: int, user_message: str, chat_history: List[Dic "options": {"num_predict": 2048, "num_ctx": 8192, "temperature": 0.1} } - req = urllib.request.Request( - OLLAMA_URL, - data=json.dumps(payload).encode("utf-8"), - headers={"Content-Type": "application/json"} - ) - try: + req = urllib.request.Request( + OLLAMA_URL, + data=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"} + ) with urllib.request.urlopen(req) as response: res_data = json.loads(response.read().decode("utf-8")) msg = res_data.get("message", {}) tool_calls = msg.get("tool_calls", []) - content_str = msg.get("content", "").strip().replace("**", "") if tool_calls: + messages.append(msg) + for tool in tool_calls: fn_name = tool["function"]["name"] fn_args = tool["function"].get("arguments", {}) + tool_result_content = "" if fn_name == "db_get_tasks": tasks = db_get_tasks(user_id) - if not tasks: - formatted_text = "Ваш список задач пуст." - else: - def get_task_num(t): - tid = str(t.get("task_id", "")) - try: - return int(tid.upper().replace("TASK-", "").strip()) - except ValueError: - return 0 + tool_result_content = json.dumps(tasks, ensure_ascii=False) - tasks_sorted = sorted(tasks, key=get_task_num, reverse=True) - lines = [f"📋 Ваш реестр задач ({len(tasks_sorted)}):\n"] - for t in tasks_sorted: - tid = t.get('task_id', '') - title = t.get('title', 'Без названия') - lines.append(f"• {tid}: {title}") - formatted_text = "\n".join(lines) + elif fn_name in ["db_get_system_prompt", "db_get_system_prompts"]: + tool_result_content = db_get_active_system_prompt() - return formatted_text, chat_history + [{"role": "user", "content": user_message}, {"role": "assistant", "content": formatted_text}] + elif fn_name == "db_add_system_prompt": + res = db_add_system_prompt( + name=fn_args.get("name", "main_agent"), + prompt_text=fn_args.get("prompt_text") + ) + tool_result_content = json.dumps(res, ensure_ascii=False) + + elif fn_name == "db_get_rules": + tool_result_content = json.dumps(db_get_rules(), ensure_ascii=False) elif fn_name == "db_add_task": res = db_add_task(user_id=user_id, module=fn_args.get("module", "general"), title=fn_args.get("title"), priority=fn_args.get("priority", "MEDIUM"), due_date=fn_args.get("due_date")) - formatted_text = f"[✓] Задача {res.get('task_id', 'TASK')} успешно создана!" if "status" in res else f"❌ Ошибка: {res.get('error')}" - return formatted_text, chat_history + [{"role": "user", "content": user_message}, {"role": "assistant", "content": formatted_text}] + tool_result_content = json.dumps(res, ensure_ascii=False) elif fn_name == "db_update_task_status": - res = db_update_task_status(user_id=user_id, task_id=fn_args.get("task_id"), status=fn_args.get("status"), due_date=fn_args.get("due_date")) - formatted_text = f"[✓] Статус задачи {fn_args.get('task_id')} обновлен!" if "status" in res else f"❌ Ошибка: {res.get('error')}" - return formatted_text, chat_history + [{"role": "user", "content": user_message}, {"role": "assistant", "content": formatted_text}] + res = db_update_task_status(user_id=user_id, task_id=str(fn_args.get("task_id")), status=fn_args.get("status", "COMPLETED"), due_date=fn_args.get("due_date")) + tool_result_content = json.dumps(res, ensure_ascii=False) elif fn_name == "db_delete_task": - task_id_to_del = fn_args.get("task_id", "").upper() - res = db_delete_task(user_id=user_id, task_id=task_id_to_del) - formatted_text = f"[✓] Задача {task_id_to_del} успешно удалена из вашей базы!" if "status" in res else f"❌ Ошибка: {res.get('error')}" - return formatted_text, chat_history + [{"role": "user", "content": user_message}, {"role": "assistant", "content": formatted_text}] + res = db_delete_task(user_id=user_id, task_id=str(fn_args.get("task_id", "")).upper()) + tool_result_content = json.dumps(res, ensure_ascii=False) - return content_str, chat_history + [{"role": "user", "content": user_message}, {"role": "assistant", "content": content_str}] + messages.append({ + "role": "tool", + "content": tool_result_content + }) + + # Вторичный запрос модели для формирования итогового ответа оператору + second_payload = { + "model": MODEL_NAME, + "messages": messages, + "stream": False, + "options": {"num_predict": 2048, "num_ctx": 8192, "temperature": 0.1} + } + sec_req = urllib.request.Request( + OLLAMA_URL, + data=json.dumps(second_payload).encode("utf-8"), + headers={"Content-Type": "application/json"} + ) + with urllib.request.urlopen(sec_req) as sec_response: + sec_res_data = json.loads(sec_response.read().decode("utf-8")) + final_content = sec_res_data.get("message", {}).get("content", "").strip().replace("**", "") + return final_content, chat_history + [{"role": "user", "content": user_message}, {"role": "assistant", "content": final_content}] + + content_str = msg.get("content", "").strip().replace("**", "") + return content_str or "Запрос обработан.", chat_history + [{"role": "user", "content": user_message}, {"role": "assistant", "content": content_str}] except urllib.error.URLError as e: - return f"Ошибка связи с Ollama ({OLLAMA_URL}): {e}", chat_history + return f"Ошибка связи с Ollama ({OLLAMA_URL}): {e}", chat_history \ No newline at end of file diff --git a/llm/db_tools.py b/llm/db_tools.py index 5eb80ab..2f7d209 100644 --- a/llm/db_tools.py +++ b/llm/db_tools.py @@ -8,6 +8,16 @@ def get_db_connection(): conn.row_factory = sqlite3.Row return conn +def normalize_task_id(task_id_input: str) -> str: + """Преобразует 6, '6', 'task-6' в красивый формат TASK-06 или TASK-006""" + if not task_id_input: + return "" + clean_id = str(task_id_input).strip().upper().replace("TASK-", "").replace("TASK", "") + if clean_id.isdigit(): + num = int(clean_id) + return f"TASK-{num:02d}" if num < 100 else f"TASK-{num:03d}" + return f"TASK-{clean_id}" + # === ЗАДАЧИ С ФИЛЬТРАЦИЕЙ ПО USER_ID === def db_get_tasks(user_id: int) -> List[Dict[str, Any]]: @@ -40,47 +50,51 @@ def db_add_task(user_id: int, module: str, title: str, priority: str = "MEDIUM", conn.close() return {"status": "success", "task_id": new_task_id, "message": f"Задача {new_task_id} создана"} -def db_update_task_status(user_id: int, task_id: str, status: str, due_date: Optional[str] = None) -> Dict[str, Any]: +def db_update_task_status(user_id: int, task_id: str, status: str = "COMPLETED", due_date: Optional[str] = None) -> Dict[str, Any]: conn = get_db_connection() cursor = conn.cursor() - task_id_upper = task_id.upper().strip() + formatted_id = normalize_task_id(task_id) if due_date: cursor.execute(""" UPDATE tasks SET status = ?, due_date = ? - WHERE UPPER(task_id) = ? AND user_id = ? - """, (status.upper(), due_date, task_id_upper, user_id)) + WHERE (UPPER(task_id) = ? OR task_id LIKE ?) AND user_id = ? + """, (status.upper(), due_date, formatted_id, f"%{task_id.strip()}", user_id)) else: cursor.execute(""" UPDATE tasks SET status = ? - WHERE UPPER(task_id) = ? AND user_id = ? - """, (status.upper(), task_id_upper, user_id)) + WHERE (UPPER(task_id) = ? OR task_id LIKE ?) AND user_id = ? + """, (status.upper(), formatted_id, f"%{task_id.strip()}", user_id)) if cursor.rowcount == 0: conn.close() - return {"error": f"Задача {task_id_upper} не найдена или принадлежит другому пользователю"} + return {"error": f"Задача {task_id} не найдена или принадлежит другому пользователю"} conn.commit() conn.close() - return {"status": "success", "message": f"Статус {task_id_upper} обновлен"} + return {"status": "success", "message": f"Статус задачи {formatted_id} обновлен на {status.upper()}"} def db_delete_task(user_id: int, task_id: str) -> Dict[str, Any]: conn = get_db_connection() cursor = conn.cursor() - task_id_upper = task_id.upper().strip() - cursor.execute("DELETE FROM tasks WHERE UPPER(task_id) = ? AND user_id = ?", (task_id_upper, user_id)) + formatted_id = normalize_task_id(task_id) + + cursor.execute(""" + DELETE FROM tasks + WHERE (UPPER(task_id) = ? OR task_id LIKE ?) AND user_id = ? + """, (formatted_id, f"%{task_id.strip()}", user_id)) if cursor.rowcount == 0: conn.close() - return {"error": f"Задача {task_id_upper} не найдена или принадлежит другому пользователю"} + return {"error": f"Задача {task_id} не найдена"} conn.commit() conn.close() - return {"status": "success", "message": f"Задача {task_id_upper} удалена"} + return {"status": "success", "message": f"Задача {formatted_id} удалена"} # === ОБЩИЕ СИСТЕМНЫЕ РЕСУРСЫ === @@ -95,11 +109,24 @@ def db_get_active_system_prompt() -> str: def db_add_system_prompt(name: str, prompt_text: str) -> Dict[str, Any]: conn = get_db_connection() cursor = conn.cursor() - cursor.execute("UPDATE system_prompts SET is_active = 0") - cursor.execute("INSERT INTO system_prompts (name, prompt_text, is_active) VALUES (?, ?, 1)", (name, prompt_text)) + + # Проверяем наличие промпта с таким именем, чтобы не плодить мусор + cursor.execute("SELECT id FROM system_prompts WHERE name = ?", (name,)) + existing = cursor.fetchone() + + if existing: + cursor.execute(""" + UPDATE system_prompts + SET prompt_text = ?, updated_at = CURRENT_TIMESTAMP, is_active = 1 + WHERE name = ? + """, (prompt_text, name)) + else: + cursor.execute("UPDATE system_prompts SET is_active = 0") + cursor.execute("INSERT INTO system_prompts (name, prompt_text, is_active) VALUES (?, ?, 1)", (name, prompt_text)) + conn.commit() conn.close() - return {"status": "success", "message": "Системный промпт обновлен"} + return {"status": "success", "message": "Системный промпт успешно обновлен"} def db_get_rules() -> List[Dict[str, Any]]: conn = get_db_connection() @@ -107,4 +134,4 @@ def db_get_rules() -> List[Dict[str, Any]]: cursor.execute("SELECT id, rule_text FROM ai_knowledge_base ORDER BY id ASC") rows = cursor.fetchall() conn.close() - return [dict(r) for r in rows] + return [dict(r) for r in rows] \ No newline at end of file diff --git a/llm/schemas.py b/llm/schemas.py index 531a10e..b9462bb 100644 --- a/llm/schemas.py +++ b/llm/schemas.py @@ -35,11 +35,11 @@ TOOLS_SCHEMA = [ "type": "function", "function": { "name": "db_add_system_prompt", - "description": "Обновить или добавить системный промпт ИИ в базу данных. Вызывай при командах 'задай системный промпт', 'измени промпт', 'обнови системный промпт'.", + "description": "Изменить, записать или добавить новый системный промпт ассистента в базу данных. Вызывай этот инструмент ВСЕГДА, когда пользователь просит 'добавь системный промпт', 'запиши промпт', 'измени инструкции ИИ'.", "parameters": { "type": "object", "properties": { - "prompt_text": {"type": "string", "description": "Полный новый текст системного промпта"}, + "prompt_text": {"type": "string", "description": "Полный текст нового системного промпта"}, "name": {"type": "string", "description": "Имя промпта, по умолчанию main_agent"} }, "required": ["prompt_text"] diff --git a/main.py b/main.py index 5abcee8..0166d6a 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,13 @@ import json +import sqlite3 +import logging import urllib.request +import os +from datetime import datetime, timedelta from typing import List, Dict, Any, Optional + +import jwt +from passlib.context import CryptContext from fastapi import FastAPI, Depends, HTTPException, status from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.staticfiles import StaticFiles @@ -8,45 +15,196 @@ from fastapi.responses import FileResponse from pydantic import BaseModel from llm.agent import process_chat_message -from llm.db_tools import db_get_tasks +from llm.db_tools import db_get_tasks, DB_PATH -API_TOKEN = "scud_secret_token_2026" +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + handlers=[logging.StreamHandler()] +) + +JWT_SECRET = "scud_jwt_secret_key_2026_orion_ai_super_secure" +ALGORITHM = "HS256" OLLAMA_URL = "http://192.168.11.3:11434/api/chat" MODEL_NAME = "qwen2.5:14b" +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") security = HTTPBearer() -def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)): - if credentials.credentials != API_TOKEN: +def get_db(): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + return conn + +def create_access_token(user_id: int, username: str, is_admin: bool) -> str: + payload = { + "sub": str(user_id), + "username": username, + "is_admin": is_admin, + "exp": datetime.utcnow() + timedelta(days=30) + } + return jwt.encode(payload, JWT_SECRET, algorithm=ALGORITHM) + +def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> Dict[str, Any]: + try: + token = credentials.credentials + payload = jwt.decode(token, JWT_SECRET, algorithms=[ALGORITHM]) + user_id = int(payload.get("sub")) + username = payload.get("username") + is_admin = bool(payload.get("is_admin", False)) + return {"id": user_id, "username": username, "is_admin": is_admin} + except Exception as e: + logging.warning(f"Auth error: {e}") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Неверный токен доступа", + detail="Недействительный или просроченный токен авторизации", headers={"WWW-Authenticate": "Bearer"}, ) - return credentials.credentials app = FastAPI(title="SCUD Orion AI Context API") -app.mount("/static", StaticFiles(directory="static"), name="static") +if os.path.exists("static"): + app.mount("/static", StaticFiles(directory="static"), name="static") + +class AuthRequest(BaseModel): + username: str + password: str + +class CreateUserRequest(BaseModel): + username: str + password: str + full_name: Optional[str] = None + is_admin: Optional[bool] = False + +class ChangePasswordRequest(BaseModel): + old_password: str + new_password: str class ChatRequest(BaseModel): session_id: str message: str +# === API МАРШРУТЫ === + @app.get("/") def read_root(): return FileResponse("static/index.html") +@app.post("/api/v1/auth/login") +def login(req: AuthRequest): + username = req.username.strip().lower() + logging.info(f"===> Попытка входа для пользователя: {username}") + + conn = get_db() + cursor = conn.cursor() + cursor.execute("SELECT id, username, password_hash, is_admin FROM users WHERE username = ?", (username,)) + user = cursor.fetchone() + conn.close() + + if not user: + logging.warning(f"===> Ошибка: Пользователь {username} не найден") + raise HTTPException(status_code=401, detail="Неверное имя пользователя или пароль") + + if not pwd_context.verify(req.password, user["password_hash"]): + logging.warning(f"===> Ошибка: Неверный пароль для {username}") + raise HTTPException(status_code=401, detail="Неверное имя пользователя или пароль") + + is_admin = bool(user["is_admin"]) or (user["username"] == "puh") + token = create_access_token(user["id"], user["username"], is_admin) + logging.info(f"===> УСПЕХ: Авторизован пользователь {username}") + + return {"status": "success", "token": token, "username": user["username"], "is_admin": is_admin} + +@app.post("/api/v1/auth/change-password") +def change_password(req: ChangePasswordRequest, current_user: Dict[str, Any] = Depends(get_current_user)): + if not req.new_password or len(req.new_password) < 4: + raise HTTPException(status_code=400, detail="Новый пароль должен содержать минимум 4 символа") + + conn = get_db() + cursor = conn.cursor() + cursor.execute("SELECT password_hash FROM users WHERE id = ?", (current_user["id"],)) + user = cursor.fetchone() + + if not user or not pwd_context.verify(req.old_password, user["password_hash"]): + conn.close() + raise HTTPException(status_code=400, detail="Неверный старый пароль") + + new_hash = pwd_context.hash(req.new_password) + cursor.execute("UPDATE users SET password_hash = ? WHERE id = ?", (new_hash, current_user["id"])) + conn.commit() + conn.close() + + logging.info(f"Пароль изменен для пользователя ID: {current_user['id']}") + return {"status": "success", "message": "Пароль успешно изменен"} + +@app.get("/api/v1/admin/users") +def list_users(current_user: Dict[str, Any] = Depends(get_current_user)): + if not current_user["is_admin"]: + raise HTTPException(status_code=403, detail="Доступ запрещен. Только для администратора.") + + conn = get_db() + cursor = conn.cursor() + cursor.execute("SELECT id, username, full_name, is_admin, created_at FROM users ORDER BY id ASC") + users = [dict(r) for r in cursor.fetchall()] + conn.close() + return users + +@app.post("/api/v1/admin/users") +def create_user(req: CreateUserRequest, current_user: Dict[str, Any] = Depends(get_current_user)): + if not current_user["is_admin"]: + raise HTTPException(status_code=403, detail="Доступ запрещен. Только для администратора.") + + username = req.username.strip().lower() + if not username or not req.password: + raise HTTPException(status_code=400, detail="Заполните имя пользователя и пароль") + + conn = get_db() + cursor = conn.cursor() + cursor.execute("SELECT id FROM users WHERE username = ?", (username,)) + if cursor.fetchone(): + conn.close() + raise HTTPException(status_code=400, detail="Пользователь с таким именем уже существует") + + pwd_hash = pwd_context.hash(req.password) + full_name = req.full_name.strip() if req.full_name else None + is_admin = 1 if req.is_admin else 0 + + cursor.execute( + "INSERT INTO users (username, password_hash, full_name, is_admin) VALUES (?, ?, ?, ?)", + (username, pwd_hash, full_name, is_admin) + ) + conn.commit() + conn.close() + + logging.info(f"Создан пользователь: {username} (admin={is_admin}) админом {current_user['username']}") + return {"status": "success", "message": f"Пользователь {username} создан"} + +@app.delete("/api/v1/admin/users/{user_id}") +def delete_user(user_id: int, current_user: Dict[str, Any] = Depends(get_current_user)): + if not current_user["is_admin"]: + raise HTTPException(status_code=403, detail="Доступ запрещен. Только для администратора.") + + if user_id == current_user["id"]: + raise HTTPException(status_code=400, detail="Нельзя удалить самого себя") + + conn = get_db() + cursor = conn.cursor() + cursor.execute("DELETE FROM users WHERE id = ?", (user_id,)) + conn.commit() + conn.close() + + logging.info(f"Удален пользователь ID: {user_id}") + return {"status": "success", "message": "Пользователь удален"} + @app.get("/api/v1/tasks") -def get_tasks(token: str = Depends(verify_token)): - return db_get_tasks() +def get_tasks(user: Dict[str, Any] = Depends(get_current_user)): + return db_get_tasks(user_id=user["id"]) @app.post("/api/v1/chat") -def chat_endpoint(req: ChatRequest, token: str = Depends(verify_token)): - reply, _ = process_chat_message(req.message) +def chat_endpoint(req: ChatRequest, user: Dict[str, Any] = Depends(get_current_user)): + reply, _ = process_chat_message(user_id=user["id"], user_message=req.message) return {"reply": reply} -# Эндпоинт для гостевого режима (без авторизации и без привязки к проекту) @app.post("/api/v1/chat/guest") def guest_chat_endpoint(req: ChatRequest): payload = { @@ -58,7 +216,6 @@ def guest_chat_endpoint(req: ChatRequest): "stream": False, "options": {"num_predict": 2048, "temperature": 0.3} } - try: req_ollama = urllib.request.Request( OLLAMA_URL, @@ -71,3 +228,24 @@ def guest_chat_endpoint(req: ChatRequest): return {"reply": reply} except Exception as e: return {"reply": f"Ошибка связи с локальной нейросетью: {e}"} + +# === СТРОГО В КОНЦЕ: ФОЛЛБЭК СТАТИКИ === + +@app.get("/{file_path:path}") +def serve_static_fallback(file_path: str): + clean_path = file_path.lstrip("/") + + target = os.path.join("static", clean_path) + if os.path.isfile(target): + return FileResponse(target) + + filename = os.path.basename(clean_path) + target_js = os.path.join("static/js", filename) + if filename.endswith(".js") and os.path.isfile(target_js): + return FileResponse(target_js, media_type="application/javascript") + + target_css = os.path.join("static/css", filename) + if filename.endswith(".css") and os.path.isfile(target_css): + return FileResponse(target_css, media_type="text/css") + + raise HTTPException(status_code=404, detail="File not found") \ No newline at end of file diff --git a/static/index.html b/static/index.html index be0c22c..2d9ed20 100644 --- a/static/index.html +++ b/static/index.html @@ -10,7 +10,7 @@ - +
@@ -19,36 +19,28 @@

SCUD Orion AI

-

Авторизация в системе

+

Авторизация в системе

-
+
-
-
- + - - - -
-
@@ -57,13 +49,86 @@
Или
-
+ + + + + +
@@ -76,21 +141,30 @@
-
+
- + + + + -
@@ -109,19 +183,18 @@
-
-
+
- - +
@@ -134,20 +207,20 @@ Мой реестр задач
- -
- - - - + + + +
diff --git a/static/js/app.js b/static/js/app.js index 718cfad..80abe00 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1,31 +1,46 @@ const AUTH_TOKEN_KEY = "scud_api_auth_token"; const SESSION_ID = "web_session_main"; -const STORAGE_KEY = 'scud_chat_input_history'; +const STORAGE_KEY = "scud_chat_input_history"; let API_TOKEN = localStorage.getItem(AUTH_TOKEN_KEY) || ""; -let CURRENT_USERNAME = localStorage.getItem('scud_username') || ""; -let IS_GUEST = localStorage.getItem('scud_is_guest') === 'true'; -let currentFilter = 'ALL'; -let allTasks = []; +let CURRENT_USERNAME = localStorage.getItem("scud_username") || ""; +let IS_ADMIN = localStorage.getItem("scud_is_admin") === "true"; +let IS_GUEST = localStorage.getItem("scud_is_guest") === "true"; -let inputHistory = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); +let inputHistory = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]"); let historyIndex = -1; -document.addEventListener('DOMContentLoaded', () => { - const userInputEl = document.getElementById('user-input'); +document.addEventListener("DOMContentLoaded", () => { + const userInputEl = document.getElementById("user-input"); if (userInputEl) { - userInputEl.addEventListener('input', function() { - this.style.height = 'auto'; - this.style.height = Math.min(this.scrollHeight, 80) + 'px'; + userInputEl.addEventListener("input", function() { + this.style.height = "auto"; + this.style.height = Math.min(this.scrollHeight, 80) + "px"; }); - userInputEl.addEventListener('keydown', function(e) { - if (e.key === 'Enter' && !e.shiftKey) { + userInputEl.addEventListener("keydown", function(e) { + // Отправка по Enter + if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); - document.getElementById('chat-form').requestSubmit(); + + const val = this.value.trim(); + if (val) { + // Сохраняем команду в историю + if (inputHistory.length === 0 || inputHistory[inputHistory.length - 1] !== val) { + inputHistory.push(val); + if (inputHistory.length > 50) inputHistory.shift(); + localStorage.setItem(STORAGE_KEY, JSON.stringify(inputHistory)); + } + historyIndex = -1; + } + + if (typeof sendMessage === "function") { + sendMessage(e); + } } - else if (e.key === 'ArrowUp') { + // История: стрелка ВВЕРХ + else if (e.key === "ArrowUp") { if (inputHistory.length > 0 && historyIndex < inputHistory.length - 1) { e.preventDefault(); if (historyIndex === -1) { @@ -33,11 +48,12 @@ document.addEventListener('DOMContentLoaded', () => { } historyIndex++; this.value = inputHistory[inputHistory.length - 1 - historyIndex]; - this.dispatchEvent(new Event('input')); + this.dispatchEvent(new Event("input")); setTimeout(() => this.setSelectionRange(this.value.length, this.value.length), 0); } } - else if (e.key === 'ArrowDown') { + // История: стрелка ВНИЗ + else if (e.key === "ArrowDown") { if (historyIndex !== -1) { e.preventDefault(); if (historyIndex > 0) { @@ -45,9 +61,9 @@ document.addEventListener('DOMContentLoaded', () => { this.value = inputHistory[inputHistory.length - 1 - historyIndex]; } else { historyIndex = -1; - this.value = this.dataset.draft || ''; + this.value = this.dataset.draft || ""; } - this.dispatchEvent(new Event('input')); + this.dispatchEvent(new Event("input")); setTimeout(() => this.setSelectionRange(this.value.length, this.value.length), 0); } } @@ -60,8 +76,10 @@ document.addEventListener('DOMContentLoaded', () => { } else if (API_TOKEN) { hideAuthModal(); updateUIState(); - loadTasks(); + if (typeof loadTasks === "function") { + loadTasks(); + } } else { showAuthModal(); } -}); +}); \ No newline at end of file diff --git a/static/js/auth.js b/static/js/auth.js index bfcf835..21f2bbb 100644 --- a/static/js/auth.js +++ b/static/js/auth.js @@ -1,79 +1,68 @@ -let authMode = 'login'; // 'login' или 'register' - function showAuthModal() { - document.getElementById('auth-modal').classList.remove('hidden'); + const el = document.getElementById("auth-modal"); + if (el) el.classList.remove("hidden"); } function hideAuthModal() { - document.getElementById('auth-modal').classList.add('hidden'); -} - -function setAuthMode(mode) { - authMode = mode; - const titleEl = document.getElementById('auth-title'); - const submitBtnText = document.getElementById('auth-submit-text'); - const toggleBtn = document.getElementById('auth-toggle-btn'); - const errorEl = document.getElementById('auth-error'); - - errorEl.classList.add('hidden'); - - if (mode === 'register') { - titleEl.innerText = "Регистрация нового пользователя"; - submitBtnText.innerText = "Зарегистрироваться"; - toggleBtn.innerText = "Уже есть аккаунт? Войти"; - } else { - titleEl.innerText = "Авторизация в системе"; - submitBtnText.innerText = "Войти в систему"; - toggleBtn.innerText = "Создать новый аккаунт"; - } -} - -function toggleAuthMode() { - setAuthMode(authMode === 'login' ? 'register' : 'login'); + const el = document.getElementById("auth-modal"); + if (el) el.classList.add("hidden"); } async function handleLogin(e) { - e.preventDefault(); - const usernameInput = document.getElementById('auth-username-input'); - const passwordInput = document.getElementById('auth-password-input'); - const errorEl = document.getElementById('auth-error'); + if (e && e.preventDefault) e.preventDefault(); + + const usernameInput = document.getElementById("auth-username-input"); + const passwordInput = document.getElementById("auth-password-input"); + const errorEl = document.getElementById("auth-error"); + + if (!usernameInput || !passwordInput) return; const username = usernameInput.value.trim(); const password = passwordInput.value; if (!username || !password) return; - errorEl.classList.add('hidden'); - const endpoint = authMode === 'register' ? '/api/v1/auth/register' : '/api/v1/auth/login'; + if (errorEl) errorEl.classList.add("hidden"); try { - const res = await fetch(endpoint, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + const res = await fetch("/api/v1/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password }) }); const data = await res.json(); if (res.status === 200) { + // Используем прямой строковый ключ, чтобы избежать ошибки ReferenceError API_TOKEN = data.token; CURRENT_USERNAME = data.username; + IS_ADMIN = data.is_admin; IS_GUEST = false; - localStorage.setItem(AUTH_TOKEN_KEY, data.token); - localStorage.setItem('scud_username', data.username); - localStorage.removeItem('scud_is_guest'); + localStorage.setItem("scud_api_auth_token", data.token); + localStorage.setItem("scud_username", data.username); + localStorage.setItem("scud_is_admin", data.is_admin ? "true" : "false"); + localStorage.removeItem("scud_is_guest"); hideAuthModal(); updateUIState(); - loadTasks(); + + if (typeof loadTasks === 'function') { + loadTasks(); + } } else { - errorEl.innerText = data.detail || "Ошибка авторизации"; - errorEl.classList.remove('hidden'); + if (errorEl) { + errorEl.innerText = data.detail || "Ошибка авторизации"; + errorEl.classList.remove("hidden"); + } } } catch (err) { - errorEl.innerText = "Ошибка соединения с сервером"; - errorEl.classList.remove('hidden'); + console.error("[Auth Error]", err); + if (errorEl) { + errorEl.innerText = "Ошибка соединения с сервером"; + errorEl.classList.remove("hidden"); + } } } @@ -81,36 +70,239 @@ function enableGuestMode() { IS_GUEST = true; API_TOKEN = ""; CURRENT_USERNAME = "Гость"; - localStorage.setItem('scud_is_guest', 'true'); + IS_ADMIN = false; + localStorage.setItem("scud_is_guest", "true"); hideAuthModal(); updateUIState(); } function logout() { - localStorage.removeItem(AUTH_TOKEN_KEY); - localStorage.removeItem('scud_username'); - localStorage.removeItem('scud_is_guest'); + localStorage.removeItem("scud_api_auth_token"); + localStorage.removeItem("scud_username"); + localStorage.removeItem("scud_is_admin"); + localStorage.removeItem("scud_is_guest"); API_TOKEN = ""; CURRENT_USERNAME = ""; + IS_ADMIN = false; IS_GUEST = false; showAuthModal(); } function updateUIState() { - const tasksBtn = document.getElementById('tasks-drawer-btn'); - const guestBadge = document.getElementById('guest-badge'); - const usernameBadge = document.getElementById('username-badge'); + const tasksBtn = document.getElementById("tasks-drawer-btn"); + const adminBtn = document.getElementById("admin-users-btn"); + const changePwdBtn = document.getElementById("change-pwd-btn"); + const guestBadge = document.getElementById("guest-badge"); + const usernameBadge = document.getElementById("username-badge"); - if (IS_GUEST) { - if (tasksBtn) tasksBtn.classList.add('hidden'); - if (guestBadge) guestBadge.classList.remove('hidden'); - if (usernameBadge) usernameBadge.classList.add('hidden'); + if (typeof IS_GUEST !== 'undefined' && IS_GUEST) { + if (tasksBtn) tasksBtn.classList.add("hidden"); + if (adminBtn) adminBtn.classList.add("hidden"); + if (changePwdBtn) changePwdBtn.classList.add("hidden"); + if (guestBadge) guestBadge.classList.remove("hidden"); + if (usernameBadge) usernameBadge.classList.add("hidden"); } else { - if (tasksBtn) tasksBtn.classList.remove('hidden'); - if (guestBadge) guestBadge.classList.add('hidden'); + if (tasksBtn) tasksBtn.classList.remove("hidden"); + if (changePwdBtn) changePwdBtn.classList.remove("hidden"); + if (guestBadge) guestBadge.classList.add("hidden"); + if (usernameBadge) { - usernameBadge.innerText = CURRENT_USERNAME || 'User'; - usernameBadge.classList.remove('hidden'); + usernameBadge.innerText = (typeof CURRENT_USERNAME !== 'undefined' && CURRENT_USERNAME) ? CURRENT_USERNAME : "User"; + usernameBadge.classList.remove("hidden"); + } + + if (adminBtn) { + const isAdminUser = (typeof IS_ADMIN !== 'undefined' && IS_ADMIN) || (typeof CURRENT_USERNAME !== 'undefined' && CURRENT_USERNAME === "puh"); + if (isAdminUser) { + adminBtn.classList.remove("hidden"); + } else { + adminBtn.classList.add("hidden"); + } } } } + +function openChangePasswordModal() { + const el = document.getElementById("change-pwd-modal"); + if (el) el.classList.remove("hidden"); +} + +function closeChangePasswordModal() { + const el = document.getElementById("change-pwd-modal"); + if (el) el.classList.add("hidden"); + + const err = document.getElementById("pwd-error"); + const succ = document.getElementById("pwd-success"); + if (err) err.classList.add("hidden"); + if (succ) succ.classList.add("hidden"); + + document.getElementById("old-pwd-input").value = ""; + document.getElementById("new-pwd-input").value = ""; + const confirmInput = document.getElementById("confirm-pwd-input"); + if (confirmInput) confirmInput.value = ""; +} + +async function handleChangePassword(e) { + if (e && e.preventDefault) e.preventDefault(); + const old_password = document.getElementById("old-pwd-input").value; + const new_password = document.getElementById("new-pwd-input").value; + const confirmInput = document.getElementById("confirm-pwd-input"); + const confirm_password = confirmInput ? confirmInput.value : new_password; + const errorEl = document.getElementById("pwd-error"); + const successEl = document.getElementById("pwd-success"); + + if (errorEl) errorEl.classList.add("hidden"); + if (successEl) successEl.classList.add("hidden"); + + if (new_password !== confirm_password) { + if (errorEl) { + errorEl.innerText = "Новые пароли не совпадают"; + errorEl.classList.remove("hidden"); + } + return; + } + + try { + const token = typeof API_TOKEN !== 'undefined' ? API_TOKEN : localStorage.getItem("scud_api_auth_token"); + const res = await fetch("/api/v1/auth/change-password", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer " + token + }, + body: JSON.stringify({ old_password, new_password }) + }); + + const data = await res.json(); + + if (res.status === 200) { + if (successEl) { + successEl.innerText = "Пароль успешно изменен!"; + successEl.classList.remove("hidden"); + } + setTimeout(closeChangePasswordModal, 1500); + } else { + if (errorEl) { + errorEl.innerText = data.detail || "Ошибка при смене пароля"; + errorEl.classList.remove("hidden"); + } + } + } catch (err) { + if (errorEl) { + errorEl.innerText = "Ошибка соединения с сервером"; + errorEl.classList.remove("hidden"); + } + } +} + +function openAdminModal() { + const el = document.getElementById("admin-modal"); + if (el) el.classList.remove("hidden"); + loadUsersList(); +} + +function closeAdminModal() { + const el = document.getElementById("admin-modal"); + if (el) el.classList.add("hidden"); +} + +async function loadUsersList() { + const listEl = document.getElementById("admin-users-list"); + if (!listEl) return; + listEl.innerHTML = '
Загрузка пользователей...
'; + + try { + const token = typeof API_TOKEN !== 'undefined' ? API_TOKEN : localStorage.getItem("scud_api_auth_token"); + const res = await fetch("/api/v1/admin/users", { + headers: { "Authorization": "Bearer " + token } + }); + const users = await res.json(); + + if (res.status === 200) { + listEl.innerHTML = users.map(u => { + const adminTag = u.is_admin ? 'ADMIN' : 'USER'; + const fullNameHtml = u.full_name ? `
${u.full_name}
` : ''; + const dateStr = u.created_at ? u.created_at.split(' ')[0] : '—'; + const deleteBtn = u.username !== CURRENT_USERNAME ? `` : 'Вы'; + + return ` +
+
+
+ ${u.username} + ${adminTag} +
+ ${fullNameHtml} +
Создан: ${dateStr}
+
+ ${deleteBtn} +
+ `; + }).join(""); + } else { + listEl.innerHTML = `
${users.detail}
`; + } + } catch (err) { + listEl.innerHTML = '
Ошибка загрузки пользователей
'; + } +} + +async function handleCreateUser(e) { + if (e && e.preventDefault) e.preventDefault(); + const username = document.getElementById("new-user-name").value.trim(); + const password = document.getElementById("new-user-pwd").value; + const fullNameInput = document.getElementById("new-user-fullname"); + const full_name = fullNameInput ? fullNameInput.value.trim() : ""; + const adminCheckbox = document.getElementById("new-user-is-admin"); + const is_admin = adminCheckbox ? adminCheckbox.checked : false; + const msgEl = document.getElementById("admin-msg"); + + if (msgEl) msgEl.classList.add("hidden"); + + try { + const token = typeof API_TOKEN !== 'undefined' ? API_TOKEN : localStorage.getItem("scud_api_auth_token"); + const res = await fetch("/api/v1/admin/users", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer " + token + }, + body: JSON.stringify({ username, password, full_name, is_admin }) + }); + + const data = await res.json(); + + if (res.status === 200) { + document.getElementById("new-user-name").value = ""; + document.getElementById("new-user-pwd").value = ""; + if (fullNameInput) fullNameInput.value = ""; + if (adminCheckbox) adminCheckbox.checked = false; + loadUsersList(); + } else { + if (msgEl) { + msgEl.innerText = data.detail || "Ошибка"; + msgEl.classList.remove("hidden"); + } + } + } catch (err) { + if (msgEl) { + msgEl.innerText = "Ошибка связи с сервером"; + msgEl.classList.remove("hidden"); + } + } +} + +async function deleteUser(userId, username) { + if (!confirm("Удалить пользователя " + username + "?")) return; + + try { + const token = typeof API_TOKEN !== 'undefined' ? API_TOKEN : localStorage.getItem("scud_api_auth_token"); + await fetch("/api/v1/admin/users/" + userId, { + method: "DELETE", + headers: { "Authorization": "Bearer " + token } + }); + loadUsersList(); + } catch (err) { + alert("Ошибка при удалении"); + } +} \ No newline at end of file diff --git a/static/js/chat.js b/static/js/chat.js index 83f8d28..8b6ecee 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -1,71 +1,97 @@ async function sendMessage(e) { - e.preventDefault(); - const input = document.getElementById('user-input'); - const chatWindow = document.getElementById('chat-window'); - const sendBtn = document.getElementById('send-btn'); + if (e && e.preventDefault) e.preventDefault(); + + const input = document.getElementById("user-input"); + const chatWindow = document.getElementById("chat-window"); + const sendBtn = document.getElementById("send-btn"); + + if (!input || !chatWindow) return; const text = input.value.trim(); if (!text) return; - if (inputHistory[inputHistory.length - 1] !== text) { - inputHistory.push(text); - if (inputHistory.length > 50) inputHistory.shift(); - localStorage.setItem(STORAGE_KEY, JSON.stringify(inputHistory)); - } - historyIndex = -1; - - chatWindow.innerHTML += ` -
+ // Вывод сообщения пользователя + const userMsgHtml = ` +
- ${text} + ${escapeHtml(text)}
`; - input.value = ''; - input.style.height = 'auto'; + chatWindow.insertAdjacentHTML("beforeend", userMsgHtml); + + input.value = ""; + input.style.height = "auto"; chatWindow.scrollTop = chatWindow.scrollHeight; - sendBtn.disabled = true; - sendBtn.classList.add('opacity-50'); + if (sendBtn) { + sendBtn.disabled = true; + sendBtn.classList.add("opacity-50"); + } - const endpoint = IS_GUEST ? '/api/v1/chat/guest' : '/api/v1/chat'; - const headers = { 'Content-Type': 'application/json' }; - if (!IS_GUEST) { - headers['Authorization'] = `Bearer ${API_TOKEN}`; + const token = typeof API_TOKEN !== 'undefined' && API_TOKEN ? API_TOKEN : localStorage.getItem("scud_api_auth_token"); + const isGuest = typeof IS_GUEST !== 'undefined' ? IS_GUEST : (localStorage.getItem("scud_is_guest") === "true"); + + const endpoint = isGuest ? "/api/v1/chat/guest" : "/api/v1/chat"; + const headers = { "Content-Type": "application/json" }; + if (!isGuest && token) { + headers["Authorization"] = "Bearer " + token; } try { const res = await fetch(endpoint, { - method: 'POST', + method: "POST", headers: headers, - body: JSON.stringify({ session_id: SESSION_ID, message: text }) + body: JSON.stringify({ session_id: "web_session_main", message: text }) }); - if (res.status === 401 && !IS_GUEST) { - logout(); + if (res.status === 401 && !isGuest) { + if (typeof logout === 'function') logout(); return; } const data = await res.json(); - const assistantTitle = IS_GUEST ? "Локальная нейросеть (Гость)" : "ИИ-Ассистент SCUD Orion AI"; + const assistantTitle = isGuest ? "Локальная нейросеть (Гость)" : "ИИ-Ассистент SCUD Orion AI"; + const replyText = data.reply || "Пустой ответ от нейросети"; - chatWindow.innerHTML += ` -
-

${assistantTitle}

-

${data.reply}

+ const botMsgHtml = ` +
+

+ ${assistantTitle} +

+

${escapeHtml(replyText)}

`; + chatWindow.insertAdjacentHTML("beforeend", botMsgHtml); chatWindow.scrollTop = chatWindow.scrollHeight; - if (!IS_GUEST) loadTasks(); + + if (!isGuest && typeof loadTasks === 'function') { + loadTasks(); + } } catch (err) { - chatWindow.innerHTML += ` -
+ console.error("[Chat Error]", err); + const errorHtml = ` +
Ошибка связи с сервером.
`; + chatWindow.insertAdjacentHTML("beforeend", errorHtml); + chatWindow.scrollTop = chatWindow.scrollHeight; } finally { - sendBtn.disabled = false; - sendBtn.classList.remove('opacity-50'); + if (sendBtn) { + sendBtn.disabled = false; + sendBtn.classList.remove("opacity-50"); + } } } + +function escapeHtml(text) { + if (!text) return ""; + return text + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} \ No newline at end of file diff --git a/static/js/tasks.js b/static/js/tasks.js index ae10b4e..36816b9 100644 --- a/static/js/tasks.js +++ b/static/js/tasks.js @@ -1,20 +1,26 @@ +let currentFilter = 'ALL'; +let allTasks = []; + function toggleDrawer() { - if (IS_GUEST) return; - const drawer = document.getElementById('task-drawer'); - const backdrop = document.getElementById('drawer-backdrop'); - const isHidden = drawer.classList.contains('translate-x-full'); + if (typeof IS_GUEST !== 'undefined' && IS_GUEST) return; + const drawer = document.getElementById("task-drawer"); + const backdrop = document.getElementById("drawer-backdrop"); + if (!drawer) return; + + const isHidden = drawer.classList.contains("translate-x-full"); if (isHidden) { - drawer.classList.remove('translate-x-full'); - backdrop.classList.remove('hidden'); + drawer.classList.remove("translate-x-full"); + if (backdrop) backdrop.classList.remove("hidden"); + loadTasks(); } else { - drawer.classList.add('translate-x-full'); - backdrop.classList.add('hidden'); + drawer.classList.add("translate-x-full"); + if (backdrop) backdrop.classList.add("hidden"); } } function setFilter(status) { currentFilter = status; - ['ALL', 'IN_PROGRESS', 'BACKLOG', 'COMPLETED'].forEach(f => { + ["ALL", "IN_PROGRESS", "BACKLOG", "COMPLETED"].forEach(f => { const btn = document.getElementById(`filter-${f}`); if (btn) { btn.className = (f === status) @@ -26,26 +32,71 @@ function setFilter(status) { } async function loadTasks() { - if (IS_GUEST || !API_TOKEN) return; + const badge = document.getElementById("task-count-badge"); + const container = document.getElementById("tasks-container"); + + const token = typeof API_TOKEN !== 'undefined' && API_TOKEN ? API_TOKEN : localStorage.getItem("scud_api_auth_token"); + const isGuest = typeof IS_GUEST !== 'undefined' ? IS_GUEST : (localStorage.getItem("scud_is_guest") === "true"); + + if (isGuest || !token) { + if (badge) badge.innerText = "0"; + return; + } + try { - const res = await fetch('/api/v1/tasks', { - headers: { 'Authorization': `Bearer ${API_TOKEN}` } + const res = await fetch("/api/v1/tasks", { + headers: { + "Authorization": "Bearer " + token, + "Content-Type": "application/json" + } }); + if (res.status === 401) { - logout(); + if (typeof logout === 'function') logout(); return; } - allTasks = await res.json(); - document.getElementById('task-count-badge').innerText = allTasks.length; + + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`); + } + + const data = await res.json(); + + // Гибкое определение структуры данных (массив или объект с ключом tasks) + if (Array.isArray(data)) { + allTasks = data; + } else if (data && Array.isArray(data.tasks)) { + allTasks = data.tasks; + } else if (data && typeof data === 'object') { + allTasks = Object.values(data).find(val => Array.isArray(val)) || []; + } else { + allTasks = []; + } + + if (badge) { + badge.innerText = allTasks.length.toString(); + } + renderTasks(); + } catch (err) { - document.getElementById('tasks-container').innerHTML = `
Ошибка загрузки задач
`; + console.error("[Tasks Error]", err); + if (badge) badge.innerText = "0"; + if (container) { + container.innerHTML = `
Ошибка обработки списка задач
`; + } } } function renderTasks() { - const container = document.getElementById('tasks-container'); - const filtered = allTasks.filter(t => currentFilter === 'ALL' || t.status === currentFilter); + const container = document.getElementById("tasks-container"); + if (!container) return; + + if (!Array.isArray(allTasks)) { + allTasks = []; + } + + const filtered = allTasks.filter(t => currentFilter === "ALL" || t.status === currentFilter); if (filtered.length === 0) { container.innerHTML = `
Нет задач с выбранным фильтром
`; @@ -53,41 +104,42 @@ function renderTasks() { } container.innerHTML = filtered.map(t => { - let statusBadge = 'bg-slate-100 text-slate-600 border-slate-200'; - let cardBg = 'bg-white'; - if (t.status === 'COMPLETED') { - statusBadge = 'bg-emerald-50 text-emerald-700 border-emerald-300 font-semibold'; - cardBg = 'bg-emerald-50/20'; - } else if (t.status === 'IN_PROGRESS') { - statusBadge = 'bg-amber-50 text-amber-700 border-amber-300 font-bold'; - cardBg = 'bg-amber-50/20 border-amber-200'; + let statusBadge = "bg-slate-100 text-slate-600 border-slate-200"; + let cardBg = "bg-white"; + + if (t.status === "COMPLETED") { + statusBadge = "bg-emerald-50 text-emerald-700 border-emerald-300 font-semibold"; + cardBg = "bg-emerald-50/20"; + } else if (t.status === "IN_PROGRESS") { + statusBadge = "bg-amber-50 text-amber-700 border-amber-300 font-bold"; + cardBg = "bg-amber-50/20 border-amber-200"; } - let priorityBadge = 'text-slate-500 bg-slate-100 border-slate-200'; - if (t.priority === 'HIGH') priorityBadge = 'text-red-700 bg-red-50 border-red-200 font-bold'; + let priorityBadge = "text-slate-500 bg-slate-100 border-slate-200"; + if (t.priority === "HIGH") priorityBadge = "text-red-700 bg-red-50 border-red-200 font-bold"; let dueDateHtml = t.due_date ? `
Срок: ${t.due_date} -
` : ''; +
` : ""; return `
- ${t.task_id} - ${t.priority || 'HIGH'} + ${t.task_id || t.id || 'TASK'} + ${t.priority || 'MEDIUM'}
- ${t.status} + ${t.status || 'BACKLOG'}
-

${t.title}

+

${t.title || t.description || ''}

- ${t.module} + ${t.module || 'General'}
${dueDateHtml}
`; - }).join(''); -} + }).join(""); +} \ No newline at end of file