уход с работы

This commit is contained in:
2026-08-06 18:55:21 +03:00
parent 955130fb54
commit 18392f11ef
5 changed files with 825 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
# Виртуальное окружение Python
venv/
.venv/
env/
# Кэш Python
__pycache__/
*.py[cod]
*$py.class
# Локальная база данных SQLite
*.db
*.sqlite
*.sqlite3
# Переменные окружения и секреты
.env
# Логи и временные файлы
*.log
.DS_Store
.idea/
.vscode/
+78
View File
@@ -0,0 +1,78 @@
import sqlite3
import os
from datetime import datetime
DB_NAME = "context_memory.db"
def init_db():
conn = sqlite3.connect(DB_NAME)
cursor = conn.cursor()
# 1. Таблица трекинга задач
cursor.execute("""
CREATE TABLE IF NOT EXISTS tasks (
task_id TEXT PRIMARY KEY,
module TEXT NOT NULL,
title TEXT NOT NULL,
status TEXT NOT NULL,
priority TEXT NOT NULL,
completed_at TEXT
)
""")
# 2. Таблица архитектурных правил и контекста
cursor.execute("""
CREATE TABLE IF NOT EXISTS architecture_memory (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT NOT NULL,
rule_text TEXT NOT NULL,
is_active INTEGER DEFAULT 1,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
""")
# Первичное заполнение реестра задач [TASK-01] .. [TASK-17]
tasks_data = [
("TASK-01", "services/data_loader", "Исправление KeyError по ФИО в Excel экспорте 1С (переход на .iloc)", "COMPLETED", "HIGH", "2026-08-03"),
("TASK-02", "services/zup_extractor", "Перевод подключения 1С:ЗУП на продакшн MS SQL сервер ACCOUNT-01 + гибридный фоллбэк", "COMPLETED", "HIGH", "2026-08-04"),
("TASK-03", "services/scud_export", "Инцидент Равина В.Э.: Фиксация 100% приоритета сырых логов pLogData СКУД над расчетом УРВ Ориона", "COMPLETED", "HIGH", "2026-08-04"),
("TASK-04", "core/database", "Реализация схемы SQLite WAL, сохранение снапшотов и маппинг колонок (department/position)", "COMPLETED", "HIGH", "2026-08-05"),
("TASK-05", "core/database", "Исправление бага дублирования номеров снапшотов (переход на вычисление MAX() по суффиксу)", "COMPLETED", "HIGH", "2026-08-05"),
("TASK-06", "scripts/fix_snapshots", "Создание и выполнение скрипта сквозной переиндексации снапшотов с префиксом Y", "COMPLETED", "HIGH", "2026-08-05"),
("TASK-07", "scripts/db_cli", "Реализация CLI-инспектора БД и выравнивание номеров срезов для идеальной верстки в терминале", "COMPLETED", "HIGH", "2026-08-05"),
("TASK-08", "main.py / logic", "Двойная проверка исключений по department_scud и 1С, удержание 100% приоритета кадровых документов", "COMPLETED", "HIGH", "2026-08-05"),
("TASK-09", "services/text_reporter", "Лаконичный вывод неразмеченных сотрудников с короткими аббревиатурами отделов СКУД (РУК, ОИЗ, ПУ и др.)", "COMPLETED", "HIGH", "2026-08-05"),
("TASK-10", "services/scud_export", "Внутридневной контроль: Извлечение полных цепочек проходов pLogData (входы/выходы за смену)", "BACKLOG", "HIGH", None),
("TASK-11", "services/data_loader", "Внутридневной контроль: Расчет количества и суммарной длительности отлучек/перекуров сверх обеда", "BACKLOG", "HIGH", None),
("TASK-12", "services/text_reporter", "Внутридневной контроль: Вывод списка системных нарушителей внутреннего распорядка в Markdown-отчет", "BACKLOG", "MEDIUM", None),
("TASK-13", "automation", "Автоматизация сбора срезов по расписанию (Cron / Systemd Timers на 12:00, 17:00, 19:00)", "BACKLOG", "HIGH", None),
("TASK-14", "services/notifications", "Интеграция Telegram-бота для алертов Администратору при массовых сбоях турникетов (>5%)", "BACKLOG", "MEDIUM", None),
("TASK-15", "api / fastapi", "Создание REST API на FastAPI (/api/v1/snapshots, /api/v1/scud/logs, /api/v1/anomalies)", "BACKLOG", "HIGH", None),
("TASK-16", "frontend / chat", "Веб-интерфейс с ИИ-чатом на базе Ollama (Qwen 2.5 14B) с поддержкой Function Calling к REST API", "BACKLOG", "HIGH", None),
("TASK-17", "core/memory", "Создание модуля персистентной памяти проекта в SQLite и CLI-инструмента для фиксации архитектурных решений", "IN_PROGRESS", "HIGH", None),
]
cursor.executemany("""
INSERT OR REPLACE INTO tasks (task_id, module, title, status, priority, completed_at)
VALUES (?, ?, ?, ?, ?, ?)
""", tasks_data)
# Базовые правила архитектуры
rules_data = [
("logic_rule", "Сырые логи pLogData СКУД имееют 100% приоритет над расчетом УРВ Ориона."),
("logic_rule", "Официальные кадровые документы 1С (отпуска, командировки, больничные) имеют абсолютный приоритет над отсутствием в СКУД."),
("scud_override", "Запрещено переименовывать ФИО сотрудников при 100% совпадении строк между 1С и СКУД в AI-верификаторе."),
("formatting", "Температура указывается исключительно в градусах Цельсия, скорости в км/ч.")
]
cursor.executemany("""
INSERT INTO architecture_memory (category, rule_text)
VALUES (?, ?)
""", rules_data)
conn.commit()
conn.close()
print("[✓] База данных контекста и трекинга задач успешно создана и заполнена!")
if __name__ == "__main__":
init_db()
+292
View File
@@ -0,0 +1,292 @@
# === ANCHOR: IMPORTS_START ===
import sqlite3
import json
import urllib.request
import urllib.error
from typing import List, Dict, Any, Tuple
from datetime import datetime
DB_NAME = "context_memory.db"
OLLAMA_URL = "http://192.168.11.3:11434/api/chat"
MODEL_NAME = "qwen2.5:14b"
# === ANCHOR: IMPORTS_END ===
# === ANCHOR: DB_TOOLS_START ===
def db_get_tasks(status: str = None) -> str:
"""Извлекает список задач из реестра БД."""
conn = sqlite3.connect(DB_NAME)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
if status:
cursor.execute("SELECT * FROM tasks WHERE status = ? ORDER BY task_id", (status.upper(),))
else:
cursor.execute("SELECT * FROM tasks ORDER BY task_id")
rows = [dict(r) for r in cursor.fetchall()]
conn.close()
return json.dumps(rows, ensure_ascii=False)
def db_update_task_status(task_id: str, status: str = None, due_date: str = None) -> str:
"""Обновляет статус и/или срок выполнения задачи."""
conn = sqlite3.connect(DB_NAME)
cursor = conn.cursor()
updates = []
params = []
if status:
status_upper = status.upper()
updates.append("status = ?")
params.append(status_upper)
if status_upper == "COMPLETED":
updates.append("completed_at = CURRENT_TIMESTAMP")
else:
updates.append("completed_at = NULL")
if due_date:
updates.append("due_date = ?")
params.append(due_date)
if not updates:
conn.close()
return json.dumps({"error": "Не указаны параметры для обновления"}, ensure_ascii=False)
params.append(task_id.upper())
query = f"UPDATE tasks SET {', '.join(updates)} WHERE task_id = ?"
cursor.execute(query, tuple(params))
if cursor.rowcount == 0:
conn.close()
return json.dumps({"error": f"Задача {task_id} не найдена"}, ensure_ascii=False)
conn.commit()
conn.close()
return json.dumps({"status": "success", "message": f"Задача {task_id} обновлена"}, ensure_ascii=False)
def db_add_task(task_id: str, module: str, title: str, priority: str = "HIGH", due_date: str = None) -> str:
"""Добавляет новую задачу в реестр."""
conn = sqlite3.connect(DB_NAME)
cursor = conn.cursor()
try:
cursor.execute(
"INSERT INTO tasks (task_id, module, title, status, priority, due_date) VALUES (?, ?, ?, 'BACKLOG', ?, ?)",
(task_id.upper(), module, title, priority.upper(), due_date)
)
conn.commit()
res = {"status": "success", "message": f"Задача {task_id} создана"}
except sqlite3.IntegrityError:
res = {"error": f"Задача с ID {task_id} уже существует"}
finally:
conn.close()
return json.dumps(res, ensure_ascii=False)
def db_get_rules() -> str:
"""Возвращает действующие архитектурные правила."""
conn = sqlite3.connect(DB_NAME)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("SELECT category, rule_text FROM architecture_memory WHERE is_active = 1")
rows = [dict(r) for r in cursor.fetchall()]
conn.close()
return json.dumps(rows, ensure_ascii=False)
# === ANCHOR: DB_TOOLS_END ===
# === ANCHOR: TOOLS_SCHEMA_START ===
TOOLS_SCHEMA = [
{
"type": "function",
"function": {
"name": "db_get_tasks",
"description": "Получить список задач проекта с возможностью фильтрации по статусу (BACKLOG, IN_PROGRESS, COMPLETED).",
"parameters": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Фильтр статуса: BACKLOG, IN_PROGRESS или COMPLETED."
}
}
}
}
},
{
"type": "function",
"function": {
"name": "db_update_task_status",
"description": "Изменить статус и/или срок выполнения (due_date) задачи в реестре.",
"parameters": {
"type": "object",
"properties": {
"task_id": {"type": "string", "description": "Идентификатор задачи, например TASK-17"},
"status": {"type": "string", "description": "Новый статус: COMPLETED, IN_PROGRESS или BACKLOG"},
"due_date": {"type": "string", "description": "Срок выполнения задачи, например '2026-08-07 14:00'"}
},
"required": ["task_id"]
}
}
},
{
"type": "function",
"function": {
"name": "db_add_task",
"description": "Добавить новую задачу в бэклог проекта.",
"parameters": {
"type": "object",
"properties": {
"task_id": {"type": "string", "description": "Уникальный ID задачи, например TASK-18"},
"module": {"type": "string", "description": "Модуль проекта, например services/scud_export"},
"title": {"type": "string", "description": "Краткое описание задачи"},
"priority": {"type": "string", "description": "Приоритет: HIGH, MEDIUM, LOW"},
"due_date": {"type": "string", "description": "Срок выполнения задачи, например '2026-08-07 14:00'"}
},
"required": ["task_id", "module", "title"]
}
}
},
{
"type": "function",
"function": {
"name": "db_get_rules",
"description": "Получить список архитектурных правил и ограничений проекта.",
"parameters": {"type": "object", "properties": {}}
}
}
]
# === ANCHOR: TOOLS_SCHEMA_END ===
# === ANCHOR: CHAT_AGENT_START ===
def process_chat_message(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")
system_prompt = {
"role": "system",
"content": (
f"Текущая дата и время сервера: {current_now}.\n"
f"Текущая дата и время сервера: {current_now}.\n"
"Ты — ИИ-ассистент проекта SCUD Orion AI. "
"У тебя есть доступ к функциям для просмотра, добавления и изменения статусов и сроков задач (due_date) в БД. "
"Когда пользователь просит показать задачи, добавить новую или изменить статус — обязательно вызывай нужную функцию. "
"После вызова функции давай пользователю понятное, лаконичное резюме результата на русском языке. "
"При запросе списка всех задач не расписывай каждую подробно, а давай краткую сводку по количествам, "
"так как детали отображаются в выезжающем реестре задач."
)
}
messages = [system_prompt] + chat_history + [{"role": "user", "content": user_message}]
payload = {
"model": MODEL_NAME,
"messages": messages,
"tools": TOOLS_SCHEMA,
"stream": False,
"options": {
"num_predict": 2048, # Достаточно для любых длинных списков и кода
"num_ctx": 8192, # Расширенная память диалога без потери скорости
"temperature": 0.2, # Низкая температура: ответы точные и без галлюцинаций
"top_p": 0.9, # Оптимальная выборка токенов
"num_gpu": 99 # Заставляет Ollama загрузить 100% слоев модели в VRAM
}
}
req = urllib.request.Request(
OLLAMA_URL,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"}
)
try:
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", [])
# Резервная проверка, если Ollama прислала вызов функции текстом
content_str = msg.get("content", "")
if not tool_calls and "name" in content_str and "db_" in content_str:
try:
parsed = json.loads(content_str)
if "name" in parsed:
tool_calls = [{"function": parsed}]
except Exception:
pass
if tool_calls:
messages.append(msg)
for tool in tool_calls:
fn_name = tool["function"]["name"]
fn_args = tool["function"].get("arguments", {})
if fn_name == "db_get_tasks":
result = db_get_tasks(status=fn_args.get("status"))
elif fn_name == "db_update_task_status":
result = db_update_task_status(
task_id=fn_args.get("task_id"),
status=fn_args.get("status"),
due_date=fn_args.get("due_date")
)
elif fn_name == "db_add_task":
result = db_add_task(
task_id=fn_args.get("task_id"),
module=fn_args.get("module"),
title=fn_args.get("title"),
priority=fn_args.get("priority", "HIGH"),
due_date=fn_args.get("due_date")
)
elif fn_name == "db_get_rules":
result = db_get_rules()
else:
result = json.dumps({"error": "Неизвестная функция"})
messages.append({
"role": "tool",
"content": result
})
# Вторым запросом передаем результат обратно модели для текстового ответа
payload = {
"model": MODEL_NAME,
"messages": messages,
"tools": TOOLS_SCHEMA,
"stream": False,
"options": {
"num_predict": 2048, # Достаточно для любых длинных списков и кода
"num_ctx": 8192, # Расширенная память диалога без потери скорости
"temperature": 0.2, # Низкая температура: ответы точные и без галлюцинаций
"top_p": 0.9, # Оптимальная выборка токенов
"num_gpu": 99 # Заставляет Ollama загрузить 100% слоев модели в VRAM
}
}
req_second = urllib.request.Request(
OLLAMA_URL,
data=json.dumps(payload_second).encode("utf-8"),
headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req_second) as resp_second:
final_res = json.loads(resp_second.read().decode("utf-8"))
final_msg = final_res.get("message", {}).get("content", "")
updated_history = chat_history + [
{"role": "user", "content": user_message},
{"role": "assistant", "content": final_msg}
]
return final_msg, updated_history
else:
final_msg = msg.get("content", "")
updated_history = chat_history + [
{"role": "user", "content": user_message},
{"role": "assistant", "content": final_msg}
]
return final_msg, updated_history
except urllib.error.URLError as e:
return f"Ошибка связи с Ollama ({OLLAMA_URL}): {e}", chat_history
# === ANCHOR: CHAT_AGENT_END ===
+166
View File
@@ -0,0 +1,166 @@
# === ANCHOR: IMPORTS_START ===
import os
import json
import sqlite3
from typing import List, Optional, Dict, Any
from fastapi import FastAPI, HTTPException, Depends, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, HTMLResponse
from pydantic import BaseModel
from llm_agent import process_chat_message
# === ANCHOR: IMPORTS_END ===
# === ANCHOR: APP_INIT_START ===
app = FastAPI(
title="SCUD Orion AI Context & Task Tracker API",
version="2.0.0",
description="REST API локального контекста, памяти и трекинга задач для ИИ-аудитора"
)
DB_NAME = "context_memory.db"
SECURITY_TOKEN = os.getenv("API_BEARER_TOKEN", "scud_secret_token_2026")
security = HTTPBearer()
# === ANCHOR: APP_INIT_END ===
# === ANCHOR: DATABASE_HELPERS_START ===
def init_chat_table():
"""Создает таблицу истории чата, если она не существует."""
conn = sqlite3.connect(DB_NAME)
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS chat_sessions (
session_id TEXT PRIMARY KEY,
history_json TEXT NOT NULL,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
conn.close()
init_chat_table()
def verify_token(credentials: HTTPAuthorizationCredentials = Security(security)):
if credentials.credentials != SECURITY_TOKEN:
raise HTTPException(status_code=403, detail="Недействительный токен доступа")
return credentials.credentials
def get_db_connection():
conn = sqlite3.connect(DB_NAME)
conn.row_factory = sqlite3.Row
return conn
# === ANCHOR: DATABASE_HELPERS_END ===
# === ANCHOR: SCHEMAS_START ===
class TaskSchema(BaseModel):
task_id: str
module: str
title: str
status: str
priority: str
completed_at: Optional[str] = None
class RuleSchema(BaseModel):
id: int
category: str
rule_text: str
is_active: int
class ChatRequest(BaseModel):
session_id: str = "default"
message: str
class ChatResponse(BaseModel):
session_id: str
reply: str
# === ANCHOR: SCHEMAS_END ===
# === ANCHOR: STATIC_ROUTES_START ===
# Подключаем папку static для отдачи HTML-интерфейса
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/", response_class=HTMLResponse)
def read_root():
return FileResponse("static/index.html")
# === ANCHOR: STATIC_ROUTES_END ===
# === ANCHOR: TASKS_ENDPOINTS_START ===
@app.get("/api/v1/tasks", response_model=List[TaskSchema], dependencies=[Depends(verify_token)])
def get_all_tasks(status: Optional[str] = None):
conn = get_db_connection()
cursor = conn.cursor()
if status:
cursor.execute("SELECT * FROM tasks WHERE status = ? ORDER BY task_id", (status.upper(),))
else:
cursor.execute("SELECT * FROM tasks ORDER BY task_id")
rows = cursor.fetchall()
conn.close()
return [dict(r) for r in rows]
@app.post("/api/v1/tasks/{task_id}/complete", dependencies=[Depends(verify_token)])
def mark_task_completed(task_id: str):
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute(
"UPDATE tasks SET status = 'COMPLETED', completed_at = CURRENT_TIMESTAMP WHERE task_id = ?",
(task_id.upper(),)
)
if cursor.rowcount == 0:
conn.close()
raise HTTPException(status_code=404, detail=f"Задача {task_id} не найдена")
conn.commit()
conn.close()
return {"status": "success", "message": f"Задача {task_id} успешно выполнена"}
# === ANCHOR: TASKS_ENDPOINTS_END ===
# === ANCHOR: RULES_AND_CHAT_ENDPOINTS_START ===
@app.get("/api/v1/rules", response_model=List[RuleSchema], dependencies=[Depends(verify_token)])
def get_active_rules():
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM architecture_memory WHERE is_active = 1 ORDER BY id")
rows = cursor.fetchall()
conn.close()
return [dict(r) for r in rows]
@app.post("/api/v1/chat", response_model=ChatResponse, dependencies=[Depends(verify_token)])
def chat_endpoint(req: ChatRequest):
"""Принимает сообщение, подтягивает историю из БД, выполняет вызовы функций к БД и сохраняет историю."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT history_json FROM chat_sessions WHERE session_id = ?", (req.session_id,))
row = cursor.fetchone()
chat_history = json.loads(row["history_json"]) if row else []
reply, updated_history = process_chat_message(req.message, chat_history)
cursor.execute(
"INSERT OR REPLACE INTO chat_sessions (session_id, history_json, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)",
(req.session_id, json.dumps(updated_history, ensure_ascii=False))
)
conn.commit()
conn.close()
return ChatResponse(session_id=req.session_id, reply=reply)
@app.get("/health")
def health_check():
return {"status": "healthy", "service": "scud_context_api", "version": "2.0.0"}
# === ANCHOR: RULES_AND_CHAT_ENDPOINTS_END ===
+266
View File
@@ -0,0 +1,266 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SCUD Orion AI — Context Manager</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body class="bg-slate-50 text-slate-800 h-screen flex flex-col font-sans">
<!-- Хедер -->
<header class="bg-white border-b border-slate-200 px-6 py-3 flex justify-between items-center shadow-sm">
<div class="flex items-center space-x-3">
<div class="bg-indigo-600 text-white p-2 rounded-lg">
<i class="fa-solid fa-brain text-lg"></i>
</div>
<div>
<h1 class="text-base font-bold leading-none text-slate-900">SCUD Orion AI</h1>
<span class="text-xs text-slate-500">Context & Task Manager</span>
</div>
</div>
<div class="flex items-center space-x-4">
<span class="flex items-center text-xs text-emerald-600 font-medium bg-emerald-50 px-2.5 py-1 rounded-full border border-emerald-200">
<span class="h-2 w-2 rounded-full bg-emerald-500 mr-1.5"></span> API Online
</span>
<button onclick="toggleDrawer()" class="bg-indigo-50 hover:bg-indigo-100 text-indigo-700 px-3.5 py-1.5 rounded-lg text-xs font-semibold flex items-center gap-2 transition border border-indigo-200">
<i class="fa-solid fa-list-check text-indigo-600"></i>
<span>Реестр задач</span>
<span id="task-count-badge" class="bg-indigo-600 text-white text-[10px] px-1.5 py-0.2 rounded-full">0</span>
</button>
</div>
</header>
<!-- Основное окно чата -->
<main class="flex-1 flex flex-col max-w-4xl w-full mx-auto bg-white my-4 rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div id="chat-window" class="flex-1 p-6 overflow-y-auto space-y-4 bg-slate-50/50">
<div class="bg-white border border-slate-200 rounded-xl p-4 max-w-2xl shadow-sm">
<p class="text-xs font-bold text-indigo-600 uppercase tracking-wider mb-1">
<i class="fa-solid fa-robot mr-1"></i> ИИ-Ассистент
</p>
<p class="text-slate-700 text-sm leading-relaxed">
Привет! Я подключен к вашей базе задач. Вы можете писать мне в чат или управлять задачами в реестре справа.
</p>
</div>
</div>
<div class="p-4 bg-white border-t border-slate-200">
<form id="chat-form" onsubmit="sendMessage(event)" class="flex gap-2">
<input type="text" id="user-input" placeholder="Команда или вопрос (например: 'покажи все задачи')..."
class="flex-1 bg-slate-50 border border-slate-300 text-slate-800 rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:border-indigo-600 focus:bg-white transition">
<button type="submit" id="send-btn" class="bg-indigo-600 hover:bg-indigo-700 text-white font-medium px-5 py-2.5 rounded-lg text-sm transition flex items-center gap-2">
<span>Отправить</span>
<i class="fa-solid fa-paper-plane text-xs"></i>
</button>
</form>
</div>
</main>
<!-- Выезжающий справа реестр задач (Drawer) -->
<div id="drawer-backdrop" onclick="toggleDrawer()" class="fixed inset-0 bg-slate-900/30 backdrop-blur-sm hidden transition-opacity z-40"></div>
<aside id="task-drawer" class="fixed right-0 top-0 h-full w-[420px] bg-white border-l border-slate-200 shadow-2xl transform translate-x-full transition-transform duration-300 ease-in-out z-50 flex flex-col">
<!-- Заголовок панели -->
<div class="p-4 border-b border-slate-200 flex justify-between items-center bg-slate-50">
<h2 class="font-bold text-slate-800 flex items-center gap-2 text-sm">
<i class="fa-solid fa-list-check text-indigo-600"></i> Реестр задач
</h2>
<div class="flex items-center gap-2">
<button onclick="loadTasks()" class="text-xs text-slate-500 hover:text-indigo-600 transition p-1" title="Обновить">
<i class="fa-solid fa-rotate-right"></i>
</button>
<button onclick="toggleDrawer()" class="text-slate-400 hover:text-slate-700 transition p-1">
<i class="fa-solid fa-xmark text-lg"></i>
</button>
</div>
</div>
<!-- Фильтры по статусу -->
<div class="flex border-b border-slate-200 bg-white px-2 pt-2 text-xs font-medium text-slate-500 gap-1">
<button onclick="setFilter('ALL')" id="filter-ALL" class="px-3 py-1.5 rounded-t-lg border-b-2 border-indigo-600 text-indigo-600 font-bold">Все</button>
<button onclick="setFilter('IN_PROGRESS')" id="filter-IN_PROGRESS" class="px-3 py-1.5 rounded-t-lg border-b-2 border-transparent hover:text-slate-700">В работе</button>
<button onclick="setFilter('BACKLOG')" id="filter-BACKLOG" class="px-3 py-1.5 rounded-t-lg border-b-2 border-transparent hover:text-slate-700">Бэклог</button>
<button onclick="setFilter('COMPLETED')" id="filter-COMPLETED" class="px-3 py-1.5 rounded-t-lg border-b-2 border-transparent hover:text-slate-700">Завершено</button>
</div>
<!-- Список карточек -->
<div id="tasks-container" class="flex-1 overflow-y-auto p-4 space-y-3 bg-slate-50/50">
<div class="text-center text-slate-400 py-8 text-xs">Загрузка задач...</div>
</div>
</aside>
<script>
const API_TOKEN = "scud_secret_token_2026";
const SESSION_ID = "web_session_main";
let currentFilter = 'ALL';
let allTasks = [];
function toggleDrawer() {
const drawer = document.getElementById('task-drawer');
const backdrop = document.getElementById('drawer-backdrop');
const isHidden = drawer.classList.contains('translate-x-full');
if (isHidden) {
drawer.classList.remove('translate-x-full');
backdrop.classList.remove('hidden');
} else {
drawer.classList.add('translate-x-full');
backdrop.classList.add('hidden');
}
}
function setFilter(status) {
currentFilter = status;
['ALL', 'IN_PROGRESS', 'BACKLOG', 'COMPLETED'].forEach(f => {
const btn = document.getElementById(`filter-${f}`);
if (f === status) {
btn.className = "px-3 py-1.5 rounded-t-lg border-b-2 border-indigo-600 text-indigo-600 font-bold";
} else {
btn.className = "px-3 py-1.5 rounded-t-lg border-b-2 border-transparent hover:text-slate-700";
}
});
renderTasks();
}
async function loadTasks() {
try {
const res = await fetch('/api/v1/tasks', {
headers: { 'Authorization': `Bearer ${API_TOKEN}` }
});
allTasks = await res.json();
document.getElementById('task-count-badge').innerText = allTasks.length;
renderTasks();
} catch (err) {
document.getElementById('tasks-container').innerHTML = `<div class="text-red-500 text-xs py-4 text-center">Ошибка загрузки задач</div>`;
}
}
async function quickUpdateStatus(taskId, newStatus) {
try {
await fetch(`/api/v1/tasks/${taskId}/complete`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${API_TOKEN}` }
});
loadTasks();
} catch (e) {
console.error(e);
}
}
function renderTasks() {
const container = document.getElementById('tasks-container');
const filtered = allTasks.filter(t => currentFilter === 'ALL' || t.status === currentFilter);
if (filtered.length === 0) {
container.innerHTML = `<div class="text-slate-400 text-xs py-8 text-center">Нет задач с выбранным фильтром</div>`;
return;
}
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 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 = '';
if (t.due_date) {
dueDateHtml = `
<div class="mt-2 text-[11px] text-amber-800 bg-amber-50 border border-amber-200 px-2 py-0.5 rounded-md flex items-center gap-1.5 w-fit font-medium">
<i class="fa-solid fa-clock text-amber-600"></i>
<span>Срок: ${t.due_date}</span>
</div>
`;
}
return `
<div class="${cardBg} border border-slate-200 rounded-xl p-3.5 shadow-sm hover:shadow-md transition">
<div class="flex justify-between items-center mb-1.5">
<div class="flex items-center gap-1.5">
<span class="font-mono text-xs font-bold text-slate-900 bg-slate-100 px-2 py-0.5 rounded border border-slate-200">${t.task_id}</span>
<span class="text-[10px] uppercase px-1.5 py-0.5 rounded border ${priorityBadge}">${t.priority || 'HIGH'}</span>
</div>
<span class="text-[10px] uppercase px-2 py-0.5 rounded border ${statusBadge}">${t.status}</span>
</div>
<h3 class="text-xs font-semibold text-slate-800 mb-1 leading-snug">${t.title}</h3>
<div class="text-[10px] text-slate-400 font-mono flex items-center gap-1">
<i class="fa-solid fa-folder-closed text-slate-300"></i>
<span>${t.module}</span>
</div>
${dueDateHtml}
</div>
`;
}).join('');
}
async function sendMessage(e) {
e.preventDefault();
const input = document.getElementById('user-input');
const chatWindow = document.getElementById('chat-window');
const sendBtn = document.getElementById('send-btn');
const text = input.value.trim();
if (!text) return;
chatWindow.innerHTML += `
<div class="flex justify-end">
<div class="bg-indigo-600 text-white rounded-xl px-4 py-2.5 max-w-2xl text-sm shadow-sm">
${text}
</div>
</div>
`;
input.value = '';
chatWindow.scrollTop = chatWindow.scrollHeight;
sendBtn.disabled = true;
sendBtn.classList.add('opacity-50');
try {
const res = await fetch('/api/v1/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_TOKEN}`
},
body: JSON.stringify({ session_id: SESSION_ID, message: text })
});
const data = await res.json();
chatWindow.innerHTML += `
<div class="bg-white border border-slate-200 rounded-xl p-4 max-w-2xl shadow-sm">
<p class="text-xs font-bold text-indigo-600 uppercase tracking-wider mb-1"><i class="fa-solid fa-robot mr-1"></i> ИИ-Ассистент</p>
<p class="text-slate-700 text-sm whitespace-pre-wrap leading-relaxed">${data.reply}</p>
</div>
`;
chatWindow.scrollTop = chatWindow.scrollHeight;
loadTasks();
} catch (err) {
chatWindow.innerHTML += `
<div class="bg-red-50 border border-red-200 rounded-xl p-4 max-w-2xl text-red-700 text-sm">
Ошибка связи с сервером API.
</div>
`;
} finally {
sendBtn.disabled = false;
sendBtn.classList.remove('opacity-50');
}
}
loadTasks();
</script>
</body>
</html>