fix(1.0.5): revert Sec-CH-UA spoof (white pages), add DevTools shortcut

- Sec-CH-UA / Sec-CH-UA-Mobile / Sec-CH-UA-Platform header overrides on
  every request in 1.0.4 broke page rendering (all views white). Reverted
  to image-Referer-only behavior from 1.0.3. The Google "embedded browser"
  fix in 1.0.4 came primarily from the adblock whitelist (which IS kept)
  — Sec-CH-UA spoofing was the suspect for the regression.
- Ctrl+Shift+I and F12 now open DevTools on the main shell and on every
  in-app browser view. Always-on so kiosk machines can be debugged
  without leaving kiosk mode.
- Restore session sequenced (await 150ms between tabs) to avoid concurrent
  create-view races where multiple setLoader/addChild interleaved.
- Update banner now shows error state with a "Повторить" button instead
  of hiding it, so install-update failures are visible to the user.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 21:24:38 +03:00
parent 542be8135a
commit 461e7ed737
4 changed files with 59 additions and 40 deletions

80
main.js
View File

@@ -334,6 +334,20 @@ ipcMain.handle('check-update-now', async () => {
// --- Window ---
function attachDevToolsShortcut(webContents) {
// Ctrl+Shift+I / F12 open DevTools on this webContents.
// Always available so a kiosk machine can be debugged without un-kiosking.
webContents.on('before-input-event', (_e, input) => {
if (input.type !== 'keyDown') return;
const isDevToolsCombo =
(input.control && input.shift && (input.key === 'I' || input.key === 'i')) ||
input.key === 'F12';
if (isDevToolsCombo) {
try { webContents.openDevTools({ mode: 'detach' }); } catch (_) {}
}
});
}
async function createWindow() {
mainWindow = new BrowserWindow({
width: 1280,
@@ -347,6 +361,8 @@ async function createWindow() {
},
});
attachDevToolsShortcut(mainWindow.webContents);
if (isDev) {
mainWindow.loadURL(RENDERER_URL);
} else {
@@ -506,19 +522,22 @@ async function restoreSession() {
const sess = cfg.openedSession;
if (!sess || !Array.isArray(sess.tabs) || !sess.tabs.length) return;
console.log(`[session] restoring ${sess.tabs.length} tab(s), active=${sess.activeName}`);
// Spawn each saved tab by replaying create-view. ipcMain.emit triggers the handler
// synchronously; the view's loadURL is fire-and-forget. We chain via setTimeout to
// avoid stacking N loaders simultaneously.
// Spawn each saved tab by replaying create-view, sequentially with a small delay.
// Concurrent create-view calls in v1.0.3 caused races: multiple setLoader/addChild
// interleaved → some views ended up unmounted (white screen). Spacing them out
// gives each view time to mount before the next steals currentView.
const fakeEvent = { sender: mainWindow.webContents };
for (const tab of sess.tabs) {
if (!tab?.name || !tab?.url) continue;
ipcMain.emit('create-view', { sender: mainWindow.webContents }, tab.name, tab.url, tab.imageUrl || '', 1.0, !!tab.useProxy);
ipcMain.emit('create-view', fakeEvent, tab.name, tab.url, tab.imageUrl || '', 1.0, !!tab.useProxy);
await new Promise(r => setTimeout(r, 150));
}
// After all spawned, the last one is `currentView`. Switch to the saved active if different.
// Switch to saved active if it isn't already the last-spawned (currentView).
if (sess.activeName === 'home') {
ipcMain.emit('hide-view', { sender: mainWindow.webContents });
ipcMain.emit('hide-view', fakeEvent);
sendOpenedApps('home');
} else if (sess.activeName && sess.activeName !== currentView?.name) {
ipcMain.emit('show-view', { sender: mainWindow.webContents }, sess.activeName);
ipcMain.emit('show-view', fakeEvent, sess.activeName);
}
} catch (e) {
console.warn('[session] restore failed:', e.message);
@@ -557,6 +576,7 @@ ipcMain.on('create-view', async (_event, name, url, imageUrl, _zoom, useProxy) =
openedApps.push(appEntry);
currentView = appEntry;
view.setBounds(getViewBounds());
attachDevToolsShortcut(view.webContents);
view.webContents.on('did-finish-load', () => {
removeLoader();
@@ -1190,37 +1210,23 @@ app.whenReady().then(async () => {
app.userAgentFallback = cleanUserAgent;
session.defaultSession.setUserAgent(cleanUserAgent);
// Chrome version from the cleaned UA — used for client hints below
const chromeVerMatch = cleanUserAgent.match(/Chrome\/(\d+)/);
const chromeMajor = chromeVerMatch ? chromeVerMatch[1] : '128';
const secChUa = `"Not_A Brand";v="8", "Chromium";v="${chromeMajor}", "Google Chrome";v="${chromeMajor}"`;
const installRequestHooks = (sess) => {
sess.webRequest.onBeforeSendHeaders(
{ urls: ['https://*/*', 'http://*/*'] },
(details, callback) => {
const headers = details.requestHeaders;
// Spoof Sec-CH-UA so embedded-browser detectors (Google login, etc.) see
// a real-Chrome brand list. Electron normally injects the app name as
// the brand which is how Google fingerprints us as "embedded/unsafe".
headers['sec-ch-ua'] = secChUa;
headers['sec-ch-ua-mobile'] = '?0';
headers['sec-ch-ua-platform'] = '"Windows"';
// Add Referer to image requests so hotlink protection doesn't block them
if (details.resourceType === 'image' && !headers['Referer'] && !headers['referer']) {
try {
const u = new URL(details.url);
headers['Referer'] = `${u.protocol}//${u.hostname}/`;
} catch (_) {}
}
callback({ requestHeaders: headers });
// Add Referer to image requests so hotlink protection doesn't block them.
// (Sec-CH-UA spoofing was tried in 1.0.4 and caused white pages — reverted.
// Google embedded-browser detection is now mitigated only via adblock whitelist
// of gstatic/google-analytics/etc., which previously was being eaten silently.)
session.defaultSession.webRequest.onBeforeSendHeaders(
{ urls: ['https://*/*', 'http://*/*'] },
(details, callback) => {
const headers = details.requestHeaders;
if (details.resourceType === 'image' && !headers['Referer'] && !headers['referer']) {
try {
const u = new URL(details.url);
headers['Referer'] = `${u.protocol}//${u.hostname}/`;
} catch (_) {}
}
);
};
installRequestHooks(session.defaultSession);
installRequestHooks(getProxySession());
installRequestHooks(getDirectSession());
callback({ requestHeaders: headers });
}
);
// Apply proxy from config before blocker tries to download filter lists
loadTrustedDomainsFromDisk();