mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-28 19:39:16 +00:00
Compare commits
5 Commits
0315ba4bc6
...
31382cab7a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31382cab7a | ||
|
|
223fd06072 | ||
|
|
4315a6f747 | ||
|
|
01744d56f0 | ||
|
|
cb9aa4540f |
@@ -30,10 +30,13 @@ export default function Dashboard() {
|
||||
queryFn: apiClient.getAccounts,
|
||||
});
|
||||
|
||||
const { data: healthStatus, isLoading: healthLoading } = useQuery({
|
||||
const { data: healthStatus, isLoading: healthLoading, isError: healthError } = useQuery({
|
||||
queryKey: ['health'],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:8000'}/api/v1/health`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Health check failed: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
refetchInterval: 30000, // Check every 30 seconds
|
||||
@@ -146,16 +149,16 @@ export default function Dashboard() {
|
||||
<Activity className="h-4 w-4 text-yellow-500 animate-pulse" />
|
||||
<span className="text-sm text-gray-600">Checking...</span>
|
||||
</>
|
||||
) : healthStatus?.success ? (
|
||||
<>
|
||||
<Wifi className="h-4 w-4 text-green-500" />
|
||||
<span className="text-sm text-gray-600">Connected</span>
|
||||
</>
|
||||
) : (
|
||||
) : healthError || !healthStatus?.success ? (
|
||||
<>
|
||||
<WifiOff className="h-4 w-4 text-red-500" />
|
||||
<span className="text-sm text-red-500">Disconnected</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Wifi className="h-4 w-4 text-green-500" />
|
||||
<span className="text-sm text-gray-600">Connected</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,11 +36,14 @@ async def get_notification_settings() -> APIResponse:
|
||||
if discord_config.get("webhook")
|
||||
else None,
|
||||
telegram=TelegramConfig(
|
||||
token="***" if telegram_config.get("token") else "",
|
||||
chat_id=telegram_config.get("chat_id", 0),
|
||||
token="***"
|
||||
if (telegram_config.get("token") or telegram_config.get("api-key"))
|
||||
else "",
|
||||
chat_id=telegram_config.get("chat_id")
|
||||
or telegram_config.get("chat-id", 0),
|
||||
enabled=telegram_config.get("enabled", True),
|
||||
)
|
||||
if telegram_config.get("token")
|
||||
if (telegram_config.get("token") or telegram_config.get("api-key"))
|
||||
else None,
|
||||
filters=NotificationFilters(
|
||||
case_insensitive=filters_config.get("case-insensitive", {}),
|
||||
@@ -158,12 +161,24 @@ async def get_notification_services() -> APIResponse:
|
||||
"telegram": {
|
||||
"name": "Telegram",
|
||||
"enabled": bool(
|
||||
notifications_config.get("telegram", {}).get("token")
|
||||
and notifications_config.get("telegram", {}).get("chat_id")
|
||||
(
|
||||
notifications_config.get("telegram", {}).get("token")
|
||||
or notifications_config.get("telegram", {}).get("api-key")
|
||||
)
|
||||
and (
|
||||
notifications_config.get("telegram", {}).get("chat_id")
|
||||
or notifications_config.get("telegram", {}).get("chat-id")
|
||||
)
|
||||
),
|
||||
"configured": bool(
|
||||
notifications_config.get("telegram", {}).get("token")
|
||||
and notifications_config.get("telegram", {}).get("chat_id")
|
||||
(
|
||||
notifications_config.get("telegram", {}).get("token")
|
||||
or notifications_config.get("telegram", {}).get("api-key")
|
||||
)
|
||||
and (
|
||||
notifications_config.get("telegram", {}).get("chat_id")
|
||||
or notifications_config.get("telegram", {}).get("chat-id")
|
||||
)
|
||||
),
|
||||
"active": notifications_config.get("telegram", {}).get("enabled", True),
|
||||
},
|
||||
|
||||
@@ -24,6 +24,17 @@ async def lifespan(app: FastAPI):
|
||||
logger.error(f"Failed to load configuration: {e}")
|
||||
raise
|
||||
|
||||
# Run database migrations
|
||||
try:
|
||||
from leggend.services.database_service import DatabaseService
|
||||
|
||||
db_service = DatabaseService()
|
||||
await db_service.run_migrations_if_needed()
|
||||
logger.info("Database migrations completed")
|
||||
except Exception as e:
|
||||
logger.error(f"Database migration failed: {e}")
|
||||
raise
|
||||
|
||||
# Start background scheduler
|
||||
scheduler.start()
|
||||
logger.info("Background scheduler started")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any, Optional
|
||||
import sqlite3
|
||||
|
||||
from loguru import logger
|
||||
|
||||
@@ -251,6 +252,138 @@ class DatabaseService:
|
||||
logger.error(f"Failed to get account details from database: {e}")
|
||||
return None
|
||||
|
||||
async def run_migrations_if_needed(self):
|
||||
"""Run all necessary database migrations"""
|
||||
if not self.sqlite_enabled:
|
||||
logger.info("SQLite database disabled, skipping migrations")
|
||||
return
|
||||
|
||||
await self._migrate_balance_timestamps_if_needed()
|
||||
|
||||
async def _migrate_balance_timestamps_if_needed(self):
|
||||
"""Check and migrate balance timestamps if needed"""
|
||||
try:
|
||||
if await self._check_balance_timestamp_migration_needed():
|
||||
logger.info("Balance timestamp migration needed, starting...")
|
||||
await self._migrate_balance_timestamps()
|
||||
logger.info("Balance timestamp migration completed")
|
||||
else:
|
||||
logger.info("Balance timestamps are already consistent")
|
||||
except Exception as e:
|
||||
logger.error(f"Balance timestamp migration failed: {e}")
|
||||
raise
|
||||
|
||||
async def _check_balance_timestamp_migration_needed(self) -> bool:
|
||||
"""Check if balance timestamps need migration"""
|
||||
from pathlib import Path
|
||||
|
||||
db_path = Path.home() / ".config" / "leggen" / "leggen.db"
|
||||
if not db_path.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check for mixed timestamp types
|
||||
cursor.execute("""
|
||||
SELECT typeof(timestamp) as type, COUNT(*) as count
|
||||
FROM balances
|
||||
GROUP BY typeof(timestamp)
|
||||
""")
|
||||
|
||||
types = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
# If we have both 'real' and 'text' types, migration is needed
|
||||
type_names = [row[0] for row in types]
|
||||
return "real" in type_names and "text" in type_names
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to check migration status: {e}")
|
||||
return False
|
||||
|
||||
async def _migrate_balance_timestamps(self):
|
||||
"""Convert all Unix timestamps to datetime strings"""
|
||||
from pathlib import Path
|
||||
|
||||
db_path = Path.home() / ".config" / "leggen" / "leggen.db"
|
||||
if not db_path.exists():
|
||||
logger.warning("Database file not found, skipping migration")
|
||||
return
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get all balances with REAL timestamps
|
||||
cursor.execute("""
|
||||
SELECT id, timestamp
|
||||
FROM balances
|
||||
WHERE typeof(timestamp) = 'real'
|
||||
ORDER BY id
|
||||
""")
|
||||
|
||||
unix_records = cursor.fetchall()
|
||||
total_records = len(unix_records)
|
||||
|
||||
if total_records == 0:
|
||||
logger.info("No Unix timestamps found to migrate")
|
||||
conn.close()
|
||||
return
|
||||
|
||||
logger.info(
|
||||
f"Migrating {total_records} balance records from Unix to datetime format"
|
||||
)
|
||||
|
||||
# Convert and update in batches
|
||||
batch_size = 100
|
||||
migrated_count = 0
|
||||
|
||||
for i in range(0, total_records, batch_size):
|
||||
batch = unix_records[i : i + batch_size]
|
||||
|
||||
for record_id, unix_timestamp in batch:
|
||||
try:
|
||||
# Convert Unix timestamp to datetime string
|
||||
dt_string = self._unix_to_datetime_string(float(unix_timestamp))
|
||||
|
||||
# Update the record
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE balances
|
||||
SET timestamp = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(dt_string, record_id),
|
||||
)
|
||||
|
||||
migrated_count += 1
|
||||
|
||||
if migrated_count % 100 == 0:
|
||||
logger.info(
|
||||
f"Migrated {migrated_count}/{total_records} balance records"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to migrate record {record_id}: {e}")
|
||||
continue
|
||||
|
||||
# Commit batch
|
||||
conn.commit()
|
||||
|
||||
conn.close()
|
||||
logger.info(f"Successfully migrated {migrated_count} balance records")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Balance timestamp migration failed: {e}")
|
||||
raise
|
||||
|
||||
def _unix_to_datetime_string(self, unix_timestamp: float) -> str:
|
||||
"""Convert Unix timestamp to datetime string"""
|
||||
dt = datetime.fromtimestamp(unix_timestamp)
|
||||
return dt.isoformat()
|
||||
|
||||
async def _persist_balance_sqlite(
|
||||
self, account_id: str, balance_data: Dict[str, Any]
|
||||
) -> None:
|
||||
@@ -313,12 +446,12 @@ class DatabaseService:
|
||||
(
|
||||
account_id,
|
||||
balance_data.get("institution_id", "unknown"),
|
||||
"active",
|
||||
balance_data.get("account_status"),
|
||||
balance_data.get("iban", "N/A"),
|
||||
float(balance_amount["amount"]),
|
||||
balance_amount["currency"],
|
||||
balance["balanceType"],
|
||||
datetime.now(),
|
||||
datetime.now().isoformat(),
|
||||
),
|
||||
)
|
||||
except sqlite3.IntegrityError:
|
||||
|
||||
@@ -95,7 +95,8 @@ class NotificationService:
|
||||
telegram_config = self.notifications_config.get("telegram", {})
|
||||
return bool(
|
||||
telegram_config.get("token")
|
||||
and telegram_config.get("chat_id")
|
||||
or telegram_config.get("api-key")
|
||||
and (telegram_config.get("chat_id") or telegram_config.get("chat-id"))
|
||||
and telegram_config.get("enabled", True)
|
||||
)
|
||||
|
||||
@@ -117,11 +118,67 @@ class NotificationService:
|
||||
|
||||
async def _send_discord_test(self, message: str) -> None:
|
||||
"""Send Discord test notification"""
|
||||
logger.info(f"Sending Discord test: {message}")
|
||||
try:
|
||||
from leggen.notifications.discord import send_expire_notification
|
||||
import click
|
||||
|
||||
# Create a mock context with the webhook
|
||||
ctx = click.Context(click.Command("test"))
|
||||
ctx.obj = {
|
||||
"notifications": {
|
||||
"discord": {
|
||||
"webhook": self.notifications_config.get("discord", {}).get(
|
||||
"webhook"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Send test notification using the actual implementation
|
||||
test_notification = {
|
||||
"bank": "Test",
|
||||
"requisition_id": "test-123",
|
||||
"status": "active",
|
||||
"days_left": 30,
|
||||
}
|
||||
send_expire_notification(ctx, test_notification)
|
||||
logger.info(f"Discord test notification sent: {message}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send Discord test notification: {e}")
|
||||
raise
|
||||
|
||||
async def _send_telegram_test(self, message: str) -> None:
|
||||
"""Send Telegram test notification"""
|
||||
logger.info(f"Sending Telegram test: {message}")
|
||||
try:
|
||||
from leggen.notifications.telegram import send_expire_notification
|
||||
import click
|
||||
|
||||
# Create a mock context with the telegram config
|
||||
ctx = click.Context(click.Command("test"))
|
||||
telegram_config = self.notifications_config.get("telegram", {})
|
||||
ctx.obj = {
|
||||
"notifications": {
|
||||
"telegram": {
|
||||
"api-key": telegram_config.get("token")
|
||||
or telegram_config.get("api-key"),
|
||||
"chat-id": telegram_config.get("chat_id")
|
||||
or telegram_config.get("chat-id"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Send test notification using the actual implementation
|
||||
test_notification = {
|
||||
"bank": "Test",
|
||||
"requisition_id": "test-123",
|
||||
"status": "active",
|
||||
"days_left": 30,
|
||||
}
|
||||
send_expire_notification(ctx, test_notification)
|
||||
logger.info(f"Telegram test notification sent: {message}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send Telegram test notification: {e}")
|
||||
raise
|
||||
|
||||
async def _send_discord_expiry(self, notification_data: Dict[str, Any]) -> None:
|
||||
"""Send Discord expiry notification"""
|
||||
|
||||
@@ -61,8 +61,19 @@ class SyncService:
|
||||
|
||||
# Get and save balances
|
||||
balances = await self.gocardless.get_account_balances(account_id)
|
||||
if balances:
|
||||
await self.database.persist_balance(account_id, balances)
|
||||
if balances and account_details:
|
||||
# Merge account details into balances data for proper persistence
|
||||
balances_with_account_info = balances.copy()
|
||||
balances_with_account_info["institution_id"] = (
|
||||
account_details.get("institution_id")
|
||||
)
|
||||
balances_with_account_info["iban"] = account_details.get("iban")
|
||||
balances_with_account_info["account_status"] = (
|
||||
account_details.get("status")
|
||||
)
|
||||
await self.database.persist_balance(
|
||||
account_id, balances_with_account_info
|
||||
)
|
||||
balances_updated += len(balances.get("balances", []))
|
||||
|
||||
# Get and save transactions
|
||||
|
||||
Reference in New Issue
Block a user