mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-29 03:59:07 +00:00
Compare commits
4 Commits
2025.9.21
...
a8f704129b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8f704129b | ||
|
|
62cd55e48f | ||
|
|
e4e3f885ea | ||
|
|
36d698f7ce |
@@ -138,3 +138,4 @@ This repository follows conventional changelog practices. Refer to `CONTRIBUTING
|
||||
- Commit message format and scoping
|
||||
- Release process using `scripts/release.sh`
|
||||
- Pre-commit hooks setup with `pre-commit install`
|
||||
- When the pre-commit fails, the commit is canceled
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
RefreshCw,
|
||||
AlertCircle,
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
Clock,
|
||||
TrendingUp,
|
||||
User,
|
||||
FileText,
|
||||
} from "lucide-react";
|
||||
import { apiClient } from "../lib/api";
|
||||
import {
|
||||
@@ -19,7 +21,73 @@ import {
|
||||
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
|
||||
import { Button } from "./ui/button";
|
||||
import { Badge } from "./ui/badge";
|
||||
import type { SyncOperationsResponse } from "../types/api";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "./ui/dialog";
|
||||
import { ScrollArea } from "./ui/scroll-area";
|
||||
import type { SyncOperationsResponse, SyncOperation } from "../types/api";
|
||||
|
||||
// Component for viewing sync operation logs
|
||||
function LogsDialog({ operation }: { operation: SyncOperation }) {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="shrink-0">
|
||||
<FileText className="h-3 w-3 mr-1" />
|
||||
<span className="hidden sm:inline">View Logs</span>
|
||||
<span className="sm:hidden">Logs</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-4xl max-h-[80vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Sync Operation Logs</DialogTitle>
|
||||
<DialogDescription>
|
||||
Operation #{operation.id} - Started at{" "}
|
||||
{new Date(operation.started_at).toLocaleString()}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="h-[60vh] w-full rounded border p-4">
|
||||
<div className="space-y-2">
|
||||
{operation.logs.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm">No logs available</p>
|
||||
) : (
|
||||
operation.logs.map((log, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="text-sm font-mono bg-muted/50 p-2 rounded text-wrap break-all"
|
||||
>
|
||||
{log}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{operation.errors.length > 0 && (
|
||||
<>
|
||||
<div className="mt-4 mb-2 text-sm font-semibold text-destructive">
|
||||
Errors:
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{operation.errors.map((error, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="text-sm font-mono bg-destructive/10 border border-destructive/20 p-2 rounded text-wrap break-all text-destructive"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default function System() {
|
||||
const {
|
||||
@@ -111,68 +179,128 @@ export default function System() {
|
||||
return (
|
||||
<div
|
||||
key={operation.id}
|
||||
className="flex items-center justify-between p-4 border rounded-lg hover:bg-accent transition-colors"
|
||||
className="border rounded-lg hover:bg-accent transition-colors"
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div
|
||||
className={`p-2 rounded-full ${
|
||||
isRunning
|
||||
? "bg-blue-100 text-blue-600"
|
||||
: operation.success
|
||||
? "bg-green-100 text-green-600"
|
||||
: "bg-red-100 text-red-600"
|
||||
}`}
|
||||
>
|
||||
{isRunning ? (
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
) : operation.success ? (
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
) : (
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<h4 className="text-sm font-medium text-foreground">
|
||||
{isRunning
|
||||
? "Sync Running"
|
||||
{/* Desktop Layout */}
|
||||
<div className="hidden md:flex items-center justify-between p-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div
|
||||
className={`p-2 rounded-full ${
|
||||
isRunning
|
||||
? "bg-blue-100 text-blue-600"
|
||||
: operation.success
|
||||
? "Sync Completed"
|
||||
: "Sync Failed"}
|
||||
</h4>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{operation.trigger_type}
|
||||
</Badge>
|
||||
? "bg-green-100 text-green-600"
|
||||
: "bg-red-100 text-red-600"
|
||||
}`}
|
||||
>
|
||||
{isRunning ? (
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
) : operation.success ? (
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
) : (
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-4 mt-1 text-xs text-muted-foreground">
|
||||
<span className="flex items-center space-x-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>
|
||||
{startedAt.toLocaleDateString()}{" "}
|
||||
{startedAt.toLocaleTimeString()}
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<h4 className="text-sm font-medium text-foreground">
|
||||
{isRunning
|
||||
? "Sync Running"
|
||||
: operation.success
|
||||
? "Sync Completed"
|
||||
: "Sync Failed"}
|
||||
</h4>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{operation.trigger_type}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4 mt-1 text-xs text-muted-foreground">
|
||||
<span className="flex items-center space-x-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>
|
||||
{startedAt.toLocaleDateString()}{" "}
|
||||
{startedAt.toLocaleTimeString()}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
{duration && <span>Duration: {duration}</span>}
|
||||
{duration && <span>Duration: {duration}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="text-right text-sm text-muted-foreground">
|
||||
<div className="flex items-center space-x-2">
|
||||
<User className="h-3 w-3" />
|
||||
<span>{operation.accounts_processed} accounts</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mt-1">
|
||||
<TrendingUp className="h-3 w-3" />
|
||||
<span>
|
||||
{operation.transactions_added} new transactions
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<LogsDialog operation={operation} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right text-sm text-muted-foreground">
|
||||
<div className="flex items-center space-x-2">
|
||||
<User className="h-3 w-3" />
|
||||
<span>{operation.accounts_processed} accounts</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mt-1">
|
||||
<TrendingUp className="h-3 w-3" />
|
||||
<span>
|
||||
{operation.transactions_added} new transactions
|
||||
</span>
|
||||
</div>
|
||||
{operation.errors.length > 0 && (
|
||||
<div className="flex items-center space-x-2 mt-1 text-red-600">
|
||||
<AlertCircle className="h-3 w-3" />
|
||||
<span>{operation.errors.length} errors</span>
|
||||
|
||||
{/* Mobile Layout */}
|
||||
<div className="md:hidden p-4 space-y-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div
|
||||
className={`p-2 rounded-full ${
|
||||
isRunning
|
||||
? "bg-blue-100 text-blue-600"
|
||||
: operation.success
|
||||
? "bg-green-100 text-green-600"
|
||||
: "bg-red-100 text-red-600"
|
||||
}`}
|
||||
>
|
||||
{isRunning ? (
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
) : operation.success ? (
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
) : (
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-foreground">
|
||||
{isRunning
|
||||
? "Sync Running"
|
||||
: operation.success
|
||||
? "Sync Completed"
|
||||
: "Sync Failed"}
|
||||
</h4>
|
||||
<Badge variant="outline" className="text-xs mt-1">
|
||||
{operation.trigger_type}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<LogsDialog operation={operation} />
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-muted-foreground space-y-2">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>
|
||||
{startedAt.toLocaleDateString()}{" "}
|
||||
{startedAt.toLocaleTimeString()}
|
||||
</span>
|
||||
{duration && <span className="ml-2">• {duration}</span>}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div className="flex items-center space-x-1">
|
||||
<User className="h-3 w-3" />
|
||||
<span>{operation.accounts_processed} accounts</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<TrendingUp className="h-3 w-3" />
|
||||
<span>{operation.transactions_added} new transactions</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -190,8 +190,7 @@ export default function TransactionsTable() {
|
||||
<div className="text-xs text-muted-foreground space-y-1">
|
||||
{account && (
|
||||
<p className="truncate">
|
||||
{account.name || "Unnamed Account"} •{" "}
|
||||
{account.institution_id}
|
||||
{account.display_name || "Unnamed Account"}
|
||||
</p>
|
||||
)}
|
||||
{(transaction.creditor_name || transaction.debtor_name) && (
|
||||
@@ -486,8 +485,7 @@ export default function TransactionsTable() {
|
||||
<div className="text-xs text-muted-foreground space-y-1 mt-1">
|
||||
{account && (
|
||||
<p className="break-words">
|
||||
{account.name || "Unnamed Account"} •{" "}
|
||||
{account.institution_id}
|
||||
{account.display_name || "Unnamed Account"}
|
||||
</p>
|
||||
)}
|
||||
{(transaction.creditor_name ||
|
||||
|
||||
21
frontend/src/components/ui/scroll-area.tsx
Normal file
21
frontend/src/components/ui/scroll-area.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative overflow-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
));
|
||||
ScrollArea.displayName = "ScrollArea";
|
||||
|
||||
export { ScrollArea };
|
||||
@@ -32,7 +32,7 @@ async def get_bank_institutions(
|
||||
countries=inst["countries"],
|
||||
logo=inst.get("logo"),
|
||||
)
|
||||
for inst in institutions_data
|
||||
for inst in institutions_data.get("results", [])
|
||||
]
|
||||
|
||||
return APIResponse(
|
||||
|
||||
@@ -112,7 +112,7 @@ class BackgroundScheduler:
|
||||
|
||||
# Send notification about the failure
|
||||
try:
|
||||
await self.notification_service.send_expiry_notification(
|
||||
await self.notification_service.send_sync_failure_notification(
|
||||
{
|
||||
"type": "sync_failure",
|
||||
"error": str(e),
|
||||
@@ -145,7 +145,7 @@ class BackgroundScheduler:
|
||||
logger.error("Maximum retries exceeded for sync job")
|
||||
# Send final failure notification
|
||||
try:
|
||||
await self.notification_service.send_expiry_notification(
|
||||
await self.notification_service.send_sync_failure_notification(
|
||||
{
|
||||
"type": "sync_final_failure",
|
||||
"error": str(e),
|
||||
|
||||
@@ -55,3 +55,44 @@ def send_transactions_message(ctx: click.Context, transactions: list):
|
||||
response.raise_for_status()
|
||||
except Exception as e:
|
||||
raise Exception(f"Discord notification failed: {e}\n{response.text}") from e
|
||||
|
||||
|
||||
def send_sync_failure_notification(ctx: click.Context, notification: dict):
|
||||
info("Sending sync failure notification to Discord")
|
||||
webhook = DiscordWebhook(url=ctx.obj["notifications"]["discord"]["webhook"])
|
||||
|
||||
# Determine color and title based on failure type
|
||||
if notification.get("type") == "sync_final_failure":
|
||||
color = "ff0000" # Red for final failure
|
||||
title = "🚨 Sync Final Failure"
|
||||
description = (
|
||||
f"Sync failed permanently after {notification['retry_count']} attempts"
|
||||
)
|
||||
else:
|
||||
color = "ffaa00" # Orange for retry
|
||||
title = "⚠️ Sync Failure"
|
||||
description = f"Sync failed (attempt {notification['retry_count']}/{notification['max_retries']}). Will retry automatically..."
|
||||
|
||||
embed = DiscordEmbed(
|
||||
title=title,
|
||||
description=description,
|
||||
color=color,
|
||||
)
|
||||
embed.set_author(
|
||||
name="Leggen",
|
||||
url="https://github.com/elisiariocouto/leggen",
|
||||
)
|
||||
embed.add_embed_field(
|
||||
name="Error",
|
||||
value=notification["error"][:1024], # Discord has field value limits
|
||||
inline=False,
|
||||
)
|
||||
embed.set_footer(text="Sync failure notification")
|
||||
embed.set_timestamp()
|
||||
|
||||
webhook.add_embed(embed)
|
||||
response = webhook.execute()
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except Exception as e:
|
||||
raise Exception(f"Discord notification failed: {e}\n{response.text}") from e
|
||||
|
||||
@@ -79,3 +79,38 @@ def send_transaction_message(ctx: click.Context, transactions: list):
|
||||
res.raise_for_status()
|
||||
except Exception as e:
|
||||
raise Exception(f"Telegram notification failed: {e}\n{res.text}") from e
|
||||
|
||||
|
||||
def send_sync_failure_notification(ctx: click.Context, notification: dict):
|
||||
token = ctx.obj["notifications"]["telegram"]["token"]
|
||||
chat_id = ctx.obj["notifications"]["telegram"]["chat_id"]
|
||||
bot_url = f"https://api.telegram.org/bot{token}/sendMessage"
|
||||
info("Sending sync failure notification to Telegram")
|
||||
|
||||
message = "*🚨 [Leggen](https://github.com/elisiariocouto/leggen)*\n"
|
||||
message += "*Sync Failed*\n\n"
|
||||
message += escape_markdown(f"Error: {notification['error']}\n")
|
||||
|
||||
if notification.get("type") == "sync_final_failure":
|
||||
message += escape_markdown(
|
||||
f"❌ Final failure after {notification['retry_count']} attempts\n"
|
||||
)
|
||||
else:
|
||||
message += escape_markdown(
|
||||
f"🔄 Attempt {notification['retry_count']}/{notification['max_retries']}\n"
|
||||
)
|
||||
message += escape_markdown("Will retry automatically...\n")
|
||||
|
||||
res = requests.post(
|
||||
bot_url,
|
||||
json={
|
||||
"chat_id": chat_id,
|
||||
"text": message,
|
||||
"parse_mode": "MarkdownV2",
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
res.raise_for_status()
|
||||
except Exception as e:
|
||||
raise Exception(f"Telegram notification failed: {e}\n{res.text}") from e
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
@@ -30,6 +30,27 @@ class GoCardlessService:
|
||||
)
|
||||
self._token = None
|
||||
|
||||
async def _make_authenticated_request(
|
||||
self, method: str, url: str, **kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""Make authenticated request with automatic token refresh on 401"""
|
||||
headers = await self._get_auth_headers()
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.request(method, url, headers=headers, **kwargs)
|
||||
_log_rate_limits(response)
|
||||
|
||||
# If we get 401, clear token cache and retry once
|
||||
if response.status_code == 401:
|
||||
logger.warning("Got 401, clearing token cache and retrying")
|
||||
self._token = None
|
||||
headers = await self._get_auth_headers()
|
||||
response = await client.request(method, url, headers=headers, **kwargs)
|
||||
_log_rate_limits(response)
|
||||
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def _get_auth_headers(self) -> Dict[str, str]:
|
||||
"""Get authentication headers for GoCardless API"""
|
||||
token = await self._get_token()
|
||||
@@ -102,74 +123,42 @@ class GoCardlessService:
|
||||
with open(auth_file, "w") as f:
|
||||
json.dump(auth_data, f)
|
||||
|
||||
async def get_institutions(self, country: str = "PT") -> List[Dict[str, Any]]:
|
||||
async def get_institutions(self, country: str = "PT") -> Dict[str, Any]:
|
||||
"""Get available bank institutions for a country"""
|
||||
headers = await self._get_auth_headers()
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{self.base_url}/institutions/",
|
||||
headers=headers,
|
||||
params={"country": country},
|
||||
)
|
||||
_log_rate_limits(response)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
return await self._make_authenticated_request(
|
||||
"GET", f"{self.base_url}/institutions/", params={"country": country}
|
||||
)
|
||||
|
||||
async def create_requisition(
|
||||
self, institution_id: str, redirect_url: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a bank connection requisition"""
|
||||
headers = await self._get_auth_headers()
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{self.base_url}/requisitions/",
|
||||
headers=headers,
|
||||
json={"institution_id": institution_id, "redirect": redirect_url},
|
||||
)
|
||||
_log_rate_limits(response)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
return await self._make_authenticated_request(
|
||||
"POST",
|
||||
f"{self.base_url}/requisitions/",
|
||||
json={"institution_id": institution_id, "redirect": redirect_url},
|
||||
)
|
||||
|
||||
async def get_requisitions(self) -> Dict[str, Any]:
|
||||
"""Get all requisitions"""
|
||||
headers = await self._get_auth_headers()
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{self.base_url}/requisitions/", headers=headers
|
||||
)
|
||||
_log_rate_limits(response)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
return await self._make_authenticated_request(
|
||||
"GET", f"{self.base_url}/requisitions/"
|
||||
)
|
||||
|
||||
async def get_account_details(self, account_id: str) -> Dict[str, Any]:
|
||||
"""Get account details"""
|
||||
headers = await self._get_auth_headers()
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{self.base_url}/accounts/{account_id}/", headers=headers
|
||||
)
|
||||
_log_rate_limits(response)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
return await self._make_authenticated_request(
|
||||
"GET", f"{self.base_url}/accounts/{account_id}/"
|
||||
)
|
||||
|
||||
async def get_account_balances(self, account_id: str) -> Dict[str, Any]:
|
||||
"""Get account balances"""
|
||||
headers = await self._get_auth_headers()
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{self.base_url}/accounts/{account_id}/balances/", headers=headers
|
||||
)
|
||||
_log_rate_limits(response)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
return await self._make_authenticated_request(
|
||||
"GET", f"{self.base_url}/accounts/{account_id}/balances/"
|
||||
)
|
||||
|
||||
async def get_account_transactions(self, account_id: str) -> Dict[str, Any]:
|
||||
"""Get account transactions"""
|
||||
headers = await self._get_auth_headers()
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{self.base_url}/accounts/{account_id}/transactions/", headers=headers
|
||||
)
|
||||
_log_rate_limits(response)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
return await self._make_authenticated_request(
|
||||
"GET", f"{self.base_url}/accounts/{account_id}/transactions/"
|
||||
)
|
||||
|
||||
@@ -289,3 +289,69 @@ class NotificationService:
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send Telegram expiry notification: {e}")
|
||||
raise
|
||||
|
||||
async def send_sync_failure_notification(
|
||||
self, notification_data: Dict[str, Any]
|
||||
) -> None:
|
||||
"""Send notification about sync failure"""
|
||||
if self._is_discord_enabled():
|
||||
await self._send_discord_sync_failure(notification_data)
|
||||
|
||||
if self._is_telegram_enabled():
|
||||
await self._send_telegram_sync_failure(notification_data)
|
||||
|
||||
async def _send_discord_sync_failure(
|
||||
self, notification_data: Dict[str, Any]
|
||||
) -> None:
|
||||
"""Send Discord sync failure notification"""
|
||||
try:
|
||||
import click
|
||||
|
||||
from leggen.notifications.discord import send_sync_failure_notification
|
||||
|
||||
# Create a mock context with the webhook
|
||||
ctx = click.Context(click.Command("sync_failure"))
|
||||
ctx.obj = {
|
||||
"notifications": {
|
||||
"discord": {
|
||||
"webhook": self.notifications_config.get("discord", {}).get(
|
||||
"webhook"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Send sync failure notification using the actual implementation
|
||||
send_sync_failure_notification(ctx, notification_data)
|
||||
logger.info(f"Sent Discord sync failure notification: {notification_data}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send Discord sync failure notification: {e}")
|
||||
raise
|
||||
|
||||
async def _send_telegram_sync_failure(
|
||||
self, notification_data: Dict[str, Any]
|
||||
) -> None:
|
||||
"""Send Telegram sync failure notification"""
|
||||
try:
|
||||
import click
|
||||
|
||||
from leggen.notifications.telegram import send_sync_failure_notification
|
||||
|
||||
# Create a mock context with the telegram config
|
||||
ctx = click.Context(click.Command("sync_failure"))
|
||||
telegram_config = self.notifications_config.get("telegram", {})
|
||||
ctx.obj = {
|
||||
"notifications": {
|
||||
"telegram": {
|
||||
"token": telegram_config.get("token"),
|
||||
"chat_id": telegram_config.get("chat_id"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Send sync failure notification using the actual implementation
|
||||
send_sync_failure_notification(ctx, notification_data)
|
||||
logger.info(f"Sent Telegram sync failure notification: {notification_data}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send Telegram sync failure notification: {e}")
|
||||
raise
|
||||
|
||||
Reference in New Issue
Block a user