13 Commits

Author SHA1 Message Date
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
8c253a90b6 chore(release): 0.0.2-alpha.8
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 4m12s
Release and Build Image / release (push) Failing after 2m19s
2026-05-06 05:08:27 -05:00
ba30281e59 feat(mobile): auth added in
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-05-06 05:07:16 -05:00
2ad78e22f1 chore(release): 0.0.2-alpha.7
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 4m3s
Release and Build Image / release (push) Failing after 2m30s
2026-05-05 19:50:58 -05:00
518c0a8c19 refactor(scanner): format changes
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-05-05 19:50:02 -05:00
cd13360cfb feat(intial auth): intial auth setup for the scanner
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-05-05 19:48:36 -05:00
4e0cf8c54c refactor(docker compose): changed to have the correct url that will be used as this is for auth 2026-05-05 14:52:36 -05:00
70 changed files with 15191 additions and 394 deletions

View File

@@ -12,20 +12,20 @@ jobs:
steps:
- name: Checkout (local)
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 }}
- 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
run: |
docker build \
-t git.tuffraid.net/cowch/lst_v3:latest \
-t git.tuffraid.net/cowch/lst_v3:${{ gitea.sha }} \
-t 10.75.9.150:3100/cowch/lst_v3:latest \
-t 10.75.9.150:3100/cowch/lst_v3:${{ gitea.sha }} \
.
- name: Push
run: |
docker push git.tuffraid.net/cowch/lst_v3:latest
docker push git.tuffraid.net/cowch/lst_v3:${{ gitea.sha }}
docker push 10.75.9.150:3100/cowch/lst_v3:latest
docker push 10.75.9.150:3100/cowch/lst_v3:${{ gitea.sha }}

View File

@@ -14,12 +14,12 @@ jobs:
# Examples:
# http://gitea.internal.lan:3000
# 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.
# Example:
# gitea.internal:3000
REGISTRY_HOST: "git.tuffraid.net"
REGISTRY_HOST: "10.75.9.150:3100" #"git.tuffraid.net"
steps:
- name: Check out repository

View File

@@ -1,5 +1,63 @@
# All Changes to LST can be found below.
## [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)
### 🌟 Enhancements
* **mobile:** auth added in ([ba30281](https://git.tuffraid.net/cowch/lst_v3/commits/ba30281e59040513a036fb7413e372457d04a7c8))
## [0.0.2-alpha.7](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.6...v0.0.2-alpha.7) (2026-05-06)
### 🌟 Enhancements
* **intial auth:** intial auth setup for the scanner ([cd13360](https://git.tuffraid.net/cowch/lst_v3/commits/cd13360cfb931daca50fd7b111e1c8f8ab09a909))
* **mobile:** new route for the ehs launcher ([649ae1e](https://git.tuffraid.net/cowch/lst_v3/commits/649ae1ee9f245a9b5d308ea8a636357bf72b1e34))
* **mobile:** shadcn like and tailwind added to make things look yummy ([7d2f048](https://git.tuffraid.net/cowch/lst_v3/commits/7d2f048932b77269568149de34351840b75486e2))
* **mobile:** update notifications and more error handling added ([30ffd84](https://git.tuffraid.net/cowch/lst_v3/commits/30ffd843c725da79ed035e2d9564f60a6babcda8))
* **scanner:** more work on the scanner and can now scan to prod no lst right now ([77b4533](https://git.tuffraid.net/cowch/lst_v3/commits/77b4533dea8314fd4fb81a597995cabd041fe188))
* **servers:** added iowa ebm ([8446dbc](https://git.tuffraid.net/cowch/lst_v3/commits/8446dbc955462235b9df35c501354761661e4f6a))
### 🐛 Bug fixes
* **mobile:** typo for version checking ([0b7318f](https://git.tuffraid.net/cowch/lst_v3/commits/0b7318f8566d15414edd3cd67c89fa5346058ab0))
### 🛠️ Code Refactor
* **docker compose:** changed to have the correct url that will be used as this is for auth ([4e0cf8c](https://git.tuffraid.net/cowch/lst_v3/commits/4e0cf8c54c4dfd68edba7e733518846a47c55064))
* **gp connection:** added in gp ip into env if not there use static name for dns ([36995e9](https://git.tuffraid.net/cowch/lst_v3/commits/36995e9fb42cfa1b72c096b8860866d70b86e70c))
* **mobile:** more look and feel work ([bb6155c](https://git.tuffraid.net/cowch/lst_v3/commits/bb6155c9692220542a52664848abf0b9eee91a43))
* **mobile:** moved the versioning lookup at at the mobile folder plus renamed ([bddc9ac](https://git.tuffraid.net/cowch/lst_v3/commits/bddc9aca0d2da2b2f53dec1250276d7a076a8601))
* **scanner:** format changes ([518c0a8](https://git.tuffraid.net/cowch/lst_v3/commits/518c0a8c19a4bff0b757bbd06ca5460d3565d8bd))
### 📈 Project Builds
* **scripts:** changing how the relase works so it purposly builds before it trys to release ([83a542d](https://git.tuffraid.net/cowch/lst_v3/commits/83a542d1b7beafe394949c001917f2b25056fac2))
## [0.0.2-alpha.6](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.1...v0.0.2-alpha.6) (2026-04-23)
## [0.0.2-alpha.1](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.0...v0.0.2-alpha.1) (2026-04-23)

View File

@@ -1,6 +1,8 @@
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as scanUserSchema from "./schema/scanUsers.js";
const dbURL = `postgres://${process.env.DATABASE_USER}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}:${process.env.DATABASE_PORT}/${process.env.DATABASE_DB}`;
const queryClient = postgres(dbURL, {
@@ -13,4 +15,10 @@ const queryClient = postgres(dbURL, {
},
});
export const db = drizzle({ client: queryClient });
//export const db = drizzle({ client: queryClient });
export const db = drizzle(queryClient, {
schema: {
...scanUserSchema,
},
});

View File

@@ -0,0 +1,48 @@
import {
boolean,
jsonb,
pgEnum,
pgTable,
text,
timestamp,
unique,
uuid,
} from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type z from "zod";
export const mobileRoleEnum = pgEnum("mobile_role", [
"user",
"lead",
"manager",
"admin",
]);
export const scanUser = pgTable(
"scan_users",
{
id: uuid("id").defaultRandom().primaryKey(),
name: text("name").notNull(), // the user that will be using the scanner
scannerId: text("scanner_id").unique().notNull(),
pinNumber: text("pin_number").unique().notNull(),
pinHash: text("pin_hash").notNull(),
excludedCommand: jsonb("excluded_commands").default([]),
role: mobileRoleEnum("role").notNull().default("user"),
active: boolean("active").default(true),
lastScan: timestamp("last_scan").defaultNow(),
add_Date: timestamp("add_Date").defaultNow(),
upd_date: timestamp("upd_date").defaultNow(),
},
(table) => ({
userNotificationUnique: unique("scan_user_unique").on(
table.scannerId,
table.pinNumber,
),
}),
);
export const scanUserSchema = createSelectSchema(scanUser);
export const newsSanUserSchema = createInsertSchema(scanUser);
export type ScanUser = z.infer<typeof scanUserSchema>;
export type NewScanUser = z.infer<typeof newsSanUserSchema>;

View File

@@ -4,10 +4,12 @@ import type z from "zod";
export const scanLog = pgTable("scan_log", {
id: uuid("id").defaultRandom().primaryKey(),
user: text("user"),
scannerId: text("scanner_id"),
message: text("message").notNull(),
prompt: text("prompt"),
commandDescription: text("command_description"),
runningNumber: text("running_number").default("0"),
status: text("status"),
lines: jsonb("lines").default([]),
add_Date: timestamp("add_Date").defaultNow(),

View File

@@ -0,0 +1,46 @@
import fs from "node:fs";
import { Router } from "express";
import path from "path";
import { fileURLToPath } from "url";
const router = Router();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const downloadDir = path.resolve(__dirname, "../../downloads/mobile");
const currentApk = {
fileName: "lst-mobile.apk",
};
router.get("/latest", (_, res) => {
const apkPath = path.join(downloadDir, currentApk.fileName);
if (!fs.existsSync(apkPath)) {
return res.status(404).json({ success: false, message: "APK not found" });
}
res.setHeader("Content-Type", "application/vnd.android.package-archive");
res.setHeader(
"Content-Disposition",
`attachment; filename="${currentApk.fileName}"`,
);
return res.sendFile(apkPath);
});
router.get("/ehs", (_, res) => {
const apkPath = path.join(downloadDir, "EHS.apk");
if (!fs.existsSync(apkPath)) {
return res.status(404).json({ success: false, message: "APK not found" });
}
res.setHeader("Content-Type", "application/vnd.android.package-archive");
res.setHeader("Content-Disposition", `attachment; filename="EHS.apk}"`);
return res.sendFile(apkPath);
});
export default router;

View File

@@ -0,0 +1,37 @@
import { Router } from "express";
import { db } from "../db/db.controller.js";
import { scanLog } from "../db/schema/scanlog.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const router = Router();
router.post("/", async (req, res) => {
const body = req.body;
const newLog = await db
.insert(scanLog)
.values({
scannerId: body.scannerId,
message: body.message,
prompt: body.prompt,
commandDescription: body.commandDescription,
status: body.status,
lines: body.lines,
user: body.user,
runningNumber: body.runningNumber,
})
.returning();
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "scan logs",
message: `New log from ${body.scannerId}`,
data: newLog,
status: 200,
});
});
export default router;

View File

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

View File

@@ -0,0 +1,335 @@
import bcrypt from "bcryptjs";
import { eq, sql } from "drizzle-orm";
import { Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import {
type NewScanUser,
type ScanUser,
scanUser,
} from "../db/schema/scanUsers.js";
import { requireAuth } from "../middleware/auth.middleware.js";
import { apiReturn, returnFunc } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
export async function hashPin(pin: string) {
// if (!/^\d{6}$/.test(pin)) {
// throw new Error("PIN must be exactly 6 digits");
// }
return bcrypt.hashSync(pin, 12);
}
const registerSchema = z.object({
name: z.string().min(2).max(100),
pinNumber: z.string(),
scannerId: z
.string()
.min(1)
.max(500)
.optional()
.describe("if you leave blank it will be the same as your username"),
role: z
.enum(["user", "lead", "manager", "admin"])
.optional()
.describe("What roles are available to use."),
pinHash: z.string().optional(),
});
r.post("/pin", async (req, res) => {
const { pin } = req.body;
if (!pin || pin.length !== 6) {
return apiReturn(res, {
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `Pin number must be a min of 6 digits`,
data: [],
status: 401,
});
}
// const user = await db
// .select()
// .from(scanUser)
// .where(eq(scanUser.pinNumber, parseInt(pin, 10)));
const user = await db.query.scanUser.findFirst({
where: (u, { eq }) => eq(u.pinNumber, pin),
});
if (!user) {
return apiReturn(res, {
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `Invalid login please try again.`,
data: [],
status: 401,
});
}
const validPin = bcrypt.compareSync(pin, user.pinHash);
if (!validPin) {
return apiReturn(res, {
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `Invalid pin please try again.`,
data: [],
status: 401,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "auth",
message: `Welcome back ${user.name}`,
data: user as ScanUser | any,
status: 200,
});
});
r.post("/user", async (req, res) => {
try {
// validate the body is correct before accepting it
let validated = registerSchema.parse(req.body);
validated = {
...validated,
pinHash: await hashPin(validated.pinNumber.toString()),
};
const values: NewScanUser = {
name: validated.name,
pinNumber: validated.pinNumber,
pinHash: validated.pinHash ?? "",
scannerId: validated.scannerId ?? "",
};
const newUser = await db.insert(scanUser).values(values).returning();
apiReturn(res, {
success: true,
level: "info", //connect.success ? "info" : "error",
module: "mobile",
subModule: "auth",
message: `${validated.name} was just created`,
data: newUser as any,
status: 200, //connect.success ? 200 : 400,
});
} catch (err) {
if (err instanceof z.ZodError) {
const flattened = z.flattenError(err);
// return res.status(400).json({
// error: "Validation failed",
// details: flattened,
// });
return apiReturn(res, {
success: false,
level: "error", //connect.success ? "info" : "error",
module: "mobile",
subModule: "auth",
message: "Validation failed",
data: [flattened.fieldErrors],
status: 400, //connect.success ? 200 : 400,
});
}
return apiReturn(res, {
success: false,
level: "error", //connect.success ? "info" : "error",
module: "mobile",
subModule: "auth",
message:
"This User already exist with this pin or scanner id please try again",
data: [err],
status: 400, //connect.success ? 200 : 400,
});
}
});
r.get("/user", requireAuth, async (_, res) => {
const { data, error } = await tryCatch(db.select().from(scanUser));
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `There was an error getting the user`,
data: error as any,
status: 400,
});
}
if (!data) {
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "auth",
message: `There are no users you should add one . `,
data: [],
status: 200,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "auth",
message: `All users. `,
data,
status: 200,
});
});
r.patch("/user/:id", requireAuth, async (req, res) => {
const updates: Record<string, unknown | null> = {};
const { id } = req.params;
const { data, error } = await tryCatch(
db.query.scanUser.findFirst({
where: (u, { eq }) => eq(u.id, `${id}`),
}),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `There was an error getting the user`,
data: error as any,
status: 400,
});
}
if (!data) {
return apiReturn(res, {
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `Invalid user id was passed over. `,
data: [],
status: 400,
});
}
if (req.body?.name !== undefined) {
updates.name = req.body.name;
}
if (req.body?.pinNumber !== undefined) {
const existing = await db.query.scanUser.findFirst({
where: (u, { eq }) => eq(u.pinHash, req.body.pinNumber),
});
if (existing)
return returnFunc({
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `${req.body.pinNumber} already exists please try again`,
data: [],
notify: false,
room: "",
});
updates.pinNumber = req.body.pinNumber;
updates.pinHash = await hashPin(req.body.pinNumber);
}
if (req.body?.scannerId !== undefined) {
updates.scannerId = req.body.scannerId;
}
if (req.body?.active !== undefined) {
updates.active = req.body.active;
}
if (req.body?.excludedCommand !== undefined) {
updates.excludedCommand = req.body.excludedCommand;
}
if (req.body?.role !== undefined) {
updates.role = req.body.role;
}
updates.upd_date = sql`NOW()`;
const updatedSetting = await db
.update(scanUser)
.set(updates)
.where(eq(scanUser.id, `${id}`))
.returning();
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "user",
message: `User ${data.name} was updated. `,
data: updatedSetting,
status: 200,
});
});
r.delete("/user/:id", requireAuth, async (req, res) => {
const { id } = req.params;
const { data, error } = await tryCatch(
db.delete(scanUser).where(eq(scanUser.id, `${id}`)),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `There was an error deleting the user`,
data: error as any,
status: 400,
});
}
if (!data) {
return apiReturn(res, {
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `There was no user to delete. `,
data: [],
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "user",
message: `User was deleted. `,
data: data ?? [],
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,21 @@
import { Router } from "express";
import { generateUniquePin } from "../utils/generateScannerPin.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const r = Router();
r.get("/new", async (_, res) => {
const getPin = await generateUniquePin();
return apiReturn(res, {
success: getPin.success,
level: getPin.level,
module: "mobile",
subModule: "auth",
message: getPin.message,
data: getPin.data,
status: getPin.success ? 200 : 400,
});
});
export default r;

View File

@@ -0,0 +1,37 @@
import { Router } from "express";
import { db } from "../db/db.controller.js";
import { scanLog } from "../db/schema/scanlog.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const router = Router();
router.post("/", async (req, res) => {
const body = req.body;
const newLog = await db
.insert(scanLog)
.values({
scannerId: body.scannerId,
message: body.message,
prompt: body.prompt,
commandDescription: body.commandDescription,
status: body.status,
lines: body.lines,
user: body.user,
runningNumber: body.runningNumber,
})
.returning();
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "scan logs",
message: `New log from ${body.scannerId}`,
data: newLog,
status: 200,
});
});
export default router;

View File

@@ -0,0 +1,28 @@
import fs from "node:fs";
import { Router } from "express";
import path from "path";
const router = Router();
const projectRoot = path.resolve("./lstMobile"); // adjust as needed
const appJsonPath = path.join(projectRoot, "app.json");
router.get("/", async (req, res) => {
const baseUrl = `${req.protocol}://${req.get("host")}`;
const raw = fs.readFileSync(appJsonPath, "utf-8");
const config = JSON.parse(raw);
const exp = config.expo;
res.json({
packageName: exp.android?.package,
versionName: exp.version,
versionCode: exp.android?.versionCode,
minSupportedVersionCode: exp?.android?.minSupportedVersionCode ?? 0,
downloadUrl: `${baseUrl}/lst/api/mobile/apk/latest`,
});
});
export default router;

View File

@@ -0,0 +1,113 @@
import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { notifications } from "../db/schema/notifications.schema.js";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { returnFunc } from "../utils/returnHelper.utils.js";
import { sendEmail } from "../utils/sendEmail.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
/**
*
*/
const func = async (data: any, emails: string) => {
// get the actual notification as items will be updated between intervals if no one touches
const { data: l, error: le } = (await tryCatch(
db.select().from(notifications).where(eq(notifications.id, data.id)),
)) as any;
if (le) {
return returnFunc({
success: false,
level: "error",
module: "notification",
subModule: "query",
message: `${data.name} encountered an error while trying to get initial info`,
data: [le],
notify: true,
});
}
// search the query db for the query by name
const sqlQuery = sqlQuerySelector(`${data.name}`) as SqlQuery;
// create the ignore audit logs ids
const ignoreIds = l[0].options[0]?.auditId
? `${l[0].options[0]?.auditId}`
: "0";
// run the check
const { data: queryRun, error } = await tryCatch(
prodQuery(
sqlQuery.query
.replace("[intervalCheck]", l[0].interval)
.replace("[ignoreList]", ignoreIds),
`Running notification query: ${l[0].name}`,
),
);
if (error) {
return returnFunc({
success: false,
level: "error",
module: "notification",
subModule: "query",
message: `Data for: ${l[0].name} encountered an error while trying to get it`,
data: [error],
notify: true,
});
}
if (queryRun.data.length > 0) {
// update the latest audit id
const { error: dbe } = await tryCatch(
db
.update(notifications)
.set({ options: [{ auditId: `${queryRun.data[0].id}` }] })
.where(eq(notifications.id, data.id)),
);
if (dbe) {
return returnFunc({
success: false,
level: "error",
module: "notification",
subModule: "query",
message: `Data for: ${l[0].name} encountered an error while trying to get it`,
data: [dbe],
notify: true,
});
}
// send the email
const sentEmail = await sendEmail({
email: emails,
subject: "Alert! Label Reprinted",
template: "reprintLabels",
context: {
items: queryRun.data,
},
});
if (!sentEmail?.success) {
return returnFunc({
success: false,
level: "error",
module: "email",
subModule: "notification",
message: `${l[0].name} failed to send the email`,
data: [sentEmail],
notify: true,
});
}
} else {
console.log("doing nothing as there is nothing to do.");
}
// TODO send the error to systemAdmin users so they do not always need to be on the notifications.
// these errors are defined per notification.
};
export default func;

View File

@@ -5,6 +5,7 @@ import { setupAuthRoutes } from "./auth/auth.routes.js";
import { setupApiDocsRoutes } from "./configs/scaler.config.js";
import { setupDatamartRoutes } from "./datamart/datamart.routes.js";
import { setupGPSqlRoutes } from "./gpSql/gpSql.routes.js";
import { setupMobileRoutes } from "./mobile/mobile.routes.js";
import { setupNotificationRoutes } from "./notification/notification.routes.js";
import { setupOCPRoutes } from "./ocp/ocp.routes.js";
import { setupOpendockRoutes } from "./opendock/opendock.routes.js";
@@ -27,4 +28,5 @@ export const setupRoutes = (baseUrl: string, app: Express) => {
setupNotificationRoutes(baseUrl, app);
setupOCPRoutes(baseUrl, app);
setupTCPRoutes(baseUrl, app);
setupMobileRoutes(baseUrl, app);
};

View File

@@ -34,7 +34,7 @@ const servers: NewServerData[] = [
name: "Lima",
server: "USLIM1VMS006",
plantToken: "uslim1",
idAddress: "10.53.0.26",
idAddress: "10.53.0.26", // port opened 3000 2222
greatPlainsPlantCode: "50",
contactEmail: "",
contactPhone: "",
@@ -56,7 +56,7 @@ const servers: NewServerData[] = [
name: "Dayton",
server: "usday1VMS006",
plantToken: "usday1",
idAddress: "10.44.0.56", // 3000 opened and working
idAddress: "10.44.0.56", // ports opened 3000 and 2222
greatPlainsPlantCode: "80",
contactEmail: "",
contactPhone: "",
@@ -122,7 +122,7 @@ const servers: NewServerData[] = [
name: "Marked Tree",
server: "USMAR1VMS006",
plantToken: "usmar1",
idAddress: "10.206.9.26",
idAddress: "10.206.9.26", // 3000,2222 requested REQ0236838
greatPlainsPlantCode: "90",
contactEmail: "",
contactPhone: "",
@@ -144,7 +144,7 @@ const servers: NewServerData[] = [
name: "Bowling Green 1",
server: "USBOW1VMS006",
plantToken: "usbow1",
idAddress: "10.25.0.26", // 3000 is open REQ0236527
idAddress: "10.25.0.26", // 3000 is open REQ0236527 2222 already open
greatPlainsPlantCode: "55",
contactEmail: "",
contactPhone: "",

View File

@@ -1,93 +0,0 @@
import fs from "node:fs";
import { Router } from "express";
import path from "path";
import { fileURLToPath } from "url";
import { db } from "../db/db.controller.js";
import { scanLog } from "../db/schema/scanlog.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const router = Router();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const downloadDir = path.resolve(__dirname, "../../downloads/mobile");
const projectRoot = path.resolve("./lstMobile"); // adjust as needed
const appJsonPath = path.join(projectRoot, "app.json");
const currentApk = {
fileName: "lst-mobile.apk",
};
router.get("/version", async (req, res) => {
const baseUrl = `${req.protocol}://${req.get("host")}`;
const raw = fs.readFileSync(appJsonPath, "utf-8");
const config = JSON.parse(raw);
const exp = config.expo;
res.json({
packageName: exp.android?.package,
versionName: exp.version,
versionCode: exp.android?.versionCode,
minSupportedVersionCode: exp?.android?.minSupportedVersionCode ?? 0,
downloadUrl: `${baseUrl}/lst/api/mobile/apk/latest`,
});
});
router.get("/apk/latest", (_, res) => {
const apkPath = path.join(downloadDir, currentApk.fileName);
if (!fs.existsSync(apkPath)) {
return res.status(404).json({ success: false, message: "APK not found" });
}
res.setHeader("Content-Type", "application/vnd.android.package-archive");
res.setHeader(
"Content-Disposition",
`attachment; filename="${currentApk.fileName}"`,
);
return res.sendFile(apkPath);
});
router.get("/apk/ehs", (_, res) => {
const apkPath = path.join(downloadDir, "EHS.apk");
if (!fs.existsSync(apkPath)) {
return res.status(404).json({ success: false, message: "APK not found" });
}
res.setHeader("Content-Type", "application/vnd.android.package-archive");
res.setHeader("Content-Disposition", `attachment; filename="EHS.apk}"`);
return res.sendFile(apkPath);
});
router.post("/logs", async (req, res) => {
const body = req.body;
const newLog = await db
.insert(scanLog)
.values({
scannerId: body.data.scannerId,
message: body.data.message,
prompt: body.data.prompt,
commandDescription: body.data.commandDescription,
status: body.data.status,
lines: body.data.lines,
})
.returning();
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "scan logs",
message: `New log from ${body.data.scannerId}`,
data: newLog,
status: 200,
});
});
export default router;

View File

@@ -4,12 +4,10 @@ import getServers from "./serverData.route.js";
import getSettings from "./settings.route.js";
import updSetting from "./settingsUpdate.route.js";
import stats from "./stats.route.js";
import mobile from "./system.mobileApp.js";
export const setupSystemRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(`${baseUrl}/api/stats`, stats);
app.use(`${baseUrl}/api/mobile`, mobile);
app.use(`${baseUrl}/api/settings`, getSettings);
app.use(`${baseUrl}/api/servers`, getServers);
app.use(`${baseUrl}/api/settings`, requireAuth, updSetting);

View File

@@ -0,0 +1,39 @@
import { db } from "../db/db.controller.js";
import { returnFunc } from "./returnHelper.utils.js";
export function generateSixDigitPin() {
return Math.floor(100000 + Math.random() * 900000).toString();
}
export async function generateUniquePin() {
for (let i = 0; i < 10; i++) {
const pin = generateSixDigitPin();
const existing = await db.query.scanUser.findFirst({
where: (u, { eq }) => eq(u.pinHash, pin), // ⚠️ we'll fix this below
});
if (!existing)
return returnFunc({
success: true,
level: "info",
module: "utils",
subModule: "genPin",
message: "New pin generated",
data: [{ pin: pin }],
notify: false,
room: "",
});
}
return returnFunc({
success: false,
level: "error",
module: "utils",
subModule: "genPin",
message: "Failed to generate unique PIN after 10 attempts",
data: [],
notify: true,
room: "",
});
}

View File

@@ -18,7 +18,7 @@ services:
environment:
- NODE_ENV=production
- LOG_LEVEL=info
- EXTERNAL_URL=http://192.168.8.222:3600
- URL=http://localhost:3600
- DATABASE_HOST=postgres # if running on the same docker then do this
- DATABASE_PORT=5432
- DATABASE_USER=${DATABASE_USER}
@@ -41,7 +41,10 @@ services:
#for all host including prod servers, plc's, printers, or other de
networks:
- docker-network
- pgNetwork
networks:
docker-network:
external: true
pgNetwork:
external: true

View File

@@ -1,5 +1,5 @@
import { Link } from "@tanstack/react-router";
import { Bell, Logs, Server, Settings } from "lucide-react";
import { Bell, Logs, Server, Settings, UsersRound } from "lucide-react";
import {
SidebarGroup,
@@ -56,22 +56,22 @@ export default function AdminSidebar({ session }: any) {
module: "admin",
active: true,
},
// {
// title: "Modules",
// url: "/admin/modules",
// icon: Settings,
// role: ["systemAdmin", "admin"],
// module: "admin",
// active: true,
// },
// {
// title: "Servers",
// url: "/admin/servers",
// icon: Server,
// role: ["systemAdmin", "admin"],
// module: "admin",
// active: true,
// },
{
title: "Users",
url: "/admin/users",
icon: UsersRound,
role: ["systemAdmin", "admin"],
module: "admin",
active: true,
},
{
title: "Scan users",
url: "/admin/scanUsers",
icon: UsersRound,
role: ["systemAdmin", "admin"],
module: "admin",
active: true,
},
];
return (
<SidebarGroup>

View File

@@ -36,6 +36,17 @@ const docs = [
},
],
},
{
title: "Mobile",
url: "/updateInstructions",
isActive: false,
items: [
{
title: "Settings",
url: "/mobile-settings",
},
],
},
];
export default function DocBar() {
const { setOpen } = useSidebar();

View File

@@ -0,0 +1,49 @@
import { Link } from "@tanstack/react-router";
import { ScanText, ScrollText } from "lucide-react";
import {
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "../ui/sidebar";
export default function MobileBar({ session }: any) {
const { setOpen } = useSidebar();
const items = [
{
title: "Update Instructions",
url: "/",
icon: ScrollText,
},
{
title: "Scan Log",
url: "/",
icon: ScanText,
},
];
console.log(session);
return (
<SidebarGroup>
<SidebarGroupLabel>Mobile</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<Link to={item.url} onClick={() => setOpen(false)}>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
);
}

View File

@@ -8,6 +8,7 @@ import {
import { useSession } from "@/lib/auth-client";
import AdminSidebar from "./AdminBar";
import DocBar from "./DocBar";
import MobileBar from "./MobileBar";
export function AppSidebar() {
const { data: session } = useSession();
@@ -22,7 +23,8 @@ export function AppSidebar() {
<SidebarMenu>
<SidebarMenuItem>
<SidebarContent>
<DocBar/>
<DocBar />
<MobileBar session={session} />
{session &&
(session.user.role === "admin" ||
session.user.role === "systemAdmin") && (

View File

@@ -0,0 +1,3 @@
export default function updateInstructions() {
return <div>updateInstructions</div>;
}

View File

@@ -11,6 +11,15 @@ import {
import React, { useState } from "react";
import { Button } from "../../components/ui/button";
import { ScrollArea, ScrollBar } from "../../components/ui/scroll-area";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "../../components/ui/select";
import {
Table,
TableBody,
@@ -26,15 +35,23 @@ type LstTableType = {
tableClassName?: string;
data: any;
columns: any;
height?: string;
pageSize?: number;
};
export default function LstTable({
className = "",
tableClassName = "",
data,
columns,
height = "h-full",
pageSize = 5,
}: LstTableType) {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [pagination, setPagination] = useState({
pageIndex: 0, //initial page index
pageSize: pageSize, //default page size
});
//console.log(data);
const table = useReactTable({
@@ -46,24 +63,33 @@ export default function LstTable({
getSortedRowModel: getSortedRowModel(),
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
onPaginationChange: setPagination,
//renderSubComponent: ({ row }: { row: any }) => <ExpandedRow row={row} />,
//getRowCanExpand: () => true,
// columnResizeMode: "onChange",
filterFns: {},
state: {
sorting,
pagination,
columnFilters,
},
});
return (
<div className={className}>
<ScrollArea className="w-full rounded-md border whitespace-nowrap">
<div>{/* TODO: Add table header in here like title */}</div>
<ScrollArea
className={`w-full rounded-md border whitespace-nowrap ${height}`}
>
<Table className={cn("w-full", tableClassName)}>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
<TableHead
key={header.id}
className="sticky top-0 z-20 bg-background"
>
{header.isPlaceholder
? null
: flexRender(
@@ -76,6 +102,7 @@ export default function LstTable({
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
@@ -107,14 +134,23 @@ export default function LstTable({
<ScrollBar orientation="horizontal" />
<ScrollBar orientation="vertical" />
</ScrollArea>
<div className="flex items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => table.firstPage()}
disabled={!table.getCanPreviousPage()}
>
{"<<"}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
{"<"}
</Button>
<Button
variant="outline"
@@ -122,8 +158,42 @@ export default function LstTable({
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
{">"}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.lastPage()}
disabled={!table.getCanNextPage()}
>
{">>"}
</Button>
<Select
value={pagination.pageSize.toString()}
onValueChange={(e) =>
setPagination({
...pagination,
pageSize: e === "all" ? data.length : parseInt(e, 10),
})
}
>
<SelectTrigger className="w-16">
<SelectValue
//id={field.name}
placeholder="Select Page"
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Page Size</SelectLabel>
<SelectItem value="5">5</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem>
<SelectItem value="all">All</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
);

View File

@@ -15,6 +15,7 @@ import { Route as DocsIndexRouteImport } from './routes/docs/index'
import { Route as DocsSplatRouteImport } from './routes/docs/$'
import { Route as AdminSettingsRouteImport } from './routes/admin/settings'
import { Route as AdminServersRouteImport } from './routes/admin/servers'
import { Route as AdminScanUsersRouteImport } from './routes/admin/scanUsers'
import { Route as AdminNotificationsRouteImport } from './routes/admin/notifications'
import { Route as AdminLogsRouteImport } from './routes/admin/logs'
import { Route as authLoginRouteImport } from './routes/(auth)/login'
@@ -52,6 +53,11 @@ const AdminServersRoute = AdminServersRouteImport.update({
path: '/admin/servers',
getParentRoute: () => rootRouteImport,
} as any)
const AdminScanUsersRoute = AdminScanUsersRouteImport.update({
id: '/admin/scanUsers',
path: '/admin/scanUsers',
getParentRoute: () => rootRouteImport,
} as any)
const AdminNotificationsRoute = AdminNotificationsRouteImport.update({
id: '/admin/notifications',
path: '/admin/notifications',
@@ -89,6 +95,7 @@ export interface FileRoutesByFullPath {
'/login': typeof authLoginRoute
'/admin/logs': typeof AdminLogsRoute
'/admin/notifications': typeof AdminNotificationsRoute
'/admin/scanUsers': typeof AdminScanUsersRoute
'/admin/servers': typeof AdminServersRoute
'/admin/settings': typeof AdminSettingsRoute
'/docs/$': typeof DocsSplatRoute
@@ -103,6 +110,7 @@ export interface FileRoutesByTo {
'/login': typeof authLoginRoute
'/admin/logs': typeof AdminLogsRoute
'/admin/notifications': typeof AdminNotificationsRoute
'/admin/scanUsers': typeof AdminScanUsersRoute
'/admin/servers': typeof AdminServersRoute
'/admin/settings': typeof AdminSettingsRoute
'/docs/$': typeof DocsSplatRoute
@@ -118,6 +126,7 @@ export interface FileRoutesById {
'/(auth)/login': typeof authLoginRoute
'/admin/logs': typeof AdminLogsRoute
'/admin/notifications': typeof AdminNotificationsRoute
'/admin/scanUsers': typeof AdminScanUsersRoute
'/admin/servers': typeof AdminServersRoute
'/admin/settings': typeof AdminSettingsRoute
'/docs/$': typeof DocsSplatRoute
@@ -134,6 +143,7 @@ export interface FileRouteTypes {
| '/login'
| '/admin/logs'
| '/admin/notifications'
| '/admin/scanUsers'
| '/admin/servers'
| '/admin/settings'
| '/docs/$'
@@ -148,6 +158,7 @@ export interface FileRouteTypes {
| '/login'
| '/admin/logs'
| '/admin/notifications'
| '/admin/scanUsers'
| '/admin/servers'
| '/admin/settings'
| '/docs/$'
@@ -162,6 +173,7 @@ export interface FileRouteTypes {
| '/(auth)/login'
| '/admin/logs'
| '/admin/notifications'
| '/admin/scanUsers'
| '/admin/servers'
| '/admin/settings'
| '/docs/$'
@@ -177,6 +189,7 @@ export interface RootRouteChildren {
authLoginRoute: typeof authLoginRoute
AdminLogsRoute: typeof AdminLogsRoute
AdminNotificationsRoute: typeof AdminNotificationsRoute
AdminScanUsersRoute: typeof AdminScanUsersRoute
AdminServersRoute: typeof AdminServersRoute
AdminSettingsRoute: typeof AdminSettingsRoute
DocsSplatRoute: typeof DocsSplatRoute
@@ -230,6 +243,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AdminServersRouteImport
parentRoute: typeof rootRouteImport
}
'/admin/scanUsers': {
id: '/admin/scanUsers'
path: '/admin/scanUsers'
fullPath: '/admin/scanUsers'
preLoaderRoute: typeof AdminScanUsersRouteImport
parentRoute: typeof rootRouteImport
}
'/admin/notifications': {
id: '/admin/notifications'
path: '/admin/notifications'
@@ -281,6 +301,7 @@ const rootRouteChildren: RootRouteChildren = {
authLoginRoute: authLoginRoute,
AdminLogsRoute: AdminLogsRoute,
AdminNotificationsRoute: AdminNotificationsRoute,
AdminScanUsersRoute: AdminScanUsersRoute,
AdminServersRoute: AdminServersRoute,
AdminSettingsRoute: AdminSettingsRoute,
DocsSplatRoute: DocsSplatRoute,

View File

@@ -3,30 +3,36 @@ import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import { Toaster } from "sonner";
import Header from "@/components/Header";
import { AppSidebar } from "@/components/Sidebar/sidebar";
import { SidebarProvider } from "@/components/ui/sidebar";
import { SidebarProvider } from "@/components/ui/sidebar";
import { ThemeProvider } from "@/lib/theme-provider";
import { useSession } from "../lib/auth-client";
const RootLayout = () => (
<div className="[--header-height:calc(--spacing(14))]">
<ThemeProvider>
<SidebarProvider className="flex flex-col" defaultOpen={false}>
<Header />
const RootLayout = () => {
const { data: session } = useSession();
return (
<div className="[--header-height:calc(--spacing(14))]">
<ThemeProvider>
<SidebarProvider className="flex flex-col" defaultOpen={false}>
<Header />
<div className="relative min-h-[calc(100svh-var(--header-height))]">
<AppSidebar />
<div className="relative min-h-[calc(100svh-var(--header-height))]">
<AppSidebar />
<main className="w-full p-4">
<div className="mx-auto w-full max-w-7xl">
<Outlet />
</div>
</main>
</div>
<main className="w-full p-4">
<div className="mx-auto w-full max-w-7xl">
<Outlet />
</div>
</main>
</div>
<Toaster expand richColors closeButton />
</SidebarProvider>
</ThemeProvider>
<TanStackRouterDevtools />
</div>
);
<Toaster expand richColors closeButton />
</SidebarProvider>
</ThemeProvider>
{session && session.user.role === "systemAdmin" && (
<TanStackRouterDevtools />
)}
</div>
);
};
export const Route = createRootRoute({ component: RootLayout });

View File

@@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/admin/scanUsers')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/admin/scanUsers"!</div>
}

View File

@@ -155,7 +155,7 @@ const ServerTable = () => {
);
}
return <LstTable data={data} columns={columns} />;
return <LstTable data={data} columns={columns} pageSize={50} />;
};
function RouteComponent() {

View File

@@ -59,6 +59,33 @@ function RouteComponent() {
Only shows machines that are attached to the silo.
</ul>
</ul>
{/* Mobile stuff */}
<li>Mobile App</li>
<ul className="list-disc list-inside indent-8">
<li>Rewrite of Alpla scan</li>
<ul className="list-disc list-inside indent-16">
<li>All old settings same as before id, ip, port</li>
<li>Currently scanned pallets will show now as well</li>
</ul>
<li>
Custom addition - login and more features NOTE: This is activated
based on how you enter the settings
</li>
<ul className="list-disc list-inside indent-16">
<li>Pin numbers login</li>
<li>
Scan a lane barcode and it returns whats in the lane and its
current status
</li>
<li>Command restrictions per pin login</li>
<li>Dock Door scanning</li>
<li>
More details on the pallet that is scanned by touching the running
number on the scanner.
</li>
</ul>
</ul>
{/* TMS integration */}
<li>TMS integration</li>
<ul className="list-disc list-inside indent-8">
<li>integration with TI to auto add in orders</li>

View File

@@ -15,8 +15,8 @@
"foregroundImage": "./assets/adaptive-icon-white.png",
"backgroundColor": "#ffffff"
},
"versionCode": 21,
"minSupportedVersionCode": 21,
"versionCode": 30,
"minSupportedVersionCode": 26,
"predictiveBackGestureEnabled": false,
"package": "net.alpla.lst.mobile"
},
@@ -26,7 +26,7 @@
"bundler": "metro"
},
"plugins": [
"./plugins/withZebraScanner",
"./plugins/withZebraDataWedge",
"expo-router",
[
"expo-splash-screen",

Binary file not shown.

View File

@@ -9,7 +9,7 @@
"ios": "expo run:ios",
"web": "expo start --web",
"lint": "expo lint",
"build:apk:clean": "expo prebuild --clean && cd android && gradlew.bat assembleRelease && npm run copy:apk",
"build:apk:clean": "expo prebuild --clean && cd android && gradlew.bat clean && 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:bump": "cd scripts && node runBuild.ts --bump",

View File

@@ -145,34 +145,32 @@ class ZebraScannerModule(
Thread.sleep(500)
val barcodeConfig = Bundle().apply {
putString("PLUGIN_NAME", "BARCODE")
putString("RESET_CONFIG", "true")
val barcodeConfig = Bundle().apply {
putString("PLUGIN_NAME", "BARCODE")
putString("RESET_CONFIG", "true")
val isLegacyTc8000 =
android.os.Build.MODEL.contains("TC8000", ignoreCase = true)
val props = Bundle().apply {
putString("scanner_input_enabled", "true")
val props = Bundle().apply {
putString("scanner_input_enabled", "true")
// Baseline that should be safe on old and new Zebra devices
putString("scanner_selection", "auto")
if (!isLegacyTc8000) {
// Newer Zebra devices
// Auto-select internal scanner
putString("scanner_selection", "auto")
putString("scanner_selection_by_identifier", "AUTO")
// Hardware trigger behavior
putString("hardware_trigger_enabled", "true")
putString("trigger_mode", "2") // HARD trigger
putString("trigger_mode", "2") // 2 = HARD trigger
// Disable Zebra's loud initial decode feedback
putString("decode_audio_feedback_uri", "")
putString("decode_haptic_feedback", "false")
putString("decode_led_feedback", "false")
}
}
putBundle("PARAM_LIST", props)
}
// add in wake on trigger
putString("trigger_wakeup_scan", "true");
}
putBundle("PARAM_LIST", props)
}
val intentConfig = Bundle().apply {
putString("PLUGIN_NAME", "INTENT")

View File

@@ -1,9 +1,34 @@
import { Tabs } from "expo-router";
import { Home, Settings } from "lucide-react-native";
import { Redirect, Tabs } from "expo-router";
import { Container, Home, Logs, Rows4, Settings } from "lucide-react-native";
import { useAppStore } from "../../hooks/useAppStore";
import { useMobileAuthStore } from "../../hooks/useMobileAuth";
// const roles = {
// adminOnly: ["admin"],
// management: ["admin", "manager"],
// allStaff: ["admin", "manager", "driver", "lead", "user"],
// };
export default function TabsLayout() {
const serverPort = useAppStore((s) => s.serverPort);
const user = useMobileAuthStore((s) => s.user);
const isUnlocked = useMobileAuthStore((s) => s.isUnlocked);
const port = parseInt(serverPort || "0", 10) >= 50000;
console.log(port);
if (!port) {
if (!user || !isUnlocked) {
return <Redirect href="/login" />;
}
}
const isNormalScanner = parseInt(serverPort || "0", 10) >= 50000;
const hasRole = (allowed: string[] = []) => {
const role = user?.role?.toLowerCase();
return role ? allowed.includes(role) : false;
};
return (
<Tabs
screenOptions={{
@@ -27,11 +52,24 @@ export default function TabsLayout() {
}}
/>
<Tabs.Screen
name="config"
name="laneCheck"
options={{
title: "settings",
title: "Lane Check",
href: isNormalScanner ? null : "/(tabs)/laneCheck",
tabBarIcon: ({ color, size }) => <Rows4 size={size} color={color} />,
}}
/>
<Tabs.Screen
name="dockScan"
options={{
title: "Dock scan",
href:
isNormalScanner || !hasRole(["admin", "manager"])
? null
: "/(tabs)/dockScan",
tabBarIcon: ({ color, size }) => (
<Settings size={size} color={color} />
<Container size={size} color={color} />
),
}}
/>
@@ -40,7 +78,10 @@ export default function TabsLayout() {
options={{
title: "Logs",
href:
parseInt(serverPort || "0", 10) >= 50000 ? null : "/(tabs)/logs",
isNormalScanner || !hasRole(["admin", "manager"])
? null
: "/(tabs)/logs",
tabBarIcon: ({ color, size }) => <Logs size={size} color={color} />,
}}
/>
{/* <Tabs.Screen
@@ -51,6 +92,15 @@ export default function TabsLayout() {
parseInt(serverPort || "0", 10) >= 50000 ? null : "/(tabs)/logs",
}}
/> */}
<Tabs.Screen
name="config"
options={{
title: "settings",
tabBarIcon: ({ color, size }) => (
<Settings size={size} color={color} />
),
}}
/>
</Tabs>
);
}

View File

@@ -1,7 +1,5 @@
import { Link } from "expo-router";
import { Text, View } from "react-native";
import Setup from "../setup";
export default function SettingsTab() {
return <Setup />
}
return <Setup />;
}

View File

@@ -0,0 +1,26 @@
import React from "react";
import { Text, View } from "react-native";
import { Button } from "../../components/ui/button";
export default function LaneCheck() {
const getInfo = async () => {
const info = "ho";
console.log(info);
};
return (
<View
style={{
flex: 1,
//justifyContent: "center",
alignItems: "center",
marginTop: 50,
}}
>
<Text>Dock Scanning</Text>
<Button onPress={getInfo}>
<Text>Check info</Text>
</Button>
</View>
);
}

View File

@@ -0,0 +1,37 @@
import React, { useCallback, useEffect } from "react";
import { Text, View } from "react-native";
import { Button } from "../../components/ui/button";
import { type ZebraScanResult, zebraScanner } from "../../lib/ZebraScanner";
export default function LaneCheck() {
const handleScan = useCallback(async (scan: ZebraScanResult) => {
console.log(scan);
}, []);
useEffect(() => {
zebraScanner.ensureProfile();
zebraScanner.startListening();
const sub = zebraScanner.addScanListener((scan) => {
//console.log("SCAN:", scan);
handleScan(scan);
});
return () => {
sub.remove();
zebraScanner.stopListening();
};
}, [handleScan]);
return (
<View
style={{
flex: 1,
//justifyContent: "center",
alignItems: "center",
marginTop: 50,
}}
>
<Text>LaneChecks</Text>
</View>
);
}

View File

@@ -1,13 +1,26 @@
import React from 'react'
import { Text, View } from 'react-native'
import React from "react";
import { Text, View } from "react-native";
import { Button } from "../../components/ui/button";
export default function Logs() {
return (
<View style={{
const getInfo = async () => {
const info = "ho";
console.log(info);
};
return (
<View
style={{
flex: 1,
//justifyContent: "center",
alignItems: "center",
marginTop: 50,
}}><Text>Logs</Text></View>
)
}}
>
<Text>Logs</Text>
<Button onPress={getInfo}>
<Text>Check info</Text>
</Button>
</View>
);
}

View File

@@ -1,11 +1,9 @@
import React from "react";
import { View } from "react-native";
import { useAppStore } from "../../hooks/useAppStore";
import ProdScanner from "../../components/ProdScanner";
import { View } from "react-native";
import LSTScanner from "../../components/LSTScanner";
import ProdScanner from "../../components/ProdScanner";
import { useAppStore } from "../../hooks/useAppStore";
export default function scanner() {
export default function Scanner() {
const serverPort = useAppStore((s) => s.serverPort);
return (
<View
@@ -16,7 +14,11 @@ export default function scanner() {
marginTop: 50,
}}
>
{parseInt(serverPort || "0", 10) >= 50000 ? <ProdScanner /> : <LSTScanner />}
{parseInt(serverPort || "0", 10) >= 50000 ? (
<ProdScanner />
) : (
<LSTScanner />
)}
</View>
);
}

View File

@@ -1,26 +1,21 @@
import { PortalHost } from "@rn-primitives/portal";
import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import "../../global.css";
import { PortalHost } from "@rn-primitives/portal";
import { View } from "react-native";
import useDeviceLock from "../hooks/useDeviceCheck";
export default function RootLayout() {
useDeviceLock();
return (
<>
<StatusBar style="dark" />
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" />
<View className="items-center">
<Stack.Screen
name="(tabs)"
options={{
title: "Pending update",
headerStyle: {
backgroundColor: "lightblue",
},
}}
/>
</View>
<Stack.Screen name="login" />
<Stack.Screen name="setup" />
<Stack.Screen name="updateScreen" />
<Stack.Screen name="(tabs)" />
</Stack>
<PortalHost />
</>

View File

@@ -1,122 +1,31 @@
import axios from "axios";
import Constants from "expo-constants";
import { Redirect, useRouter } from "expo-router";
import { useEffect, useState } from "react";
import { Redirect } from "expo-router";
import { ActivityIndicator, Text, View } from "react-native";
import { useAppStore } from "../hooks/useAppStore";
import { useServerStore } from "../hooks/useServerCheck";
import { devDelay } from "../lib/devMode";
import { useAppStartup } from "../hooks/useAppStartup";
const startupMessages = {
loading: "Loading app...",
validating: "Validating data...",
scannerMode: "Checking scanner mode...",
normalScanner: "Starting normal ALPLAprod scanner that has no LST rules",
checkingUpdates: "Checking for updates...",
opening: "Opening LST scan app...",
error: "Something went wrong during startup.",
};
export default function Index() {
const router = useRouter();
const [message, setMessage] = useState(<Text>Starting app...</Text>);
const [ready, setReady] = useState(false);
const setServerVersion = useServerStore((s) => s.setServerVersion);
const { ready, startupRoute, status } = useAppStartup();
const hasHydrated = useAppStore((s) => s.hasHydrated);
const serverPort = useAppStore((s) => s.serverPort);
const serverIp = useAppStore((s) => s.serverIp);
const build = Constants.expoConfig?.android?.versionCode ?? 1;
const hasValidSetup = useAppStore((s) => s.hasValidSetup);
useEffect(() => {
if (!hasHydrated) {
setMessage(<Text>Loading app...</Text>);
return;
}
const startup = async () => {
try {
await devDelay(1500);
setMessage(<Text>Validating data...</Text>);
await devDelay(1500);
if (!hasValidSetup()) {
router.replace("/setup");
return;
}
// checking for lst.
console.log(
`http://${serverIp}:${parseInt(serverPort || "0", 10) >= 50000 ? "3000" : serverPort}/lst/api/mobile/version`,
);
try {
const res = await axios.get(
`http://${serverIp}:${parseInt(serverPort || "0", 10) >= 50000 ? "3000" : serverPort}/lst/api/mobile/version`,
{
timeout: 5000,
},
);
console.log(res.data);
// if the build version dose not match the latest server version force update
if (res.status === 200) {
setServerVersion(res.data);
}
// TODO: change the header to show orange and theres a new version
// console.log(build < res.data.minSupportedVersionCode);
// if (build < res.data.minSupportedVersionCode) {
// router.replace("/updateScreen");
// return;
// }
} catch (error) {
console.log("Error: ", error);
}
setMessage(<Text>Checking scanner mode...</Text>);
await devDelay(1500);
if (parseInt(serverPort || "0", 10) >= 50000) {
setMessage(
<Text>
Starting normal alplaprod scanner that has no LST rules
</Text>,
);
await devDelay(1500);
//router.replace("/scanner");
setReady(true);
return;
}
setMessage(<Text>Checking for updates</Text>);
await devDelay(1500);
// TODO if theres an update go to update screen message :D
setMessage(<Text>Opening LST scan app</Text>);
await devDelay(3250);
//router.replace("/scanner");
setReady(true);
} catch (error) {
console.log("Startup error", error);
setMessage(<Text>Something went wrong during startup.</Text>);
}
};
startup();
}, [
hasHydrated,
hasValidSetup,
serverPort,
serverIp,
router,
setServerVersion,
]);
if (ready && startupRoute) {
return <Redirect href={startupRoute as any} />;
}
if (ready) {
return <Redirect href="/(tabs)/scanner" />;
return <Redirect href="/login" />;
}
return (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
marginTop: 12,
}}
>
{message}
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text>{startupMessages[status]}</Text>
<ActivityIndicator size="large" />
</View>
);

View File

@@ -0,0 +1,86 @@
import axios from "axios";
import { useRouter } from "expo-router";
import { useState } from "react";
import { Alert, Button, Text, View } from "react-native";
import { Input } from "../components/ui/input";
import { useAppStore } from "../hooks/useAppStore";
import { useMobileAuthStore } from "../hooks/useMobileAuth";
export default function Login() {
// doing this causes rerender and sub
//const { setUser } = useMobileAuthStore();
const [pin, setPin] = useState("");
const serverPort = useAppStore((s) => s.serverPort);
const serverIp = useAppStore((s) => s.serverIp);
const router = useRouter();
const onLogin = async () => {
if (pin.length < 6) {
console.log("pin must be min 6 ");
}
console.log(pin);
try {
const res = await axios.post(
`http://${serverIp}:${parseInt(serverPort || "0", 10) >= 50000 ? "3000" : serverPort}/lst/api/mobile/auth/pin`,
{ pin },
{
timeout: 5000,
},
);
if (res.status === 200) {
// this way to set the user is direct and basically a 1 shot
useMobileAuthStore.getState().setUser(res.data.data);
return router.replace("/(tabs)/scanner");
}
} catch (error) {
console.log(error);
Alert.alert("Login Error", `Invalid pin please try again`);
}
};
const config = () => {
console.log("config");
return router.replace("/setup");
};
return (
<View
style={{
flex: 1,
//justifyContent: "center",
alignItems: "center",
marginTop: 50,
}}
>
<View className="flex items-center m-5">
<Text style={{ fontSize: 20, fontWeight: "600" }}>
LST Scanner Login
</Text>
<View className="w-64 p-4">
<Input
className="w-fit"
keyboardType="number-pad"
textContentType="oneTimeCode"
placeholder="Pin number"
onChangeText={setPin}
/>
</View>
</View>
<View>
<Text className="p-3">
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
time.
</Text>
</View>
<View className="flex gap-2 flex-row">
<Button title="Login" onPress={onLogin} />
<Button title="Config" onPress={config} />
</View>
</View>
);
}

View File

@@ -25,6 +25,8 @@ export default function Setup() {
const server = useServerStore((s) => s.serverVersion);
// TODO: if on lst version and the user is manager or admin just login
const authCheck = () => {
if (pin === "6971") {
setAuth(true);
@@ -151,13 +153,13 @@ export default function Setup() {
marginTop: "auto",
alignItems: "center",
padding: 10,
marginBottom: 12,
marginBottom: 50,
}}
>
<Text className="text-[12] color-#666">
<Text className="text-sm color-[#312f2f]">
App v{version}-{build}
</Text>
<Text className="text-[12] color-#666">
<Text className="text-sm color-[#312f2f]">
Server version - v{server?.versionName}-{server?.versionCode}
</Text>
</View>

View File

@@ -1,24 +1,236 @@
import React from 'react'
import { View, Text } from 'react-native'
import axios from "axios";
import { format } from "date-fns-tz";
import { useCallback, useEffect, useState } from "react";
import { Alert, Button, Text, View } from "react-native";
import { useAppStore } from "../hooks/useAppStore";
import { useMobileAuthStore } from "../hooks/useMobileAuth";
import { useScannerStore } from "../hooks/useScannerStore";
import { scannerFeedback } from "../lib/feedbackScan";
import { sendTcpMessage } from "../lib/tcpScan";
import { versionCheck } from "../lib/versionValidation";
import { type ZebraScanResult, zebraScanner } from "../lib/ZebraScanner";
import { ScannedLabelBox } from "./ScannedLabels";
import { GlobalFooter } from "./UpdateFooter";
import { Separator } from "./ui/separator";
const STX = "\x02";
const ETX = "\x03";
const formatName = (name?: string) =>
name ? name.charAt(0).toUpperCase() + name.slice(1).toLowerCase() : "";
export default function LSTScanner() {
return (
<View><View style={{ alignItems: "center", margin: 10 }}>
<Text style={{ fontSize: 20, fontWeight: "600" }}>LST Scanner</Text>
</View>
<View
style={{
marginTop: 50,
alignItems: "center",
}}
>
<Text>Relocate</Text>
<Text>0 / 4</Text>
</View>
{/* <View>
<Text>List of recent scanned pallets TBA</Text>
</View> */}
</View>
)
const user = useMobileAuthStore((s) => s.user);
const logout = useMobileAuthStore((s) => s.logout);
// TODO : move to off tcp stuff after od
const lastScan = useScannerStore((s) => s.lastScan);
const setLastScan = useScannerStore((s) => s.setLastScan);
const [tagScans, setTagScans] = useState<any>([]);
const scannerIdFromStore = useAppStore((s) => s.scannerId);
const serverIp = useAppStore((s) => s.serverIp);
const serverPort = useAppStore((s) => s.serverPort);
const [bgColor, setBGColor] = useState<string | null>(null);
const handleScan = useCallback(
async (scan: ZebraScanResult) => {
await scannerFeedback({
type: "scan",
sound: true,
vibrate: true,
led: true,
});
const isAlphaStart = /^[a-zA-Z]/.test(scan.data);
const isExcluded = (user?.excludedCommand ?? []).some((cmd) =>
scan.data.toLowerCase().includes(cmd.toLowerCase()),
);
console.log(user?.excludedCommand);
if (isAlphaStart && isExcluded) {
Alert.alert(
"Command not allowed",
`Command: ${scan.data}\n\nPlease contact logistics if this is an error`,
);
return;
}
let commandToSend = `${STX}${user?.scannerId}@${scan.data}${ETX}`;
// if we are sscc we need to scan like this .... <STX>98@]C100090087710038712256<ETX>
if (scan.data.startsWith("000")) {
commandToSend = `${STX}${user?.scannerId}@]C1${scan.data}${ETX}`;
setTagScans((prev: any) => [
{
label: parseInt(scan.data.slice(10, -1) || "000", 10).toString(),
date: format(new Date(Date.now()), "HH:mm"),
},
...prev,
]);
}
const scanned = (await sendTcpMessage(
commandToSend,
serverIp,
50004,
)) as any;
// send the logs to lst but allow it to time out if it dose not exist just bc.
try {
await axios.post(`http://${serverIp.trim()}:3000/lst/api/mobile/logs`, {
scannerId: user?.scannerId ?? "0",
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()
: "0",
});
} catch (error) {
console.log(error);
}
// const response = await sendTcpMessage(tcpMessage);
console.log(scanned.data);
if (scanned.data.status !== "error") {
await scannerFeedback({
type: "good",
sound: true,
vibrate: true,
led: true,
});
setBGColor("bg-green-500");
// version check
versionCheck();
// auth update
useMobileAuthStore.getState().updateLastScan();
setTimeout(() => {
setBGColor(null);
}, 1 * 1000);
}
if (scanned.data.status === "error") {
await scannerFeedback({
type: scanned.data.status === "error" ? "bad" : "good",
sound: true,
vibrate: true,
led: true,
});
setBGColor("bg-red-500");
setTimeout(() => {
setBGColor(null);
}, 1 * 1000);
}
setLastScan(scanned.data);
// if we change commands we want to zero out the last scanned labels
if (isAlphaStart) {
setTagScans([]);
}
},
[
serverIp,
setLastScan,
user?.scannerId,
user?.name,
user?.excludedCommand?.some,
user?.excludedCommand,
],
);
const clearScans = () => {
setTagScans([]);
};
//console.log(lastScan);
useEffect(() => {
zebraScanner.ensureProfile();
zebraScanner.startListening();
const sub = zebraScanner.addScanListener((scan) => {
//console.log("SCAN:", scan);
handleScan(scan);
});
return () => {
sub.remove();
zebraScanner.stopListening();
};
}, [handleScan]);
return (
<View className={`${bgColor ?? ""} flex-1 w-screen`}>
<View style={{ alignItems: "center", margin: 5 }}>
<Text style={{ fontSize: 14, fontWeight: "600" }}>
User: {formatName(user?.name ?? "")}
</Text>
<Text style={{ fontSize: 18, fontWeight: "600" }}>
LST Scanner id: {user?.scannerId}
</Text>
<View
style={{
marginTop: 5,
alignItems: "center",
}}
>
{!lastScan ? (
<View style={{ marginTop: 10, alignItems: "center" }}>
<Text className="text-xl font-bold">Ready to scan</Text>
<Text>Waiting for first scan...</Text>
</View>
) : (
<View
style={{
marginTop: 10,
alignItems: "center",
}}
>
{lastScan.lines
?.filter((line) => !/^\d+@$/.test(line))
.map((i) => {
return (
<View
style={{ marginTop: 10, alignItems: "center" }}
key={i}
>
<Text style={{ fontSize: 18, fontWeight: "600" }}>
{i}
</Text>
</View>
);
})}
</View>
)}
</View>
</View>
<Separator className="m-2" />
<View className="flex-1 w-full px-4">
<ScannedLabelBox
labels={tagScans}
color={bgColor}
clearScan={clearScans}
/>
</View>
<View className="m-2">
{user && (
<View className="items-center">
<Button title="Logout" onPress={logout} />
</View>
)}
</View>
<View>
<GlobalFooter />
</View>
</View>
);
}

View File

@@ -3,9 +3,11 @@ import { format } from "date-fns-tz";
import { useCallback, useEffect, useState } from "react";
import { Text, View } from "react-native";
import { useAppStore } from "../hooks/useAppStore";
import { useMobileAuthStore } from "../hooks/useMobileAuth";
import { useScannerStore } from "../hooks/useScannerStore";
import { scannerFeedback } from "../lib/feedbackScan";
import { sendTcpMessage } from "../lib/tcpScan";
import { versionCheck } from "../lib/versionValidation";
import { type ZebraScanResult, zebraScanner } from "../lib/ZebraScanner";
import { ScannedLabelBox } from "./ScannedLabels";
import { GlobalFooter } from "./UpdateFooter";
@@ -25,6 +27,13 @@ export default function ProdScanner() {
const handleScan = useCallback(
async (scan: ZebraScanResult) => {
await scannerFeedback({
type: "scan",
sound: true,
vibrate: true,
led: true,
});
let commandToSend = `${STX}${scannerIdFromStore}@${scan.data}${ETX}`;
// if we are sscc we need to scan like this .... <STX>98@]C100090087710038712256<ETX>
@@ -45,11 +54,16 @@ export default function ProdScanner() {
parseInt(serverPort || "0", 10),
)) as any;
// 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()
: "0",
};
try {
await axios.post(
`http://${serverIp.trim()}:3000/lst/api/mobile/logs`,
scanned,
data,
);
} catch (error) {
console.log(error);
@@ -64,6 +78,13 @@ export default function ProdScanner() {
led: true,
});
setBGColor("bg-green-500");
// version check
versionCheck();
// auth update
useMobileAuthStore.getState().updateLastScan();
setTimeout(() => {
setBGColor(null);
}, 1 * 1000);
@@ -137,7 +158,7 @@ export default function ProdScanner() {
.map((i) => {
return (
<View style={{ marginTop: 10, alignItems: "center" }} key={i}>
<Text style={{ fontSize: 20, fontWeight: "600" }}>{i}</Text>
<Text style={{ fontSize: 18, fontWeight: "600" }}>{i}</Text>
</View>
);
})}

View File

@@ -0,0 +1,29 @@
import { cn } from '@/lib/utils';
import { Platform, TextInput } from 'react-native';
function Input({ className, ...props }: React.ComponentProps<typeof TextInput> & React.RefAttributes<TextInput>) {
return (
<TextInput
className={cn(
'dark:bg-input/30 border-input bg-background text-foreground flex h-10 w-full min-w-0 flex-row items-center rounded-md border px-3 py-1 text-base leading-5 shadow-sm shadow-black/5 sm:h-9',
props.editable === false &&
cn(
'opacity-50',
Platform.select({ web: 'disabled:pointer-events-none disabled:cursor-not-allowed' })
),
Platform.select({
web: cn(
'placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground outline-none transition-[color,box-shadow] md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive'
),
native: 'placeholder:text-muted-foreground/50',
}),
className
)}
{...props}
/>
);
}
export { Input };

View File

@@ -0,0 +1,111 @@
import axios from "axios";
import Constants from "expo-constants";
import { useEffect, useRef, useState } from "react";
import { devDelay } from "../lib/devMode";
import { versionCheck } from "../lib/versionValidation";
import { useAppStore } from "./useAppStore";
import { useServerStore } from "./useServerCheck";
type StartupStatus =
| "loading"
| "validating"
| "scannerMode"
| "normalScanner"
| "checkingUpdates"
| "opening"
| "error";
export function useAppStartup() {
const [ready, setReady] = useState(false);
const [status, setStatus] = useState<StartupStatus>("loading");
const [startupRoute, setStartupRoute] = useState<string | null>(null);
const hasRunKey = useRef<string | null>(null);
const hasHydrated = useAppStore((s) => s.hasHydrated);
const serverPort = useAppStore((s) => s.serverPort);
const serverIp = useAppStore((s) => s.serverIp);
useEffect(() => {
if (!hasHydrated) {
setStatus("loading");
return;
}
const runKey = `${serverIp}:${serverPort}`;
if (hasRunKey.current === runKey) {
return;
}
hasRunKey.current = runKey;
let cancelled = false;
const startup = async () => {
try {
setReady(false);
setStartupRoute(null);
await devDelay(1500);
if (cancelled) return;
setStatus("validating");
await devDelay(1500);
if (cancelled) return;
const hasValidSetup = useAppStore.getState().hasValidSetup;
if (!hasValidSetup()) {
setStartupRoute("/setup");
setReady(true);
return;
}
await versionCheck();
setStatus("scannerMode");
await devDelay(1500);
if (cancelled) return;
if (parseInt(serverPort || "0", 10) >= 50000) {
setStatus("normalScanner");
await devDelay(1500);
setStartupRoute("/scanner");
setReady(true);
return;
}
setStatus("checkingUpdates");
console.log("checking updates");
await devDelay(1500);
if (cancelled) return;
setStatus("opening");
console.log("opening");
await devDelay(1500);
if (cancelled) return;
setStartupRoute("/(tabs)/scanner");
console.log("scanner");
setReady(true);
} catch (error) {
console.log("Startup error:", error);
setStatus("error");
}
};
startup();
return () => {
cancelled = true;
};
}, [hasHydrated, serverIp, serverPort]);
return {
ready,
startupRoute,
status,
};
}

View File

@@ -0,0 +1,37 @@
import { useEffect, useRef, useState } from "react";
import { AppState, type AppStateStatus } from "react-native";
import { useMobileAuthStore } from "./useMobileAuth";
export default function useDeviceLock() {
const [appState, setAppState] = useState<AppStateStatus>(
AppState.currentState,
);
const appStateRef = useRef<AppStateStatus>(AppState.currentState);
useEffect(() => {
const subscription = AppState.addEventListener("change", (nextAppState) => {
const previousAppState = appStateRef.current;
const wasActive = previousAppState === "active";
// if the we see aggressive locking then we should remove inactive.
const isNowInactive =
nextAppState === "background" || nextAppState === "inactive";
if (wasActive && isNowInactive) {
const auth = useMobileAuthStore.getState();
if (auth.shouldLockForIdle()) {
auth.lock();
}
}
appStateRef.current = nextAppState;
setAppState(nextAppState);
});
return () => subscription.remove();
}, []);
return appState;
}

View File

@@ -0,0 +1,52 @@
import { create } from "zustand";
const ONE_HOUR = 1000 * 60 * 60;
type MobileUser = {
id: string;
name: string;
role: "user" | "lead" | "manager" | "admin";
excludedCommand: string[];
scannerId: string;
};
type AuthState = {
user: MobileUser | null;
isUnlocked: boolean;
lastScanAt: number | null;
setUser: (user: MobileUser) => void;
updateLastScan: () => void;
lock: () => void;
logout: () => void;
shouldLockForIdle: () => boolean;
};
export const useMobileAuthStore = create<AuthState>((set, get) => ({
user: null,
isUnlocked: false,
lastScanAt: null,
setUser: (user) =>
set({
user,
isUnlocked: true,
lastScanAt: Date.now(),
}),
updateLastScan: () => set({ lastScanAt: Date.now() }),
lock: () => set({ 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

@@ -28,9 +28,9 @@ export const zebraScanner = {
ZebraScanner.triggerScan();
},
ensureProfile() {
ZebraScanner.ensureProfile();
},
ensureProfile() {
ZebraScanner.ensureProfile();
},
addScanListener(
callback: (scan: ZebraScanResult) => void,

View File

@@ -0,0 +1,13 @@
const roleRank = {
user: 1,
lead: 2,
manager: 3,
admin: 4,
} as const;
export function hasMobileRole(
userRole: keyof typeof roleRank,
requiredRole: keyof typeof roleRank,
) {
return roleRank[userRole] >= roleRank[requiredRole];
}

View File

@@ -2,12 +2,13 @@ import { createAudioPlayer } from "expo-audio";
import * as Haptics from "expo-haptics";
export type ScanFeedback = {
type: "good" | "bad";
type: "good" | "bad" | "scan";
sound?: boolean;
vibrate?: boolean;
led?: boolean;
};
const scan = createAudioPlayer(require("../../assets/sounds/scan.wav"));
const goodSound = createAudioPlayer(require("../../assets/sounds/good.wav"));
const badSound = createAudioPlayer(require("../../assets/sounds/bad.wav"));
@@ -18,14 +19,15 @@ export async function scannerFeedback({
led = true,
}: ScanFeedback) {
if (sound) {
const player = type === "good" ? goodSound : badSound;
const player =
type === "scan" ? scan : type === "good" ? goodSound : badSound;
player.seekTo(0);
player.play();
}
if (vibrate) {
await Haptics.notificationAsync(
type === "good"
type === "good" || type === "scan"
? Haptics.NotificationFeedbackType.Success
: Haptics.NotificationFeedbackType.Error,
);

View File

@@ -1,43 +1,73 @@
import axios from "axios";
import { useAppStore } from "../hooks/useAppStore";
import { useServerStore } from "../hooks/useServerCheck";
export type ServerVersionInfo = {
packageName: string;
versionName: string;
versionCode: number;
minSupportedVersionCode: number;
fileName: string;
packageName: string;
versionName: string;
versionCode: number;
minSupportedVersionCode: number;
fileName: string;
};
export type StartupStatus =
| { state: "checking" }
| { state: "needs-config" }
| { state: "offline" }
| { state: "blocked"; reason: string; server: ServerVersionInfo }
| { state: "warning"; message: string; server: ServerVersionInfo }
| { state: "ready"; server: ServerVersionInfo | null };
| { state: "checking" }
| { state: "needs-config" }
| { state: "offline" }
| { state: "blocked"; reason: string; server: ServerVersionInfo }
| { state: "warning"; message: string; server: ServerVersionInfo }
| { state: "ready"; server: ServerVersionInfo | null };
export function evaluateVersion(
appBuildCode: number,
server: ServerVersionInfo
appBuildCode: number,
server: ServerVersionInfo,
): StartupStatus {
if (appBuildCode < server.minSupportedVersionCode) {
return {
state: "blocked",
reason: "This scanner app is too old and must be updated before use.",
server,
};
}
if (appBuildCode < server.minSupportedVersionCode) {
return {
state: "blocked",
reason: "This scanner app is too old and must be updated before use.",
server,
};
}
if (appBuildCode !== server.versionCode) {
return {
state: "warning",
message: `A newer version is available. Installed build: ${appBuildCode}, latest build: ${server.versionCode}.`,
server,
};
}
if (appBuildCode !== server.versionCode) {
return {
state: "warning",
message: `A newer version is available. Installed build: ${appBuildCode}, latest build: ${server.versionCode}.`,
server,
};
}
return {
state: "ready",
server,
};
}
return {
state: "ready",
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,14 @@
CREATE TYPE "public"."mobile_role" AS ENUM('user', 'lead', 'manager', 'admin');--> statement-breakpoint
CREATE TABLE "scan_users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" text NOT NULL,
"scanner_id" integer NOT NULL,
"pin_number" integer NOT NULL,
"pin_hash" text NOT NULL,
"excluded_commands" text DEFAULT '',
"role" "mobile_role" DEFAULT 'user' NOT NULL,
"active" boolean DEFAULT true,
"last_scan" timestamp DEFAULT now(),
"add_Date" timestamp DEFAULT now(),
"upd_date" timestamp DEFAULT now()
);

View File

@@ -0,0 +1,3 @@
ALTER TABLE "scan_users" ADD CONSTRAINT "scan_users_scanner_id_unique" UNIQUE("scanner_id");--> statement-breakpoint
ALTER TABLE "scan_users" ADD CONSTRAINT "scan_users_pin_number_unique" UNIQUE("pin_number");--> statement-breakpoint
ALTER TABLE "scan_users" ADD CONSTRAINT "scan_user_unique" UNIQUE("scanner_id","pin_number");

View File

@@ -0,0 +1,2 @@
ALTER TABLE "scan_users" ALTER COLUMN "scanner_id" SET DATA TYPE text;--> statement-breakpoint
ALTER TABLE "scan_users" ALTER COLUMN "pin_number" SET DATA TYPE text;

View File

@@ -0,0 +1,3 @@
ALTER TABLE "scan_users" ALTER COLUMN "excluded_commands" SET DATA TYPE jsonb;--> statement-breakpoint
ALTER TABLE "scan_users" ALTER COLUMN "excluded_commands" SET DEFAULT '';--> statement-breakpoint
ALTER TABLE "scan_log" ADD COLUMN "user" text;

View File

@@ -0,0 +1 @@
ALTER TABLE "scan_users" ALTER COLUMN "excluded_commands" SET DEFAULT '[]'::jsonb;

View File

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

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

File diff suppressed because it is too large Load Diff

View File

@@ -295,6 +295,48 @@
"when": 1777509638464,
"tag": "0041_bright_tempest",
"breakpoints": true
},
{
"idx": 42,
"version": "7",
"when": 1777659968051,
"tag": "0042_melted_talon",
"breakpoints": true
},
{
"idx": 43,
"version": "7",
"when": 1777664911423,
"tag": "0043_melted_lyja",
"breakpoints": true
},
{
"idx": 44,
"version": "7",
"when": 1777666145468,
"tag": "0044_steady_magneto",
"breakpoints": true
},
{
"idx": 45,
"version": "7",
"when": 1778059667805,
"tag": "0045_quick_khan",
"breakpoints": true
},
{
"idx": 46,
"version": "7",
"when": 1778059910210,
"tag": "0046_chemical_the_leader",
"breakpoints": true
},
{
"idx": 47,
"version": "7",
"when": 1778068577325,
"tag": "0047_spotty_queen_noir",
"breakpoints": true
}
]
}

361
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "lst_v3",
"version": "0.0.2-alpha.6",
"version": "0.0.2-alpha.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "lst_v3",
"version": "0.0.2-alpha.6",
"version": "0.0.2-alpha.9",
"license": "ISC",
"dependencies": {
"@dotenvx/dotenvx": "^1.57.0",
@@ -14,6 +14,7 @@
"@socket.io/admin-ui": "^0.5.1",
"archiver": "^7.0.1",
"axios": "^1.13.6",
"bcryptjs": "^3.0.3",
"better-auth": "^1.5.5",
"concurrently": "^9.2.1",
"cors": "^2.8.6",
@@ -26,6 +27,7 @@
"express": "^5.2.1",
"husky": "^9.1.7",
"ldapts": "^8.1.7",
"modbus-serial": "^8.0.25",
"morgan": "^1.10.1",
"mssql": "^12.2.1",
"multer": "^2.1.1",
@@ -2116,6 +2118,263 @@
"node": ">=22"
}
},
"node_modules/@serialport/binding-mock": {
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-10.2.2.tgz",
"integrity": "sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==",
"license": "MIT",
"optional": true,
"dependencies": {
"@serialport/bindings-interface": "^1.2.1",
"debug": "^4.3.3"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/@serialport/bindings-cpp": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-13.0.0.tgz",
"integrity": "sha512-r25o4Bk/vaO1LyUfY/ulR6hCg/aWiN6Wo2ljVlb4Pj5bqWGcSRC4Vse4a9AcapuAu/FeBzHCbKMvRQeCuKjzIQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@serialport/bindings-interface": "1.2.2",
"@serialport/parser-readline": "12.0.0",
"debug": "4.4.0",
"node-addon-api": "8.3.0",
"node-gyp-build": "4.8.4"
},
"engines": {
"node": ">=18.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-delimiter": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-12.0.0.tgz",
"integrity": "sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-readline": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-12.0.0.tgz",
"integrity": "sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==",
"license": "MIT",
"optional": true,
"dependencies": {
"@serialport/parser-delimiter": "12.0.0"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/bindings-cpp/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"optional": true,
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/@serialport/bindings-interface": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz",
"integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^12.22 || ^14.13 || >=16"
}
},
"node_modules/@serialport/parser-byte-length": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-13.0.0.tgz",
"integrity": "sha512-32yvqeTAqJzAEtX5zCrN1Mej56GJ5h/cVFsCDPbF9S1ZSC9FWjOqNAgtByseHfFTSTs/4ZBQZZcZBpolt8sUng==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-cctalk": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-13.0.0.tgz",
"integrity": "sha512-RErAe57g9gvnlieVYGIn1xymb1bzNXb2QtUQd14FpmbQQYlcrmuRnJwKa1BgTCujoCkhtaTtgHlbBWOxm8U2uA==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-delimiter": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-13.0.0.tgz",
"integrity": "sha512-Qqyb0FX1avs3XabQqNaZSivyVbl/yl0jywImp7ePvfZKLwx7jBZjvL+Hawt9wIG6tfq6zbFM24vzCCK7REMUig==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-inter-byte-timeout": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-13.0.0.tgz",
"integrity": "sha512-a0w0WecTW7bD2YHWrpTz1uyiWA2fDNym0kjmPeNSwZ2XCP+JbirZt31l43m2ey6qXItTYVuQBthm75sPVeHnGA==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-packet-length": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-13.0.0.tgz",
"integrity": "sha512-60ZDDIqYRi0Xs2SPZUo4Jr5LLIjtb+rvzPKMJCohrO6tAqSDponcNpcB1O4W21mKTxYjqInSz+eMrtk0LLfZIg==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=8.6.0"
}
},
"node_modules/@serialport/parser-readline": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-13.0.0.tgz",
"integrity": "sha512-dov3zYoyf0dt1Sudd1q42VVYQ4WlliF0MYvAMA3MOyiU1IeG4hl0J6buBA2w4gl3DOCC05tGgLDN/3yIL81gsA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@serialport/parser-delimiter": "13.0.0"
},
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-ready": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-13.0.0.tgz",
"integrity": "sha512-JNUQA+y2Rfs4bU+cGYNqOPnNMAcayhhW+XJZihSLQXOHcZsFnOa2F9YtMg9VXRWIcnHldHYtisp62Etjlw24bw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-regex": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-13.0.0.tgz",
"integrity": "sha512-m7HpIf56G5XcuDdA3DB34Z0pJiwxNRakThEHjSa4mG05OnWYv0IG8l2oUyYfuGMowQWaVnQ+8r+brlPxGVH+eA==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-slip-encoder": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-13.0.0.tgz",
"integrity": "sha512-fUHZEExm6izJ7rg0A1yjXwu4sOzeBkPAjDZPfb+XQoqgtKAk+s+HfICiYn7N2QU9gyaeCO8VKgWwi+b/DowYOg==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-spacepacket": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-13.0.0.tgz",
"integrity": "sha512-DoXJ3mFYmyD8X/8931agJvrBPxqTaYDsPoly9/cwQSeh/q4EjQND9ySXBxpWz5WcpyCU4jOuusqCSAPsbB30Eg==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/stream": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-13.0.0.tgz",
"integrity": "sha512-F7xLJKsjGo2WuEWMSEO1SimRcOA+WtWICsY13r0ahx8s2SecPQH06338g28OT7cW7uRXI7oEQAk62qh5gHJW3g==",
"license": "MIT",
"optional": true,
"dependencies": {
"@serialport/bindings-interface": "1.2.2",
"debug": "4.4.0"
},
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/stream/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"optional": true,
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/@simple-libs/child-process-utils": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz",
@@ -2172,6 +2431,12 @@
"socket.io": ">=3.1.0"
}
},
"node_modules/@socket.io/admin-ui/node_modules/bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
"license": "MIT"
},
"node_modules/@socket.io/admin-ui/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
@@ -3145,10 +3410,13 @@
"license": "MIT"
},
"node_modules/bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
"license": "MIT"
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
"license": "BSD-3-Clause",
"bin": {
"bcrypt": "bin/bcrypt"
}
},
"node_modules/better-auth": {
"version": "1.5.5",
@@ -9124,6 +9392,18 @@
"node": ">=10"
}
},
"node_modules/modbus-serial": {
"version": "8.0.25",
"resolved": "https://registry.npmjs.org/modbus-serial/-/modbus-serial-8.0.25.tgz",
"integrity": "sha512-T6OHW80k7DtYZF96onavw84IXNu44EW+fybgVftWAGOraL8vTmMZod8w6thOrWj2I2qHC9Gsn2nitVTUDih+6A==",
"license": "ISC",
"dependencies": {
"debug": "^4.4.3"
},
"optionalDependencies": {
"serialport": "^13.0.0"
}
},
"node_modules/modify-values": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz",
@@ -9396,6 +9676,28 @@
"smart-buffer": "^4.1.0"
}
},
"node_modules/node-addon-api": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.0.tgz",
"integrity": "sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"license": "MIT",
"optional": true,
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/nodemailer": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.3.tgz",
@@ -10741,6 +11043,53 @@
"node": ">= 18"
}
},
"node_modules/serialport": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/serialport/-/serialport-13.0.0.tgz",
"integrity": "sha512-PHpnTd8isMGPfFTZNCzOZp9m4mAJSNWle9Jxu6BPTcWq7YXl5qN7tp8Sgn0h+WIGcD6JFz5QDgixC2s4VW7vzg==",
"license": "MIT",
"optional": true,
"dependencies": {
"@serialport/binding-mock": "10.2.2",
"@serialport/bindings-cpp": "13.0.0",
"@serialport/parser-byte-length": "13.0.0",
"@serialport/parser-cctalk": "13.0.0",
"@serialport/parser-delimiter": "13.0.0",
"@serialport/parser-inter-byte-timeout": "13.0.0",
"@serialport/parser-packet-length": "13.0.0",
"@serialport/parser-readline": "13.0.0",
"@serialport/parser-ready": "13.0.0",
"@serialport/parser-regex": "13.0.0",
"@serialport/parser-slip-encoder": "13.0.0",
"@serialport/parser-spacepacket": "13.0.0",
"@serialport/stream": "13.0.0",
"debug": "4.4.0"
},
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/serialport/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"optional": true,
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/serve-static": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "lst_v3",
"version": "0.0.2-alpha.6",
"version": "0.0.2-alpha.9",
"description": "The tool that supports us in our everyday alplaprod",
"main": "index.js",
"scripts": {
@@ -69,6 +69,7 @@
"@socket.io/admin-ui": "^0.5.1",
"archiver": "^7.0.1",
"axios": "^1.13.6",
"bcryptjs": "^3.0.3",
"better-auth": "^1.5.5",
"concurrently": "^9.2.1",
"cors": "^2.8.6",
@@ -81,6 +82,7 @@
"express": "^5.2.1",
"husky": "^9.1.7",
"ldapts": "^8.1.7",
"modbus-serial": "^8.0.25",
"morgan": "^1.10.1",
"mssql": "^12.2.1",
"multer": "^2.1.1",