From c208ecea954eca5a75d528aa80257631b0a80cd4 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 21 Feb 2026 04:16:41 +0100 Subject: [PATCH] feat: implement manual per-account refresh for instant ban and cooldown updates --- frontend/electron/main.ts | 102 ++++++++++++++++++++--------- frontend/electron/preload.ts | 1 + frontend/src/hooks/useAccounts.tsx | 9 ++- frontend/src/pages/Dashboard.tsx | 16 ++++- 4 files changed, 93 insertions(+), 35 deletions(-) diff --git a/frontend/electron/main.ts b/frontend/electron/main.ts index 28f25f7..8413f1e 100644 --- a/frontend/electron/main.ts +++ b/frontend/electron/main.ts @@ -188,6 +188,57 @@ const handleSwitchAccount = async (loginName: string) => { } catch (e) { return false; } }; +// --- Scraper Helper --- +const scrapeAccountData = async (account: Account) => { + const now = new Date(); + try { + // 1. Refresh Basic Profile & Bans + const profile = await fetchProfileData(account.steamId, account.steamLoginSecure); + const bans = await scrapeBanStatus(profile.profileUrl, account.steamLoginSecure); + + account.personaName = profile.personaName; + account.profileUrl = profile.profileUrl; + account.vacBanned = bans.vacBanned; + account.gameBans = bans.gameBans; + account.status = (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none'; + account.lastBanCheck = now.toISOString(); + + if (profile.avatar && (!account.localAvatar || profile.avatar !== account.avatar)) { + account.avatar = profile.avatar; + const localPath = await downloadAvatar(account.steamId, profile.avatar); + if (localPath) account.localAvatar = localPath; + } + + // 2. Refresh Cooldowns if session is active + if (account.steamLoginSecure) { + try { + const result = await scrapeCooldown(account.steamId, account.steamLoginSecure); + account.authError = false; + account.lastScrapeTime = now.toISOString(); + if (result.isActive) { + account.cooldownExpiresAt = result.expiresAt ? result.expiresAt.toISOString() : new Date(Date.now() + 86400000).toISOString(); + if (backend) await backend.pushCooldown(account.steamId, account.cooldownExpiresAt); + } else { + account.cooldownExpiresAt = undefined; + if (backend) await backend.pushCooldown(account.steamId, undefined); + } + } catch (e: any) { + if (e.message.includes('cookie') || e.message.includes('Sign In')) account.authError = true; + } + } + + // 3. Share updated state with backend + if (backend && !account._id.startsWith('shared_')) { + await backend.shareAccount(account); + } + + return true; + } catch (e) { + console.error(`[Scraper] Failed to scrape ${account.personaName}:`, e); + return false; + } +}; + // --- Sync Worker --- const syncAccounts = async () => { initBackend(); @@ -244,30 +295,13 @@ const syncAccounts = async () => { const now = new Date(); // OPTIMIZATION: Ensure ALL authenticated accounts are shared with the server on every sync cycle - // this guarantees that even if a push failed previously, it will be reconciled now. if (backend && !account._id.startsWith('shared_')) { - console.log(`[Sync] Reconciling account with server: ${account.personaName}`); await backend.shareAccount(account); } const lastCheck = account.lastBanCheck ? new Date(account.lastBanCheck) : new Date(0); if ((now.getTime() - lastCheck.getTime()) / 3600000 > 6 || !account.personaName) { - const profile = await fetchProfileData(account.steamId, account.steamLoginSecure); - const bans = await scrapeBanStatus(profile.profileUrl, account.steamLoginSecure); - account.personaName = profile.personaName; account.profileUrl = profile.profileUrl; - account.vacBanned = bans.vacBanned; account.gameBans = bans.gameBans; - account.status = (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none'; - account.lastBanCheck = now.toISOString(); - if (profile.avatar && (!account.localAvatar || profile.avatar !== account.avatar)) { - account.avatar = profile.avatar; - const localPath = await downloadAvatar(account.steamId, profile.avatar); - if (localPath) account.localAvatar = localPath; - } - if (account.loginName) { - const config = steamClient.extractAccountConfig(account.loginName); - if (config) { account.loginConfig = config; account.sessionUpdatedAt = new Date().toISOString(); } - } - if (backend) await backend.shareAccount(account); + await scrapeAccountData(account); scrapeChanges = true; } @@ -276,20 +310,8 @@ const syncAccounts = async () => { const lastScrape = account.lastScrapeTime ? new Date(account.lastScrapeTime) : new Date(0); if ((now.getTime() - lastScrape.getTime()) / 3600000 > 8) { await new Promise(r => setTimeout(r, Math.floor(Math.random() * 60000) + 5000)); - try { - const result = await scrapeCooldown(account.steamId, account.steamLoginSecure); - account.authError = false; account.lastScrapeTime = new Date().toISOString(); - if (result.isActive) { - account.cooldownExpiresAt = result.expiresAt ? result.expiresAt.toISOString() : new Date(Date.now() + 86400000).toISOString(); - if (backend) await backend.pushCooldown(account.steamId, account.cooldownExpiresAt); - } else if (account.cooldownExpiresAt) { - account.cooldownExpiresAt = undefined; - if (backend) await backend.pushCooldown(account.steamId, undefined); - } - scrapeChanges = true; - } catch (e: any) { - if (e.message.includes('cookie') || e.message.includes('Sign In')) { account.authError = true; scrapeChanges = true; } - } + await scrapeAccountData(account); + scrapeChanges = true; } } } catch (error) { } @@ -437,6 +459,22 @@ ipcMain.handle('login-to-server', async () => { ipcMain.handle('get-server-user-info', () => ({ steamId: store.get('serverConfig').serverSteamId })); ipcMain.handle('sync-now', async () => { await syncAccounts(); return true; }); + +ipcMain.handle('scrape-account', async (event, steamId: string) => { + const accounts = store.get('accounts') as Account[]; + const account = accounts.find(a => a.steamId === steamId); + if (!account) return false; + + console.log(`[Main] Manually triggering scrape for ${account.personaName}...`); + const success = await scrapeAccountData(account); + if (success) { + store.set('accounts', accounts); + if (mainWindow) mainWindow.webContents.send('accounts-updated', accounts); + updateTrayMenu(); + } + return success; +}); + ipcMain.handle('add-account', async (event, { identifier }) => { try { initBackend(); diff --git a/frontend/electron/preload.ts b/frontend/electron/preload.ts index c8c8385..5da78f1 100644 --- a/frontend/electron/preload.ts +++ b/frontend/electron/preload.ts @@ -19,6 +19,7 @@ contextBridge.exposeInMainWorld('electronAPI', { loginToServer: () => ipcRenderer.invoke('login-to-server'), getServerUserInfo: () => ipcRenderer.invoke('get-server-user-info'), syncNow: () => ipcRenderer.invoke('sync-now'), + scrapeAccount: (steamId: string) => ipcRenderer.invoke('scrape-account', steamId), getCommunityAccounts: () => ipcRenderer.invoke('get-community-accounts'), getServerUsers: () => ipcRenderer.invoke('get-server-users'), diff --git a/frontend/src/hooks/useAccounts.tsx b/frontend/src/hooks/useAccounts.tsx index aed28de..e5722e5 100644 --- a/frontend/src/hooks/useAccounts.tsx +++ b/frontend/src/hooks/useAccounts.tsx @@ -49,6 +49,7 @@ interface AccountsContextType { updateServerConfig: (config: Partial) => Promise; loginToServer: () => Promise; syncNow: () => Promise; + scrapeAccount: (steamId: string) => Promise; getCommunityAccounts: () => Promise; getServerUsers: () => Promise; refreshAccounts: (showLoading?: boolean) => Promise; @@ -114,6 +115,12 @@ export const AccountsProvider: React.FC<{ children: React.ReactNode }> = ({ chil } }; + const scrapeAccount = async (steamId: string) => { + const success = await (window as any).electronAPI.scrapeAccount(steamId); + if (success) await syncNow(); + return success; + }; + const addAccount = async (data: { identifier: string }) => { await (window as any).electronAPI.addAccount(data); await refreshAccounts(); @@ -194,7 +201,7 @@ export const AccountsProvider: React.FC<{ children: React.ReactNode }> = ({ chil accounts, serverConfig, isLoading, isSyncing, addAccount, updateAccount, deleteAccount, switchAccount, openSteamAppLogin, openSteamLogin, updateServerConfig, loginToServer, getCommunityAccounts, getServerUsers, shareAccountWithUser, revokeAccountAccess, revokeAllAccountAccess, syncNow, refreshAccounts, - adminGetStats, adminGetUsers, adminDeleteUser, adminGetAccounts, adminRemoveAccount + scrapeAccount, adminGetStats, adminGetUsers, adminDeleteUser, adminGetAccounts, adminRemoveAccount }}> {children} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 16d6dee..b1e25f4 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -376,11 +376,12 @@ const AccountRow: React.FC<{ onSwitch: (login: string) => void, onAuth: () => void }> = ({ account, onDelete, onSwitch, onAuth }) => { - const { shareAccountWithUser, revokeAccountAccess, revokeAllAccountAccess, getServerUsers, serverConfig } = useAccounts(); + const { shareAccountWithUser, revokeAccountAccess, revokeAllAccountAccess, getServerUsers, serverConfig, scrapeAccount } = useAccounts(); const [timeLeft, setTimeLeft] = useState(null); const [isShareOpen, setIsShareOpen] = useState(false); const [targetUserId, setTargetUserId] = useState(''); const [isSharing, setIsSharing] = useState(false); + const [isRefreshing, setIsRefreshing] = useState(false); const [serverUsers, setServerUsers] = useState([]); const cooldownDate = account?.cooldownExpiresAt ? new Date(account.cooldownExpiresAt) : null; @@ -404,6 +405,12 @@ const AccountRow: React.FC<{ const [imgSrc, setImgSrc] = useState(avatarSrc); useEffect(() => { setImgSrc(avatarSrc); }, [avatarSrc]); + const handleRefresh = async () => { + setIsRefreshing(true); + await scrapeAccount(account.steamId); + setIsRefreshing(false); + }; + const handleOpenShare = async () => { setIsShareOpen(true); try { @@ -522,7 +529,12 @@ const AccountRow: React.FC<{ {account.steamLoginSecure && !account.authError ? : (account.authError ? : )} {account.steamLoginSecure && !account.authError && ( - TRACKING + + TRACKING + + {isRefreshing ? : } + + )}