33 Commits

Author SHA1 Message Date
f5bae2c0c2 fix(anaylistics): changes to the daily section so it populates correctly now
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 1m9s
2026-05-11 15:41:20 -05:00
05758791be fix(scanner): fixes to be more clear that you need to scan a command to start
closes #16
2026-05-11 15:40:49 -05:00
51026e3e2c ci(notification): removal of more console logs that shouldnt be here 2026-05-11 15:38:44 -05:00
9631736e26 chore(mobile): removed console log that shouldnt be there 2026-05-11 15:38:04 -05:00
ce9d8eaaf5 feat(scan users): added in the place to add the new scanner users in 2026-05-11 15:37:38 -05:00
1bbf5c2a49 fix(table): skelly table causing hydration error 2026-05-11 15:35:46 -05:00
13718fe702 fix(anaylitics): unique values were missing causing a weird crash 2026-05-11 14:00:54 -05:00
0de2579942 fix(scanner): changed to not crash on logging
cloases #19
2026-05-11 13:35:07 -05:00
7c31b43a4a fix(app): emit.maxlistener issue
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m27s
BREAKING CHANGE: moved teh middleware to call the api hits to the main app and removed from
everywhere else

closes #18
2026-05-11 13:25:43 -05:00
85e96f5ed1 fix(scanner): logut out corrections
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m24s
refs #17
2026-05-11 07:59:17 -05:00
6b515c608f chore(release): 0.0.2-alpha.10
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m12s
Release and Build Image / release (push) Successful in 16s
2026-05-08 15:09:49 -05:00
d8869b103b fix(scan user): typo 2026-05-08 15:08:33 -05:00
1dba774abc chore(server): removed a console log that shouldnt be there
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 1m10s
2026-05-08 15:06:08 -05:00
505d7cea5d refactor(scan): bump in build and style update 2026-05-08 15:05:47 -05:00
1ff5e5032f test(scanusers): added in scan users as test 2026-05-08 15:05:09 -05:00
5fa70da90c chore(file): name changes.. spelled wrong 2026-05-08 15:04:31 -05:00
0459cd788a fix(spelling): corrected the spelling on the file 2026-05-08 15:03:53 -05:00
7d7d991122 fix(schema): typo in add_date 2026-05-08 15:03:33 -05:00
2721bb2a3b feat(api hits): added in api hits for monitoring 2026-05-08 15:03:03 -05:00
4424c742d2 refactor(analyitics): finished analyitics as a base 2026-05-08 15:02:34 -05:00
6d8499bfb8 ci(templates): force useage 2026-05-08 15:01:44 -05:00
9edafc9d28 feat(analytics): added in backend anaylitics 2026-05-07 10:20:50 -05:00
e9b0101095 ci(template): bug in getting the template to work correctly
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m28s
2026-05-07 09:01:15 -05:00
ca885fb01a ci(templates): added in templates for the repo to make it more easy to manage and add in new ideas
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m24s
2026-05-07 08:50:06 -05:00
edb3668548 refactor(scanner): added toasts in to make it look better
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m25s
2026-05-06 19:42:52 -05:00
87803eed43 feat(scanner): added in lanechecks 2026-05-06 19:42:22 -05:00
e61038e004 chore(release): 0.0.2-alpha.9
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m42s
Release and Build Image / release (push) Successful in 28s
2026-05-06 13:34:30 -05:00
d99449ddc4 test(scanner): lane check
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m41s
2026-05-06 13:30:58 -05:00
3552ca31f9 build(builds): changed to ip as its on the same server
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 4s
2026-05-06 12:27:20 -05:00
b578f05d64 build(release): bypass cloudflare upload limit
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 3m43s
2026-05-06 12:17:42 -05:00
4ca74de279 refactor(mobile): valildation of server after each scan
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 3m40s
2026-05-06 12:10:14 -05:00
12412536d1 refactor(scanner): finished login stuff for current routes 2026-05-06 12:09:47 -05:00
a38e2e0339 refactor(scanner): added in running number 2026-05-06 12:09:09 -05:00
88 changed files with 16891 additions and 2688 deletions

View File

@@ -0,0 +1,66 @@
---
name: Bug Report
about: Report something that is broken or not working correctly
title: "[BUG] "
ref: "main"
labels:
- bug
---
# Summary
Briefly explain the issue.
---
# Steps To Reproduce
1. Go to ...
2. Click ...
3. Scan ...
4. Error occurs ...
---
# Expected Behavior
What should have happened?
---
# Actual Behavior
What actually happened instead?
---
# Severity
- [ ] Low
- [ ] Medium
- [ ] High
- [ ] Critical
---
# Environment
Example:
- Production
- Development
- Zebra Scanner
- Mobile Device
- Windows Server
- Docker
---
# Logs / Screenshots
Paste logs or upload screenshots here.
```txt
Paste logs here

View File

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

View File

@@ -0,0 +1,47 @@
---
name: Enhancement
about: Improve or refine an existing feature
title: "[ENHANCEMENT] "
ref: "main"
labels:
- enhancement
---
# Existing Feature
What current feature or workflow is being improved?
Example:
- Notifications
- Scanner Login
- Release Monitor
- Printing
- Auth
---
# Proposed Improvement
Describe the improvement.
---
# Expected Benefit
Why would this improvement help?
---
# Impact
- [ ] Small
- [ ] Medium
- [ ] Large
---
# Additional Notes
Anything else worth mentioning.

View File

@@ -0,0 +1,40 @@
---
name: Feature Request
about: Suggest a brand new feature or module
title: "[FEATURE] "
ref: "main"
labels:
- feature
---
# Problem Statement
What problem are you trying to solve?
---
# Proposed Solution
Describe the feature you would like added.
---
# Alternatives Considered
Any other ideas, workarounds, or approaches?
---
# Priority
- [ ] Nice to Have
- [ ] Medium Priority
- [ ] High Priority
- [ ] Critical
---
# Additional Context
Add mockups, screenshots, examples, or notes here.

View File

@@ -12,20 +12,20 @@ jobs:
steps: steps:
- name: Checkout (local) - name: Checkout (local)
run: | run: |
git clone https://git.tuffraid.net/cowch/lst_v3.git . git clone http://10.75.9.150:3100/cowch/lst_v3.git .
git checkout ${{ gitea.sha }} git checkout ${{ gitea.sha }}
- name: Login to registry - name: Login to registry
run: echo "${{ secrets.PASSWORD }}" | docker login git.tuffraid.net -u "cowch" --password-stdin run: echo "${{ secrets.PASSWORD }}" | docker login 10.75.9.150:3100 -u "cowch" --password-stdin
- name: Build image - name: Build image
run: | run: |
docker build \ docker build \
-t git.tuffraid.net/cowch/lst_v3:latest \ -t 10.75.9.150:3100/cowch/lst_v3:latest \
-t git.tuffraid.net/cowch/lst_v3:${{ gitea.sha }} \ -t 10.75.9.150:3100/cowch/lst_v3:${{ gitea.sha }} \
. .
- name: Push - name: Push
run: | run: |
docker push git.tuffraid.net/cowch/lst_v3:latest docker push 10.75.9.150:3100/cowch/lst_v3:latest
docker push git.tuffraid.net/cowch/lst_v3:${{ gitea.sha }} docker push 10.75.9.150:3100/cowch/lst_v3:${{ gitea.sha }}

View File

@@ -14,12 +14,12 @@ jobs:
# Examples: # Examples:
# http://gitea.internal.lan:3000 # http://gitea.internal.lan:3000
# https://gitea-origin.yourdomain.local # https://gitea-origin.yourdomain.local
GITEA_INTERNAL_URL: "https://git.tuffraid.net" GITEA_INTERNAL_URL: "http://10.75.9.150:3100" #"https://git.tuffraid.net"
# Internal/origin registry host. Usually same host as above, but without protocol. # Internal/origin registry host. Usually same host as above, but without protocol.
# Example: # Example:
# gitea.internal:3000 # gitea.internal:3000
REGISTRY_HOST: "git.tuffraid.net" REGISTRY_HOST: "10.75.9.150:3100" #"git.tuffraid.net"
steps: steps:
- name: Check out repository - name: Check out repository

1
.gitignore vendored
View File

@@ -11,6 +11,7 @@ node-v24.14.0-x64.msi
postgresql-17.9-2-windows-x64.exe postgresql-17.9-2-windows-x64.exe
VSCodeUserSetup-x64-1.112.0.exe VSCodeUserSetup-x64-1.112.0.exe
nssm.exe nssm.exe
frontend/.tanstack
# Logs # Logs
logs logs

View File

@@ -1,5 +1,66 @@
# All Changes to LST can be found below. # All Changes to LST can be found below.
## [0.0.2-alpha.10](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.9...v0.0.2-alpha.10) (2026-05-08)
### 🌟 Enhancements
* **analytics:** added in backend anaylitics ([9edafc9](https://git.tuffraid.net/cowch/lst_v3/commits/9edafc9d2810f339d197c10dfc6a037b3352d81f))
* **api hits:** added in api hits for monitoring ([2721bb2](https://git.tuffraid.net/cowch/lst_v3/commits/2721bb2a3bf1f829591d26a0716f74c4f7fc0c79))
* **scanner:** added in lanechecks ([87803ee](https://git.tuffraid.net/cowch/lst_v3/commits/87803eed43069b73de3f66e6524bb45da9c46334))
### 🐛 Bug fixes
* **scan user:** typo ([d8869b1](https://git.tuffraid.net/cowch/lst_v3/commits/d8869b103b80e4208b3928a370a9524ef33d25cd))
* **schema:** typo in add_date ([7d7d991](https://git.tuffraid.net/cowch/lst_v3/commits/7d7d9911223905d6767b87d2471b6607a90f1ea7))
* **spelling:** corrected the spelling on the file ([0459cd7](https://git.tuffraid.net/cowch/lst_v3/commits/0459cd788aaad6ac54a67e23f798ce5e5a437394))
### 📝 Chore
* **file:** name changes.. spelled wrong ([5fa70da](https://git.tuffraid.net/cowch/lst_v3/commits/5fa70da90ca290ee45088e9c8eb06ba48a6677af))
* **server:** removed a console log that shouldnt be there ([1dba774](https://git.tuffraid.net/cowch/lst_v3/commits/1dba774abc54bf20850c3f26d49926e86d59712d))
### 🛠️ Code Refactor
* **analyitics:** finished analyitics as a base ([4424c74](https://git.tuffraid.net/cowch/lst_v3/commits/4424c742d24dc230b2bc1782e33535184c378cf0))
* **scan:** bump in build and style update ([505d7ce](https://git.tuffraid.net/cowch/lst_v3/commits/505d7cea5d2f52fc4a3ec1edff1878be703c4034))
* **scanner:** added toasts in to make it look better ([edb3668](https://git.tuffraid.net/cowch/lst_v3/commits/edb366854825f4c24ab5d77cf88759465d067f00))
### 📝 Testing Code
* **scanusers:** added in scan users as test ([1ff5e50](https://git.tuffraid.net/cowch/lst_v3/commits/1ff5e5032f9c8bf81f972dc99d6c86ba8d3936c6))
### 📈 Project changes
* **template:** bug in getting the template to work correctly ([e9b0101](https://git.tuffraid.net/cowch/lst_v3/commits/e9b01010954624aed738cd6e4b82fccbba195cc4))
* **templates:** added in templates for the repo to make it more easy to manage and add in new ideas ([ca885fb](https://git.tuffraid.net/cowch/lst_v3/commits/ca885fb01a3c8bc22694c2e05269c43fcd4de70e))
* **templates:** force useage ([6d8499b](https://git.tuffraid.net/cowch/lst_v3/commits/6d8499bfb85f7b9131b1ec7b31a17c4256d0f0cf))
## [0.0.2-alpha.9](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.8...v0.0.2-alpha.9) (2026-05-06)
### 🛠️ Code Refactor
* **mobile:** valildation of server after each scan ([4ca74de](https://git.tuffraid.net/cowch/lst_v3/commits/4ca74de2795cea7244e38697d16afe2822164ed6))
* **scanner:** added in running number ([a38e2e0](https://git.tuffraid.net/cowch/lst_v3/commits/a38e2e033977b725538e9a9046098d94194d549e))
* **scanner:** finished login stuff for current routes ([1241253](https://git.tuffraid.net/cowch/lst_v3/commits/12412536d10981013053c39d156c6c9cb0babd11))
### 📝 Testing Code
* **scanner:** lane check ([d99449d](https://git.tuffraid.net/cowch/lst_v3/commits/d99449ddc4e2777c1b0fe9189ba0a7c01fe1dd8f))
### 📈 Project Builds
* **builds:** changed to ip as its on the same server ([3552ca3](https://git.tuffraid.net/cowch/lst_v3/commits/3552ca31f9f7b3bcbe557a145e7eb154bfdae79c))
* **release:** bypass cloudflare upload limit ([b578f05](https://git.tuffraid.net/cowch/lst_v3/commits/b578f05d6482f9b6f30febeee6ab0b708a70f68b))
## [0.0.2-alpha.8](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.7...v0.0.2-alpha.8) (2026-05-06) ## [0.0.2-alpha.8](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.7...v0.0.2-alpha.8) (2026-05-06)

View File

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

View File

@@ -3,7 +3,9 @@ import { fileURLToPath } from "node:url";
import { toNodeHandler } from "better-auth/node"; import { toNodeHandler } from "better-auth/node";
import express from "express"; import express from "express";
import morgan from "morgan"; import morgan from "morgan";
import { umamiConfig } from "./configs/umami.config.js";
import { createLogger } from "./logger/logger.controller.js"; import { createLogger } from "./logger/logger.controller.js";
import { routeHitMiddleware } from "./middleware/routeHit.middleware.js";
import { setupRoutes } from "./routeHandler.routes.js"; import { setupRoutes } from "./routeHandler.routes.js";
import { auth } from "./utils/auth.utils.js"; import { auth } from "./utils/auth.utils.js";
import { lstCors } from "./utils/cors.utils.js"; import { lstCors } from "./utils/cors.utils.js";
@@ -29,10 +31,27 @@ const createApp = async () => {
app.use(morgan("dev")); app.use(morgan("dev"));
app.set("trust proxy", true); app.set("trust proxy", true);
app.use(lstCors()); app.use(lstCors());
app.use(routeHitMiddleware);
app.all(`${baseUrl}/api/auth/*splat`, toNodeHandler(auth)); app.all(`${baseUrl}/api/auth/*splat`, toNodeHandler(auth));
app.use(express.json()); app.use(express.json());
setupRoutes(baseUrl, app); 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( app.use(
`${baseUrl}/app`, `${baseUrl}/app`,
express.static(join(__dirname, "../frontend/dist")), express.static(join(__dirname, "../frontend/dist")),

View File

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

@@ -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,45 @@
import {
date,
integer,
pgTable,
text,
timestamp,
unique,
uuid,
} from "drizzle-orm/pg-core";
export const analyticsDaily = pgTable(
"analytics_daily",
{
id: uuid("id").defaultRandom().primaryKey(),
businessDate: date("business_date", { mode: "string" }).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(),
},
(table) => [
unique("analytics_daily_business_route_unique").on(
table.businessDate,
table.method,
table.routePattern,
table.module,
),
],
);

View File

@@ -9,9 +9,10 @@ export const scanLog = pgTable("scan_log", {
message: text("message").notNull(), message: text("message").notNull(),
prompt: text("prompt"), prompt: text("prompt"),
commandDescription: text("command_description"), commandDescription: text("command_description"),
runningNumber: text("running_number").default("0"),
status: text("status"), status: text("status"),
lines: jsonb("lines").default([]), lines: jsonb("lines").default([]),
add_Date: timestamp("add_Date").defaultNow(), add_Date: timestamp("add_date").defaultNow(),
}); });
export const scanLogSchema = createSelectSchema(scanLog); export const scanLogSchema = createSelectSchema(scanLog);

View File

@@ -1,5 +1,6 @@
import { type Express, Router } from "express"; import { type Express, Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js"; import { requireAuth } from "../middleware/auth.middleware.js";
import restart from "./gpSqlRestart.route.js"; import restart from "./gpSqlRestart.route.js";
import start from "./gpSqlStart.route.js"; import start from "./gpSqlStart.route.js";
import stop from "./gpSqlStop.route.js"; import stop from "./gpSqlStop.route.js";

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

@@ -0,0 +1,54 @@
import { eq } from "drizzle-orm";
import { Router } from "express";
import { db } from "../db/db.controller.js";
import { scanUser } from "../db/schema/scanUsers.js";
import { settings } from "../db/schema/settings.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const r = Router();
// scanners that are dedicated to specific users.
const SPECIAL_SCANNERS = [69, 98];
const buildAllowedScannerIds = (scannerCount: number) => {
const generatedIds = Array.from({ length: scannerCount }, (_, i) => i + 1);
return Array.from(new Set([...generatedIds, ...SPECIAL_SCANNERS])).sort(
(a, b) => a - b,
);
};
r.get("/", async (_, res) => {
// get the scan users and setting
const scanusers = await db.select().from(scanUser);
const scannerIdSetting = await db
.select()
.from(settings)
.where(eq(settings.name, "scannerIds"));
const usedScannerIds = scanusers.map((x) => Number(x.scannerId));
const allowedScannerIds = buildAllowedScannerIds(
Number(scannerIdSetting[0]?.value ?? 0),
);
const availableScannerIds = allowedScannerIds.filter(
(id) => !usedScannerIds.includes(id),
);
const data = availableScannerIds.map((id) => ({
label: `${id}`,
value: id,
}));
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "scanner",
message: `There are ${availableScannerIds.length} scanner id's`,
data,
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,34 @@
import { Router } from "express";
import { runProdApi } from "../utils/prodEndpoint.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const router = Router();
router.post("/", async (req, res) => {
const body = req.body;
const lane = body.lane.split("#");
console.log(lane[2]);
const laneData = await runProdApi({
method: "post",
endpoint: "/public/v1.1/Warehousing/GetWarehouseUnits",
data: [
{
laneIds: [lane[2]],
},
],
});
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "lane check",
message: `all data for lane Id: ${lane}`,
data: laneData?.data ?? [],
status: 200,
});
});
export default router;

View File

@@ -1,5 +1,7 @@
import type { Express } from "express"; import type { Express } from "express";
import downloads from "./donwloadApps.route.js"; import available from "./availableScanIds.route.js";
import downloads from "./downloadApps.route.js";
import lanes from "./laneCheck.js";
import authPin from "./mobileAuth.route.js"; import authPin from "./mobileAuth.route.js";
import newPin from "./mobilePin.route.js"; import newPin from "./mobilePin.route.js";
import logs from "./scanLogs.route.js"; import logs from "./scanLogs.route.js";
@@ -7,11 +9,14 @@ import version from "./version.route.js";
export const setupMobileRoutes = (baseUrl: string, app: Express) => { export const setupMobileRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this //stats will be like this as we dont need to change this
app.use(`${baseUrl}/api/mobile/version`, version); app.use(`${baseUrl}/api/mobile/version`, version);
app.use(`${baseUrl}/api/mobile/apk`, downloads); app.use(`${baseUrl}/api/mobile/apk`, downloads);
app.use(`${baseUrl}/api/mobile/logs`, logs); app.use(`${baseUrl}/api/mobile/logs`, logs);
app.use(`${baseUrl}/api/mobile/auth`, authPin); app.use(`${baseUrl}/api/mobile/auth`, authPin);
app.use(`${baseUrl}/api/mobile/pin`, newPin); app.use(`${baseUrl}/api/mobile/pin`, newPin);
app.use(`${baseUrl}/api/mobile/laneCheck`, lanes);
app.use(`${baseUrl}/api/mobile/available`, available);
// all other system should be under /api/system/* // all other system should be under /api/system/*
}; };

View File

@@ -162,6 +162,14 @@ r.post("/user", async (req, res) => {
r.get("/user", requireAuth, async (_, res) => { r.get("/user", requireAuth, async (_, res) => {
const { data, error } = await tryCatch(db.select().from(scanUser)); const { data, error } = await tryCatch(db.select().from(scanUser));
// await trackLstEvent({
// eventName: "mobile_get_users",
// url: "/mobile/users",
// eventData: {
// module: "mobile",
// },
// });
if (error) { if (error) {
return apiReturn(res, { return apiReturn(res, {
success: false, success: false,
@@ -263,6 +271,10 @@ r.patch("/user/:id", requireAuth, async (req, res) => {
updates.active = req.body.active; updates.active = req.body.active;
} }
if (req.body?.excludedCommand !== undefined) {
updates.excludedCommand = req.body.excludedCommand;
}
if (req.body?.role !== undefined) { if (req.body?.role !== undefined) {
updates.role = req.body.role; updates.role = req.body.role;
} }

View File

@@ -12,12 +12,14 @@ router.post("/", async (req, res) => {
const newLog = await db const newLog = await db
.insert(scanLog) .insert(scanLog)
.values({ .values({
scannerId: body.scannerId, scannerId: body.scannerId ?? "",
message: body.message, message: body.message ?? "",
prompt: body.prompt, prompt: body.prompt ?? "",
commandDescription: body.commandDescription, commandDescription: body.commandDescription ?? "",
status: body.status, status: body.status ?? "",
lines: body.lines, lines: body.lines ?? "",
user: body.user ?? "",
runningNumber: body.runningNumber ?? "",
}) })
.returning(); .returning();

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import { type Express, Router } from "express"; import { type Express, Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js"; import { requireAuth } from "../middleware/auth.middleware.js";
import { featureCheck } from "../middleware/featureActive.middleware.js"; import { featureCheck } from "../middleware/featureActive.middleware.js";
import listener from "./ocp.printer.listener.js"; import listener from "./ocp.printer.listener.js";
import update from "./ocp.printer.update.js"; import update from "./ocp.printer.update.js";

View File

@@ -1,6 +1,7 @@
import { type Express, Router } from "express"; import { type Express, Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js"; import { requireAuth } from "../middleware/auth.middleware.js";
import { featureCheck } from "../middleware/featureActive.middleware.js"; import { featureCheck } from "../middleware/featureActive.middleware.js";
import getApt from "./opendockGetRelease.route.js"; import getApt from "./opendockGetRelease.route.js";
export const setupOpendockRoutes = (baseUrl: string, app: Express) => { export const setupOpendockRoutes = (baseUrl: string, app: Express) => {

View File

@@ -1,5 +1,6 @@
import { type Express, Router } from "express"; import { type Express, Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js"; import { requireAuth } from "../middleware/auth.middleware.js";
import restart from "./prodSqlRestart.route.js"; import restart from "./prodSqlRestart.route.js";
import start from "./prodSqlStart.route.js"; import start from "./prodSqlStart.route.js";
import stop from "./prodSqlStop.route.js"; import stop from "./prodSqlStop.route.js";

View File

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

View File

@@ -76,6 +76,16 @@ const newSettings: NewSetting[] = [
roles: ["admin"], roles: ["admin"],
seedVersion: 1, seedVersion: 1,
}, },
{
name: "mobile",
value: "0",
active: false,
description: "LST Android Mobile app",
moduleName: "mobile",
settingType: "feature",
roles: ["admin"],
seedVersion: 1,
},
// standard settings // standard settings
{ {
@@ -304,6 +314,49 @@ const newSettings: NewSetting[] = [
roles: ["admin"], roles: ["admin"],
seedVersion: 1, 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,
},
{
name: "scannerIds",
value: "10",
active: false,
description:
"How many scanners ids are setup for this, there should be a lst_scanner instance created.",
moduleName: "mobile",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
]; ];
export const baseSettingValidationCheck = async () => { export const baseSettingValidationCheck = async () => {

View File

@@ -1,5 +1,6 @@
import type { Express } from "express"; import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js"; import { requireAuth } from "../middleware/auth.middleware.js";
import getServers from "./serverData.route.js"; import getServers from "./serverData.route.js";
import getSettings from "./settings.route.js"; import getSettings from "./settings.route.js";
import updSetting from "./settingsUpdate.route.js"; import updSetting from "./settingsUpdate.route.js";

View File

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

View File

@@ -0,0 +1,148 @@
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>`CAST(${businessDate} AS date)`,
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,
};
}
const values = rows.map((row) => ({
...row,
businessDate: row.businessDate,
firstHitAt: new Date(row.firstHitAt),
lastHitAt: new Date(row.lastHitAt),
}));
await db
.insert(analyticsDaily)
.values(values)
.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

@@ -10,7 +10,7 @@ export async function generateUniquePin() {
const pin = generateSixDigitPin(); const pin = generateSixDigitPin();
const existing = await db.query.scanUser.findFirst({ const existing = await db.query.scanUser.findFirst({
where: (u, { eq }) => eq(u.pinHash, pin), // ⚠️ we'll fix this below where: (u, { eq }) => eq(u.pinHash, pin),
}); });
if (!existing) if (!existing)
@@ -37,3 +37,13 @@ export async function generateUniquePin() {
room: "", room: "",
}); });
} }
// export const pinExists = async (pin: string | number) => {
// const existing = await db.query.scanUser.findFirst({
// where: (u, { eq }) => eq(u.pinHash, pin),
// });
// if (!existing) return true;
// return false;
// };

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,4 +1,5 @@
import type { Express } from "express"; import type { Express } from "express";
import getActiveJobs from "./cronerActiveJobs.route.js"; import getActiveJobs from "./cronerActiveJobs.route.js";
import jobStatusChange from "./cronerStatusChange.route.js"; import jobStatusChange from "./cronerStatusChange.route.js";
export const setupUtilsRoutes = (baseUrl: string, app: Express) => { export const setupUtilsRoutes = (baseUrl: string, app: Express) => {

View File

@@ -7,7 +7,15 @@
<title>Logistics Support Tool</title> <title>Logistics Support Tool</title>
</head> </head>
<body> <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> <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> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

View File

@@ -68,7 +68,7 @@ export default function AdminSidebar({ session }: any) {
title: "Scan users", title: "Scan users",
url: "/admin/scanUsers", url: "/admin/scanUsers",
icon: UsersRound, icon: UsersRound,
role: ["systemAdmin", "admin"], role: ["systemAdmin", "admin", "manager"],
module: "admin", module: "admin",
active: true, active: true,
}, },
@@ -79,9 +79,9 @@ export default function AdminSidebar({ session }: any) {
<SidebarGroupContent> <SidebarGroupContent>
<SidebarMenu> <SidebarMenu>
{items.map((item) => ( {items.map((item) => (
<> <div key={item.title}>
{item.role.includes(session.user.role) && ( {item.role.includes(session.user.role) && (
<SidebarMenuItem key={item.title}> <SidebarMenuItem>
<SidebarMenuButton asChild> <SidebarMenuButton asChild>
<Link to={item.url} onClick={() => setOpen(false)}> <Link to={item.url} onClick={() => setOpen(false)}>
<item.icon /> <item.icon />
@@ -90,7 +90,7 @@ export default function AdminSidebar({ session }: any) {
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
)} )}
</> </div>
))} ))}
</SidebarMenu> </SidebarMenu>
</SidebarGroupContent> </SidebarGroupContent>

View File

@@ -25,8 +25,6 @@ export default function MobileBar({ session }: any) {
}, },
]; ];
console.log(session);
return ( return (
<SidebarGroup> <SidebarGroup>
<SidebarGroupLabel>Mobile</SidebarGroupLabel> <SidebarGroupLabel>Mobile</SidebarGroupLabel>

View File

@@ -27,7 +27,8 @@ export function AppSidebar() {
<MobileBar session={session} /> <MobileBar session={session} />
{session && {session &&
(session.user.role === "admin" || (session.user.role === "admin" ||
session.user.role === "systemAdmin") && ( session.user.role === "systemAdmin" ||
session.user.role === "manager") && (
<AdminSidebar session={session} /> <AdminSidebar session={session} />
)} )}
</SidebarContent> </SidebarContent>

View File

@@ -1,64 +1,67 @@
import { cva, type VariantProps } from "class-variance-authority"; import * as React from "react"
import { Slot } from "radix-ui"; import { cva, type VariantProps } from "class-variance-authority"
import type * as React from "react"; import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
destructive: outline:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
outline: secondary:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
secondary: ghost:
"bg-secondary text-secondary-foreground hover:bg-secondary/80", "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
ghost: destructive:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3", default:
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4", sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
icon: "size-9", lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", icon: "size-8",
"icon-sm": "size-8", "icon-xs":
"icon-lg": "size-10", "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
}, "icon-sm":
}, "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
defaultVariants: { "icon-lg": "size-9",
variant: "default", },
size: "default", },
}, defaultVariants: {
}, variant: "default",
); size: "default",
},
}
)
function Button({ function Button({
className, className,
variant = "default", variant = "default",
size = "default", size = "default",
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<"button"> & }: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & { VariantProps<typeof buttonVariants> & {
asChild?: boolean; asChild?: boolean
}) { }) {
const Comp = asChild ? Slot.Root : "button"; const Comp = asChild ? Slot.Root : "button"
return ( return (
<Comp <Comp
data-slot="button" data-slot="button"
data-variant={variant} data-variant={variant}
data-size={size} data-size={size}
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
{...props} {...props}
/> />
); )
} }
export { Button, buttonVariants }; export { Button, buttonVariants }

View File

@@ -0,0 +1,166 @@
import * as React from "react"
import { Dialog as DialogPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { XIcon } from "lucide-react"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close data-slot="dialog-close" asChild>
<Button
variant="ghost"
className="absolute top-2 right-2"
size="icon-sm"
>
<XIcon
/>
<span className="sr-only">Close</span>
</Button>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean
}) {
return (
<div
data-slot="dialog-footer"
className={cn(
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close asChild>
<Button variant="outline">Close</Button>
</DialogPrimitive.Close>
)}
</div>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn(
"text-base leading-none font-medium",
className
)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn(
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className
)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

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,25 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import axios from "axios";
export function getScannerIds() {
return queryOptions({
queryKey: ["getScannerIds"],
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/available", {
withCredentials: true,
timeout: 5000,
});
return data.data;
};

View File

@@ -17,11 +17,13 @@ export default function SkellyTable({ rows = 5, columns = 4 }: TableSkelly) {
<div className="rounded-md border"> <div className="rounded-md border">
<Table> <Table>
<TableHeader> <TableHeader>
{Array.from({ length: columns }).map((_, i) => ( <TableRow>
<TableHead key={i}> {Array.from({ length: columns }).map((_, i) => (
<Skeleton className="h-4 w-[80px]" /> <TableHead key={i}>
</TableHead> <Skeleton className="h-4 w-[80px]" />
))} </TableHead>
))}
</TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{Array.from({ length: rows }).map((_, r) => ( {Array.from({ length: rows }).map((_, r) => (

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

View File

@@ -13,6 +13,7 @@ import { Route as AboutRouteImport } from './routes/about'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as DocsIndexRouteImport } from './routes/docs/index' import { Route as DocsIndexRouteImport } from './routes/docs/index'
import { Route as DocsSplatRouteImport } from './routes/docs/$' import { Route as DocsSplatRouteImport } from './routes/docs/$'
import { Route as AdminUsersRouteImport } from './routes/admin/users'
import { Route as AdminSettingsRouteImport } from './routes/admin/settings' import { Route as AdminSettingsRouteImport } from './routes/admin/settings'
import { Route as AdminServersRouteImport } from './routes/admin/servers' import { Route as AdminServersRouteImport } from './routes/admin/servers'
import { Route as AdminScanUsersRouteImport } from './routes/admin/scanUsers' import { Route as AdminScanUsersRouteImport } from './routes/admin/scanUsers'
@@ -43,6 +44,11 @@ const DocsSplatRoute = DocsSplatRouteImport.update({
path: '/docs/$', path: '/docs/$',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const AdminUsersRoute = AdminUsersRouteImport.update({
id: '/admin/users',
path: '/admin/users',
getParentRoute: () => rootRouteImport,
} as any)
const AdminSettingsRoute = AdminSettingsRouteImport.update({ const AdminSettingsRoute = AdminSettingsRouteImport.update({
id: '/admin/settings', id: '/admin/settings',
path: '/admin/settings', path: '/admin/settings',
@@ -98,6 +104,7 @@ export interface FileRoutesByFullPath {
'/admin/scanUsers': typeof AdminScanUsersRoute '/admin/scanUsers': typeof AdminScanUsersRoute
'/admin/servers': typeof AdminServersRoute '/admin/servers': typeof AdminServersRoute
'/admin/settings': typeof AdminSettingsRoute '/admin/settings': typeof AdminSettingsRoute
'/admin/users': typeof AdminUsersRoute
'/docs/$': typeof DocsSplatRoute '/docs/$': typeof DocsSplatRoute
'/docs/': typeof DocsIndexRoute '/docs/': typeof DocsIndexRoute
'/user/profile': typeof authUserProfileRoute '/user/profile': typeof authUserProfileRoute
@@ -113,6 +120,7 @@ export interface FileRoutesByTo {
'/admin/scanUsers': typeof AdminScanUsersRoute '/admin/scanUsers': typeof AdminScanUsersRoute
'/admin/servers': typeof AdminServersRoute '/admin/servers': typeof AdminServersRoute
'/admin/settings': typeof AdminSettingsRoute '/admin/settings': typeof AdminSettingsRoute
'/admin/users': typeof AdminUsersRoute
'/docs/$': typeof DocsSplatRoute '/docs/$': typeof DocsSplatRoute
'/docs': typeof DocsIndexRoute '/docs': typeof DocsIndexRoute
'/user/profile': typeof authUserProfileRoute '/user/profile': typeof authUserProfileRoute
@@ -129,6 +137,7 @@ export interface FileRoutesById {
'/admin/scanUsers': typeof AdminScanUsersRoute '/admin/scanUsers': typeof AdminScanUsersRoute
'/admin/servers': typeof AdminServersRoute '/admin/servers': typeof AdminServersRoute
'/admin/settings': typeof AdminSettingsRoute '/admin/settings': typeof AdminSettingsRoute
'/admin/users': typeof AdminUsersRoute
'/docs/$': typeof DocsSplatRoute '/docs/$': typeof DocsSplatRoute
'/docs/': typeof DocsIndexRoute '/docs/': typeof DocsIndexRoute
'/(auth)/user/profile': typeof authUserProfileRoute '/(auth)/user/profile': typeof authUserProfileRoute
@@ -146,6 +155,7 @@ export interface FileRouteTypes {
| '/admin/scanUsers' | '/admin/scanUsers'
| '/admin/servers' | '/admin/servers'
| '/admin/settings' | '/admin/settings'
| '/admin/users'
| '/docs/$' | '/docs/$'
| '/docs/' | '/docs/'
| '/user/profile' | '/user/profile'
@@ -161,6 +171,7 @@ export interface FileRouteTypes {
| '/admin/scanUsers' | '/admin/scanUsers'
| '/admin/servers' | '/admin/servers'
| '/admin/settings' | '/admin/settings'
| '/admin/users'
| '/docs/$' | '/docs/$'
| '/docs' | '/docs'
| '/user/profile' | '/user/profile'
@@ -176,6 +187,7 @@ export interface FileRouteTypes {
| '/admin/scanUsers' | '/admin/scanUsers'
| '/admin/servers' | '/admin/servers'
| '/admin/settings' | '/admin/settings'
| '/admin/users'
| '/docs/$' | '/docs/$'
| '/docs/' | '/docs/'
| '/(auth)/user/profile' | '/(auth)/user/profile'
@@ -192,6 +204,7 @@ export interface RootRouteChildren {
AdminScanUsersRoute: typeof AdminScanUsersRoute AdminScanUsersRoute: typeof AdminScanUsersRoute
AdminServersRoute: typeof AdminServersRoute AdminServersRoute: typeof AdminServersRoute
AdminSettingsRoute: typeof AdminSettingsRoute AdminSettingsRoute: typeof AdminSettingsRoute
AdminUsersRoute: typeof AdminUsersRoute
DocsSplatRoute: typeof DocsSplatRoute DocsSplatRoute: typeof DocsSplatRoute
DocsIndexRoute: typeof DocsIndexRoute DocsIndexRoute: typeof DocsIndexRoute
authUserProfileRoute: typeof authUserProfileRoute authUserProfileRoute: typeof authUserProfileRoute
@@ -229,6 +242,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DocsSplatRouteImport preLoaderRoute: typeof DocsSplatRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/admin/users': {
id: '/admin/users'
path: '/admin/users'
fullPath: '/admin/users'
preLoaderRoute: typeof AdminUsersRouteImport
parentRoute: typeof rootRouteImport
}
'/admin/settings': { '/admin/settings': {
id: '/admin/settings' id: '/admin/settings'
path: '/admin/settings' path: '/admin/settings'
@@ -304,6 +324,7 @@ const rootRouteChildren: RootRouteChildren = {
AdminScanUsersRoute: AdminScanUsersRoute, AdminScanUsersRoute: AdminScanUsersRoute,
AdminServersRoute: AdminServersRoute, AdminServersRoute: AdminServersRoute,
AdminSettingsRoute: AdminSettingsRoute, AdminSettingsRoute: AdminSettingsRoute,
AdminUsersRoute: AdminUsersRoute,
DocsSplatRoute: DocsSplatRoute, DocsSplatRoute: DocsSplatRoute,
DocsIndexRoute: DocsIndexRoute, DocsIndexRoute: DocsIndexRoute,
authUserProfileRoute: authUserProfileRoute, authUserProfileRoute: authUserProfileRoute,

View File

@@ -51,6 +51,8 @@ export default function NotificationsSubCard({ user }: any) {
})); }));
} }
console.log(n);
return ( return (
<div> <div>
<Card className="p-3 w-lg"> <Card className="p-3 w-lg">

View File

@@ -0,0 +1,161 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import axios from "axios";
import { useState } from "react";
import { toast } from "sonner";
import { Button } from "../../../components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "../../../components/ui/dialog";
import { useAppForm } from "../../../lib/formSutff";
import { getScannerIds } from "../../../lib/queries/getScannerIds";
export default function NewScanUser({ refetch }: { refetch: any }) {
const [open, setOpen] = useState(false);
const { data, refetch: scannerFetch } = useSuspenseQuery(getScannerIds());
const form = useAppForm({
defaultValues: {
name: "",
scannerId: "",
pinNumber: "",
},
onSubmit: async ({ value }) => {
if (value.scannerId === "") {
toast.error(
"Scanner id is required please select a scanner id before submitting ",
);
return;
}
try {
const { data } = await axios.post(
"/lst/api/mobile/auth/user",
{
name: value.name,
pinNumber: value.pinNumber,
scannerId: value.scannerId,
},
{
withCredentials: true,
timeout: 15000,
validateStatus: () => true,
},
);
if (data.success) {
toast.success(
`${value.name}, was just created and can now log into the scanner with PIN: ${value.pinNumber}`,
);
form.reset();
setOpen(false);
refetch();
}
if (!data.success) {
toast.error(data.message);
return;
}
} catch (error) {
console.error(error);
}
},
});
const closeModel = (e: boolean) => {
setOpen(e);
if (!e) {
form.reset();
scannerFetch();
}
};
const openForm = () => {
setOpen(true);
scannerFetch();
};
let n: any = [];
if (data) {
n = data.map((i: any) => ({
label: i.label,
value: i.value.toString(),
}));
}
return (
<Dialog onOpenChange={(e) => closeModel(e)} open={open}>
<Button onClick={openForm}>Create new user</Button>
<DialogContent showCloseButton={false}>
<DialogHeader>
<DialogTitle>Create New Scan user.</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<div className="mb-2">
<form.AppField name="name">
{(field) => (
<field.InputField
label="Name"
inputType="text"
required={true}
/>
)}
</form.AppField>
</div>
<div className="w-32">
<form.AppField name="scannerId">
{(field) => (
<field.SelectField
label="Scanner Id"
placeholder="Select New scanner Id"
options={n}
/>
)}
</form.AppField>
</div>
<div className="flex flex-row">
<div>
<form.AppField name="pinNumber">
{(field) => (
<field.InputField
label="Pin Number"
inputType="number"
required={true}
/>
)}
</form.AppField>
</div>
<div className="mt-9 ml-2">
<Button
type="button"
onClick={async () => {
const { data } = await axios.get("/lst/api/mobile/pin/new");
form.setFieldValue("pinNumber", data.data[0].pin);
}}
>
New Pin
</Button>
</div>
</div>
<div className="flex justify-end mt-2 ">
<form.AppForm>
<form.SubmitButton>Submit</form.SubmitButton>
</form.AppForm>
</div>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,9 +1,258 @@
import { createFileRoute } from '@tanstack/react-router' import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { createColumnHelper } from "@tanstack/react-table";
import axios from "axios";
import { format } from "date-fns-tz";
import { CircleFadingArrowUp, Trash } from "lucide-react";
import { Suspense, useState } from "react";
import { toast } from "sonner";
import { Button } from "../../components/ui/button";
import { Spinner } from "../../components/ui/spinner";
import { authClient } from "../../lib/auth-client";
import { getScanUsers } from "../../lib/queries/getScanUsers";
import EditableCellInput from "../../lib/tableStuff/EditableCellInput";
import LstTable from "../../lib/tableStuff/LstTable";
import SearchableHeader from "../../lib/tableStuff/SearchableHeader";
import SkellyTable from "../../lib/tableStuff/SkellyTable";
import NewScanUser from "./-components/NewScanUser";
export const Route = createFileRoute('/admin/scanUsers')({ export const Route = createFileRoute("/admin/scanUsers")({
component: RouteComponent, beforeLoad: async ({ location }) => {
}) const { data: session } = await authClient.getSession();
const allowedRole = ["systemAdmin", "admin"];
if (!session?.user) {
throw redirect({
to: "/",
search: {
redirect: location.href,
},
});
}
if (!allowedRole.includes(session.user.role as string)) {
throw redirect({
to: "/",
});
}
return { user: session.user };
},
component: RouteComponent,
});
const updateSettings = async (
id: string,
data: Record<string, string | number | boolean | null>,
) => {
//console.log(id, data);
try {
const res = await axios.patch(`/lst/api/mobile/auth/user/${id}`, data, {
withCredentials: true,
timeout: 15000,
validateStatus: () => true,
});
toast.success(`User was just updated`);
return res;
} catch (err) {
toast.error("Error in updating the user");
return err;
}
};
const ScanUserTable = () => {
const { data, refetch } = useSuspenseQuery(getScanUsers());
const columnHelper = createColumnHelper<any>();
const updateSetting = useMutation({
mutationFn: ({
id,
field,
value,
}: {
id: string;
field: string;
value: string | number | boolean | null;
}) => updateSettings(id, { [field]: value }),
onSuccess: () => {
// refetch or update cache
refetch();
},
});
const columns = [
columnHelper.accessor("name", {
header: ({ column }) => (
<SearchableHeader column={column} title="Name" searchable={true} />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("scannerId", {
header: ({ column }) => (
<SearchableHeader
column={column}
title="Scanner ID"
searchable={false}
/>
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("pinNumber", {
header: ({ column }) => (
<SearchableHeader column={column} title="Pin Number" />
),
filterFn: "includesString",
cell: ({ row, getValue }) => (
<div className="flex flex-row gap-2">
<div>
<EditableCellInput
value={getValue()}
id={row.original.name}
field="value"
onSubmit={({ id, field, value }) => {
updateSetting.mutate({ id, field, value });
}}
/>
</div>
<div className="">
<Button
type="button"
onClick={async () => {
const { data } = await axios.get("/lst/api/mobile/pin/new");
updateSetting.mutate({
id: row.original.id,
field: "pinNumber",
value: data.data[0].pin,
});
}}
>
New Pin
</Button>
</div>
</div>
),
}),
columnHelper.accessor("lastScan", {
header: ({ column }) => (
<SearchableHeader column={column} title="Last Scan" />
),
cell: (i) => <span>{format(i.getValue(), "M/d/yyyy HH:mm")}</span>,
}),
columnHelper.accessor("excludedCommand", {
header: ({ column }) => (
<SearchableHeader column={column} title="Command id's Not Allowed" />
),
cell: (i) => {
const commands = i.getValue().join();
return (
<span>{commands === "" ? "All commands allowed" : commands}</span>
);
},
}),
columnHelper.accessor("deleteUser", {
header: ({ column }) => (
<SearchableHeader
column={column}
title="Delete User"
searchable={false}
/>
),
filterFn: "includesString",
cell: (i) => {
// biome-ignore lint: just removing the lint for now to get this going will maybe fix later
const [activeToggle, setActiveToggle] = useState(false);
const onTrigger = async () => {
setActiveToggle(true);
try {
const res = await axios.delete(
`/lst/api/mobile/auth/user/${i.row.original.id}`,
{
withCredentials: true,
timeout: 5000,
validateStatus: () => true,
},
);
if (res.data.success) {
toast.success(`${i.row.original.name} was deleted.`);
refetch();
setActiveToggle(false);
}
if (!res.data.success) {
toast.error(
`${i.row.original.name} encountered an error when trying to delete: ${res.data.message}`,
);
refetch();
setActiveToggle(false);
}
} catch (error) {
setActiveToggle(false);
console.error(error);
}
};
return (
<div>
<div className="flex items-center space-x-2">
<Button
variant="destructive"
disabled={activeToggle}
onClick={onTrigger}
>
{activeToggle ? (
<span>
<Spinner />
</span>
) : (
<span>
<Trash />
</span>
)}
</Button>
</div>
</div>
);
},
}),
];
return (
<div>
<div className="flex justify-end m-2">
<Suspense
fallback={
<div>
<p>Loading...</p>
</div>
}
>
<NewScanUser refetch={refetch} />
</Suspense>
</div>
<div>
<LstTable data={data} columns={columns} pageSize={50} />
</div>
</div>
);
};
// const NewUserForm = ()=>{
// const { data, refetch } = useSuspenseQuery(getScanUsers());
// }
function RouteComponent() { function RouteComponent() {
return <div>Hello "/admin/scanUsers"!</div> //const { data: session } = useSession();
return (
<Suspense fallback={<SkellyTable />}>
<ScanUserTable />
</Suspense>
);
} }

View File

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

View File

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

View File

@@ -15,8 +15,8 @@
"foregroundImage": "./assets/adaptive-icon-white.png", "foregroundImage": "./assets/adaptive-icon-white.png",
"backgroundColor": "#ffffff" "backgroundColor": "#ffffff"
}, },
"versionCode": 24, "versionCode": 33,
"minSupportedVersionCode": 21, "minSupportedVersionCode": 33,
"predictiveBackGestureEnabled": false, "predictiveBackGestureEnabled": false,
"package": "net.alpla.lst.mobile" "package": "net.alpla.lst.mobile"
}, },

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@
"ios": "expo run:ios", "ios": "expo run:ios",
"web": "expo start --web", "web": "expo start --web",
"lint": "expo lint", "lint": "expo lint",
"build:apk:clean": "expo prebuild --clean && cd android && gradlew.bat clean && gradlew.bat assembleRelease && npm run copy:apk", "build:apk:clean": "expo prebuild --clean && cd android && gradlew.bat assembleRelease && npm run copy:apk",
"build:apk": "expo prebuild && cd android && gradlew.bat assembleRelease && npm run copy:apk", "build:apk": "expo prebuild && cd android && gradlew.bat assembleRelease && npm run copy:apk",
"build:mobile": "cd scripts && node runBuild.ts", "build:mobile": "cd scripts && node runBuild.ts",
"build:mobile:bump": "cd scripts && node runBuild.ts --bump", "build:mobile:bump": "cd scripts && node runBuild.ts --bump",
@@ -22,6 +22,7 @@
"@react-navigation/bottom-tabs": "^7.15.5", "@react-navigation/bottom-tabs": "^7.15.5",
"@react-navigation/elements": "^2.9.10", "@react-navigation/elements": "^2.9.10",
"@react-navigation/native": "^7.1.33", "@react-navigation/native": "^7.1.33",
"@rn-primitives/dialog": "^1.4.0",
"@rn-primitives/portal": "^1.4.0", "@rn-primitives/portal": "^1.4.0",
"@rn-primitives/separator": "^1.4.0", "@rn-primitives/separator": "^1.4.0",
"@rn-primitives/slot": "^1.4.0", "@rn-primitives/slot": "^1.4.0",
@@ -56,10 +57,11 @@
"react-dom": "19.2.0", "react-dom": "19.2.0",
"react-native": "0.83.4", "react-native": "0.83.4",
"react-native-gesture-handler": "~2.30.0", "react-native-gesture-handler": "~2.30.0",
"react-native-reanimated": "^4.2.1", "react-native-reanimated": "4.2.1",
"react-native-safe-area-context": "~5.6.2", "react-native-safe-area-context": "~5.6.2",
"react-native-screens": "~4.23.0", "react-native-screens": "~4.23.0",
"react-native-tcp-socket": "^6.4.1", "react-native-tcp-socket": "^6.4.1",
"react-native-toast-message": "^2.3.3",
"react-native-web": "~0.21.0", "react-native-web": "~0.21.0",
"react-native-worklets": "0.7.2", "react-native-worklets": "0.7.2",
"socket.io-client": "^4.8.3", "socket.io-client": "^4.8.3",

View File

@@ -1,5 +1,12 @@
import { Redirect, Tabs } from "expo-router"; 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 { useAppStore } from "../../hooks/useAppStore";
import { useMobileAuthStore } from "../../hooks/useMobileAuth"; import { useMobileAuthStore } from "../../hooks/useMobileAuth";
@@ -15,9 +22,11 @@ export default function TabsLayout() {
const isUnlocked = useMobileAuthStore((s) => s.isUnlocked); const isUnlocked = useMobileAuthStore((s) => s.isUnlocked);
const port = parseInt(serverPort || "0", 10) >= 50000; const port = parseInt(serverPort || "0", 10) >= 50000;
console.log(port);
if (!user || (!isUnlocked && !port)) { if (!port) {
return <Redirect href="/login" />; if (!user || !isUnlocked) {
return <Redirect href="/login" />;
}
} }
const isNormalScanner = parseInt(serverPort || "0", 10) >= 50000; const isNormalScanner = parseInt(serverPort || "0", 10) >= 50000;
@@ -49,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 <Tabs.Screen
name="laneCheck" name="laneCheck"
options={{ options={{
title: "Lane Check", title: "Lane Check",
href: isNormalScanner ? null : "/(tabs)/laneCheck", href: isNormalScanner ? null : "/(tabs)/laneCheck",
tabBarIcon: ({ color, size }) => <Rows4 size={size} color={color} />, tabBarIcon: ({ color, size }) => <Rows4 size={size} color={color} />,
}} }}

View File

@@ -1,37 +1,210 @@
import React, { useCallback, useEffect } from "react"; import axios from "axios";
import { Text, View } from "react-native"; import { format } from "date-fns-tz";
import { useFocusEffect } from "expo-router";
import type React from "react";
import { useCallback, useEffect, useState } from "react";
import { ScrollView, Text, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import Toast from "react-native-toast-message";
import { GlobalFooter } from "../../components/UpdateFooter";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { Card, CardContent, CardHeader } from "../../components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../../components/ui/dialog";
import { useAppStore } from "../../hooks/useAppStore";
import { scannerFeedback } from "../../lib/feedbackScan";
import { type ZebraScanResult, zebraScanner } from "../../lib/ZebraScanner"; import { type ZebraScanResult, zebraScanner } from "../../lib/ZebraScanner";
const InfoRow = ({
label,
value,
}: {
label: string;
value: React.ReactNode;
}) => {
return (
<View className="flex-row justify-between gap-4 py-2 border-b border-gray-200">
<Text className="text-sm text-gray-500">{label}</Text>
<Text className="text-sm font-medium text-gray-900 text-right flex-1">
{value}
</Text>
</View>
);
};
export default function LaneCheck() { export default function LaneCheck() {
const handleScan = useCallback(async (scan: ZebraScanResult) => { const [units, setUnits] = useState<any>(null);
console.log(scan); const serverIp = useAppStore((s) => s.serverIp);
}, []);
useEffect(() => { const handleScan = useCallback(
zebraScanner.ensureProfile(); async (scan: ZebraScanResult) => {
zebraScanner.startListening(); setUnits(null);
await scannerFeedback({
type: "scan",
sound: true,
vibrate: true,
led: true,
});
if (!scan.data.startsWith("loc")) {
Toast.show({
type: "error",
text1: "Scan error",
text2: "The last scan was not a lane please try again",
});
const sub = zebraScanner.addScanListener((scan) => { return;
//console.log("SCAN:", scan); }
handleScan(scan); try {
}); const res = await axios.post(
`http://${serverIp.trim()}:3000/lst/api/mobile/lanecheck`,
{
lane: scan.data,
},
{
timeout: 5000,
},
);
if (res.status === 200) {
setUnits(res.data);
Toast.show({
type: "info",
text1: "Lane Data",
text2: "All Loading Units from this lane will be listed below",
});
}
} catch (error) {
console.log(error);
Toast.show({
type: "error",
text1: "Lane Data",
text2: "Error getting lane data please try again",
});
}
},
[serverIp.trim],
);
useFocusEffect(
useCallback(() => {
zebraScanner.startListening();
const sub = zebraScanner.addScanListener((scan) => {
//console.log("SCAN:", scan);
handleScan(scan);
});
return () => {
sub.remove();
zebraScanner.stopListening();
//setUnits(null);
};
}, [handleScan]),
);
return () => {
sub.remove();
zebraScanner.stopListening();
};
}, [handleScan]);
return ( return (
<View <View
style={{ style={{
flex: 1,
//justifyContent: "center", //justifyContent: "center",
alignItems: "center", alignItems: "center",
marginTop: 50, marginTop: 50,
}} }}
> >
<Text>LaneChecks</Text> {units ? (
// <SafeAreaView className={`flex-1 w-full items-center`}>
// <ScrollView className="w-full flex-1">
// <View className="flex items-center gap-2 w-full">
// {units.data?.map((i: any, index: any) => (
// <View key={`${i.runningNumber}-${index}`}>
// <Text>example</Text>
// </View>
// ))}
// </View>
// </ScrollView>
// </SafeAreaView>
<SafeAreaView className={`w-full items-center`}>
<View style={{ padding: 2 }}>
<Text>There Are {units.data.length} units in this lane</Text>
</View>
<ScrollView className="w-full" style={{ marginBottom: 20 }}>
<View>
{units.data.map((i, index) => (
<View
key={`${i.runningNumber}-${index}`}
style={{
justifyContent: "center",
margin: 2,
}}
>
<Dialog>
<DialogTrigger>
<Card
className="w-full"
style={{
borderColor:
i.state === "QualityBlocked" ? "red" : undefined,
borderWidth: 4,
}}
>
<CardContent>
<Text>
{i.articleId} - {i.articleName}
</Text>
<Text>
Running Number: {i.runningNumber ?? "Non barcoded"}
</Text>
</CardContent>
</Card>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
Details for Article {i.articleId}, Rn:
{i.runningNumber ?? "Non barcoded"}{" "}
</DialogTitle>
<DialogDescription>
<InfoRow
label="Production Date"
value={format(i.productionDate, "MM/dd/yyyy HH:mm")}
/>
<InfoRow label="Quantity" value={i.quantity} />
{i.state === "QualityBlocked" && (
<InfoRow
label="Defect"
value={i.mainDefectGroupDescription}
/>
)}
{i.state === "QualityBlocked" && (
<InfoRow
label="Description"
value={i.mainDefectDescription}
/>
)}
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</View>
))}
</View>
</ScrollView>
</SafeAreaView>
) : (
<View className="mt-50">
<Text className="text-2xl text-center">
Please scan a lane to see all Units that are in the lane.
</Text>
</View>
)}
<View>
<GlobalFooter />
</View>
</View> </View>
); );
} }

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

@@ -2,10 +2,16 @@ import { PortalHost } from "@rn-primitives/portal";
import { Stack } from "expo-router"; import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar"; import { StatusBar } from "expo-status-bar";
import "../../global.css"; import "../../global.css";
import { useEffect } from "react";
import Toast from "react-native-toast-message";
import useDeviceLock from "../hooks/useDeviceCheck"; import useDeviceLock from "../hooks/useDeviceCheck";
import { zebraScanner } from "../lib/ZebraScanner";
export default function RootLayout() { export default function RootLayout() {
useDeviceLock(); useDeviceLock();
useEffect(() => {
zebraScanner.ensureProfile();
}, []);
return ( return (
<> <>
@@ -18,6 +24,7 @@ export default function RootLayout() {
<Stack.Screen name="(tabs)" /> <Stack.Screen name="(tabs)" />
</Stack> </Stack>
<PortalHost /> <PortalHost />
<Toast />
</> </>
); );
} }

View File

@@ -2,11 +2,15 @@ import axios from "axios";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { useState } from "react"; import { useState } from "react";
import { Button, Text, View } from "react-native"; import { Alert, Button, Text, View } from "react-native";
import Toast from "react-native-toast-message";
import { Input } from "../components/ui/input"; import { Input } from "../components/ui/input";
import { useAppStore } from "../hooks/useAppStore"; import { useAppStore } from "../hooks/useAppStore";
import { useMobileAuthStore } from "../hooks/useMobileAuth"; import { useMobileAuthStore } from "../hooks/useMobileAuth";
const formatName = (name?: string) =>
name ? name.charAt(0).toUpperCase() + name.slice(1).toLowerCase() : "";
export default function Login() { export default function Login() {
// doing this causes rerender and sub // doing this causes rerender and sub
//const { setUser } = useMobileAuthStore(); //const { setUser } = useMobileAuthStore();
@@ -33,11 +37,18 @@ export default function Login() {
if (res.status === 200) { if (res.status === 200) {
// this way to set the user is direct and basically a 1 shot // this way to set the user is direct and basically a 1 shot
Toast.show({
type: "success",
text1: `Welcome back ${formatName(res.data.data.name)}`,
});
useMobileAuthStore.getState().setUser(res.data.data); useMobileAuthStore.getState().setUser(res.data.data);
return router.replace("/(tabs)/scanner"); return router.replace("/(tabs)/scanner");
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
//Alert.alert("Login Error", `Invalid pin please try again`);
Toast.show({ type: "error", text1: `Invalid pin please try again` });
} }
}; };
@@ -70,7 +81,7 @@ export default function Login() {
</View> </View>
</View> </View>
<View> <View>
<Text> <Text className="p-3">
Warning: If you are logged into another scanner you will encounter Warning: If you are logged into another scanner you will encounter
scan errors, please do not try to log into more than 1 scanner at a scan errors, please do not try to log into more than 1 scanner at a
time. time.

View File

@@ -2,6 +2,7 @@ import Constants from "expo-constants";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { useState } from "react"; import { useState } from "react";
import { Alert, Button, Text, TextInput, View } from "react-native"; import { Alert, Button, Text, TextInput, View } from "react-native";
import Toast from "react-native-toast-message";
import { useAppStore } from "../hooks/useAppStore"; import { useAppStore } from "../hooks/useAppStore";
import { useServerStore } from "../hooks/useServerCheck"; import { useServerStore } from "../hooks/useServerCheck";
@@ -25,18 +26,29 @@ export default function Setup() {
const server = useServerStore((s) => s.serverVersion); const server = useServerStore((s) => s.serverVersion);
// TODO: if on lst version and the user is manager or admin just login
const authCheck = () => { const authCheck = () => {
if (pin === "6971") { if (pin === "6971") {
setAuth(true); setAuth(true);
} else { } else {
Alert.alert("Incorrect pin entered please try again"); //Alert.alert("Incorrect pin entered please try again");
Toast.show({
type: "error",
text1: "Incorrect pin entered please try again",
});
setPin(""); setPin("");
} }
}; };
const handleSave = async () => { const handleSave = async () => {
if (!serverIp.trim() || !serverPort.trim()) { if (!serverIp.trim() || !serverPort.trim()) {
Alert.alert("Missing info", "Please fill in both fields."); //Alert.alert("Missing info", "Please fill in both fields.");
Toast.show({
type: "error",
text1: "Missing info",
text2: "Please fill in both fields.",
});
return; return;
} }
@@ -48,7 +60,12 @@ export default function Setup() {
isRegistered: true, isRegistered: true,
}); });
Alert.alert("Saved", "Config saved to device."); //Alert.alert("Saved", "Config saved to device.");
Toast.show({
type: "info",
text1: "Saved",
text2: "Config saved to device.",
});
//router.replace("/"); //router.replace("/");
}; };
return ( return (

View File

@@ -1,5 +1,6 @@
import axios from "axios"; import axios from "axios";
import { format } from "date-fns-tz"; import { format } from "date-fns-tz";
import { Redirect, useFocusEffect, useRouter } from "expo-router";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { Alert, Button, Text, View } from "react-native"; import { Alert, Button, Text, View } from "react-native";
import { useAppStore } from "../hooks/useAppStore"; import { useAppStore } from "../hooks/useAppStore";
@@ -7,6 +8,7 @@ import { useMobileAuthStore } from "../hooks/useMobileAuth";
import { useScannerStore } from "../hooks/useScannerStore"; import { useScannerStore } from "../hooks/useScannerStore";
import { scannerFeedback } from "../lib/feedbackScan"; import { scannerFeedback } from "../lib/feedbackScan";
import { sendTcpMessage } from "../lib/tcpScan"; import { sendTcpMessage } from "../lib/tcpScan";
import { versionCheck } from "../lib/versionValidation";
import { type ZebraScanResult, zebraScanner } from "../lib/ZebraScanner"; import { type ZebraScanResult, zebraScanner } from "../lib/ZebraScanner";
import { ScannedLabelBox } from "./ScannedLabels"; import { ScannedLabelBox } from "./ScannedLabels";
import { GlobalFooter } from "./UpdateFooter"; import { GlobalFooter } from "./UpdateFooter";
@@ -21,14 +23,13 @@ const formatName = (name?: string) =>
export default function LSTScanner() { export default function LSTScanner() {
const user = useMobileAuthStore((s) => s.user); const user = useMobileAuthStore((s) => s.user);
const logout = useMobileAuthStore((s) => s.logout); const logout = useMobileAuthStore((s) => s.logout);
const router = useRouter();
// TODO : move to off tcp stuff after od // TODO : move to off tcp stuff after od
const lastScan = useScannerStore((s) => s.lastScan); const lastScan = useScannerStore((s) => s.lastScan);
const setLastScan = useScannerStore((s) => s.setLastScan); const setLastScan = useScannerStore((s) => s.setLastScan);
const [tagScans, setTagScans] = useState<any>([]); const [tagScans, setTagScans] = useState<any>([]);
const scannerIdFromStore = useAppStore((s) => s.scannerId);
const serverIp = useAppStore((s) => s.serverIp); const serverIp = useAppStore((s) => s.serverIp);
const serverPort = useAppStore((s) => s.serverPort);
const [bgColor, setBGColor] = useState<string | null>(null); const [bgColor, setBGColor] = useState<string | null>(null);
const handleScan = useCallback( const handleScan = useCallback(
@@ -45,10 +46,15 @@ export default function LSTScanner() {
scan.data.toLowerCase().includes(cmd.toLowerCase()), scan.data.toLowerCase().includes(cmd.toLowerCase()),
); );
console.log(user?.excludedCommand);
if (isAlphaStart && isExcluded) { if (isAlphaStart && isExcluded) {
Alert.alert( Alert.alert(
`Command: ${scan.data} is not allowed to be used, please contact logistics if this is an error`, "Command not allowed",
`Command: ${scan.data}\n\nPlease contact logistics if this is an error`,
); );
return;
} }
let commandToSend = `${STX}${user?.scannerId}@${scan.data}${ETX}`; let commandToSend = `${STX}${user?.scannerId}@${scan.data}${ETX}`;
@@ -68,16 +74,26 @@ export default function LSTScanner() {
const scanned = (await sendTcpMessage( const scanned = (await sendTcpMessage(
commandToSend, commandToSend,
serverIp, serverIp,
parseInt(serverPort || "0", 10), 50004,
)) as any; )) as any;
// send the logs to lst but allow it to time out if it dose not exist just bc. // send the logs to lst but allow it to time out if it dose not exist just bc.
const logInfo = { ...scanned, user: user?.name };
try { try {
await axios.post( await axios.post(`http://${serverIp.trim()}:3000/lst/api/mobile/logs`, {
`http://${serverIp.trim()}:3000/lst/api/mobile/logs`, scannerId: user?.scannerId ?? "0",
logInfo, message: scanned.data.message,
); prompt: scanned.data.prompt,
commandDescription: scanned.data.commandDescription,
status: scanned.data.status,
lines: scanned.data.lines,
user: user?.name ?? "prodScan",
runningNumber: scan.data.startsWith("000")
? parseInt(scan.data.slice(10, -1) || "000", 10).toString()
: scan.data.startsWith("loc")
? scan.data
: "0",
});
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
@@ -90,7 +106,15 @@ export default function LSTScanner() {
vibrate: true, vibrate: true,
led: true, led: true,
}); });
setBGColor("bg-green-500"); setBGColor("bg-green-500");
// version check
versionCheck();
// auth update
useMobileAuthStore.getState().updateLastScan();
setTimeout(() => { setTimeout(() => {
setBGColor(null); setBGColor(null);
}, 1 * 1000); }, 1 * 1000);
@@ -117,7 +141,6 @@ export default function LSTScanner() {
}, },
[ [
serverIp, serverIp,
serverPort,
setLastScan, setLastScan,
user?.scannerId, user?.scannerId,
user?.name, user?.name,
@@ -130,22 +153,30 @@ export default function LSTScanner() {
setTagScans([]); setTagScans([]);
}; };
const logoutScanner = () => {
setTagScans([]);
setLastScan(null);
logout();
router.replace("/");
};
//console.log(lastScan); //console.log(lastScan);
useEffect(() => { useFocusEffect(
zebraScanner.ensureProfile(); useCallback(() => {
zebraScanner.startListening(); zebraScanner.startListening();
const sub = zebraScanner.addScanListener((scan) => { const sub = zebraScanner.addScanListener((scan) => {
//console.log("SCAN:", scan); //console.log("SCAN:", scan);
handleScan(scan); handleScan(scan);
}); });
return () => { return () => {
sub.remove(); sub.remove();
zebraScanner.stopListening(); zebraScanner.stopListening();
}; };
}, [handleScan]); }, [handleScan]),
);
return ( return (
<View className={`${bgColor ?? ""} flex-1 w-screen`}> <View className={`${bgColor ?? ""} flex-1 w-screen`}>
<View style={{ alignItems: "center", margin: 5 }}> <View style={{ alignItems: "center", margin: 5 }}>
@@ -164,7 +195,11 @@ export default function LSTScanner() {
{!lastScan ? ( {!lastScan ? (
<View style={{ marginTop: 10, alignItems: "center" }}> <View style={{ marginTop: 10, alignItems: "center" }}>
<Text className="text-xl font-bold">Ready to scan</Text> <Text className="text-xl font-bold">Ready to scan</Text>
<Text>Waiting for first scan...</Text> <Text>Please Scan a command to start scanning...</Text>
<Text className="text-sm">
Scanning a label could cause errors due to incorrect previous
command scanned
</Text>
</View> </View>
) : ( ) : (
<View <View
@@ -203,7 +238,7 @@ export default function LSTScanner() {
<View className="m-2"> <View className="m-2">
{user && ( {user && (
<View className="items-center"> <View className="items-center">
<Button title="Logout" onPress={logout} /> <Button title="Logout" onPress={logoutScanner} />
</View> </View>
)} )}
</View> </View>

View File

@@ -1,11 +1,14 @@
import axios from "axios"; import axios from "axios";
import { format } from "date-fns-tz"; import { format } from "date-fns-tz";
import { useFocusEffect } from "expo-router";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { Text, View } from "react-native"; import { Text, View } from "react-native";
import { useAppStore } from "../hooks/useAppStore"; import { useAppStore } from "../hooks/useAppStore";
import { useMobileAuthStore } from "../hooks/useMobileAuth";
import { useScannerStore } from "../hooks/useScannerStore"; import { useScannerStore } from "../hooks/useScannerStore";
import { scannerFeedback } from "../lib/feedbackScan"; import { scannerFeedback } from "../lib/feedbackScan";
import { sendTcpMessage } from "../lib/tcpScan"; import { sendTcpMessage } from "../lib/tcpScan";
import { versionCheck } from "../lib/versionValidation";
import { type ZebraScanResult, zebraScanner } from "../lib/ZebraScanner"; import { type ZebraScanResult, zebraScanner } from "../lib/ZebraScanner";
import { ScannedLabelBox } from "./ScannedLabels"; import { ScannedLabelBox } from "./ScannedLabels";
import { GlobalFooter } from "./UpdateFooter"; import { GlobalFooter } from "./UpdateFooter";
@@ -52,11 +55,18 @@ export default function ProdScanner() {
parseInt(serverPort || "0", 10), parseInt(serverPort || "0", 10),
)) as any; )) as any;
// send the logs to lst but allow it to time out if it dose not exist just bc. // send the logs to lst but allow it to time out if it dose not exist just bc.
const data = {
...scanned.data,
runningNumber: scan.data.startsWith("000")
? parseInt(scan.data.slice(10, -1) || "000", 10).toString()
: scan.data.startsWith("loc")
? scan.data
: "0",
};
try { try {
await axios.post( await axios.post(
`http://${serverIp.trim()}:3000/lst/api/mobile/logs`, `http://${serverIp.trim()}:3000/lst/api/mobile/logs`,
scanned, data,
); );
} catch (error) { } catch (error) {
console.log(error); console.log(error);
@@ -71,6 +81,13 @@ export default function ProdScanner() {
led: true, led: true,
}); });
setBGColor("bg-green-500"); setBGColor("bg-green-500");
// version check
versionCheck();
// auth update
useMobileAuthStore.getState().updateLastScan();
setTimeout(() => { setTimeout(() => {
setBGColor(null); setBGColor(null);
}, 1 * 1000); }, 1 * 1000);
@@ -104,20 +121,21 @@ export default function ProdScanner() {
//console.log(lastScan); //console.log(lastScan);
useEffect(() => { useFocusEffect(
zebraScanner.ensureProfile(); useCallback(() => {
zebraScanner.startListening(); zebraScanner.startListening();
const sub = zebraScanner.addScanListener((scan) => { const sub = zebraScanner.addScanListener((scan) => {
//console.log("SCAN:", scan); //console.log("SCAN:", scan);
handleScan(scan); handleScan(scan);
}); });
return () => { return () => {
sub.remove(); sub.remove();
zebraScanner.stopListening(); zebraScanner.stopListening();
}; };
}, [handleScan]); }, [handleScan]),
);
return ( return (
<View className={`${bgColor ?? ""} flex-1 w-screen`}> <View className={`${bgColor ?? ""} flex-1 w-screen`}>
<View> <View>

View File

@@ -28,7 +28,7 @@ export function GlobalFooter() {
{!hasUpdate && shouldUpdate && ( {!hasUpdate && shouldUpdate && (
<View className="bg-[#FDBA74]"> <View className="bg-[#FDBA74]">
<Link href={"/updateScreen"}> <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 There is an update click me for instructions
</Text> </Text>
</Link> </Link>

View File

@@ -0,0 +1,140 @@
import { Icon } from '@/components/ui/icon';
import { NativeOnlyAnimatedView } from '@/components/ui/native-only-animated-view';
import { cn } from '@/lib/utils';
import * as DialogPrimitive from '@rn-primitives/dialog';
import { X } from 'lucide-react-native';
import * as React from 'react';
import { Platform, Text, View, type ViewProps } from 'react-native';
import { FadeIn, FadeOut } from 'react-native-reanimated';
import { FullWindowOverlay as RNFullWindowOverlay } from 'react-native-screens';
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const FullWindowOverlay = Platform.OS === 'ios' ? RNFullWindowOverlay : React.Fragment;
function DialogOverlay({
className,
children,
...props
}: Omit<React.ComponentProps<typeof DialogPrimitive.Overlay>, 'asChild'> & {
children?: React.ReactNode;
}) {
return (
<FullWindowOverlay>
<DialogPrimitive.Overlay
className={cn(
'absolute bottom-0 left-0 right-0 top-0 flex items-center justify-center bg-black/50 p-2',
Platform.select({
web: 'animate-in fade-in-0 fixed cursor-default [&>*]:cursor-auto',
}),
className
)}
{...props}
asChild={Platform.OS !== 'web'}>
<NativeOnlyAnimatedView entering={FadeIn.duration(200)} exiting={FadeOut.duration(150)}>
<NativeOnlyAnimatedView entering={FadeIn.delay(50)} exiting={FadeOut.duration(150)}>
<>{children}</>
</NativeOnlyAnimatedView>
</NativeOnlyAnimatedView>
</DialogPrimitive.Overlay>
</FullWindowOverlay>
);
}
function DialogContent({
className,
portalHost,
children,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
portalHost?: string;
}) {
return (
<DialogPortal hostName={portalHost}>
<DialogOverlay>
<DialogPrimitive.Content
className={cn(
'bg-background border-border z-50 mx-auto flex w-full max-w-[calc(100%-2rem)] flex-col gap-4 rounded-lg border p-6 shadow-lg shadow-black/5 sm:max-w-lg',
Platform.select({
web: 'animate-in fade-in-0 zoom-in-95 duration-200',
}),
className
)}
{...props}>
<>{children}</>
<DialogPrimitive.Close
className={cn(
'absolute right-4 top-4 rounded opacity-70 active:opacity-100',
Platform.select({
web: 'ring-offset-background focus:ring-ring data-[state=open]:bg-accent transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2',
})
)}
hitSlop={12}>
<Icon
as={X}
className={cn('text-accent-foreground web:pointer-events-none size-4 shrink-0')}
/>
<Text className="sr-only">Close</Text>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogOverlay>
</DialogPortal>
);
}
function DialogHeader({ className, ...props }: ViewProps) {
return (
<View className={cn('flex flex-col gap-2 text-center sm:text-left', className)} {...props} />
);
}
function DialogFooter({ className, ...props }: ViewProps) {
return (
<View
className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
{...props}
/>
);
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
className={cn('text-foreground text-lg font-semibold leading-none', className)}
{...props}
/>
);
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
);
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};

View File

@@ -0,0 +1,57 @@
import { TextClassContext } from '@/components/ui/text';
import { cn } from '@/lib/utils';
import type { LucideIcon, LucideProps } from 'lucide-react-native';
import { cssInterop } from 'nativewind';
import * as React from 'react';
type IconProps = LucideProps & {
as: LucideIcon;
} & React.RefAttributes<LucideIcon>;
function IconImpl({ as: IconComponent, ...props }: IconProps) {
return <IconComponent {...props} />;
}
cssInterop(IconImpl, {
className: {
target: 'style',
nativeStyleToProp: {
height: 'size',
width: 'size',
},
},
});
/**
* A wrapper component for Lucide icons with Nativewind `className` support via `cssInterop`.
*
* This component allows you to render any Lucide icon while applying utility classes
* using `nativewind`. It avoids the need to wrap or configure each icon individually.
*
* @component
* @example
* ```tsx
* import { ArrowRight } from 'lucide-react-native';
* import { Icon } from '@/registry/components/ui/icon';
*
* <Icon as={ArrowRight} className="text-red-500" size={16} />
* ```
*
* @param {LucideIcon} as - The Lucide icon component to render.
* @param {string} className - Utility classes to style the icon using Nativewind.
* @param {number} size - Icon size (defaults to 14).
* @param {...LucideProps} ...props - Additional Lucide icon props passed to the "as" icon.
*/
function Icon({ as: IconComponent, className, size = 14, ...props }: IconProps) {
const textClass = React.useContext(TextClassContext);
return (
<IconImpl
as={IconComponent}
className={cn('text-foreground', textClass, className)}
size={size}
{...props}
/>
);
}
export { Icon };

View File

@@ -0,0 +1,23 @@
import { Platform } from 'react-native';
import Animated from 'react-native-reanimated';
/**
* This component is used to wrap animated views that should only be animated on native.
* @param props - The props for the animated view.
* @returns The animated view if the platform is native, otherwise the children.
* @example
* <NativeOnlyAnimatedView entering={FadeIn} exiting={FadeOut}>
* <Text>I am only animated on native</Text>
* </NativeOnlyAnimatedView>
*/
function NativeOnlyAnimatedView(
props: React.ComponentProps<typeof Animated.View> & React.RefAttributes<typeof Animated.View>
) {
if (Platform.OS === 'web') {
return <>{props.children as React.ReactNode}</>;
} else {
return <Animated.View {...props} />;
}
}
export { NativeOnlyAnimatedView };

View File

@@ -1,5 +1,5 @@
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import * as Slot from '@rn-primitives/slot'; import { Slot } from '@rn-primitives/slot';
import { cva, type VariantProps } from 'class-variance-authority'; import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react'; import * as React from 'react';
import { Platform, Text as RNText, type Role } from 'react-native'; import { Platform, Text as RNText, type Role } from 'react-native';
@@ -70,11 +70,12 @@ function Text({
variant = 'default', variant = 'default',
...props ...props
}: React.ComponentProps<typeof RNText> & }: React.ComponentProps<typeof RNText> &
React.RefAttributes<typeof RNText> &
TextVariantProps & { TextVariantProps & {
asChild?: boolean; asChild?: boolean;
}) { }) {
const textClass = React.useContext(TextClassContext); const textClass = React.useContext(TextClassContext);
const Component = asChild ? Slot.Text : RNText; const Component = asChild ? Slot : RNText;
return ( return (
<Component <Component
className={cn(textVariants({ variant }), textClass, className)} className={cn(textVariants({ variant }), textClass, className)}

View File

@@ -2,6 +2,7 @@ import axios from "axios";
import Constants from "expo-constants"; import Constants from "expo-constants";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { devDelay } from "../lib/devMode"; import { devDelay } from "../lib/devMode";
import { versionCheck } from "../lib/versionValidation";
import { useAppStore } from "./useAppStore"; import { useAppStore } from "./useAppStore";
import { useServerStore } from "./useServerCheck"; import { useServerStore } from "./useServerCheck";
@@ -24,7 +25,6 @@ export function useAppStartup() {
const hasHydrated = useAppStore((s) => s.hasHydrated); const hasHydrated = useAppStore((s) => s.hasHydrated);
const serverPort = useAppStore((s) => s.serverPort); const serverPort = useAppStore((s) => s.serverPort);
const serverIp = useAppStore((s) => s.serverIp); const serverIp = useAppStore((s) => s.serverIp);
const setServerVersion = useServerStore((s) => s.setServerVersion);
useEffect(() => { useEffect(() => {
if (!hasHydrated) { if (!hasHydrated) {
@@ -62,29 +62,7 @@ export function useAppStartup() {
return; return;
} }
const port = await versionCheck();
parseInt(serverPort || "0", 10) >= 50000 ? "3000" : serverPort;
try {
const res = await axios.get(
`http://${serverIp}:${port}/lst/api/mobile/version`,
{ timeout: 5000 },
);
if (res.status === 200) {
setServerVersion(res.data);
}
const build = Constants.expoConfig?.android?.versionCode ?? 1;
if (build < res.data.minSupportedVersionCode) {
setStartupRoute("/updateScreen");
setReady(true);
return;
}
} catch (error) {
console.log("Version check error:", error);
}
setStatus("scannerMode"); setStatus("scannerMode");
await devDelay(1500); await devDelay(1500);
@@ -123,7 +101,7 @@ export function useAppStartup() {
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [hasHydrated, serverIp, serverPort, setServerVersion]); }, [hasHydrated, serverIp, serverPort]);
return { return {
ready, ready,

View File

@@ -19,7 +19,11 @@ export default function useDeviceLock() {
nextAppState === "background" || nextAppState === "inactive"; nextAppState === "background" || nextAppState === "inactive";
if (wasActive && isNowInactive) { if (wasActive && isNowInactive) {
useMobileAuthStore.getState().lock(); const auth = useMobileAuthStore.getState();
if (auth.shouldLockForIdle()) {
auth.lock();
}
} }
appStateRef.current = nextAppState; appStateRef.current = nextAppState;

View File

@@ -1,5 +1,7 @@
import { create } from "zustand"; import { create } from "zustand";
const ONE_HOUR = 1000 * 60 * 60;
type MobileUser = { type MobileUser = {
id: string; id: string;
name: string; name: string;
@@ -11,19 +13,40 @@ type MobileUser = {
type AuthState = { type AuthState = {
user: MobileUser | null; user: MobileUser | null;
isUnlocked: boolean; isUnlocked: boolean;
lastScanAt: number | null;
setUser: (user: MobileUser) => void; setUser: (user: MobileUser) => void;
updateLastScan: () => void;
lock: () => void; lock: () => void;
logout: () => void; logout: () => void;
shouldLockForIdle: () => boolean;
}; };
export const useMobileAuthStore = create<AuthState>((set) => ({ export const useMobileAuthStore = create<AuthState>((set, get) => ({
user: null, user: null,
isUnlocked: false, isUnlocked: false,
lastScanAt: null,
setUser: (user) => set({ user, isUnlocked: true }), setUser: (user) =>
set({
user,
isUnlocked: true,
lastScanAt: Date.now(),
}),
updateLastScan: () => set({ lastScanAt: Date.now() }),
lock: () => set({ isUnlocked: false }), lock: () => set({ isUnlocked: false }),
logout: () => set({ user: null, isUnlocked: false }), logout: () =>
set({
user: null,
isUnlocked: false,
lastScanAt: null,
}),
shouldLockForIdle: () => {
const lastScanAt = get().lastScanAt;
if (!lastScanAt) return true;
return Date.now() - lastScanAt > ONE_HOUR;
},
})); }));

View File

@@ -37,4 +37,7 @@ export const zebraScanner = {
): EmitterSubscription { ): EmitterSubscription {
return scannerEmitter.addListener("barcodeScanned", callback); return scannerEmitter.addListener("barcodeScanned", callback);
}, },
disableScannerInput() {
ZebraScanner.disableScannerInput();
},
}; };

View File

@@ -1,43 +1,73 @@
import axios from "axios";
import { useAppStore } from "../hooks/useAppStore";
import { useServerStore } from "../hooks/useServerCheck";
export type ServerVersionInfo = { export type ServerVersionInfo = {
packageName: string; packageName: string;
versionName: string; versionName: string;
versionCode: number; versionCode: number;
minSupportedVersionCode: number; minSupportedVersionCode: number;
fileName: string; fileName: string;
}; };
export type StartupStatus = export type StartupStatus =
| { state: "checking" } | { state: "checking" }
| { state: "needs-config" } | { state: "needs-config" }
| { state: "offline" } | { state: "offline" }
| { state: "blocked"; reason: string; server: ServerVersionInfo } | { state: "blocked"; reason: string; server: ServerVersionInfo }
| { state: "warning"; message: string; server: ServerVersionInfo } | { state: "warning"; message: string; server: ServerVersionInfo }
| { state: "ready"; server: ServerVersionInfo | null }; | { state: "ready"; server: ServerVersionInfo | null };
export function evaluateVersion( export function evaluateVersion(
appBuildCode: number, appBuildCode: number,
server: ServerVersionInfo server: ServerVersionInfo,
): StartupStatus { ): StartupStatus {
if (appBuildCode < server.minSupportedVersionCode) { if (appBuildCode < server.minSupportedVersionCode) {
return { return {
state: "blocked", state: "blocked",
reason: "This scanner app is too old and must be updated before use.", reason: "This scanner app is too old and must be updated before use.",
server, server,
}; };
} }
if (appBuildCode !== server.versionCode) { if (appBuildCode !== server.versionCode) {
return { return {
state: "warning", state: "warning",
message: `A newer version is available. Installed build: ${appBuildCode}, latest build: ${server.versionCode}.`, message: `A newer version is available. Installed build: ${appBuildCode}, latest build: ${server.versionCode}.`,
server, server,
}; };
} }
return { return {
state: "ready", state: "ready",
server, server,
}; };
} }
export const versionCheck = async () => {
const { setServerVersion } = useServerStore.getState();
const { serverPort, serverIp } = useAppStore.getState();
const port = parseInt(serverPort || "0", 10) >= 50000 ? "3000" : serverPort;
try {
const res = await axios.get(
`http://${serverIp}:${port}/lst/api/mobile/version`,
{ timeout: 5000 },
);
if (res.status === 200) {
setServerVersion(res.data);
}
// const build = Constants.expoConfig?.android?.versionCode ?? 1;
// if (build < res.data.minSupportedVersionCode) {
// setStartupRoute("/updateScreen");
// setReady(true);
// return;
// }
} catch (error) {
console.log("Version check error:", error);
}
};

View File

@@ -0,0 +1 @@
ALTER TABLE "scan_log" ADD COLUMN "running_number" text DEFAULT '0';

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
);

View File

@@ -0,0 +1 @@
ALTER TABLE "analytics_daily" ADD CONSTRAINT "analytics_daily_business_route_unique" UNIQUE("business_date","method","route_pattern","module");

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -330,6 +330,41 @@
"when": 1778059910210, "when": 1778059910210,
"tag": "0046_chemical_the_leader", "tag": "0046_chemical_the_leader",
"breakpoints": true "breakpoints": true
},
{
"idx": 47,
"version": "7",
"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
},
{
"idx": 51,
"version": "7",
"when": 1778525497824,
"tag": "0051_sad_war_machine",
"breakpoints": true
} }
] ]
} }

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "lst_v3", "name": "lst_v3",
"version": "0.0.2-alpha.8", "version": "0.0.2-alpha.10",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "lst_v3", "name": "lst_v3",
"version": "0.0.2-alpha.8", "version": "0.0.2-alpha.10",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@dotenvx/dotenvx": "^1.57.0", "@dotenvx/dotenvx": "^1.57.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "lst_v3", "name": "lst_v3",
"version": "0.0.2-alpha.8", "version": "0.0.2-alpha.10",
"description": "The tool that supports us in our everyday alplaprod", "description": "The tool that supports us in our everyday alplaprod",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@@ -106,6 +106,8 @@ function Update-Server {
"CLIENT_SECRET" = "zsJeyjMN2yDDqfyzSsh96OtlA2714F5d" "CLIENT_SECRET" = "zsJeyjMN2yDDqfyzSsh96OtlA2714F5d"
"CLIENT_SCOPES" = "openid profile email groups" "CLIENT_SCOPES" = "openid profile email groups"
"DISCOVERY_URL" = "https://auth.tuffraid.net/oidc/.well-known/openid-configuration" "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 = @() $linesToAppend = @()