Deploy to production #31
			
				
			
		
		
		
	| 
						 | 
				
			
			@ -2,9 +2,8 @@
 | 
			
		|||
import { LoadingOverlay, Modal, ModalProps, Table } from '@mantine/core';
 | 
			
		||||
import { useCallback, useEffect, useMemo, useState } from 'react';
 | 
			
		||||
import { getDetailBidHistories } from '../../apis/bid-histories';
 | 
			
		||||
import { extractNumber } from '../../lib/table/ultils';
 | 
			
		||||
import { IBid } from '../../system/type';
 | 
			
		||||
import { formatTime } from '../../utils';
 | 
			
		||||
import { extractNumber, formatTime } from '../../utils';
 | 
			
		||||
 | 
			
		||||
export interface IShowHistoriesBidGraysApiModalProps extends ModalProps {
 | 
			
		||||
    data: IBid | null;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,9 +6,8 @@ import { Socket } from "socket.io-client";
 | 
			
		|||
import { getImagesWorking } from "../../apis/bid";
 | 
			
		||||
import { useStatusToolStore } from "../../lib/zustand/use-status-tool-store";
 | 
			
		||||
import { IBid, IWebBid } from "../../system/type";
 | 
			
		||||
import { cn, extractDomainSmart, stringToColor } from "../../utils";
 | 
			
		||||
import { cn, extractDomainSmart, findNearestClosingChild, isTimeReached, stringToColor, subtractSeconds } from "../../utils";
 | 
			
		||||
import ShowImageModal from "./show-image-modal";
 | 
			
		||||
import { isTimeReached, subtractSeconds } from "../../lib/table/ultils";
 | 
			
		||||
export interface IWorkingPageProps {
 | 
			
		||||
  data: (IBid | IWebBid) & { type: string };
 | 
			
		||||
  socket: Socket;
 | 
			
		||||
| 
						 | 
				
			
			@ -111,6 +110,11 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
 | 
			
		|||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  if(!isIBid(data)){
 | 
			
		||||
    console.log(data)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Box
 | 
			
		||||
| 
						 | 
				
			
			@ -140,10 +144,17 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
 | 
			
		|||
            <Text className="text-xs tracking-wide">{`Current price: $${data.current_price}`}</Text>
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          
 | 
			
		||||
 | 
			
		||||
          <Text className="text-sm italic opacity-80">
 | 
			
		||||
            {moment(lastUpdate).format("HH:mm:ss DD/MM/YYYY")}
 | 
			
		||||
          </Text>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
          {!isIBid(data) && <Tooltip label={'Time to tracking'}><Text>{`TT: ${moment(subtractSeconds(findNearestClosingChild(data)?.close_time || '', data.early_tracking_seconds)).format(
 | 
			
		||||
                  "HH:mm:ss DD/MM/YYYY"
 | 
			
		||||
                )}`}</Text></Tooltip>}
 | 
			
		||||
 | 
			
		||||
          <Box className="flex items-center gap-3">
 | 
			
		||||
            {isIBid(data) && (
 | 
			
		||||
              <Tooltip label={'Close time'}>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,7 +14,7 @@ import { z } from "zod";
 | 
			
		|||
import { createWebBid, updateWebBid } from "../../apis/web-bid";
 | 
			
		||||
import { useConfirmStore } from "../../lib/zustand/use-confirm";
 | 
			
		||||
import { IWebBid } from "../../system/type";
 | 
			
		||||
import { extractDomain } from "../../utils";
 | 
			
		||||
import { extractDomain, formatTimeFromMinutes } from "../../utils";
 | 
			
		||||
export interface IWebBidModelProps extends ModalProps {
 | 
			
		||||
  data: IWebBid | null;
 | 
			
		||||
  onUpdated?: () => void;
 | 
			
		||||
| 
						 | 
				
			
			@ -158,8 +158,8 @@ export default function WebBidModal({
 | 
			
		|||
          className="col-span-2"
 | 
			
		||||
          size="sm"
 | 
			
		||||
          label={`Arrival offset seconds (${
 | 
			
		||||
            form.getValues()["arrival_offset_seconds"] / 60
 | 
			
		||||
          } minutes)`}
 | 
			
		||||
             formatTimeFromMinutes(form.getValues()["arrival_offset_seconds"] / 60)
 | 
			
		||||
          })`}
 | 
			
		||||
          placeholder="msg: 300"
 | 
			
		||||
          {...form.getInputProps("arrival_offset_seconds")}
 | 
			
		||||
        />
 | 
			
		||||
| 
						 | 
				
			
			@ -168,8 +168,8 @@ export default function WebBidModal({
 | 
			
		|||
          className="col-span-2"
 | 
			
		||||
          size="sm"
 | 
			
		||||
          label={`Early tracking seconds (${
 | 
			
		||||
            form.getValues()["early_tracking_seconds"] / 60
 | 
			
		||||
          } minutes)`}
 | 
			
		||||
             formatTimeFromMinutes(form.getValues()["early_tracking_seconds"] / 60)
 | 
			
		||||
          })`}
 | 
			
		||||
          placeholder="msg: 600"
 | 
			
		||||
          {...form.getInputProps("early_tracking_seconds")}
 | 
			
		||||
        />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -117,28 +117,4 @@ export const removeFalsy = (data: { [key: string]: string | number }) => {
 | 
			
		|||
  }, {} as { [key: string]: string | number });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function extractNumber(str: string) {
 | 
			
		||||
  const match = str.match(/\d+(\.\d+)?/);
 | 
			
		||||
  return match ? parseFloat(match[0]) : null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function subtractMinutes(time: string, minutes: number) {
 | 
			
		||||
  const date = new Date(time);
 | 
			
		||||
  date.setMinutes(date.getMinutes() - minutes);
 | 
			
		||||
  return date.toUTCString();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function subtractSeconds(time: string, seconds: number) {
 | 
			
		||||
  const date = new Date(time);
 | 
			
		||||
  date.setSeconds(date.getSeconds() - seconds);
 | 
			
		||||
  return date.toUTCString();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isTimeReached(targetTime: string) {
 | 
			
		||||
  if (!targetTime) return false;
 | 
			
		||||
 | 
			
		||||
  const targetDate = new Date(targetTime);
 | 
			
		||||
  const now = new Date();
 | 
			
		||||
 | 
			
		||||
  return now >= targetDate;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import { ActionIcon, Badge, Box, Menu, Text, Tooltip } from "@mantine/core";
 | 
			
		||||
import { ActionIcon, Anchor, Badge, Box, Menu, Text, Tooltip } from "@mantine/core";
 | 
			
		||||
import { useDisclosure } from "@mantine/hooks";
 | 
			
		||||
import {
 | 
			
		||||
  IconAd,
 | 
			
		||||
| 
						 | 
				
			
			@ -7,7 +7,7 @@ import {
 | 
			
		|||
  IconHammer,
 | 
			
		||||
  IconHistory,
 | 
			
		||||
  IconMenu,
 | 
			
		||||
  IconTrash,
 | 
			
		||||
  IconTrash
 | 
			
		||||
} from "@tabler/icons-react";
 | 
			
		||||
import _ from "lodash";
 | 
			
		||||
import { useMemo, useRef, useState } from "react";
 | 
			
		||||
| 
						 | 
				
			
			@ -51,6 +51,11 @@ export default function Bids() {
 | 
			
		|||
      key: "name",
 | 
			
		||||
      title: "Name",
 | 
			
		||||
      typeFilter: "text",
 | 
			
		||||
      renderRow(row) {
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        return <Anchor className="text-[14px]"  href={row.url} size="sm" style={{color: 'inherit'}}>{row.name}</Anchor>
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      key: "web_bid",
 | 
			
		||||
| 
						 | 
				
			
			@ -187,9 +192,7 @@ export default function Bids() {
 | 
			
		|||
  const table = useMemo(() => {
 | 
			
		||||
    return (
 | 
			
		||||
      <Table
 | 
			
		||||
        onClickRow={(row) => {
 | 
			
		||||
          window.open(row.url, "_blank");
 | 
			
		||||
        }}
 | 
			
		||||
        
 | 
			
		||||
        tableChildProps={{
 | 
			
		||||
          trbody: {
 | 
			
		||||
            className: "cursor-pointer",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,6 +44,7 @@ export interface IWebBid extends ITimestamp {
 | 
			
		|||
    active: boolean;
 | 
			
		||||
    arrival_offset_seconds: number;
 | 
			
		||||
    early_tracking_seconds: number;
 | 
			
		||||
    snapshot_at: string | null
 | 
			
		||||
    children: IBid[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,8 @@ import { clsx, type ClassValue } from "clsx";
 | 
			
		|||
import { twMerge } from "tailwind-merge";
 | 
			
		||||
import moment from "moment";
 | 
			
		||||
import { IWebBid } from "../system/type";
 | 
			
		||||
import _ from 'lodash'
 | 
			
		||||
 | 
			
		||||
export function cn(...args: ClassValue[]) {
 | 
			
		||||
  return twMerge(clsx(args));
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -201,3 +203,63 @@ export function extractDomainSmart(url: string) {
 | 
			
		|||
    return url;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export function findNearestClosingChild(webBid: IWebBid) {
 | 
			
		||||
  const now = Date.now();
 | 
			
		||||
 | 
			
		||||
  const validChildren = webBid.children.filter(
 | 
			
		||||
    (child) => child.close_time && !isNaN(new Date(child.close_time).getTime())
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (validChildren.length === 0) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const nearestChild = _.minBy(validChildren, (child) => {
 | 
			
		||||
    return Math.abs(new Date(child.close_time!).getTime() - now);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return nearestChild || null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function extractNumber(str: string) {
 | 
			
		||||
  const match = str.match(/\d+(\.\d+)?/);
 | 
			
		||||
  return match ? parseFloat(match[0]) : null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function subtractMinutes(time: string, minutes: number) {
 | 
			
		||||
  const date = new Date(time);
 | 
			
		||||
  date.setMinutes(date.getMinutes() - minutes);
 | 
			
		||||
  return date.toUTCString();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function subtractSeconds(time: string, seconds: number) {
 | 
			
		||||
  const date = new Date(time);
 | 
			
		||||
  date.setSeconds(date.getSeconds() - seconds);
 | 
			
		||||
  return date.toUTCString();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isTimeReached(targetTime: string) {
 | 
			
		||||
  if (!targetTime) return false;
 | 
			
		||||
 | 
			
		||||
  const targetDate = new Date(targetTime);
 | 
			
		||||
  const now = new Date();
 | 
			
		||||
 | 
			
		||||
  return now >= targetDate;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatTimeFromMinutes(minutes: number): string {
 | 
			
		||||
  // Tính ngày, giờ, phút từ số phút
 | 
			
		||||
  const days = Math.floor(minutes / (60 * 24));
 | 
			
		||||
  const hours = Math.floor((minutes % (60 * 24)) / 60);
 | 
			
		||||
  const mins = minutes % 60;
 | 
			
		||||
 | 
			
		||||
  let result = '';
 | 
			
		||||
 | 
			
		||||
  if (days > 0) result += `${days} ${days > 1? 'days' :'day'} `;
 | 
			
		||||
  if (hours > 0) result += `${hours} ${hours > 1 ? 'hours' : 'hour'} `;
 | 
			
		||||
  if (mins > 0 || result === '') result += `${mins} minutes`;
 | 
			
		||||
 | 
			
		||||
  return result.trim();
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@
 | 
			
		|||
/node_modules
 | 
			
		||||
/build
 | 
			
		||||
/public
 | 
			
		||||
/bot-data
 | 
			
		||||
 | 
			
		||||
# Logs
 | 
			
		||||
logs
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1 +1 @@
 | 
			
		|||
{"createdAt":1746603511532}
 | 
			
		||||
{"createdAt":1747011314493}
 | 
			
		||||
| 
						 | 
				
			
			@ -23,6 +23,9 @@ export class WebBid extends Timestamp {
 | 
			
		|||
  @Column({ default: 600 })
 | 
			
		||||
  early_tracking_seconds: number;
 | 
			
		||||
 | 
			
		||||
  @Column({ default: null })
 | 
			
		||||
  snapshot_at: Date | null;
 | 
			
		||||
 | 
			
		||||
  @Column({ default: null, nullable: true })
 | 
			
		||||
  @Exclude()
 | 
			
		||||
  password: string;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -228,7 +228,10 @@ export class BidsService {
 | 
			
		|||
    if (!bid.close_time && !bid.start_bid_time) {
 | 
			
		||||
      // Thiết lập thời gian bắt đầu là 5 phút trước khi đóng
 | 
			
		||||
      // bid.start_bid_time = new Date().toUTCString();
 | 
			
		||||
      bid.start_bid_time = subtractMinutes(close_time, bid.web_bid.arrival_offset_seconds/ 60);
 | 
			
		||||
      bid.start_bid_time = subtractMinutes(
 | 
			
		||||
        close_time,
 | 
			
		||||
        bid.web_bid.arrival_offset_seconds / 60,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Kiểm tra nếu thời gian đóng bid đã đạt tới (tức phiên đấu giá đã kết thúc)
 | 
			
		||||
| 
						 | 
				
			
			@ -422,6 +425,11 @@ export class BidsService {
 | 
			
		|||
        }),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    // update time snapshot for API BID
 | 
			
		||||
    if (type === 'API_BID') {
 | 
			
		||||
      this.webBidsService.webBidRepo.update(id, { snapshot_at: new Date() });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.eventEmitter.emit(`working`, {
 | 
			
		||||
      status: 're-update',
 | 
			
		||||
      id,
 | 
			
		||||
| 
						 | 
				
			
			@ -510,11 +518,9 @@ export class BidsService {
 | 
			
		|||
    return AppResponse.toResponse(files);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async emitLoginStatus(data: ClientUpdateLoginStatusDto){
 | 
			
		||||
  async emitLoginStatus(data: ClientUpdateLoginStatusDto) {
 | 
			
		||||
    this.eventEmitter.emit(Event.statusLogin(data.data), data);
 | 
			
		||||
 | 
			
		||||
    this.eventEmitter.emit(Event.statusLogin(data.data), data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    return AppResponse.toResponse(true)
 | 
			
		||||
    return AppResponse.toResponse(true);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,7 +13,7 @@ import browser from "./system/browser.js";
 | 
			
		|||
import configs from "./system/config.js";
 | 
			
		||||
import {
 | 
			
		||||
  delay,
 | 
			
		||||
  isPageAvailable,
 | 
			
		||||
  findNearestClosingChild,
 | 
			
		||||
  isTimeReached,
 | 
			
		||||
  safeClosePage,
 | 
			
		||||
  subtractSeconds,
 | 
			
		||||
| 
						 | 
				
			
			@ -270,20 +270,20 @@ const clearLazyTab = async () => {
 | 
			
		|||
    // product tabs
 | 
			
		||||
    const productTabs = _.flatMap(MANAGER_BIDS, "children");
 | 
			
		||||
 | 
			
		||||
    for (const item of [...productTabs, ...MANAGER_BIDS]) {
 | 
			
		||||
      if (!item.page_context) continue;
 | 
			
		||||
    // for (const item of [...productTabs, ...MANAGER_BIDS]) {
 | 
			
		||||
    //   if (!item.page_context) continue;
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        const avalableResult = await isPageAvailable(item.page_context);
 | 
			
		||||
    //   try {
 | 
			
		||||
    //     const avalableResult = await isPageAvailable(item.page_context);
 | 
			
		||||
 | 
			
		||||
        if (!avalableResult) {
 | 
			
		||||
          await safeClosePage(item);
 | 
			
		||||
        }
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        console.warn("⚠️ Error checking page_context.title()", e.message);
 | 
			
		||||
        await safeClosePage(item);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    //     if (!avalableResult) {
 | 
			
		||||
    //       await safeClosePage(item);
 | 
			
		||||
    //     }
 | 
			
		||||
    //   } catch (e) {
 | 
			
		||||
    //     console.warn("⚠️ Error checking page_context.title()", e.message);
 | 
			
		||||
    //     await safeClosePage(item);
 | 
			
		||||
    //   }
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    for (const page of pages) {
 | 
			
		||||
      try {
 | 
			
		||||
| 
						 | 
				
			
			@ -303,13 +303,6 @@ const clearLazyTab = async () => {
 | 
			
		|||
            productTab?.web_bid?.early_tracking_seconds || 0
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          console.log("%cindex.js:291 {object}", "color: #007acc;", {
 | 
			
		||||
            earlyTrackingTime,
 | 
			
		||||
            it_time: isTimeReached(earlyTrackingTime),
 | 
			
		||||
            close_time: productTab.close_time,
 | 
			
		||||
            tracking_time: productTab?.web_bid?.early_tracking_seconds,
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          if (!isTimeReached(earlyTrackingTime)) {
 | 
			
		||||
            await safeClosePage(productTab);
 | 
			
		||||
            console.log(`🛑 Unused page detected: ${pageUrl}`);
 | 
			
		||||
| 
						 | 
				
			
			@ -354,6 +347,15 @@ const clearLazyTab = async () => {
 | 
			
		|||
        console.warn(`⚠️ Error handling page: ${pageErr.message}`);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Delete lazy tracking page
 | 
			
		||||
    Promise.allSettled(
 | 
			
		||||
      MANAGER_BIDS.map(async (item) => {
 | 
			
		||||
        if (await item.isLazy()) {
 | 
			
		||||
          safeClosePage(item);
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.error("❌ Error in clearLazyTab:", err.message);
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,10 +5,12 @@ import browser from "../system/browser.js";
 | 
			
		|||
import CONSTANTS from "../system/constants.js";
 | 
			
		||||
import {
 | 
			
		||||
  findEarlyLoginTime,
 | 
			
		||||
  findNearestClosingChild,
 | 
			
		||||
  getPathLocalData,
 | 
			
		||||
  getPathProfile,
 | 
			
		||||
  isTimeReached,
 | 
			
		||||
  sanitizeFileName,
 | 
			
		||||
  subtractSeconds,
 | 
			
		||||
} from "../system/utils.js";
 | 
			
		||||
import { Bid } from "./bid.js";
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -24,6 +26,8 @@ export class ApiBid extends Bid {
 | 
			
		|||
  // browser_context;
 | 
			
		||||
  username;
 | 
			
		||||
  password;
 | 
			
		||||
  early_tracking_seconds;
 | 
			
		||||
  snapshot_at;
 | 
			
		||||
 | 
			
		||||
  constructor({
 | 
			
		||||
    url,
 | 
			
		||||
| 
						 | 
				
			
			@ -35,6 +39,8 @@ export class ApiBid extends Bid {
 | 
			
		|||
    updated_at,
 | 
			
		||||
    origin_url,
 | 
			
		||||
    active,
 | 
			
		||||
    early_tracking_seconds,
 | 
			
		||||
    snapshot_at,
 | 
			
		||||
  }) {
 | 
			
		||||
    super(BID_TYPE.API_BID, url);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -45,6 +51,8 @@ export class ApiBid extends Bid {
 | 
			
		|||
    this.active = active;
 | 
			
		||||
    this.username = username;
 | 
			
		||||
    this.password = password;
 | 
			
		||||
    this.early_tracking_seconds = early_tracking_seconds;
 | 
			
		||||
    this.snapshot_at = snapshot_at;
 | 
			
		||||
    this.id = id;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -58,6 +66,8 @@ export class ApiBid extends Bid {
 | 
			
		|||
    updated_at,
 | 
			
		||||
    origin_url,
 | 
			
		||||
    active,
 | 
			
		||||
    early_tracking_seconds,
 | 
			
		||||
    snapshot_at,
 | 
			
		||||
  }) {
 | 
			
		||||
    this.created_at = created_at;
 | 
			
		||||
    this.updated_at = updated_at;
 | 
			
		||||
| 
						 | 
				
			
			@ -67,6 +77,8 @@ export class ApiBid extends Bid {
 | 
			
		|||
    this.username = username;
 | 
			
		||||
    this.password = password;
 | 
			
		||||
    this.url = url;
 | 
			
		||||
    this.early_tracking_seconds = early_tracking_seconds;
 | 
			
		||||
    this.snapshot_at = snapshot_at;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  puppeteer_connect = async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -79,12 +91,81 @@ export class ApiBid extends Bid {
 | 
			
		|||
    await this.restoreContext();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  async handlePrevListen() {
 | 
			
		||||
    console.log(`👂 [${this.id}] Start handlePrevListen...`);
 | 
			
		||||
 | 
			
		||||
    // Chỉ bắt đầu check khi ảnh đã được chụp
 | 
			
		||||
    if (this.snapshot_at) {
 | 
			
		||||
      const nearestCloseTime = findNearestClosingChild(this);
 | 
			
		||||
 | 
			
		||||
      if (!nearestCloseTime) {
 | 
			
		||||
        console.log(`❌ [${this.id}] No nearest closing child found.`);
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const { close_time } = nearestCloseTime;
 | 
			
		||||
      console.log(`📅 [${this.id}] Nearest close_time: ${close_time}`);
 | 
			
		||||
 | 
			
		||||
      const timeToTracking = subtractSeconds(
 | 
			
		||||
        close_time,
 | 
			
		||||
        this.early_tracking_seconds || 0
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      console.log(
 | 
			
		||||
        `🕰️ [${this.id}] Time to tracking: ${new Date(
 | 
			
		||||
          timeToTracking
 | 
			
		||||
        ).toISOString()}`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (!isTimeReached(timeToTracking)) {
 | 
			
		||||
        console.log(`⏳ [${this.id}] Not time to track yet.`);
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.log(`🔌 [${this.id}] Connecting to puppeteer...`);
 | 
			
		||||
    await this.puppeteer_connect();
 | 
			
		||||
 | 
			
		||||
    console.log(`✅ [${this.id}] Connected. Executing actions...`);
 | 
			
		||||
    await this.action();
 | 
			
		||||
 | 
			
		||||
    console.log(`🎯 [${this.id}] handlePrevListen completed.`);
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async isLazy() {
 | 
			
		||||
    // Nếu chưa có ảnh chụp working => tab not lazy
 | 
			
		||||
    if (!this.snapshot_at) return false;
 | 
			
		||||
 | 
			
		||||
    const nearestCloseTime = findNearestClosingChild(this);
 | 
			
		||||
 | 
			
		||||
    // Nếu không có nearest close => tab lazy
 | 
			
		||||
    if (!nearestCloseTime) return true;
 | 
			
		||||
 | 
			
		||||
    const { close_time } = nearestCloseTime;
 | 
			
		||||
 | 
			
		||||
    const timeToTracking = subtractSeconds(
 | 
			
		||||
      close_time,
 | 
			
		||||
      this.early_tracking_seconds || 0
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Nếu chưa đến giờ tracking => tab lazy
 | 
			
		||||
    if (!isTimeReached(timeToTracking)) return true;
 | 
			
		||||
 | 
			
		||||
    // Các trường hợp còn lại => not lazy
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  listen_events = async () => {
 | 
			
		||||
    if (this.page_context) return;
 | 
			
		||||
 | 
			
		||||
    await this.puppeteer_connect();
 | 
			
		||||
    // await this.puppeteer_connect();
 | 
			
		||||
 | 
			
		||||
    await this.action();
 | 
			
		||||
    // await this.action();
 | 
			
		||||
 | 
			
		||||
    const results = await this.handlePrevListen();
 | 
			
		||||
 | 
			
		||||
    if (!results) return;
 | 
			
		||||
 | 
			
		||||
    await this.saveContext();
 | 
			
		||||
  };
 | 
			
		||||
| 
						 | 
				
			
			@ -107,16 +188,16 @@ export class ApiBid extends Bid {
 | 
			
		|||
 | 
			
		||||
      if (!fs.existsSync(dirPath)) {
 | 
			
		||||
        fs.mkdirSync(dirPath, { recursive: true });
 | 
			
		||||
        console.log(`📂 Save at folder: ${dirPath}`);
 | 
			
		||||
        console.log(`📂 [${this.id}] Save at folder: ${dirPath}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      fs.writeFileSync(
 | 
			
		||||
        path.join(dirPath, sanitizeFileName(this.origin_url) + ".json"),
 | 
			
		||||
        JSON.stringify(contextData, null, 2)
 | 
			
		||||
      );
 | 
			
		||||
      console.log("✅ Context saved!");
 | 
			
		||||
      console.log(`✅ [${this.id}] Context saved!`);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log("Save Context: ", error.message);
 | 
			
		||||
      console.log(`[${this.id}] Save Context: , ${error.message}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -132,7 +213,7 @@ export class ApiBid extends Bid {
 | 
			
		|||
    // Restore Cookies
 | 
			
		||||
    await this.page_context.setCookie(...contextData.cookies);
 | 
			
		||||
 | 
			
		||||
    console.log("🔄 Context restored!");
 | 
			
		||||
    console.log(`🔄 [${this.id}] Context restored!`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async onCloseLogin() {}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -113,7 +113,7 @@ export class LangtonsApiBid extends ApiBid {
 | 
			
		|||
            return { success: false, error: error.toString() };
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        { usernam: this.username, password: this.password },
 | 
			
		||||
        { username: this.username, password: this.password },
 | 
			
		||||
        csrfToken
 | 
			
		||||
      ); // truyền biến vào page context
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -158,34 +158,14 @@ export class LangtonsApiBid extends ApiBid {
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async submitPrevCode() {
 | 
			
		||||
  async submitCode({ name, code }) {
 | 
			
		||||
    try {
 | 
			
		||||
      const prevCodeData = await this.loadCodeFromLocal();
 | 
			
		||||
 | 
			
		||||
      if (!prevCodeData) return false;
 | 
			
		||||
 | 
			
		||||
      await this.page_context.goto(
 | 
			
		||||
        "https://www.langtons.com.au/account/mfalogin?rurl=5"
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      await this.page_context.waitForNavigation({
 | 
			
		||||
        timeout: 8000,
 | 
			
		||||
        waitUntil: "domcontentloaded",
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (!(await page.type("#code", code, { delay: 120 }))) {
 | 
			
		||||
        await this.clearCodeFromLocal();
 | 
			
		||||
 | 
			
		||||
        await this.page_context.goto(configs.WEB_URLS.LANGTONS.LOGIN_URL);
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const csrfToken = await this.getCsrfToken();
 | 
			
		||||
 | 
			
		||||
      if (!csrfToken) return false;
 | 
			
		||||
 | 
			
		||||
      const responsePrevCode = await this.callSubmitPrevCodeApi(
 | 
			
		||||
        prevCodeData.code,
 | 
			
		||||
        code,
 | 
			
		||||
        csrfToken
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -199,9 +179,6 @@ export class LangtonsApiBid extends ApiBid {
 | 
			
		|||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // submit tiếp api login....
 | 
			
		||||
      await this.page_context.goto(this.url);
 | 
			
		||||
 | 
			
		||||
      return true;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error(`❌ [${this.id}] Error submitPrevCode:`, error);
 | 
			
		||||
| 
						 | 
				
			
			@ -310,24 +287,31 @@ export class LangtonsApiBid extends ApiBid {
 | 
			
		|||
      // save code to local
 | 
			
		||||
      // await this.saveCodeToLocal({ name, code });
 | 
			
		||||
 | 
			
		||||
      // ⌨ Enter verification code
 | 
			
		||||
      console.log(`✍ [${this.id}] Entering verification code...`);
 | 
			
		||||
      await page.type("#code", code, { delay: 120 });
 | 
			
		||||
      // // ⌨ Enter verification code
 | 
			
		||||
      // console.log(`✍ [${this.id}] Entering verification code...`);
 | 
			
		||||
      // await page.type("#code", code, { delay: 120 });
 | 
			
		||||
 | 
			
		||||
      // 🚀 Click the verification confirmation button
 | 
			
		||||
      console.log(
 | 
			
		||||
        `🔘 [${this.id}] Clicking the verification confirmation button`
 | 
			
		||||
      );
 | 
			
		||||
      await page.click(".btn.btn-block.btn-primary", { delay: 90 });
 | 
			
		||||
      // // 🚀 Click the verification confirmation button
 | 
			
		||||
      // console.log(
 | 
			
		||||
      //   `🔘 [${this.id}] Clicking the verification confirmation button`
 | 
			
		||||
      // );
 | 
			
		||||
      // await page.click(".btn.btn-block.btn-primary", { delay: 90 });
 | 
			
		||||
 | 
			
		||||
      // ⏳ Wait for navigation after verification
 | 
			
		||||
      console.log(
 | 
			
		||||
        `⏳ [${this.id}] Waiting for navigation after verification...`
 | 
			
		||||
      );
 | 
			
		||||
      await page.waitForNavigation({
 | 
			
		||||
        timeout: 15000,
 | 
			
		||||
        waitUntil: "domcontentloaded",
 | 
			
		||||
      });
 | 
			
		||||
      const reuslt = await this.submitCode({ name, code });
 | 
			
		||||
 | 
			
		||||
      if (!reuslt) {
 | 
			
		||||
        console.log(`[${this.id}] Wrote verifi code failure`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // // ⏳ Wait for navigation after verification
 | 
			
		||||
      // console.log(
 | 
			
		||||
      //   `⏳ [${this.id}] Waiting for navigation after verification...`
 | 
			
		||||
      // );
 | 
			
		||||
      // await page.waitForNavigation({
 | 
			
		||||
      //   timeout: 15000,
 | 
			
		||||
      //   waitUntil: "domcontentloaded",
 | 
			
		||||
      // });
 | 
			
		||||
 | 
			
		||||
      await page.goto(this.url, { waitUntil: "networkidle2" });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -441,8 +425,11 @@ export class LangtonsApiBid extends ApiBid {
 | 
			
		|||
  listen_events = async () => {
 | 
			
		||||
    if (this.page_context) return;
 | 
			
		||||
 | 
			
		||||
    await this.puppeteer_connect();
 | 
			
		||||
    await this.action();
 | 
			
		||||
    // await this.puppeteer_connect();
 | 
			
		||||
    // await this.action();
 | 
			
		||||
    const results = await this.handlePrevListen();
 | 
			
		||||
 | 
			
		||||
    if (!results) return;
 | 
			
		||||
 | 
			
		||||
    this.reloadInterval = setInterval(async () => {
 | 
			
		||||
      try {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import fs from 'fs';
 | 
			
		||||
import configs from '../../system/config.js';
 | 
			
		||||
import { delay, getPathProfile, safeClosePage } from '../../system/utils.js';
 | 
			
		||||
import { ApiBid } from '../api-bid.js';
 | 
			
		||||
import fs from "fs";
 | 
			
		||||
import configs from "../../system/config.js";
 | 
			
		||||
import { delay, getPathProfile, safeClosePage } from "../../system/utils.js";
 | 
			
		||||
import { ApiBid } from "../api-bid.js";
 | 
			
		||||
 | 
			
		||||
export class LawsonsApiBid extends ApiBid {
 | 
			
		||||
  reloadInterval = null;
 | 
			
		||||
| 
						 | 
				
			
			@ -14,7 +14,11 @@ export class LawsonsApiBid extends ApiBid {
 | 
			
		|||
      // Tạo timeout để reject sau 1 phút nếu không có phản hồi
 | 
			
		||||
      const timeout = setTimeout(() => {
 | 
			
		||||
        global.socket.off(`verify-code.${this.origin_url}`); // Xóa listener tránh rò rỉ bộ nhớ
 | 
			
		||||
                rej(new Error(`[${this.id}] Timeout: No verification code received within 2 minute.`));
 | 
			
		||||
        rej(
 | 
			
		||||
          new Error(
 | 
			
		||||
            `[${this.id}] Timeout: No verification code received within 2 minute.`
 | 
			
		||||
          )
 | 
			
		||||
        );
 | 
			
		||||
      }, 120 * 1000); // 60 giây
 | 
			
		||||
 | 
			
		||||
      global.socket.on(`verify-code.${this.origin_url}`, async (data) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -30,7 +34,9 @@ export class LawsonsApiBid extends ApiBid {
 | 
			
		|||
 | 
			
		||||
    const filePath = getPathProfile(this.origin_url);
 | 
			
		||||
 | 
			
		||||
        return !(await this.page_context.$('#emailLogin')) && fs.existsSync(filePath);
 | 
			
		||||
    return (
 | 
			
		||||
      !(await this.page_context.$("#emailLogin")) && fs.existsSync(filePath)
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  waitVerifyData = async () =>
 | 
			
		||||
| 
						 | 
				
			
			@ -38,7 +44,11 @@ export class LawsonsApiBid extends ApiBid {
 | 
			
		|||
      // Tạo timeout để reject sau 1 phút nếu không có phản hồi
 | 
			
		||||
      const timeout = setTimeout(() => {
 | 
			
		||||
        global.socket.off(`verify-code.${this.origin_url}`); // Xóa listener tránh rò rỉ bộ nhớ
 | 
			
		||||
                rej(new Error(`[${this.id}] Timeout: No verification code received within 1 minute.`));
 | 
			
		||||
        rej(
 | 
			
		||||
          new Error(
 | 
			
		||||
            `[${this.id}] Timeout: No verification code received within 1 minute.`
 | 
			
		||||
          )
 | 
			
		||||
        );
 | 
			
		||||
      }, 60 * 1000); // 60 giây
 | 
			
		||||
 | 
			
		||||
      global.socket.on(`verify-code.${this.origin_url}`, async (data) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -52,7 +62,7 @@ export class LawsonsApiBid extends ApiBid {
 | 
			
		|||
  async enterOTP(otp) {
 | 
			
		||||
    try {
 | 
			
		||||
      // Selector cho tất cả các input OTP
 | 
			
		||||
            const inputSelector = '.MuiDialog-container .container input';
 | 
			
		||||
      const inputSelector = ".MuiDialog-container .container input";
 | 
			
		||||
 | 
			
		||||
      // Chờ cho các input OTP xuất hiện
 | 
			
		||||
      await this.page_context.waitForSelector(inputSelector, { timeout: 8000 });
 | 
			
		||||
| 
						 | 
				
			
			@ -68,10 +78,10 @@ export class LawsonsApiBid extends ApiBid {
 | 
			
		|||
        }
 | 
			
		||||
        console.log(`✅ OTP entered successfully: ${otp}`);
 | 
			
		||||
      } else {
 | 
			
		||||
                console.error('❌ Invalid OTP or input fields count');
 | 
			
		||||
        console.error("❌ Invalid OTP or input fields count");
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
            console.error('❌ Error entering OTP:', error);
 | 
			
		||||
      console.error("❌ Error entering OTP:", error);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -80,13 +90,15 @@ export class LawsonsApiBid extends ApiBid {
 | 
			
		|||
      if (!this.page_context) return false;
 | 
			
		||||
 | 
			
		||||
      // Selector của các phần tử trên trang
 | 
			
		||||
            const button = '.form-input-wrapper.form-group > .btn.btn-primary'; // Nút để tiếp tục quá trình xác minh
 | 
			
		||||
            const remember = '.PrivateSwitchBase-input'; // Checkbox "Remember me"
 | 
			
		||||
      const button = ".form-input-wrapper.form-group > .btn.btn-primary"; // Nút để tiếp tục quá trình xác minh
 | 
			
		||||
      const remember = ".PrivateSwitchBase-input"; // Checkbox "Remember me"
 | 
			
		||||
      const continueButton =
 | 
			
		||||
                '.MuiButtonBase-root.MuiButton-root.MuiButton-contained.MuiButton-containedPrimary.MuiButton-sizeMedium.MuiButton-containedSizeMedium.MuiButton-colorPrimary.MuiButton-root'; // Nút "Continue"
 | 
			
		||||
        ".MuiButtonBase-root.MuiButton-root.MuiButton-contained.MuiButton-containedPrimary.MuiButton-sizeMedium.MuiButton-containedSizeMedium.MuiButton-colorPrimary.MuiButton-root"; // Nút "Continue"
 | 
			
		||||
 | 
			
		||||
      // Chờ cho nút xác minh xuất hiện
 | 
			
		||||
            console.log(`🔎 [${this.id}] Waiting for the button with selector: ${button}`);
 | 
			
		||||
      console.log(
 | 
			
		||||
        `🔎 [${this.id}] Waiting for the button with selector: ${button}`
 | 
			
		||||
      );
 | 
			
		||||
      await this.page_context.waitForSelector(button, { timeout: 8000 });
 | 
			
		||||
 | 
			
		||||
      console.log(`✅ [${this.id}] Button found, clicking the first button.`);
 | 
			
		||||
| 
						 | 
				
			
			@ -104,7 +116,9 @@ export class LawsonsApiBid extends ApiBid {
 | 
			
		|||
 | 
			
		||||
      // Nhận mã OTP để nhập vào form
 | 
			
		||||
      const { name, code } = await this.waitVerifyData();
 | 
			
		||||
            console.log(`🔎 [${this.id}] Waiting for OTP input, received code: ${code}`);
 | 
			
		||||
      console.log(
 | 
			
		||||
        `🔎 [${this.id}] Waiting for OTP input, received code: ${code}`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // Nhập mã OTP vào form
 | 
			
		||||
      await this.enterOTP(code);
 | 
			
		||||
| 
						 | 
				
			
			@ -112,22 +126,30 @@ export class LawsonsApiBid extends ApiBid {
 | 
			
		|||
 | 
			
		||||
      // Chờ cho checkbox "Remember me" xuất hiện
 | 
			
		||||
      await this.page_context.waitForSelector(remember, { timeout: 8000 });
 | 
			
		||||
            console.log(`🔎 [${this.id}] Waiting for remember me checkbox with selector: ${remember}`);
 | 
			
		||||
      console.log(
 | 
			
		||||
        `🔎 [${this.id}] Waiting for remember me checkbox with selector: ${remember}`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // Click vào checkbox "Remember me"
 | 
			
		||||
      await this.page_context.click(remember, { delay: 92 });
 | 
			
		||||
      console.log(`✅ [${this.id}] Remember me checkbox clicked.`);
 | 
			
		||||
 | 
			
		||||
      // Chờ cho nút "Continue" xuất hiện
 | 
			
		||||
            await this.page_context.waitForSelector(continueButton, { timeout: 8000 });
 | 
			
		||||
            console.log(`🔎 [${this.id}] Waiting for continue button with selector: ${continueButton}`);
 | 
			
		||||
      await this.page_context.waitForSelector(continueButton, {
 | 
			
		||||
        timeout: 8000,
 | 
			
		||||
      });
 | 
			
		||||
      console.log(
 | 
			
		||||
        `🔎 [${this.id}] Waiting for continue button with selector: ${continueButton}`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // Click vào nút "Continue"
 | 
			
		||||
      await this.page_context.click(continueButton, { delay: 100 });
 | 
			
		||||
      console.log(`✅ [${this.id}] Continue button clicked.`);
 | 
			
		||||
 | 
			
		||||
      // Chờ cho trang tải hoàn tất sau khi click "Continue"
 | 
			
		||||
            await this.page_context.waitForNavigation({ waitUntil: 'domcontentloaded' });
 | 
			
		||||
      await this.page_context.waitForNavigation({
 | 
			
		||||
        waitUntil: "domcontentloaded",
 | 
			
		||||
      });
 | 
			
		||||
      console.log(`✅ [${this.id}] Navigation completed.`);
 | 
			
		||||
 | 
			
		||||
      return true;
 | 
			
		||||
| 
						 | 
				
			
			@ -144,10 +166,10 @@ export class LawsonsApiBid extends ApiBid {
 | 
			
		|||
 | 
			
		||||
    const filePath = getPathProfile(this.origin_url);
 | 
			
		||||
 | 
			
		||||
        await page.waitForNavigation({ waitUntil: 'domcontentloaded' });
 | 
			
		||||
    await page.waitForNavigation({ waitUntil: "domcontentloaded" });
 | 
			
		||||
 | 
			
		||||
    // 🛠 Check if already logged in (login input should not be visible or profile exists)
 | 
			
		||||
        if (!(await page.$('#emailLogin')) && fs.existsSync(filePath)) {
 | 
			
		||||
    if (!(await page.$("#emailLogin")) && fs.existsSync(filePath)) {
 | 
			
		||||
      console.log(`✅ [${this.id}] Already logged in, skipping login process.`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -158,18 +180,24 @@ export class LawsonsApiBid extends ApiBid {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    const children = this.children.filter((item) => item.page_context);
 | 
			
		||||
        console.log(`🔍 [${this.id}] Found ${children.length} child pages to close.`);
 | 
			
		||||
    console.log(
 | 
			
		||||
      `🔍 [${this.id}] Found ${children.length} child pages to close.`
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (children.length > 0) {
 | 
			
		||||
      console.log(`🛑 [${this.id}] Closing child pages...`);
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        children.map((item) => {
 | 
			
		||||
                    console.log(`➡ [${this.id}] Closing child page with context: ${item.page_context}`);
 | 
			
		||||
          console.log(
 | 
			
		||||
            `➡ [${this.id}] Closing child page with context: ${item.page_context}`
 | 
			
		||||
          );
 | 
			
		||||
          return safeClosePage(item);
 | 
			
		||||
                }),
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
            console.log(`➡ [${this.id}] Closing main page context: ${this.page_context}`);
 | 
			
		||||
      console.log(
 | 
			
		||||
        `➡ [${this.id}] Closing main page context: ${this.page_context}`
 | 
			
		||||
      );
 | 
			
		||||
      await safeClosePage(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -178,22 +206,25 @@ export class LawsonsApiBid extends ApiBid {
 | 
			
		|||
    try {
 | 
			
		||||
      // ⌨ Enter email
 | 
			
		||||
      console.log(`✍ [${this.id}] Entering email:`, this.username);
 | 
			
		||||
            await page.type('#emailLogin', this.username, { delay: 100 });
 | 
			
		||||
      await page.type("#emailLogin", this.username, { delay: 100 });
 | 
			
		||||
 | 
			
		||||
      // ⌨ Enter password
 | 
			
		||||
      console.log(`✍ [${this.id}] Entering password...`);
 | 
			
		||||
            await page.type('#passwordLogin', this.password, { delay: 150 });
 | 
			
		||||
      await page.type("#passwordLogin", this.password, { delay: 150 });
 | 
			
		||||
 | 
			
		||||
      // 🚀 Click the login button
 | 
			
		||||
      console.log(`🔘 [${this.id}] Clicking the "Login" button`);
 | 
			
		||||
            await page.click('#signInBtn', { delay: 92 });
 | 
			
		||||
      await page.click("#signInBtn", { delay: 92 });
 | 
			
		||||
 | 
			
		||||
      const result = await this.waitToTwoVerify();
 | 
			
		||||
 | 
			
		||||
      // ⏳ Wait for navigation after login
 | 
			
		||||
      if (!result) {
 | 
			
		||||
        console.log(`⏳ [${this.id}] Waiting for navigation after login...`);
 | 
			
		||||
                await page.waitForNavigation({ timeout: 8000, waitUntil: 'domcontentloaded' });
 | 
			
		||||
        await page.waitForNavigation({
 | 
			
		||||
          timeout: 8000,
 | 
			
		||||
          waitUntil: "domcontentloaded",
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (this.page_context.url() == this.url) {
 | 
			
		||||
| 
						 | 
				
			
			@ -204,7 +235,10 @@ export class LawsonsApiBid extends ApiBid {
 | 
			
		|||
        console.log(`❌ [${this.id}] Login Failure!`);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
            console.error(`❌ [${this.id}] Error during login process:`, error.message);
 | 
			
		||||
      console.error(
 | 
			
		||||
        `❌ [${this.id}] Error during login process:`,
 | 
			
		||||
        error.message
 | 
			
		||||
      );
 | 
			
		||||
    } finally {
 | 
			
		||||
      global.IS_CLEANING = true;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -214,7 +248,7 @@ export class LawsonsApiBid extends ApiBid {
 | 
			
		|||
    try {
 | 
			
		||||
      const page = this.page_context;
 | 
			
		||||
 | 
			
		||||
            page.on('response', async (response) => {
 | 
			
		||||
      page.on("response", async (response) => {
 | 
			
		||||
        const request = response.request();
 | 
			
		||||
        if (request.redirectChain().length > 0) {
 | 
			
		||||
          if (response.url().includes(configs.WEB_CONFIGS.LAWSONS.LOGIN_URL)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -223,31 +257,39 @@ export class LawsonsApiBid extends ApiBid {
 | 
			
		|||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
            await page.goto(this.url, { waitUntil: 'networkidle2' });
 | 
			
		||||
      await page.goto(this.url, { waitUntil: "networkidle2" });
 | 
			
		||||
 | 
			
		||||
      await page.bringToFront();
 | 
			
		||||
 | 
			
		||||
      // Set userAgent
 | 
			
		||||
            await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
 | 
			
		||||
      await page.setUserAgent(
 | 
			
		||||
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
 | 
			
		||||
      );
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
            console.log('Error [action]: ', error.message);
 | 
			
		||||
      console.log("Error [action]: ", error.message);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  listen_events = async () => {
 | 
			
		||||
    if (this.page_context) return;
 | 
			
		||||
 | 
			
		||||
        await this.puppeteer_connect();
 | 
			
		||||
        await this.action();
 | 
			
		||||
    // await this.puppeteer_connect();
 | 
			
		||||
    // await this.action();
 | 
			
		||||
 | 
			
		||||
    const results = await this.handlePrevListen();
 | 
			
		||||
 | 
			
		||||
    if (!results) return;
 | 
			
		||||
 | 
			
		||||
    this.reloadInterval = setInterval(async () => {
 | 
			
		||||
      try {
 | 
			
		||||
        if (this.page_context && !this.page_context.isClosed()) {
 | 
			
		||||
          console.log(`🔄 [${this.id}] Reloading page...`);
 | 
			
		||||
                    await this.page_context.reload({ waitUntil: 'networkidle2' });
 | 
			
		||||
          await this.page_context.reload({ waitUntil: "networkidle2" });
 | 
			
		||||
          console.log(`✅ [${this.id}] Page reloaded successfully.`);
 | 
			
		||||
        } else {
 | 
			
		||||
                    console.log(`❌ [${this.id}] Page context is closed. Stopping reload.`);
 | 
			
		||||
          console.log(
 | 
			
		||||
            `❌ [${this.id}] Page context is closed. Stopping reload.`
 | 
			
		||||
          );
 | 
			
		||||
          clearInterval(this.reloadInterval);
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -128,8 +128,12 @@ export class PicklesApiBid extends ApiBid {
 | 
			
		|||
  listen_events = async () => {
 | 
			
		||||
    if (this.page_context) return;
 | 
			
		||||
 | 
			
		||||
    await this.puppeteer_connect();
 | 
			
		||||
    await this.action();
 | 
			
		||||
    // await this.puppeteer_connect();
 | 
			
		||||
    // await this.action();
 | 
			
		||||
 | 
			
		||||
    const results = await this.handlePrevListen();
 | 
			
		||||
 | 
			
		||||
    if (!results) return;
 | 
			
		||||
 | 
			
		||||
    this.reloadInterval = setInterval(async () => {
 | 
			
		||||
      try {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@ import CONSTANTS from "./constants.js";
 | 
			
		|||
import fs from "fs";
 | 
			
		||||
import path from "path";
 | 
			
		||||
import { updateStatusWork } from "./apis/bid.js";
 | 
			
		||||
import _ from "lodash";
 | 
			
		||||
 | 
			
		||||
export const isNumber = (value) => !isNaN(value) && !isNaN(parseFloat(value));
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -303,3 +304,21 @@ export async function isPageAvailable(page) {
 | 
			
		|||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function findNearestClosingChild(webBid) {
 | 
			
		||||
  const now = Date.now();
 | 
			
		||||
 | 
			
		||||
  const validChildren = webBid.children.filter(
 | 
			
		||||
    (child) => child.close_time && !isNaN(new Date(child.close_time).getTime())
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (validChildren.length === 0) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const nearestChild = _.minBy(validChildren, (child) => {
 | 
			
		||||
    return Math.abs(new Date(child.close_time).getTime() - now);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return nearestChild || null;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue