Run bank sync in background to avoid gateway timeouts
sync-now and sync-all now return 202 Accepted immediately and dispatch work to a background task. The frontend uses the existing SignalR BankSyncComplete notification to stop the spinner and display results. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -47,10 +47,10 @@ export const bankSyncApi = {
|
|||||||
api.put<LinkedAccountInfo>(`/bank-sync/linked-accounts/${linkedAccountId}`, data),
|
api.put<LinkedAccountInfo>(`/bank-sync/linked-accounts/${linkedAccountId}`, data),
|
||||||
|
|
||||||
syncNow: (connectionId: string) =>
|
syncNow: (connectionId: string) =>
|
||||||
api.post<SyncResult>(`/bank-sync/connections/${connectionId}/sync-now`),
|
api.post(`/bank-sync/connections/${connectionId}/sync-now`),
|
||||||
|
|
||||||
syncAll: () =>
|
syncAll: () =>
|
||||||
api.post<SyncResult>('/bank-sync/sync-all'),
|
api.post('/bank-sync/sync-all'),
|
||||||
|
|
||||||
getSyncLogs: (connectionId?: string, limit = 50) =>
|
getSyncLogs: (connectionId?: string, limit = 50) =>
|
||||||
api.get<SyncLogEntry[]>('/bank-sync/sync-logs', {
|
api.get<SyncLogEntry[]>('/bank-sync/sync-logs', {
|
||||||
|
|||||||
@@ -514,12 +514,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { useNotifications } from '@/composables/useNotifications'
|
||||||
import { bankSyncApi } from '@/services/bankSync'
|
import { bankSyncApi } from '@/services/bankSync'
|
||||||
import { accountsApi } from '@/services/accounts'
|
import { accountsApi } from '@/services/accounts'
|
||||||
import { aiCategorizationApi } from '@/services/aiCategorization'
|
import { aiCategorizationApi } from '@/services/aiCategorization'
|
||||||
import type { SyncConnection, LinkedAccountInfo, Account, SyncLogEntry, PlaidCredentials, AiCategorizationSettings, OllamaConnectionTestResult } from '@/types'
|
import type { SyncConnection, LinkedAccountInfo, Account, SyncLogEntry, PlaidCredentials, AiCategorizationSettings, OllamaConnectionTestResult, BankSyncNotification } from '@/types'
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
@@ -801,18 +802,27 @@ async function toggleConnection(conn: SyncConnection) {
|
|||||||
async function syncNow(conn: SyncConnection) {
|
async function syncNow(conn: SyncConnection) {
|
||||||
syncingConnections[conn.id] = true
|
syncingConnections[conn.id] = true
|
||||||
try {
|
try {
|
||||||
const { data } = await bankSyncApi.syncNow(conn.id)
|
await bankSyncApi.syncNow(conn.id)
|
||||||
syncResultMessage.value = `Synced ${conn.institutionName}: ${data.transactionsImported} imported, ${data.transactionsSkipped} skipped`
|
|
||||||
showSyncResult.value = true
|
|
||||||
await loadConnections()
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
syncingConnections[conn.id] = false
|
||||||
errorMessage.value = err.response?.data?.error || 'Sync failed'
|
errorMessage.value = err.response?.data?.error || 'Sync failed'
|
||||||
showError.value = true
|
showError.value = true
|
||||||
} finally {
|
|
||||||
syncingConnections[conn.id] = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle sync results via SignalR notification
|
||||||
|
const { notifications } = useNotifications()
|
||||||
|
watch(notifications, (notifs) => {
|
||||||
|
const latest = notifs[0]
|
||||||
|
if (latest && !latest.read && latest.type === 'BankSyncComplete') {
|
||||||
|
const data = latest.data as BankSyncNotification
|
||||||
|
syncingConnections[data.connectionId] = false
|
||||||
|
syncResultMessage.value = `Synced ${data.institutionName}: ${data.transactionsImported} imported, ${data.transactionsSkipped} skipped`
|
||||||
|
showSyncResult.value = true
|
||||||
|
loadConnections()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
function confirmDeleteConnection(conn: SyncConnection) {
|
function confirmDeleteConnection(conn: SyncConnection) {
|
||||||
deleteTarget.value = conn
|
deleteTarget.value = conn
|
||||||
showDeleteDialog.value = true
|
showDeleteDialog.value = true
|
||||||
|
|||||||
@@ -12,9 +12,16 @@ namespace Purrse.Api.Controllers;
|
|||||||
public class BankSyncController : ControllerBase
|
public class BankSyncController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IBankSyncService _bankSyncService;
|
private readonly IBankSyncService _bankSyncService;
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
private readonly ILogger<BankSyncController> _logger;
|
||||||
private Guid UserId => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
private Guid UserId => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||||
|
|
||||||
public BankSyncController(IBankSyncService bankSyncService) => _bankSyncService = bankSyncService;
|
public BankSyncController(IBankSyncService bankSyncService, IServiceScopeFactory scopeFactory, ILogger<BankSyncController> logger)
|
||||||
|
{
|
||||||
|
_bankSyncService = bankSyncService;
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("connections")]
|
[HttpGet("connections")]
|
||||||
public async Task<ActionResult<List<SyncConnectionResponse>>> GetConnections()
|
public async Task<ActionResult<List<SyncConnectionResponse>>> GetConnections()
|
||||||
@@ -68,12 +75,32 @@ public class BankSyncController : ControllerBase
|
|||||||
=> Ok(await _bankSyncService.UpdateLinkedAccountAsync(UserId, id, request));
|
=> Ok(await _bankSyncService.UpdateLinkedAccountAsync(UserId, id, request));
|
||||||
|
|
||||||
[HttpPost("connections/{id}/sync-now")]
|
[HttpPost("connections/{id}/sync-now")]
|
||||||
public async Task<ActionResult<SyncResultResponse>> SyncNow(Guid id)
|
public IActionResult SyncNow(Guid id)
|
||||||
=> Ok(await _bankSyncService.SyncNowAsync(UserId, id));
|
{
|
||||||
|
var userId = UserId;
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var service = scope.ServiceProvider.GetRequiredService<IBankSyncService>();
|
||||||
|
try { await service.SyncNowAsync(userId, id); }
|
||||||
|
catch (Exception ex) { _logger.LogError(ex, "Background sync failed for connection {Id}", id); }
|
||||||
|
});
|
||||||
|
return Accepted();
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("sync-all")]
|
[HttpPost("sync-all")]
|
||||||
public async Task<ActionResult<SyncResultResponse>> SyncAll()
|
public IActionResult SyncAll()
|
||||||
=> Ok(await _bankSyncService.SyncAllAsync(UserId));
|
{
|
||||||
|
var userId = UserId;
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var service = scope.ServiceProvider.GetRequiredService<IBankSyncService>();
|
||||||
|
try { await service.SyncAllAsync(userId); }
|
||||||
|
catch (Exception ex) { _logger.LogError(ex, "Background sync-all failed for user {UserId}", userId); }
|
||||||
|
});
|
||||||
|
return Accepted();
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("sync-logs")]
|
[HttpGet("sync-logs")]
|
||||||
public async Task<ActionResult<List<SyncLogResponse>>> GetSyncLogs([FromQuery] Guid? connectionId, [FromQuery] int limit = 50)
|
public async Task<ActionResult<List<SyncLogResponse>>> GetSyncLogs([FromQuery] Guid? connectionId, [FromQuery] int limit = 50)
|
||||||
|
|||||||
Reference in New Issue
Block a user