Lint and reformat.

This commit is contained in:
Elisiário Couto
2025-09-28 23:01:04 +01:00
committed by Elisiário Couto
parent 22ec0e36b1
commit 222bb2ec64
7 changed files with 242 additions and 214 deletions

View File

@@ -58,9 +58,11 @@ export default function S3BackupConfigDrawer({
setOpen(false);
toast.success("S3 backup configuration saved successfully");
},
onError: (error: any) => {
onError: (error: Error & { response?: { data?: { detail?: string } } }) => {
console.error("Failed to update S3 backup configuration:", error);
const message = error?.response?.data?.detail || "Failed to save S3 configuration. Please check your settings and try again.";
const message =
error?.response?.data?.detail ||
"Failed to save S3 configuration. Please check your settings and try again.";
toast.error(message);
},
});
@@ -73,11 +75,15 @@ export default function S3BackupConfigDrawer({
}),
onSuccess: () => {
console.log("S3 connection test successful");
toast.success("S3 connection test successful! Your configuration is working correctly.");
toast.success(
"S3 connection test successful! Your configuration is working correctly.",
);
},
onError: (error: any) => {
onError: (error: Error & { response?: { data?: { detail?: string } } }) => {
console.error("Failed to test S3 connection:", error);
const message = error?.response?.data?.detail || "S3 connection test failed. Please verify your credentials and settings.";
const message =
error?.response?.data?.detail ||
"S3 connection test failed. Please verify your credentials and settings.";
toast.error(message);
},
});
@@ -98,9 +104,7 @@ export default function S3BackupConfigDrawer({
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>
{trigger || <EditButton />}
</DrawerTrigger>
<DrawerTrigger asChild>{trigger || <EditButton />}</DrawerTrigger>
<DrawerContent>
<div className="mx-auto w-full max-w-sm">
<DrawerHeader>
@@ -148,7 +152,10 @@ export default function S3BackupConfigDrawer({
type="password"
value={config.secret_access_key}
onChange={(e) =>
setConfig({ ...config, secret_access_key: e.target.value })
setConfig({
...config,
secret_access_key: e.target.value,
})
}
placeholder="Your AWS Secret Access Key"
required
@@ -212,15 +219,21 @@ export default function S3BackupConfigDrawer({
<Label htmlFor="path_style">Use path-style addressing</Label>
</div>
<p className="text-xs text-muted-foreground">
Enable for older S3 implementations or certain S3-compatible services
Enable for older S3 implementations or certain S3-compatible
services
</p>
</>
)}
<DrawerFooter className="px-0">
<div className="flex space-x-2">
<Button type="submit" disabled={updateMutation.isPending || !config.enabled}>
{updateMutation.isPending ? "Saving..." : "Save Configuration"}
<Button
type="submit"
disabled={updateMutation.isPending || !config.enabled}
>
{updateMutation.isPending
? "Saving..."
: "Save Configuration"}
</Button>
{config.enabled && isConfigValid && (
<Button

View File

@@ -203,8 +203,10 @@ export default function Settings() {
}
};
const isLoading = accountsLoading || settingsLoading || servicesLoading || backupLoading;
const hasError = accountsError || settingsError || servicesError || backupError;
const isLoading =
accountsLoading || settingsLoading || servicesLoading || backupLoading;
const hasError =
accountsError || settingsError || servicesError || backupError;
if (isLoading) {
return <AccountsSkeleton />;
@@ -757,7 +759,8 @@ export default function Settings() {
<span>S3 Backup Configuration</span>
</CardTitle>
<CardDescription>
Configure automatic database backups to Amazon S3 or S3-compatible storage
Configure automatic database backups to Amazon S3 or
S3-compatible storage
</CardDescription>
</CardHeader>
@@ -769,7 +772,8 @@ export default function Settings() {
No S3 backup configured
</h3>
<p className="text-muted-foreground mb-4">
Set up S3 backup to automatically backup your database to the cloud.
Set up S3 backup to automatically backup your database to
the cloud.
</p>
<S3BackupConfigDrawer settings={backupSettings} />
</div>
@@ -786,26 +790,33 @@ export default function Settings() {
S3 Backup
</h4>
<div className="flex items-center space-x-2">
<div className={`w-2 h-2 rounded-full ${
backupSettings.s3.enabled
? 'bg-green-500'
: 'bg-muted-foreground'
}`} />
<div
className={`w-2 h-2 rounded-full ${
backupSettings.s3.enabled
? "bg-green-500"
: "bg-muted-foreground"
}`}
/>
<span className="text-sm text-muted-foreground">
{backupSettings.s3.enabled ? 'Enabled' : 'Disabled'}
{backupSettings.s3.enabled
? "Enabled"
: "Disabled"}
</span>
</div>
</div>
<div className="mt-2 space-y-1">
<p className="text-sm text-muted-foreground">
<span className="font-medium">Bucket:</span> {backupSettings.s3.bucket_name}
<span className="font-medium">Bucket:</span>{" "}
{backupSettings.s3.bucket_name}
</p>
<p className="text-sm text-muted-foreground">
<span className="font-medium">Region:</span> {backupSettings.s3.region}
<span className="font-medium">Region:</span>{" "}
{backupSettings.s3.region}
</p>
{backupSettings.s3.endpoint_url && (
<p className="text-sm text-muted-foreground">
<span className="font-medium">Endpoint:</span> {backupSettings.s3.endpoint_url}
<span className="font-medium">Endpoint:</span>{" "}
{backupSettings.s3.endpoint_url}
</p>
)}
</div>
@@ -813,16 +824,17 @@ export default function Settings() {
</div>
<S3BackupConfigDrawer settings={backupSettings} />
</div>
<div className="p-4 bg-muted rounded-lg">
<h5 className="font-medium mb-2">Backup Information</h5>
<p className="text-sm text-muted-foreground mb-3">
Database backups are stored in the "leggen_backups/" folder in your S3 bucket.
Backups include the complete SQLite database file.
Database backups are stored in the "leggen_backups/"
folder in your S3 bucket. Backups include the complete
SQLite database file.
</p>
<div className="flex space-x-2">
<Button
size="sm"
<Button
size="sm"
variant="outline"
onClick={() => {
// TODO: Implement manual backup trigger
@@ -831,8 +843,8 @@ export default function Settings() {
>
Create Backup Now
</Button>
<Button
size="sm"
<Button
size="sm"
variant="outline"
onClick={() => {
// TODO: Implement backup list view

View File

@@ -281,9 +281,8 @@ export const apiClient = {
// Backup endpoints
getBackupSettings: async (): Promise<BackupSettings> => {
const response = await api.get<ApiResponse<BackupSettings>>(
"/backup/settings",
);
const response =
await api.get<ApiResponse<BackupSettings>>("/backup/settings");
return response.data.data;
},

View File

@@ -8,170 +8,170 @@
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as TransactionsRouteImport } from './routes/transactions'
import { Route as SystemRouteImport } from './routes/system'
import { Route as SettingsRouteImport } from './routes/settings'
import { Route as NotificationsRouteImport } from './routes/notifications'
import { Route as BankConnectedRouteImport } from './routes/bank-connected'
import { Route as AnalyticsRouteImport } from './routes/analytics'
import { Route as IndexRouteImport } from './routes/index'
import { Route as rootRouteImport } from "./routes/__root";
import { Route as TransactionsRouteImport } from "./routes/transactions";
import { Route as SystemRouteImport } from "./routes/system";
import { Route as SettingsRouteImport } from "./routes/settings";
import { Route as NotificationsRouteImport } from "./routes/notifications";
import { Route as BankConnectedRouteImport } from "./routes/bank-connected";
import { Route as AnalyticsRouteImport } from "./routes/analytics";
import { Route as IndexRouteImport } from "./routes/index";
const TransactionsRoute = TransactionsRouteImport.update({
id: '/transactions',
path: '/transactions',
id: "/transactions",
path: "/transactions",
getParentRoute: () => rootRouteImport,
} as any)
} as any);
const SystemRoute = SystemRouteImport.update({
id: '/system',
path: '/system',
id: "/system",
path: "/system",
getParentRoute: () => rootRouteImport,
} as any)
} as any);
const SettingsRoute = SettingsRouteImport.update({
id: '/settings',
path: '/settings',
id: "/settings",
path: "/settings",
getParentRoute: () => rootRouteImport,
} as any)
} as any);
const NotificationsRoute = NotificationsRouteImport.update({
id: '/notifications',
path: '/notifications',
id: "/notifications",
path: "/notifications",
getParentRoute: () => rootRouteImport,
} as any)
} as any);
const BankConnectedRoute = BankConnectedRouteImport.update({
id: '/bank-connected',
path: '/bank-connected',
id: "/bank-connected",
path: "/bank-connected",
getParentRoute: () => rootRouteImport,
} as any)
} as any);
const AnalyticsRoute = AnalyticsRouteImport.update({
id: '/analytics',
path: '/analytics',
id: "/analytics",
path: "/analytics",
getParentRoute: () => rootRouteImport,
} as any)
} as any);
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
id: "/",
path: "/",
getParentRoute: () => rootRouteImport,
} as any)
} as any);
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/analytics': typeof AnalyticsRoute
'/bank-connected': typeof BankConnectedRoute
'/notifications': typeof NotificationsRoute
'/settings': typeof SettingsRoute
'/system': typeof SystemRoute
'/transactions': typeof TransactionsRoute
"/": typeof IndexRoute;
"/analytics": typeof AnalyticsRoute;
"/bank-connected": typeof BankConnectedRoute;
"/notifications": typeof NotificationsRoute;
"/settings": typeof SettingsRoute;
"/system": typeof SystemRoute;
"/transactions": typeof TransactionsRoute;
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/analytics': typeof AnalyticsRoute
'/bank-connected': typeof BankConnectedRoute
'/notifications': typeof NotificationsRoute
'/settings': typeof SettingsRoute
'/system': typeof SystemRoute
'/transactions': typeof TransactionsRoute
"/": typeof IndexRoute;
"/analytics": typeof AnalyticsRoute;
"/bank-connected": typeof BankConnectedRoute;
"/notifications": typeof NotificationsRoute;
"/settings": typeof SettingsRoute;
"/system": typeof SystemRoute;
"/transactions": typeof TransactionsRoute;
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/analytics': typeof AnalyticsRoute
'/bank-connected': typeof BankConnectedRoute
'/notifications': typeof NotificationsRoute
'/settings': typeof SettingsRoute
'/system': typeof SystemRoute
'/transactions': typeof TransactionsRoute
__root__: typeof rootRouteImport;
"/": typeof IndexRoute;
"/analytics": typeof AnalyticsRoute;
"/bank-connected": typeof BankConnectedRoute;
"/notifications": typeof NotificationsRoute;
"/settings": typeof SettingsRoute;
"/system": typeof SystemRoute;
"/transactions": typeof TransactionsRoute;
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fileRoutesByFullPath: FileRoutesByFullPath;
fullPaths:
| '/'
| '/analytics'
| '/bank-connected'
| '/notifications'
| '/settings'
| '/system'
| '/transactions'
fileRoutesByTo: FileRoutesByTo
| "/"
| "/analytics"
| "/bank-connected"
| "/notifications"
| "/settings"
| "/system"
| "/transactions";
fileRoutesByTo: FileRoutesByTo;
to:
| '/'
| '/analytics'
| '/bank-connected'
| '/notifications'
| '/settings'
| '/system'
| '/transactions'
| "/"
| "/analytics"
| "/bank-connected"
| "/notifications"
| "/settings"
| "/system"
| "/transactions";
id:
| '__root__'
| '/'
| '/analytics'
| '/bank-connected'
| '/notifications'
| '/settings'
| '/system'
| '/transactions'
fileRoutesById: FileRoutesById
| "__root__"
| "/"
| "/analytics"
| "/bank-connected"
| "/notifications"
| "/settings"
| "/system"
| "/transactions";
fileRoutesById: FileRoutesById;
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AnalyticsRoute: typeof AnalyticsRoute
BankConnectedRoute: typeof BankConnectedRoute
NotificationsRoute: typeof NotificationsRoute
SettingsRoute: typeof SettingsRoute
SystemRoute: typeof SystemRoute
TransactionsRoute: typeof TransactionsRoute
IndexRoute: typeof IndexRoute;
AnalyticsRoute: typeof AnalyticsRoute;
BankConnectedRoute: typeof BankConnectedRoute;
NotificationsRoute: typeof NotificationsRoute;
SettingsRoute: typeof SettingsRoute;
SystemRoute: typeof SystemRoute;
TransactionsRoute: typeof TransactionsRoute;
}
declare module '@tanstack/react-router' {
declare module "@tanstack/react-router" {
interface FileRoutesByPath {
'/transactions': {
id: '/transactions'
path: '/transactions'
fullPath: '/transactions'
preLoaderRoute: typeof TransactionsRouteImport
parentRoute: typeof rootRouteImport
}
'/system': {
id: '/system'
path: '/system'
fullPath: '/system'
preLoaderRoute: typeof SystemRouteImport
parentRoute: typeof rootRouteImport
}
'/settings': {
id: '/settings'
path: '/settings'
fullPath: '/settings'
preLoaderRoute: typeof SettingsRouteImport
parentRoute: typeof rootRouteImport
}
'/notifications': {
id: '/notifications'
path: '/notifications'
fullPath: '/notifications'
preLoaderRoute: typeof NotificationsRouteImport
parentRoute: typeof rootRouteImport
}
'/bank-connected': {
id: '/bank-connected'
path: '/bank-connected'
fullPath: '/bank-connected'
preLoaderRoute: typeof BankConnectedRouteImport
parentRoute: typeof rootRouteImport
}
'/analytics': {
id: '/analytics'
path: '/analytics'
fullPath: '/analytics'
preLoaderRoute: typeof AnalyticsRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
"/transactions": {
id: "/transactions";
path: "/transactions";
fullPath: "/transactions";
preLoaderRoute: typeof TransactionsRouteImport;
parentRoute: typeof rootRouteImport;
};
"/system": {
id: "/system";
path: "/system";
fullPath: "/system";
preLoaderRoute: typeof SystemRouteImport;
parentRoute: typeof rootRouteImport;
};
"/settings": {
id: "/settings";
path: "/settings";
fullPath: "/settings";
preLoaderRoute: typeof SettingsRouteImport;
parentRoute: typeof rootRouteImport;
};
"/notifications": {
id: "/notifications";
path: "/notifications";
fullPath: "/notifications";
preLoaderRoute: typeof NotificationsRouteImport;
parentRoute: typeof rootRouteImport;
};
"/bank-connected": {
id: "/bank-connected";
path: "/bank-connected";
fullPath: "/bank-connected";
preLoaderRoute: typeof BankConnectedRouteImport;
parentRoute: typeof rootRouteImport;
};
"/analytics": {
id: "/analytics";
path: "/analytics";
fullPath: "/analytics";
preLoaderRoute: typeof AnalyticsRouteImport;
parentRoute: typeof rootRouteImport;
};
"/": {
id: "/";
path: "/";
fullPath: "/";
preLoaderRoute: typeof IndexRouteImport;
parentRoute: typeof rootRouteImport;
};
}
}
@@ -183,7 +183,7 @@ const rootRouteChildren: RootRouteChildren = {
SettingsRoute: SettingsRoute,
SystemRoute: SystemRoute,
TransactionsRoute: TransactionsRoute,
}
};
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
._addFileTypes<FileRouteTypes>();

View File

@@ -7,32 +7,34 @@ from pydantic import BaseModel, Field
class S3Config(BaseModel):
"""S3 backup configuration model for API."""
access_key_id: str = Field(..., description="AWS S3 access key ID")
secret_access_key: str = Field(..., description="AWS S3 secret access key")
bucket_name: str = Field(..., description="S3 bucket name")
region: str = Field(default="us-east-1", description="AWS S3 region")
endpoint_url: Optional[str] = Field(default=None, description="Custom S3 endpoint URL")
endpoint_url: Optional[str] = Field(
default=None, description="Custom S3 endpoint URL"
)
path_style: bool = Field(default=False, description="Use path-style addressing")
enabled: bool = Field(default=True, description="Enable S3 backups")
class BackupSettings(BaseModel):
"""Backup settings model for API."""
s3: Optional[S3Config] = None
class BackupTest(BaseModel):
"""Backup connection test request model."""
service: str = Field(..., description="Backup service type (s3)")
config: S3Config = Field(..., description="S3 configuration to test")
class BackupInfo(BaseModel):
"""Backup file information model."""
key: str = Field(..., description="S3 object key")
last_modified: str = Field(..., description="Last modified timestamp (ISO format)")
size: int = Field(..., description="File size in bytes")
@@ -40,6 +42,8 @@ class BackupInfo(BaseModel):
class BackupOperation(BaseModel):
"""Backup operation request model."""
operation: str = Field(..., description="Operation type (backup, restore)")
backup_key: Optional[str] = Field(default=None, description="Backup key for restore operations")
backup_key: Optional[str] = Field(
default=None, description="Backup key for restore operations"
)

View File

@@ -1,6 +1,5 @@
"""API routes for backup management."""
from fastapi import APIRouter, HTTPException
from loguru import logger
@@ -24,10 +23,10 @@ async def get_backup_settings() -> APIResponse:
"""Get current backup settings."""
try:
backup_config = config.backup_config
# Build response safely without exposing secrets
s3_config = backup_config.get("s3", {})
settings = BackupSettings(
s3=S3Config(
access_key_id="***" if s3_config.get("access_key_id") else "",
@@ -41,13 +40,13 @@ async def get_backup_settings() -> APIResponse:
if s3_config.get("bucket_name")
else None,
)
return APIResponse(
success=True,
data=settings,
message="Backup settings retrieved successfully",
)
except Exception as e:
logger.error(f"Failed to get backup settings: {e}")
raise HTTPException(
@@ -71,20 +70,20 @@ async def update_backup_settings(settings: BackupSettings) -> APIResponse:
path_style=settings.s3.path_style,
enabled=settings.s3.enabled,
)
# Test connection
backup_service = BackupService()
connection_success = await backup_service.test_connection(s3_config)
if not connection_success:
raise HTTPException(
status_code=400,
detail="S3 connection test failed. Please check your configuration."
detail="S3 connection test failed. Please check your configuration.",
)
# Update backup config
backup_config = {}
if settings.s3:
backup_config["s3"] = {
"access_key_id": settings.s3.access_key_id,
@@ -95,17 +94,17 @@ async def update_backup_settings(settings: BackupSettings) -> APIResponse:
"path_style": settings.s3.path_style,
"enabled": settings.s3.enabled,
}
# Save to config
if backup_config:
config.update_section("backup", backup_config)
return APIResponse(
success=True,
data={"updated": True},
message="Backup settings updated successfully",
)
except HTTPException:
raise
except Exception as e:
@@ -123,7 +122,7 @@ async def test_backup_connection(test_request: BackupTest) -> APIResponse:
raise HTTPException(
status_code=400, detail="Only 's3' service is supported"
)
# Convert API model to config model
s3_config = S3BackupConfig(
access_key_id=test_request.config.access_key_id,
@@ -134,10 +133,10 @@ async def test_backup_connection(test_request: BackupTest) -> APIResponse:
path_style=test_request.config.path_style,
enabled=test_request.config.enabled,
)
backup_service = BackupService()
success = await backup_service.test_connection(s3_config)
if success:
return APIResponse(
success=True,
@@ -149,7 +148,7 @@ async def test_backup_connection(test_request: BackupTest) -> APIResponse:
success=False,
message="S3 connection test failed",
)
except HTTPException:
raise
except Exception as e:
@@ -164,26 +163,26 @@ async def list_backups() -> APIResponse:
"""List available backups."""
try:
backup_config = config.backup_config.get("s3", {})
if not backup_config.get("bucket_name"):
return APIResponse(
success=True,
data=[],
message="No S3 backup configuration found",
)
# Convert config to model
s3_config = S3BackupConfig(**backup_config)
backup_service = BackupService(s3_config)
backups = await backup_service.list_backups()
return APIResponse(
success=True,
data=backups,
message=f"Found {len(backups)} backups",
)
except Exception as e:
logger.error(f"Failed to list backups: {e}")
raise HTTPException(
@@ -196,12 +195,10 @@ async def backup_operation(operation_request: BackupOperation) -> APIResponse:
"""Perform backup operation (backup or restore)."""
try:
backup_config = config.backup_config.get("s3", {})
if not backup_config.get("bucket_name"):
raise HTTPException(
status_code=400, detail="S3 backup is not configured"
)
raise HTTPException(status_code=400, detail="S3 backup is not configured")
# Convert config to model with validation
try:
s3_config = S3BackupConfig(**backup_config)
@@ -209,14 +206,14 @@ async def backup_operation(operation_request: BackupOperation) -> APIResponse:
raise HTTPException(
status_code=400, detail=f"Invalid S3 configuration: {str(e)}"
) from e
backup_service = BackupService(s3_config)
if operation_request.operation == "backup":
# Backup database
database_path = path_manager.get_database_path()
success = await backup_service.backup_database(database_path)
if success:
return APIResponse(
success=True,
@@ -228,19 +225,20 @@ async def backup_operation(operation_request: BackupOperation) -> APIResponse:
success=False,
message="Database backup failed",
)
elif operation_request.operation == "restore":
if not operation_request.backup_key:
raise HTTPException(
status_code=400, detail="backup_key is required for restore operation"
status_code=400,
detail="backup_key is required for restore operation",
)
# Restore database
database_path = path_manager.get_database_path()
success = await backup_service.restore_database(
operation_request.backup_key, database_path
)
if success:
return APIResponse(
success=True,
@@ -256,11 +254,11 @@ async def backup_operation(operation_request: BackupOperation) -> APIResponse:
raise HTTPException(
status_code=400, detail="Invalid operation. Use 'backup' or 'restore'"
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to perform backup operation: {e}")
raise HTTPException(
status_code=500, detail=f"Failed to perform backup operation: {str(e)}"
) from e
) from e

View File

@@ -37,7 +37,9 @@ class S3BackupConfig(BaseModel):
secret_access_key: str = Field(..., description="AWS S3 secret access key")
bucket_name: str = Field(..., description="S3 bucket name")
region: str = Field(default="us-east-1", description="AWS S3 region")
endpoint_url: Optional[str] = Field(default=None, description="Custom S3 endpoint URL")
endpoint_url: Optional[str] = Field(
default=None, description="Custom S3 endpoint URL"
)
path_style: bool = Field(default=False, description="Use path-style addressing")
enabled: bool = Field(default=True, description="Enable S3 backups")