2 Commits

Author SHA1 Message Date
5d611fd8be Merge pull request 'feat: implement comprehensive admin dashboard for server management and user oversight' (#5) from release/v1.3.0 into main
Some checks failed
Build and Release / build (push) Has been cancelled
Reviewed-on: #5
2026-02-21 03:35:22 +01:00
88d2a2133c Merge pull request 'chore: bump version to 1.2.0 and commit recent fixes/features including tray and auth isolation' (#4) from release/v1.2.0 into main
All checks were successful
Build and Release / build (push) Successful in 5m37s
Reviewed-on: #4
2026-02-21 03:21:45 +01:00
15 changed files with 410 additions and 653 deletions

View File

@@ -60,23 +60,9 @@ const initBackend = () => {
};
// --- System Tray ---
const createTray = () => {
// Try to find the icon in various standard locations
const possiblePaths = [
path_1.default.join(__dirname, '..', 'assets-build'), // Dev
path_1.default.join(process.resourcesPath, 'assets-build'), // Packaged (External)
path_1.default.join(electron_1.app.getAppPath(), 'dist', 'assets-build'), // Packaged (Internal dist)
path_1.default.join(electron_1.app.getAppPath(), 'assets-build') // Packaged (Internal root)
];
let assetsDir = '';
for (const p of possiblePaths) {
if (fs_1.default.existsSync(p)) {
assetsDir = p;
break;
}
}
const possibleIcons = ['icon.png', 'icon.svg'];
const assetsDir = path_1.default.join(__dirname, '..', 'assets-build');
const possibleIcons = ['icon.svg', 'icon.png'];
let iconPath = '';
if (assetsDir) {
for (const name of possibleIcons) {
const fullPath = path_1.default.join(assetsDir, name);
if (fs_1.default.existsSync(fullPath)) {
@@ -84,24 +70,27 @@ const createTray = () => {
break;
}
}
}
console.log(`[Tray] Resolved assets directory: ${assetsDir || 'NOT FOUND'}`);
console.log(`[Tray] Attempting to initialize with icon: ${iconPath || 'NONE FOUND'}`);
if (!iconPath) {
console.warn(`[Tray] FAILED: No valid icon found in searched paths.`);
console.warn(`[Tray] FAILED: No valid icon found in ${assetsDir}`);
return;
}
try {
const icon = electron_1.nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 });
tray = new electron_1.Tray(icon);
tray.setToolTip('Ultimate Ban Tracker');
tray.on('click', () => { if (mainWindow) {
tray.on('click', () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
} });
updateTrayMenu();
}
catch (e) { }
});
updateTrayMenu();
console.log(`[Tray] Successfully initialized`);
}
catch (e) {
console.error(`[Tray] Critical error during initialization: ${e.message}`);
}
};
const updateTrayMenu = () => {
if (!tray)
@@ -119,7 +108,11 @@ const updateTrayMenu = () => {
click: () => handleSwitchAccount(acc.loginName)
})) : [{ 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' },
{ label: 'Show Dashboard', click: () => { if (mainWindow)
mainWindow.show(); } },
@@ -162,58 +155,8 @@ const handleSwitchAccount = async (loginName) => {
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, now.toISOString());
}
else {
account.cooldownExpiresAt = undefined;
if (backend)
await backend.pushCooldown(account.steamId, undefined, now.toISOString());
}
}
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 ---
const syncAccounts = async (isManual = false) => {
console.log(`[Sync] Phase 1: Pulling from server...`);
const syncAccounts = async () => {
initBackend();
let accounts = store.get('accounts');
let hasChanges = false;
@@ -224,13 +167,12 @@ const syncAccounts = async (isManual = false) => {
const exists = accounts.find(a => a.steamId === s.steamId);
if (!exists) {
accounts.push({
_id: `shared_${s.steamId}`, steamId: s.steamId, personaName: s.personaName,
avatar: s.avatar, profileUrl: s.profileUrl, vacBanned: s.vacBanned,
gameBans: s.gameBans, cooldownExpiresAt: s.cooldownExpiresAt,
loginName: s.loginName || '', steamLoginSecure: s.steamLoginSecure,
loginConfig: s.loginConfig, sessionUpdatedAt: s.sessionUpdatedAt,
autoCheckCooldown: !!s.steamLoginSecure, status: (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none',
lastBanCheck: new Date().toISOString(), sharedWith: s.sharedWith
_id: `shared_${s.steamId}`,
steamId: s.steamId, personaName: s.personaName, avatar: s.avatar, profileUrl: s.profileUrl,
vacBanned: s.vacBanned, gameBans: s.gameBans, cooldownExpiresAt: s.cooldownExpiresAt,
loginName: s.loginName || '', steamLoginSecure: s.steamLoginSecure, loginConfig: s.loginConfig,
sessionUpdatedAt: s.sessionUpdatedAt, autoCheckCooldown: !!s.steamLoginSecure,
status: (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none', lastBanCheck: new Date().toISOString()
});
hasChanges = true;
}
@@ -250,28 +192,8 @@ const syncAccounts = async (isManual = false) => {
exists.sessionUpdatedAt = s.sessionUpdatedAt;
hasChanges = true;
}
// Metadata Sync (Pull)
const sMetaDate = s.lastMetadataCheck ? new Date(s.lastMetadataCheck) : new Date(0);
const lMetaDate = exists.lastBanCheck ? new Date(exists.lastBanCheck) : new Date(0);
if (sMetaDate > lMetaDate) {
exists.personaName = s.personaName;
exists.avatar = s.avatar;
exists.vacBanned = s.vacBanned;
exists.gameBans = s.gameBans;
exists.status = (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none';
exists.lastBanCheck = s.lastMetadataCheck;
hasChanges = true;
}
// Cooldown Sync (Pull)
const sScrapeDate = s.lastScrapeTime ? new Date(s.lastScrapeTime) : new Date(0);
const lScrapeDate = exists.lastScrapeTime ? new Date(exists.lastScrapeTime) : new Date(0);
if (sScrapeDate > lScrapeDate) {
if (s.cooldownExpiresAt && (!exists.cooldownExpiresAt || new Date(s.cooldownExpiresAt) > new Date(exists.cooldownExpiresAt))) {
exists.cooldownExpiresAt = s.cooldownExpiresAt;
exists.lastScrapeTime = s.lastScrapeTime;
hasChanges = true;
}
if (JSON.stringify(exists.sharedWith) !== JSON.stringify(s.sharedWith)) {
exists.sharedWith = s.sharedWith;
hasChanges = true;
}
}
@@ -285,44 +207,88 @@ const syncAccounts = async (isManual = false) => {
mainWindow.webContents.send('accounts-updated', accounts);
updateTrayMenu();
}
// Phase 2: Background Scrapes
const runScrapes = async () => {
console.log(`[Sync] Phase 2: Starting background checks for ${accounts.length} accounts...`);
const currentAccounts = [...store.get('accounts')];
if (accounts.length === 0)
return;
const updatedAccounts = [...accounts];
let scrapeChanges = false;
for (const account of currentAccounts) {
for (const account of updatedAccounts) {
try {
const now = new Date();
if (backend && !account._id.startsWith('shared_'))
// 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);
const lastScrape = account.lastScrapeTime ? new Date(account.lastScrapeTime) : new Date(0);
const needsMetadata = (now.getTime() - lastCheck.getTime()) / 3600000 > 6 || !account.personaName;
const needsCooldown = account.autoCheckCooldown && account.steamLoginSecure && (now.getTime() - lastScrape.getTime()) / 3600000 > 8;
if (needsMetadata || needsCooldown || isManual) {
if (!isManual && needsCooldown)
await new Promise(r => setTimeout(r, Math.floor(Math.random() * 30000) + 5000));
if (await scrapeAccountData(account))
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);
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);
if ((now.getTime() - lastScrape.getTime()) / 3600000 > 8) {
await new Promise(r => setTimeout(r, Math.floor(Math.random() * 60000) + 5000));
try {
const result = await (0, scraper_1.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) {
if (e.message.includes('cookie') || e.message.includes('Sign In')) {
account.authError = true;
scrapeChanges = true;
}
}
}
}
}
catch (error) { }
}
if (scrapeChanges) {
store.set('accounts', currentAccounts);
store.set('accounts', updatedAccounts);
if (mainWindow)
mainWindow.webContents.send('accounts-updated', currentAccounts);
mainWindow.webContents.send('accounts-updated', updatedAccounts);
updateTrayMenu();
}
console.log('[Sync] Sync cycle finished.');
};
if (isManual)
await runScrapes();
else
runScrapes();
};
const scheduleNextSync = () => {
setTimeout(async () => { await syncAccounts(false); scheduleNextSync(); }, isDev ? 300000 : 1800000);
setTimeout(async () => { await syncAccounts(); scheduleNextSync(); }, isDev ? 120000 : 1800000);
};
// --- Discovery ---
const addingAccounts = new Set();
@@ -345,20 +311,11 @@ const handleLocalAccountsFound = async (localAccounts) => {
const profile = await (0, steam_web_1.fetchProfileData)(local.steamId);
const bans = await (0, steam_web_1.scrapeBanStatus)(profile.profileUrl);
const localPath = await downloadAvatar(profile.steamId, profile.avatar);
// Wait and retry snagging the config (Steam takes time to write it)
let loginConfig = undefined;
for (let i = 0; i < 3; i++) {
await new Promise(r => setTimeout(r, 2000));
loginConfig = steam_client_1.steamClient.extractAccountConfig(local.accountName);
if (loginConfig)
break;
}
currentAccounts.push({
_id: Date.now().toString() + Math.random().toString().slice(2, 5),
steamId: local.steamId, personaName: profile.personaName || local.accountName,
loginName: local.accountName, autoCheckCooldown: false, avatar: profile.avatar,
localAvatar: localPath, profileUrl: profile.profileUrl,
loginConfig, sessionUpdatedAt: loginConfig ? new Date().toISOString() : undefined,
status: (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none',
vacBanned: bans.vacBanned, gameBans: bans.gameBans, lastBanCheck: new Date().toISOString()
});
@@ -375,7 +332,7 @@ const handleLocalAccountsFound = async (localAccounts) => {
updateTrayMenu();
}
};
// --- Main Window ---
// --- Main Window Creation ---
function createWindow() {
mainWindow = new electron_1.BrowserWindow({
width: 1280, height: 800, title: "Ultimate Ban Tracker", backgroundColor: '#171a21', autoHideMenuBar: true,
@@ -394,6 +351,7 @@ function createWindow() {
else
mainWindow.loadFile(path_1.default.join(__dirname, '..', 'dist', 'index.html'));
}
// --- App Lifecycle ---
electron_1.app.whenReady().then(() => {
electron_1.protocol.handle('steam-resource', (request) => {
let rawPath = decodeURIComponent(request.url.replace('steam-resource://', ''));
@@ -412,7 +370,7 @@ electron_1.app.whenReady().then(() => {
createWindow();
createTray();
initBackend();
setTimeout(() => syncAccounts(false), 5000);
setTimeout(syncAccounts, 5000);
scheduleNextSync();
steam_client_1.steamClient.startWatching(handleLocalAccountsFound);
});
@@ -439,7 +397,7 @@ electron_1.ipcMain.handle('login-to-server', async () => {
return false;
return new Promise((resolve) => {
const authWindow = new electron_1.BrowserWindow({
width: 800, height: 700, parent: mainWindow || undefined, modal: true, title: 'Login to Server',
width: 800, height: 700, parent: mainWindow || undefined, modal: true, title: 'Login to Ban Tracker Server',
webPreferences: { nodeIntegration: false, contextIsolation: true }
});
authWindow.loadURL(`${config.url}/auth/steam`);
@@ -481,21 +439,7 @@ 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('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('sync-now', async () => { await syncAccounts(); return true; });
electron_1.ipcMain.handle('add-account', async (event, { identifier }) => {
try {
initBackend();
@@ -513,7 +457,7 @@ electron_1.ipcMain.handle('add-account', async (event, { identifier }) => {
loginName: existing.loginName || '', steamLoginSecure: existing.steamLoginSecure,
loginConfig: existing.loginConfig, sessionUpdatedAt: existing.sessionUpdatedAt,
autoCheckCooldown: !!existing.steamLoginSecure, status: (existing.vacBanned || existing.gameBans > 0) ? 'banned' : 'none',
lastBanCheck: new Date().toISOString(), sharedWith: existing.sharedWith
lastBanCheck: new Date().toISOString()
};
store.set('accounts', [...accounts, newAccount]);
updateTrayMenu();
@@ -588,54 +532,18 @@ electron_1.ipcMain.handle('admin-delete-user', async (event, userId) => { initBa
electron_1.ipcMain.handle('admin-get-accounts', async () => { initBackend(); return backend ? await backend.getAdminAccounts() : []; });
electron_1.ipcMain.handle('admin-remove-account', async (event, steamId) => { initBackend(); if (backend)
await backend.forceRemoveAccount(steamId); return true; });
electron_1.ipcMain.handle('switch-account', async (event, loginName) => {
if (!loginName)
return false;
try {
// PROACTIVE SYNC: Try to snag the freshest token before we kill Steam
const accounts = store.get('accounts');
const account = accounts.find(a => a.loginName === loginName);
if (account && !account._id.startsWith('shared_')) {
const freshConfig = steam_client_1.steamClient.extractAccountConfig(loginName);
if (freshConfig) {
account.loginConfig = freshConfig;
account.sessionUpdatedAt = new Date().toISOString();
if (backend)
await backend.shareAccount(account);
store.set('accounts', accounts);
}
}
await killSteam();
if (process.platform === 'win32') {
const regBase = 'reg add "HKCU\\Software\\Valve\\Steam"';
const commands = [
`${regBase} /v AutoLoginUser /t REG_SZ /d "${loginName}" /f`,
`${regBase} /v RememberPassword /t REG_DWORD /d 1 /f`,
`${regBase} /v AlreadyLoggedIn /t REG_DWORD /d 1 /f`,
`${regBase} /v WantsOfflineMode /t REG_DWORD /d 0 /f`
];
await new Promise((res, rej) => (0, child_process_1.exec)(commands.join(' && '), (e) => e ? rej(e) : res()));
if (account && account.loginConfig)
steam_client_1.steamClient.injectAccountConfig(loginName, account.loginConfig);
}
else if (process.platform === 'linux') {
await steam_client_1.steamClient.setAutoLoginUser(loginName, account?.loginConfig, account?.steamId);
}
startSteam();
return true;
}
catch (e) {
return false;
}
});
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-steam-app-login', async () => {
console.log('[SteamClient] Preparing for fresh login...');
await killSteam();
if (process.platform === 'win32') {
// Clear auto-login registry
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()));
}
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, "");
}
const command = process.platform === 'win32' ? 'start steam://open/login' : 'xdg-open steam://open/login';
@@ -643,20 +551,34 @@ electron_1.ipcMain.handle('open-steam-app-login', async () => {
return true;
});
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 loginSession = electron_1.session.fromPartition(partitionId);
if (!expectedSteamId)
// 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) {
const accounts = store.get('accounts');
const account = accounts.find(a => a.steamId === expectedSteamId);
if (account?.steamLoginSecure) {
console.log(`[Auth] Pre-injecting existing cookies for ${account.personaName}...`);
const cookiePairs = account.steamLoginSecure.split(';').map(c => c.trim());
for (const pair of cookiePairs) {
const [name, value] = pair.split('=');
if (name && value) {
try {
await loginSession.cookies.set({ url: 'https://steamcommunity.com', domain: 'steamcommunity.com', name, value, path: '/', secure: true, httpOnly: name.includes('Secure') });
await loginSession.cookies.set({
url: 'https://steamcommunity.com',
domain: 'steamcommunity.com',
name: name,
value: value,
path: '/',
secure: true,
httpOnly: name.includes('Secure')
});
}
catch (e) { }
}

View File

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

View File

@@ -68,23 +68,19 @@ class BackendService {
loginName: account.loginName,
steamLoginSecure: account.steamLoginSecure,
loginConfig: account.loginConfig,
sessionUpdatedAt: account.sessionUpdatedAt,
lastMetadataCheck: account.lastBanCheck,
lastScrapeTime: account.lastScrapeTime,
cooldownExpiresAt: account.cooldownExpiresAt
sessionUpdatedAt: account.sessionUpdatedAt
}, { headers: this.headers });
}
catch (e) {
console.error('[Backend] Failed to share account');
}
}
async pushCooldown(steamId, cooldownExpiresAt, lastScrapeTime) {
async pushCooldown(steamId, cooldownExpiresAt) {
if (!this.token)
return;
try {
await axios_1.default.patch(`${this.url}/api/sync/${steamId}/cooldown`, {
cooldownExpiresAt,
lastScrapeTime
cooldownExpiresAt
}, { headers: this.headers });
}
catch (e) {

View File

@@ -57,25 +57,19 @@ const scrapeCooldown = async (steamId, steamLoginSecure) => {
let expirationDate = undefined;
$('table').each((_, table) => {
const headers = $(table).find('th').map((_, th) => $(th).text().trim()).get();
const expirationIndex = headers.findIndex(h => h.includes('Competitive Cooldown Expiration') || h.includes('Cooldown Expiration'));
const expirationIndex = headers.findIndex(h => h.includes('Competitive Cooldown Expiration'));
if (expirationIndex !== -1) {
const rows = $(table).find('tr').not(':has(th)');
rows.each((_, row) => {
const dateText = $(row).find('td').eq(expirationIndex).text().trim();
const firstRow = $(table).find('tr').not(':has(th)').first();
const dateText = firstRow.find('td').eq(expirationIndex).text().trim();
if (dateText && dateText !== '') {
// Steam uses 'GMT' which some JS engines don't parse well, replace with 'UTC'
const cleanDateText = dateText.replace(' GMT', ' UTC');
const parsed = new Date(cleanDateText);
if (!isNaN(parsed.getTime())) {
// We want the newest expiration date found
if (!expirationDate || parsed > expirationDate) {
expirationDate = parsed;
}
}
}
});
}
});
if (expirationDate && expirationDate.getTime() > Date.now()) {
console.log(`[Scraper] Found active cooldown until: ${expirationDate.toISOString()}`);
return {

View File

@@ -21,8 +21,7 @@ class SteamClientService {
if (platform === 'win32') {
const possiblePaths = [
'C:\\Program Files (x86)\\Steam',
'C:\\Program Files\\Steam',
path_1.default.join(process.env.APPDATA || '', 'Steam'),
'C:\\Program Files\\Steam'
];
this.steamPath = possiblePaths.find(p => fs_1.default.existsSync(p)) || null;
}
@@ -30,8 +29,7 @@ class SteamClientService {
const possiblePaths = [
path_1.default.join(home, '.steam/steam'),
path_1.default.join(home, '.local/share/Steam'),
path_1.default.join(home, '.var/app/com.valvesoftware.Steam/.steam/steam'), // Flatpak
path_1.default.join(home, 'snap/steam/common/.steam/steam'), // Snap
path_1.default.join(home, '.var/app/com.valvesoftware.Steam/.steam/steam')
];
this.steamPath = possiblePaths.find(p => fs_1.default.existsSync(p)) || null;
}
@@ -49,35 +47,12 @@ class SteamClientService {
return null;
return path_1.default.join(this.steamPath, 'config', 'config.vdf');
}
/**
* Safe Atomic Write: Writes to a temp file and renames it.
* This prevents file corruption if the app crashes during write.
*/
safeWriteVdf(filePath, data) {
const tempPath = `${filePath}.tmp_${Date.now()}`;
const dir = path_1.default.dirname(filePath);
try {
if (!fs_1.default.existsSync(dir))
fs_1.default.mkdirSync(dir, { recursive: true });
const vdfContent = (0, simple_vdf_1.stringify)(data);
fs_1.default.writeFileSync(tempPath, vdfContent, 'utf-8');
// Atomic rename
fs_1.default.renameSync(tempPath, filePath);
}
catch (e) {
console.error(`[SteamClient] Atomic write failed for ${filePath}: ${e.message}`);
if (fs_1.default.existsSync(tempPath))
fs_1.default.unlinkSync(tempPath);
throw e;
}
}
startWatching(callback) {
this.onAccountsChanged = callback;
const loginUsersPath = this.getLoginUsersPath();
if (loginUsersPath && fs_1.default.existsSync(loginUsersPath)) {
this.readLocalAccounts();
chokidar_1.default.watch(loginUsersPath, { persistent: true, ignoreInitial: true }).on('change', () => {
console.log(`[SteamClient] loginusers.vdf changed, re-scanning...`);
chokidar_1.default.watch(loginUsersPath, { persistent: true }).on('change', () => {
this.readLocalAccounts();
});
}
@@ -88,20 +63,16 @@ class SteamClientService {
return;
try {
const content = fs_1.default.readFileSync(filePath, 'utf-8');
if (!content.trim())
return; // Empty file
const data = (0, simple_vdf_1.parse)(content);
if (!data || !data.users)
return;
const accounts = [];
for (const [steamId64, userData] of Object.entries(data.users)) {
const user = userData;
if (!user || !user.AccountName)
continue;
accounts.push({
steamId: steamId64,
accountName: user.AccountName,
personaName: user.PersonaName || user.AccountName,
personaName: user.PersonaName,
timestamp: parseInt(user.Timestamp) || 0
});
}
@@ -120,38 +91,32 @@ class SteamClientService {
const content = fs_1.default.readFileSync(configPath, 'utf-8');
const data = (0, simple_vdf_1.parse)(content);
const accounts = data?.InstallConfigStore?.Software?.Valve?.Steam?.Accounts;
return (accounts && accounts[accountName]) ? accounts[accountName] : null;
if (accounts && accounts[accountName]) {
return accounts[accountName];
}
}
catch (e) {
console.error('[SteamClient] Failed to extract config.vdf data');
return null;
}
return null;
}
injectAccountConfig(accountName, accountData) {
const configPath = this.getConfigVdfPath();
if (!configPath)
return;
let data = {
InstallConfigStore: {
Software: {
Valve: {
Steam: {
Accounts: {}
}
}
}
}
};
// Create directory if it doesn't exist
const configDir = path_1.default.dirname(configPath);
if (!fs_1.default.existsSync(configDir))
fs_1.default.mkdirSync(configDir, { recursive: true });
let data = { InstallConfigStore: { Software: { Valve: { Steam: { Accounts: {} } } } } };
if (fs_1.default.existsSync(configPath)) {
try {
const content = fs_1.default.readFileSync(configPath, 'utf-8');
const parsed = (0, simple_vdf_1.parse)(content);
if (parsed && typeof parsed === 'object')
data = parsed;
data = (0, simple_vdf_1.parse)(content);
}
catch (e) { }
}
// Ensure safe nesting
// Ensure structure exists
if (!data.InstallConfigStore)
data.InstallConfigStore = {};
if (!data.InstallConfigStore.Software)
@@ -164,22 +129,25 @@ class SteamClientService {
data.InstallConfigStore.Software.Valve.Steam.Accounts = {};
data.InstallConfigStore.Software.Valve.Steam.Accounts[accountName] = accountData;
try {
this.safeWriteVdf(configPath, data);
console.log(`[SteamClient] Safely injected session for ${accountName}`);
fs_1.default.writeFileSync(configPath, (0, simple_vdf_1.stringify)(data));
console.log(`[SteamClient] Injected login config for ${accountName} into config.vdf`);
}
catch (e) {
console.error('[SteamClient] Failed to write config.vdf');
}
catch (e) { }
}
async setAutoLoginUser(accountName, accountConfig, steamId) {
const platform = os_1.default.platform();
const loginUsersPath = this.getLoginUsersPath();
if (loginUsersPath) {
const configDir = path_1.default.dirname(loginUsersPath);
if (!fs_1.default.existsSync(configDir))
fs_1.default.mkdirSync(configDir, { recursive: true });
let data = { users: {} };
if (fs_1.default.existsSync(loginUsersPath)) {
try {
const content = fs_1.default.readFileSync(loginUsersPath, 'utf-8');
const parsed = (0, simple_vdf_1.parse)(content);
if (parsed && parsed.users)
data = parsed;
data = (0, simple_vdf_1.parse)(content);
}
catch (e) { }
}
@@ -188,7 +156,7 @@ class SteamClientService {
let found = false;
for (const [id, user] of Object.entries(data.users)) {
const u = user;
if (u.AccountName?.toLowerCase() === accountName.toLowerCase()) {
if (u.AccountName.toLowerCase() === accountName.toLowerCase()) {
u.mostrecent = "1";
u.RememberPassword = "1";
u.AllowAutoLogin = "1";
@@ -201,8 +169,8 @@ class SteamClientService {
u.mostrecent = "0";
}
}
if (!found && steamId && accountName) {
console.log(`[SteamClient] Provisioning new user profile for ${accountName}`);
if (!found && steamId) {
console.log(`[SteamClient] Provisioning user ${accountName} into loginusers.vdf`);
data.users[steamId] = {
AccountName: accountName,
PersonaName: accountName,
@@ -216,53 +184,51 @@ class SteamClientService {
};
}
try {
this.safeWriteVdf(loginUsersPath, data);
fs_1.default.writeFileSync(loginUsersPath, (0, simple_vdf_1.stringify)(data));
}
catch (e) { }
catch (e) {
console.error('[SteamClient] Failed to write loginusers.vdf');
}
if (accountConfig && accountName) {
}
if (accountConfig) {
this.injectAccountConfig(accountName, accountConfig);
}
// --- Linux Registry / Registry.vdf Hardening ---
if (platform === 'linux') {
const regLocations = [
path_1.default.join(os_1.default.homedir(), '.steam', 'registry.vdf'),
path_1.default.join(os_1.default.homedir(), '.steam', 'steam', 'registry.vdf')
];
for (const regPath of regLocations) {
if (!fs_1.default.existsSync(path_1.default.dirname(regPath)))
continue;
let regData = { Registry: { HKCU: { Software: { Valve: { Steam: {
AutoLoginUser: "",
RememberPassword: "1",
AlreadyLoggedIn: "1"
} } } } } };
let regData = { Registry: { HKCU: { Software: { Valve: { Steam: {} } } } } };
if (fs_1.default.existsSync(regPath)) {
try {
const content = fs_1.default.readFileSync(regPath, 'utf-8');
const parsed = (0, simple_vdf_1.parse)(content);
if (parsed && typeof parsed === 'object')
regData = parsed;
regData = (0, simple_vdf_1.parse)(content);
}
catch (e) { }
}
// Deep merge helper
const ensurePath = (obj, keys) => {
let curr = obj;
for (const key of keys) {
if (!curr[key] || typeof curr[key] !== 'object')
curr[key] = {};
curr = curr[key];
else {
const regDir = path_1.default.dirname(regPath);
if (!fs_1.default.existsSync(regDir))
fs_1.default.mkdirSync(regDir, { recursive: true });
}
return curr;
const setPath = (obj, keys, val) => {
let curr = obj;
for (let i = 0; i < keys.length - 1; i++) {
if (!curr[keys[i]])
curr[keys[i]] = {};
curr = curr[keys[i]];
}
curr[keys[keys.length - 1]] = val;
};
const steamKey = ensurePath(regData, ['Registry', 'HKCU', 'Software', 'Valve', 'Steam']);
steamKey.AutoLoginUser = accountName;
steamKey.RememberPassword = "1";
steamKey.AlreadyLoggedIn = "1";
steamKey.WantsOfflineMode = "0";
const steamReg = ['Registry', 'HKCU', 'Software', 'Valve', 'Steam'];
setPath(regData, [...steamReg, 'AutoLoginUser'], accountName);
setPath(regData, [...steamReg, 'RememberPassword'], "1");
setPath(regData, [...steamReg, 'AlreadyLoggedIn'], "1");
setPath(regData, [...steamReg, 'WantsOfflineMode'], "0");
try {
this.safeWriteVdf(regPath, regData);
fs_1.default.writeFileSync(regPath, (0, simple_vdf_1.stringify)(regData));
console.log(`[SteamClient] Registry updated: ${regPath}`);
}
catch (e) { }
}

View File

@@ -40,7 +40,6 @@ interface Account {
cooldownExpiresAt?: string;
authError?: boolean;
notes?: string;
sharedWith?: any[];
}
interface ServerConfig {
@@ -49,7 +48,6 @@ interface ServerConfig {
serverSteamId?: string;
enabled: boolean;
theme?: string;
isAdmin?: boolean;
}
// --- App State ---
@@ -93,26 +91,10 @@ const initBackend = () => {
// --- System Tray ---
const createTray = () => {
// Try to find the icon in various standard locations
const possiblePaths = [
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)
];
let assetsDir = '';
for (const p of possiblePaths) {
if (fs.existsSync(p)) {
assetsDir = p;
break;
}
}
const possibleIcons = ['icon.png', 'icon.svg'];
const assetsDir = path.join(__dirname, '..', 'assets-build');
const possibleIcons = ['icon.svg', 'icon.png'];
let iconPath = '';
if (assetsDir) {
for (const name of possibleIcons) {
const fullPath = path.join(assetsDir, name);
if (fs.existsSync(fullPath)) {
@@ -120,13 +102,11 @@ const createTray = () => {
break;
}
}
}
console.log(`[Tray] Resolved assets directory: ${assetsDir || 'NOT FOUND'}`);
console.log(`[Tray] Attempting to initialize with icon: ${iconPath || 'NONE FOUND'}`);
if (!iconPath) {
console.warn(`[Tray] FAILED: No valid icon found in searched paths.`);
console.warn(`[Tray] FAILED: No valid icon found in ${assetsDir}`);
return;
}
@@ -134,15 +114,24 @@ const createTray = () => {
const icon = nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 });
tray = new Tray(icon);
tray.setToolTip('Ultimate Ban Tracker');
tray.on('click', () => { if (mainWindow) { mainWindow.show(); mainWindow.focus(); } });
tray.on('click', () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
});
updateTrayMenu();
} catch (e) { }
console.log(`[Tray] Successfully initialized`);
} catch (e: any) {
console.error(`[Tray] Critical error during initialization: ${e.message}`);
}
};
const updateTrayMenu = () => {
if (!tray) return;
const accounts = store.get('accounts') as Account[];
const config = store.get('serverConfig');
const contextMenu = Menu.buildFromTemplate([
{ label: `Ultimate Ban Tracker v${app.getVersion()}`, enabled: false },
{ type: 'separator' },
@@ -154,11 +143,16 @@ const updateTrayMenu = () => {
click: () => handleSwitchAccount(acc.loginName)
})) : [{ 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' },
{ label: 'Show Dashboard', click: () => { if (mainWindow) mainWindow.show(); } },
{ label: 'Quit', click: () => { (app as any).isQuitting = true; app.quit(); } }
]);
tray.setContextMenu(contextMenu);
};
@@ -194,49 +188,8 @@ const handleSwitchAccount = async (loginName: string) => {
} 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, now.toISOString());
} else {
account.cooldownExpiresAt = undefined;
if (backend) await backend.pushCooldown(account.steamId, undefined, now.toISOString());
}
} 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 ---
const syncAccounts = async (isManual = false) => {
console.log(`[Sync] Phase 1: Pulling from server...`);
const syncAccounts = async () => {
initBackend();
let accounts = store.get('accounts') as Account[];
let hasChanges = false;
@@ -248,13 +201,12 @@ const syncAccounts = async (isManual = false) => {
const exists = accounts.find(a => a.steamId === s.steamId);
if (!exists) {
accounts.push({
_id: `shared_${s.steamId}`, steamId: s.steamId, personaName: s.personaName,
avatar: s.avatar, profileUrl: s.profileUrl, vacBanned: s.vacBanned,
gameBans: s.gameBans, cooldownExpiresAt: s.cooldownExpiresAt,
loginName: s.loginName || '', steamLoginSecure: s.steamLoginSecure,
loginConfig: s.loginConfig, sessionUpdatedAt: s.sessionUpdatedAt,
autoCheckCooldown: !!s.steamLoginSecure, status: (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none',
lastBanCheck: new Date().toISOString(), sharedWith: s.sharedWith
_id: `shared_${s.steamId}`,
steamId: s.steamId, personaName: s.personaName, avatar: s.avatar, profileUrl: s.profileUrl,
vacBanned: s.vacBanned, gameBans: s.gameBans, cooldownExpiresAt: s.cooldownExpiresAt,
loginName: s.loginName || '', steamLoginSecure: s.steamLoginSecure, loginConfig: s.loginConfig,
sessionUpdatedAt: s.sessionUpdatedAt, autoCheckCooldown: !!s.steamLoginSecure,
status: (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none', lastBanCheck: new Date().toISOString()
});
hasChanges = true;
} else {
@@ -267,31 +219,8 @@ const syncAccounts = async (isManual = false) => {
exists.sessionUpdatedAt = s.sessionUpdatedAt;
hasChanges = true;
}
// Metadata Sync (Pull)
const sMetaDate = s.lastMetadataCheck ? new Date(s.lastMetadataCheck) : new Date(0);
const lMetaDate = exists.lastBanCheck ? new Date(exists.lastBanCheck) : new Date(0);
if (sMetaDate > lMetaDate) {
exists.personaName = s.personaName;
exists.avatar = s.avatar;
exists.vacBanned = s.vacBanned;
exists.gameBans = s.gameBans;
exists.status = (s.vacBanned || s.gameBans > 0) ? 'banned' : 'none';
exists.lastBanCheck = s.lastMetadataCheck;
hasChanges = true;
}
// Cooldown Sync (Pull)
const sScrapeDate = s.lastScrapeTime ? new Date(s.lastScrapeTime) : new Date(0);
const lScrapeDate = exists.lastScrapeTime ? new Date(exists.lastScrapeTime) : new Date(0);
if (sScrapeDate > lScrapeDate) {
if (s.cooldownExpiresAt && (!exists.cooldownExpiresAt || new Date(s.cooldownExpiresAt) > new Date(exists.cooldownExpiresAt))) {
exists.cooldownExpiresAt = s.cooldownExpiresAt;
exists.lastScrapeTime = s.lastScrapeTime;
hasChanges = true;
}
if (JSON.stringify(exists.sharedWith) !== JSON.stringify(s.sharedWith)) {
exists.sharedWith = s.sharedWith;
hasChanges = true;
}
}
@@ -305,39 +234,76 @@ const syncAccounts = async (isManual = false) => {
updateTrayMenu();
}
// Phase 2: Background Scrapes
const runScrapes = async () => {
console.log(`[Sync] Phase 2: Starting background checks for ${accounts.length} accounts...`);
const currentAccounts = [...store.get('accounts') as Account[]];
if (accounts.length === 0) return;
const updatedAccounts = [...accounts];
let scrapeChanges = false;
for (const account of currentAccounts) {
for (const account of updatedAccounts) {
try {
const now = new Date();
if (backend && !account._id.startsWith('shared_')) await backend.shareAccount(account);
const lastCheck = account.lastBanCheck ? new Date(account.lastBanCheck) : new Date(0);
const lastScrape = account.lastScrapeTime ? new Date(account.lastScrapeTime) : new Date(0);
const needsMetadata = (now.getTime() - lastCheck.getTime()) / 3600000 > 6 || !account.personaName;
const needsCooldown = account.autoCheckCooldown && account.steamLoginSecure && (now.getTime() - lastScrape.getTime()) / 3600000 > 8;
if (needsMetadata || needsCooldown || isManual) {
if (!isManual && needsCooldown) await new Promise(r => setTimeout(r, Math.floor(Math.random() * 30000) + 5000));
if (await scrapeAccountData(account)) scrapeChanges = true;
// 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);
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);
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; }
}
}
}
} catch (error) { }
}
if (scrapeChanges) {
store.set('accounts', currentAccounts);
if (mainWindow) mainWindow.webContents.send('accounts-updated', currentAccounts);
store.set('accounts', updatedAccounts);
if (mainWindow) mainWindow.webContents.send('accounts-updated', updatedAccounts);
updateTrayMenu();
}
console.log('[Sync] Sync cycle finished.');
};
if (isManual) await runScrapes(); else runScrapes();
};
const scheduleNextSync = () => {
setTimeout(async () => { await syncAccounts(false); scheduleNextSync(); }, isDev ? 300000 : 1800000);
setTimeout(async () => { await syncAccounts(); scheduleNextSync(); }, isDev ? 120000 : 1800000);
};
// --- Discovery ---
@@ -356,21 +322,11 @@ const handleLocalAccountsFound = async (localAccounts: LocalSteamAccount[]) => {
const profile = await fetchProfileData(local.steamId);
const bans = await scrapeBanStatus(profile.profileUrl);
const localPath = await downloadAvatar(profile.steamId, profile.avatar);
// Wait and retry snagging the config (Steam takes time to write it)
let loginConfig = undefined;
for (let i = 0; i < 3; i++) {
await new Promise(r => setTimeout(r, 2000));
loginConfig = steamClient.extractAccountConfig(local.accountName);
if (loginConfig) break;
}
currentAccounts.push({
_id: Date.now().toString() + Math.random().toString().slice(2, 5),
steamId: local.steamId, personaName: profile.personaName || local.accountName,
loginName: local.accountName, autoCheckCooldown: false, avatar: profile.avatar,
localAvatar: localPath, profileUrl: profile.profileUrl,
loginConfig, sessionUpdatedAt: loginConfig ? new Date().toISOString() : undefined,
status: (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none',
vacBanned: bans.vacBanned, gameBans: bans.gameBans, lastBanCheck: new Date().toISOString()
});
@@ -386,21 +342,28 @@ const handleLocalAccountsFound = async (localAccounts: LocalSteamAccount[]) => {
}
};
// --- Main Window ---
// --- Main Window Creation ---
function createWindow() {
mainWindow = new BrowserWindow({
width: 1280, height: 800, title: "Ultimate Ban Tracker", backgroundColor: '#171a21', autoHideMenuBar: true,
webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, contextIsolation: true }
});
mainWindow.setMenu(null);
mainWindow.on('close', (event) => {
if (!(app as any).isQuitting) { event.preventDefault(); mainWindow?.hide(); }
if (!(app as any).isQuitting) {
event.preventDefault();
mainWindow?.hide();
}
return false;
});
if (isDev) mainWindow.loadURL('http://localhost:5173');
else mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html'));
}
// --- App Lifecycle ---
app.whenReady().then(() => {
protocol.handle('steam-resource', (request) => {
let rawPath = decodeURIComponent(request.url.replace('steam-resource://', ''));
@@ -409,10 +372,11 @@ app.whenReady().then(() => {
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 }); }
});
createWindow();
createTray();
initBackend();
setTimeout(() => syncAccounts(false), 5000);
setTimeout(syncAccounts, 5000);
scheduleNextSync();
steamClient.startWatching(handleLocalAccountsFound);
});
@@ -437,17 +401,19 @@ ipcMain.handle('login-to-server', async () => {
if (!config.url) return false;
return new Promise<boolean>((resolve) => {
const authWindow = new BrowserWindow({
width: 800, height: 700, parent: mainWindow || undefined, modal: true, title: 'Login to Server',
width: 800, height: 700, parent: mainWindow || undefined, modal: true, title: 'Login to Ban Tracker Server',
webPreferences: { nodeIntegration: false, contextIsolation: true }
});
authWindow.loadURL(`${config.url}/auth/steam`);
let captured = false;
const saveServerAuth = (token: string) => {
if (captured) return; captured = true;
let serverSteamId = undefined; let isAdmin = false;
let serverSteamId = undefined;
let isAdmin = false;
try {
const payload = JSON.parse(Buffer.from(token.split('.')[1]!, 'base64').toString());
serverSteamId = payload.steamId; isAdmin = !!payload.isAdmin;
serverSteamId = payload.steamId;
isAdmin = !!payload.isAdmin;
} catch (e) {}
const current = store.get('serverConfig');
store.set('serverConfig', { ...current, token, serverSteamId, isAdmin, enabled: true });
@@ -470,21 +436,7 @@ ipcMain.handle('login-to-server', async () => {
});
ipcMain.handle('get-server-user-info', () => ({ steamId: store.get('serverConfig').serverSteamId }));
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('sync-now', async () => { await syncAccounts(); return true; });
ipcMain.handle('add-account', async (event, { identifier }) => {
try {
initBackend();
@@ -501,7 +453,7 @@ ipcMain.handle('add-account', async (event, { identifier }) => {
loginName: existing.loginName || '', steamLoginSecure: existing.steamLoginSecure,
loginConfig: existing.loginConfig, sessionUpdatedAt: existing.sessionUpdatedAt,
autoCheckCooldown: !!existing.steamLoginSecure, status: (existing.vacBanned || existing.gameBans > 0) ? 'banned' : 'none',
lastBanCheck: new Date().toISOString(), sharedWith: existing.sharedWith
lastBanCheck: new Date().toISOString()
};
store.set('accounts', [...accounts, newAccount]);
updateTrayMenu();
@@ -571,74 +523,64 @@ ipcMain.handle('admin-delete-user', async (event, userId: string) => { initBacke
ipcMain.handle('admin-get-accounts', async () => { initBackend(); return backend ? await backend.getAdminAccounts() : []; });
ipcMain.handle('admin-remove-account', async (event, steamId: string) => { initBackend(); if (backend) await backend.forceRemoveAccount(steamId); return true; });
ipcMain.handle('switch-account', async (event, loginName: string) => {
if (!loginName) return false;
try {
// PROACTIVE SYNC: Try to snag the freshest token before we kill Steam
const accounts = store.get('accounts') as Account[];
const account = accounts.find(a => a.loginName === loginName);
if (account && !account._id.startsWith('shared_')) {
const freshConfig = steamClient.extractAccountConfig(loginName);
if (freshConfig) {
account.loginConfig = freshConfig;
account.sessionUpdatedAt = new Date().toISOString();
if (backend) await backend.shareAccount(account);
store.set('accounts', accounts);
}
}
await killSteam();
if (process.platform === 'win32') {
const regBase = 'reg add "HKCU\\Software\\Valve\\Steam"';
const commands = [
`${regBase} /v AutoLoginUser /t REG_SZ /d "${loginName}" /f`,
`${regBase} /v RememberPassword /t REG_DWORD /d 1 /f`,
`${regBase} /v AlreadyLoggedIn /t REG_DWORD /d 1 /f`,
`${regBase} /v WantsOfflineMode /t REG_DWORD /d 0 /f`
];
await new Promise<void>((res, rej) => exec(commands.join(' && '), (e) => e ? rej(e) : res()));
if (account && account.loginConfig) steamClient.injectAccountConfig(loginName, account.loginConfig);
} else if (process.platform === 'linux') {
await steamClient.setAutoLoginUser(loginName, account?.loginConfig, account?.steamId);
}
startSteam();
return true;
} catch (e) { return false; }
});
ipcMain.handle('switch-account', async (event, loginName: string) => await handleSwitchAccount(loginName));
ipcMain.handle('open-external', (event, url: string) => shell.openExternal(url));
ipcMain.handle('open-steam-app-login', async () => {
console.log('[SteamClient] Preparing for fresh login...');
await killSteam();
if (process.platform === 'win32') {
// Clear auto-login registry
const clearReg = 'reg add "HKCU\\Software\\Valve\\Steam" /v AutoLoginUser /t REG_SZ /d "" /f';
await new Promise<void>((res) => exec(clearReg, () => res()));
} else if (process.platform === 'linux') {
// On Linux we can use the steamClient helper to set an empty user
await steamClient.setAutoLoginUser("", undefined, "");
}
const command = process.platform === 'win32' ? 'start steam://open/login' : 'xdg-open steam://open/login';
exec(command);
return true;
});
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 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) {
const accounts = store.get('accounts') as Account[];
const account = accounts.find(a => a.steamId === expectedSteamId);
if (account?.steamLoginSecure) {
console.log(`[Auth] Pre-injecting existing cookies for ${account.personaName}...`);
const cookiePairs = account.steamLoginSecure.split(';').map(c => c.trim());
for (const pair of cookiePairs) {
const [name, value] = pair.split('=');
if (name && value) {
try { await loginSession.cookies.set({ url: 'https://steamcommunity.com', domain: 'steamcommunity.com', name, value, path: '/', secure: true, httpOnly: name.includes('Secure') }); } catch (e) {}
try {
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) => {
const loginWindow = new BrowserWindow({
width: 800, height: 700, parent: mainWindow || undefined, modal: true, title: 'Login to Steam',

View File

@@ -19,7 +19,6 @@ 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'),

View File

@@ -62,22 +62,18 @@ export class BackendService {
loginName: account.loginName,
steamLoginSecure: account.steamLoginSecure,
loginConfig: account.loginConfig,
sessionUpdatedAt: account.sessionUpdatedAt,
lastMetadataCheck: account.lastBanCheck,
lastScrapeTime: account.lastScrapeTime,
cooldownExpiresAt: account.cooldownExpiresAt
sessionUpdatedAt: account.sessionUpdatedAt
}, { headers: this.headers });
} catch (e) {
console.error('[Backend] Failed to share account');
}
}
public async pushCooldown(steamId: string, cooldownExpiresAt?: string, lastScrapeTime?: string) {
public async pushCooldown(steamId: string, cooldownExpiresAt?: string) {
if (!this.token) return;
try {
await axios.patch(`${this.url}/api/sync/${steamId}/cooldown`, {
cooldownExpiresAt,
lastScrapeTime
cooldownExpiresAt
}, { headers: this.headers });
} catch (e) {
console.error(`[Backend] Failed to push cooldown for ${steamId}`);

View File

@@ -29,27 +29,22 @@ export const scrapeCooldown = async (steamId: string, steamLoginSecure: string):
$('table').each((_, table) => {
const headers = $(table).find('th').map((_, th) => $(th).text().trim()).get();
const expirationIndex = headers.findIndex(h => h.includes('Competitive Cooldown Expiration') || h.includes('Cooldown Expiration'));
const expirationIndex = headers.findIndex(h => h.includes('Competitive Cooldown Expiration'));
if (expirationIndex !== -1) {
const rows = $(table).find('tr').not(':has(th)');
rows.each((_, row) => {
const dateText = $(row).find('td').eq(expirationIndex).text().trim();
const firstRow = $(table).find('tr').not(':has(th)').first();
const dateText = firstRow.find('td').eq(expirationIndex).text().trim();
if (dateText && dateText !== '') {
// Steam uses 'GMT' which some JS engines don't parse well, replace with 'UTC'
const cleanDateText = dateText.replace(' GMT', ' UTC');
const parsed = new Date(cleanDateText);
if (!isNaN(parsed.getTime())) {
// We want the newest expiration date found
if (!expirationDate || parsed > (expirationDate as Date)) {
expirationDate = parsed;
}
}
}
});
}
});
if (expirationDate && (expirationDate as Date).getTime() > Date.now()) {
console.log(`[Scraper] Found active cooldown until: ${(expirationDate as Date).toISOString()}`);

View File

@@ -26,16 +26,14 @@ class SteamClientService {
if (platform === 'win32') {
const possiblePaths = [
'C:\\Program Files (x86)\\Steam',
'C:\\Program Files\\Steam',
path.join(process.env.APPDATA || '', 'Steam'),
'C:\\Program Files\\Steam'
];
this.steamPath = possiblePaths.find(p => fs.existsSync(p)) || null;
} else if (platform === 'linux') {
const possiblePaths = [
path.join(home, '.steam/steam'),
path.join(home, '.local/share/Steam'),
path.join(home, '.var/app/com.valvesoftware.Steam/.steam/steam'), // Flatpak
path.join(home, 'snap/steam/common/.steam/steam'), // Snap
path.join(home, '.var/app/com.valvesoftware.Steam/.steam/steam')
];
this.steamPath = possiblePaths.find(p => fs.existsSync(p)) || null;
}
@@ -55,36 +53,13 @@ class SteamClientService {
return path.join(this.steamPath, 'config', 'config.vdf');
}
/**
* Safe Atomic Write: Writes to a temp file and renames it.
* This prevents file corruption if the app crashes during write.
*/
private safeWriteVdf(filePath: string, data: any) {
const tempPath = `${filePath}.tmp_${Date.now()}`;
const dir = path.dirname(filePath);
try {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const vdfContent = stringify(data);
fs.writeFileSync(tempPath, vdfContent, 'utf-8');
// Atomic rename
fs.renameSync(tempPath, filePath);
} catch (e: any) {
console.error(`[SteamClient] Atomic write failed for ${filePath}: ${e.message}`);
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
throw e;
}
}
public startWatching(callback: (accounts: LocalSteamAccount[]) => void) {
this.onAccountsChanged = callback;
const loginUsersPath = this.getLoginUsersPath();
if (loginUsersPath && fs.existsSync(loginUsersPath)) {
this.readLocalAccounts();
chokidar.watch(loginUsersPath, { persistent: true, ignoreInitial: true }).on('change', () => {
console.log(`[SteamClient] loginusers.vdf changed, re-scanning...`);
chokidar.watch(loginUsersPath, { persistent: true }).on('change', () => {
this.readLocalAccounts();
});
}
@@ -96,20 +71,16 @@ class SteamClientService {
try {
const content = fs.readFileSync(filePath, 'utf-8');
if (!content.trim()) return; // Empty file
const data = parse(content) as any;
if (!data || !data.users) return;
const accounts: LocalSteamAccount[] = [];
for (const [steamId64, userData] of Object.entries(data.users)) {
const user = userData as any;
if (!user || !user.AccountName) continue;
accounts.push({
steamId: steamId64,
accountName: user.AccountName,
personaName: user.PersonaName || user.AccountName,
personaName: user.PersonaName,
timestamp: parseInt(user.Timestamp) || 0
});
}
@@ -127,39 +98,35 @@ class SteamClientService {
try {
const content = fs.readFileSync(configPath, 'utf-8');
const data = parse(content) as any;
const accounts = data?.InstallConfigStore?.Software?.Valve?.Steam?.Accounts;
return (accounts && accounts[accountName]) ? accounts[accountName] : null;
if (accounts && accounts[accountName]) {
return accounts[accountName];
}
} catch (e) {
console.error('[SteamClient] Failed to extract config.vdf data');
return null;
}
return null;
}
public injectAccountConfig(accountName: string, accountData: any) {
const configPath = this.getConfigVdfPath();
if (!configPath) return;
let data: any = {
InstallConfigStore: {
Software: {
Valve: {
Steam: {
Accounts: {}
}
}
}
}
};
// Create directory if it doesn't exist
const configDir = path.dirname(configPath);
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
let data: any = { InstallConfigStore: { Software: { Valve: { Steam: { Accounts: {} } } } } };
if (fs.existsSync(configPath)) {
try {
const content = fs.readFileSync(configPath, 'utf-8');
const parsed = parse(content) as any;
if (parsed && typeof parsed === 'object') data = parsed;
data = parse(content) as any;
} catch (e) { }
}
// Ensure safe nesting
// Ensure structure exists
if (!data.InstallConfigStore) data.InstallConfigStore = {};
if (!data.InstallConfigStore.Software) data.InstallConfigStore.Software = {};
if (!data.InstallConfigStore.Software.Valve) data.InstallConfigStore.Software.Valve = {};
@@ -169,9 +136,11 @@ class SteamClientService {
data.InstallConfigStore.Software.Valve.Steam.Accounts[accountName] = accountData;
try {
this.safeWriteVdf(configPath, data);
console.log(`[SteamClient] Safely injected session for ${accountName}`);
} catch (e) { }
fs.writeFileSync(configPath, stringify(data));
console.log(`[SteamClient] Injected login config for ${accountName} into config.vdf`);
} catch (e) {
console.error('[SteamClient] Failed to write config.vdf');
}
}
public async setAutoLoginUser(accountName: string, accountConfig?: any, steamId?: string): Promise<boolean> {
@@ -179,12 +148,14 @@ class SteamClientService {
const loginUsersPath = this.getLoginUsersPath();
if (loginUsersPath) {
const configDir = path.dirname(loginUsersPath);
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
let data: any = { users: {} };
if (fs.existsSync(loginUsersPath)) {
try {
const content = fs.readFileSync(loginUsersPath, 'utf-8');
const parsed = parse(content) as any;
if (parsed && parsed.users) data = parsed;
data = parse(content) as any;
} catch (e) { }
}
@@ -193,7 +164,7 @@ class SteamClientService {
let found = false;
for (const [id, user] of Object.entries(data.users)) {
const u = user as any;
if (u.AccountName?.toLowerCase() === accountName.toLowerCase()) {
if (u.AccountName.toLowerCase() === accountName.toLowerCase()) {
u.mostrecent = "1";
u.RememberPassword = "1";
u.AllowAutoLogin = "1";
@@ -206,8 +177,8 @@ class SteamClientService {
}
}
if (!found && steamId && accountName) {
console.log(`[SteamClient] Provisioning new user profile for ${accountName}`);
if (!found && steamId) {
console.log(`[SteamClient] Provisioning user ${accountName} into loginusers.vdf`);
data.users[steamId] = {
AccountName: accountName,
PersonaName: accountName,
@@ -222,15 +193,16 @@ class SteamClientService {
}
try {
this.safeWriteVdf(loginUsersPath, data);
} catch (e) { }
fs.writeFileSync(loginUsersPath, stringify(data));
} catch (e) {
console.error('[SteamClient] Failed to write loginusers.vdf');
}
}
if (accountConfig && accountName) {
if (accountConfig) {
this.injectAccountConfig(accountName, accountConfig);
}
// --- Linux Registry / Registry.vdf Hardening ---
if (platform === 'linux') {
const regLocations = [
path.join(os.homedir(), '.steam', 'registry.vdf'),
@@ -238,40 +210,36 @@ class SteamClientService {
];
for (const regPath of regLocations) {
if (!fs.existsSync(path.dirname(regPath))) continue;
let regData: any = { Registry: { HKCU: { Software: { Valve: { Steam: {
AutoLoginUser: "",
RememberPassword: "1",
AlreadyLoggedIn: "1"
} } } } } };
let regData: any = { Registry: { HKCU: { Software: { Valve: { Steam: {} } } } } };
if (fs.existsSync(regPath)) {
try {
const content = fs.readFileSync(regPath, 'utf-8');
const parsed = parse(content) as any;
if (parsed && typeof parsed === 'object') regData = parsed;
regData = parse(content) as any;
} catch (e) { }
} else {
const regDir = path.dirname(regPath);
if (!fs.existsSync(regDir)) fs.mkdirSync(regDir, { recursive: true });
}
// Deep merge helper
const ensurePath = (obj: any, keys: string[]) => {
const setPath = (obj: any, keys: string[], val: string) => {
let curr = obj;
for (const key of keys) {
if (!curr[key] || typeof curr[key] !== 'object') curr[key] = {};
curr = curr[key];
for (let i = 0; i < keys.length - 1; i++) {
if (!curr[keys[i]!]) curr[keys[i]!] = {};
curr = curr[keys[i]!];
}
return curr;
curr[keys[keys.length - 1]!] = val;
};
const steamKey = ensurePath(regData, ['Registry', 'HKCU', 'Software', 'Valve', 'Steam']);
steamKey.AutoLoginUser = accountName;
steamKey.RememberPassword = "1";
steamKey.AlreadyLoggedIn = "1";
steamKey.WantsOfflineMode = "0";
const steamReg = ['Registry', 'HKCU', 'Software', 'Valve', 'Steam'];
setPath(regData, [...steamReg, 'AutoLoginUser'], accountName);
setPath(regData, [...steamReg, 'RememberPassword'], "1");
setPath(regData, [...steamReg, 'AlreadyLoggedIn'], "1");
setPath(regData, [...steamReg, 'WantsOfflineMode'], "0");
try {
this.safeWriteVdf(regPath, regData);
fs.writeFileSync(regPath, stringify(regData));
console.log(`[SteamClient] Registry updated: ${regPath}`);
} catch (e) { }
}
}

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Ultimate Ban Tracker</title>
<title>frontend</title>
</head>
<body>
<div id="root"></div>

View File

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

View File

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

View File

@@ -49,7 +49,6 @@ interface AccountsContextType {
updateServerConfig: (config: Partial<ServerConfig>) => Promise<void>;
loginToServer: () => Promise<void>;
syncNow: () => Promise<void>;
scrapeAccount: (steamId: string) => Promise<boolean>;
getCommunityAccounts: () => Promise<any[]>;
getServerUsers: () => Promise<any[]>;
refreshAccounts: (showLoading?: boolean) => Promise<void>;
@@ -115,12 +114,6 @@ 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();
@@ -201,7 +194,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,
scrapeAccount, adminGetStats, adminGetUsers, adminDeleteUser, adminGetAccounts, adminRemoveAccount
adminGetStats, adminGetUsers, adminDeleteUser, adminGetAccounts, adminRemoveAccount
}}>
{children}
</AccountsContext.Provider>

View File

@@ -376,12 +376,11 @@ const AccountRow: React.FC<{
onSwitch: (login: string) => void,
onAuth: () => void
}> = ({ account, onDelete, onSwitch, onAuth }) => {
const { shareAccountWithUser, revokeAccountAccess, revokeAllAccountAccess, getServerUsers, serverConfig, scrapeAccount } = useAccounts();
const { shareAccountWithUser, revokeAccountAccess, revokeAllAccountAccess, getServerUsers, serverConfig } = useAccounts();
const [timeLeft, setTimeLeft] = useState<string | null>(null);
const [isShareOpen, setIsShareOpen] = useState(false);
const [targetUserId, setTargetUserId] = useState('');
const [isSharing, setIsSharing] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const [serverUsers, setServerUsers] = useState<any[]>([]);
const cooldownDate = account?.cooldownExpiresAt ? new Date(account.cooldownExpiresAt) : null;
@@ -405,12 +404,6 @@ 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 {
@@ -529,12 +522,7 @@ const AccountRow: React.FC<{
{account.steamLoginSecure && !account.authError ? <VerifiedUserIcon fontSize="inherit" /> : (account.authError ? <LockResetIcon fontSize="inherit" /> : <BoltIcon fontSize="inherit" />)}
</IconButton>
{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>
<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>
</Tooltip>