1+
2+ import type * as puppeteerType from 'puppeteer' ;
3+ import type * as fsType from 'fs' ;
4+ const { createRequire } = require ( 'module' ) ;
5+ const requireFromDisk = createRequire ( __filename ) ;
6+ process . env [ 'PUPPETEER_CACHE_DIR' ] = require ( 'path' ) . resolve ( __dirname , '.cache/puppeteer' ) ;
7+ const puppeteer : typeof puppeteerType = requireFromDisk ( 'puppeteer' ) ;
8+ const fs : typeof fsType = require ( 'fs' ) ;
9+
10+ ( async ( ) => {
11+ // Simple argument parser for named flags
12+ const args = process . argv . slice ( 2 ) ;
13+ // Aliases for flags
14+ const flagAliases = Object . entries ( {
15+ '-u' : 'url' , '--url' : 'url' ,
16+ '-U' : 'username' , '--username' : 'username' ,
17+ '-p' : 'password' , '--password' : 'password' ,
18+ '-o' : 'output' , '--output' : 'output' ,
19+ '-h' : 'headless' , '--headless' : 'headless' ,
20+ '-v' : 'verbose' , '--verbose' : 'verbose' ,
21+ '-f' : 'force' , '--force' : 'force' ,
22+ '-i' : 'ignoressl' , '--ignoressl' : 'ignoressl' ,
23+ '-t' : 'timeout' , '--timeout' : 'timeout'
24+ } ) ;
25+
26+ // Helper to get argument value by flag or alias
27+ const getArg = ( name : string ) => {
28+ const flags = flagAliases . filter ( ( [ _k , v ] ) => v === name ) . map ( ( [ k ] ) => k ) ;
29+ for ( const flag of flags ) {
30+ const idx = args . findIndex ( a => a === flag ) ;
31+ if ( idx !== - 1 && idx + 1 < args . length ) {
32+ const val = args [ idx + 1 ] ;
33+ if ( val === '-' || ! val . startsWith ( '-' ) ) {
34+ return val ;
35+ }
36+ }
37+ }
38+ return undefined ;
39+ } ;
40+ // Helper to check if a flag or alias is present
41+ const hasFlag = ( name : string ) => {
42+ const flags = flagAliases . filter ( ( [ _k , v ] ) => v === name ) . map ( ( [ k ] ) => k ) ;
43+ return flags . some ( flag => args . includes ( flag ) ) ;
44+ } ;
45+
46+ // Special case: -v or --version with no other parameters
47+ const onlyVersion = ( args . length === 1 && ( args [ 0 ] === '-v' || args [ 0 ] === '--version' ) ) ;
48+ if ( hasFlag ( 'version' ) || onlyVersion ) {
49+ console . log ( 'v0.1.0' ) ;
50+ return ;
51+ }
52+
53+ const url = getArg ( 'url' ) ;
54+ const username = getArg ( 'username' ) ;
55+ const password = getArg ( 'password' ) ;
56+ const outputFile = getArg ( 'output' ) || 'errors.json' ;
57+ const force = hasFlag ( 'force' ) ;
58+ const headlessArg = getArg ( 'headless' ) ;
59+ const verbose = hasFlag ( 'verbose' ) || getArg ( 'verbose' ) === 'true' ;
60+
61+ if ( ! url || ! username || ! password ) {
62+ console . error (
63+ 'Usage: graphics-error-scraper [options]\n' +
64+ ' -u, --url <url> Target URL (required)\n' +
65+ ' -U, --username <username> Username (required)\n' +
66+ ' -p, --password <password> Password (required)\n' +
67+ ' -o, --output <file> Output file (default: errors.json, use - for stdout)\n' +
68+ ' -f, --force Overwrite output file if it exists\n' +
69+ ' -i, --ignoressl Ignore SSL certificate errors\n' +
70+ ' -t, --timeout <ms> Timeout for page actions in milliseconds (default: 180000)\n' +
71+ ' -h, --headless <bool> Headless mode (true/false, default: true)\n' +
72+ ' -v, --verbose Verbose logging (flag or "true")\n' +
73+ ' --version, -v Print version and exit'
74+ ) ;
75+ process . exit ( 1 ) ;
76+ }
77+ // headless: optional, defaults to true, accepts 'true' or 'false'
78+ const headless = headlessArg === undefined ? true : headlessArg . toLowerCase ( ) === 'true' ;
79+ const ignoreSSL = hasFlag ( 'ignoressl' ) ;
80+ const timeoutArg = getArg ( 'timeout' ) ;
81+ const timeout = timeoutArg !== undefined && ! isNaN ( Number ( timeoutArg ) ) ? Number ( timeoutArg ) : 180000 ;
82+
83+ // Helper for conditional logging
84+ const log = ( ...args : any [ ] ) => { if ( verbose ) console . log ( ...args ) ; } ;
85+
86+ // Browser setup
87+ const startTime = Date . now ( ) ;
88+ log ( "Started: " + new Date ( ) . toLocaleString ( ) ) ;
89+ log ( `Navigating to ${ url } ` ) ;
90+ const launchOpts : puppeteerType . LaunchOptions & Record < string , any > = headless
91+ ? { headless : true }
92+ : { headless : false , defaultViewport : null , args : [ '--start-maximized' ] } ;
93+ if ( ignoreSSL ) {
94+ launchOpts . acceptInsecureCerts = true ;
95+ if ( launchOpts . args ) {
96+ launchOpts . args . push ( '--disable-features=HttpsFirstBalancedModeAutoEnable' ) ;
97+ } else {
98+ launchOpts . args = [ '--disable-features=HttpsFirstBalancedModeAutoEnable' ] ;
99+ }
100+ }
101+ launchOpts . timeout = timeout ;
102+ launchOpts . protocolTimeout = timeout ;
103+ const browser = await puppeteer . launch ( launchOpts ) ;
104+ const [ page ] = await browser . pages ( ) ;
105+ page . setDefaultTimeout ( timeout ) ;
106+ page . setDefaultNavigationTimeout ( timeout ) ;
107+
108+ // Promise-based wait function
109+ async function wait ( ) {
110+ await new Promise ( resolve => setTimeout ( resolve , 1000 ) ) ;
111+ await page . waitForNetworkIdle ( ) ;
112+ }
113+
114+ async function logout ( ) {
115+ await page . locator ( 'img[title="System Menu"]' ) . click ( ) ;
116+ await wait ( ) ;
117+ await page . evaluate ( ( ) => {
118+ const x = ( document . getElementById ( 'rightMenuiframe' ) as HTMLIFrameElement ) ?. contentWindow ?. document . getElementById ( 'main_logout' ) ;
119+ if ( x ?. onmouseup ) {
120+ x . onmouseup ( new MouseEvent ( 'mouseup' , { bubbles : false } ) ) ;
121+ }
122+ } ) ;
123+ await wait ( ) ;
124+ await browser . close ( ) ;
125+ process . exit ( 0 ) ;
126+ }
127+
128+ // Login
129+ await page . goto ( url ) ;
130+ log ( 'Logging in...' ) ;
131+ await page . locator ( '#nameInput' ) . fill ( username ) ;
132+ await page . locator ( '#pass' ) . fill ( password ) ;
133+ await page . locator ( '#submit' ) . click ( ) ;
134+ await page . waitForNavigation ( ) ;
135+ await wait ( ) ;
136+ const navFrame = await ( await ( await ( await page . mainFrame ( ) . $ ( '#navTableFrame' ) ) ?. contentFrame ( ) ) ?. $ ( '#navContent' ) ) ?. contentFrame ( ) ;
137+ if ( ! navFrame ) {
138+ console . error ( 'Navigation frame not found. Possibly invalid credentials.' ) ;
139+ process . exit ( 3 ) ;
140+ }
141+
142+ // Expand all areas
143+ log ( "Expanding geographic tree nodes..." ) ;
144+ while ( await page . evaluate ( ( ) => {
145+ let change = false ;
146+ const doc = ( document . getElementById ( 'navTableFrame' ) as HTMLIFrameElement ) ?. contentWindow ?. document . getElementById ( 'navContent' ) ;
147+ const doc2 = ( doc as HTMLIFrameElement ) ?. contentWindow ?. document ;
148+ if ( ! doc2 ) {
149+ return false ;
150+ }
151+ for ( const x of doc2 . querySelectorAll ( '.TreeCtrl-twisty' ) ) {
152+ if ( ( x as HTMLElement ) . getAttribute ( 'src' ) ?. includes ( '/clean_collapsed.png' ) ) {
153+ const icon = x . parentElement ?. querySelector ( '.TreeCtrl-content > img.TreeCtrl-icon' ) ;
154+ if ( icon && ( icon as HTMLElement ) . getAttribute ( 'src' ) ?. endsWith ( '/area.gif' ) ) {
155+ ( x as HTMLElement ) . click ( ) ;
156+ change = true ;
157+ }
158+ }
159+ }
160+ return change ;
161+ } ) ) {
162+ await wait ( ) ;
163+ }
164+
165+ // Check for errors
166+ log ( "Checking for errors..." ) ;
167+ async function getDisplayPath ( g : any ) {
168+ return await g . evaluate ( ( g : any ) => {
169+ let p ;
170+ let s = '' ;
171+ let q ;
172+ while ( p = g ?. querySelector ( '.TreeCtrl-text' ) ?. innerText ) {
173+ s = p + ( s ? ' / ' : '' ) + s ;
174+ q = g ?. parentElement ?. parentElement ?. parentElement ?. parentElement ?. querySelector ( '.TreeCtrl-content' ) ?? undefined ;
175+ if ( g === q ) {
176+ break ;
177+ }
178+ g = q ;
179+ }
180+ return s ;
181+ } ) ;
182+ }
183+
184+ const errors = [ ] ;
185+ for ( const g of await navFrame . $$ ( '.TreeCtrl-outer[id^=geoTree] .TreeCtrl-content' ) ) {
186+ await g . click ( ) ;
187+ await wait ( ) ;
188+ if ( await page . $ ( '#actButtonSpan > span[title="View graphics"]' ) && await page . $ ( '#errorIndication:not([style*="display: none"])' ) ) {
189+ const e = await page . evaluate ( ( ) => {
190+ return {
191+ //@ts -ignore
192+ mainErrors : DisplayError . getMainErrors ( ) ,
193+ //@ts -ignore
194+ actionErrors : DisplayError . getActionErrors ( ) ,
195+ //@ts -ignore
196+ infoMessages : DisplayError . getInfoMessages ( )
197+ } ;
198+ } ) ;
199+ // Set url property to undefined if present in any error object
200+ const cleanList = ( arr : any [ ] ) => arr . map ( obj => {
201+ if ( obj && typeof obj === 'object' && 'url' in obj ) {
202+ return { ...obj , url : undefined } ;
203+ }
204+ return obj ;
205+ } ) ;
206+ const errorObj : any = { path : await getDisplayPath ( g ) } ;
207+ if ( e . mainErrors && e . mainErrors . length > 0 ) errorObj . mainErrors = cleanList ( e . mainErrors ) ;
208+ if ( e . actionErrors && e . actionErrors . length > 0 ) errorObj . actionErrors = cleanList ( e . actionErrors ) ;
209+ if ( e . infoMessages && e . infoMessages . length > 0 ) errorObj . infoMessages = cleanList ( e . infoMessages ) ;
210+ errors . push ( errorObj ) ;
211+ }
212+ }
213+ if ( outputFile === '-' ) {
214+ console . log ( JSON . stringify ( errors , null , 2 ) ) ;
215+ } else {
216+ if ( ! force && fs . existsSync ( outputFile ) ) {
217+ console . error ( `Output file '${ outputFile } ' already exists. Use --force or -f to overwrite.` ) ;
218+ process . exit ( 2 ) ;
219+ }
220+ fs . writeFileSync ( outputFile , JSON . stringify ( errors , null , 2 ) ) ;
221+ log ( `Results written to ${ outputFile } ` ) ;
222+ }
223+
224+ // Logout
225+ log ( "Logging out..." ) ;
226+ await logout ( ) ;
227+ log ( "Ended: " + new Date ( ) . toLocaleString ( ) ) ;
228+ log ( `Completed in ${ ( ( Date . now ( ) - startTime ) / 60000 ) . toFixed ( 2 ) } minutes.` ) ;
229+ } ) ( ) ;
0 commit comments