1-
1+ // main.js
22import { app , BrowserWindow , BrowserView } from 'electron' ;
3- import path from 'node:path' ;
43import fs from 'node:fs' ;
4+ import path from 'node:path' ;
55
6- let mainWindow , topView , bottomView , config ;
7-
8- // Mitigate blank window on some GPUs / VMs
6+ // ---- Runtime safety: reduce chances of "blank" windows on some GPUs/VMs
97app . disableHardwareAcceleration ( ) ;
108
11- function loadConfig ( ) {
12- const exeDir = path . dirname ( app . getPath ( 'exe' ) ) ;
9+ // -------- Globals
10+ let mainWindow ;
11+ let topView ;
12+ let bottomView ;
13+ let config ;
14+ let cfgPathInUse = null ;
15+ let defaultUA = { top : null , bottom : null } ;
16+
17+ // ---- Helpers
18+
19+ function getExeDir ( ) {
20+ try {
21+ return path . dirname ( app . getPath ( 'exe' ) ) ;
22+ } catch {
23+ return process . cwd ( ) ;
24+ }
25+ }
26+
27+ function findConfigPath ( ) {
28+ const exeDir = getExeDir ( ) ;
1329 const candidates = [
14- path . join ( exeDir , 'config.json' ) ,
15- path . join ( process . cwd ( ) , 'config.json' )
30+ path . join ( exeDir , 'config.json' ) , // portable / next to EXE (preferred)
31+ path . join ( process . cwd ( ) , 'config.json' ) , // dev fallback
1632 ] ;
1733 for ( const p of candidates ) {
18- try { if ( fs . existsSync ( p ) ) return normalize ( JSON . parse ( fs . readFileSync ( p , 'utf-8' ) ) ) ; } catch { }
34+ try {
35+ if ( fs . existsSync ( p ) ) return p ;
36+ } catch { }
1937 }
20- return normalize ( { "topUrl" : "https://awacs-portal2.icloud-prd.eu-west-1.aws.pmicloud.biz/speed/ro70-09-10/operators?plant=RO70&area=Secondary" , "bottomUrl" : "https://awacs-portal2.icloud-prd.eu-west-1.aws.pmicloud.biz/speed/ro70-11-12/operators?plant=RO70&area=Secondary" , "dividerRatio" : 0.55 , "minWidth" : 1200 , "minHeight" : 800 , "userAgent" : "" , "lockToInitialOrigin" : false } ) ;
38+ // If none exist, default to exeDir for future writes/expectations
39+ return path . join ( exeDir , 'config.json' ) ;
2140}
2241
23- function normalize ( cfg ) {
42+ function normalizeConfig ( cfg ) {
2443 return {
25- topUrl : cfg . topUrl || 'https://example.com' ,
26- bottomUrl : cfg . bottomUrl || 'https://example.com' ,
27- dividerRatio : Math . min ( 0.9 , Math . max ( 0.1 , cfg . dividerRatio ?? 0.55 ) ) ,
28- minWidth : cfg . minWidth ?? 1000 ,
29- minHeight : cfg . minHeight ?? 700 ,
30- userAgent : cfg . userAgent || '' ,
31- lockToInitialOrigin : cfg . lockToInitialOrigin ?? false
44+ topUrl : cfg ? .topUrl || 'https://example.com' ,
45+ bottomUrl : cfg ? .bottomUrl || 'https://example.com' ,
46+ dividerRatio : Math . min ( 0.9 , Math . max ( 0.1 , cfg ? .dividerRatio ?? 0.55 ) ) ,
47+ minWidth : cfg ? .minWidth ?? 1000 ,
48+ minHeight : cfg ? .minHeight ?? 700 ,
49+ userAgent : typeof cfg ? .userAgent === 'string' ? cfg . userAgent . trim ( ) : '' ,
50+ lockToInitialOrigin : ! ! cfg ? .lockToInitialOrigin ,
3251 } ;
3352}
3453
35- function getOrigin ( u ) { try { const x = new URL ( u ) ; return `${ x . protocol } //${ x . host } ` ; } catch { return '' ; } }
54+ function loadConfig ( ) {
55+ const p = cfgPathInUse || findConfigPath ( ) ;
56+ cfgPathInUse = p ;
57+ try {
58+ if ( fs . existsSync ( p ) ) {
59+ const raw = fs . readFileSync ( p , 'utf-8' ) ;
60+ const parsed = JSON . parse ( raw ) ;
61+ return normalizeConfig ( parsed ) ;
62+ }
63+ } catch { }
64+ // Fallback defaults (you can inline your own baked defaults here if desired)
65+ return normalizeConfig ( { } ) ;
66+ }
67+
68+ function getOrigin ( u ) {
69+ try {
70+ const x = new URL ( u ) ;
71+ return `${ x . protocol } //${ x . host } ` ;
72+ } catch {
73+ return '' ;
74+ }
75+ }
3676
3777function guardNavigation ( view , initialUrl ) {
3878 if ( ! config . lockToInitialOrigin ) return ;
3979 const origin = getOrigin ( initialUrl ) ;
40- view . webContents . on ( 'will-navigate' , ( e , url ) => { if ( getOrigin ( url ) !== origin ) e . preventDefault ( ) ; } ) ;
41- view . webContents . setWindowOpenHandler ( ( d ) => { return ( getOrigin ( d . url ) !== origin ) ? { action :'deny' } : { action :'allow' } ; } ) ;
80+
81+ view . webContents . on ( 'will-navigate' , ( e , url ) => {
82+ if ( getOrigin ( url ) !== origin ) e . preventDefault ( ) ;
83+ } ) ;
84+ view . webContents . setWindowOpenHandler ( ( d ) => {
85+ if ( getOrigin ( d . url ) !== origin ) return { action : 'deny' } ;
86+ return { action : 'allow' } ;
87+ } ) ;
88+ }
89+
90+ // Simple debounce for file watcher
91+ function debounce ( fn , ms = 250 ) {
92+ let t ;
93+ return ( ...args ) => {
94+ clearTimeout ( t ) ;
95+ t = setTimeout ( ( ) => fn ( ...args ) , ms ) ;
96+ } ;
4297}
4398
99+ // ---- Layout
100+
44101function layout ( ) {
45102 if ( ! mainWindow || ! topView || ! bottomView ) return ;
46- const [ w , h ] = mainWindow . getContentSize ( ) ;
103+ const [ w , h ] = mainWindow . getContentSize ( ) ;
47104 const split = Math . round ( h * config . dividerRatio ) ;
48- topView . setBounds ( { x : 0 , y : 0 , width :w , height :split } ) ;
49- topView . setAutoResize ( { width :true , height :true } ) ;
50- bottomView . setBounds ( { x : 0 , y : split , width :w , height :h - split } ) ;
51- bottomView . setAutoResize ( { width :true , height :true } ) ;
105+ topView . setBounds ( { x : 0 , y : 0 , width : w , height : split } ) ;
106+ topView . setAutoResize ( { width : true , height : true } ) ;
107+ bottomView . setBounds ( { x : 0 , y : split , width : w , height : h - split } ) ;
108+ bottomView . setAutoResize ( { width : true , height : true } ) ;
52109}
53110
111+ // ---- Apply config changes live
112+
113+ function applyConfigChanges ( next ) {
114+ // Determine what changed
115+ const urlsChanged =
116+ next . topUrl !== config . topUrl || next . bottomUrl !== config . bottomUrl ;
117+ const ratioChanged = next . dividerRatio !== config . dividerRatio ;
118+ const uaChanged = next . userAgent !== config . userAgent ;
119+ const lockChanged = next . lockToInitialOrigin !== config . lockToInitialOrigin ;
120+
121+ config = next ;
122+
123+ // Update user agent on each view if changed
124+ if ( uaChanged && topView && bottomView ) {
125+ try {
126+ if ( config . userAgent ) {
127+ topView . webContents . setUserAgent ( config . userAgent ) ;
128+ bottomView . webContents . setUserAgent ( config . userAgent ) ;
129+ } else {
130+ // Restore defaults if we captured them
131+ if ( defaultUA . top ) topView . webContents . setUserAgent ( defaultUA . top ) ;
132+ if ( defaultUA . bottom ) bottomView . webContents . setUserAgent ( defaultUA . bottom ) ;
133+ }
134+ } catch { }
135+ }
136+
137+ // Re-guard navigation if lock setting changed
138+ if ( lockChanged ) {
139+ // Remove old handlers by recreating views is heavy; instead, rely on handler logic
140+ // Since we only ever *tighten* rules, we can add guards now if they weren't present
141+ guardNavigation ( topView , config . topUrl ) ;
142+ guardNavigation ( bottomView , config . bottomUrl ) ;
143+ }
144+
145+ // Reload URLs if changed or UA changed (UA affects next navigation)
146+ if ( urlsChanged || uaChanged ) {
147+ const loadOpts = config . userAgent ? { userAgent : config . userAgent } : undefined ;
148+ if ( topView && topView . webContents . getURL ( ) !== config . topUrl ) {
149+ topView . webContents . loadURL ( config . topUrl , loadOpts ) . catch ( ( ) => { } ) ;
150+ } else if ( uaChanged && topView ) {
151+ topView . webContents . reload ( ) ;
152+ }
153+ if ( bottomView && bottomView . webContents . getURL ( ) !== config . bottomUrl ) {
154+ bottomView . webContents . loadURL ( config . bottomUrl , loadOpts ) . catch ( ( ) => { } ) ;
155+ } else if ( uaChanged && bottomView ) {
156+ bottomView . webContents . reload ( ) ;
157+ }
158+ }
159+
160+ // Re-apply layout if divider changed
161+ if ( ratioChanged ) layout ( ) ;
162+ }
163+
164+ function watchConfigFile ( ) {
165+ if ( ! cfgPathInUse ) return ;
166+ try {
167+ const handler = debounce ( ( ) => {
168+ try {
169+ const next = loadConfig ( ) ;
170+ applyConfigChanges ( next ) ;
171+ } catch {
172+ // Ignore transient parse errors while file is being saved
173+ }
174+ } , 300 ) ;
175+
176+ // Prefer fs.watch; if it fails on network shares, fall back to watchFile
177+ const watcher = fs . watch ( cfgPathInUse , { persistent : false } , handler ) ;
178+ // On some filesystems, rename events may fire; also handle errors silently
179+ watcher . on ( 'error' , ( ) => {
180+ try {
181+ fs . unwatchFile ( cfgPathInUse ) ;
182+ } catch { }
183+ try {
184+ fs . watchFile ( cfgPathInUse , { interval : 500 } , handler ) ;
185+ } catch { }
186+ } ) ;
187+ } catch {
188+ // Fallback: polling
189+ try {
190+ fs . watchFile ( cfgPathInUse , { interval : 500 } , debounce ( ( ) => {
191+ const next = loadConfig ( ) ;
192+ applyConfigChanges ( next ) ;
193+ } , 300 ) ) ;
194+ } catch { }
195+ }
196+ }
197+
198+ // ---- Create window and views
199+
54200function createWindow ( ) {
55201 config = loadConfig ( ) ;
56202
@@ -61,41 +207,78 @@ function createWindow() {
61207 minWidth : config . minWidth ,
62208 minHeight : config . minHeight ,
63209 backgroundColor : '#111111' ,
64- show : true ,
65- autoHideMenuBar : true ,
66- webPreferences : { nodeIntegration :false , contextIsolation :true , sandbox :true , backgroundThrottling :false }
210+ show : true , // show immediately
211+ autoHideMenuBar : true , // no menu (no address bar)
212+ webPreferences : {
213+ nodeIntegration : false ,
214+ contextIsolation : true ,
215+ sandbox : true ,
216+ backgroundThrottling : false ,
217+ } ,
67218 } ) ;
68219
69- topView = new BrowserView ( { webPreferences : { contextIsolation :true , sandbox :true } } ) ;
70- bottomView = new BrowserView ( { webPreferences : { contextIsolation :true , sandbox :true } } ) ;
220+ // Safety net: ensure it shows even if pages hang
221+ setTimeout ( ( ) => { try { mainWindow . show ( ) ; } catch { } } , 1500 ) ;
222+
223+ // Create the two panes
224+ topView = new BrowserView ( {
225+ webPreferences : { contextIsolation : true , sandbox : true } ,
226+ } ) ;
227+ bottomView = new BrowserView ( {
228+ webPreferences : { contextIsolation : true , sandbox : true } ,
229+ } ) ;
71230
72231 mainWindow . setBrowserView ( topView ) ;
73232 mainWindow . addBrowserView ( bottomView ) ;
74233
75- const loadOpts = config . userAgent ? { userAgent : config . userAgent } : undefined ;
234+ // Capture default UA strings before any overrides
235+ try {
236+ defaultUA . top = topView . webContents . getUserAgent ?. ( ) || null ;
237+ defaultUA . bottom = bottomView . webContents . getUserAgent ?. ( ) || null ;
238+ } catch { }
76239
77- setTimeout ( ( ) => { try { mainWindow . show ( ) ; } catch { } } , 2000 ) ;
240+ // Optional custom UA
241+ if ( config . userAgent ) {
242+ try {
243+ topView . webContents . setUserAgent ( config . userAgent ) ;
244+ bottomView . webContents . setUserAgent ( config . userAgent ) ;
245+ } catch { }
246+ }
78247
79- topView . webContents . loadURL ( config . topUrl , loadOpts ) . catch ( ( ) => { } ) ;
80- bottomView . webContents . loadURL ( config . bottomUrl , loadOpts ) . catch ( ( ) => { } ) ;
248+ // Load URLs
249+ const loadOpts = config . userAgent ? { userAgent : config . userAgent } : undefined ;
250+ topView . webContents . loadURL ( config . topUrl , loadOpts ) . catch ( ( ) => { } ) ;
251+ bottomView . webContents . loadURL ( config . bottomUrl , loadOpts ) . catch ( ( ) => { } ) ;
81252
253+ // Navigation guard (optional, per config)
82254 guardNavigation ( topView , config . topUrl ) ;
83255 guardNavigation ( bottomView , config . bottomUrl ) ;
84256
257+ // Layout now and on resize
85258 mainWindow . on ( 'resize' , layout ) ;
86- mainWindow . on ( 'ready-to-show' , ( ) => { layout ( ) ; try { mainWindow . show ( ) ; } catch { } } ) ;
259+ mainWindow . once ( 'ready-to-show' , ( ) => { layout ( ) ; try { mainWindow . show ( ) ; } catch { } } ) ;
87260
261+ // Useful diagnostics if something fails to load
88262 for ( const wc of [ topView . webContents , bottomView . webContents ] ) {
89263 wc . on ( 'did-fail-load' , ( _e , code , desc , validatedURL ) => {
90264 console . error ( 'did-fail-load' , code , desc , validatedURL ) ;
91265 } ) ;
92266 wc . on ( 'crashed' , ( ) => console . error ( 'webContents crashed' ) ) ;
93267 }
268+
269+ // Start watching config.json for live updates
270+ watchConfigFile ( ) ;
94271}
95272
273+ // ---- App lifecycle
274+
96275app . whenReady ( ) . then ( ( ) => {
97276 createWindow ( ) ;
98- app . on ( 'activate' , ( ) => { if ( BrowserWindow . getAllWindows ( ) . length === 0 ) createWindow ( ) ; } ) ;
277+ app . on ( 'activate' , ( ) => {
278+ if ( BrowserWindow . getAllWindows ( ) . length === 0 ) createWindow ( ) ;
279+ } ) ;
99280} ) ;
100281
101- app . on ( 'window-all-closed' , ( ) => { if ( process . platform !== 'darwin' ) app . quit ( ) ; } ) ;
282+ app . on ( 'window-all-closed' , ( ) => {
283+ if ( process . platform !== 'darwin' ) app . quit ( ) ;
284+ } ) ;
0 commit comments