Merge pull request 'staging' (#6) from staging into production
Reviewed-on: #6
This commit is contained in:
commit
2a9fb6b506
|
|
@ -0,0 +1,2 @@
|
|||
VITE_BASE_URL = 'http://localhost:4000/api/v1/admin/'
|
||||
VITE_SOCKET_URL = 'http://localhost:4000'
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
ENVIRONMENT = 'prod'
|
||||
SOCKET_URL = 'http://localhost:4000'
|
||||
BASE_URL = 'http://localhost:4000/api/v1/'
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
],
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue