pickxel and fix login

This commit is contained in:
Admin 2025-05-05 10:05:12 +07:00
parent b13712a317
commit 65ae7da6e6
18 changed files with 886 additions and 431 deletions

View File

@ -1,27 +1,29 @@
import { generateNestParams, handleError, handleSuccess } from '.';
import axios from '../lib/axios';
import { IWebBid } from '../system/type';
import { removeFalsyValues } from '../utils';
import { generateNestParams, handleError, handleSuccess } from ".";
import axios from "../lib/axios";
import { IWebBid } from "../system/type";
import { removeFalsyValues } from "../utils";
const BASE_URL = 'web-bids';
const BASE_URL = "web-bids";
export const getWebBids = async (params: Record<string, string | number>) => {
return await axios({
url: BASE_URL,
params: generateNestParams(params),
withCredentials: true,
method: 'GET',
method: "GET",
});
};
export const createWebBid = async (bid: Omit<IWebBid, 'id' | 'created_at' | 'updated_at' | 'is_system_account'>) => {
export const createWebBid = async (
bid: Omit<IWebBid, "id" | "created_at" | "updated_at" | "is_system_account">
) => {
const newData = removeFalsyValues(bid);
try {
const { data } = await axios({
url: BASE_URL,
withCredentials: true,
method: 'POST',
method: "POST",
data: newData,
});
@ -34,14 +36,30 @@ export const createWebBid = async (bid: Omit<IWebBid, 'id' | 'created_at' | 'upd
};
export const updateWebBid = async (bid: Partial<IWebBid>) => {
const { url, password, username, origin_url, active } = removeFalsyValues(bid, ['active']);
const {
url,
password,
username,
origin_url,
active,
arrival_offset_seconds,
// early_login_seconds
} = removeFalsyValues(bid, ["active"]);
try {
const { data } = await axios({
url: `${BASE_URL}/` + bid.id,
withCredentials: true,
method: 'PUT',
data: { url, password, username, origin_url, active },
method: "PUT",
data: {
url,
password,
username,
origin_url,
active,
arrival_offset_seconds,
// early_login_seconds
},
});
handleSuccess(data);
@ -57,7 +75,7 @@ export const deleteWebBid = async (web: IWebBid) => {
const { data } = await axios({
url: `${BASE_URL}/` + web.id,
withCredentials: true,
method: 'DELETE',
method: "DELETE",
});
handleSuccess(data);
@ -77,7 +95,7 @@ export const deletesWebBid = async (web: IWebBid[]) => {
const { data } = await axios({
url: `${BASE_URL}/deletes`,
withCredentials: true,
method: 'POST',
method: "POST",
data: {
ids,
},

View File

@ -20,7 +20,11 @@ export default function ShowHistoriesBidPicklesApiModal({ data, onUpdated, ...pr
<Table.Tr key={index}>
<Table.Td>{element['bidderAnonName']}</Table.Td>
<Table.Td>{element['actualBid']}</Table.Td>
<<<<<<< HEAD
<Table.Td>{formatTime(new Date(element['bidTimeInMilliSeconds']).toUTCString())}</Table.Td>
=======
<Table.Td>{formatTime(new Date(element['bidTimeInMilliSeconds']).toUTCString(), 'HH:mm:ss DD/MM/YYYY')}</Table.Td>
>>>>>>> 26b10a7 (pickxel and fix login)
</Table.Tr>
));
}, [histories]);

View File

@ -13,7 +13,7 @@ export default function ShowHistoriesModal({ data, onUpdated, ...props }: IShowH
<Table.Tr key={element.id}>
<Table.Td>{element.id}</Table.Td>
<Table.Td>{element.price}</Table.Td>
<Table.Td>{formatTime(element.created_at, 'DD/MM/YYYY HH:MM')}</Table.Td>
<Table.Td>{formatTime(new Date(element.created_at).toUTCString(), 'HH:mm:ss DD/MM/YYYY')}</Table.Td>
</Table.Tr>
));

View File

@ -1,23 +1,44 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Button, LoadingOverlay, Modal, ModalProps, TextInput } from '@mantine/core';
import { useForm, zodResolver } from '@mantine/form';
import _ from 'lodash';
import { useEffect, useRef, useState } from 'react';
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 {
Button,
LoadingOverlay,
Modal,
ModalProps,
NumberInput,
TextInput,
} from "@mantine/core";
import { useForm, zodResolver } from "@mantine/form";
import _ from "lodash";
import { useEffect, useRef, useState } from "react";
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";
export interface IWebBidModelProps extends ModalProps {
data: IWebBid | null;
onUpdated?: () => void;
}
const schema = {
url: z.string({ message: 'Url is required' }).url('Invalid url format'),
url: z.string({ message: "Url is required" }).url("Invalid url format"),
arrival_offset_seconds: z
.number({ message: "Arrival offset seconds is required" })
.refine((val) => val >= 60, {
message: "Arrival offset seconds must be at least 60 seconds (1 minute)",
}),
early_login_seconds: z
.number({ message: "Early login seconds is required" })
.refine((val) => val >= 600, {
message: "Early login seconds must be at least 600 seconds (10 minute)",
}),
};
export default function WebBidModal({ data, onUpdated, ...props }: IWebBidModelProps) {
export default function WebBidModal({
data,
onUpdated,
...props
}: IWebBidModelProps) {
const form = useForm({
validate: zodResolver(z.object(schema)),
});
@ -31,10 +52,15 @@ export default function WebBidModal({ data, onUpdated, ...props }: IWebBidModelP
const handleSubmit = async (values: typeof form.values) => {
if (data) {
setConfirm({
title: 'Update ?',
title: "Update ?",
message: `This web will be update`,
handleOk: async () => {
setLoading(true);
console.log(
"%csrc/components/web-bid/web-bid-modal.tsx:54 values",
"color: #007acc;",
values
);
const result = await updateWebBid(values);
setLoading(false);
@ -47,15 +73,20 @@ export default function WebBidModal({ data, onUpdated, ...props }: IWebBidModelP
}
},
okButton: {
color: 'blue',
value: 'Update',
color: "blue",
value: "Update",
},
});
} else {
const { url, origin_url } = values;
const { url, origin_url, arrival_offset_seconds, early_login_seconds } = values;
setLoading(true);
const result = await createWebBid({ url, origin_url } as IWebBid);
const result = await createWebBid({
url,
origin_url,
arrival_offset_seconds,
early_login_seconds
} as IWebBid);
setLoading(false);
if (!result) return;
@ -87,7 +118,7 @@ export default function WebBidModal({ data, onUpdated, ...props }: IWebBidModelP
useEffect(() => {
if (form.values?.url) {
form.setFieldValue('origin_url', extractDomain(form.values.url));
form.setFieldValue("origin_url", extractDomain(form.values.url));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [form.values]);
@ -96,23 +127,70 @@ export default function WebBidModal({ data, onUpdated, ...props }: IWebBidModelP
<Modal
className="relative"
classNames={{
header: '!flex !item-center !justify-center w-full',
header: "!flex !item-center !justify-center w-full",
}}
{...props}
size={'xl'}
size={"xl"}
title={<span className="text-xl font-bold">Web</span>}
centered
>
<form onSubmit={form.onSubmit(handleSubmit)} className="grid grid-cols-2 gap-2.5">
<TextInput withAsterisk className="col-span-2" size="sm" label="Domain" {...form.getInputProps('origin_url')} />
<TextInput withAsterisk className="col-span-2" size="sm" label="Tracking url" {...form.getInputProps('url')} />
<form
onSubmit={form.onSubmit(handleSubmit)}
className="grid grid-cols-2 gap-2.5"
>
<TextInput
withAsterisk
className="col-span-2"
size="sm"
label="Domain"
{...form.getInputProps("origin_url")}
/>
<TextInput
withAsterisk
className="col-span-2"
size="sm"
label="Tracking url"
{...form.getInputProps("url")}
/>
<Button disabled={_.isEqual(form.getValues(), prevData.current)} className="col-span-2" type="submit" fullWidth size="sm" mt="md">
{data ? 'Update' : 'Create'}
<NumberInput
description="Note: that only integer minutes are accepted."
className="col-span-2"
size="sm"
label={`Arrival offset seconds (${
form.getValues()["arrival_offset_seconds"] / 60
} minutes)`}
placeholder="msg: 300"
{...form.getInputProps("arrival_offset_seconds")}
/>
{/* <NumberInput
description="Note: that only integer minutes are accepted."
className="col-span-2"
size="sm"
label={`Early login seconds (${
form.getValues()["early_login_seconds"] / 60
} minutes)`}
placeholder="msg: 600"
{...form.getInputProps("early_login_seconds")}
/> */}
<Button
disabled={_.isEqual(form.getValues(), prevData.current)}
className="col-span-2"
type="submit"
fullWidth
size="sm"
mt="md"
>
{data ? "Update" : "Create"}
</Button>
</form>
<LoadingOverlay visible={loading} zIndex={1000} overlayProps={{ blur: 2 }} />
<LoadingOverlay
visible={loading}
zIndex={1000}
overlayProps={{ blur: 2 }}
/>
</Modal>
);
}

View File

@ -18,24 +18,7 @@ export interface ITimestamp {
updated_at: string;
}
export interface IBid extends ITimestamp {
id: number;
max_price: number;
reserve_price: number;
current_price: number;
name: string | null;
quantity: number;
url: string;
model: string;
lot_id: string;
plus_price: number;
close_time: string | null;
start_bid_time: string | null;
first_bid: boolean;
status: 'biding' | 'out-bid' | 'win-bid';
histories: IHistory[];
web_bid: IWebBid;
}
export interface IHistory extends ITimestamp {
id: number;
@ -59,9 +42,30 @@ export interface IWebBid extends ITimestamp {
username: string | null;
password: string | null;
active: boolean;
arrival_offset_seconds: number;
early_login_seconds: number;
children: IBid[];
}
export interface IBid extends ITimestamp {
id: number;
max_price: number;
reserve_price: number;
current_price: number;
name: string | null;
quantity: number;
url: string;
model: string;
lot_id: string;
plus_price: number;
close_time: string | null;
start_bid_time: string | null;
first_bid: boolean;
status: 'biding' | 'out-bid' | 'win-bid';
histories: IHistory[];
web_bid: IWebBid;
}
export interface IPermission extends ITimestamp {
id: number;
name: string;

View File

@ -3,6 +3,10 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
import moment from "moment";
<<<<<<< HEAD
=======
import { IWebBid } from "../system/type";
>>>>>>> 26b10a7 (pickxel and fix login)
export function cn(...args: ClassValue[]) {
return twMerge(clsx(args));
}
@ -150,4 +154,31 @@ export function stringToColor(str: string): string {
const hash = hashStringToInt(str);
const index = hash % colorPalette.length;
return colorPalette[index];
<<<<<<< HEAD
=======
}
export function findEarlyLoginTime(webBid: IWebBid): string | null {
const now = new Date();
// Bước 1: Lọc ra những bid có close_time hợp lệ
const validChildren = webBid.children.filter(child => child.close_time);
if (validChildren.length === 0) return null;
// Bước 2: Tìm bid có close_time gần hiện tại nhất
const closestBid = validChildren.reduce((closest, current) => {
const closestDiff = Math.abs(new Date(closest.close_time!).getTime() - now.getTime());
const currentDiff = Math.abs(new Date(current.close_time!).getTime() - now.getTime());
return currentDiff < closestDiff ? current : closest;
});
if (!closestBid.close_time) return null;
// Bước 3: Tính toán thời gian login sớm
const closeTime = new Date(closestBid.close_time);
closeTime.setSeconds(closeTime.getSeconds() - (webBid.early_login_seconds || 0));
return closeTime.toISOString();
>>>>>>> 26b10a7 (pickxel and fix login)
}

View File

@ -1 +1,5 @@
<<<<<<< HEAD
{"createdAt":1745827424853}
=======
{"createdAt":1746413672600}
>>>>>>> 26b10a7 (pickxel and fix login)

View File

@ -21,6 +21,10 @@ export class GraysApi {
switch(bid.web_bid.origin_url){
<<<<<<< HEAD
=======
// GRAYS
>>>>>>> 26b10a7 (pickxel and fix login)
case 'https://www.grays.com': {
const response = await axios({
url: `https://www.grays.com/api/LotInfo/GetBiddingHistory?lotId=${lot_id}&currencyCode=AUD`,
@ -32,6 +36,11 @@ export class GraysApi {
return AppResponse.toResponse([])
}
<<<<<<< HEAD
=======
// PICKLES
>>>>>>> 26b10a7 (pickxel and fix login)
case 'https://www.pickles.com.au': {
const response = await axios({
@ -39,7 +48,11 @@ export class GraysApi {
});
if (response.data) {
<<<<<<< HEAD
return AppResponse.toResponse(response.data);
=======
return AppResponse.toResponse(response.data.Bids);
>>>>>>> 26b10a7 (pickxel and fix login)
}
return AppResponse.toResponse([])

View File

@ -1,4 +1,4 @@
import { IsBoolean, IsOptional, IsString, IsUrl } from 'class-validator';
import { IsBoolean, IsNumber, IsOptional, IsString, IsUrl, Min } from 'class-validator';
export class UpdateWebBidDto {
@IsUrl()
@ -9,6 +9,16 @@ export class UpdateWebBidDto {
@IsOptional()
url: string;
@IsNumber()
@Min(60)
@IsOptional()
arrival_offset_seconds: number;
@IsNumber()
@Min(600)
@IsOptional()
early_login_seconds: number;
@IsString()
@IsOptional()
username: string;

View File

@ -17,6 +17,12 @@ export class WebBid extends Timestamp {
@Column({ default: null, nullable: true })
username: string;
@Column({ default: 300 })
arrival_offset_seconds: number;
@Column({ default: 600 })
early_login_seconds: number;
@Column({ default: null, nullable: true })
@Exclude()
password: string;

View File

@ -228,7 +228,7 @@ 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, 5);
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)

View File

@ -80,7 +80,7 @@ export class WebBidsService {
async emitAccountUpdate(id: WebBid['id']) {
const data = await this.webBidRepo.findOne({
where: { id, children: { status: 'biding' } },
relations: { children: true },
relations: { children: { web_bid: true } },
});
this.eventEmitter.emit(Event.WEB_UPDATED, data || null);

View File

@ -10,7 +10,12 @@ import {
} from "./service/app-service.js";
import browser from "./system/browser.js";
import configs from "./system/config.js";
import { delay, isTimeReached, safeClosePage } from "./system/utils.js";
import {
delay,
findEarlyLoginTime,
isTimeReached,
safeClosePage,
} from "./system/utils.js";
import { updateLoginStatus } from "./system/apis/bid.js";
global.IS_CLEANING = true;
@ -53,6 +58,59 @@ const handleUpdateProductTabs = (data) => {
MANAGER_BIDS = newDataManager;
};
const addProductTab = (data) => {
if (
typeof data !== "object" ||
data === null ||
!Array.isArray(data.children)
) {
console.warn("Data must be an object with a children array");
return;
}
const { children, ...web } = data;
if (children.length === 0) {
console.warn(
`⚠️ No children found for bid id ${web.id}, skipping addProductTab`
);
return;
}
const managerBidMap = new Map(MANAGER_BIDS.map((bid) => [bid.id, bid]));
const prevApiBid = managerBidMap.get(web.id);
if (prevApiBid) {
// Cập nhật
const updatedChildren = prevApiBid.children;
children.forEach((newChild) => {
const existingChildIndex = updatedChildren.findIndex(
(c) => c.id === newChild.id
);
if (existingChildIndex !== -1) {
updatedChildren[existingChildIndex].setNewData(newChild);
} else {
updatedChildren.push(createBidProduct(web, newChild));
}
});
prevApiBid.setNewData({ ...web, children: updatedChildren });
} else {
// Tạo mới
const newChildren = children.map((item) => createBidProduct(web, item));
const newApiBid = createApiBid({ ...web, children: newChildren });
MANAGER_BIDS.push(newApiBid);
console.log("%cindex.js:116 {MANAGER_BIDS}", "color: #007acc;", {
MANAGER_BIDS,
children: newChildren,
});
}
};
const tracking = async () => {
console.log("🚀 Tracking process started...");
@ -68,6 +126,20 @@ const tracking = async () => {
})
);
await Promise.allSettled(
MANAGER_BIDS.filter((bid) => !bid.page_context).map((apiBid) => {
console.log(`🎧 Listening to events for close login: ${apiBid.id}`);
return (apiBid.onCloseLogin = (data) => {
// Loại bỏ class hiện có. Tạo tiền đề cho việc tạo đối tượng mới lại
MANAGER_BIDS = MANAGER_BIDS.filter(
(item) => item.id !== data.id && item.type !== data.type
);
addProductTab(data);
});
})
);
Promise.allSettled(
productTabs.map(async (productTab) => {
console.log(`📌 Processing Product ID: ${productTab.id}`);
@ -159,6 +231,72 @@ const tracking = async () => {
}
};
// const clearLazyTab = async () => {
// if (!global.IS_CLEANING) {
// console.log("🚀 Cleaning flag is OFF. Proceeding with operation.");
// return;
// }
// if (!browser) {
// console.warn("⚠️ Browser is not available or disconnected.");
// return;
// }
// try {
// const pages = await browser.pages();
// // Lấy danh sách URL từ flattenedArray
// const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [
// item.url,
// ...item.children.map((child) => child.url),
// ]).filter(Boolean); // Lọc bỏ null hoặc undefined
// console.log(
// "🔍 Page URLs:",
// pages.map((page) => page.url())
// );
// for (const page of pages) {
// const pageUrl = page.url();
// if (!pageUrl || pageUrl === "about:blank") continue;
// if (!activeUrls.includes(pageUrl)) {
// if (!page.isClosed() && browser.isConnected()) {
// try {
// const bidData = MANAGER_BIDS.filter((item) => item.page_context)
// .map((i) => ({
// current_url: i.page_context.url(),
// data: i,
// }))
// .find((j) => j.current_url === pageUrl);
// console.log(bidData);
// if (bidData && bidData.data) {
// await safeClosePage(bidData.data);
// } else {
// // 👇 Wrap close with timeout + error catch
// await Promise.race([
// page.close(),
// new Promise((_, reject) =>
// setTimeout(() => reject(new Error("Close timeout")), 3000)
// ),
// ]);
// }
// console.log(`🛑 Closing unused tab: ${pageUrl}`);
// } catch (err) {
// console.warn(`⚠️ Error closing tab ${pageUrl}: ${err.message}`);
// }
// }
// }
// }
// } catch (err) {
// console.error("❌ Error in clearLazyTab:", err.message);
// }
// };
const clearLazyTab = async () => {
if (!global.IS_CLEANING) {
console.log("🚀 Cleaning flag is OFF. Proceeding with operation.");
@ -172,27 +310,26 @@ const clearLazyTab = async () => {
try {
const pages = await browser.pages();
console.log("🔍 Found pages:", pages.length);
// Lấy danh sách URL từ flattenedArray
const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [
item.url,
...item.children.map((child) => child.url),
]).filter(Boolean); // Lọc bỏ null hoặc undefined
console.log(
"🔍 Page URLs:",
pages.map((page) => page.url())
);
]).filter(Boolean);
for (const page of pages) {
try {
if (page.isClosed()) continue; // Trang đã đóng thì bỏ qua
const pageUrl = page.url();
// 🔥 Bỏ qua tab 'about:blank' hoặc tab không có URL
if (!pageUrl || pageUrl === "about:blank") continue;
if (activeUrls.includes(pageUrl)) continue;
page.removeAllListeners();
console.log(`🛑 Unused page detected: ${pageUrl}`);
if (!activeUrls.includes(pageUrl)) {
if (!page.isClosed() && browser.isConnected()) {
try {
const bidData = MANAGER_BIDS.filter((item) => item.page_context)
.map((i) => ({
current_url: i.page_context.url(),
@ -200,19 +337,26 @@ const clearLazyTab = async () => {
}))
.find((j) => j.current_url === pageUrl);
console.log(bidData);
if (bidData && bidData.data) {
await safeClosePage(bidData.data);
} else {
await page.close();
try {
await Promise.race([
page.close(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Close timeout")), 3000)
),
]);
} catch (closeErr) {
console.warn(
`⚠️ Error closing page ${pageUrl}: ${closeErr.message}`
);
}
}
console.log(`🛑 Closing unused tab: ${pageUrl}`);
} catch (err) {
console.warn(`⚠️ Error closing tab ${pageUrl}:, err.message`);
}
}
console.log(`✅ Closed page: ${pageUrl}`);
} catch (pageErr) {
console.warn(`⚠️ Error handling page: ${pageErr.message}`);
}
}
} catch (err) {
@ -343,6 +487,10 @@ const trackingLoginStatus = async () => {
await safeClosePage(tab);
MANAGER_BIDS = MANAGER_BIDS.filter((item) => item.id != data.id);
addProductTab(data);
global.IS_CLEANING = true;
} else {
console.log("⚠️ No profile found to delete.");

View File

@ -1,10 +1,15 @@
import * as fs from 'fs';
import path from 'path';
import BID_TYPE from '../system/bid-type.js';
import browser from '../system/browser.js';
import CONSTANTS from '../system/constants.js';
import { getPathProfile, sanitizeFileName } from '../system/utils.js';
import { Bid } from './bid.js';
import * as fs from "fs";
import path from "path";
import BID_TYPE from "../system/bid-type.js";
import browser from "../system/browser.js";
import CONSTANTS from "../system/constants.js";
import {
findEarlyLoginTime,
getPathProfile,
isTimeReached,
sanitizeFileName,
} from "../system/utils.js";
import { Bid } from "./bid.js";
export class ApiBid extends Bid {
id;
@ -19,7 +24,17 @@ export class ApiBid extends Bid {
username;
password;
constructor({ url, username, password, id, children, created_at, updated_at, origin_url, active }) {
constructor({
url,
username,
password,
id,
children,
created_at,
updated_at,
origin_url,
active,
}) {
super(BID_TYPE.API_BID, url);
this.created_at = created_at;
@ -32,7 +47,17 @@ export class ApiBid extends Bid {
this.id = id;
}
setNewData({ url, username, password, id, children, created_at, updated_at, origin_url, active }) {
setNewData({
url,
username,
password,
id,
children,
created_at,
updated_at,
origin_url,
active,
}) {
this.created_at = created_at;
this.updated_at = updated_at;
this.children = children;
@ -68,7 +93,9 @@ export class ApiBid extends Bid {
try {
const cookies = await this.browser_context.cookies();
const localStorageData = await this.page_context.evaluate(() => JSON.stringify(localStorage));
const localStorageData = await this.page_context.evaluate(() =>
JSON.stringify(localStorage)
);
const contextData = {
cookies,
@ -82,10 +109,13 @@ export class ApiBid extends Bid {
console.log(`📂 Save at folder: ${dirPath}`);
}
fs.writeFileSync(path.join(dirPath, sanitizeFileName(this.origin_url) + '.json'), JSON.stringify(contextData, null, 2));
console.log('✅ Context saved!');
fs.writeFileSync(
path.join(dirPath, sanitizeFileName(this.origin_url) + ".json"),
JSON.stringify(contextData, null, 2)
);
console.log("✅ Context saved!");
} catch (error) {
console.log('Save Context: ', error.message);
console.log("Save Context: ", error.message);
}
}
@ -96,11 +126,19 @@ export class ApiBid extends Bid {
if (!fs.existsSync(filePath)) return;
const contextData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
const contextData = JSON.parse(fs.readFileSync(filePath, "utf8"));
// Restore Cookies
await this.page_context.setCookie(...contextData.cookies);
console.log('🔄 Context restored!');
console.log("🔄 Context restored!");
}
async onCloseLogin() {}
async isTimeToLogin() {
const earlyLoginTime = findEarlyLoginTime(this);
return earlyLoginTime && isTimeReached(earlyLoginTime);
}
}

View File

@ -85,6 +85,8 @@ export class LangtonsApiBid extends ApiBid {
`➡ [${this.id}] Closing main page context: ${this.page_context}`
);
await safeClosePage(this);
await this.onCloseLogin(this);
}
console.log(`🔑 [${this.id}] Starting login process...`);

View File

@ -79,9 +79,21 @@ export class LangtonsProductBid extends ProductBid {
timeout / 1000
}s`
);
this.page_context.off("response", onResponse); // Gỡ bỏ listener khi timeout
this.page_context?.off("response", onResponse); // Gỡ bỏ listener khi timeout
await this.page_context.reload({ waitUntil: "networkidle0" }); // reload page
try {
if (!this.page_context.isClosed()) {
await this.page_context.reload({ waitUntil: "networkidle0" });
console.log(`🔁 [${this.id}] Reload page in waitForApiResponse`);
} else {
console.log(`⚠️ [${this.id}] Cannot reload, page already closed.`);
}
} catch (error) {
console.error(
`❌ [${this.id}] Error reloading page:`,
error?.message
);
}
console.log(`🔁 [${this.id}] Reload page in waitForApiResponse`);
resolve(null);
@ -161,8 +173,8 @@ export class LangtonsProductBid extends ProductBid {
lot_id: result?.lotId || null,
reserve_price: result.lotData?.minimumBid || null,
current_price: result.lotData?.currentMaxBid || null,
// close_time: close_time && !this.close_time ? String(close_time) : null,
close_time: close_time ? String(close_time) : null,
// close_time: close_time && !this.close_time ? String(close_time) : null,
name,
},
// [],
@ -441,7 +453,11 @@ export class LangtonsProductBid extends ProductBid {
console.log(
`⚠️ [${this.id}] Ignored response for lotId: ${lotData?.lotId}`
);
if (!this.page_context.isClosed()) {
await this.page_context.reload({ waitUntil: "networkidle0" });
}
console.log(`🔁 [${this.id}] Reload page in gotoLink`);
return;
}

View File

@ -248,7 +248,7 @@ export class LawsonsProductBid extends ProductBid {
`===============Start call to submit [${this.id}] ================`
);
await delay(2000);
await delay(200);
// Nếu chưa bid, thực hiện đặt giá
console.log(
@ -368,7 +368,10 @@ export class LawsonsProductBid extends ProductBid {
console.log(`✅ [${this.id}] No bid needed. Conditions not met.`);
}
if (new Date(this.updated_at).getTime() > Date.now() - 120 * 1000) {
if (
new Date(this.updated_at).getTime() > Date.now() - 120 * 1000 &&
!this.page_context.isClosed()
) {
await this.page_context.reload({ waitUntil: "networkidle0" });
}
} catch (error) {

View File

@ -1,19 +1,31 @@
import CONSTANTS from './constants.js';
import fs from 'fs';
import path from 'path';
import { updateStatusWork } from './apis/bid.js';
import CONSTANTS from "./constants.js";
import fs from "fs";
import path from "path";
import { updateStatusWork } from "./apis/bid.js";
export const isNumber = (value) => !isNaN(value) && !isNaN(parseFloat(value));
export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_IMAGE.ERRORS) => {
export const takeSnapshot = async (
page,
item,
imageName,
type = CONSTANTS.TYPE_IMAGE.ERRORS
) => {
if (!page || page.isClosed()) return;
try {
const baseDir = path.join(CONSTANTS.ERROR_IMAGES_PATH, item.type, String(item.id)); // Thư mục theo lot_id
const baseDir = path.join(
CONSTANTS.ERROR_IMAGES_PATH,
item.type,
String(item.id)
); // Thư mục theo lot_id
const typeDir = path.join(baseDir, type); // Thư mục con theo type
// Tạo tên file, nếu type === 'work' thì không có timestamp
const fileName = type === CONSTANTS.TYPE_IMAGE.WORK ? `${imageName}.png` : `${imageName}_${new Date().toISOString().replace(/[:.]/g, '-')}.png`;
const fileName =
type === CONSTANTS.TYPE_IMAGE.WORK
? `${imageName}.png`
: `${imageName}_${new Date().toISOString().replace(/[:.]/g, "-")}.png`;
const filePath = path.join(typeDir, fileName);
@ -25,15 +37,19 @@ export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_
// await page.waitForSelector('body', { visible: true, timeout: 5000 });
// Kiểm tra có thể điều hướng trang không
const isPageResponsive = await page.evaluate(() => document.readyState === 'complete');
const isPageResponsive = await page.evaluate(
() => document.readyState === "complete"
);
if (!isPageResponsive) {
console.log('🚫 Page is unresponsive, skipping snapshot.');
console.log("🚫 Page is unresponsive, skipping snapshot.");
return;
}
// Chờ tối đa 15 giây, nếu không thấy thì bỏ qua
await page.waitForSelector('body', { visible: true, timeout: 15000 }).catch(() => {
console.log('⚠️ Body selector not found, skipping snapshot.');
await page
.waitForSelector("body", { visible: true, timeout: 15000 })
.catch(() => {
console.log("⚠️ Body selector not found, skipping snapshot.");
return;
});
@ -47,18 +63,38 @@ export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_
await updateStatusWork(item, filePath);
}
} catch (error) {
console.log('Error when snapshot: ' + error.message);
console.log("Error when snapshot: " + error.message);
}
};
export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
export const safeClosePageReal = async (page) => {
if (!page) return;
try {
if (page.isClosed()) {
console.log(`✅ Page already closed: ${page.url()}`);
return;
}
page.removeAllListeners(); // ✂️ Remove hết listeners trước khi close
await page.close({ runBeforeUnload: true }); // 🛑 Đóng an toàn
console.log(`✅ Successfully closed page: ${page.url()}`);
} catch (err) {
console.warn(
`⚠️ Error closing page ${page.url ? page.url() : ""}: ${err.message}`
);
}
};
export async function safeClosePage(item) {
try {
const page = item.page_context;
if (!page?.isClosed() && page?.close) {
await page.close();
await safeClosePageReal(page);
// await page.close();
}
item.page_context = undefined;
@ -85,11 +121,14 @@ export function extractNumber(str) {
}
export const sanitizeFileName = (url) => {
return url.replace(/[:\/]/g, '_');
return url.replace(/[:\/]/g, "_");
};
export const getPathProfile = (origin_url) => {
return path.join(CONSTANTS.PROFILE_PATH, sanitizeFileName(origin_url) + '.json');
return path.join(
CONSTANTS.PROFILE_PATH,
sanitizeFileName(origin_url) + ".json"
);
};
export function removeFalsyValues(obj, excludeKeys = []) {
@ -129,7 +168,9 @@ export function convertAETtoUTC(dateString) {
};
// Tách chuỗi đầu vào
const parts = dateString.match(/(\w+)\s(\d+)\s(\w+)\s(\d+),\s(\d+)\s(PM|AM)\sAET/);
const parts = dateString.match(
/(\w+)\s(\d+)\s(\w+)\s(\d+),\s(\d+)\s(PM|AM)\sAET/
);
if (!parts) {
throw new Error("Error format: 'Sun 6 Apr 2025, 9 PM AET'");
}
@ -138,11 +179,20 @@ export function convertAETtoUTC(dateString) {
// Chuyển đổi giờ sang định dạng 24h
let hours = parseInt(hour, 10);
if (period === 'PM' && hours !== 12) hours += 12;
if (period === 'AM' && hours === 12) hours = 0;
if (period === "PM" && hours !== 12) hours += 12;
if (period === "AM" && hours === 12) hours = 0;
// Tạo đối tượng Date ban đầu (chưa điều chỉnh múi giờ)
const date = new Date(Date.UTC(parseInt(year, 10), monthMap[month], parseInt(day, 10), hours, 0, 0));
const date = new Date(
Date.UTC(
parseInt(year, 10),
monthMap[month],
parseInt(day, 10),
hours,
0,
0
)
);
// Hàm kiểm tra DST cho AET
function isDST(date) {
@ -175,6 +225,36 @@ export function convertAETtoUTC(dateString) {
}
export function extractPriceNumber(priceString) {
const cleaned = priceString.replace(/[^\d.]/g, '');
const cleaned = priceString.replace(/[^\d.]/g, "");
return parseFloat(cleaned);
}
export function findEarlyLoginTime(webBid) {
const now = new Date();
// Bước 1: Lọc ra những bid có close_time hợp lệ
const validChildren = webBid.children.filter((child) => child.close_time);
if (validChildren.length === 0) return null;
// Bước 2: Tìm bid có close_time gần hiện tại nhất
const closestBid = validChildren.reduce((closest, current) => {
const closestDiff = Math.abs(
new Date(closest.close_time).getTime() - now.getTime()
);
const currentDiff = Math.abs(
new Date(current.close_time).getTime() - now.getTime()
);
return currentDiff < closestDiff ? current : closest;
});
if (!closestBid.close_time) return null;
// Bước 3: Tính toán thời gian login sớm
const closeTime = new Date(closestBid.close_time);
closeTime.setSeconds(
closeTime.getSeconds() - (webBid.early_login_seconds || 0)
);
return closeTime.toISOString();
}