feat: implement manual per-account refresh for instant ban and cooldown updates

This commit is contained in:
2026-02-21 04:16:41 +01:00
parent cf78e3c329
commit c208ecea95
4 changed files with 93 additions and 35 deletions

View File

@@ -188,6 +188,57 @@ const handleSwitchAccount = async (loginName: string) => {
} catch (e) { return false; } } 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 --- // --- Sync Worker ---
const syncAccounts = async () => { const syncAccounts = async () => {
initBackend(); initBackend();
@@ -244,30 +295,13 @@ const syncAccounts = async () => {
const now = new Date(); const now = new Date();
// OPTIMIZATION: Ensure ALL authenticated accounts are shared with the server on every sync cycle // 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_')) { if (backend && !account._id.startsWith('shared_')) {
console.log(`[Sync] Reconciling account with server: ${account.personaName}`);
await backend.shareAccount(account); await backend.shareAccount(account);
} }
const lastCheck = account.lastBanCheck ? new Date(account.lastBanCheck) : new Date(0); const lastCheck = account.lastBanCheck ? new Date(account.lastBanCheck) : new Date(0);
if ((now.getTime() - lastCheck.getTime()) / 3600000 > 6 || !account.personaName) { if ((now.getTime() - lastCheck.getTime()) / 3600000 > 6 || !account.personaName) {
const profile = await fetchProfileData(account.steamId, account.steamLoginSecure); await scrapeAccountData(account);
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);
scrapeChanges = true; scrapeChanges = true;
} }
@@ -276,20 +310,8 @@ const syncAccounts = async () => {
const lastScrape = account.lastScrapeTime ? new Date(account.lastScrapeTime) : new Date(0); const lastScrape = account.lastScrapeTime ? new Date(account.lastScrapeTime) : new Date(0);
if ((now.getTime() - lastScrape.getTime()) / 3600000 > 8) { if ((now.getTime() - lastScrape.getTime()) / 3600000 > 8) {
await new Promise(r => setTimeout(r, Math.floor(Math.random() * 60000) + 5000)); await new Promise(r => setTimeout(r, Math.floor(Math.random() * 60000) + 5000));
try { await scrapeAccountData(account);
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; scrapeChanges = true;
} catch (e: any) {
if (e.message.includes('cookie') || e.message.includes('Sign In')) { account.authError = true; scrapeChanges = true; }
}
} }
} }
} catch (error) { } } 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('get-server-user-info', () => ({ steamId: store.get('serverConfig').serverSteamId }));
ipcMain.handle('sync-now', async () => { await syncAccounts(); return true; }); 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 }) => { ipcMain.handle('add-account', async (event, { identifier }) => {
try { try {
initBackend(); initBackend();

View File

@@ -19,6 +19,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
loginToServer: () => ipcRenderer.invoke('login-to-server'), loginToServer: () => ipcRenderer.invoke('login-to-server'),
getServerUserInfo: () => ipcRenderer.invoke('get-server-user-info'), getServerUserInfo: () => ipcRenderer.invoke('get-server-user-info'),
syncNow: () => ipcRenderer.invoke('sync-now'), syncNow: () => ipcRenderer.invoke('sync-now'),
scrapeAccount: (steamId: string) => ipcRenderer.invoke('scrape-account', steamId),
getCommunityAccounts: () => ipcRenderer.invoke('get-community-accounts'), getCommunityAccounts: () => ipcRenderer.invoke('get-community-accounts'),
getServerUsers: () => ipcRenderer.invoke('get-server-users'), getServerUsers: () => ipcRenderer.invoke('get-server-users'),

View File

@@ -49,6 +49,7 @@ interface AccountsContextType {
updateServerConfig: (config: Partial<ServerConfig>) => Promise<void>; updateServerConfig: (config: Partial<ServerConfig>) => Promise<void>;
loginToServer: () => Promise<void>; loginToServer: () => Promise<void>;
syncNow: () => Promise<void>; syncNow: () => Promise<void>;
scrapeAccount: (steamId: string) => Promise<boolean>;
getCommunityAccounts: () => Promise<any[]>; getCommunityAccounts: () => Promise<any[]>;
getServerUsers: () => Promise<any[]>; getServerUsers: () => Promise<any[]>;
refreshAccounts: (showLoading?: boolean) => Promise<void>; refreshAccounts: (showLoading?: boolean) => Promise<void>;
@@ -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 }) => { const addAccount = async (data: { identifier: string }) => {
await (window as any).electronAPI.addAccount(data); await (window as any).electronAPI.addAccount(data);
await refreshAccounts(); await refreshAccounts();
@@ -194,7 +201,7 @@ export const AccountsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
accounts, serverConfig, isLoading, isSyncing, addAccount, updateAccount, deleteAccount, accounts, serverConfig, isLoading, isSyncing, addAccount, updateAccount, deleteAccount,
switchAccount, openSteamAppLogin, openSteamLogin, updateServerConfig, loginToServer, switchAccount, openSteamAppLogin, openSteamLogin, updateServerConfig, loginToServer,
getCommunityAccounts, getServerUsers, shareAccountWithUser, revokeAccountAccess, revokeAllAccountAccess, syncNow, refreshAccounts, getCommunityAccounts, getServerUsers, shareAccountWithUser, revokeAccountAccess, revokeAllAccountAccess, syncNow, refreshAccounts,
adminGetStats, adminGetUsers, adminDeleteUser, adminGetAccounts, adminRemoveAccount scrapeAccount, adminGetStats, adminGetUsers, adminDeleteUser, adminGetAccounts, adminRemoveAccount
}}> }}>
{children} {children}
</AccountsContext.Provider> </AccountsContext.Provider>

View File

@@ -376,11 +376,12 @@ const AccountRow: React.FC<{
onSwitch: (login: string) => void, onSwitch: (login: string) => void,
onAuth: () => void onAuth: () => void
}> = ({ account, onDelete, onSwitch, onAuth }) => { }> = ({ account, onDelete, onSwitch, onAuth }) => {
const { shareAccountWithUser, revokeAccountAccess, revokeAllAccountAccess, getServerUsers, serverConfig } = useAccounts(); const { shareAccountWithUser, revokeAccountAccess, revokeAllAccountAccess, getServerUsers, serverConfig, scrapeAccount } = useAccounts();
const [timeLeft, setTimeLeft] = useState<string | null>(null); const [timeLeft, setTimeLeft] = useState<string | null>(null);
const [isShareOpen, setIsShareOpen] = useState(false); const [isShareOpen, setIsShareOpen] = useState(false);
const [targetUserId, setTargetUserId] = useState(''); const [targetUserId, setTargetUserId] = useState('');
const [isSharing, setIsSharing] = useState(false); const [isSharing, setIsSharing] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const [serverUsers, setServerUsers] = useState<any[]>([]); const [serverUsers, setServerUsers] = useState<any[]>([]);
const cooldownDate = account?.cooldownExpiresAt ? new Date(account.cooldownExpiresAt) : null; const cooldownDate = account?.cooldownExpiresAt ? new Date(account.cooldownExpiresAt) : null;
@@ -404,6 +405,12 @@ const AccountRow: React.FC<{
const [imgSrc, setImgSrc] = useState(avatarSrc); const [imgSrc, setImgSrc] = useState(avatarSrc);
useEffect(() => { setImgSrc(avatarSrc); }, [avatarSrc]); useEffect(() => { setImgSrc(avatarSrc); }, [avatarSrc]);
const handleRefresh = async () => {
setIsRefreshing(true);
await scrapeAccount(account.steamId);
setIsRefreshing(false);
};
const handleOpenShare = async () => { const handleOpenShare = async () => {
setIsShareOpen(true); setIsShareOpen(true);
try { try {
@@ -522,7 +529,12 @@ const AccountRow: React.FC<{
{account.steamLoginSecure && !account.authError ? <VerifiedUserIcon fontSize="inherit" /> : (account.authError ? <LockResetIcon fontSize="inherit" /> : <BoltIcon fontSize="inherit" />)} {account.steamLoginSecure && !account.authError ? <VerifiedUserIcon fontSize="inherit" /> : (account.authError ? <LockResetIcon fontSize="inherit" /> : <BoltIcon fontSize="inherit" />)}
</IconButton> </IconButton>
{account.steamLoginSecure && !account.authError && ( {account.steamLoginSecure && !account.authError && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Typography variant="caption" sx={{ color: 'success.main', fontWeight: 'bold', fontSize: '0.6rem' }}>TRACKING</Typography> <Typography variant="caption" sx={{ color: 'success.main', fontWeight: 'bold', fontSize: '0.6rem' }}>TRACKING</Typography>
<IconButton size="small" onClick={handleRefresh} disabled={isRefreshing} sx={{ p: 0.2, color: 'text.secondary', '&:hover': { color: 'primary.main' } }}>
{isRefreshing ? <CircularProgress size={10} color="inherit" /> : <SyncIcon sx={{ fontSize: 12 }} />}
</IconButton>
</Box>
)} )}
</Box> </Box>
</Tooltip> </Tooltip>