4 Commits

8 changed files with 285 additions and 274 deletions

View File

@@ -70,27 +70,19 @@ const createTray = () => {
break; break;
} }
} }
console.log(`[Tray] Attempting to initialize with icon: ${iconPath || 'NONE FOUND'}`); if (!iconPath)
if (!iconPath) {
console.warn(`[Tray] FAILED: No valid icon found in ${assetsDir}`);
return; return;
}
try { try {
const icon = electron_1.nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 }); const icon = electron_1.nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 });
tray = new electron_1.Tray(icon); tray = new electron_1.Tray(icon);
tray.setToolTip('Ultimate Ban Tracker'); tray.setToolTip('Ultimate Ban Tracker');
tray.on('click', () => { tray.on('click', () => { if (mainWindow) {
if (mainWindow) { mainWindow.show();
mainWindow.show(); mainWindow.focus();
mainWindow.focus(); } });
}
});
updateTrayMenu(); updateTrayMenu();
console.log(`[Tray] Successfully initialized`);
}
catch (e) {
console.error(`[Tray] Critical error during initialization: ${e.message}`);
} }
catch (e) { }
}; };
const updateTrayMenu = () => { const updateTrayMenu = () => {
if (!tray) if (!tray)
@@ -108,11 +100,7 @@ const updateTrayMenu = () => {
click: () => handleSwitchAccount(acc.loginName) click: () => handleSwitchAccount(acc.loginName)
})) : [{ label: 'No accounts found', enabled: false }] })) : [{ label: 'No accounts found', enabled: false }]
}, },
{ { label: 'Sync Now', enabled: !!config?.enabled, click: () => syncAccounts(true) },
label: 'Sync Now',
enabled: !!config?.enabled,
click: () => syncAccounts()
},
{ type: 'separator' }, { type: 'separator' },
{ label: 'Show Dashboard', click: () => { if (mainWindow) { label: 'Show Dashboard', click: () => { if (mainWindow)
mainWindow.show(); } }, mainWindow.show(); } },
@@ -155,8 +143,58 @@ const handleSwitchAccount = async (loginName) => {
return false; return false;
} }
}; };
// --- Scraper Helper ---
const scrapeAccountData = async (account) => {
const now = new Date();
try {
const profile = await (0, steam_web_1.fetchProfileData)(account.steamId, account.steamLoginSecure);
const bans = await (0, steam_web_1.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.steamLoginSecure) {
try {
const result = await (0, scraper_1.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) {
if (e.message.includes('cookie') || e.message.includes('Sign In'))
account.authError = true;
}
}
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 (isManual = false) => {
console.log(`[Sync] Phase 1: Pulling from server...`);
initBackend(); initBackend();
let accounts = store.get('accounts'); let accounts = store.get('accounts');
let hasChanges = false; let hasChanges = false;
@@ -167,12 +205,13 @@ const syncAccounts = async () => {
const exists = accounts.find(a => a.steamId === s.steamId); const exists = accounts.find(a => a.steamId === s.steamId);
if (!exists) { if (!exists) {
accounts.push({ accounts.push({
_id: `shared_${s.steamId}`, _id: `shared_${s.steamId}`, steamId: s.steamId, personaName: s.personaName,
steamId: s.steamId, personaName: s.personaName, avatar: s.avatar, profileUrl: s.profileUrl, avatar: s.avatar, profileUrl: s.profileUrl, vacBanned: s.vacBanned,
vacBanned: s.vacBanned, gameBans: s.gameBans, cooldownExpiresAt: s.cooldownExpiresAt, gameBans: s.gameBans, cooldownExpiresAt: s.cooldownExpiresAt,
loginName: s.loginName || '', steamLoginSecure: s.steamLoginSecure, loginConfig: s.loginConfig, loginName: s.loginName || '', steamLoginSecure: s.steamLoginSecure,
sessionUpdatedAt: s.sessionUpdatedAt, autoCheckCooldown: !!s.steamLoginSecure, loginConfig: s.loginConfig, sessionUpdatedAt: s.sessionUpdatedAt,
status: (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none', lastBanCheck: new Date().toISOString() autoCheckCooldown: !!s.steamLoginSecure, status: (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none',
lastBanCheck: new Date().toISOString(), sharedWith: s.sharedWith
}); });
hasChanges = true; hasChanges = true;
} }
@@ -196,6 +235,10 @@ const syncAccounts = async () => {
exists.cooldownExpiresAt = s.cooldownExpiresAt; exists.cooldownExpiresAt = s.cooldownExpiresAt;
hasChanges = true; hasChanges = true;
} }
if (JSON.stringify(exists.sharedWith) !== JSON.stringify(s.sharedWith)) {
exists.sharedWith = s.sharedWith;
hasChanges = true;
}
} }
} }
} }
@@ -207,88 +250,44 @@ const syncAccounts = async () => {
mainWindow.webContents.send('accounts-updated', accounts); mainWindow.webContents.send('accounts-updated', accounts);
updateTrayMenu(); updateTrayMenu();
} }
if (accounts.length === 0) // Phase 2: Background Scrapes
return; const runScrapes = async () => {
const updatedAccounts = [...accounts]; console.log(`[Sync] Phase 2: Starting background checks for ${accounts.length} accounts...`);
let scrapeChanges = false; const currentAccounts = [...store.get('accounts')];
for (const account of updatedAccounts) { let scrapeChanges = false;
try { for (const account of currentAccounts) {
const now = new Date(); try {
// OPTIMIZATION: Ensure ALL authenticated accounts are shared with the server on every sync cycle const now = new Date();
// 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);
}
const lastCheck = account.lastBanCheck ? new Date(account.lastBanCheck) : new Date(0);
if ((now.getTime() - lastCheck.getTime()) / 3600000 > 6 || !account.personaName) {
const profile = await (0, steam_web_1.fetchProfileData)(account.steamId, account.steamLoginSecure);
const bans = await (0, steam_web_1.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 = steam_client_1.steamClient.extractAccountConfig(account.loginName);
if (config) {
account.loginConfig = config;
account.sessionUpdatedAt = new Date().toISOString();
}
}
if (backend)
await backend.shareAccount(account); await backend.shareAccount(account);
scrapeChanges = true; const lastCheck = account.lastBanCheck ? new Date(account.lastBanCheck) : new Date(0);
}
if (account.autoCheckCooldown && account.steamLoginSecure) {
if (account.cooldownExpiresAt && new Date(account.cooldownExpiresAt) > now)
continue;
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) { const needsMetadata = (now.getTime() - lastCheck.getTime()) / 3600000 > 6 || !account.personaName;
await new Promise(r => setTimeout(r, Math.floor(Math.random() * 60000) + 5000)); const needsCooldown = account.autoCheckCooldown && account.steamLoginSecure && (now.getTime() - lastScrape.getTime()) / 3600000 > 8;
try { if (needsMetadata || needsCooldown || isManual) {
const result = await (0, scraper_1.scrapeCooldown)(account.steamId, account.steamLoginSecure); if (!isManual && needsCooldown)
account.authError = false; await new Promise(r => setTimeout(r, Math.floor(Math.random() * 30000) + 5000));
account.lastScrapeTime = new Date().toISOString(); if (await scrapeAccountData(account))
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) {
if (e.message.includes('cookie') || e.message.includes('Sign In')) {
account.authError = true;
scrapeChanges = true;
}
}
} }
} }
catch (error) { }
} }
catch (error) { } if (scrapeChanges) {
} store.set('accounts', currentAccounts);
if (scrapeChanges) { if (mainWindow)
store.set('accounts', updatedAccounts); mainWindow.webContents.send('accounts-updated', currentAccounts);
if (mainWindow) updateTrayMenu();
mainWindow.webContents.send('accounts-updated', updatedAccounts); }
updateTrayMenu(); console.log('[Sync] Sync cycle finished.');
} };
if (isManual)
await runScrapes();
else
runScrapes();
}; };
const scheduleNextSync = () => { const scheduleNextSync = () => {
setTimeout(async () => { await syncAccounts(); scheduleNextSync(); }, isDev ? 120000 : 1800000); setTimeout(async () => { await syncAccounts(false); scheduleNextSync(); }, isDev ? 300000 : 1800000);
}; };
// --- Discovery --- // --- Discovery ---
const addingAccounts = new Set(); const addingAccounts = new Set();
@@ -332,7 +331,7 @@ const handleLocalAccountsFound = async (localAccounts) => {
updateTrayMenu(); updateTrayMenu();
} }
}; };
// --- Main Window Creation --- // --- Main Window ---
function createWindow() { function createWindow() {
mainWindow = new electron_1.BrowserWindow({ mainWindow = new electron_1.BrowserWindow({
width: 1280, height: 800, title: "Ultimate Ban Tracker", backgroundColor: '#171a21', autoHideMenuBar: true, width: 1280, height: 800, title: "Ultimate Ban Tracker", backgroundColor: '#171a21', autoHideMenuBar: true,
@@ -351,7 +350,6 @@ function createWindow() {
else else
mainWindow.loadFile(path_1.default.join(__dirname, '..', 'dist', 'index.html')); mainWindow.loadFile(path_1.default.join(__dirname, '..', 'dist', 'index.html'));
} }
// --- App Lifecycle ---
electron_1.app.whenReady().then(() => { electron_1.app.whenReady().then(() => {
electron_1.protocol.handle('steam-resource', (request) => { electron_1.protocol.handle('steam-resource', (request) => {
let rawPath = decodeURIComponent(request.url.replace('steam-resource://', '')); let rawPath = decodeURIComponent(request.url.replace('steam-resource://', ''));
@@ -370,7 +368,7 @@ electron_1.app.whenReady().then(() => {
createWindow(); createWindow();
createTray(); createTray();
initBackend(); initBackend();
setTimeout(syncAccounts, 5000); setTimeout(() => syncAccounts(false), 5000);
scheduleNextSync(); scheduleNextSync();
steam_client_1.steamClient.startWatching(handleLocalAccountsFound); steam_client_1.steamClient.startWatching(handleLocalAccountsFound);
}); });
@@ -397,7 +395,7 @@ electron_1.ipcMain.handle('login-to-server', async () => {
return false; return false;
return new Promise((resolve) => { return new Promise((resolve) => {
const authWindow = new electron_1.BrowserWindow({ const authWindow = new electron_1.BrowserWindow({
width: 800, height: 700, parent: mainWindow || undefined, modal: true, title: 'Login to Ban Tracker Server', width: 800, height: 700, parent: mainWindow || undefined, modal: true, title: 'Login to Server',
webPreferences: { nodeIntegration: false, contextIsolation: true } webPreferences: { nodeIntegration: false, contextIsolation: true }
}); });
authWindow.loadURL(`${config.url}/auth/steam`); authWindow.loadURL(`${config.url}/auth/steam`);
@@ -439,7 +437,21 @@ electron_1.ipcMain.handle('login-to-server', async () => {
}); });
}); });
electron_1.ipcMain.handle('get-server-user-info', () => ({ steamId: store.get('serverConfig').serverSteamId })); electron_1.ipcMain.handle('get-server-user-info', () => ({ steamId: store.get('serverConfig').serverSteamId }));
electron_1.ipcMain.handle('sync-now', async () => { await syncAccounts(); return true; }); electron_1.ipcMain.handle('sync-now', async () => { await syncAccounts(true); return true; });
electron_1.ipcMain.handle('scrape-account', async (event, steamId) => {
const accounts = store.get('accounts');
const account = accounts.find(a => a.steamId === steamId);
if (!account)
return false;
const success = await scrapeAccountData(account);
if (success) {
store.set('accounts', accounts);
if (mainWindow)
mainWindow.webContents.send('accounts-updated', accounts);
updateTrayMenu();
}
return success;
});
electron_1.ipcMain.handle('add-account', async (event, { identifier }) => { electron_1.ipcMain.handle('add-account', async (event, { identifier }) => {
try { try {
initBackend(); initBackend();
@@ -457,7 +469,7 @@ electron_1.ipcMain.handle('add-account', async (event, { identifier }) => {
loginName: existing.loginName || '', steamLoginSecure: existing.steamLoginSecure, loginName: existing.loginName || '', steamLoginSecure: existing.steamLoginSecure,
loginConfig: existing.loginConfig, sessionUpdatedAt: existing.sessionUpdatedAt, loginConfig: existing.loginConfig, sessionUpdatedAt: existing.sessionUpdatedAt,
autoCheckCooldown: !!existing.steamLoginSecure, status: (existing.vacBanned || existing.gameBans > 0) ? 'banned' : 'none', autoCheckCooldown: !!existing.steamLoginSecure, status: (existing.vacBanned || existing.gameBans > 0) ? 'banned' : 'none',
lastBanCheck: new Date().toISOString() lastBanCheck: new Date().toISOString(), sharedWith: existing.sharedWith
}; };
store.set('accounts', [...accounts, newAccount]); store.set('accounts', [...accounts, newAccount]);
updateTrayMenu(); updateTrayMenu();
@@ -535,15 +547,12 @@ electron_1.ipcMain.handle('admin-remove-account', async (event, steamId) => { in
electron_1.ipcMain.handle('switch-account', async (event, loginName) => await handleSwitchAccount(loginName)); electron_1.ipcMain.handle('switch-account', async (event, loginName) => await handleSwitchAccount(loginName));
electron_1.ipcMain.handle('open-external', (event, url) => electron_1.shell.openExternal(url)); electron_1.ipcMain.handle('open-external', (event, url) => electron_1.shell.openExternal(url));
electron_1.ipcMain.handle('open-steam-app-login', async () => { electron_1.ipcMain.handle('open-steam-app-login', async () => {
console.log('[SteamClient] Preparing for fresh login...');
await killSteam(); await killSteam();
if (process.platform === 'win32') { if (process.platform === 'win32') {
// Clear auto-login registry
const clearReg = 'reg add "HKCU\\Software\\Valve\\Steam" /v AutoLoginUser /t REG_SZ /d "" /f'; const clearReg = 'reg add "HKCU\\Software\\Valve\\Steam" /v AutoLoginUser /t REG_SZ /d "" /f';
await new Promise((res) => (0, child_process_1.exec)(clearReg, () => res())); await new Promise((res) => (0, child_process_1.exec)(clearReg, () => res()));
} }
else if (process.platform === 'linux') { else if (process.platform === 'linux') {
// On Linux we can use the steamClient helper to set an empty user
await steam_client_1.steamClient.setAutoLoginUser("", undefined, ""); await steam_client_1.steamClient.setAutoLoginUser("", undefined, "");
} }
const command = process.platform === 'win32' ? 'start steam://open/login' : 'xdg-open steam://open/login'; const command = process.platform === 'win32' ? 'start steam://open/login' : 'xdg-open steam://open/login';
@@ -551,34 +560,20 @@ electron_1.ipcMain.handle('open-steam-app-login', async () => {
return true; return true;
}); });
electron_1.ipcMain.handle('open-steam-login', async (event, expectedSteamId) => { electron_1.ipcMain.handle('open-steam-login', async (event, expectedSteamId) => {
// Use a unique partition per account to prevent session bleeding
const partitionId = expectedSteamId ? `persist:steam-login-${expectedSteamId}` : 'persist:steam-login-new'; const partitionId = expectedSteamId ? `persist:steam-login-${expectedSteamId}` : 'persist:steam-login-new';
const loginSession = electron_1.session.fromPartition(partitionId); const loginSession = electron_1.session.fromPartition(partitionId);
// If adding a brand new account, explicitly clear previous trash if (!expectedSteamId)
if (!expectedSteamId) {
console.log('[Auth] Clearing session for new account login...');
await loginSession.clearStorageData({ storages: ['cookies', 'localstorage', 'indexdb'] }); await loginSession.clearStorageData({ storages: ['cookies', 'localstorage', 'indexdb'] });
}
// If we have an existing cookie string for this account, pre-inject it
if (expectedSteamId) { if (expectedSteamId) {
const accounts = store.get('accounts'); const accounts = store.get('accounts');
const account = accounts.find(a => a.steamId === expectedSteamId); const account = accounts.find(a => a.steamId === expectedSteamId);
if (account?.steamLoginSecure) { if (account?.steamLoginSecure) {
console.log(`[Auth] Pre-injecting existing cookies for ${account.personaName}...`);
const cookiePairs = account.steamLoginSecure.split(';').map(c => c.trim()); const cookiePairs = account.steamLoginSecure.split(';').map(c => c.trim());
for (const pair of cookiePairs) { for (const pair of cookiePairs) {
const [name, value] = pair.split('='); const [name, value] = pair.split('=');
if (name && value) { if (name && value) {
try { try {
await loginSession.cookies.set({ await loginSession.cookies.set({ url: 'https://steamcommunity.com', domain: 'steamcommunity.com', name, value, path: '/', secure: true, httpOnly: name.includes('Secure') });
url: 'https://steamcommunity.com',
domain: 'steamcommunity.com',
name: name,
value: value,
path: '/',
secure: true,
httpOnly: name.includes('Secure')
});
} }
catch (e) { } catch (e) { }
} }

View File

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

View File

@@ -40,6 +40,7 @@ interface Account {
cooldownExpiresAt?: string; cooldownExpiresAt?: string;
authError?: boolean; authError?: boolean;
notes?: string; notes?: string;
sharedWith?: any[];
} }
interface ServerConfig { interface ServerConfig {
@@ -48,6 +49,7 @@ interface ServerConfig {
serverSteamId?: string; serverSteamId?: string;
enabled: boolean; enabled: boolean;
theme?: string; theme?: string;
isAdmin?: boolean;
} }
// --- App State --- // --- App State ---
@@ -91,22 +93,40 @@ const initBackend = () => {
// --- System Tray --- // --- System Tray ---
const createTray = () => { const createTray = () => {
const assetsDir = path.join(__dirname, '..', 'assets-build'); // Try to find the icon in various standard locations
const possibleIcons = ['icon.svg', 'icon.png']; const possiblePaths = [
let iconPath = ''; path.join(__dirname, '..', 'assets-build'), // Dev
path.join(process.resourcesPath, 'assets-build'), // Packaged (External)
path.join(app.getAppPath(), 'dist', 'assets-build'), // Packaged (Internal dist)
path.join(app.getAppPath(), 'assets-build') // Packaged (Internal root)
];
for (const name of possibleIcons) { let assetsDir = '';
const fullPath = path.join(assetsDir, name); for (const p of possiblePaths) {
if (fs.existsSync(fullPath)) { if (fs.existsSync(p)) {
iconPath = fullPath; assetsDir = p;
break; break;
} }
} }
const possibleIcons = ['icon.png', 'icon.svg'];
let iconPath = '';
if (assetsDir) {
for (const name of possibleIcons) {
const fullPath = path.join(assetsDir, name);
if (fs.existsSync(fullPath)) {
iconPath = fullPath;
break;
}
}
}
console.log(`[Tray] Resolved assets directory: ${assetsDir || 'NOT FOUND'}`);
console.log(`[Tray] Attempting to initialize with icon: ${iconPath || 'NONE FOUND'}`); console.log(`[Tray] Attempting to initialize with icon: ${iconPath || 'NONE FOUND'}`);
if (!iconPath) { if (!iconPath) {
console.warn(`[Tray] FAILED: No valid icon found in ${assetsDir}`); console.warn(`[Tray] FAILED: No valid icon found in searched paths.`);
return; return;
} }
@@ -114,24 +134,15 @@ const createTray = () => {
const icon = nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 }); const icon = nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 });
tray = new Tray(icon); tray = new Tray(icon);
tray.setToolTip('Ultimate Ban Tracker'); tray.setToolTip('Ultimate Ban Tracker');
tray.on('click', () => { tray.on('click', () => { if (mainWindow) { mainWindow.show(); mainWindow.focus(); } });
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
});
updateTrayMenu(); updateTrayMenu();
console.log(`[Tray] Successfully initialized`); } catch (e) { }
} catch (e: any) {
console.error(`[Tray] Critical error during initialization: ${e.message}`);
}
}; };
const updateTrayMenu = () => { const updateTrayMenu = () => {
if (!tray) return; if (!tray) return;
const accounts = store.get('accounts') as Account[]; const accounts = store.get('accounts') as Account[];
const config = store.get('serverConfig'); const config = store.get('serverConfig');
const contextMenu = Menu.buildFromTemplate([ const contextMenu = Menu.buildFromTemplate([
{ label: `Ultimate Ban Tracker v${app.getVersion()}`, enabled: false }, { label: `Ultimate Ban Tracker v${app.getVersion()}`, enabled: false },
{ type: 'separator' }, { type: 'separator' },
@@ -143,16 +154,11 @@ const updateTrayMenu = () => {
click: () => handleSwitchAccount(acc.loginName) click: () => handleSwitchAccount(acc.loginName)
})) : [{ label: 'No accounts found', enabled: false }] })) : [{ label: 'No accounts found', enabled: false }]
}, },
{ { label: 'Sync Now', enabled: !!config?.enabled, click: () => syncAccounts(true) },
label: 'Sync Now',
enabled: !!config?.enabled,
click: () => syncAccounts()
},
{ type: 'separator' }, { type: 'separator' },
{ label: 'Show Dashboard', click: () => { if (mainWindow) mainWindow.show(); } }, { label: 'Show Dashboard', click: () => { if (mainWindow) mainWindow.show(); } },
{ label: 'Quit', click: () => { (app as any).isQuitting = true; app.quit(); } } { label: 'Quit', click: () => { (app as any).isQuitting = true; app.quit(); } }
]); ]);
tray.setContextMenu(contextMenu); tray.setContextMenu(contextMenu);
}; };
@@ -188,8 +194,49 @@ 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 {
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.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;
}
}
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 (isManual = false) => {
console.log(`[Sync] Phase 1: Pulling from server...`);
initBackend(); initBackend();
let accounts = store.get('accounts') as Account[]; let accounts = store.get('accounts') as Account[];
let hasChanges = false; let hasChanges = false;
@@ -201,12 +248,13 @@ const syncAccounts = async () => {
const exists = accounts.find(a => a.steamId === s.steamId); const exists = accounts.find(a => a.steamId === s.steamId);
if (!exists) { if (!exists) {
accounts.push({ accounts.push({
_id: `shared_${s.steamId}`, _id: `shared_${s.steamId}`, steamId: s.steamId, personaName: s.personaName,
steamId: s.steamId, personaName: s.personaName, avatar: s.avatar, profileUrl: s.profileUrl, avatar: s.avatar, profileUrl: s.profileUrl, vacBanned: s.vacBanned,
vacBanned: s.vacBanned, gameBans: s.gameBans, cooldownExpiresAt: s.cooldownExpiresAt, gameBans: s.gameBans, cooldownExpiresAt: s.cooldownExpiresAt,
loginName: s.loginName || '', steamLoginSecure: s.steamLoginSecure, loginConfig: s.loginConfig, loginName: s.loginName || '', steamLoginSecure: s.steamLoginSecure,
sessionUpdatedAt: s.sessionUpdatedAt, autoCheckCooldown: !!s.steamLoginSecure, loginConfig: s.loginConfig, sessionUpdatedAt: s.sessionUpdatedAt,
status: (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none', lastBanCheck: new Date().toISOString() autoCheckCooldown: !!s.steamLoginSecure, status: (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none',
lastBanCheck: new Date().toISOString(), sharedWith: s.sharedWith
}); });
hasChanges = true; hasChanges = true;
} else { } else {
@@ -223,6 +271,10 @@ const syncAccounts = async () => {
exists.cooldownExpiresAt = s.cooldownExpiresAt; exists.cooldownExpiresAt = s.cooldownExpiresAt;
hasChanges = true; hasChanges = true;
} }
if (JSON.stringify(exists.sharedWith) !== JSON.stringify(s.sharedWith)) {
exists.sharedWith = s.sharedWith;
hasChanges = true;
}
} }
} }
} catch (e) { } } catch (e) { }
@@ -234,76 +286,39 @@ const syncAccounts = async () => {
updateTrayMenu(); updateTrayMenu();
} }
if (accounts.length === 0) return; // Phase 2: Background Scrapes
const runScrapes = async () => {
const updatedAccounts = [...accounts]; console.log(`[Sync] Phase 2: Starting background checks for ${accounts.length} accounts...`);
let scrapeChanges = false; const currentAccounts = [...store.get('accounts') as Account[]];
let scrapeChanges = false;
for (const account of updatedAccounts) { for (const account of currentAccounts) {
try { try {
const now = new Date(); const now = new Date();
if (backend && !account._id.startsWith('shared_')) await backend.shareAccount(account);
// OPTIMIZATION: Ensure ALL authenticated accounts are shared with the server on every sync cycle const lastCheck = account.lastBanCheck ? new Date(account.lastBanCheck) : new Date(0);
// 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);
scrapeChanges = true;
}
if (account.autoCheckCooldown && account.steamLoginSecure) {
if (account.cooldownExpiresAt && new Date(account.cooldownExpiresAt) > now) continue;
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) { const needsMetadata = (now.getTime() - lastCheck.getTime()) / 3600000 > 6 || !account.personaName;
await new Promise(r => setTimeout(r, Math.floor(Math.random() * 60000) + 5000)); const needsCooldown = account.autoCheckCooldown && account.steamLoginSecure && (now.getTime() - lastScrape.getTime()) / 3600000 > 8;
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; }
}
}
}
} catch (error) { }
}
if (scrapeChanges) { if (needsMetadata || needsCooldown || isManual) {
store.set('accounts', updatedAccounts); if (!isManual && needsCooldown) await new Promise(r => setTimeout(r, Math.floor(Math.random() * 30000) + 5000));
if (mainWindow) mainWindow.webContents.send('accounts-updated', updatedAccounts); if (await scrapeAccountData(account)) scrapeChanges = true;
updateTrayMenu(); }
} } catch (error) { }
}
if (scrapeChanges) {
store.set('accounts', currentAccounts);
if (mainWindow) mainWindow.webContents.send('accounts-updated', currentAccounts);
updateTrayMenu();
}
console.log('[Sync] Sync cycle finished.');
};
if (isManual) await runScrapes(); else runScrapes();
}; };
const scheduleNextSync = () => { const scheduleNextSync = () => {
setTimeout(async () => { await syncAccounts(); scheduleNextSync(); }, isDev ? 120000 : 1800000); setTimeout(async () => { await syncAccounts(false); scheduleNextSync(); }, isDev ? 300000 : 1800000);
}; };
// --- Discovery --- // --- Discovery ---
@@ -342,28 +357,21 @@ const handleLocalAccountsFound = async (localAccounts: LocalSteamAccount[]) => {
} }
}; };
// --- Main Window Creation --- // --- Main Window ---
function createWindow() { function createWindow() {
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: 1280, height: 800, title: "Ultimate Ban Tracker", backgroundColor: '#171a21', autoHideMenuBar: true, width: 1280, height: 800, title: "Ultimate Ban Tracker", backgroundColor: '#171a21', autoHideMenuBar: true,
webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, contextIsolation: true } webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, contextIsolation: true }
}); });
mainWindow.setMenu(null); mainWindow.setMenu(null);
mainWindow.on('close', (event) => { mainWindow.on('close', (event) => {
if (!(app as any).isQuitting) { if (!(app as any).isQuitting) { event.preventDefault(); mainWindow?.hide(); }
event.preventDefault();
mainWindow?.hide();
}
return false; return false;
}); });
if (isDev) mainWindow.loadURL('http://localhost:5173'); if (isDev) mainWindow.loadURL('http://localhost:5173');
else mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html')); else mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html'));
} }
// --- App Lifecycle ---
app.whenReady().then(() => { app.whenReady().then(() => {
protocol.handle('steam-resource', (request) => { protocol.handle('steam-resource', (request) => {
let rawPath = decodeURIComponent(request.url.replace('steam-resource://', '')); let rawPath = decodeURIComponent(request.url.replace('steam-resource://', ''));
@@ -372,11 +380,10 @@ app.whenReady().then(() => {
if (!fs.existsSync(absolutePath)) return new Response('Not Found', { status: 404 }); if (!fs.existsSync(absolutePath)) return new Response('Not Found', { status: 404 });
try { return net.fetch(pathToFileURL(absolutePath).toString()); } catch (e) { return new Response('Error', { status: 500 }); } try { return net.fetch(pathToFileURL(absolutePath).toString()); } catch (e) { return new Response('Error', { status: 500 }); }
}); });
createWindow(); createWindow();
createTray(); createTray();
initBackend(); initBackend();
setTimeout(syncAccounts, 5000); setTimeout(() => syncAccounts(false), 5000);
scheduleNextSync(); scheduleNextSync();
steamClient.startWatching(handleLocalAccountsFound); steamClient.startWatching(handleLocalAccountsFound);
}); });
@@ -401,19 +408,17 @@ ipcMain.handle('login-to-server', async () => {
if (!config.url) return false; if (!config.url) return false;
return new Promise<boolean>((resolve) => { return new Promise<boolean>((resolve) => {
const authWindow = new BrowserWindow({ const authWindow = new BrowserWindow({
width: 800, height: 700, parent: mainWindow || undefined, modal: true, title: 'Login to Ban Tracker Server', width: 800, height: 700, parent: mainWindow || undefined, modal: true, title: 'Login to Server',
webPreferences: { nodeIntegration: false, contextIsolation: true } webPreferences: { nodeIntegration: false, contextIsolation: true }
}); });
authWindow.loadURL(`${config.url}/auth/steam`); authWindow.loadURL(`${config.url}/auth/steam`);
let captured = false; let captured = false;
const saveServerAuth = (token: string) => { const saveServerAuth = (token: string) => {
if (captured) return; captured = true; if (captured) return; captured = true;
let serverSteamId = undefined; let serverSteamId = undefined; let isAdmin = false;
let isAdmin = false;
try { try {
const payload = JSON.parse(Buffer.from(token.split('.')[1]!, 'base64').toString()); const payload = JSON.parse(Buffer.from(token.split('.')[1]!, 'base64').toString());
serverSteamId = payload.steamId; serverSteamId = payload.steamId; isAdmin = !!payload.isAdmin;
isAdmin = !!payload.isAdmin;
} catch (e) {} } catch (e) {}
const current = store.get('serverConfig'); const current = store.get('serverConfig');
store.set('serverConfig', { ...current, token, serverSteamId, isAdmin, enabled: true }); store.set('serverConfig', { ...current, token, serverSteamId, isAdmin, enabled: true });
@@ -436,7 +441,21 @@ 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(true); 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;
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();
@@ -453,7 +472,7 @@ ipcMain.handle('add-account', async (event, { identifier }) => {
loginName: existing.loginName || '', steamLoginSecure: existing.steamLoginSecure, loginName: existing.loginName || '', steamLoginSecure: existing.steamLoginSecure,
loginConfig: existing.loginConfig, sessionUpdatedAt: existing.sessionUpdatedAt, loginConfig: existing.loginConfig, sessionUpdatedAt: existing.sessionUpdatedAt,
autoCheckCooldown: !!existing.steamLoginSecure, status: (existing.vacBanned || existing.gameBans > 0) ? 'banned' : 'none', autoCheckCooldown: !!existing.steamLoginSecure, status: (existing.vacBanned || existing.gameBans > 0) ? 'banned' : 'none',
lastBanCheck: new Date().toISOString() lastBanCheck: new Date().toISOString(), sharedWith: existing.sharedWith
}; };
store.set('accounts', [...accounts, newAccount]); store.set('accounts', [...accounts, newAccount]);
updateTrayMenu(); updateTrayMenu();
@@ -527,60 +546,35 @@ ipcMain.handle('switch-account', async (event, loginName: string) => await handl
ipcMain.handle('open-external', (event, url: string) => shell.openExternal(url)); ipcMain.handle('open-external', (event, url: string) => shell.openExternal(url));
ipcMain.handle('open-steam-app-login', async () => { ipcMain.handle('open-steam-app-login', async () => {
console.log('[SteamClient] Preparing for fresh login...');
await killSteam(); await killSteam();
if (process.platform === 'win32') { if (process.platform === 'win32') {
// Clear auto-login registry
const clearReg = 'reg add "HKCU\\Software\\Valve\\Steam" /v AutoLoginUser /t REG_SZ /d "" /f'; const clearReg = 'reg add "HKCU\\Software\\Valve\\Steam" /v AutoLoginUser /t REG_SZ /d "" /f';
await new Promise<void>((res) => exec(clearReg, () => res())); await new Promise<void>((res) => exec(clearReg, () => res()));
} else if (process.platform === 'linux') { } else if (process.platform === 'linux') {
// On Linux we can use the steamClient helper to set an empty user
await steamClient.setAutoLoginUser("", undefined, ""); await steamClient.setAutoLoginUser("", undefined, "");
} }
const command = process.platform === 'win32' ? 'start steam://open/login' : 'xdg-open steam://open/login'; const command = process.platform === 'win32' ? 'start steam://open/login' : 'xdg-open steam://open/login';
exec(command); exec(command);
return true; return true;
}); });
ipcMain.handle('open-steam-login', async (event, expectedSteamId: string) => { ipcMain.handle('open-steam-login', async (event, expectedSteamId: string) => {
// Use a unique partition per account to prevent session bleeding
const partitionId = expectedSteamId ? `persist:steam-login-${expectedSteamId}` : 'persist:steam-login-new'; const partitionId = expectedSteamId ? `persist:steam-login-${expectedSteamId}` : 'persist:steam-login-new';
const loginSession = session.fromPartition(partitionId); const loginSession = session.fromPartition(partitionId);
if (!expectedSteamId) await loginSession.clearStorageData({ storages: ['cookies', 'localstorage', 'indexdb'] });
// If adding a brand new account, explicitly clear previous trash
if (!expectedSteamId) {
console.log('[Auth] Clearing session for new account login...');
await loginSession.clearStorageData({ storages: ['cookies', 'localstorage', 'indexdb'] });
}
// If we have an existing cookie string for this account, pre-inject it
if (expectedSteamId) { if (expectedSteamId) {
const accounts = store.get('accounts') as Account[]; const accounts = store.get('accounts') as Account[];
const account = accounts.find(a => a.steamId === expectedSteamId); const account = accounts.find(a => a.steamId === expectedSteamId);
if (account?.steamLoginSecure) { if (account?.steamLoginSecure) {
console.log(`[Auth] Pre-injecting existing cookies for ${account.personaName}...`);
const cookiePairs = account.steamLoginSecure.split(';').map(c => c.trim()); const cookiePairs = account.steamLoginSecure.split(';').map(c => c.trim());
for (const pair of cookiePairs) { for (const pair of cookiePairs) {
const [name, value] = pair.split('='); const [name, value] = pair.split('=');
if (name && value) { if (name && value) {
try { try { await loginSession.cookies.set({ url: 'https://steamcommunity.com', domain: 'steamcommunity.com', name, value, path: '/', secure: true, httpOnly: name.includes('Secure') }); } catch (e) {}
await loginSession.cookies.set({
url: 'https://steamcommunity.com',
domain: 'steamcommunity.com',
name: name,
value: value,
path: '/',
secure: true,
httpOnly: name.includes('Secure')
});
} catch (e) {}
} }
} }
} }
} }
return new Promise<boolean>((resolve) => { return new Promise<boolean>((resolve) => {
const loginWindow = new BrowserWindow({ const loginWindow = new BrowserWindow({
width: 800, height: 700, parent: mainWindow || undefined, modal: true, title: 'Login to Steam', width: 800, height: 700, parent: mainWindow || undefined, modal: true, title: 'Login to Steam',

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

@@ -1,12 +1,12 @@
{ {
"name": "ultimate-ban-tracker-desktop", "name": "ultimate-ban-tracker-desktop",
"version": "1.2.0", "version": "1.3.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ultimate-ban-tracker-desktop", "name": "ultimate-ban-tracker-desktop",
"version": "1.2.0", "version": "1.3.0",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",

View File

@@ -1,7 +1,7 @@
{ {
"name": "ultimate-ban-tracker-desktop", "name": "ultimate-ban-tracker-desktop",
"description": "Professional Steam Account Manager & Ban Tracker", "description": "Professional Steam Account Manager & Ban Tracker",
"version": "1.2.0", "version": "1.3.0",
"author": "Nils Pukropp <nils@narl.io>", "author": "Nils Pukropp <nils@narl.io>",
"homepage": "https://narl.io", "homepage": "https://narl.io",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
@@ -28,7 +28,8 @@
}, },
"files": [ "files": [
"dist/**/*", "dist/**/*",
"dist-electron/**/*" "dist-electron/**/*",
"assets-build/**/*"
], ],
"linux": { "linux": {
"target": [ "target": [

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 && (
<Typography variant="caption" sx={{ color: 'success.main', fontWeight: 'bold', fontSize: '0.6rem' }}>TRACKING</Typography> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<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>