updox-bot / index.js
sonuprasad23's picture
changes
48a476c
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();
});