update early tracking time

This commit is contained in:
Admin 2025-05-07 14:42:32 +07:00
parent 8e2f274668
commit 0ca1e6846c
15 changed files with 249 additions and 158 deletions

View File

@ -43,7 +43,7 @@ export const updateWebBid = async (bid: Partial<IWebBid>) => {
origin_url,
active,
arrival_offset_seconds,
// early_login_seconds
early_tracking_seconds
} = removeFalsyValues(bid, ["active"]);
try {
@ -58,7 +58,7 @@ export const updateWebBid = async (bid: Partial<IWebBid>) => {
origin_url,
active,
arrival_offset_seconds,
// early_login_seconds
early_tracking_seconds
},
});

View File

@ -1,4 +1,4 @@
import { Badge, Box, Button, Image, Text } from "@mantine/core";
import { Badge, Box, Button, Image, Text, Tooltip } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import moment from "moment";
import { useEffect, useState } from "react";
@ -8,6 +8,7 @@ import { useStatusToolStore } from "../../lib/zustand/use-status-tool-store";
import { IBid, IWebBid } from "../../system/type";
import { cn, stringToColor } from "../../utils";
import ShowImageModal from "./show-image-modal";
import { isTimeReached, subtractSeconds } from "../../lib/table/ultils";
export interface IWorkingPageProps {
data: (IBid | IWebBid) & { type: string };
socket: Socket;
@ -85,12 +86,6 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
useEffect(() => {
const onLoginStatus = (data: { data: IWebBid; login_status: boolean }) => {
setPayloadLoginStatus(data);
console.log(
"%csrc/components/dashboard/working-page.tsx:60 data",
"color: #007acc;",
data
);
};
const origin_url = isIBid(data) ? data.web_bid.origin_url : data.origin_url;
@ -144,9 +139,43 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
{isIBid(data) && (
<Text className="text-xs tracking-wide">{`Current price: $${data.current_price}`}</Text>
)}
<Text className="text-sm italic opacity-80">
{moment(lastUpdate).format("HH:mm:ss DD/MM/YYYY")}
</Text>
<Box className="flex items-center gap-3">
{isIBid(data) && (
<Tooltip label={'Close time'}>
<Text
style={{ fontSize: "12px" }}
className="tracking-wide"
>{`CLT: ${moment(data.close_time).format(
"HH:mm:ss DD/MM/YYYY"
)}`}</Text>
</Tooltip>
)}
{isIBid(data) &&
data.close_time &&
!isTimeReached(
subtractSeconds(
data.close_time,
data.web_bid?.early_tracking_seconds || 0
)
) && (
<Tooltip label={'Time to tracking'}>
<Text
style={{ fontSize: "12px" }}
className="tracking-wide"
>{`TT: ${moment(
subtractSeconds(
data.close_time,
data.web_bid?.early_tracking_seconds || 0
)
).format("HH:mm:ss DD/MM/YYYY")}`}</Text>
</Tooltip>
)}
</Box>
<Box className="flex items-center gap-4">
<Button
size="xs"
@ -177,7 +206,9 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
</Badge>
<Badge
color={stringToColor(isIBid(data) ? data.web_bid.origin_url : data.origin_url)}
color={stringToColor(
isIBid(data) ? data.web_bid.origin_url : data.origin_url
)}
size="xs"
>
{isIBid(data) ? data.web_bid.origin_url : data.origin_url}

View File

@ -27,7 +27,7 @@ const schema = {
.refine((val) => val >= 60, {
message: "Arrival offset seconds must be at least 60 seconds (1 minute)",
}),
early_login_seconds: z
early_tracking_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)",
@ -78,14 +78,14 @@ export default function WebBidModal({
},
});
} else {
const { url, origin_url, arrival_offset_seconds, early_login_seconds } = values;
const { url, origin_url, arrival_offset_seconds, early_tracking_seconds } = values;
setLoading(true);
const result = await createWebBid({
url,
origin_url,
arrival_offset_seconds,
early_login_seconds
early_tracking_seconds
} as IWebBid);
setLoading(false);
@ -163,16 +163,16 @@ export default function WebBidModal({
placeholder="msg: 300"
{...form.getInputProps("arrival_offset_seconds")}
/>
{/* <NumberInput
<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
label={`Early tracking seconds (${
form.getValues()["early_tracking_seconds"] / 60
} minutes)`}
placeholder="msg: 600"
{...form.getInputProps("early_login_seconds")}
/> */}
{...form.getInputProps("early_tracking_seconds")}
/>
<Button
disabled={_.isEqual(form.getValues(), prevData.current)}

View File

@ -1,84 +1,144 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { IColumn, IDataFilter, ITableFilter, ITableShort, TShort } from '../type';
import {
IColumn,
IDataFilter,
ITableFilter,
ITableShort,
TShort,
} from "../type";
export const defaultPrefixShort = 'order_by_';
export const defaultPathToData = 'data';
export const flowShort: TShort[] = ['desc', 'asc'];
export const defaultKeyPerpage = 'per_page';
export const defaultKeyPage = 'page';
export const searchKey = 'search_key';
export const defaultPrefixShort = "order_by_";
export const defaultPathToData = "data";
export const flowShort: TShort[] = ["desc", "asc"];
export const defaultKeyPerpage = "per_page";
export const defaultKeyPage = "page";
export const searchKey = "search_key";
export const defaultStyleHightlight = { color: 'red', backgroundColor: 'yellow' } as React.CSSProperties;
export const defaultStyleHightlight = {
color: "red",
backgroundColor: "yellow",
} as React.CSSProperties;
export const defaultPerpageValue = '10';
export const defaultPerpageValue = "10";
export const getParamsData = <R extends Record<string, string | number>>(options: { prefixShort?: string; columns: IColumn<R>[] }) => {
const paramsUrl = new URLSearchParams(window.location.search);
export const getParamsData = <
R extends Record<string, string | number>
>(options: {
prefixShort?: string;
columns: IColumn<R>[];
}) => {
const paramsUrl = new URLSearchParams(window.location.search);
if (!paramsUrl.size) return {};
if (!paramsUrl.size) return {};
const prefixShort = options?.prefixShort || defaultPrefixShort;
const prefixShort = options?.prefixShort || defaultPrefixShort;
const paramObject: { [key: string]: string | number } = {};
paramsUrl.forEach((value, key) => {
paramObject[key] = value;
const paramObject: { [key: string]: string | number } = {};
paramsUrl.forEach((value, key) => {
paramObject[key] = value;
});
const pramsKeys = Object.keys(paramObject);
if (pramsKeys.length <= 0)
return {
shortParamsData: {},
searchParamsData: {},
filterParamsData: [],
params: {},
};
const shortParamsData = pramsKeys
.filter(
(item) =>
item.includes(prefixShort) &&
options.columns
.map((col) => col.key)
.includes(item.replace(prefixShort, "") as IColumn<R>["key"])
)
.map((i) => {
return {
key: i.replace(prefixShort, ""),
type: flowShort.includes(paramObject[i] as TShort)
? paramObject[i]
: "asc",
} as ITableShort<R>;
});
const pramsKeys = Object.keys(paramObject);
const filterParamsData = pramsKeys
// .filter((item) => options.columns.map((col) => col.key).includes(item as IColumn<R>['key']))
.map((item) => ({ key: item, type: paramObject[item] } as ITableFilter<R>));
if (pramsKeys.length <= 0)
return {
shortParamsData: {},
searchParamsData: {},
filterParamsData: [],
params: {},
};
const shortObject = shortParamsData[0]
? {
[`${prefixShort}${shortParamsData[0].key}`]: String(
shortParamsData[0].type
),
}
: {};
const shortParamsData = pramsKeys
.filter((item) => item.includes(prefixShort) && options.columns.map((col) => col.key).includes(item.replace(prefixShort, '') as IColumn<R>['key']))
.map((i) => {
return { key: i.replace(prefixShort, ''), type: flowShort.includes(paramObject[i] as TShort) ? paramObject[i] : 'asc' } as ITableShort<R>;
});
const pramsData: IDataFilter[] = [
...(filterParamsData ? (filterParamsData as IDataFilter[]) : []),
];
const filterParamsData = pramsKeys
// .filter((item) => options.columns.map((col) => col.key).includes(item as IColumn<R>['key']))
.map((item) => ({ key: item, type: paramObject[item] } as ITableFilter<R>));
let params = convertToParams(
pramsData as unknown as Record<string, string | number>[]
);
const shortObject = shortParamsData[0] ? { [`${prefixShort}${shortParamsData[0].key}`]: String(shortParamsData[0].type) } : {};
const pramsData: IDataFilter[] = [...(filterParamsData ? (filterParamsData as IDataFilter[]) : [])];
let params = convertToParams(pramsData as unknown as Record<string, string | number>[]);
if (shortParamsData.length) {
params = {
...params,
...shortObject,
};
}
return { shortParamsData: shortParamsData[0] || {}, filterParamsData, params };
if (shortParamsData.length) {
params = {
...params,
...shortObject,
};
}
return {
shortParamsData: shortParamsData[0] || {},
filterParamsData,
params,
};
};
export const convertToParams = (filter: Record<string, string | number>[]) => {
const params = filter.reduce((prev, cur) => {
prev[cur.key] = cur.type;
const params = filter.reduce((prev, cur) => {
prev[cur.key] = cur.type;
return prev;
}, {} as Record<string, string | number>);
return prev;
}, {} as Record<string, string | number>);
return params;
return params;
};
export const removeFalsy = (data: { [key: string]: string | number }) => {
return Object.keys(data).reduce((prev, cur) => {
if (data[cur]) {
prev[cur] = data[cur];
}
return prev;
}, {} as { [key: string]: string | number });
return Object.keys(data).reduce((prev, cur) => {
if (data[cur]) {
prev[cur] = data[cur];
}
return prev;
}, {} as { [key: string]: string | number });
};
export function extractNumber(str: string) {
const match = str.match(/\d+(\.\d+)?/);
return match ? parseFloat(match[0]) : null;
const match = str.match(/\d+(\.\d+)?/);
return match ? parseFloat(match[0]) : null;
}
export function subtractMinutes(time: string, minutes: number) {
const date = new Date(time);
date.setMinutes(date.getMinutes() - minutes);
return date.toUTCString();
}
export function subtractSeconds(time: string, seconds: number) {
const date = new Date(time);
date.setSeconds(date.getSeconds() - seconds);
return date.toUTCString();
}
export function isTimeReached(targetTime: string) {
if (!targetTime) return false;
const targetDate = new Date(targetTime);
const now = new Date();
return now >= targetDate;
}

View File

@ -167,7 +167,7 @@ export default function DashBoard() {
</Box>
</Tooltip>
</Box>
<Box className="grid grid-cols-4 gap-4 mt-5">
<Box className="grid lg:grid-cols-3 2xl:grid-cols-4 gap-4 mt-5">
{workingData.length > 0 &&
workingData.map((item, index) => (
<WorkingPage socket={socket} data={item} key={item.id + index} />

View File

@ -43,7 +43,7 @@ export interface IWebBid extends ITimestamp {
password: string | null;
active: boolean;
arrival_offset_seconds: number;
early_login_seconds: number;
early_tracking_seconds: number;
children: IBid[];
}

View File

@ -172,7 +172,7 @@ export function findEarlyLoginTime(webBid: IWebBid): string | 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));
closeTime.setSeconds(closeTime.getSeconds() - (webBid.early_tracking_seconds || 0));
return closeTime.toISOString();
}

View File

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

View File

@ -17,7 +17,7 @@ export class UpdateWebBidDto {
@IsNumber()
@Min(600)
@IsOptional()
early_login_seconds: number;
early_tracking_seconds: number;
@IsString()
@IsOptional()

View File

@ -21,7 +21,7 @@ export class WebBid extends Timestamp {
arrival_offset_seconds: number;
@Column({ default: 600 })
early_login_seconds: number;
early_tracking_seconds: number;
@Column({ default: null, nullable: true })
@Exclude()

View File

@ -8,15 +8,15 @@ import {
deleteProfile,
shouldUpdateProductTab,
} from "./service/app-service.js";
import { updateLoginStatus } from "./system/apis/bid.js";
import browser from "./system/browser.js";
import configs from "./system/config.js";
import {
delay,
findEarlyLoginTime,
isTimeReached,
safeClosePage,
subtractSeconds,
} from "./system/utils.js";
import { updateLoginStatus } from "./system/apis/bid.js";
global.IS_CLEANING = true;
@ -156,6 +156,21 @@ const tracking = async () => {
}
}
// Thời điểm tracking liên tục
const earlyTrackingTime = subtractSeconds(
productTab.close_time,
productTab?.web_bid?.early_tracking_seconds || 0
);
// Check không mở tab nếu chưa đến giờ
if (productTab.close_time && !isTimeReached(earlyTrackingTime)) {
console.log(
`⏳ [${productTab.id}] Early tracking time not reached yet. ` +
`Waiting until ${earlyTrackingTime} (current time: ${new Date().toISOString()})`
);
return;
}
// Kết nối Puppeteer nếu chưa có page_context
if (!productTab.page_context) {
console.log(
@ -231,72 +246,6 @@ 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.");
@ -317,6 +266,9 @@ const clearLazyTab = async () => {
...item.children.map((child) => child.url),
]).filter(Boolean);
// product tabs
const productTabs = _.flatMap(MANAGER_BIDS, "children");
for (const page of pages) {
try {
if (page.isClosed()) continue; // Trang đã đóng thì bỏ qua
@ -324,8 +276,35 @@ const clearLazyTab = async () => {
const pageUrl = page.url();
if (!pageUrl || pageUrl === "about:blank") continue;
if (activeUrls.includes(pageUrl)) continue;
if (activeUrls.includes(pageUrl)) {
const productTab = productTabs.find((item) => item.url === pageUrl);
if (!productTab || !productTab?.close_time) continue;
const earlyTrackingTime = subtractSeconds(
productTab.close_time,
productTab?.web_bid?.early_tracking_seconds || 0
);
console.log("%cindex.js:291 {object}", "color: #007acc;", {
earlyTrackingTime,
it_time: isTimeReached(earlyTrackingTime),
close_time: productTab.close_time,
tracking_time: productTab?.web_bid?.early_tracking_seconds,
});
if (!isTimeReached(earlyTrackingTime)) {
await safeClosePage(productTab);
console.log(`🛑 Unused page detected: ${pageUrl}`);
continue;
}
continue;
}
// remove all listents
page.removeAllListeners();
console.log(`🛑 Unused page detected: ${pageUrl}`);
@ -473,6 +452,15 @@ const trackingLoginStatus = async () => {
socket.on("webUpdated", async (data) => {
console.log("📢 Account was updated:", data);
const webBid = MANAGER_BIDS.find((item) => item.id === data.id);
if (
webBid &&
webBid.username === data.username &&
webBid.password === data.password
)
return;
const isDeleted = deleteProfile(data);
if (isDeleted) {

View File

@ -20,7 +20,7 @@ export class ApiBid extends Bid {
updated_at;
origin_url;
active;
browser_context;
// browser_context;
username;
password;

View File

@ -9,6 +9,7 @@ export class Bid {
url;
action;
page_context;
browser_context;
constructor(type, url, puppeteer_connect) {
this.type = type;

View File

@ -120,6 +120,7 @@ export class ProductBid extends Bid {
const page = await context.newPage();
this.page_context = page;
this.browser_context = context;
};
async restoreContext(context) {

View File

@ -94,13 +94,15 @@ export async function safeClosePage(item) {
if (!page?.isClosed() && page?.close) {
await safeClosePageReal(page);
// await page.close();
}
item.page_context = undefined;
if (item?.page_context) {
item.page_context = undefined;
}
if (item?.browser_context) {
item.browser_context?.close();
}
} catch (error) {
console.log("Can't close item: " + item.id);
}
@ -258,3 +260,15 @@ export function findEarlyLoginTime(webBid) {
return closeTime.toISOString();
}
export function subtractMinutes(time, minutes) {
const date = new Date(time);
date.setMinutes(date.getMinutes() - minutes);
return date.toUTCString();
}
export function subtractSeconds(time, seconds) {
const date = new Date(time);
date.setSeconds(date.getSeconds() - seconds);
return date.toUTCString();
}