Merge pull request 'staging' (#6) from staging into production

Reviewed-on: #6
This commit is contained in:
zelda 2025-03-25 19:21:00 +11:00
commit 2a9fb6b506
23 changed files with 268 additions and 185 deletions

2
auto-bid-admin/.env.dev Normal file
View File

@ -0,0 +1,2 @@
VITE_BASE_URL = 'http://localhost:4000/api/v1/admin/'
VITE_SOCKET_URL = 'http://localhost:4000'

View File

@ -1,10 +1,10 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Badge, Box, Button, Modal, ModalProps, PasswordInput, Text, TextInput, Tooltip } from '@mantine/core';
import { Badge, Box, Button, LoadingOverlay, Modal, ModalProps, PasswordInput, Text, TextInput, Tooltip } from '@mantine/core';
import { useForm, zodResolver } from '@mantine/form';
import { useDisclosure } from '@mantine/hooks';
import { IconPlus } from '@tabler/icons-react';
import _ from 'lodash';
import { useEffect, useRef } from 'react';
import { useEffect, useRef, useState } from 'react';
import { z } from 'zod';
import { createAdmin, updateAdmin } from '../../apis/admin';
import { useConfirmStore } from '../../lib/zustand/use-confirm';
@ -40,6 +40,8 @@ export default function AdminModal({ data, onUpdated, ...props }: IAdminModelPro
validate: zodResolver(data ? updateSchema : createSchema),
});
const [loading, setLoading] = useState(false);
const [opened, { open, close }] = useDisclosure(false);
const { deletePermission, setPermissions, permissions, basePermission } = usePermissionStore();
@ -54,7 +56,9 @@ export default function AdminModal({ data, onUpdated, ...props }: IAdminModelPro
title: 'Update ?',
message: `This account will be update`,
handleOk: async () => {
setLoading(true);
const result = await updateAdmin(values);
setLoading(false);
if (!result) return;
@ -72,7 +76,10 @@ export default function AdminModal({ data, onUpdated, ...props }: IAdminModelPro
} else {
const { confirmPassword, ...newValues } = values;
setLoading(true);
const result = await createAdmin(newValues as Omit<IAdmin, 'id' | 'created_at' | 'updated_at' | 'is_system_account'>);
setLoading(false);
if (!result) return;
@ -110,6 +117,7 @@ export default function AdminModal({ data, onUpdated, ...props }: IAdminModelPro
return (
<Modal
className="relative"
classNames={{
header: '!flex !item-center !justify-center w-full',
}}
@ -160,6 +168,8 @@ export default function AdminModal({ data, onUpdated, ...props }: IAdminModelPro
</form>
<PermissionDrawer opened={opened} onClose={close} />
<LoadingOverlay visible={loading} zIndex={1000} overlayProps={{ blur: 2 }} />
</Modal>
);
}

View File

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Button, Modal, ModalProps, PasswordInput } from '@mantine/core';
import { Button, LoadingOverlay, Modal, ModalProps, PasswordInput } from '@mantine/core';
import { useForm, zodResolver } from '@mantine/form';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { z } from 'zod';
import { grantNewPasswordAdmin } from '../../apis/admin';
import { useConfirmStore } from '../../lib/zustand/use-confirm';
@ -26,6 +26,8 @@ export default function GrantNewPasswordModal({ data, onUpdated, ...props }: IAd
validate: zodResolver(schema),
});
const [loading, setLoading] = useState(false);
const { setConfirm } = useConfirmStore();
const handleSubmit = async (values: typeof form.values) => {
@ -34,10 +36,12 @@ export default function GrantNewPasswordModal({ data, onUpdated, ...props }: IAd
title: 'Update ?',
message: `This account will be update`,
handleOk: async () => {
setLoading(true);
const result = await grantNewPasswordAdmin({
id: data.id,
password: values.password,
});
setLoading(false);
if (!result) return;
@ -73,6 +77,7 @@ export default function GrantNewPasswordModal({ data, onUpdated, ...props }: IAd
return (
<Modal
className="relative"
classNames={{
header: '!flex !item-center !justify-center w-full',
}}
@ -89,6 +94,8 @@ export default function GrantNewPasswordModal({ data, onUpdated, ...props }: IAd
{'Grant'}
</Button>
</form>
<LoadingOverlay visible={loading} zIndex={1000} overlayProps={{ blur: 2 }} />
</Modal>
);
}

View File

@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Button, Modal, ModalProps, NumberInput, TextInput } from '@mantine/core';
import { Button, LoadingOverlay, Modal, ModalProps, NumberInput, TextInput } from '@mantine/core';
import { useForm, zodResolver } from '@mantine/form';
import _ from 'lodash';
import { useEffect, useRef } from 'react';
import { useEffect, useRef, useState } from 'react';
import { z } from 'zod';
import { createBid, updateBid } from '../../apis/bid';
import { useConfirmStore } from '../../lib/zustand/use-confirm';
@ -28,13 +28,17 @@ export default function BidModal({ data, onUpdated, ...props }: IBidModelProps)
const { setConfirm } = useConfirmStore();
const [loading, setLoading] = useState(false);
const handleSubmit = async (values: typeof form.values) => {
if (data) {
setConfirm({
title: 'Update ?',
message: `This product will be update`,
handleOk: async () => {
setLoading(true);
const result = await updateBid(values);
setLoading(false);
if (!result) return;
@ -52,8 +56,11 @@ export default function BidModal({ data, onUpdated, ...props }: IBidModelProps)
} else {
const { url, max_price, plus_price } = values;
setLoading(true);
const result = await createBid({ url, max_price, plus_price } as IBid);
setLoading(false);
if (!result) return;
props.onClose();
@ -83,6 +90,7 @@ export default function BidModal({ data, onUpdated, ...props }: IBidModelProps)
return (
<Modal
className="relative"
classNames={{
header: '!flex !item-center !justify-center w-full',
}}
@ -102,6 +110,8 @@ export default function BidModal({ data, onUpdated, ...props }: IBidModelProps)
{data ? 'Update' : 'Create'}
</Button>
</form>
<LoadingOverlay visible={loading} zIndex={1000} overlayProps={{ blur: 2 }} />
</Modal>
);
}

View File

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Modal, ModalProps, Table } from '@mantine/core';
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';
@ -14,6 +14,8 @@ export interface IShowHistoriesBidGraysApiModalProps extends ModalProps {
export default function ShowHistoriesBidGraysApiModal({ data, onUpdated, ...props }: IShowHistoriesBidGraysApiModalProps) {
const [histories, setHistories] = useState<Record<string, string>[]>([]);
const [loading, setLoading] = useState(false);
const rows = useMemo(() => {
return histories.map((element) => (
<Table.Tr key={element.LotId}>
@ -33,7 +35,9 @@ export default function ShowHistoriesBidGraysApiModal({ data, onUpdated, ...prop
return;
}
setLoading(true);
const response = await getDetailBidHistories(data?.lot_id);
setLoading(false);
if (response.data && response.data) {
setHistories(response.data);
@ -45,7 +49,7 @@ export default function ShowHistoriesBidGraysApiModal({ data, onUpdated, ...prop
}, [handleCallApi]);
return (
<Modal {...props} size="xl" title={<span className="text-xl font-bold">BIDDING HISTORY</span>} centered>
<Modal className="relative" {...props} size="xl" title={<span className="text-xl font-bold">BIDDING HISTORY</span>} centered>
<Table striped highlightOnHover withTableBorder withColumnBorders>
<Table.Thead>
<Table.Tr>
@ -68,6 +72,8 @@ export default function ShowHistoriesBidGraysApiModal({ data, onUpdated, ...prop
)}
</Table.Tbody>
</Table>
<LoadingOverlay visible={loading} zIndex={1000} overlayProps={{ blur: 2 }} />
</Modal>
);
}

View File

@ -1,4 +1,4 @@
import { Avatar, Button, Menu, Modal, PasswordInput } from '@mantine/core';
import { Avatar, Button, LoadingOverlay, Menu, Modal, PasswordInput } from '@mantine/core';
import { useForm, zodResolver } from '@mantine/form';
import { useDisclosure } from '@mantine/hooks';
import { IconLogout, IconSettings, IconUser } from '@tabler/icons-react';
@ -7,6 +7,7 @@ import { z } from 'zod';
import { changePassword, logout } from '../apis/auth';
import { useConfirmStore } from '../lib/zustand/use-confirm';
import Links from '../system/links';
import { useState } from 'react';
const schema = z
.object({
@ -24,6 +25,8 @@ export default function UserMenu() {
const { setConfirm } = useConfirmStore();
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const form = useForm({
initialValues: {
@ -59,11 +62,14 @@ export default function UserMenu() {
message: 'This account will change password !',
okButton: { value: 'Sure' },
handleOk: async () => {
setLoading(true);
const data = await changePassword({
newPassword: values.newPassword,
password: values.currentPassword,
});
setLoading(false);
if (data && data.data) {
navigate(Links.LOGIN);
close();
@ -95,7 +101,7 @@ export default function UserMenu() {
</Menu.Dropdown>
</Menu>
<Modal opened={opened} onClose={close} title="Change password" centered>
<Modal className="relative" opened={opened} onClose={close} title="Change password" centered>
<form onSubmit={form.onSubmit(handleSubmit)} className="flex flex-col gap-2.5">
<PasswordInput size="sm" label="Current password" {...form.getInputProps('currentPassword')} />
<PasswordInput size="sm" label="New password" {...form.getInputProps('newPassword')} />
@ -104,6 +110,8 @@ export default function UserMenu() {
Change
</Button>
</form>
<LoadingOverlay visible={loading} zIndex={1000} overlayProps={{ blur: 2 }} />
</Modal>
</>
);

View File

@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Button, Modal, ModalProps, PasswordInput, TextInput } from '@mantine/core';
import { Button, LoadingOverlay, Modal, ModalProps, PasswordInput, TextInput } from '@mantine/core';
import { useForm, zodResolver } from '@mantine/form';
import _ from 'lodash';
import { useEffect, useRef } from 'react';
import { useEffect, useRef, useState } from 'react';
import { z } from 'zod';
import { updateWebBid } from '../../apis/web-bid';
import { useConfirmStore } from '../../lib/zustand/use-confirm';
@ -22,6 +22,8 @@ export default function WebAccountModal({ data, onUpdated, ...props }: IWebBidMo
validate: zodResolver(schema),
});
const [loading, setLoading] = useState(false);
const prevData = useRef<IWebBid | null>(data);
const { setConfirm } = useConfirmStore();
@ -32,7 +34,9 @@ export default function WebAccountModal({ data, onUpdated, ...props }: IWebBidMo
title: 'Update ?',
message: `This account will be update`,
handleOk: async () => {
setLoading(true);
const result = await updateWebBid(values);
setLoading(false);
if (!result) return;
@ -48,7 +52,9 @@ export default function WebAccountModal({ data, onUpdated, ...props }: IWebBidMo
},
});
} else {
setLoading(true);
const result = await updateWebBid(values);
setLoading(false);
if (!result) return;
@ -79,6 +85,7 @@ export default function WebAccountModal({ data, onUpdated, ...props }: IWebBidMo
return (
<Modal
className="relative"
classNames={{
header: '!flex !item-center !justify-center w-full',
}}
@ -95,6 +102,8 @@ export default function WebAccountModal({ data, onUpdated, ...props }: IWebBidMo
{data ? 'Update' : 'Create'}
</Button>
</form>
<LoadingOverlay visible={loading} zIndex={1000} overlayProps={{ blur: 2 }} />
</Modal>
);
}

View File

@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Button, Modal, ModalProps, TextInput } from '@mantine/core';
import { Button, LoadingOverlay, Modal, ModalProps, TextInput } from '@mantine/core';
import { useForm, zodResolver } from '@mantine/form';
import _ from 'lodash';
import { useEffect, useRef } from 'react';
import { useEffect, useRef, useState } from 'react';
import { z } from 'zod';
import { createWebBid, updateWebBid } from '../../apis/web-bid';
import { useConfirmStore } from '../../lib/zustand/use-confirm';
@ -22,6 +22,8 @@ export default function WebBidModal({ data, onUpdated, ...props }: IWebBidModelP
validate: zodResolver(z.object(schema)),
});
const [loading, setLoading] = useState(false);
const prevData = useRef<IWebBid | null>(data);
const { setConfirm } = useConfirmStore();
@ -32,7 +34,9 @@ export default function WebBidModal({ data, onUpdated, ...props }: IWebBidModelP
title: 'Update ?',
message: `This web will be update`,
handleOk: async () => {
setLoading(true);
const result = await updateWebBid(values);
setLoading(false);
if (!result) return;
@ -50,7 +54,9 @@ export default function WebBidModal({ data, onUpdated, ...props }: IWebBidModelP
} else {
const { url, origin_url } = values;
setLoading(true);
const result = await createWebBid({ url, origin_url } as IWebBid);
setLoading(false);
if (!result) return;
@ -88,6 +94,7 @@ export default function WebBidModal({ data, onUpdated, ...props }: IWebBidModelP
return (
<Modal
className="relative"
classNames={{
header: '!flex !item-center !justify-center w-full',
}}
@ -104,6 +111,8 @@ export default function WebBidModal({ data, onUpdated, ...props }: IWebBidModelP
{data ? 'Update' : 'Create'}
</Button>
</form>
<LoadingOverlay visible={loading} zIndex={1000} overlayProps={{ blur: 2 }} />
</Modal>
);
}

View File

@ -34,7 +34,7 @@ export default function Login() {
<Box className="w-screen h-screen flex items-center justify-center">
<Paper w={400} radius="md" p="xl" withBorder>
<Text size="xl" className="text-center" fw={500}>
Login to KTQ Admin
Login to Bid System
</Text>
<Divider label="" labelPosition="center" my="lg" />

30
auto-bid-server/.env.dev Normal file
View File

@ -0,0 +1,30 @@
DB_HOST=127.0.0.1
# DB_HOST=127.0.0.1
DB_USERNAME=root
DB_PASSWORD=123
ENVIRONMENT='dev'
DB_PORT=3306
DB_NAME=auto-bids
PORT = 4000
APP_PATH = 'http://localhost:4000'
CORS = "http://localhost:5173, http://localhost:3000"
SECRET_KEY = "kgmwljwekqiq25232mdmsgnekwhlwekmglkwjqjwqw"
# DEV GROUP
TELEGRAM_BOT_TOKEN = "7963294152:AAE8b9AbsyLYzpeJbUMIcelVWlCBN5mLJ2o"
CHAT_ID = "-1002593407119"
# Bid histories GROUP
# TELEGRAM_BOT_TOKEN = "7533631751:AAEfE7Ei015U1sSsYPSAYwbYXWFl5D7y_18"
# CHAT_ID = "-1002535794248"

View File

@ -2,6 +2,11 @@ import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateAdminTable1742778498009 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// {
// 'username': 'admin',
// 'password': 'Admin@123'
// }
await queryRunner.query(`
INSERT INTO admins (email, username, password, is_system_account) VALUES
('admin@gmail.com', 'admin', '$2b$10$eF7K4Msw32e5ZC2cU78KgOqxMJygQcPDt5xXZP29inBBIV9KEsoyO', 1);

View File

@ -24,6 +24,9 @@ export default class Admin extends Timestamp {
@Column({ type: 'boolean', default: false })
is_system_account: boolean;
@ManyToMany(() => Permission, (permission) => permission.admins)
@ManyToMany(() => Permission, (permission) => permission.admins, {
cascade: true,
onDelete: 'CASCADE',
})
permissions: Permission[];
}

3
auto-bid-tool/.env.dev Normal file
View File

@ -0,0 +1,3 @@
ENVIRONMENT = 'prod'
SOCKET_URL = 'http://localhost:4000'
BASE_URL = 'http://localhost:4000/api/v1/'

View File

@ -40,9 +40,6 @@ lerna-debug.log*
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory

View File

@ -5,14 +5,11 @@ import { createApiBid, createBidProduct, deleteProfile, shouldUpdateProductTab }
import browser from './system/browser.js';
import configs from './system/config.js';
import { delay, isTimeReached, safeClosePage } from './system/utils.js';
import pLimit from 'p-limit';
let MANAGER_BIDS = [];
let _INTERVAL_TRACKING_ID = null;
let _CLEAR_LAZY_TAB_ID = null;
let _WORK_TRACKING_ID = null;
global.IS_CLEANING = false;
const activeTasks = new Set();
const handleUpdateProductTabs = (data) => {
if (!Array.isArray(data)) {
@ -48,142 +45,91 @@ const handleUpdateProductTabs = (data) => {
MANAGER_BIDS = newDataManager;
};
// const tracking = async () => {
// if (_INTERVAL_TRACKING_ID) {
// clearInterval(_INTERVAL_TRACKING_ID);
// _INTERVAL_TRACKING_ID = null;
// }
// _INTERVAL_TRACKING_ID = setInterval(async () => {
// const productTabs = _.flatMap(MANAGER_BIDS, 'children');
// for (const productTab of productTabs) {
// // Tìm parent context nếu chưa có
// if (!productTab.parent_browser_context) {
// const parent = _.find(MANAGER_BIDS, { id: productTab.web_bid.id });
// productTab.parent_browser_context = parent?.browser_context;
// if (!productTab.parent_browser_context) {
// console.log(`🔄 Waiting for parent process to start... (Product ID: ${productTab.id})`);
// continue;
// }
// }
// // Kết nối Puppeteer nếu chưa có page_context
// if (!productTab.page_context) {
// console.log(`🔌 Connecting to page for Product ID: ${productTab.id}`);
// await productTab.puppeteer_connect();
// }
// // Nếu URL thay đổi, điều hướng đến URL mới
// if (productTab.page_context.url() !== productTab.url) {
// await productTab.gotoLink();
// }
// // Kiểm tra nếu cần cập nhật trước khi gọi update()
// if (shouldUpdateProductTab(productTab)) {
// console.log(`🔄 Updating Product ID: ${productTab.id}...`);
// await productTab.update();
// } else {
// console.log(`⏳ Product ID: ${productTab.id} was updated recently. Skipping update.`);
// }
// // Nếu chưa có first_bid (trạng thái chưa đặt giá)
// if (!productTab.first_bid) {
// console.log(`🎯 Tracking out-bid event for Product ID: ${productTab.id}`);
// continue;
// }
// // Nếu chưa đến giờ bid
// if (productTab.start_bid_time && !isTimeReached(productTab.start_bid_time)) {
// console.log(`⏳ Not yet time to bid. Skipping Product ID: ${productTab.id}`);
// continue;
// }
// console.log(`🚀 Executing action for Product ID: ${productTab.id}`);
// await productTab.action();
// }
// }, configs.AUTO_TRACKING_DELAY);
// };
const tracking = async () => {
console.log('🚀 Tracking process started...');
while (true) {
console.log('🔍 Scanning active bids...');
const productTabs = _.flatMap(MANAGER_BIDS, 'children');
try {
console.log('🔍 Scanning active bids...');
const productTabs = _.flatMap(MANAGER_BIDS, 'children');
for (const apiBid of MANAGER_BIDS) {
if (apiBid.page_context) continue;
// Lắng nghe sự kiện của API bids (chạy song song)
await Promise.allSettled(
MANAGER_BIDS.filter((bid) => !bid.page_context).map((apiBid) => {
console.log(`🎧 Listening to events for API Bid ID: ${apiBid.id}`);
return apiBid.listen_events();
}),
);
console.log(`🎧 Listening to events for API Bid ID: ${apiBid.id}`);
await apiBid.listen_events();
Promise.allSettled(
productTabs.map(async (productTab) => {
console.log(`📌 Processing Product ID: ${productTab.id}`);
// Xác định parent context
if (!productTab.parent_browser_context) {
const parent = _.find(MANAGER_BIDS, { id: productTab.web_bid.id });
productTab.parent_browser_context = parent?.browser_context;
if (!productTab.parent_browser_context) {
console.log(`⏳ Waiting for parent process... (Product ID: ${productTab.id})`);
return;
}
}
// Kết nối Puppeteer nếu chưa có page_context
if (!productTab.page_context) {
console.log(`🔌 Connecting to page for Product ID: ${productTab.id}`);
await productTab.puppeteer_connect();
}
// Kiểm tra URL và điều hướng nếu cần
if ((await productTab.page_context.url()) !== productTab.url) {
console.log(`🔄 Redirecting to new URL for Product ID: ${productTab.id}`);
await productTab.gotoLink();
}
// Cập nhật nếu cần thiết
if (shouldUpdateProductTab(productTab)) {
console.log(`🔄 Updating Product ID: ${productTab.id}...`);
await productTab.update();
} else {
console.log(`⏳ Product ID: ${productTab.id} was updated recently. Skipping update.`);
}
// Chờ first bid
if (!productTab.first_bid) {
console.log(`🎯 Waiting for first bid for Product ID: ${productTab.id}`);
return;
}
// Kiểm tra thời gian bid
if (productTab.start_bid_time && !isTimeReached(productTab.start_bid_time)) {
console.log(`⏳ Not yet time to bid. Skipping Product ID: ${productTab.id}`);
return;
}
// Thực thi hành động
console.log(`🚀 Executing action for Product ID: ${productTab.id}`);
await productTab.action();
}),
);
// Dọn dẹp tab không dùng
console.log('🧹 Cleaning up unused tabs...');
clearLazyTab();
// Cập nhật trạng thái tracking
console.log('📊 Tracking work status...');
workTracking();
} catch (error) {
console.error('❌ Error in tracking loop:', error);
}
for (const productTab of productTabs) {
console.log(`📌 Processing Product ID: ${productTab.id}`);
// Tìm parent context nếu chưa có
if (!productTab.parent_browser_context) {
const parent = _.find(MANAGER_BIDS, { id: productTab.web_bid.id });
productTab.parent_browser_context = parent?.browser_context;
if (!productTab.parent_browser_context) {
console.log(`⏳ Waiting for parent process to start... (Product ID: ${productTab.id})`);
continue;
}
}
// Kết nối Puppeteer nếu chưa có page_context
if (!productTab.page_context) {
console.log(`🔌 Connecting to page for Product ID: ${productTab.id}`);
await productTab.puppeteer_connect();
}
// Nếu URL thay đổi, điều hướng đến URL mới
if (productTab.page_context.url() !== productTab.url) {
console.log(`🔄 Redirecting to new URL for Product ID: ${productTab.id}`);
await productTab.gotoLink();
}
// Kiểm tra nếu cần cập nhật trước khi gọi update()
if (shouldUpdateProductTab(productTab)) {
console.log(`🔄 Updating Product ID: ${productTab.id}...`);
await productTab.update();
} else {
console.log(`⏳ Product ID: ${productTab.id} was updated recently. Skipping update.`);
}
// Nếu chưa có first_bid (trạng thái chưa đặt giá)
if (!productTab.first_bid) {
console.log(`🎯 Waiting for first bid for Product ID: ${productTab.id}`);
continue;
}
// Nếu chưa đến giờ bid
if (productTab.start_bid_time && !isTimeReached(productTab.start_bid_time)) {
console.log(`⏳ Not yet time to bid. Skipping Product ID: ${productTab.id}`);
continue;
}
console.log(`🚀 Executing action for Product ID: ${productTab.id}`);
await productTab.action();
}
console.log('🧹 Cleaning up unused tabs...');
await clearLazyTab();
console.log('📊 Tracking work status...');
workTracking();
console.log(`⏳ Waiting ${configs.AUTO_TRACKING_DELAY / 1000} seconds before the next iteration...`);
await delay(configs.AUTO_TRACKING_DELAY);
}
};
const clearLazyTab = async () => {
if (!global.IS_CLEANING) return;
if (!browser) {
console.warn('⚠️ Browser is not available or disconnected.');
return;
@ -212,7 +158,7 @@ const clearLazyTab = async () => {
await page.close();
console.log(`🛑 Closing unused tab: ${pageUrl}`);
} catch (err) {
console.warn(`⚠️ Error closing tab ${pageUrl}:`, err.message);
console.warn(`⚠️ Error closing tab ${pageUrl}:, err.message`);
}
}
}
@ -225,14 +171,27 @@ const clearLazyTab = async () => {
const workTracking = async () => {
try {
const activeData = _.flatMap(MANAGER_BIDS, (item) => [item, ...item.children]);
const limit = pLimit(5);
for (const item of activeData) {
if (item.page_context && !item.page_context.isClosed()) {
item.handleTakeWorkSnapshot();
}
}
await Promise.allSettled(
activeData
.filter((item) => item.page_context && !item.page_context.isClosed())
.filter((item) => !activeTasks.has(item.id))
.map((item) =>
limit(async () => {
activeTasks.add(item.id);
try {
await item.handleTakeWorkSnapshot();
} catch (error) {
console.error(`[❌ ERROR] Snapshot failed for Product ID: ${item.id}`, error);
} finally {
activeTasks.delete(item.id);
}
}),
),
);
} catch (error) {
console.log('Lỗi rồi:', error);
console.error(`[❌ ERROR] Work tracking failed: ${error.message}\n`, error.stack);
}
};
@ -253,8 +212,6 @@ const workTracking = async () => {
console.log('📢 Bids Data:', data);
handleUpdateProductTabs(data);
// await Promise.all(MANAGER_BIDS.map((apiBid) => apiBid.listen_events()));
});
socket.on('webUpdated', async (data) => {
@ -270,8 +227,6 @@ const workTracking = async () => {
if (tabs.length <= 0) return;
await Promise.all(tabs.map((tab) => safeClosePage(tab)));
await Promise.all(MANAGER_BIDS.map((apiBid) => apiBid.listen_events()));
} else {
console.log('⚠️ No profile found to delete.');
}
@ -279,8 +234,4 @@ const workTracking = async () => {
// AUTO TRACKING
tracking();
// clearLazyTab();
// workTracking();
})();

View File

@ -19,10 +19,11 @@ export class Bid {
if (!this.page_context) return;
try {
console.log(`✅ Page loaded. Taking snapshot for Product ID: ${this.id}`);
// await this.page_context.waitForSelector('#pageContainer', { timeout: 10000 });
console.log(`✅ Page fully loaded. Taking snapshot for Product ID: ${this.id}`);
takeSnapshot(this.page_context, this, 'working', CONSTANTS.TYPE_IMAGE.WORK);
} catch (error) {
console.error(`❌ Error taking snapshot for Product ID: ${this.id}:`, error.message);
}
}, 500);
}, 1000);
}

View File

@ -134,14 +134,12 @@ export class GrayApiBid extends ApiBid {
// 🔍 Check if already logged in (login input should not be visible)
if (!(await page.$('input[name="username"]')) || fs.existsSync(filePath)) {
console.log('✅ Already logged in, skipping login.');
global.IS_CLEANING = true;
this.retry_login = 0; // Reset retry count
return;
}
console.log('🔑 Starting login process...');
global.IS_CLEANING = false;
try {
await page.type('input[name="username"]', this.username, { delay: 100 });
@ -156,7 +154,6 @@ export class GrayApiBid extends ApiBid {
if (!(await page.$('input[name="username"]'))) {
console.log('✅ Login successful!');
this.retry_login = 0; // Reset retry count after success
global.IS_CLEANING = true;
return;
}
@ -211,7 +208,7 @@ export class GrayApiBid extends ApiBid {
});
page.on('load', async () => {
console.log('🔄 Trang đã reload, khởi động lại polling...');
console.log('🔄 The page has reloaded, restarting polling...');
// await takeSnapshot(this.page_context, this, 'working', CONSTANTS.TYPE_IMAGE.WORK);

View File

@ -38,8 +38,6 @@ export class GraysProductBid extends ProductBid {
});
if (!response.status) {
// await this.handleReturnProductPage(page);
// await safeClosePage(this);
return { result: false, bid_price: 0 };
}
@ -95,7 +93,6 @@ export class GraysProductBid extends ProductBid {
if (!close_time || new Date(close_time).getTime() <= new Date().getTime()) {
console.log(`Product is close ${close_time}`);
// await safeClosePage(this);
return { result: true, close_time };
}
@ -126,10 +123,8 @@ export class GraysProductBid extends ProductBid {
await takeSnapshot(page, this, 'bid-success', CONSTANTS.TYPE_IMAGE.SUCCESS);
return true;
} catch (error) {
console.log({ error: error.message });
console.log('❌ Timeout to loading');
await takeSnapshot(page, this, 'timeout to loading');
// await safeClosePage(this);
return false;
}
}
@ -140,8 +135,6 @@ export class GraysProductBid extends ProductBid {
}
async handleUpdateBid({ lot_id, close_time, name, current_price, reserve_price }) {
// if (close_time && this.close_time == close_time) return;
const response = await updateBid(this.id, { lot_id, close_time, name, current_price, reserve_price: Number(reserve_price) || 0 });
if (response) {
@ -236,7 +229,6 @@ export class GraysProductBid extends ProductBid {
if (!resultPlaceBid) {
console.log('❌ Error occurred while placing the bid.');
await takeSnapshot(page, this, 'place-bid-action');
// await safeClosePage(this);
return;
}
@ -245,7 +237,6 @@ export class GraysProductBid extends ProductBid {
await this.handleReturnProductPage(page);
} catch (error) {
console.error(`🚨 Error navigating the page: ${error.message}`);
// safeClosePage(this);
}
};
}

View File

@ -12,6 +12,7 @@
"axios": "^1.8.2",
"dotenv": "^16.4.7",
"lodash": "^4.17.21",
"p-limit": "^6.2.0",
"puppeteer": "^24.4.0",
"puppeteer-extra": "^3.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2",
@ -1294,6 +1295,21 @@
"wrappy": "1"
}
},
"node_modules/p-limit": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.2.0.tgz",
"integrity": "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==",
"license": "MIT",
"dependencies": {
"yocto-queue": "^1.1.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pac-proxy-agent": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
@ -1992,6 +2008,18 @@
"fd-slicer": "~1.1.0"
}
},
"node_modules/yocto-queue": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz",
"integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==",
"license": "MIT",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zod": {
"version": "3.24.2",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",

View File

@ -15,6 +15,7 @@
"axios": "^1.8.2",
"dotenv": "^16.4.7",
"lodash": "^4.17.21",
"p-limit": "^6.2.0",
"puppeteer": "^24.4.0",
"puppeteer-extra": "^3.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2",

View File

@ -5,6 +5,7 @@ import configs from '../system/config.js';
import CONSTANTS from '../system/constants.js';
import { sanitizeFileName } from '../system/utils.js';
import * as fs from 'fs';
import _ from 'lodash';
const ONE_MINUTE = 60 * 1000;

View File

@ -5,19 +5,35 @@ import StealthPlugin from 'puppeteer-extra-plugin-stealth';
puppeteer.use(StealthPlugin());
const browser = await puppeteer.launch({
headless: process.env.ENVIRONMENT === 'prod' ? true : false,
headless: process.env.ENVIRONMENT === 'prod' ? 'new' : false,
// userDataDir: CONSTANTS.PROFILE_PATH, // Thư mục lưu profile
timeout: 60000,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--disable-ipc-flooding-protection',
'--disable-features=CalculateNativeWinOcclusion,AudioServiceOutOfProcess',
'--disable-background-timer-throttling',
'--disable-dev-shm-usage',
'--disable-gpu',
'--disable-software-rasterizer',
'--disable-background-networking',
'--disable-sync',
'--mute-audio',
'--no-first-run',
'--no-default-browser-check',
'--ignore-certificate-errors',
'--start-maximized',
'--disable-site-isolation-trials', // Tắt sandbox riêng cho từng site
'--memory-pressure-off', // Tắt cơ chế bảo vệ bộ nhớ
'--disk-cache-size=0', // Không dùng cache để giảm bộ nhớ
'--enable-low-end-device-mode', // Kích hoạt chế độ tiết kiệm RAM
'--disable-best-effort-tasks', // Tắt tác vụ không quan trọng
'--disable-accelerated-2d-canvas', // Không dùng GPU để vẽ canvas
'--disable-threaded-animation', // Giảm animation chạy trên nhiều thread
'--disable-threaded-scrolling', // Tắt cuộn trang đa luồng
'--disable-logging', // Tắt log debug
'--blink-settings=imagesEnabled=false', // Không tải hình ảnh,
'--disable-background-timer-throttling', // Tránh việc throttling các timer khi chạy nền.
'--disable-webrtc',
'--disable-ipc-flooding-protection', // Nếu có extension cần IPC, cái này giúp tối ưu.
],
});

View File

@ -39,7 +39,7 @@ export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_
});
// Chụp ảnh màn hình và lưu vào filePath
await page.screenshot({ path: filePath, fullPage: true });
await page.screenshot({ path: filePath });
console.log(`📸 Image saved at: ${filePath}`);
@ -49,8 +49,6 @@ export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_
}
} catch (error) {
console.log('Error when snapshot: ' + error.message);
} finally {
global.IS_CLEANING = true;
}
};