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
-
Авторизация в системе
+
Авторизация в системе
-
+
+
+
+
+
+ Смена пароля
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Управление пользователями
+
+
+
+
+
+
+
+
Загрузка пользователей...
+
+
+
+
@@ -76,21 +141,30 @@
-
+
Гость
- Admin
+ puh
-
@@ -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