235 lines
9.4 KiB
Python
235 lines
9.4 KiB
Python
import json
|
||
import logging
|
||
from pathlib import Path
|
||
from typing import Optional, Dict, Any, List
|
||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||
from pydantic import BaseModel, Field
|
||
|
||
from app.auth import require_user
|
||
from app.database import get_db
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = APIRouter(prefix="/api/feedback", tags=["Feedback & Bug Tracker"])
|
||
|
||
# Path to workspace memory file for seamless Antigravity AI reading
|
||
MEMORY_REPORTS_FILE = Path("/home/ai-projects/memory/USER_REPORTS.md")
|
||
|
||
|
||
class FeedbackCreateRequest(BaseModel):
|
||
category: str = Field(default="bug", description="Category: 'bug', 'feature', 'note'")
|
||
message: str = Field(..., min_length=1, max_length=5000)
|
||
context_url: Optional[str] = Field(default="", max_length=500)
|
||
context_data: Optional[Dict[str, Any]] = None
|
||
|
||
|
||
class FeedbackStatusUpdateRequest(BaseModel):
|
||
status: str = Field(..., pattern="^(new|in_progress|resolved|closed)$")
|
||
|
||
|
||
def sync_reports_to_markdown(conn):
|
||
"""
|
||
Synchronizes feedback_reports from SQLite to /home/ai-projects/memory/USER_REPORTS.md.
|
||
This enables Antigravity and AI agents to instantly inspect user-reported bugs and feature requests.
|
||
"""
|
||
try:
|
||
rows = conn.execute("""
|
||
SELECT f.id, f.category, f.message, f.context_url, f.context_data_json, f.status, f.created_at, f.updated_at, u.username
|
||
FROM feedback_reports f
|
||
JOIN users u ON f.user_id = u.id
|
||
ORDER BY f.id DESC
|
||
""").fetchall()
|
||
|
||
lines = [
|
||
"# 📋 Пользовательские репорты & Запросы на доработку (study.qpid.pro)",
|
||
"",
|
||
"> 🔄 Данный файл автоматически синхронизируется с виджетом обратной связи на сайте.",
|
||
"> AI-ассистент (Antigravity) считывает этот файл для анализа найденных багов и планирования задач.",
|
||
"",
|
||
f"**Всего репортов:** {len(rows)} | **Последняя синхронизация:** сейчас",
|
||
"",
|
||
"## 🔴 Активные репорты (Требуют внимания / В работе)",
|
||
"",
|
||
]
|
||
|
||
active_rows = [r for r in rows if r["status"] in ("new", "in_progress")]
|
||
resolved_rows = [r for r in rows if r["status"] in ("resolved", "closed")]
|
||
|
||
if not active_rows:
|
||
lines.append("🎉 *Активных багов или задач нет! Все известные репорты закрыты.*")
|
||
lines.append("")
|
||
else:
|
||
for r in active_rows:
|
||
cat_emoji = "🐛 БАГ" if r["category"] == "bug" else ("💡 ФИЧА" if r["category"] == "feature" else "📝 ЗАМЕТКА")
|
||
status_icon = "🆕 Новый" if r["status"] == "new" else "⏳ В работе"
|
||
|
||
ctx_extra = ""
|
||
if r["context_data_json"]:
|
||
try:
|
||
c_dict = json.loads(r["context_data_json"])
|
||
parts = []
|
||
if "screen" in c_dict: parts.append(f"Экран: {c_dict['screen']}")
|
||
if "device" in c_dict: parts.append(f"Устройство: {c_dict['device']}")
|
||
if parts: ctx_extra = f" ({', '.join(parts)})"
|
||
except Exception:
|
||
pass
|
||
|
||
lines.append(f"### #{r['id']} [{cat_emoji}] — {status_icon} (`{r['created_at']}`)")
|
||
lines.append(f"- **Пользователь**: `{r['username']}`")
|
||
if r["context_url"]:
|
||
lines.append(f"- **Раздел/Страница**: `{r['context_url']}`{ctx_extra}")
|
||
lines.append(f"- **Сообщение**:\n> {r['message'].strip()}")
|
||
lines.append("")
|
||
|
||
lines.extend([
|
||
"---",
|
||
"",
|
||
"## ✅ Решенные / Закрытые задачи",
|
||
"",
|
||
])
|
||
|
||
if not resolved_rows:
|
||
lines.append("*Пока нет закрытых репортов.*")
|
||
lines.append("")
|
||
else:
|
||
for r in resolved_rows:
|
||
cat_emoji = "🐛" if r["category"] == "bug" else ("💡" if r["category"] == "feature" else "📝")
|
||
lines.append(f"- [x] **#{r['id']} {cat_emoji} ({r['created_at']})**: {r['message'].strip()} *(Страница: {r['context_url'] or 'общая'})*")
|
||
lines.append("")
|
||
|
||
MEMORY_REPORTS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||
MEMORY_REPORTS_FILE.write_text("\n".join(lines), encoding="utf-8")
|
||
except Exception as e:
|
||
logger.warning(f"Could not sync reports to markdown: {e}")
|
||
|
||
|
||
@router.get("", response_model=List[Dict[str, Any]])
|
||
def list_feedback(
|
||
category: Optional[str] = Query(None),
|
||
status_filter: Optional[str] = Query(None, alias="status"),
|
||
user: dict = Depends(require_user),
|
||
):
|
||
"""Lists feedback reports. Admins see all reports; regular users see their own."""
|
||
is_admin = bool(user.get("is_admin"))
|
||
with get_db() as conn:
|
||
query = """
|
||
SELECT f.id, f.user_id, f.category, f.message, f.context_url, f.context_data_json, f.status, f.created_at, f.updated_at, u.username
|
||
FROM feedback_reports f
|
||
JOIN users u ON f.user_id = u.id
|
||
WHERE 1=1
|
||
"""
|
||
params = []
|
||
if not is_admin:
|
||
query += " AND f.user_id = ?"
|
||
params.append(user["id"])
|
||
|
||
if category:
|
||
query += " AND f.category = ?"
|
||
params.append(category)
|
||
|
||
if status_filter:
|
||
query += " AND f.status = ?"
|
||
params.append(status_filter)
|
||
|
||
query += " ORDER BY f.id DESC LIMIT 100"
|
||
rows = conn.execute(query, params).fetchall()
|
||
|
||
result = []
|
||
for r in rows:
|
||
ctx_data = {}
|
||
if r["context_data_json"]:
|
||
try:
|
||
ctx_data = json.loads(r["context_data_json"])
|
||
except Exception:
|
||
pass
|
||
|
||
result.append({
|
||
"id": r["id"],
|
||
"user_id": r["user_id"],
|
||
"username": r["username"],
|
||
"category": r["category"],
|
||
"message": r["message"],
|
||
"context_url": r["context_url"],
|
||
"context_data": ctx_data,
|
||
"status": r["status"],
|
||
"created_at": r["created_at"],
|
||
"updated_at": r["updated_at"],
|
||
})
|
||
return result
|
||
|
||
|
||
@router.post("", status_code=status.HTTP_201_CREATED)
|
||
def create_feedback(
|
||
payload: FeedbackCreateRequest,
|
||
user: dict = Depends(require_user),
|
||
):
|
||
"""Submits a new bug, feature request, or note from the website chat widget."""
|
||
allowed_categories = {"bug", "feature", "note"}
|
||
category = payload.category if payload.category in allowed_categories else "bug"
|
||
ctx_json = json.dumps(payload.context_data or {}, ensure_ascii=False)
|
||
|
||
with get_db() as conn:
|
||
cursor = conn.execute(
|
||
"""
|
||
INSERT INTO feedback_reports (user_id, category, message, context_url, context_data_json, status)
|
||
VALUES (?, ?, ?, ?, ?, 'new')
|
||
""",
|
||
(user["id"], category, payload.message.strip(), payload.context_url or "", ctx_json),
|
||
)
|
||
report_id = cursor.lastrowid
|
||
sync_reports_to_markdown(conn)
|
||
|
||
return {
|
||
"ok": True,
|
||
"id": report_id,
|
||
"category": category,
|
||
"status": "new",
|
||
"message": "Репорт успешно отправлен и передан в систему задач",
|
||
}
|
||
|
||
|
||
@router.patch("/{report_id}")
|
||
def update_feedback_status(
|
||
report_id: int,
|
||
payload: FeedbackStatusUpdateRequest,
|
||
user: dict = Depends(require_user),
|
||
):
|
||
"""Updates the status of a report (e.g. 'in_progress', 'resolved')."""
|
||
with get_db() as conn:
|
||
existing = conn.execute("SELECT id, user_id FROM feedback_reports WHERE id = ?", (report_id,)).fetchone()
|
||
if not existing:
|
||
raise HTTPException(status_code=404, detail="Репорт не найден")
|
||
|
||
is_admin = bool(user.get("is_admin"))
|
||
if not is_admin and existing["user_id"] != user["id"]:
|
||
raise HTTPException(status_code=403, detail="Недостаточно прав")
|
||
|
||
conn.execute(
|
||
"UPDATE feedback_reports SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||
(payload.status, report_id),
|
||
)
|
||
sync_reports_to_markdown(conn)
|
||
|
||
return {"ok": True, "id": report_id, "status": payload.status}
|
||
|
||
|
||
@router.delete("/{report_id}")
|
||
def delete_feedback(
|
||
report_id: int,
|
||
user: dict = Depends(require_user),
|
||
):
|
||
"""Deletes a report."""
|
||
with get_db() as conn:
|
||
existing = conn.execute("SELECT id, user_id FROM feedback_reports WHERE id = ?", (report_id,)).fetchone()
|
||
if not existing:
|
||
raise HTTPException(status_code=404, detail="Репорт не найден")
|
||
|
||
is_admin = bool(user.get("is_admin"))
|
||
if not is_admin and existing["user_id"] != user["id"]:
|
||
raise HTTPException(status_code=403, detail="Недостаточно прав")
|
||
|
||
conn.execute("DELETE FROM feedback_reports WHERE id = ?", (report_id,))
|
||
sync_reports_to_markdown(conn)
|
||
|
||
return {"ok": True, "id": report_id}
|