10 Commits

45 changed files with 7597 additions and 37 deletions

View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -1,12 +1,18 @@
import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import build from "./admin.build.js";
import update from "./admin.updateServer.js";
export const setupAdminRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(`${baseUrl}/api/admin/build`, requireAuth, build);
app.use(`${baseUrl}/api/admin/build`, requireAuth, update);
app.use(`${baseUrl}/api/admin/build`, requireAuth, routeHitMiddleware, build);
app.use(
`${baseUrl}/api/admin/build`,
requireAuth,
routeHitMiddleware,
update,
);
// all other system should be under /api/system/*
};

View File

@@ -3,6 +3,7 @@ import { fileURLToPath } from "node:url";
import { toNodeHandler } from "better-auth/node";
import express from "express";
import morgan from "morgan";
import { umamiConfig } from "./configs/umami.config.js";
import { createLogger } from "./logger/logger.controller.js";
import { setupRoutes } from "./routeHandler.routes.js";
import { auth } from "./utils/auth.utils.js";
@@ -33,6 +34,22 @@ const createApp = async () => {
app.use(express.json());
setupRoutes(baseUrl, app);
app.get(`${baseUrl}/api/lst-config.js`, (_, res) => {
res.type("application/javascript");
res.setHeader("Cache-Control", "no-store");
res.send(`
window.LST_CONFIG = {
appName: ${JSON.stringify(umamiConfig.appName ?? "LST")},
site: ${JSON.stringify(umamiConfig.site ?? "unknown")},
server: ${JSON.stringify(umamiConfig.server ?? "unknown")},
appVersion: ${JSON.stringify(umamiConfig.appVersion ?? "dev")},
umamiHost: ${JSON.stringify(umamiConfig.umamiHost ?? "")},
umamiWebsiteId: ${JSON.stringify(umamiConfig.umamiWebsiteId ?? "")}
};
`);
});
app.use(
`${baseUrl}/app`,
express.static(join(__dirname, "../frontend/dist")),

View File

@@ -1,9 +1,11 @@
import type { Express } from "express";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import login from "./login.route.js";
import register from "./register.route.js";
export const setupAuthRoutes = (baseUrl: string, app: Express) => {
//setup all the routes
app.use(routeHitMiddleware);
app.use(`${baseUrl}/api/authentication/login`, login);
app.use(`${baseUrl}/api/authentication/register`, register);
};

View File

@@ -0,0 +1,21 @@
export type UmamiRuntimeConfig = {
appName: string;
site: string;
server: string;
appVersion: string;
umamiHost: string;
umamiWebsiteId: string;
};
export const umamiConfig: UmamiRuntimeConfig = {
appName: process.env.APP_NAME ?? "LST",
site: process.env.URL ?? "unknown",
server: process.env.PROD_PLANT_TOKEN ?? "unknown", // could also be server name based on our setup.
appVersion: process.env.NODE_ENV ?? "dev",
umamiHost: process.env.UMAMI_HOST ?? "",
umamiWebsiteId: process.env.UMAMI_WEBSITE_ID ?? "",
};
export function isUmamiEnabled() {
return Boolean(umamiConfig.umamiHost && umamiConfig.umamiWebsiteId);
}

View File

@@ -1,5 +1,5 @@
import type { Express } from "express";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { datamartData } from "./datamartData.utlis.js";
import runQuery from "./getDatamart.route.js";
@@ -30,7 +30,7 @@ export const setupDatamartRoutes = (baseUrl: string, app: Express) => {
// });
//setup all the routes
app.use(routeHitMiddleware);
app.use(`${baseUrl}/api/datamart`, runQuery);
// just sending a get on datamart will return all the queries that we can call.

View File

@@ -0,0 +1,21 @@
import { integer, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
export const analytics = pgTable("analytics", {
id: uuid("id").defaultRandom().primaryKey(),
createdAt: timestamp("created_at").defaultNow().notNull(),
method: text("method").notNull(),
routePattern: text("route_pattern").notNull(),
actualPath: text("actual_path").notNull(),
statusCode: integer("status_code").notNull(),
durationMs: integer("duration_ms").notNull(),
module: text("module"),
userId: text("user_id"),
userEmail: text("user_email"),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
});

View File

@@ -0,0 +1,33 @@
import {
date,
integer,
pgTable,
text,
timestamp,
uuid,
} from "drizzle-orm/pg-core";
export const analyticsDaily = pgTable("analytics_daily", {
id: uuid("id").defaultRandom().primaryKey(),
businessDate: date("business_date").notNull(),
method: text("method").notNull(),
routePattern: text("route_pattern").notNull(),
module: text("module").notNull(),
totalHits: integer("total_hits").notNull(),
uniqueUsers: integer("unique_users").notNull(),
successCount: integer("success_count").notNull(),
errorCount: integer("error_count").notNull(),
avgDurationMs: integer("avg_duration_ms").notNull(),
maxDurationMs: integer("max_duration_ms").notNull(),
firstHitAt: timestamp("first_hit_at").notNull(),
lastHitAt: timestamp("last_hit_at").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});

View File

@@ -12,7 +12,7 @@ export const scanLog = pgTable("scan_log", {
runningNumber: text("running_number").default("0"),
status: text("status"),
lines: jsonb("lines").default([]),
add_Date: timestamp("add_Date").defaultNow(),
add_Date: timestamp("add_date").defaultNow(),
});
export const scanLogSchema = createSelectSchema(scanLog);

View File

@@ -1,5 +1,6 @@
import { type Express, Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import restart from "./gpSqlRestart.route.js";
import start from "./gpSqlStart.route.js";
import stop from "./gpSqlStop.route.js";
@@ -8,6 +9,7 @@ export const setupGPSqlRoutes = (baseUrl: string, app: Express) => {
// Apply auth to entire router
const router = Router();
router.use(requireAuth);
app.use(routeHitMiddleware);
router.use(start);
router.use(stop);

View File

@@ -0,0 +1,83 @@
// routeHit.middleware.ts
import type { NextFunction, Request, Response } from "express";
import {
createRouteHit,
shouldIgnoreRoute,
} from "../utils/analyticRouteHits.utils.js";
export function routeHitMiddleware(
req: Request,
res: Response,
next: NextFunction,
) {
const start = performance.now();
res.on("finish", () => {
const actualPath = getActualPath(req);
if (shouldIgnoreRoute(actualPath)) {
return;
}
const durationMs = Math.round(performance.now() - start);
const routePattern = getRoutePattern(req) as string;
const module = getModuleName(req);
void createRouteHit({
method: req.method,
routePattern,
actualPath,
statusCode: res.statusCode,
durationMs,
module,
// adjust these names to your Better Auth/session shape
userId: req.user?.id ?? null,
userEmail: req.user?.email ?? null,
ipAddress: req.ip ?? null,
userAgent: req.get("user-agent") ?? null,
}).catch((err) => {
console.error("Failed to save route hit", err);
});
});
next();
}
function getActualPath(req: Request) {
return req.originalUrl.split("?")[0] ?? req.path ?? "unknown";
}
function getRoutePattern(req: Request) {
const baseUrl = req.baseUrl || "";
const routePath = req.route?.path;
if (typeof routePath === "string") {
return `${baseUrl}${routePath}`;
}
return getActualPath(req);
}
function getModuleName(req: Request) {
const path = req.originalUrl.split("?")[0];
if (path?.includes("/printers")) return "printers";
if (path?.includes("/releases")) return "releases";
if (path?.includes("/quality")) return "quality";
if (path?.includes("/scanner")) return "scanner";
if (path?.includes("/settings")) return "settings";
if (path?.includes("/users")) return "users";
if (path?.includes("/mobile")) return "mobile";
if (path?.includes("/servers")) return "servers";
if (path?.includes("/logistics")) return "servers";
if (path?.includes("/ocp")) return "ocp";
if (path?.includes("/auth")) return "auth";
if (path?.includes("/datamart")) return "datamart";
if (path?.includes("/opendock")) return "opendock";
return "unknown";
}

View File

@@ -1,5 +1,6 @@
import type { Express } from "express";
import downloads from "./donwloadApps.route.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import downloads from "./downloadApps.route.js";
import lanes from "./laneCheck.js";
import authPin from "./mobileAuth.route.js";
import newPin from "./mobilePin.route.js";
@@ -8,6 +9,9 @@ import version from "./version.route.js";
export const setupMobileRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(routeHitMiddleware);
app.use(`${baseUrl}/api/mobile/version`, version);
app.use(`${baseUrl}/api/mobile/apk`, downloads);
app.use(`${baseUrl}/api/mobile/logs`, logs);

View File

@@ -162,6 +162,14 @@ r.post("/user", async (req, res) => {
r.get("/user", requireAuth, async (_, res) => {
const { data, error } = await tryCatch(db.select().from(scanUser));
// await trackLstEvent({
// eventName: "mobile_get_users",
// url: "/mobile/users",
// eventData: {
// module: "mobile",
// },
// });
if (error) {
return apiReturn(res, {
success: false,

View File

@@ -1,7 +1,9 @@
import fs from "node:fs";
import { and, eq } from "drizzle-orm";
import { Router } from "express";
import path from "path";
import { db } from "../db/db.controller.js";
import { settings } from "../db/schema/settings.schema.js";
const router = Router();
@@ -10,6 +12,15 @@ const appJsonPath = path.join(projectRoot, "app.json");
router.get("/", async (req, res) => {
const baseUrl = `${req.protocol}://${req.get("host")}`;
const mobileSettings = await db
.select()
.from(settings)
.where(
and(
eq(settings.moduleName, "mobile"),
eq(settings.settingType, "standard"),
),
);
const raw = fs.readFileSync(appJsonPath, "utf-8");
const config = JSON.parse(raw);
@@ -22,6 +33,7 @@ router.get("/", async (req, res) => {
versionCode: exp.android?.versionCode,
minSupportedVersionCode: exp?.android?.minSupportedVersionCode ?? 0,
downloadUrl: `${baseUrl}/lst/api/mobile/apk/latest`,
settings: mobileSettings,
});
});

View File

@@ -1,5 +1,6 @@
import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import manual from "./notification.manualTrigger.js";
import getNotifications from "./notification.route.js";
import updateNote from "./notification.update.route.js";
@@ -10,13 +11,48 @@ import updateSub from "./notificationSub.update.route.js";
export const setupNotificationRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(`${baseUrl}/api/notification`, requireAuth, getNotifications);
app.use(`${baseUrl}/api/notification`, requireAuth, updateNote);
app.use(`${baseUrl}/api/notification/manual`, requireAuth, manual);
app.use(`${baseUrl}/api/notification/sub`, requireAuth, subs);
app.use(`${baseUrl}/api/notification/sub`, requireAuth, newSub);
app.use(`${baseUrl}/api/notification/sub`, requireAuth, updateSub);
app.use(`${baseUrl}/api/notification/sub`, requireAuth, deleteSub);
app.use(
`${baseUrl}/api/notification`,
requireAuth,
routeHitMiddleware,
getNotifications,
);
app.use(
`${baseUrl}/api/notification`,
requireAuth,
routeHitMiddleware,
updateNote,
);
app.use(
`${baseUrl}/api/notification/manual`,
requireAuth,
routeHitMiddleware,
manual,
);
app.use(
`${baseUrl}/api/notification/sub`,
requireAuth,
routeHitMiddleware,
subs,
);
app.use(
`${baseUrl}/api/notification/sub`,
requireAuth,
routeHitMiddleware,
newSub,
);
app.use(
`${baseUrl}/api/notification/sub`,
requireAuth,
routeHitMiddleware,
updateSub,
);
app.use(
`${baseUrl}/api/notification/sub`,
requireAuth,
routeHitMiddleware,
deleteSub,
);
// all other system should be under /api/system/*
};

View File

@@ -1,6 +1,7 @@
import { type Express, Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { featureCheck } from "../middleware/featureActive.middleware.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import listener from "./ocp.printer.listener.js";
import update from "./ocp.printer.update.js";
@@ -17,6 +18,8 @@ export const setupOCPRoutes = (baseUrl: string, app: Express) => {
// auth routes below here
router.use(requireAuth);
app.use(routeHitMiddleware);
router.use(update);
//router.use("");

View File

@@ -1,6 +1,7 @@
import { type Express, Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { featureCheck } from "../middleware/featureActive.middleware.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import getApt from "./opendockGetRelease.route.js";
export const setupOpendockRoutes = (baseUrl: string, app: Express) => {
@@ -13,6 +14,7 @@ export const setupOpendockRoutes = (baseUrl: string, app: Express) => {
// we need to make sure we are authenticated to see the releases
router.use(requireAuth);
app.use(routeHitMiddleware);
router.use(getApt);
app.use(`${baseUrl}/api/opendock`, router);

View File

@@ -1,5 +1,6 @@
import { type Express, Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import restart from "./prodSqlRestart.route.js";
import start from "./prodSqlStart.route.js";
import stop from "./prodSqlStop.route.js";
@@ -8,6 +9,7 @@ export const setupProdSqlRoutes = (baseUrl: string, app: Express) => {
// Apply auth to entire router
const router = Router();
router.use(requireAuth);
app.use(routeHitMiddleware);
router.use(start);
router.use(stop);

View File

@@ -18,6 +18,11 @@ import { setupSocketIORoutes } from "./socket.io/serverSetup.js";
import { serversChecks } from "./system/serverData.controller.js";
import { baseSettingValidationCheck } from "./system/settingsBase.controller.js";
import { startTCPServer } from "./tcpServer/tcp.server.js";
import {
aggregateRouteHitsForBusinessDay,
cleanupOldRouteHits,
runRouteHitAnalyticsCron,
} from "./utils/analyticRouteHits.utils.js";
import { createCronJob } from "./utils/croner.utils.js";
import { sendEmail } from "./utils/sendEmail.utils.js";
@@ -68,10 +73,16 @@ const start = async () => {
createCronJob("logsCleanup", "0 15 5 * * *", () => dbCleanup("logs", 120));
historicalSchedule();
createCronJob("aggregateHits", "0 0 7 * * *", async () =>
runRouteHitAnalyticsCron(),
);
createCronJob("cleanHitsUp", "0 0 7 * * *", () => cleanupOldRouteHits());
// one shots only needed to run on server startups
createNotifications();
startNotifications();
serversChecks();
aggregateRouteHitsForBusinessDay();
}, 5 * 1000);
process.on("uncaughtException", async (err) => {

View File

@@ -76,6 +76,16 @@ const newSettings: NewSetting[] = [
roles: ["admin"],
seedVersion: 1,
},
{
name: "mobile",
value: "0",
active: false,
description: "LST Android Mobile app",
moduleName: "mobile",
settingType: "feature",
roles: ["admin"],
seedVersion: 1,
},
// standard settings
{
@@ -304,6 +314,38 @@ const newSettings: NewSetting[] = [
roles: ["admin"],
seedVersion: 1,
},
{
name: "laneCheck",
value: "0",
active: false,
description:
"Allows the driver to scan a lane and see what is in the lane and details about each pallet.",
moduleName: "mobile",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "dockScan",
value: "0",
active: false,
description:
"Enables dock door scanning, must have a dock scanner setup for this to work.",
moduleName: "mobile",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "cycleCounting",
value: "0",
active: false,
description: "Enables a cycle count to be triggered from the scanner.",
moduleName: "mobile",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
];
export const baseSettingValidationCheck = async () => {

View File

@@ -1,5 +1,6 @@
import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import getServers from "./serverData.route.js";
import getSettings from "./settings.route.js";
import updSetting from "./settingsUpdate.route.js";
@@ -7,6 +8,7 @@ import stats from "./stats.route.js";
export const setupSystemRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(routeHitMiddleware);
app.use(`${baseUrl}/api/stats`, stats);
app.use(`${baseUrl}/api/settings`, getSettings);
app.use(`${baseUrl}/api/servers`, getServers);

View File

@@ -1,14 +1,21 @@
import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import restart from "./tcpRestart.route.js";
import start from "./tcpStart.route.js";
import stop from "./tcpStop.route.js";
export const setupTCPRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(`${baseUrl}/api/tcp/start`, requireAuth, start);
app.use(`${baseUrl}/api/tcp/stop`, requireAuth, stop);
app.use(`${baseUrl}/api/tcp/restart`, requireAuth, restart);
app.use(`${baseUrl}/api/tcp/start`, requireAuth, routeHitMiddleware, start);
app.use(`${baseUrl}/api/tcp/stop`, requireAuth, routeHitMiddleware, stop);
app.use(
`${baseUrl}/api/tcp/restart`,
requireAuth,
routeHitMiddleware,
restart,
);
// all other system should be under /api/system/*
};

View File

@@ -0,0 +1,141 @@
import { and, count, countDistinct, gte, lt, sql } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { analytics } from "../db/schema/analytics.schema.js";
import { analyticsDaily } from "../db/schema/dailyAnalytics.schema.js";
export const ignoredRoutePrefixes = [
"/health",
"/favicon.ico",
"/socket.io",
"/lst/api/ws",
"/lst-config.js",
];
export function shouldIgnoreRoute(path: string) {
return ignoredRoutePrefixes.some((prefix) => path.startsWith(prefix));
}
type CreateRouteHitInput = {
method: string;
routePattern: string;
actualPath: string;
statusCode: number;
durationMs: number;
module?: string | null;
userId?: string | null;
userEmail?: string | null;
ipAddress?: string | null;
userAgent?: string | null;
};
export async function createRouteHit(input: CreateRouteHitInput) {
await db.insert(analytics).values(input);
}
function getPreviousBusinessDayWindow(date = new Date()) {
const end = new Date(date);
end.setHours(7, 0, 0, 0);
const start = new Date(end);
start.setDate(start.getDate() - 1);
const businessDate = start.toISOString().slice(0, 10);
return {
start,
end,
businessDate,
};
}
export async function runRouteHitAnalyticsCron(): Promise<void> {
const result = await aggregateRouteHitsForBusinessDay();
await cleanupOldRouteHits();
console.log("Route hit analytics aggregated", result);
}
export async function aggregateRouteHitsForBusinessDay() {
const { start, end, businessDate } = getPreviousBusinessDayWindow();
const rows = await db
.select({
businessDate: sql<string>`${businessDate}`,
method: analytics.method,
routePattern: analytics.routePattern,
module: sql<string>`COALESCE(${analytics.module}, 'unknown')`,
totalHits: count(),
uniqueUsers: countDistinct(analytics.userId),
successCount: sql<number>`
COUNT(*) FILTER (WHERE ${analytics.statusCode} < 400)
`,
errorCount: sql<number>`
COUNT(*) FILTER (WHERE ${analytics.statusCode} >= 400)
`,
avgDurationMs: sql<number>`
COALESCE(ROUND(AVG(${analytics.durationMs})), 0)
`,
maxDurationMs: sql<number>`
COALESCE(MAX(${analytics.durationMs}), 0)
`,
firstHitAt: sql<Date>`
COALESCE(MIN(${analytics.createdAt}), NOW())
`,
lastHitAt: sql<Date>`
COALESCE(MAX(${analytics.createdAt}), NOW())
`,
})
.from(analytics)
.where(and(gte(analytics.createdAt, start), lt(analytics.createdAt, end)))
.groupBy(
analytics.method,
analytics.routePattern,
sql`COALESCE(${analytics.module}, 'unknown')`,
);
if (rows.length === 0) {
return {
businessDate,
inserted: 0,
};
}
await db
.insert(analyticsDaily)
.values(rows)
.onConflictDoUpdate({
target: [
analyticsDaily.businessDate,
analyticsDaily.method,
analyticsDaily.routePattern,
analyticsDaily.module,
],
set: {
totalHits: sql`excluded.total_hits`,
uniqueUsers: sql`excluded.unique_users`,
successCount: sql`excluded.success_count`,
errorCount: sql`excluded.error_count`,
avgDurationMs: sql`excluded.avg_duration_ms`,
maxDurationMs: sql`excluded.max_duration_ms`,
firstHitAt: sql`excluded.first_hit_at`,
lastHitAt: sql`excluded.last_hit_at`,
updatedAt: sql`now()`,
},
});
return {
businessDate,
inserted: rows.length,
};
}
export async function cleanupOldRouteHits() {
await db
.delete(analytics)
.where(lt(analytics.createdAt, sql`now() - interval '4 days'`));
}

View File

@@ -0,0 +1,61 @@
import { isUmamiEnabled, umamiConfig } from "../configs/umami.config.js";
type TrackLstEventInput = {
eventName: string;
eventData?: Record<string, unknown>;
url?: string;
hostname?: string;
};
export async function trackLstEvent({
eventName,
eventData,
url = "/backend",
hostname = umamiConfig.server,
}: TrackLstEventInput): Promise<void> {
if (!isUmamiEnabled()) return;
try {
await fetch(`${umamiConfig.umamiHost}/api/send`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": "LST-Backend",
},
body: JSON.stringify({
type: "event",
payload: {
website: umamiConfig.umamiWebsiteId,
name: eventName,
url,
hostname,
language: "en-US",
screen: "backend",
data: {
app: umamiConfig.appName,
site: umamiConfig.site,
server: umamiConfig.server,
appVersion: umamiConfig.appVersion,
source: "backend",
...eventData,
},
},
}),
});
} catch (err) {
console.error("Failed to send Umami backend event", err);
}
}
/*
await trackLstEvent({
eventName: "label_print_completed",
url: "/backend/printers",
eventData: {
module: "printers",
printerName,
labelType,
},
});
*/

View File

@@ -1,7 +1,9 @@
import type { Express } from "express";
import { routeHitMiddleware } from "../middleware/routeHit.middleware.js";
import getActiveJobs from "./cronerActiveJobs.route.js";
import jobStatusChange from "./cronerStatusChange.route.js";
export const setupUtilsRoutes = (baseUrl: string, app: Express) => {
app.use(routeHitMiddleware);
app.use(`${baseUrl}/api/utils/croner`, getActiveJobs);
app.use(`${baseUrl}/api/utils/croner`, jobStatusChange);
};

View File

@@ -7,7 +7,15 @@
<title>Logistics Support Tool</title>
</head>
<body>
<script>
const configScript = document.createElement("script");
configScript.src = `${window.location.origin}/lst/api/lst-config.js`;
configScript.defer = false;
document.head.appendChild(configScript);
</script>
<div id="root"></div>
<script defer src="https://stats.tuffraid.net/script.js" data-website-id="49bc2489-3930-4358-a13d-1cc609336572"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,25 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import axios from "axios";
export function getScanUsers() {
return queryOptions({
queryKey: ["getScanUsers"],
queryFn: () => fetch(),
staleTime: 5000,
refetchOnWindowFocus: true,
placeholderData: keepPreviousData,
});
}
const fetch = async () => {
if (window.location.hostname === "localhost") {
await new Promise((res) => setTimeout(res, 1500));
}
const { data } = await axios.get("/lst/api/mobile/auth/user", {
withCredentials: true,
timeout: 5000,
});
return data.data;
};

View File

@@ -0,0 +1,65 @@
type RuntimeConfig = {
appName: string;
site: string;
server: string;
appVersion: string;
umamiHost: string;
umamiWebsiteId: string;
};
declare global {
interface Window {
LST_CONFIG?: RuntimeConfig;
umami?: {
track: (eventName: string, eventData?: Record<string, unknown>) => void;
};
}
}
export const runtimeConfig: RuntimeConfig = {
appName: window.LST_CONFIG?.appName ?? "LST",
site: window.LST_CONFIG?.site ?? "unknown",
server: window.LST_CONFIG?.server ?? "unknown",
appVersion: window.LST_CONFIG?.appVersion ?? "dev",
umamiHost: window.LST_CONFIG?.umamiHost ?? "",
umamiWebsiteId: window.LST_CONFIG?.umamiWebsiteId ?? "",
};
export function loadUmami() {
if (!runtimeConfig.umamiHost) return;
if (!runtimeConfig.umamiWebsiteId) return;
if (document.querySelector("script[data-website-id]")) return;
const script = document.createElement("script");
script.defer = true;
script.src = `${runtimeConfig.umamiHost}/script.js`;
script.setAttribute("data-website-id", runtimeConfig.umamiWebsiteId);
document.head.appendChild(script);
}
export function trackLstEvent(
eventName: string,
eventData?: Record<string, unknown>,
) {
window.umami?.track(eventName, {
app: runtimeConfig.appName,
site: runtimeConfig.site,
server: runtimeConfig.server,
appVersion: runtimeConfig.appVersion,
...eventData,
});
}
/*
event type
trackLstEvent("exampleClick", {
module: "example",
action: "test_click",
label: "Example Button",
page: window.location.pathname,
});
*/

View File

@@ -4,6 +4,7 @@ import ReactDOM from "react-dom/client";
import "./index.css";
import { createRouter, RouterProvider } from "@tanstack/react-router";
import socket from "./lib/socket.io";
import { loadUmami } from "./lib/umami.utils";
// Import the generated route tree
import { routeTree } from "./routeTree.gen";
@@ -38,6 +39,7 @@ declare module "@tanstack/react-router" {
// Render the app
const rootElement = document.getElementById("root")!;
if (!rootElement.innerHTML) {
loadUmami();
const root = ReactDOM.createRoot(rootElement);
root.render(
<StrictMode>

View File

@@ -1,9 +1,16 @@
import { createFileRoute } from '@tanstack/react-router'
import { useSuspenseQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import { getScanUsers } from "../../lib/queries/getScanUsers";
export const Route = createFileRoute('/admin/scanUsers')({
component: RouteComponent,
})
export const Route = createFileRoute("/admin/scanUsers")({
component: RouteComponent,
});
const ScanUserTable = () => {
const { data, refetch } = useSuspenseQuery(getScanUsers());
console.log(data);
return <div>Hello "/admin/scanUsers"!</div>;
};
function RouteComponent() {
return <div>Hello "/admin/scanUsers"!</div>
return <ScanUserTable />;
}

View File

@@ -163,7 +163,6 @@ function RouteComponent() {
const columnHelper = createColumnHelper<any>();
console.log(window.location);
const logColumns = [
columnHelper.accessor("timestamp", {
header: ({ column }) => (

View File

@@ -3,6 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import z from "zod";
import { useSession } from "../lib/auth-client";
import { trackLstEvent } from "../lib/umami.utils";
export const Route = createFileRoute("/")({
validateSearch: z.object({
@@ -27,6 +28,16 @@ function Index() {
url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ";
}
//test tracking
const click = () => {
trackLstEvent("silly_click", {
module: "silly",
action: "click",
label: "rick rolled",
page: window.location.pathname,
});
};
return (
<div className="flex justify-center m-10 flex-col">
<h3 className="w-2xl text-3xl">Welcome Lst - V3</h3>
@@ -43,16 +54,18 @@ function Index() {
<b>
<strong>Click</strong>
</b>
</a>
<a
href={`https://www.youtube.com/watch?v=dQw4w9WgXcQ`}
target="_blank"
rel="noopener"
>
<b>
<strong> Here</strong>
</b>
</a>
</a>{" "}
<button onClick={click}>
<a
href={`https://www.youtube.com/watch?v=dQw4w9WgXcQ`}
target="_blank"
rel="noopener"
>
<b>
<strong> Here</strong>
</b>
</a>
</button>
</p>
</div>
);

View File

@@ -15,7 +15,7 @@
"foregroundImage": "./assets/adaptive-icon-white.png",
"backgroundColor": "#ffffff"
},
"versionCode": 31,
"versionCode": 32,
"minSupportedVersionCode": 26,
"predictiveBackGestureEnabled": false,
"package": "net.alpla.lst.mobile"

View File

@@ -1,5 +1,12 @@
import { Redirect, Tabs } from "expo-router";
import { Container, Home, Logs, Rows4, Settings } from "lucide-react-native";
import {
Boxes,
Container,
Home,
Logs,
Rows4,
Settings,
} from "lucide-react-native";
import { useAppStore } from "../../hooks/useAppStore";
import { useMobileAuthStore } from "../../hooks/useMobileAuth";
@@ -51,11 +58,18 @@ export default function TabsLayout() {
// },
}}
/>
<Tabs.Screen
name="ppoo"
options={{
title: "PPOO",
href: isNormalScanner ? null : "/(tabs)/ppoo",
tabBarIcon: ({ color, size }) => <Boxes size={size} color={color} />,
}}
/>
<Tabs.Screen
name="laneCheck"
options={{
title: "Lane Check",
href: isNormalScanner ? null : "/(tabs)/laneCheck",
tabBarIcon: ({ color, size }) => <Rows4 size={size} color={color} />,
}}

View File

@@ -0,0 +1,18 @@
import React from "react";
import { Text, View } from "react-native";
import { Button } from "../../components/ui/button";
export default function PPOO() {
return (
<View
style={{
flex: 1,
//justifyContent: "center",
alignItems: "center",
marginTop: 50,
}}
>
<Text>Ppo checks</Text>
</View>
);
}

View File

@@ -28,7 +28,7 @@ export function GlobalFooter() {
{!hasUpdate && shouldUpdate && (
<View className="bg-[#FDBA74]">
<Link href={"/updateScreen"}>
<Text className="h-[32] font-medium text-lg text-wrap text-center">
<Text className="h-[16] font-medium text-base text-wrap text-center">
There is an update click me for instructions
</Text>
</Link>

View File

@@ -0,0 +1,14 @@
CREATE TABLE "analytics" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"method" text NOT NULL,
"route_pattern" text NOT NULL,
"actual_path" text NOT NULL,
"status_code" integer NOT NULL,
"duration_ms" integer NOT NULL,
"module" text,
"user_id" text,
"user_email" text,
"ip_address" text,
"user_agent" text
);

View File

@@ -0,0 +1 @@
ALTER TABLE "scan_log" RENAME COLUMN "add_Date" TO "add_date";

View File

@@ -0,0 +1,17 @@
CREATE TABLE "analytics_daily" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"business_date" date NOT NULL,
"method" text NOT NULL,
"route_pattern" text NOT NULL,
"module" text NOT NULL,
"total_hits" integer NOT NULL,
"unique_users" integer NOT NULL,
"success_count" integer NOT NULL,
"error_count" integer NOT NULL,
"avg_duration_ms" integer NOT NULL,
"max_duration_ms" integer NOT NULL,
"first_hit_at" timestamp NOT NULL,
"last_hit_at" timestamp NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -337,6 +337,27 @@
"when": 1778068577325,
"tag": "0047_spotty_queen_noir",
"breakpoints": true
},
{
"idx": 48,
"version": "7",
"when": 1778165976086,
"tag": "0048_little_amazoness",
"breakpoints": true
},
{
"idx": 49,
"version": "7",
"when": 1778166074209,
"tag": "0049_futuristic_silk_fever",
"breakpoints": true
},
{
"idx": 50,
"version": "7",
"when": 1778169641819,
"tag": "0050_concerned_vivisector",
"breakpoints": true
}
]
}

View File

@@ -106,6 +106,8 @@ function Update-Server {
"CLIENT_SECRET" = "zsJeyjMN2yDDqfyzSsh96OtlA2714F5d"
"CLIENT_SCOPES" = "openid profile email groups"
"DISCOVERY_URL" = "https://auth.tuffraid.net/oidc/.well-known/openid-configuration"
"UMAMI_HOST" = "https://stats.tuffraid.net"
"UMAMI_WEBSITE_ID" = "49bc2489-3930-4358-a13d-1cc609336572"
}
$linesToAppend = @()