v1.6.0: max_bot fixes — feature keys, flush→commit, test-run, categories, broadcast page, proxy error handling, deploy scripts
This commit is contained in:
@@ -49,7 +49,8 @@ def ctx(request, user, **kw):
|
||||
"role_permissions": getattr(request.state, 'role_permissions', {}),
|
||||
"role_override_active": original_user is not None,
|
||||
"original_role": original_user["role"] if original_user else None,
|
||||
"available_roles": available_roles.get(user["role"], [user["role"]]) if user else []}
|
||||
"available_roles": available_roles.get(user["role"], [user["role"]]) if user else [],
|
||||
"page_name": kw.get("active_page", "")}
|
||||
c.update(kw)
|
||||
return c
|
||||
|
||||
@@ -76,11 +77,13 @@ async def users_create(request: Request, db: AsyncSession = Depends(get_db), use
|
||||
if not login or not password:
|
||||
return RedirectResponse(url="/service/users", status_code=302)
|
||||
from app.auth import hash_password
|
||||
max_id_val = form.get("max_user_id")
|
||||
new_user = User(
|
||||
login=login,
|
||||
password_hash=hash_password(password),
|
||||
full_name=form.get("full_name"), role=form.get("role", "technician"),
|
||||
phone=form.get("phone", ""), email=form.get("email", ""),
|
||||
max_user_id=int(max_id_val) if max_id_val and max_id_val.strip() else None,
|
||||
is_active=form.get("is_active", "1") == "1",
|
||||
)
|
||||
db.add(new_user); await db.flush()
|
||||
@@ -102,6 +105,8 @@ async def users_edit(request: Request, db: AsyncSession = Depends(get_db), user:
|
||||
u.role = form.get("role", u.role)
|
||||
u.phone = form.get("phone", u.phone)
|
||||
u.email = form.get("email", u.email)
|
||||
max_id_val = form.get("max_user_id")
|
||||
u.max_user_id = int(max_id_val) if max_id_val and max_id_val.strip() else None
|
||||
if "is_active" in form:
|
||||
u.is_active = form["is_active"] == "1"
|
||||
from app.auth import hash_password
|
||||
@@ -1243,104 +1248,85 @@ import aiohttp
|
||||
|
||||
MAX_BOT_URL = "http://127.0.0.1:8002"
|
||||
|
||||
async def _proxy(method, path, json_body=None):
|
||||
try:
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30)) as session:
|
||||
async with session.request(method, f"{MAX_BOT_URL}{path}", json=json_body) as resp:
|
||||
return JSONResponse(await resp.json())
|
||||
except aiohttp.ClientError as e:
|
||||
return JSONResponse({"error": f"Bot service unavailable: {e}", "ok": False}, status_code=503)
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e), "ok": False}, status_code=500)
|
||||
|
||||
|
||||
@router.get("/service/api/bot/settings")
|
||||
async def api_bot_settings(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"{MAX_BOT_URL}/api/bot/settings") as resp:
|
||||
return JSONResponse(await resp.json())
|
||||
return await _proxy("GET", "/api/bot/settings")
|
||||
|
||||
@router.post("/service/api/bot/settings/save")
|
||||
async def api_bot_settings_save(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(f"{MAX_BOT_URL}/api/bot/settings/save", json=body) as resp:
|
||||
return JSONResponse(await resp.json())
|
||||
return await _proxy("POST", "/api/bot/settings/save", body)
|
||||
|
||||
@router.get("/service/api/bot/templates")
|
||||
async def api_bot_templates(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"{MAX_BOT_URL}/api/bot/templates") as resp:
|
||||
return JSONResponse(await resp.json())
|
||||
return await _proxy("GET", "/api/bot/templates")
|
||||
|
||||
@router.post("/service/api/bot/templates/save")
|
||||
async def api_bot_templates_save(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(f"{MAX_BOT_URL}/api/bot/templates/save", json=body) as resp:
|
||||
return JSONResponse(await resp.json())
|
||||
return await _proxy("POST", "/api/bot/templates/save", body)
|
||||
|
||||
@router.get("/service/api/bot/features")
|
||||
async def api_bot_features(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"{MAX_BOT_URL}/api/bot/features") as resp:
|
||||
return JSONResponse(await resp.json())
|
||||
return await _proxy("GET", "/api/bot/features")
|
||||
|
||||
@router.post("/service/api/bot/features/toggle")
|
||||
async def api_bot_features_toggle(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(f"{MAX_BOT_URL}/api/bot/features/toggle", json=body) as resp:
|
||||
return JSONResponse(await resp.json())
|
||||
return await _proxy("POST", "/api/bot/features/toggle", body)
|
||||
|
||||
@router.get("/service/api/bot/categories")
|
||||
async def api_bot_categories(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"{MAX_BOT_URL}/api/bot/categories") as resp:
|
||||
return JSONResponse(await resp.json())
|
||||
return await _proxy("GET", "/api/bot/categories")
|
||||
|
||||
@router.post("/service/api/bot/categories/create")
|
||||
async def api_bot_categories_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(f"{MAX_BOT_URL}/api/bot/categories/create", json=body) as resp:
|
||||
return JSONResponse(await resp.json())
|
||||
return await _proxy("POST", "/api/bot/categories/create", body)
|
||||
|
||||
@router.post("/service/api/bot/categories/edit")
|
||||
async def api_bot_categories_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(f"{MAX_BOT_URL}/api/bot/categories/edit", json=body) as resp:
|
||||
return JSONResponse(await resp.json())
|
||||
return await _proxy("POST", "/api/bot/categories/edit", body)
|
||||
|
||||
@router.post("/service/api/bot/categories/delete")
|
||||
async def api_bot_categories_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(f"{MAX_BOT_URL}/api/bot/categories/delete", json=body) as resp:
|
||||
return JSONResponse(await resp.json())
|
||||
return await _proxy("POST", "/api/bot/categories/delete", body)
|
||||
|
||||
@router.get("/service/api/bot/kb")
|
||||
async def api_bot_kb(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"{MAX_BOT_URL}/api/bot/kb") as resp:
|
||||
return JSONResponse(await resp.json())
|
||||
return await _proxy("GET", "/api/bot/kb")
|
||||
|
||||
@router.post("/service/api/bot/kb/create")
|
||||
async def api_bot_kb_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(f"{MAX_BOT_URL}/api/bot/kb/create", json=body) as resp:
|
||||
return JSONResponse(await resp.json())
|
||||
return await _proxy("POST", "/api/bot/kb/create", body)
|
||||
|
||||
@router.post("/service/api/bot/kb/edit")
|
||||
async def api_bot_kb_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(f"{MAX_BOT_URL}/api/bot/kb/edit", json=body) as resp:
|
||||
return JSONResponse(await resp.json())
|
||||
return await _proxy("POST", "/api/bot/kb/edit", body)
|
||||
|
||||
@router.post("/service/api/bot/kb/delete")
|
||||
async def api_bot_kb_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(f"{MAX_BOT_URL}/api/bot/kb/delete", json=body) as resp:
|
||||
return JSONResponse(await resp.json())
|
||||
return await _proxy("POST", "/api/bot/kb/delete", body)
|
||||
|
||||
@router.get("/service/api/bot/conversations")
|
||||
async def api_bot_conversations(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"{MAX_BOT_URL}/api/bot/conversations") as resp:
|
||||
return JSONResponse(await resp.json())
|
||||
return await _proxy("GET", "/api/bot/conversations")
|
||||
|
||||
@router.get("/service/api/bot/users")
|
||||
async def api_bot_users(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner")),
|
||||
@@ -1350,22 +1336,16 @@ async def api_bot_users(db: AsyncSession = Depends(get_db), user: dict = Depends
|
||||
params = f"?page={page}&limit={limit}&sort_by={sort_by}&sort_dir={sort_dir}"
|
||||
if consent:
|
||||
params += f"&consent={consent}"
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"{MAX_BOT_URL}/api/bot/users{params}") as resp:
|
||||
return JSONResponse(await resp.json())
|
||||
return await _proxy("GET", f"/api/bot/users{params}")
|
||||
|
||||
@router.get("/service/api/bot/analytics")
|
||||
async def api_bot_analytics(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"{MAX_BOT_URL}/api/bot/analytics") as resp:
|
||||
return JSONResponse(await resp.json())
|
||||
return await _proxy("GET", "/api/bot/analytics")
|
||||
|
||||
@router.post("/service/api/bot/test-run")
|
||||
async def api_bot_test_run(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(f"{MAX_BOT_URL}/api/bot/test-run", json=body) as resp:
|
||||
return JSONResponse(await resp.json())
|
||||
return await _proxy("POST", "/api/bot/test-run", body)
|
||||
|
||||
@router.get("/service/api/bot/broadcast/recipients")
|
||||
async def api_bot_broadcast_recipients(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner")),
|
||||
@@ -1373,20 +1353,221 @@ async def api_bot_broadcast_recipients(db: AsyncSession = Depends(get_db), user:
|
||||
params = f"?page={page}&limit={limit}"
|
||||
if q:
|
||||
params += f"&q={q}"
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"{MAX_BOT_URL}/api/bot/broadcast/recipients{params}") as resp:
|
||||
return JSONResponse(await resp.json())
|
||||
return await _proxy("GET", f"/api/bot/broadcast/recipients{params}")
|
||||
|
||||
@router.get("/service/api/bot/broadcast/history")
|
||||
async def api_bot_broadcast_history(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner")),
|
||||
page: int = Query(1), limit: int = Query(20)):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"{MAX_BOT_URL}/api/bot/broadcast/history?page={page}&limit={limit}") as resp:
|
||||
return JSONResponse(await resp.json())
|
||||
return await _proxy("GET", f"/api/bot/broadcast/history?page={page}&limit={limit}")
|
||||
|
||||
@router.post("/service/api/bot/broadcast")
|
||||
async def api_bot_broadcast(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(f"{MAX_BOT_URL}/api/bot/broadcast", json=body) as resp:
|
||||
return JSONResponse(await resp.json())
|
||||
return await _proxy("POST", "/api/bot/broadcast", body)
|
||||
|
||||
|
||||
@router.get("/service/bot-settings/handoffs")
|
||||
async def bot_handoffs_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return templates.TemplateResponse("pages/bot_handoffs.html", ctx(request, user=user, active_page="bot-handoffs", title="Handoff-диалоги"))
|
||||
|
||||
|
||||
@router.get("/service/bot-settings/handoffs/poll")
|
||||
async def bot_handoffs_poll(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return await _proxy("GET", "/api/bot/handoffs")
|
||||
|
||||
|
||||
@router.post("/service/bot-settings/handoffs/{handoff_id}/take")
|
||||
async def bot_handoff_take(handoff_id: int, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return await _proxy("POST", f"/api/bot/handoffs/{handoff_id}/take")
|
||||
|
||||
|
||||
@router.get("/service/bot-settings/notifications")
|
||||
async def bot_notifications_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return templates.TemplateResponse("pages/bot_notifications.html", ctx(request, user=user, active_page="bot-notifications", title="Уведомления бота"))
|
||||
|
||||
|
||||
@router.get("/service/bot-settings/notifications/data")
|
||||
async def bot_notifications_data(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return await _proxy("GET", "/api/bot/notifications")
|
||||
|
||||
|
||||
@router.post("/service/bot-settings/notifications/{notif_id}/read")
|
||||
async def bot_notification_read(notif_id: int, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return await _proxy("POST", f"/api/bot/notifications/{notif_id}/read")
|
||||
|
||||
|
||||
# ===================== BOT RISK QUESTIONS =====================
|
||||
|
||||
|
||||
@router.get("/service/bot-settings/risk-questions")
|
||||
async def bot_risk_questions_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return templates.TemplateResponse("pages/bot_risk_questions.html", ctx(request, user=user, active_page="bot-risk-questions", title="Вопросы риск-инжиниринга"))
|
||||
|
||||
|
||||
@router.get("/service/bot-settings/storage")
|
||||
async def bot_storage_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return templates.TemplateResponse("pages/bot_storage.html", ctx(request, user=user, active_page="bot-storage", title="Яндекс.Диск"))
|
||||
|
||||
|
||||
@router.get("/service/bot-settings/broadcasts")
|
||||
async def bot_broadcasts_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return templates.TemplateResponse("pages/bot_broadcasts.html", ctx(request, user=user, active_page="bot-broadcasts", title="Рассылки"))
|
||||
|
||||
|
||||
@router.get("/service/api/bot/risk-questions")
|
||||
async def api_bot_risk_questions(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return await _proxy("GET", "/api/bot/risk-questions")
|
||||
|
||||
|
||||
@router.post("/service/api/bot/risk-questions/create")
|
||||
async def api_bot_risk_questions_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
return await _proxy("POST", "/api/bot/risk-questions/create", body)
|
||||
|
||||
|
||||
@router.post("/service/api/bot/risk-questions/edit")
|
||||
async def api_bot_risk_questions_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
return await _proxy("POST", "/api/bot/risk-questions/edit", body)
|
||||
|
||||
|
||||
@router.post("/service/api/bot/risk-questions/toggle")
|
||||
async def api_bot_risk_questions_toggle(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
return await _proxy("POST", "/api/bot/risk-questions/toggle", body)
|
||||
|
||||
|
||||
@router.post("/service/api/bot/risk-questions/delete")
|
||||
async def api_bot_risk_questions_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
return await _proxy("POST", "/api/bot/risk-questions/delete", body)
|
||||
|
||||
|
||||
# ===================== BOT TICKETS =====================
|
||||
|
||||
|
||||
@router.get("/service/bot-settings/tickets")
|
||||
async def bot_tickets_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return templates.TemplateResponse("pages/bot_tickets.html", ctx(request, user=user, active_page="bot-tickets", title="Заявки"))
|
||||
|
||||
|
||||
@router.get("/service/api/bot/tickets")
|
||||
async def api_bot_tickets(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner")),
|
||||
status: str = Query(None), priority: str = Query(None)):
|
||||
params = ""
|
||||
if status: params += f"status={status}&"
|
||||
if priority: params += f"priority={priority}"
|
||||
return await _proxy("GET", f"/api/bot/tickets?{params}")
|
||||
|
||||
|
||||
@router.get("/service/api/bot/tickets/{ticket_id}")
|
||||
async def api_bot_ticket_get(ticket_id: int, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return await _proxy("GET", f"/api/bot/tickets/{ticket_id}")
|
||||
|
||||
|
||||
@router.post("/service/api/bot/tickets/create")
|
||||
async def api_bot_tickets_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
return await _proxy("POST", "/api/bot/tickets/create", body)
|
||||
|
||||
|
||||
@router.post("/service/api/bot/tickets/{ticket_id}/message")
|
||||
async def api_bot_tickets_message(ticket_id: int, request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
return await _proxy("POST", f"/api/bot/tickets/{ticket_id}/message", body)
|
||||
|
||||
|
||||
@router.post("/service/api/bot/tickets/{ticket_id}/status")
|
||||
async def api_bot_tickets_status(ticket_id: int, request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
body = await request.json()
|
||||
return await _proxy("POST", f"/api/bot/tickets/{ticket_id}/status", body)
|
||||
|
||||
|
||||
@router.get("/service/api/bot/engineers")
|
||||
async def api_bot_engineers(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
|
||||
return await _proxy("GET", "/api/bot/engineers")
|
||||
|
||||
|
||||
# ===================== BOT STORAGE (Yandex Disk) =====================
|
||||
|
||||
async def _get_yandex_client(db: AsyncSession):
|
||||
from app.yandex_disk import YandexDiskClient
|
||||
result = await db.execute(text("SELECT value FROM bot_settings WHERE key = 'yandex_disk_token'"))
|
||||
row = result.scalar_one_or_none()
|
||||
if not row:
|
||||
return None
|
||||
return YandexDiskClient(row)
|
||||
|
||||
|
||||
@router.get("/service/api/bot/storage/list")
|
||||
async def api_bot_storage_list(path: str = Query(""), db: AsyncSession = Depends(get_db),
|
||||
user: dict = Depends(require_role("owner"))):
|
||||
client = await _get_yandex_client(db)
|
||||
if not client:
|
||||
return JSONResponse({"error": "Yandex Disk not configured"}, status_code=400)
|
||||
try:
|
||||
items = await client.list_folder(path)
|
||||
return JSONResponse(items)
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
|
||||
@router.get("/service/api/bot/storage/search")
|
||||
async def api_bot_storage_search(query: str = Query(""), db: AsyncSession = Depends(get_db),
|
||||
user: dict = Depends(require_role("owner"))):
|
||||
client = await _get_yandex_client(db)
|
||||
if not client:
|
||||
return JSONResponse({"error": "Yandex Disk not configured"}, status_code=400)
|
||||
try:
|
||||
items = await client.search(query)
|
||||
return JSONResponse(items)
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
|
||||
@router.get("/service/api/bot/storage/public-link")
|
||||
async def api_bot_storage_public_link(path: str = Query(""), expiry: int = Query(30),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: dict = Depends(require_role("owner"))):
|
||||
client = await _get_yandex_client(db)
|
||||
if not client:
|
||||
return JSONResponse({"error": "Yandex Disk not configured"}, status_code=400)
|
||||
try:
|
||||
url = await client.get_public_url(path, expiry)
|
||||
return JSONResponse({"url": url})
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
|
||||
@router.get("/service/api/bot/storage/download")
|
||||
async def api_bot_storage_download(path: str = Query(""), db: AsyncSession = Depends(get_db),
|
||||
user: dict = Depends(require_role("owner"))):
|
||||
client = await _get_yandex_client(db)
|
||||
if not client:
|
||||
return JSONResponse({"error": "Yandex Disk not configured"}, status_code=400)
|
||||
try:
|
||||
info = await client.get_file_info(path)
|
||||
dl_url = info.get("public_url", "")
|
||||
if not dl_url:
|
||||
dl_info = await client._request("GET", "/resources/download", params={"path": path})
|
||||
dl_url = dl_info.get("href", "")
|
||||
return JSONResponse({"url": dl_url})
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
|
||||
# ===================== HANDOFF CLOSE =====================
|
||||
|
||||
|
||||
@router.post("/service/api/bot/handoffs/{handoff_id}/close")
|
||||
async def api_bot_handoff_close(handoff_id: int, db: AsyncSession = Depends(get_db),
|
||||
user: dict = Depends(require_role("owner"))):
|
||||
return await _proxy("POST", f"/api/bot/handoffs/{handoff_id}/close")
|
||||
@@ -0,0 +1,129 @@
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="logo sidebar-logo-text">Aegis<span>One</span></div>
|
||||
<div style="font-size:12px;color:var(--text-muted);letter-spacing:0.05em;">Service Portal</div>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<div class="sidebar-section">Панель</div>
|
||||
<a href="/service/dashboard" class="{% if active_page == 'dashboard' %}active{% endif %}">Дашборд</a>
|
||||
{% if role_permissions.get('charts', True) != False %}<a href="/service/charts" class="{% if active_page == 'charts' %}active{% endif %}">Графики</a>{% endif %}
|
||||
{% if role_permissions.get('ceo', True) != False %}<a href="/service/ceo" class="{% if active_page == 'ceo' %}active{% endif %}">CEO дашборд</a>{% endif %}
|
||||
|
||||
<div class="sidebar-section">Управление</div>
|
||||
{% if role_permissions.get('customers', True) != False %}<a href="/service/customers" class="{% if active_page == 'customers' %}active{% endif %}">Клиенты</a>{% endif %}
|
||||
{% if role_permissions.get('objects', True) != False %}<a href="/service/objects" class="{% if active_page == 'objects' %}active{% endif %}">Объекты</a>{% endif %}
|
||||
{% if role_permissions.get('sla', True) != False %}<a href="/service/sla" class="{% if active_page == 'sla' %}active{% endif %}">SLA контракты</a>{% endif %}
|
||||
{% if role_permissions.get('questionnaire', True) != False %}<a href="/service/questionnaire" class="{% if active_page == 'questionnaire' %}active{% endif %}">Опросник</a>{% endif %}
|
||||
{% if role_permissions.get('passports', True) != False %}<a href="/service/passports" class="{% if active_page == 'passports' %}active{% endif %}">Паспорта объектов</a>{% endif %}
|
||||
|
||||
<div class="sidebar-section">Работа</div>
|
||||
{% if role_permissions.get('users', True) != False %}<a href="/service/users" class="{% if active_page == 'users' %}active{% endif %}">Сотрудники</a>{% endif %}
|
||||
{% if role_permissions.get('assignments', True) != False %}<a href="/service/assignments" class="{% if active_page == 'assignments' %}active{% endif %}">Назначение сотрудников</a>{% endif %}
|
||||
{% if role_permissions.get('docs', True) != False %}<a href="/service/documents/" class="{% if active_page == 'docs' %}active{% endif %}">Документация</a>{% endif %}
|
||||
{% if role_permissions.get('tasks', True) != False %}<a href="/service/tasks" class="{% if active_page == 'tasks' %}active{% endif %}">Задачи</a>{% endif %}
|
||||
{% if role_permissions.get('reports', True) != False %}<a href="/service/reports" class="{% if active_page == 'reports' %}active{% endif %}">Отчёты</a>{% endif %}
|
||||
{% if role_permissions.get('incidents', True) != False %}<a href="/service/incidents" class="{% if active_page == 'incidents' %}active{% endif %}">Инциденты</a>{% endif %}
|
||||
{% if role_permissions.get('checklist', True) != False %}<a href="/service/checklist" class="{% if active_page == 'checklist' %}active{% endif %}">Чек-лист</a>{% endif %}
|
||||
{% if role_permissions.get('tech_access', True) != False %}<a href="/service/documents/tech-access" class="{% if active_page == 'tech-access' %}active{% endif %}">Доступ техников</a>{% endif %}
|
||||
|
||||
<div class="sidebar-section">Чат-бот Max</div>
|
||||
{% if role_permissions.get('bot_analytics', True) != False %}<a href="/service/bot-settings/analytics" class="{% if active_page == 'bot-analytics' %}active{% endif %}">Аналитика</a>{% endif %}
|
||||
{% if role_permissions.get('bot_users', True) != False %}<a href="/service/bot-settings/users" class="{% if active_page == 'bot-users' %}active{% endif %}">Пользователи бота</a>{% endif %}
|
||||
{% if role_permissions.get('bot_conversations', True) != False %}<a href="/service/bot-settings/conversations" class="{% if active_page == 'bot-conversations' %}active{% endif %}">Диалоги бота</a>{% endif %}
|
||||
{% if role_permissions.get('bot_tickets', True) != False %}<a href="/service/bot-settings/tickets" class="{% if active_page == 'bot-tickets' %}active{% endif %}">Заявки</a>{% endif %}
|
||||
{% if role_permissions.get('bot_handoffs', True) != False %}<a href="/service/bot-settings/handoffs" class="{% if active_page == 'bot-handoffs' %}active{% endif %}">Handoff-диалоги</a>{% endif %}
|
||||
{% if role_permissions.get('bot_notifications', True) != False %}<a href="/service/bot-settings/notifications" class="{% if active_page == 'bot-notifications' %}active{% endif %}">Уведомления</a>{% endif %}
|
||||
{% if role_permissions.get('bot_risk_questions', True) != False %}<a href="/service/bot-settings/risk-questions" class="{% if active_page == 'bot-risk-questions' %}active{% endif %}">Вопросы риск-инжиниринга</a>{% endif %}
|
||||
{% if role_permissions.get('bot_storage', True) != False %}<a href="/service/bot-settings/storage" class="{% if active_page == 'bot-storage' %}active{% endif %}">Яндекс.Диск</a>{% endif %}
|
||||
{% if role_permissions.get('bot_broadcasts', True) != False %}<a href="/service/bot-settings/broadcasts" class="{% if active_page == 'bot-broadcasts' %}active{% endif %}">Рассылки</a>{% endif %}
|
||||
|
||||
<div class="sidebar-section">Контент</div>
|
||||
{% if role_permissions.get('blog', True) != False %}<a href="/service/blog" class="{% if active_page == 'blog' %}active{% endif %}">Управление блогом</a>{% endif %}
|
||||
{% if role_permissions.get('cases', True) != False %}<a href="/service/cases" class="{% if active_page == 'cases' %}active{% endif %}">Примеры из практики</a>{% endif %}
|
||||
|
||||
<div class="sidebar-section">Внешние системы</div>
|
||||
<a href="https://git.aegisone.ru" target="_blank" rel="noopener">Gitea</a>
|
||||
<a href="{{ PORTAINER_URL }}" target="_blank" rel="noopener">Portainer</a>
|
||||
|
||||
<div class="sidebar-section">Настройки</div>
|
||||
<a href="/service/questionnaire-config" class="{% if active_page == 'questionnaire-config' %}active{% endif %}">Настройка опросника</a>
|
||||
<a href="/service/formulas" class="{% if active_page == 'formulas' %}active{% endif %}">Формулы</a>
|
||||
<a href="/service/bot-settings" class="{% if active_page == 'bot-settings' %}active{% endif %}">Чат-бот Max</a>
|
||||
<a href="/service/portal-settings" class="{% if active_page == 'portal-settings' %}active{% endif %}">Настройка портала</a>
|
||||
<a href="/service/role-settings" class="{% if active_page == 'role-settings' %}active{% endif %}">Настройка Ролей</a>
|
||||
<a href="/service/ideas" class="{% if active_page == 'ideas' %}active{% endif %}">Идеи</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-name">{{ user.full_name }}</div>
|
||||
<a href="/service/logout" style="display:inline-block;margin:6px 0;font-size:12px;">Выйти</a>
|
||||
<div class="version-pill" onclick="openChangelog()" title="История изменений">v{{ app_version }}</div>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="modal-overlay" id="changelog-modal"><div class="modal">
|
||||
<button type="button" class="modal-close" onclick="closeChangelog()">✕</button>
|
||||
<h2>История изменений</h2>
|
||||
<div id="changelog-content" class="doc-content" style="padding:0;background:transparent;border:none;"></div>
|
||||
</div></div>
|
||||
|
||||
<script>
|
||||
function openChangelog(){
|
||||
var modal = document.getElementById('changelog-modal');
|
||||
modal.classList.add('open');
|
||||
var content = document.getElementById('changelog-content');
|
||||
if(!content.dataset.loaded){
|
||||
content.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
||||
fetch('/service/api/changelog').then(function(r){ return r.json() }).then(function(data){
|
||||
content.innerHTML = data.html || '<p>Нет данных</p>';
|
||||
content.dataset.loaded = '1';
|
||||
});
|
||||
}
|
||||
}
|
||||
function closeChangelog(){
|
||||
document.getElementById('changelog-modal').classList.remove('open');
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
var nav = document.querySelector('.navbar__menu');
|
||||
if(!nav) return;
|
||||
var sidebarLinks = document.querySelectorAll('.sidebar-nav a[href]');
|
||||
var saved = JSON.parse(localStorage.getItem('sidebar_favorites') || '[]');
|
||||
sidebarLinks.forEach(function(link){
|
||||
var star = document.createElement('span');
|
||||
star.className = 'sidebar-star';
|
||||
var url = link.getAttribute('href');
|
||||
star.textContent = saved.indexOf(url) !== -1 ? '\u2605' : '\u2606';
|
||||
star.style.cssText = 'cursor:pointer;margin-right:6px;font-size:14px;user-select:none;';
|
||||
star.addEventListener('click', function(e){
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
var favs = JSON.parse(localStorage.getItem('sidebar_favorites') || '[]');
|
||||
var idx = favs.indexOf(url);
|
||||
if(idx !== -1){ favs.splice(idx,1); star.textContent = '\u2606'; }
|
||||
else { favs.push(url); star.textContent = '\u2605'; }
|
||||
localStorage.setItem('sidebar_favorites', JSON.stringify(favs));
|
||||
renderFavorites();
|
||||
});
|
||||
link.insertBefore(star, link.firstChild);
|
||||
});
|
||||
function renderFavorites(){
|
||||
var existing = nav.querySelectorAll('.nav-fav');
|
||||
existing.forEach(function(e){ e.remove(); });
|
||||
var favs = JSON.parse(localStorage.getItem('sidebar_favorites') || '[]');
|
||||
favs.forEach(function(url){
|
||||
var orig = document.querySelector('.sidebar-nav a[href="' + url + '"]');
|
||||
if(!orig) return;
|
||||
var li = document.createElement('li');
|
||||
li.className = 'nav-fav';
|
||||
var a = document.createElement('a');
|
||||
a.href = url; a.textContent = orig.textContent.replace(/^[☆★]/, '').trim();
|
||||
li.appendChild(a);
|
||||
nav.insertBefore(li, nav.lastElementChild);
|
||||
});
|
||||
}
|
||||
renderFavorites();
|
||||
var sidebarNav = document.querySelector('.sidebar-nav');
|
||||
var savedScroll = sessionStorage.getItem('sidebar_scroll');
|
||||
if (sidebarNav && savedScroll) sidebarNav.scrollTop = parseInt(savedScroll, 10);
|
||||
sidebarNav && sidebarNav.addEventListener('click', function(){
|
||||
sessionStorage.setItem('sidebar_scroll', this.scrollTop);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,241 @@
|
||||
{% extends "page.html" %}
|
||||
{% block page_content %}
|
||||
<style>
|
||||
.broadcast-layout { display:flex; gap:24px; margin-top:16px; flex-wrap:wrap; }
|
||||
.broadcast-left { flex:1; min-width:300px; }
|
||||
.broadcast-right { flex:1; min-width:300px; }
|
||||
.msg-preview { background:#e3f2fd; border-radius:12px; padding:12px 16px; margin-top:8px;
|
||||
white-space:pre-wrap; word-break:break-word; line-height:1.5; font-size:14px; }
|
||||
.msg-preview.empty { color:#999; font-style:italic; }
|
||||
.recipient-item { display:flex; align-items:center; gap:8px; padding:6px 8px;
|
||||
border-bottom:1px solid #eee; cursor:pointer; }
|
||||
.recipient-item:hover { background:#f8f9fa; }
|
||||
.recipient-item .name { flex:1; }
|
||||
.recipient-item .phone { color:#666; font-size:13px; }
|
||||
.recipient-list { max-height:300px; overflow-y:auto; border:1px solid #ddd; border-radius:8px;
|
||||
margin-top:8px; }
|
||||
.recipient-list .select-all { padding:8px; border-bottom:1px solid #eee; background:#f8f9fa;
|
||||
font-weight:600; cursor:pointer; user-select:none; }
|
||||
.modal-overlay { display:none; position:fixed; top:0; left:0; right:0; bottom:0;
|
||||
background:rgba(0,0,0,0.5); z-index:1000;
|
||||
align-items:center; justify-content:center; }
|
||||
.modal-overlay.active { display:flex; }
|
||||
.modal-box { background:#fff; border-radius:12px; padding:24px; max-width:600px;
|
||||
width:90%; max-height:80vh; overflow-y:auto; }
|
||||
.modal-box h3 { margin:0 0 16px; }
|
||||
.modal-box .actions { display:flex; gap:12px; justify-content:flex-end; margin-top:20px; }
|
||||
.broadcast-history { margin-top:24px; }
|
||||
.broadcast-history table { width:100%; }
|
||||
.broadcast-history td, .broadcast-history th { padding:8px 12px; text-align:left; border-bottom:1px solid #eee; }
|
||||
.broadcast-history .text-col { max-width:300px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
</style>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Рассылки</h2></div>
|
||||
<div class="broadcast-layout">
|
||||
<div class="broadcast-left">
|
||||
<div class="form-group">
|
||||
<label>Текст сообщения</label>
|
||||
<textarea id="broadcast-text" rows="5" style="width:100%;" oninput="updatePreview()" placeholder="Введите текст рассылки... Markdown поддерживается"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label>📱 Предпросмотр сообщения</label>
|
||||
<div id="live-preview" class="msg-preview empty">Текст сообщения появится здесь...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="broadcast-right">
|
||||
<div class="form-group">
|
||||
<label>Получатели</label>
|
||||
<input type="text" id="recipient-search" placeholder="Поиск..." style="width:100%;margin-bottom:8px;" oninput="loadRecipients()">
|
||||
</div>
|
||||
<div class="recipient-list" id="recipient-list">
|
||||
<div class="select-all" onclick="toggleSelectAll()">
|
||||
<span id="select-all-checkbox">☐</span> Выбрать всех согласных
|
||||
<span style="float:right;color:#666;font-weight:400;" id="recipient-count"></span>
|
||||
</div>
|
||||
<div id="recipient-items"></div>
|
||||
</div>
|
||||
<div style="display:flex;gap:12px;margin-top:12px;">
|
||||
<button class="btn btn-primary" onclick="previewBroadcast()">👁 Предпросмотр</button>
|
||||
<button class="btn btn-primary" onclick="sendBroadcast()">📨 Отправить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card broadcast-history">
|
||||
<div class="card-header"><h3>История рассылок</h3></div>
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr><th>ID</th><th>Текст</th><th>Получателей</th><th>Дата</th></tr></thead>
|
||||
<tbody id="history-list"></tbody>
|
||||
</table></div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="preview-modal">
|
||||
<div class="modal-box">
|
||||
<h3>👁 Предпросмотр рассылки</h3>
|
||||
<div style="background:#e3f2fd;border-radius:12px;padding:16px;white-space:pre-wrap;line-height:1.5;" id="preview-text"></div>
|
||||
<div style="margin-top:16px;padding:12px;background:#f8f9fa;border-radius:8px;">
|
||||
<strong>Получателей:</strong> <span id="preview-count"></span>
|
||||
<div style="margin-top:8px;max-height:150px;overflow-y:auto;" id="preview-recipients"></div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-secondary" onclick="closePreview()">Отмена</button>
|
||||
<button class="btn btn-primary" id="preview-send-btn" onclick="confirmSend()">📨 Отправить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var selectedUserIds = new Set();
|
||||
var allRecipients = [];
|
||||
var currentDraftId = null;
|
||||
|
||||
function updatePreview(){
|
||||
var text = document.getElementById('broadcast-text').value.trim();
|
||||
var preview = document.getElementById('live-preview');
|
||||
if(text){
|
||||
preview.textContent = text;
|
||||
preview.className = 'msg-preview';
|
||||
} else {
|
||||
preview.textContent = 'Текст сообщения появится здесь...';
|
||||
preview.className = 'msg-preview empty';
|
||||
}
|
||||
}
|
||||
|
||||
function loadRecipients(){
|
||||
var q = document.getElementById('recipient-search').value.trim();
|
||||
var url = '/service/api/bot/broadcast/recipients?limit=200';
|
||||
if(q) url += '&q=' + encodeURIComponent(q);
|
||||
fetch(url).then(function(r){ return r.json() }).then(function(d){
|
||||
allRecipients = d.users || [];
|
||||
renderRecipients();
|
||||
});
|
||||
}
|
||||
|
||||
function renderRecipients(){
|
||||
var container = document.getElementById('recipient-items');
|
||||
var countEl = document.getElementById('recipient-count');
|
||||
container.innerHTML = '';
|
||||
countEl.textContent = allRecipients.length + ' чел.';
|
||||
allRecipients.forEach(function(u){
|
||||
var div = document.createElement('div');
|
||||
div.className = 'recipient-item';
|
||||
var checked = selectedUserIds.has(u.max_user_id) ? '☑' : '☐';
|
||||
var name = (u.first_name || '') + ' ' + (u.last_name || '');
|
||||
if(!name.trim()) name = 'ID ' + u.max_user_id;
|
||||
div.innerHTML = '<span class="cb">' + checked + '</span>' +
|
||||
'<span class="name">' + name + '</span>' +
|
||||
'<span class="phone">' + (u.phone || '') + '</span>';
|
||||
div.onclick = function(){ toggleRecipient(u.max_user_id); };
|
||||
container.appendChild(div);
|
||||
});
|
||||
updateSelectAllCheckbox();
|
||||
}
|
||||
|
||||
function toggleRecipient(id){
|
||||
if(selectedUserIds.has(id)){
|
||||
selectedUserIds['delete'](id);
|
||||
} else {
|
||||
selectedUserIds.add(id);
|
||||
}
|
||||
renderRecipients();
|
||||
}
|
||||
|
||||
function toggleSelectAll(){
|
||||
if(selectedUserIds.size === allRecipients.length){
|
||||
selectedUserIds.clear();
|
||||
} else {
|
||||
selectedUserIds = new Set(allRecipients.map(function(u){ return u.max_user_id; }));
|
||||
}
|
||||
renderRecipients();
|
||||
}
|
||||
|
||||
function updateSelectAllCheckbox(){
|
||||
var cb = document.getElementById('select-all-checkbox');
|
||||
if(allRecipients.length > 0 && selectedUserIds.size === allRecipients.length){
|
||||
cb.textContent = '☑';
|
||||
} else {
|
||||
cb.textContent = '☐';
|
||||
}
|
||||
}
|
||||
|
||||
function previewBroadcast(){
|
||||
var text = document.getElementById('broadcast-text').value.trim();
|
||||
if(!text){ alert('Введите текст рассылки'); return; }
|
||||
var userIds = Array.from(selectedUserIds);
|
||||
fetch('/service/api/bot/broadcast', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({text: text, user_ids: userIds, action: 'preview', draft_id: currentDraftId})
|
||||
}).then(function(r){ return r.json() }).then(function(d){
|
||||
if(d.error){ alert(d.error); return; }
|
||||
currentDraftId = d.draft_id;
|
||||
document.getElementById('preview-text').textContent = d.text;
|
||||
document.getElementById('preview-count').textContent = d.recipient_count + ' чел.';
|
||||
var recEl = document.getElementById('preview-recipients');
|
||||
recEl.innerHTML = d.recipients.join(', ');
|
||||
document.getElementById('preview-modal').className = 'modal-overlay active';
|
||||
});
|
||||
}
|
||||
|
||||
function closePreview(){
|
||||
document.getElementById('preview-modal').className = 'modal-overlay';
|
||||
}
|
||||
|
||||
function confirmSend(){
|
||||
var text = document.getElementById('broadcast-text').value.trim();
|
||||
if(!currentDraftId){ alert('Сначала сделайте предпросмотр'); return; }
|
||||
if(!confirm('Отправить рассылку?')) return;
|
||||
var userIds = Array.from(selectedUserIds);
|
||||
document.getElementById('preview-send-btn').disabled = true;
|
||||
fetch('/service/api/bot/broadcast', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({text: text, user_ids: userIds, action: 'send', draft_id: currentDraftId})
|
||||
}).then(function(r){ return r.json() }).then(function(d){
|
||||
closePreview();
|
||||
if(d.ok){
|
||||
alert('Рассылка отправлена: ' + d.sent_to + ' из ' + d.total_recipients + ' получателей');
|
||||
document.getElementById('broadcast-text').value = '';
|
||||
document.getElementById('live-preview').textContent = 'Текст сообщения появится здесь...';
|
||||
document.getElementById('live-preview').className = 'msg-preview empty';
|
||||
selectedUserIds.clear();
|
||||
renderRecipients();
|
||||
currentDraftId = null;
|
||||
loadHistory();
|
||||
} else {
|
||||
alert('Ошибка: ' + (d.error || 'неизвестная'));
|
||||
}
|
||||
document.getElementById('preview-send-btn').disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function sendBroadcast(){
|
||||
var text = document.getElementById('broadcast-text').value.trim();
|
||||
if(!text){ alert('Введите текст рассылки'); return; }
|
||||
var userIds = Array.from(selectedUserIds);
|
||||
if(userIds.length === 0 && !confirm('Не выбрано ни одного получателя. Отправить всем согласным?')) return;
|
||||
previewBroadcast();
|
||||
}
|
||||
|
||||
function loadHistory(){
|
||||
fetch('/service/api/bot/broadcast/history').then(function(r){ return r.json() }).then(function(d){
|
||||
var tbody = document.getElementById('history-list');
|
||||
tbody.innerHTML = '';
|
||||
(d.broadcasts || []).forEach(function(b){
|
||||
var tr = document.createElement('tr');
|
||||
tr.innerHTML = '<td>' + b.id + '</td>' +
|
||||
'<td class="text-col" title="' + b.text.replace(/"/g,'"') + '">' + b.text + '</td>' +
|
||||
'<td>' + b.recipient_count + '</td>' +
|
||||
'<td>' + b.sent_at + '</td>';
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
if(!d.broadcasts || d.broadcasts.length === 0){
|
||||
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center;color:#999;">Рассылок ещё не было</td></tr>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadRecipients();
|
||||
loadHistory();
|
||||
</script>
|
||||
{% endblock %}
|
||||
+12
-1
@@ -46,7 +46,9 @@ echo "Version: $VERSION"
|
||||
# Create archive
|
||||
cd ..
|
||||
ARCHIVE="other/aegisone-py-deploy.tar.gz"
|
||||
tar czf "$ARCHIVE" py_service/ --exclude='py_service/__pycache__' --exclude='py_service/.venv' --exclude='py_service/.git' --exclude='py_service/app/__pycache__' --exclude='py_service/app/**/__pycache__'
|
||||
tar czf "$ARCHIVE" \
|
||||
py_service/ --exclude='py_service/__pycache__' --exclude='py_service/.venv' --exclude='py_service/.git' --exclude='py_service/app/__pycache__' --exclude='py_service/app/**/__pycache__' \
|
||||
max_bot/ --exclude='max_bot/__pycache__' --exclude='max_bot/.venv'
|
||||
echo "Archive created: $ARCHIVE"
|
||||
|
||||
# Upload
|
||||
@@ -66,6 +68,15 @@ ssh angel@81.177.141.34 << 'SSHEOF'
|
||||
docker compose build --no-cache --network host
|
||||
docker compose up -d
|
||||
echo "Service restarted"
|
||||
|
||||
# Deploy max_bot
|
||||
echo ""
|
||||
echo "--- Deploying max_bot ---"
|
||||
if [ -d "/opt/projects/max_bot" ]; then
|
||||
bash max_bot/max_bot_deploy.sh
|
||||
else
|
||||
echo "⚠️ max_bot directory not found — skipping"
|
||||
fi
|
||||
SSHEOF
|
||||
|
||||
# Commit changelog
|
||||
|
||||
@@ -143,6 +143,12 @@ ROUTE_PERMISSIONS: dict[tuple[str, str], tuple[str, ...]] = {
|
||||
("GET", "/service/bot-settings/users"): ("owner",),
|
||||
("GET", "/service/bot-settings/test-runner"): ("owner",),
|
||||
("GET", "/service/bot-settings/analytics"): ("owner",),
|
||||
("GET", "/service/bot-settings/risk-questions"): ("owner",),
|
||||
("GET", "/service/bot-settings/handoffs"): ("owner",),
|
||||
("GET", "/service/bot-settings/notifications"): ("owner",),
|
||||
("GET", "/service/bot-settings/broadcasts"): ("owner",),
|
||||
("GET", "/service/bot-settings/storage"): ("owner",),
|
||||
("GET", "/service/bot-settings/tickets"): ("owner",),
|
||||
|
||||
# === PORTAL / ROLE SETTINGS ===
|
||||
("GET", "/service/portal-settings"): ("owner",),
|
||||
@@ -179,6 +185,34 @@ ROUTE_PERMISSIONS: dict[tuple[str, str], tuple[str, ...]] = {
|
||||
("GET", "/service/api/bot/broadcast/history"): ("owner",),
|
||||
("POST", "/service/api/bot/broadcast"): ("owner",),
|
||||
|
||||
# === BOT RISK QUESTIONS ===
|
||||
("GET", "/service/api/bot/risk-questions"): ("owner",),
|
||||
("POST", "/service/api/bot/risk-questions/create"): ("owner",),
|
||||
("POST", "/service/api/bot/risk-questions/edit"): ("owner",),
|
||||
("POST", "/service/api/bot/risk-questions/toggle"): ("owner",),
|
||||
("POST", "/service/api/bot/risk-questions/delete"): ("owner",),
|
||||
|
||||
# === BOT STORAGE ===
|
||||
("GET", "/service/api/bot/storage/list"): ("owner",),
|
||||
("GET", "/service/api/bot/storage/search"): ("owner",),
|
||||
("GET", "/service/api/bot/storage/public-link"): ("owner",),
|
||||
("GET", "/service/api/bot/storage/download"): ("owner",),
|
||||
|
||||
# === BOT TICKETS ===
|
||||
("GET", "/service/api/bot/tickets"): ("owner",),
|
||||
("GET", "/service/api/bot/tickets/{ticket_id}"): ("owner",),
|
||||
("POST", "/service/api/bot/tickets/create"): ("owner",),
|
||||
("POST", "/service/api/bot/tickets/{ticket_id}/message"): ("owner",),
|
||||
("POST", "/service/api/bot/tickets/{ticket_id}/status"): ("owner",),
|
||||
("GET", "/service/api/bot/engineers"): ("owner",),
|
||||
|
||||
# === BOT HANDOFFS / NOTIFICATIONS (non-standard paths) ===
|
||||
("GET", "/service/bot-settings/handoffs/poll"): ("owner",),
|
||||
("POST", "/service/bot-settings/handoffs/{handoff_id}/take"): ("owner",),
|
||||
("POST", "/service/api/bot/handoffs/{handoff_id}/close"): ("owner",),
|
||||
("GET", "/service/bot-settings/notifications/data"): ("owner",),
|
||||
("POST", "/service/bot-settings/notifications/{notif_id}/read"): ("owner",),
|
||||
|
||||
# === OTHER API (main.py) ===
|
||||
("GET", "/service/api/shs"): ("owner", "engineer", "technician"),
|
||||
("GET", "/service/api/tasks"): ("owner", "engineer", "technician"),
|
||||
@@ -207,6 +241,12 @@ DYNAMIC_ROUTES: set[tuple[str, str]] = {
|
||||
("GET", "/service/documents/{slug}/edit"),
|
||||
("POST", "/service/documents/{slug}/edit"),
|
||||
("GET", "/service/documents/{slug}/download"),
|
||||
("POST", "/service/bot-settings/handoffs/{handoff_id}/take"),
|
||||
("POST", "/service/bot-settings/notifications/{notif_id}/read"),
|
||||
("POST", "/service/api/bot/handoffs/{handoff_id}/close"),
|
||||
("GET", "/service/api/bot/tickets/{ticket_id}"),
|
||||
("POST", "/service/api/bot/tickets/{ticket_id}/message"),
|
||||
("POST", "/service/api/bot/tickets/{ticket_id}/status"),
|
||||
}
|
||||
|
||||
# Routes excluded from discover test (internal framework routes)
|
||||
|
||||
Reference in New Issue
Block a user