feat: add connections dashboard with background health checks

- Add /connections page showing all integrations with online/offline status
- Add background health checker that polls Evotor API every 10 minutes
- Add is_online and last_checked_at fields to evotor_connections table
- Replace Evotor navbar link with unified Connections link
- Redirect connect/disconnect flows to /connections
- Add Alembic migration for new columns

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mguschin
2026-03-06 15:26:49 +03:00
parent 9edb77efba
commit 379f781e1e
15 changed files with 510 additions and 19 deletions

42
web/routes/connections.py Normal file
View File

@@ -0,0 +1,42 @@
from fastapi import APIRouter, Request, Depends
from fastapi.responses import RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from web.auth import get_current_user
from web.database import get_db
from web.models import User, EvotorConnection
router = APIRouter()
templates = Jinja2Templates(directory="web/templates")
@router.get("/connections")
def connections_page(
request: Request,
db: Session = Depends(get_db),
user: User | None = Depends(get_current_user),
):
if not user:
return RedirectResponse("/login", 303)
evotor = db.query(EvotorConnection).filter(EvotorConnection.user_id == user.id).first()
connections = [
{
"name": "Эвотор",
"icon": "bi-shop",
"connected": evotor is not None,
"is_online": evotor.is_online if evotor else False,
"last_checked_at": evotor.last_checked_at if evotor else None,
"details": evotor.store_name if evotor else None,
"connect_url": "/evotor/connect",
"disconnect_url": "/evotor/disconnect",
}
]
return templates.TemplateResponse("connections.html", {
"request": request,
"user": user,
"connections": connections,
})

View File

@@ -118,22 +118,27 @@ async def evotor_callback(
pass # Store info is optional; token is still saved
# Save or update connection
from datetime import datetime
connection = db.query(EvotorConnection).filter(EvotorConnection.user_id == user.id).first()
if connection:
connection.access_token = access_token
connection.store_id = store_id
connection.store_name = store_name
connection.is_online = True
connection.last_checked_at = datetime.utcnow()
else:
connection = EvotorConnection(
user_id=user.id,
access_token=access_token,
store_id=store_id,
store_name=store_name,
is_online=True,
last_checked_at=datetime.utcnow(),
)
db.add(connection)
db.commit()
return RedirectResponse("/evotor", 303)
return RedirectResponse("/connections", 303)
@router.post("/disconnect")
@@ -150,4 +155,4 @@ async def evotor_disconnect(
db.delete(connection)
db.commit()
return RedirectResponse("/evotor", 303)
return RedirectResponse("/connections", 303)

View File

@@ -47,7 +47,7 @@ async def forgot_submit(request: Request, db: Session = Depends(get_db)):
return templates.TemplateResponse("message.html", {
"request": request, "user": None,
"title": "Сброс пароля",
"message": "Если аккаунт с таким email существует, ссылка для сброса пароля выведена в консоль сервера.",
"message": "Если аккаунт с таким email существует, мы отправили письмо со ссылкой для сброса пароля.",
})