@@ -16,7 +16,9 @@ import "faz-quill-emoji/autoregister";
1616const domain = ( ( window . location . hostname . search ( 'check' ) != - 1 ) || ( window . location . hostname . search ( '127' ) != - 1 ) ) ? 'https://api.check.vssfalcons.com' : `http://${ document . domain } :5000` ;
1717if ( window . location . pathname . split ( '?' ) [ 0 ] . endsWith ( '/admin' ) ) window . location . pathname = '/admin/' ;
1818const params = Object . fromEntries ( ( new URL ( location ) ) . searchParams ) ;
19- const ws = new WebSocket ( `${ domain } /ws?${ new URLSearchParams ( { role : 'admin' } ) . toString ( ) } ` ) ;
19+ var ws = null ;
20+ var reconnectAttempts = 0 ;
21+ var createWebSocket ;
2022
2123var archiveTypeSelected = null ;
2224var courses = [ ] ;
@@ -103,6 +105,86 @@ try {
103105 }
104106 }
105107
108+ createWebSocket = function ( ) {
109+ try {
110+ const usr = storage . get ( 'usr' ) || '' ;
111+ const pwd = storage . get ( 'pwd' ) || '' ;
112+ const params = new URLSearchParams ( { role : 'admin' , usr, pwd } ) . toString ( ) ;
113+ if ( ws && ( ( ws . readyState === WebSocket . OPEN ) || ( ws . readyState === WebSocket . CONNECTING ) ) ) return ;
114+ if ( ws ) ws . close ( ) ;
115+ ws = new WebSocket ( `${ domain } /ws?${ params } ` ) ;
116+ ws . addEventListener ( 'open' , ( ) => {
117+ reconnectAttempts = 0 ;
118+ } ) ;
119+ ws . addEventListener ( 'message' , ( e ) => {
120+ const data = JSON . parse ( e . data ) ;
121+ if ( data . type === 'studentDraw' ) {
122+ const { sessionKey, meta, stroke } = data ;
123+ if ( ( meta . source || 'unknown' ) !== 'clicker' ) return ;
124+ if ( ! liveDrawingSessions [ sessionKey ] ) createSession ( { data } , sessionKey ) ;
125+ const info = liveDrawingSessions [ sessionKey ] ;
126+ info . strokes = info . strokes || [ ] ;
127+ info . strokes . push ( { stroke } ) ;
128+ renderStrokesIntoSession ( sessionKey , info . strokes ) ;
129+ info . wrapper . querySelectorAll ( '.meta' ) . forEach ( el => el . style . display = hideSeatCodes ? 'none' : 'block' ) ;
130+ } else if ( data . type === 'sessionSync' ) {
131+ if ( ! data || ! data . data || ! data . data . meta || ! data . data . meta . source || ! data . data . meta . seatCode || ! data . data . strokes || ! Array . isArray ( data . data . strokes ) || ! data . data . strokes . length ) return ;
132+ const sessionKey = data . sessionKey ;
133+ if ( ! liveDrawingSessions [ sessionKey ] ) createSession ( data , sessionKey ) ;
134+ } else if ( data . type === 'studentUndo' ) {
135+ const { sessionKey, strokeId } = data ;
136+ const info = liveDrawingSessions [ sessionKey ] ;
137+ if ( ! info ) return ;
138+ info . strokes = info . strokes || [ ] ;
139+ info . strokes = info . strokes . filter ( s => {
140+ const st = s . stroke || s ;
141+ if ( ! st ) return true ;
142+ if ( Array . isArray ( st ) ) return ! st . some ( i => ( i && i . id && String ( i . id ) === String ( strokeId ) ) ) ;
143+ if ( st && st . id && String ( st . id ) === String ( strokeId ) ) return false ;
144+ return true ;
145+ } ) ;
146+ renderStrokesIntoSession ( sessionKey , info . strokes ) ;
147+ } else if ( data . type === 'studentClear' ) {
148+ const { sessionKey } = data ;
149+ const info = liveDrawingSessions [ sessionKey ] ;
150+ if ( ! info ) return ;
151+ info . strokes = [ ] ;
152+ renderStrokesIntoSession ( sessionKey , info . strokes || [ ] ) ;
153+ } else if ( data . type === 'resetPeriod' ) {
154+ const period = String ( data . period ) ;
155+ Object . values ( liveDrawingSessions ) . forEach ( info => {
156+ const seat = info ?. meta ?. seatCode ?. toString ?. ( ) || '' ;
157+ if ( seat && seat . startsWith ( period ) ) {
158+ if ( info && info . canvas ) {
159+ const context = info . canvas . getContext ( '2d' ) ;
160+ context . clearRect ( 0 , 0 , info . canvas . width , info . canvas . height ) ;
161+ info . strokes = [ ] ;
162+ }
163+ }
164+ } ) ;
165+ }
166+ } ) ;
167+ ws . addEventListener ( 'close' , ( ) => {
168+ console . warn ( 'Admin websocket closed, scheduling reconnect' ) ;
169+ scheduleAdminReconnect ( ) ;
170+ } ) ;
171+ ws . addEventListener ( 'error' , ( err ) => {
172+ console . error ( 'Admin websocket error' , err ) ;
173+ try { ws . close ( ) ; } catch ( e ) { console . warn ( 'adminWs close failed' , e ) ; }
174+ } ) ;
175+ } catch ( e ) {
176+ scheduleAdminReconnect ( ) ;
177+ }
178+ }
179+
180+ function scheduleAdminReconnect ( ) {
181+ reconnectAttempts = ( reconnectAttempts || 0 ) + 1 ;
182+ const delay = Math . min ( 30000 , Math . pow ( 2 , Math . min ( reconnectAttempts , 6 ) ) * 1000 ) ;
183+ setTimeout ( ( ) => {
184+ createWebSocket ( ) ;
185+ } , delay ) ;
186+ }
187+
106188 if ( ! ( await auth . bulkLoad ( getAdminFields ( ) , storage . get ( "usr" ) , storage . get ( "pwd" ) , true , false , ( ) => {
107189 auth . admin ( init ) ;
108190 pollingOff ( ) ;
@@ -124,7 +206,6 @@ try {
124206 settings = bulkLoad . settings || { } ;
125207 if ( document . querySelector ( '.users' ) ) {
126208 if ( document . getElementById ( 'add-user-button' ) ) document . getElementById ( 'add-user-button' ) . addEventListener ( 'click' , addUserModal ) ;
127-
128209 document . querySelector ( '.users' ) . innerHTML = '<div class="row header"><span>User</span><span>Role</span><span>Partial Access</span><span>Full Access</span><span>Anonymous</span><span>Actions</span></div>' ;
129210 if ( users . length > 0 ) {
130211 document . getElementById ( 'no-users' ) . setAttribute ( 'hidden' , '' ) ;
@@ -178,7 +259,6 @@ try {
178259 }
179260 if ( document . querySelector ( '.passwords' ) ) {
180261 if ( document . getElementById ( 'remove-passwords' ) ) document . getElementById ( 'remove-passwords' ) . addEventListener ( 'click' , removePasswordsModal ) ;
181-
182262 document . querySelector ( '.passwords' ) . innerHTML = '<div class="row header"><span>Seat Code</span><span>Saved Settings</span><span>Actions</span></div>' ;
183263 if ( passwords . length > 0 ) {
184264 document . getElementById ( 'no-passwords' ) . setAttribute ( 'hidden' , '' ) ;
@@ -363,6 +443,9 @@ try {
363443 document . getElementById ( "period-input" ) . innerHTML += `<option value="${ coursePeriod . period } ">Period ${ coursePeriod . period } - ${ coursePeriod . name } </option>` ;
364444 } ) ;
365445 if ( document . getElementById ( 'live-drawing-periods' ) ) {
446+ courses . flatMap ( course => JSON . parse ( course . periods ) ) . forEach ( period => {
447+ getOrCreateGroup ( period ) ;
448+ } )
366449 var currentPeriod = getExtendedPeriod ( ) ;
367450 if ( ( currentPeriod != - 1 ) && courses . flatMap ( course => JSON . parse ( course . periods ) . map ( period => { return { period, name : course . name } } ) ) . some ( coursePeriod => coursePeriod . period === ( currentPeriod + 1 ) ) ) {
368451 document . getElementById ( "period-input" ) . value = getExtendedPeriod ( ) + 1 ;
@@ -375,7 +458,7 @@ try {
375458 }
376459 if ( document . getElementById ( 'saved-live-drawings' ) && params && params . id ) {
377460 const sessionId = params . id ;
378- const savedSession = await fetch ( domain + '/draw/sessions/' + sessionId ) ;
461+ const savedSession = await fetch ( domain + '/draw/sessions/' + sessionId + '?usr=' + encodeURIComponent ( storage . get ( 'usr' ) ) + '&pwd=' + encodeURIComponent ( storage . get ( 'pwd' ) ) ) ;
379462 const savedSessionJSON = await savedSession . json ( ) ;
380463 document . title = `${ savedSessionJSON . session . name } - Virtual Checker` ;
381464 document . querySelector ( '.section h1' ) . innerText = savedSessionJSON . session . name ;
@@ -432,7 +515,17 @@ try {
432515 ui . reloadUnsavedInputs ( ) ;
433516 }
434517
435- init ( ) ;
518+ init ( )
519+ . then ( ( ) => {
520+ const initializeWebSocket = ( ) => {
521+ if ( typeof createWebSocket === 'function' ) {
522+ createWebSocket ( ) ;
523+ } else {
524+ setTimeout ( initializeWebSocket , 200 ) ;
525+ }
526+ } ;
527+ initializeWebSocket ( ) ;
528+ } ) ;
436529
437530 window . addEventListener ( 'beforeunload' , function ( event ) {
438531 if ( ! ui . unsavedChanges ) return ;
@@ -677,6 +770,8 @@ try {
677770 body : JSON . stringify ( {
678771 course_id : course . id ,
679772 platform : 'clicker' ,
773+ usr : storage . get ( 'usr' ) ,
774+ pwd : storage . get ( 'pwd' )
680775 } ) ,
681776 } )
682777 . then ( async ( r ) => {
@@ -735,6 +830,8 @@ try {
735830 body : JSON . stringify ( {
736831 course_id : course . id ,
737832 platform : 'checker' ,
833+ usr : storage . get ( 'usr' ) ,
834+ pwd : storage . get ( 'pwd' )
738835 } ) ,
739836 } )
740837 . then ( async ( r ) => {
@@ -6536,7 +6633,7 @@ try {
65366633 goToPage ( paginationSection , Math . ceil ( pagination [ group ] . total / ( storage . get ( "rowsPerPage" ) ? Number ( storage . get ( "rowsPerPage" ) ) : pagination [ group ] . perPage ) ) - 1 ) ;
65376634 }
65386635
6539- function getOrCreateGroup ( period , source ) {
6636+ function getOrCreateGroup ( period ) {
65406637 let el = document . querySelector ( `[data-period="${ period } "]` ) ;
65416638 if ( el ) return el ;
65426639 el = document . createElement ( 'div' ) ;
@@ -6549,7 +6646,7 @@ try {
65496646 function createSession ( data , sessionKey ) {
65506647 const [ source , seatCode ] = sessionKey . split ( '::' ) ;
65516648 if ( ( data . data . meta . source || 'unknown' ) !== 'clicker' ) return ;
6552- const sessions = getOrCreateGroup ( Number ( seatCode . slice ( 0 , 1 ) ) , data . data . meta . source || 'unknown' ) ;
6649+ const sessions = getOrCreateGroup ( Number ( seatCode . slice ( 0 , 1 ) ) ) ;
65536650 const sessionDiv = document . createElement ( 'div' ) ;
65546651 sessionDiv . className = 'session' ;
65556652 sessionDiv . innerHTML = `<span class="meta">${ data . data . meta . seatCode } </span><div class="canvas-wrapper"></div><div class="overlays"></div>` ;
@@ -6582,58 +6679,7 @@ try {
65826679 }
65836680 }
65846681
6585- if ( document . getElementById ( 'live-drawing-periods' ) ) ws . addEventListener ( 'message' , ( e ) => {
6586- const data = JSON . parse ( e . data ) ;
6587- if ( data . type === 'studentDraw' ) {
6588- const { sessionKey, meta, stroke } = data ;
6589- if ( ( meta . source || 'unknown' ) !== 'clicker' ) return ;
6590- if ( ! liveDrawingSessions [ sessionKey ] ) createSession ( { data } , sessionKey ) ;
6591- const info = liveDrawingSessions [ sessionKey ] ;
6592- info . strokes = info . strokes || [ ] ;
6593- info . strokes . push ( { stroke } ) ;
6594- renderStrokesIntoSession ( sessionKey , info . strokes ) ;
6595- info . wrapper . querySelectorAll ( '.meta' ) . forEach ( el => el . style . display = hideSeatCodes ? 'none' : 'block' ) ;
6596- } else if ( data . type === 'sessionSync' ) {
6597- if ( ! data || ! data . data || ! data . data . meta || ! data . data . meta . source || ! data . data . meta . seatCode || ! data . data . strokes || ! Array . isArray ( data . data . strokes ) || ! data . data . strokes . length ) return ;
6598- const sessionKey = data . sessionKey ;
6599- if ( ! liveDrawingSessions [ sessionKey ] ) createSession ( data , sessionKey ) ;
6600- } else if ( data . type === 'studentClear' ) {
6601- const { sessionKey } = data ;
6602- const info = liveDrawingSessions [ sessionKey ] ;
6603- if ( info ) {
6604- info . strokes = [ ] ;
6605- renderStrokesIntoSession ( sessionKey , info . strokes ) ;
6606- }
6607- } else if ( data . type === 'resetPeriod' ) {
6608- const period = String ( data . period ) ;
6609- Object . keys ( liveDrawingSessions ) . forEach ( sessionKey => {
6610- const parts = sessionKey . split ( '::' ) ;
6611- const seat = parts [ 1 ] || '' ;
6612- if ( String ( seat ) . startsWith ( period ) ) {
6613- const info = liveDrawingSessions [ sessionKey ] ;
6614- if ( info && info . canvas ) {
6615- const ctx = info . canvas . getContext ( '2d' ) ;
6616- ctx . clearRect ( 0 , 0 , info . canvas . width , info . canvas . height ) ;
6617- }
6618- if ( info ) info . strokes = [ ] ;
6619- }
6620- } ) ;
6621- } else if ( data . type === 'studentUndo' ) {
6622- const { sessionKey, strokeId } = data ;
6623- const info = liveDrawingSessions [ sessionKey ] ;
6624- if ( info && Array . isArray ( info . strokes ) ) {
6625- const before = info . strokes . length ;
6626- info . strokes = info . strokes . filter ( s => {
6627- const seg = s . stroke || s ;
6628- if ( ! seg ) return true ;
6629- return String ( seg . id ) !== String ( strokeId ) ;
6630- } ) ;
6631- if ( info . strokes . length !== before ) {
6632- renderStrokesIntoSession ( sessionKey , info . strokes ) ;
6633- }
6634- }
6635- }
6636- } ) ;
6682+ // Live drawing websocket messages are handled on the adminWs created by createAdminWS()
66376683
66386684 function renderStrokesIntoSession ( sessionKey , strokes ) {
66396685 const info = liveDrawingSessions [ sessionKey ] ;
@@ -6725,7 +6771,9 @@ try {
67256771 created : new Date ( ) . toISOString ( ) ,
67266772 period : document . getElementById ( 'period-input' ) . value || null ,
67276773 name : `Live Drawing Session - Period ${ document . getElementById ( 'period-input' ) . value || 0 } - ${ new Date ( ) . toLocaleString ( ) } ` ,
6728- }
6774+ } ,
6775+ usr : storage . get ( 'usr' ) ,
6776+ pwd : storage . get ( 'pwd' )
67296777 } )
67306778 } ) ;
67316779 const saveSessionJSON = await saveSession . json ( ) ;
@@ -6785,7 +6833,7 @@ try {
67856833 } ) ;
67866834
67876835 async function refreshSavedLiveDrawingSessions ( ) {
6788- const refreshSessions = await fetch ( domain + '/draw/sessions' ) ;
6836+ const refreshSessions = await fetch ( domain + '/draw/sessions?usr=' + encodeURIComponent ( storage . get ( 'usr' ) ) + '&pwd=' + encodeURIComponent ( storage . get ( 'pwd' ) ) ) ;
67896837 const refreshSessionsJSON = await refreshSessions . json ( ) ;
67906838 document . querySelector ( '.saved-live-drawing-sessions' ) . innerHTML = '' ;
67916839 if ( refreshSessionsJSON . sessions && refreshSessionsJSON . sessions . length ) {
0 commit comments