Spaces:
Build error
Build error
| const express = require('express'); | |
| const http = require('http'); | |
| const path = require('path'); | |
| const WebSocket = require('ws'); | |
| const puppeteer = require('puppeteer'); | |
| const app = express(); | |
| const server = http.createServer(app); | |
| const wss = new WebSocket.Server({ server }); | |
| app.use(express.static(path.join(__dirname, 'public'))); | |
| app.get('/health', (req, res) => { | |
| res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString() }); | |
| }); | |
| const LOGIN_EMAIL = process.env.LOGIN_EMAIL; | |
| const LOGIN_PASSWORD = process.env.LOGIN_PASSWORD; | |
| const LOGIN_URL = 'https://static.practicefusion.com/apps/auth/?sessionId=48a4227f-b3a3-49ed-bc72-6df4b0533d56&state=vn7gnwhM9JcNhBkBGJEffcseDaCaealLLV3pVktV0RA.iLHD0UfE0jM.Catalyst&clientId=l7xxb2e568db27ed45a5908aaf31f7ce9651'; | |
| function broadcast(event, data) { | |
| const message = JSON.stringify({ event, data }); | |
| console.log(`Broadcasting: ${event} - ${typeof data === 'string' ? data : JSON.stringify(data)}`); | |
| wss.clients.forEach(client => { | |
| if (client.readyState === WebSocket.OPEN) { | |
| client.send(message); | |
| } | |
| }); | |
| } | |
| const bookmarkletCode = ` | |
| (function(){ | |
| function wait(ms){return new Promise(r=>setTimeout(r,ms));} | |
| var stopFlag=false,isRunning=false,timerId=null,timerLeft=0,timerBtn=null,processedRows=new Set(); | |
| function log(){console.log.apply(console,arguments);} | |
| function setTimer(s){ | |
| timerLeft=s; | |
| updateTimerBtn(); | |
| if(timerId)clearInterval(timerId); | |
| timerId=setInterval(function(){ | |
| if(stopFlag){ | |
| clearInterval(timerId); | |
| return; | |
| } | |
| timerLeft--; | |
| updateTimerBtn(); | |
| if(timerLeft<=0){ | |
| clearInterval(timerId); | |
| } | |
| },1000); | |
| } | |
| function ensureTimerBtn(){ | |
| if(timerBtn&&document.body.contains(timerBtn))return; | |
| timerBtn=document.createElement('button'); | |
| timerBtn.style.cssText='position:fixed;top:12px;right:12px;z-index:2147483647;font-weight:bold;font-size:14px;padding:8px 12px;background:#d32f2f;color:#fff;border:none;border-radius:6px;cursor:pointer;box-shadow:0 1px 8px rgba(0,0,0,.18)'; | |
| timerBtn.onclick=function(){ | |
| stopFlag=true; | |
| alert("Stopped by Stop button"); | |
| }; | |
| document.body.appendChild(timerBtn); | |
| updateTimerBtn(); | |
| } | |
| function updateTimerBtn(){ | |
| if(!timerBtn)ensureTimerBtn(); | |
| timerBtn.textContent='⏹️ Stop • Next in '+(timerLeft>0?timerLeft:0)+'s'; | |
| } | |
| function addStopHotkey(){ | |
| document.addEventListener('keydown',hotkeyHandler); | |
| } | |
| function removeStopHotkey(){ | |
| document.removeEventListener('keydown',hotkeyHandler); | |
| } | |
| function hotkeyHandler(e){ | |
| if(e.ctrlKey&&e.key==='Enter'){ | |
| stopFlag=true; | |
| alert('Automation stopped (Ctrl+Enter)'); | |
| } | |
| } | |
| function extractSearchValue(msgbox){ | |
| var html=msgbox.innerHTML.replace(/\\s+/g,' ').trim(); | |
| if(!html)return[null,null]; | |
| var phoneMatch=html.match(/Phone\\s*:\\s*([^<]+?)(?:<br>|$)/i); | |
| if(phoneMatch&&phoneMatch[1].trim()!==''){ | |
| return[phoneMatch[1].replace(/[^\\d]/g,'').trim(),'phone']; | |
| } | |
| var custMatch=html.match(/Customer ID\\s*:\\s*([^<]+?)(?:<br>|$)/i); | |
| if(custMatch&&custMatch[1].trim()!==''){ | |
| return[custMatch[1].trim(),'customerId']; | |
| } | |
| var firstN=html.match(/First Name\\s*:\\s*([^<]+?)(?:<br>|$)/i); | |
| var lastN=html.match(/Last Name\\s*:\\s*([^<]+?)(?:<br>|$)/i); | |
| if(firstN&&lastN){ | |
| return[(firstN[1].trim()+' '+lastN[1].trim()).replace(/\\s+/g,' '),'fullName']; | |
| } | |
| return[null,null]; | |
| } | |
| async function clickPatientAgeSpanMatching(name,mode){ | |
| if(!name)return false; | |
| var firstWord=name.split(' ')[0].toLowerCase(); | |
| var results=[...document.querySelectorAll('.result[data-result-type="patients"] .title.ng-binding')]; | |
| var foundSpan=null; | |
| for(let title of results){ | |
| var patientText=title.childNodes[0]?title.childNodes[0].textContent.trim():''; | |
| var span=title.querySelector('span.ng-binding[style*="float"]')||title.querySelector('span.ng-binding'); | |
| if(!patientText||!span)continue; | |
| if(mode==='phone'||mode==='customerId'){ | |
| foundSpan=span; | |
| break; | |
| } | |
| let patientTextLower=patientText.toLowerCase(); | |
| if(patientTextLower===name.toLowerCase()||patientTextLower.split(' ')[0]===firstWord||patientTextLower.includes(firstWord)){ | |
| foundSpan=span; | |
| break; | |
| } | |
| } | |
| if(foundSpan){ | |
| foundSpan.click(); | |
| log('Clicked matching patient span:',foundSpan.textContent); | |
| return true; | |
| } | |
| return false; | |
| } | |
| async function clickProviderDropdown(name){ | |
| let search=document.querySelector('#search-selection-input-notify-doc'); | |
| if(!search)return false; | |
| search.value=name; | |
| search.focus(); | |
| search.dispatchEvent(new Event('input',{bubbles:true})); | |
| search.dispatchEvent(new KeyboardEvent('keyup',{bubbles:true})); | |
| await wait(800); | |
| const options=[...document.querySelectorAll('div.item.ng-binding,div.item.ng-binding.ng-scope')]; | |
| let found=options.find(o=>o.textContent.trim()===name)||options.find(o=>o.textContent.includes(name)); | |
| if(found){ | |
| found.click(); | |
| log('Clicked provider option:',name); | |
| return true; | |
| } | |
| return false; | |
| } | |
| async function clickSendButton(){ | |
| let btn=document.querySelector('.modal-footer .doer-actions button.ui.large.darkblue.button[ng-click*="submit"]'); | |
| if(btn&&!btn.disabled&&!btn.classList.contains('disabled')){ | |
| btn.click(); | |
| log('Clicked Send'); | |
| return true; | |
| } | |
| return false; | |
| } | |
| async function clickDiscardButton(){ | |
| let discard=document.querySelector('.doer-actions button.ui.large.defgray.button[ng-click*="close"]'); | |
| if(discard){ | |
| discard.click(); | |
| log('Clicked Discard'); | |
| return true; | |
| } | |
| return false; | |
| } | |
| async function clickRefreshAndWait(){ | |
| const btn=document.querySelector('#looker_refresh'); | |
| if(btn&&!btn.disabled){ | |
| btn.click(); | |
| log('Clicked Refresh'); | |
| }else{ | |
| log('Refresh button not found/disabled'); | |
| } | |
| setTimer(5); | |
| await wait(5000); | |
| } | |
| function getBottomAutoReceiptRow(){ | |
| const rows=[...document.querySelectorAll('.item_row')]; | |
| for(let i=rows.length-1;i>=0;i--){ | |
| const row=rows[i]; | |
| if(processedRows.has(row))continue; | |
| const txt=(row.querySelector('.item_from.ng-binding')||{}).textContent?.trim(); | |
| if(txt==='Auto-Receipt')return row; | |
| } | |
| return null; | |
| } | |
| async function automateAutoReceiptOnce(){ | |
| if(stopFlag)return false; | |
| const row=getBottomAutoReceiptRow(); | |
| if(!row){ | |
| log('No Auto-Receipt row found, refreshing in 5s...'); | |
| await clickRefreshAndWait(); | |
| return false; | |
| } | |
| processedRows.add(row); | |
| let expandIcon=row.querySelector('.icon.collapse_icon.expand')||row.querySelector('[ng-click*="openItem"]'); | |
| if(!expandIcon)return false; | |
| expandIcon.click(); | |
| await wait(1200); | |
| if(stopFlag)return false; | |
| let messageText=document.querySelector('.message-text'); | |
| if(!messageText||!messageText.textContent.trim()){ | |
| log('Blank message text - discarding'); | |
| await clickDiscardButton(); | |
| return false; | |
| } | |
| let[val,mode]=extractSearchValue(messageText); | |
| if(!val){ | |
| log('No Phone/CustomerID/Name - discarding'); | |
| await clickDiscardButton(); | |
| return false; | |
| } | |
| let sendBtn=row.querySelector('button.sendItem[data-qa*="send"]'); | |
| if(!sendBtn){ | |
| for(let b of row.querySelectorAll('button')){ | |
| if(b.textContent.includes('Send Item')){ | |
| sendBtn=b; | |
| break; | |
| } | |
| } | |
| } | |
| if(!sendBtn)return false; | |
| sendBtn.click(); | |
| await wait(1200); | |
| if(stopFlag)return false; | |
| let searchInput=document.querySelector('#doerSearchContactsQuery'); | |
| if(!searchInput)return false; | |
| searchInput.value=val; | |
| searchInput.focus(); | |
| searchInput.dispatchEvent(new Event('input',{bubbles:true})); | |
| searchInput.dispatchEvent(new KeyboardEvent('keyup',{bubbles:true})); | |
| await wait(1200); | |
| if(stopFlag)return false; | |
| let ageClick=await clickPatientAgeSpanMatching(val,mode); | |
| if(!ageClick){ | |
| let noRes=document.querySelector('.category .title'); | |
| if(noRes&&noRes.textContent.includes('No results')){ | |
| log('No results - discarding'); | |
| await clickDiscardButton(); | |
| return false; | |
| } | |
| } | |
| await wait(500); | |
| if(stopFlag)return false; | |
| let ehrBtn=document.querySelector('#addressee_action_emr')||[...document.querySelectorAll('div[ng-click*="toggleCheckedAction"]')].find(d=>d.textContent.includes('Send to EHR')); | |
| if(ehrBtn){ | |
| ehrBtn.click(); | |
| await wait(500); | |
| let okBtn=[...document.querySelectorAll('button.ui.button')].find(b=>b.textContent.trim()==='OK'); | |
| if(okBtn){ | |
| okBtn.click(); | |
| await wait(500); | |
| } | |
| } | |
| let subj=document.querySelector('#subject-emr-1-0')||document.querySelector('input[placeholder="Document name"]'); | |
| if(subj){ | |
| subj.value='Receipt'; | |
| subj.focus(); | |
| subj.dispatchEvent(new Event('input',{bubbles:true})); | |
| } | |
| await wait(300); | |
| if(stopFlag)return false; | |
| await clickProviderDropdown('Derin Patel'); | |
| await wait(300); | |
| if(stopFlag)return false; | |
| await clickSendButton(); | |
| await clickRefreshAndWait(); | |
| return true; | |
| } | |
| async function mainLoop(){ | |
| if(isRunning)return; | |
| isRunning=true; | |
| stopFlag=false; | |
| ensureTimerBtn(); | |
| addStopHotkey(); | |
| try{ | |
| while(!stopFlag){ | |
| let did=await automateAutoReceiptOnce(); | |
| if(stopFlag)break; | |
| if(!did && !stopFlag){ | |
| setTimer(5); | |
| await wait(5000); | |
| } | |
| } | |
| }finally{ | |
| isRunning=false; | |
| removeStopHotkey(); | |
| if(timerId)clearInterval(timerId); | |
| if(timerBtn&&document.body.contains(timerBtn))timerBtn.remove(); | |
| } | |
| } | |
| mainLoop(); | |
| })(); | |
| `; | |
| // --- Main Automation & Supervisor Logic --- | |
| async function runAndSuperviseAutomation() { | |
| broadcast('STATUS', 'Supervisor: Starting automation process...'); | |
| console.log('Starting browser...'); | |
| let browser; | |
| try { | |
| if (!LOGIN_EMAIL || !LOGIN_PASSWORD) { | |
| throw new Error("LOGIN_EMAIL or LOGIN_PASSWORD secret is not set in Hugging Face Space settings."); | |
| } | |
| browser = await puppeteer.launch({ | |
| headless: true, | |
| args: ['--no-sandbox', '--disable-setuid-sandbox'] | |
| }); | |
| const page = await browser.newPage(); | |
| await page.exposeFunction('logToNode', (event, data) => { | |
| console.log(`LOG FROM BROWSER: ${event}`, data || ''); | |
| broadcast(event, data); | |
| }); | |
| console.log('Navigating to login page...'); | |
| broadcast('STATUS', 'Navigating to login page...'); | |
| await page.goto(LOGIN_URL, { waitUntil: 'networkidle2' }); | |
| console.log('Entering credentials...'); | |
| broadcast('STATUS', 'Entering credentials...'); | |
| await page.type('#inputUsername', LOGIN_EMAIL); | |
| await page.type('#inputPswd', LOGIN_PASSWORD); | |
| console.log('Clicking login button...'); | |
| broadcast('STATUS', 'Attempting to log in...'); | |
| await page.click('#loginButton'); | |
| await page.waitForNavigation({ waitUntil: 'networkidle2' }); | |
| console.log('Login successful. Current URL:', page.url()); | |
| console.log('Login successful. Injecting automation script.'); | |
| broadcast('STATUS', 'Login successful. Starting automation script...'); | |
| await page.evaluate(bookmarkletCode); | |
| console.log('Automation script running. Monitoring...'); | |
| broadcast('STATUS', 'Automation is now running in the background'); | |
| while (true) { | |
| await new Promise(resolve => setTimeout(resolve, 30000)); | |
| try { | |
| await page.evaluate(() => document.title); | |
| } catch (error) { | |
| console.log('Page became unresponsive, restarting...'); | |
| break; | |
| } | |
| } | |
| } catch (error) { | |
| console.error('An error occurred in the automation process:', error); | |
| broadcast('ERROR', `A critical error occurred: ${error.message}. Supervisor will restart in 15 seconds.`); | |
| } finally { | |
| if (browser) { | |
| await browser.close(); | |
| } | |
| console.log('Restarting automation in 15 seconds...'); | |
| setTimeout(schedule, 15 * 1000); | |
| } | |
| } | |
| function schedule() { | |
| const now = new Date(); | |
| const timeZone = 'America/New_York'; | |
| const currentHour = parseInt(now.toLocaleString('en-US', { hour: '2-digit', hour12: false, timeZone }), 10); | |
| if (currentHour >= 8 && currentHour < 18) { | |
| console.log(`Starting automation at ${now.toLocaleString('en-US', { timeZone })}`); | |
| runAndSuperviseAutomation(); | |
| } else { | |
| const message = `Outside of scheduled hours (8 AM - 6 PM ET). Current time: ${now.toLocaleString('en-US', { timeZone })}. Waiting for the next valid time slot.`; | |
| console.log(message); | |
| broadcast('STATUS', message); | |
| setTimeout(schedule, 30 * 60 * 1000); | |
| } | |
| } | |
| // WebSocket connection handling | |
| wss.on('connection', (ws) => { | |
| console.log('New WebSocket connection established'); | |
| ws.send(JSON.stringify({ event: 'STATUS', data: 'Connected to automation server' })); | |
| ws.on('close', () => { | |
| console.log('WebSocket connection closed'); | |
| }); | |
| ws.on('error', (error) => { | |
| console.error('WebSocket error:', error); | |
| }); | |
| }); | |
| const PORT = process.env.PORT || 7860; | |
| server.listen(PORT, '0.0.0.0', () => { | |
| console.log(`Server is running on port ${PORT}`); | |
| console.log('UI is available at the root path'); | |
| console.log('Automation will start if within business hours (8 AM - 6 PM ET)'); | |
| schedule(); | |
| }); | |