Compare commits
9 Commits
3734d9daac
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d6328ab764 | |||
| a6d53f0266 | |||
| 7962463927 | |||
| f716de1a58 | |||
| 88cef2a56c | |||
| cb00addee9 | |||
| b832d7aa1e | |||
| 32517d0c98 | |||
| 82f8369640 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -149,3 +149,4 @@ dist
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
frontend/.tanstack/tmp/2249110e-da91fb0b1b87b6c4cc3e2c2cd25037fd
|
||||
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"workbench.colorTheme": "Default Dark+",
|
||||
"workbench.colorTheme": "Dark+",
|
||||
"terminal.integrated.env.windows": {},
|
||||
"editor.formatOnSave": true,
|
||||
"typescript.preferences.importModuleSpecifier": "relative",
|
||||
|
||||
38
backend/admin/admin.build.ts
Normal file
38
backend/admin/admin.build.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* To be able to run this we need to set our dev pc in the .env.
|
||||
* if its empty just ignore it. this will just be the double catch
|
||||
*/
|
||||
|
||||
import { Router } from "express";
|
||||
import { build, building } from "../utils/build.utils.js";
|
||||
import { apiReturn } from "../utils/returnHelper.utils.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post("/release", async (_, res) => {
|
||||
if (!building) {
|
||||
build();
|
||||
|
||||
return apiReturn(res, {
|
||||
success: true,
|
||||
level: "info",
|
||||
module: "admin",
|
||||
subModule: "build",
|
||||
message: `The build has been triggered see logs for progress of the current build.`,
|
||||
data: [],
|
||||
status: 200,
|
||||
});
|
||||
} else {
|
||||
return apiReturn(res, {
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "admin",
|
||||
subModule: "build",
|
||||
message: `There is a build in progress already please check the logs for on going progress.`,
|
||||
data: [],
|
||||
status: 200,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
12
backend/admin/admin.routes.ts
Normal file
12
backend/admin/admin.routes.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { Express } from "express";
|
||||
import { requireAuth } from "../middleware/auth.middleware.js";
|
||||
import build from "./admin.build.js";
|
||||
import update from "./admin.updateServer.js";
|
||||
|
||||
export const setupAdminRoutes = (baseUrl: string, app: Express) => {
|
||||
//stats will be like this as we dont need to change this
|
||||
app.use(`${baseUrl}/api/admin/build`, requireAuth, build);
|
||||
app.use(`${baseUrl}/api/admin/build`, requireAuth, update);
|
||||
|
||||
// all other system should be under /api/system/*
|
||||
};
|
||||
86
backend/admin/admin.updateServer.ts
Normal file
86
backend/admin/admin.updateServer.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* To be able to run this we need to set our dev pc in the .env.
|
||||
* if its empty just ignore it. this will just be the double catch
|
||||
*/
|
||||
|
||||
import { Router } from "express";
|
||||
import z from "zod";
|
||||
import { building } from "../utils/build.utils.js";
|
||||
import { runUpdate, updating } from "../utils/deployApp.js";
|
||||
import { apiReturn } from "../utils/returnHelper.utils.js";
|
||||
|
||||
const updateServer = z.object({
|
||||
server: z.string(),
|
||||
destination: z.string(),
|
||||
token: z.string().min(5, "Plant tokens should be at least 5 characters long"),
|
||||
});
|
||||
|
||||
const router = Router();
|
||||
|
||||
type Update = {
|
||||
success: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
router.post("/updateServer", async (req, res) => {
|
||||
try {
|
||||
const validated = updateServer.parse(req.body);
|
||||
|
||||
if (!updating && !building) {
|
||||
const update = (await runUpdate({
|
||||
server: validated.server,
|
||||
destination: validated.destination,
|
||||
token: validated.token,
|
||||
})) as Update;
|
||||
return apiReturn(res, {
|
||||
success: update.success,
|
||||
level: update.success ? "info" : "error",
|
||||
module: "admin",
|
||||
subModule: "update",
|
||||
message: update.message,
|
||||
data: [],
|
||||
status: 200,
|
||||
});
|
||||
} else {
|
||||
return apiReturn(res, {
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "admin",
|
||||
subModule: "update",
|
||||
message: `${validated.server}: ${validated.token} is already being updated, or is currently building the app.`,
|
||||
data: [],
|
||||
status: 200,
|
||||
});
|
||||
}
|
||||
} 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: "routes",
|
||||
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: "routes",
|
||||
subModule: "auth",
|
||||
message: "Internal Server Error creating user",
|
||||
data: [err],
|
||||
status: 400, //connect.success ? 200 : 400,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -3,8 +3,13 @@ import type sql from "mssql";
|
||||
const username = "gpviewer";
|
||||
const password = "gp$$ViewOnly!";
|
||||
|
||||
const port = process.env.SQL_PORT
|
||||
? Number.parseInt(process.env.SQL_PORT, 10)
|
||||
: undefined;
|
||||
|
||||
export const gpSqlConfig: sql.config = {
|
||||
server: `USMCD1VMS011`,
|
||||
server: `${process.env.GP_SERVER ?? "USMCD1VMS011"}`,
|
||||
port: port,
|
||||
database: `ALPLA`,
|
||||
user: username,
|
||||
password: password,
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import type sql from "mssql";
|
||||
|
||||
const port = process.env.SQL_PORT
|
||||
? Number.parseInt(process.env.SQL_PORT, 10)
|
||||
: undefined;
|
||||
|
||||
export const prodSqlConfig: sql.config = {
|
||||
server: `${process.env.PROD_SERVER}`,
|
||||
database: `AlplaPROD_${process.env.PROD_PLANT_TOKEN}_cus`,
|
||||
port: port,
|
||||
user: process.env.PROD_USER,
|
||||
password: process.env.PROD_PASSWORD,
|
||||
options: {
|
||||
|
||||
@@ -116,10 +116,17 @@ export const runDatamartQuery = async (data: Data) => {
|
||||
|
||||
// for queries that will need to be ran on legacy until we get the plant updated need to go in here
|
||||
const doubleQueries = ["inventory"];
|
||||
const sqlQuery = sqlQuerySelector(
|
||||
`datamart.${fd.data[0].activated > 0 && !doubleQueries.includes(data.name) ? data.name : `legacy.${data.name}`}`,
|
||||
) as SqlQuery;
|
||||
let queryFile = "";
|
||||
|
||||
if (doubleQueries.includes(data.name)) {
|
||||
queryFile = `datamart.${
|
||||
fd.data[0].activated > 0 ? data.name : `legacy.${data.name}`
|
||||
}`;
|
||||
} else {
|
||||
queryFile = `datamart.${data.name}`;
|
||||
}
|
||||
|
||||
const sqlQuery = sqlQuerySelector(queryFile) as SqlQuery;
|
||||
// checking if warehousing is as it will start to effect a lot of queries for plants that are not on 2.
|
||||
|
||||
const getDataMartInfo = datamartData.filter((x) => x.endpoint === data.name);
|
||||
@@ -172,6 +179,12 @@ export const runDatamartQuery = async (data: Data) => {
|
||||
data.options.articles
|
||||
? `and r.ArticleHumanReadableId in (${data.options.articles})`
|
||||
: "--and r.ArticleHumanReadableId in ([articles]) ",
|
||||
)
|
||||
.replace(
|
||||
"and DeliveredQuantity > 0",
|
||||
data.options.all
|
||||
? "--and DeliveredQuantity > 0"
|
||||
: "and DeliveredQuantity > 0",
|
||||
);
|
||||
|
||||
break;
|
||||
@@ -201,10 +214,15 @@ export const runDatamartQuery = async (data: Data) => {
|
||||
"--,l.MachineLocation,l.MachineName,l.ProductionLotRunningNumber as lot",
|
||||
`${data.options.lots ? `,l.MachineLocation,l.MachineName,l.ProductionLotRunningNumber as lot` : `--,l.MachineLocation,l.MachineName,l.ProductionLotRunningNumber as lot`}`,
|
||||
)
|
||||
.replaceAll(
|
||||
"--,l.MachineLocation,l.MachineName,l.ProductionLotRunningNumber",
|
||||
`${data.options.lots ? `,l.MachineLocation,l.MachineName,l.ProductionLotRunningNumber` : `--,l.MachineLocation,l.MachineName,l.ProductionLotRunningNumber`}`,
|
||||
)
|
||||
.replaceAll(
|
||||
"--,l.WarehouseDescription,l.LaneDescription",
|
||||
`${data.options.locations ? `,l.WarehouseDescription,l.LaneDescription` : `--,l.WarehouseDescription,l.LaneDescription`}`,
|
||||
);
|
||||
|
||||
break;
|
||||
case "fakeEDIUpdate":
|
||||
datamartQuery = datamartQuery.replace(
|
||||
@@ -231,7 +249,6 @@ export const runDatamartQuery = async (data: Data) => {
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case "psiDeliveryData":
|
||||
datamartQuery = datamartQuery
|
||||
.replace("[startDate]", `${data.options.startDate}`)
|
||||
|
||||
10
backend/db/schema/buildHistory.schema.ts
Normal file
10
backend/db/schema/buildHistory.schema.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { integer, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
||||
|
||||
export const deploymentHistory = pgTable("deployment_history", {
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
serverId: uuid("server_id"),
|
||||
buildNumber: integer("build_number").notNull(),
|
||||
status: text("status").notNull(), // started, success, failed
|
||||
message: text("message"),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
});
|
||||
40
backend/db/schema/serverData.schema.ts
Normal file
40
backend/db/schema/serverData.schema.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
boolean,
|
||||
integer,
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
||||
import type z from "zod";
|
||||
|
||||
export const serverData = pgTable(
|
||||
"server_data",
|
||||
{
|
||||
server_id: uuid("id").defaultRandom().primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
server: text("server"),
|
||||
plantToken: text("plant_token").notNull().unique(),
|
||||
idAddress: text("id_address"),
|
||||
greatPlainsPlantCode: text("great_plains_plant_code"),
|
||||
contactEmail: text("contact_email"),
|
||||
contactPhone: text("contact_phone"),
|
||||
active: boolean("active").default(true),
|
||||
serverLoc: text("server_loc"),
|
||||
lastUpdated: timestamp("last_updated").defaultNow(),
|
||||
buildNumber: integer("build_number"),
|
||||
isUpgrading: boolean("is_upgrading").default(false),
|
||||
},
|
||||
|
||||
// (table) => [
|
||||
// // uniqueIndex('emailUniqueIndex').on(sql`lower(${table.email})`),
|
||||
// uniqueIndex("plant_token").on(table.plantToken),
|
||||
// ],
|
||||
);
|
||||
|
||||
export const serverDataSchema = createSelectSchema(serverData);
|
||||
export const newServerDataSchema = createInsertSchema(serverData);
|
||||
|
||||
export type ServerDataSchema = z.infer<typeof serverDataSchema>;
|
||||
export type NewServerData = z.infer<typeof newServerDataSchema>;
|
||||
@@ -1,10 +1,27 @@
|
||||
import type { InferSelectModel } from "drizzle-orm";
|
||||
import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||
import {
|
||||
boolean,
|
||||
integer,
|
||||
jsonb,
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
||||
import type z from "zod";
|
||||
|
||||
export const serverStats = pgTable("stats", {
|
||||
id: text("id").primaryKey().default("serverStats"),
|
||||
build: integer("build").notNull().default(1),
|
||||
lastUpdate: timestamp("last_update").defaultNow(),
|
||||
export const appStats = pgTable("app_stats", {
|
||||
id: text("id").primaryKey().default("primary"),
|
||||
currentBuild: integer("current_build").notNull().default(1),
|
||||
lastBuildAt: timestamp("last_build_at"),
|
||||
lastDeployAt: timestamp("last_deploy_at"),
|
||||
building: boolean("building").notNull().default(false),
|
||||
updating: boolean("updating").notNull().default(false),
|
||||
lastUpdated: timestamp("last_updated").defaultNow(),
|
||||
meta: jsonb("meta").$type<Record<string, unknown>>().default({}),
|
||||
});
|
||||
|
||||
export type ServerStats = InferSelectModel<typeof serverStats>;
|
||||
export const appStatsSchema = createSelectSchema(appStats);
|
||||
export const newAppStatsSchema = createInsertSchema(appStats, {});
|
||||
|
||||
export type AppStats = z.infer<typeof appStatsSchema>;
|
||||
export type NewAppStats = z.infer<typeof newAppStatsSchema>;
|
||||
|
||||
@@ -53,13 +53,14 @@ export const connectGPSql = async () => {
|
||||
notify: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
reconnectToSql;
|
||||
return returnFunc({
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "system",
|
||||
subModule: "db",
|
||||
message: "Failed to connect to the prod sql server.",
|
||||
message: "Failed to connect to the gp sql server.",
|
||||
data: [error],
|
||||
notify: false,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Express } from "express";
|
||||
|
||||
import { setupAdminRoutes } from "./admin/admin.routes.js";
|
||||
import { setupAuthRoutes } from "./auth/auth.routes.js";
|
||||
// import the routes and route setups
|
||||
import { setupApiDocsRoutes } from "./configs/scaler.config.js";
|
||||
@@ -16,6 +16,7 @@ import { setupUtilsRoutes } from "./utils/utils.routes.js";
|
||||
export const setupRoutes = (baseUrl: string, app: Express) => {
|
||||
//routes that are on by default
|
||||
setupSystemRoutes(baseUrl, app);
|
||||
setupAdminRoutes(baseUrl, app);
|
||||
setupApiDocsRoutes(baseUrl, app);
|
||||
setupProdSqlRoutes(baseUrl, app);
|
||||
setupGPSqlRoutes(baseUrl, app);
|
||||
|
||||
@@ -15,6 +15,7 @@ import { opendockSocketMonitor } from "./opendock/opendockSocketMonitor.utils.js
|
||||
import { connectProdSql } from "./prodSql/prodSqlConnection.controller.js";
|
||||
import { monitorAlplaPurchase } from "./purchase/purchase.controller.js";
|
||||
import { setupSocketIORoutes } from "./socket.io/serverSetup.js";
|
||||
import { serversChecks } from "./system/serverData.controller.js";
|
||||
import { baseSettingValidationCheck } from "./system/settingsBase.controller.js";
|
||||
import { startTCPServer } from "./tcpServer/tcp.server.js";
|
||||
import { createCronJob } from "./utils/croner.utils.js";
|
||||
@@ -70,6 +71,7 @@ const start = async () => {
|
||||
// one shots only needed to run on server startups
|
||||
createNotifications();
|
||||
startNotifications();
|
||||
serversChecks();
|
||||
}, 5 * 1000);
|
||||
|
||||
process.on("uncaughtException", async (err) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ type RoomDefinition<T = unknown> = {
|
||||
|
||||
export const protectedRooms: any = {
|
||||
logs: { requiresAuth: true, role: ["admin", "systemAdmin"] },
|
||||
admin: { requiresAuth: true, role: ["admin", "systemAdmin"] },
|
||||
//admin: { requiresAuth: false, role: ["admin", "systemAdmin"] },
|
||||
};
|
||||
|
||||
export const roomDefinition: Record<RoomId, RoomDefinition> = {
|
||||
@@ -36,4 +36,16 @@ export const roomDefinition: Record<RoomId, RoomDefinition> = {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
admin: {
|
||||
seed: async (limit) => {
|
||||
console.info(limit);
|
||||
return [];
|
||||
},
|
||||
},
|
||||
"admin:build": {
|
||||
seed: async (limit) => {
|
||||
console.info(limit);
|
||||
return [];
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -88,14 +88,12 @@ export const setupSocketIORoutes = (baseUrl: string, server: HttpServer) => {
|
||||
});
|
||||
}
|
||||
|
||||
const roles = Array.isArray(config.role) ? config.role : [config.role];
|
||||
|
||||
console.log(roles, s.user.role);
|
||||
const roles = Array.isArray(config?.role) ? config?.role : [config?.role];
|
||||
|
||||
//if (config?.role && s.user?.role !== config.role) {
|
||||
if (config?.role && !roles.includes(s.user?.role)) {
|
||||
return s.emit("room-error", {
|
||||
room: rn,
|
||||
roomId: rn,
|
||||
message: `Not authorized to be in room: ${rn}`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export type RoomId = "logs" | "labels"; //| "alerts" | "metrics";
|
||||
export type RoomId = "logs" | "labels" | "admin" | "admin:build"; //| "alerts" | "metrics";
|
||||
|
||||
154
backend/system/serverData.controller.ts
Normal file
154
backend/system/serverData.controller.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { db } from "../db/db.controller.js";
|
||||
import {
|
||||
type NewServerData,
|
||||
serverData,
|
||||
} from "../db/schema/serverData.schema.js";
|
||||
import { createLogger } from "../logger/logger.controller.js";
|
||||
import { tryCatch } from "../utils/trycatch.utils.js";
|
||||
|
||||
const servers: NewServerData[] = [
|
||||
{
|
||||
name: "Test server 1",
|
||||
server: "USMCD1VMS036",
|
||||
plantToken: "test3",
|
||||
idAddress: "10.193.0.56",
|
||||
greatPlainsPlantCode: "00",
|
||||
contactEmail: "",
|
||||
contactPhone: "",
|
||||
serverLoc: "D$\\LST_V3",
|
||||
buildNumber: 1,
|
||||
},
|
||||
{
|
||||
name: "Test server 2",
|
||||
server: "USIOW1VMS036",
|
||||
plantToken: "test2",
|
||||
idAddress: "10.75.0.56",
|
||||
greatPlainsPlantCode: "00",
|
||||
contactEmail: "",
|
||||
contactPhone: "",
|
||||
serverLoc: "D$\\LST_V3",
|
||||
buildNumber: 1,
|
||||
},
|
||||
{
|
||||
name: "Lima",
|
||||
server: "USLIM1VMS006",
|
||||
plantToken: "uslim1",
|
||||
idAddress: "10.53.0.26",
|
||||
greatPlainsPlantCode: "50",
|
||||
contactEmail: "",
|
||||
contactPhone: "",
|
||||
serverLoc: "D$\\LST_V3",
|
||||
buildNumber: 1,
|
||||
},
|
||||
{
|
||||
name: "Houston",
|
||||
server: "ushou1VMS006",
|
||||
plantToken: "ushou1",
|
||||
idAddress: "10.195.0.26",
|
||||
greatPlainsPlantCode: "20",
|
||||
contactEmail: "",
|
||||
contactPhone: "",
|
||||
serverLoc: "D$\\LST_V3",
|
||||
buildNumber: 1,
|
||||
},
|
||||
{
|
||||
name: "Dayton",
|
||||
server: "usday1VMS006",
|
||||
plantToken: "usday1",
|
||||
idAddress: "10.44.0.56",
|
||||
greatPlainsPlantCode: "80",
|
||||
contactEmail: "",
|
||||
contactPhone: "",
|
||||
serverLoc: "D$\\LST_V3",
|
||||
buildNumber: 1,
|
||||
},
|
||||
{
|
||||
name: "West Bend",
|
||||
server: "usweb1VMS006",
|
||||
plantToken: "usweb1",
|
||||
idAddress: "10.80.0.26",
|
||||
greatPlainsPlantCode: "65",
|
||||
contactEmail: "",
|
||||
contactPhone: "",
|
||||
serverLoc: "D$\\LST_V3",
|
||||
buildNumber: 1,
|
||||
},
|
||||
{
|
||||
name: "Jeff City",
|
||||
server: "usjci1VMS006",
|
||||
plantToken: "usjci",
|
||||
idAddress: "10.167.0.26",
|
||||
greatPlainsPlantCode: "40",
|
||||
contactEmail: "",
|
||||
contactPhone: "",
|
||||
serverLoc: "D$\\LST_V3",
|
||||
buildNumber: 1,
|
||||
},
|
||||
{
|
||||
name: "Sherman",
|
||||
server: "usshe1vms006",
|
||||
plantToken: "usshe1",
|
||||
idAddress: "10.205.0.26",
|
||||
greatPlainsPlantCode: "21",
|
||||
contactEmail: "",
|
||||
contactPhone: "",
|
||||
serverLoc: "D$\\LST_V3",
|
||||
buildNumber: 1,
|
||||
},
|
||||
{
|
||||
name: "McDonough",
|
||||
server: "USMCD1VMS006",
|
||||
plantToken: "usmcd1",
|
||||
idAddress: "10.193.0.26",
|
||||
greatPlainsPlantCode: "10",
|
||||
contactEmail: "",
|
||||
contactPhone: "",
|
||||
serverLoc: "D$\\LST_V3",
|
||||
buildNumber: 82,
|
||||
},
|
||||
{
|
||||
name: "St. Peters",
|
||||
server: "USTP1VMS006",
|
||||
plantToken: "usstp1",
|
||||
idAddress: "10.37.0.26",
|
||||
greatPlainsPlantCode: "45",
|
||||
contactEmail: "",
|
||||
contactPhone: "",
|
||||
serverLoc: "D$\\LST_V3",
|
||||
buildNumber: 1,
|
||||
},
|
||||
];
|
||||
|
||||
export const serversChecks = async () => {
|
||||
const log = createLogger({ module: "system", subModule: "serverData" });
|
||||
const { data, error } = await tryCatch(
|
||||
db
|
||||
.insert(serverData)
|
||||
.values(servers)
|
||||
.onConflictDoUpdate({
|
||||
target: serverData.plantToken,
|
||||
set: {
|
||||
server: sql`excluded.server`,
|
||||
name: sql`excluded.name`,
|
||||
idAddress: sql`excluded."id_address"`,
|
||||
greatPlainsPlantCode: sql`excluded.great_plains_plant_code`,
|
||||
contactEmail: sql`excluded."contact_email"`,
|
||||
contactPhone: sql`excluded.contact_phone`,
|
||||
serverLoc: sql`excluded.server_loc`,
|
||||
},
|
||||
})
|
||||
.returning(),
|
||||
);
|
||||
|
||||
if (error) {
|
||||
log.error(
|
||||
{ error: error },
|
||||
"There was an error when adding or updating the servers.",
|
||||
);
|
||||
}
|
||||
|
||||
if (data) {
|
||||
log.info({}, "All Servers were added/updated");
|
||||
}
|
||||
};
|
||||
43
backend/system/serverData.route.ts
Normal file
43
backend/system/serverData.route.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { type Response, Router } from "express";
|
||||
import { db } from "../db/db.controller.js";
|
||||
import { serverData } from "../db/schema/serverData.schema.js";
|
||||
import { apiReturn } from "../utils/returnHelper.utils.js";
|
||||
import { tryCatch } from "../utils/trycatch.utils.js";
|
||||
|
||||
// export const updateSetting = async (setting: Setting) => {
|
||||
// // TODO: when the setting is a feature setting we will need to have it run each kill switch on the crons well just stop them and during a reset it just wont start them
|
||||
// // TODO: when the setting is a system we will need to force an app restart
|
||||
// // TODO: when the setting is standard we don't do anything.
|
||||
// };
|
||||
|
||||
const r = Router();
|
||||
|
||||
r.get("/", async (_, res: Response) => {
|
||||
const { data: sName, error: sError } = await tryCatch(
|
||||
db.select().from(serverData).orderBy(serverData.name),
|
||||
);
|
||||
|
||||
if (sError) {
|
||||
return apiReturn(res, {
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "system",
|
||||
subModule: "serverData",
|
||||
message: `There was an error getting the servers `,
|
||||
data: [sError],
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
return apiReturn(res, {
|
||||
success: true,
|
||||
level: "info",
|
||||
module: "system",
|
||||
subModule: "serverData",
|
||||
message: `All current servers`,
|
||||
data: sName ?? [],
|
||||
status: 200,
|
||||
});
|
||||
});
|
||||
|
||||
export default r;
|
||||
@@ -27,7 +27,7 @@ router.get("/", async (_, res) => {
|
||||
? sqlServerStats?.data[0].UptimeSeconds
|
||||
: [],
|
||||
eomFGPkgSheetVersion: 1, // this is the excel file version when we have a change to the macro we want to grab this
|
||||
masterMacroFile: 1,
|
||||
masterMacroFile: 1.1,
|
||||
tcpServerOnline: isServerRunning,
|
||||
sqlServerConnected: prodSql,
|
||||
gpServerConnected: gpSql,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Express } from "express";
|
||||
import { requireAuth } from "../middleware/auth.middleware.js";
|
||||
import getServers from "./serverData.route.js";
|
||||
import getSettings from "./settings.route.js";
|
||||
import updSetting from "./settingsUpdate.route.js";
|
||||
import stats from "./stats.route.js";
|
||||
@@ -10,6 +11,7 @@ export const setupSystemRoutes = (baseUrl: string, app: Express) => {
|
||||
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);
|
||||
|
||||
// all other system should be under /api/system/*
|
||||
|
||||
91
backend/utils/build.utils.ts
Normal file
91
backend/utils/build.utils.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { createLogger } from "../logger/logger.controller.js";
|
||||
import { emitToRoom } from "../socket.io/roomEmitter.socket.js";
|
||||
import { updateAppStats } from "./updateAppStats.utils.js";
|
||||
import { zipBuild } from "./zipper.utils.js";
|
||||
|
||||
export const emitBuildLog = (message: string, level = "info") => {
|
||||
const payload = {
|
||||
type: "build",
|
||||
level,
|
||||
message,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
//console.log(`[BUILD][${level.toUpperCase()}] ${message}`);
|
||||
|
||||
emitToRoom("admin:build", payload as any);
|
||||
if (payload.level === "info") {
|
||||
log.info({ stack: payload }, payload.message);
|
||||
}
|
||||
|
||||
// if (log) {
|
||||
// log(payload);
|
||||
// }
|
||||
};
|
||||
|
||||
export let building = false;
|
||||
const log = createLogger({ module: "utils", subModule: "builds" });
|
||||
export const build = async () => {
|
||||
const appDir = process.env.DEV_DIR ?? "";
|
||||
return new Promise((resolve) => {
|
||||
building = true;
|
||||
|
||||
updateAppStats({
|
||||
lastUpdated: new Date(),
|
||||
building: true,
|
||||
});
|
||||
|
||||
emitBuildLog(`Starting build in: ${appDir}`);
|
||||
|
||||
const child = spawn("npm", ["run", "build"], {
|
||||
cwd: appDir,
|
||||
shell: true,
|
||||
});
|
||||
|
||||
child.stdout.on("data", (data) => {
|
||||
const lines = data.toString().split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
if (line.trim() !== "") {
|
||||
emitBuildLog(line, "info");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on("data", (data) => {
|
||||
const lines = data.toString().split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
if (line.trim() !== "") {
|
||||
emitBuildLog(line, "error");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
emitBuildLog("Build completed successfully.", "info");
|
||||
building = false;
|
||||
zipBuild();
|
||||
resolve(true);
|
||||
} else {
|
||||
building = false;
|
||||
updateAppStats({
|
||||
lastUpdated: new Date(),
|
||||
building: false,
|
||||
});
|
||||
emitBuildLog(`Build failed with code ${code}`, "error");
|
||||
//reject(new Error(`Build failed with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
child.on("error", (err) => {
|
||||
building = false;
|
||||
updateAppStats({
|
||||
lastUpdated: new Date(),
|
||||
building: false,
|
||||
});
|
||||
emitBuildLog(`Process error: ${err.message}`, "error");
|
||||
// reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
123
backend/utils/deployApp.ts
Normal file
123
backend/utils/deployApp.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { db } from "../db/db.controller.js";
|
||||
import { serverData } from "../db/schema/serverData.schema.js";
|
||||
import { appStats } from "../db/schema/stats.schema.js";
|
||||
//import { createLogger } from "../logger/logger.controller.js";
|
||||
import { emitBuildLog } from "./build.utils.js";
|
||||
import { returnFunc } from "./returnHelper.utils.js";
|
||||
|
||||
// const log = createLogger({ module: "utils", subModule: "deploy" });
|
||||
export let updating = false;
|
||||
|
||||
const updateServerBuildNumber = async (token: string) => {
|
||||
// get the current build
|
||||
const buildNum = await db.select().from(appStats);
|
||||
|
||||
// update the build now
|
||||
|
||||
await db
|
||||
.update(serverData)
|
||||
.set({ buildNumber: buildNum[0]?.currentBuild, lastUpdated: sql`NOW()` })
|
||||
.where(eq(serverData.plantToken, token));
|
||||
};
|
||||
export const runUpdate = ({
|
||||
server,
|
||||
destination,
|
||||
token,
|
||||
}: {
|
||||
server: string;
|
||||
destination: string;
|
||||
token: string;
|
||||
}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
updating = true;
|
||||
const scriptPath = process.env.UPDATE_SCRIPT_PATH;
|
||||
if (!scriptPath) {
|
||||
return returnFunc({
|
||||
success: true,
|
||||
level: "error",
|
||||
module: "utils",
|
||||
subModule: "deploy",
|
||||
message: "UPDATE_SCRIPT_PATH please make sure you have this set.",
|
||||
data: [],
|
||||
notify: true,
|
||||
room: "admin",
|
||||
});
|
||||
}
|
||||
|
||||
const args = [
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
scriptPath,
|
||||
"-Server",
|
||||
server,
|
||||
"-Destination",
|
||||
destination,
|
||||
"-Token",
|
||||
token,
|
||||
"-ADM_USER",
|
||||
process.env.DEV_USER ?? "",
|
||||
"-ADM_PASSWORD",
|
||||
process.env.DEV_PASSWORD ?? "",
|
||||
"-AppDir",
|
||||
process.env.DEV_DIR ?? "",
|
||||
];
|
||||
|
||||
emitBuildLog(`Starting update for ${server}`);
|
||||
|
||||
const child = spawn("powershell.exe", args, {
|
||||
shell: false,
|
||||
});
|
||||
|
||||
child.stdout.on("data", (data) => {
|
||||
const lines = data.toString().split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
emitBuildLog(line);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on("data", (data) => {
|
||||
const lines = data.toString().split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
emitBuildLog(line, "error");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
emitBuildLog(`Update completed for ${server}`);
|
||||
updating = false;
|
||||
updateServerBuildNumber(token);
|
||||
resolve({
|
||||
success: true,
|
||||
message: `Update completed for ${server}`,
|
||||
data: [],
|
||||
});
|
||||
} else {
|
||||
emitBuildLog(`Update failed for ${server} (code ${code})`, "error");
|
||||
updating = false;
|
||||
reject({
|
||||
success: false,
|
||||
message: `Update failed for ${server} (code ${code})`,
|
||||
data: [],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
child.on("error", (err) => {
|
||||
emitBuildLog(`Process error: ${err.message}`, "error");
|
||||
updating = false;
|
||||
reject({
|
||||
success: false,
|
||||
message: `${server}: Encountered an error while processing: ${err.message} `,
|
||||
data: err,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -14,7 +14,8 @@ export interface ReturnHelper<T = unknown[]> {
|
||||
| "email"
|
||||
| "purchase"
|
||||
| "tcp"
|
||||
| "logistics";
|
||||
| "logistics"
|
||||
| "admin";
|
||||
subModule: string;
|
||||
|
||||
level: "info" | "error" | "debug" | "fatal" | "warn";
|
||||
|
||||
17
backend/utils/updateAppStats.utils.ts
Normal file
17
backend/utils/updateAppStats.utils.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { db } from "../db/db.controller.js";
|
||||
import { appStats } from "../db/schema/stats.schema.js";
|
||||
|
||||
export const updateAppStats = async (
|
||||
data: Partial<typeof appStats.$inferInsert>,
|
||||
) => {
|
||||
await db
|
||||
.insert(appStats)
|
||||
.values({
|
||||
id: "primary",
|
||||
...data,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: appStats.id,
|
||||
set: data,
|
||||
});
|
||||
};
|
||||
177
backend/utils/zipper.utils.ts
Normal file
177
backend/utils/zipper.utils.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import fs from "node:fs";
|
||||
import fsp from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import archiver from "archiver";
|
||||
import { createLogger } from "../logger/logger.controller.js";
|
||||
import { emitBuildLog } from "./build.utils.js";
|
||||
import { updateAppStats } from "./updateAppStats.utils.js";
|
||||
|
||||
const log = createLogger({ module: "utils", subModule: "zip" });
|
||||
|
||||
const exists = async (target: string) => {
|
||||
try {
|
||||
await fsp.access(target);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getNextBuildNumber = async (buildNumberFile: string) => {
|
||||
if (!(await exists(buildNumberFile))) {
|
||||
await fsp.writeFile(buildNumberFile, "1", "utf8");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const raw = await fsp.readFile(buildNumberFile, "utf8");
|
||||
const current = Number.parseInt(raw.trim(), 10);
|
||||
|
||||
if (Number.isNaN(current) || current < 1) {
|
||||
await fsp.writeFile(buildNumberFile, "1", "utf8");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const next = current + 1;
|
||||
|
||||
await fsp.writeFile(buildNumberFile, String(next), "utf8");
|
||||
|
||||
// update the server with the next build number
|
||||
|
||||
await updateAppStats({
|
||||
currentBuild: next,
|
||||
lastBuildAt: new Date(),
|
||||
building: true,
|
||||
});
|
||||
|
||||
return next;
|
||||
};
|
||||
|
||||
const cleanupOldBuilds = async (buildFolder: string, maxBuilds: number) => {
|
||||
const entries = await fsp.readdir(buildFolder, { withFileTypes: true });
|
||||
|
||||
const zipFiles: { fullPath: string; name: string; mtimeMs: number }[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile()) continue;
|
||||
if (!/^LSTV3-\d+\.zip$/i.test(entry.name)) continue;
|
||||
|
||||
const fullPath = path.join(buildFolder, entry.name);
|
||||
const stat = await fsp.stat(fullPath);
|
||||
|
||||
zipFiles.push({
|
||||
fullPath,
|
||||
name: entry.name,
|
||||
mtimeMs: stat.mtimeMs,
|
||||
});
|
||||
}
|
||||
|
||||
zipFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
||||
|
||||
const toRemove = zipFiles.slice(maxBuilds);
|
||||
|
||||
for (const file of toRemove) {
|
||||
await fsp.rm(file.fullPath, { force: true });
|
||||
emitBuildLog(`Removed old build: ${file.name}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const zipBuild = async () => {
|
||||
const appDir = process.env.DEV_DIR ?? "";
|
||||
const maxBuilds = Number(process.env.MAX_BUILDS ?? 5);
|
||||
|
||||
if (!appDir) {
|
||||
log.error({ notify: true }, "Forgot to add in the dev dir into the env");
|
||||
return;
|
||||
}
|
||||
|
||||
const includesFile = path.join(appDir, ".includes");
|
||||
const buildNumberFile = path.join(appDir, ".buildNumber");
|
||||
const buildFolder = path.join(appDir, "builds");
|
||||
const tempFolder = path.join(appDir, "temp", "zip-temp");
|
||||
if (!(await exists(includesFile))) {
|
||||
log.error({ notify: true }, "Missing .includes file common");
|
||||
return;
|
||||
}
|
||||
|
||||
await fsp.mkdir(buildFolder, { recursive: true });
|
||||
|
||||
const buildNumber = await getNextBuildNumber(buildNumberFile);
|
||||
const zipFileName = `LSTV3-${buildNumber}.zip`;
|
||||
const zipFile = path.join(buildFolder, zipFileName);
|
||||
// make the folders in case they are not created already
|
||||
emitBuildLog(`Using build number: ${buildNumber}`);
|
||||
|
||||
if (await exists(tempFolder)) {
|
||||
await fsp.rm(tempFolder, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
await fsp.mkdir(tempFolder, { recursive: true });
|
||||
|
||||
const includes = (await fsp.readFile(includesFile, "utf8"))
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
emitBuildLog(`Preparing zip from ${includes.length} include entries`);
|
||||
|
||||
for (const relPath of includes) {
|
||||
const source = path.join(appDir, relPath);
|
||||
const dest = path.join(tempFolder, relPath);
|
||||
|
||||
if (!(await exists(source))) {
|
||||
emitBuildLog(`Skipping missing path: ${relPath}`, "error");
|
||||
continue;
|
||||
}
|
||||
|
||||
const stat = await fsp.stat(source);
|
||||
await fsp.mkdir(path.dirname(dest), { recursive: true });
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
emitBuildLog(`Copying folder: ${relPath}`);
|
||||
await fsp.cp(source, dest, { recursive: true });
|
||||
} else {
|
||||
emitBuildLog(`Copying file: ${relPath}`);
|
||||
await fsp.copyFile(source, dest);
|
||||
}
|
||||
}
|
||||
|
||||
// if something crazy happens and we get the same build lets just reuse it
|
||||
// if (await exists(zipFile)) {
|
||||
// await fsp.rm(zipFile, { force: true });
|
||||
// }
|
||||
|
||||
emitBuildLog(`Creating zip: ${zipFile}`);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const output = fs.createWriteStream(zipFile);
|
||||
const archive = archiver("zip", { zlib: { level: 9 } });
|
||||
|
||||
output.on("close", () => resolve());
|
||||
output.on("error", reject);
|
||||
archive.on("error", reject);
|
||||
|
||||
archive.pipe(output);
|
||||
|
||||
// zip contents of temp folder, not temp folder itself
|
||||
archive.directory(tempFolder, false);
|
||||
archive.finalize();
|
||||
});
|
||||
|
||||
await fsp.rm(tempFolder, { recursive: true, force: true });
|
||||
|
||||
emitBuildLog(`Zip completed successfully: ${zipFile}`);
|
||||
|
||||
await cleanupOldBuilds(buildFolder, maxBuilds);
|
||||
|
||||
await updateAppStats({
|
||||
lastUpdated: new Date(),
|
||||
building: false,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
buildNumber,
|
||||
zipFile,
|
||||
zipFileName,
|
||||
};
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
meta {
|
||||
name: Login
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{url}}/api/auth/sign-in/email
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
headers {
|
||||
Origin: http://localhost:3000
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"email": "blake.matthes@alpla.com",
|
||||
"password": "nova0511"
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
// // grab the raw Set-Cookie header
|
||||
// const cookies = res.headers["set-cookie"];
|
||||
|
||||
// const sessionCookie = cookies[0].split(";")[0];
|
||||
|
||||
// // Save it as an environment variable
|
||||
// bru.setEnvVar("session_cookie", sessionCookie);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
meta {
|
||||
name: Register
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{url}}/api/authentication/register
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"name":"Blake", // option when in the frontend as we will pass over as username if not added
|
||||
"username": "matthes01",
|
||||
"email": "blake.matthes@alpla.com",
|
||||
"password": "nova0511"
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
// // grab the raw Set-Cookie header
|
||||
// const cookies = res.headers["set-cookie"];
|
||||
|
||||
// const sessionCookie = cookies[0].split(";")[0];
|
||||
|
||||
// // Save it as an environment variable
|
||||
// bru.setEnvVar("session_cookie", sessionCookie);
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
meta {
|
||||
name: auth
|
||||
seq: 5
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
meta {
|
||||
name: getSession
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{url}}/api/auth/get-session
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "lst_v3",
|
||||
"type": "collection",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
".git"
|
||||
]
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
docs {
|
||||
All Api endpoints to the logistics support tool
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
meta {
|
||||
name: Get queries
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{url}}/api/datamart
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
meta {
|
||||
name: Run Query
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{url}}/api/datamart/:name?historical=x
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
params:query {
|
||||
historical: x
|
||||
}
|
||||
|
||||
params:path {
|
||||
name: inventory
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
meta {
|
||||
name: datamart
|
||||
seq: 2
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
vars {
|
||||
url: http://localhost:3000/lst
|
||||
readerIp: 10.44.14.215
|
||||
}
|
||||
vars:secret [
|
||||
token
|
||||
]
|
||||
@@ -1,20 +0,0 @@
|
||||
meta {
|
||||
name: Get All notifications.
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{url}}/api/notification
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
Passing all as a query param will return all queries active and none active
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
meta {
|
||||
name: Subscribe to notification
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{url}}/api/notification/sub
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"userId":"m6AbQXFwOXoX3YKLfwWgq2LIdDqS5jqv",
|
||||
"notificationId": "0399eb2a-39df-48b7-9f1c-d233cec94d2e",
|
||||
"emails": ["blake.matthes@alpla.com","blake.matthes@alpla.com"]
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
meta {
|
||||
name: notifications
|
||||
seq: 7
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
meta {
|
||||
name: remove sub notification
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
delete {
|
||||
url: {{url}}/api/notification/sub
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"userId":"0kHd6Kkdub4GW6rK1qa1yjWwqXtvykqT",
|
||||
"notificationId": "0399eb2a-39df-48b7-9f1c-d233cec94d2e",
|
||||
"emails": ["blake.mattes@alpla.com"]
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
meta {
|
||||
name: subscriptions
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{url}}/api/notification/sub
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
meta {
|
||||
name: update notification
|
||||
type: http
|
||||
seq: 6
|
||||
}
|
||||
|
||||
patch {
|
||||
url: {{url}}/api/notification/:id
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
params:path {
|
||||
id: 0399eb2a-39df-48b7-9f1c-d233cec94d2e
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"active" : true,
|
||||
"options": []
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
Passing all as a query param will return all queries active and none active
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
meta {
|
||||
name: update sub notification
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
patch {
|
||||
url: {{url}}/api/notification/sub
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"userId":"m6AbQXFwOXoX3YKLfwWgq2LIdDqS5jqv",
|
||||
"notificationId": "0399eb2a-39df-48b7-9f1c-d233cec94d2e",
|
||||
"emails": ["cowchmonkey@gmail.com"]
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
meta {
|
||||
name: Printer Listenter
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{url}}/api/ocp/printer/listener/line_1
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"message":"xnvjdhhgsdfr"
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
meta {
|
||||
name: ocp
|
||||
seq: 9
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
meta {
|
||||
name: GetApt
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{url}}/api/opendock
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
meta {
|
||||
name: Sql Start
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{url}}/api/system/prodsql/start
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
meta {
|
||||
name: Sql restart
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{url}}/api/system/prodsql/restart
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
meta {
|
||||
name: Sql stop
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{url}}/api/system/prodsql/stop
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
meta {
|
||||
name: prodSql
|
||||
seq: 6
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
meta {
|
||||
name: rfidReaders
|
||||
seq: 8
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
meta {
|
||||
name: reader
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: https://usday1prod.alpla.net/lst/old/api/rfid/mgtevents/line3.1
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
meta {
|
||||
name: Config
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://{{readerIp}}/cloud/config
|
||||
body: none
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{token}}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
meta {
|
||||
name: Login
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://{{readerIp}}/cloud/localRestLogin
|
||||
body: none
|
||||
auth: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: admin
|
||||
password: Zebra123!
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
const body = res.getBody();
|
||||
|
||||
if (body.message) {
|
||||
bru.setEnvVar("token", body.message);
|
||||
} else {
|
||||
bru.setEnvVar("token", "error");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
meta {
|
||||
name: Update Config
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
put {
|
||||
url: https://{{readerIp}}/cloud/config
|
||||
body: json
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{token}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"GPIO-LED": {
|
||||
"GPODefaults": {
|
||||
"1": "HIGH",
|
||||
"2": "HIGH",
|
||||
"3": "HIGH",
|
||||
"4": "HIGH"
|
||||
},
|
||||
"LEDDefaults": {
|
||||
"3": "GREEN"
|
||||
},
|
||||
"TAG_READ": [
|
||||
{
|
||||
"pin": 1,
|
||||
"state": "HIGH",
|
||||
"type": "GPO"
|
||||
}
|
||||
]
|
||||
},
|
||||
"READER-GATEWAY": {
|
||||
"batching": [
|
||||
{
|
||||
"maxPayloadSizePerReport": 256000,
|
||||
"reportingInterval": 2000
|
||||
},
|
||||
{
|
||||
"maxPayloadSizePerReport": 256000,
|
||||
"reportingInterval": 2000
|
||||
}
|
||||
],
|
||||
"endpointConfig": {
|
||||
"data": {
|
||||
"event": {
|
||||
"connections": [
|
||||
{
|
||||
"additionalOptions": {
|
||||
"retention": {
|
||||
"maxEventRetentionTimeInMin": 500,
|
||||
"maxNumEvents": 150000,
|
||||
"throttle": 100
|
||||
}
|
||||
},
|
||||
"description": "",
|
||||
"name": "LST",
|
||||
"options": {
|
||||
"URL": "https://usday1prod.alpla.net/lst/old/api/rfid/taginfo/line3.4",
|
||||
"security": {
|
||||
"CACertificateFileLocation": "",
|
||||
"authenticationOptions": {},
|
||||
"authenticationType": "NONE",
|
||||
"verifyHost": false,
|
||||
"verifyPeer": false
|
||||
}
|
||||
},
|
||||
"type": "httpPost"
|
||||
},
|
||||
{
|
||||
"additionalOptions": {
|
||||
"retention": {
|
||||
"maxEventRetentionTimeInMin": 500,
|
||||
"maxNumEvents": 150000,
|
||||
"throttle": 100
|
||||
}
|
||||
},
|
||||
"description": "",
|
||||
"name": "mgt",
|
||||
"options": {
|
||||
"URL": "https://usday1prod.alpla.net/lst/old/api/rfid/mgtevents/line3.4",
|
||||
"security": {
|
||||
"CACertificateFileLocation": "",
|
||||
"authenticationOptions": {},
|
||||
"authenticationType": "NONE",
|
||||
"verifyHost": false,
|
||||
"verifyPeer": false
|
||||
}
|
||||
},
|
||||
"type": "httpPost"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"managementEventConfig": {
|
||||
"errors": {
|
||||
"antenna": false,
|
||||
"cpu": {
|
||||
"reportIntervalInSec": 1800,
|
||||
"threshold": 90
|
||||
},
|
||||
"database": true,
|
||||
"flash": {
|
||||
"reportIntervalInSec": 1800,
|
||||
"threshold": 90
|
||||
},
|
||||
"ntp": true,
|
||||
"radio": true,
|
||||
"radio_control": true,
|
||||
"ram": {
|
||||
"reportIntervalInSec": 1800,
|
||||
"threshold": 90
|
||||
},
|
||||
"reader_gateway": true,
|
||||
"userApp": {
|
||||
"reportIntervalInSec": 1800,
|
||||
"threshold": 120
|
||||
}
|
||||
},
|
||||
"gpiEvents": true,
|
||||
"gpoEvents": true,
|
||||
"heartbeat": {
|
||||
"fields": {
|
||||
"radio_control": [
|
||||
"ANTENNAS",
|
||||
"RADIO_ACTIVITY",
|
||||
"RADIO_CONNECTION",
|
||||
"CPU",
|
||||
"RAM",
|
||||
"UPTIME",
|
||||
"NUM_ERRORS",
|
||||
"NUM_WARNINGS",
|
||||
"NUM_TAG_READS",
|
||||
"NUM_TAG_READS_PER_ANTENNA",
|
||||
"NUM_DATA_MESSAGES_TXED",
|
||||
"NUM_RADIO_PACKETS_RXED"
|
||||
],
|
||||
"reader_gateway": [
|
||||
"NUM_DATA_MESSAGES_RXED",
|
||||
"NUM_MANAGEMENT_EVENTS_TXED",
|
||||
"NUM_DATA_MESSAGES_TXED",
|
||||
"NUM_DATA_MESSAGES_RETAINED",
|
||||
"NUM_DATA_MESSAGES_DROPPED",
|
||||
"CPU",
|
||||
"RAM",
|
||||
"UPTIME",
|
||||
"NUM_ERRORS",
|
||||
"NUM_WARNINGS",
|
||||
"INTERFACE_CONNECTION_STATUS",
|
||||
"NOLOCKQ_DEPTH"
|
||||
],
|
||||
"system": [
|
||||
"CPU",
|
||||
"FLASH",
|
||||
"NTP",
|
||||
"RAM",
|
||||
"SYSTEMTIME",
|
||||
"TEMPERATURE",
|
||||
"UPTIME",
|
||||
"GPO",
|
||||
"GPI",
|
||||
"POWER_NEGOTIATION",
|
||||
"POWER_SOURCE",
|
||||
"MAC_ADDRESS",
|
||||
"HOSTNAME"
|
||||
],
|
||||
"userapps": [
|
||||
"STATUS",
|
||||
"CPU",
|
||||
"RAM",
|
||||
"UPTIME",
|
||||
"NUM_DATA_MESSAGES_RXED",
|
||||
"NUM_DATA_MESSAGES_TXED",
|
||||
"INCOMING_DATA_BUFFER_PERCENTAGE_REMAINING",
|
||||
"OUTGOING_DATA_BUFFER_PERCENTAGE_REMAINING"
|
||||
]
|
||||
},
|
||||
"interval": 60
|
||||
},
|
||||
"userappEvents": true,
|
||||
"warnings": {
|
||||
"cpu": {
|
||||
"reportIntervalInSec": 1800,
|
||||
"threshold": 80
|
||||
},
|
||||
"database": true,
|
||||
"flash": {
|
||||
"reportIntervalInSec": 1800,
|
||||
"threshold": 80
|
||||
},
|
||||
"ntp": true,
|
||||
"radio_api": true,
|
||||
"radio_control": true,
|
||||
"ram": {
|
||||
"reportIntervalInSec": 1800,
|
||||
"threshold": 80
|
||||
},
|
||||
"reader_gateway": true,
|
||||
"temperature": {
|
||||
"ambient": 75,
|
||||
"pa": 105
|
||||
},
|
||||
"userApp": {
|
||||
"reportIntervalInSec": 1800,
|
||||
"threshold": 60
|
||||
}
|
||||
}
|
||||
},
|
||||
"retention": [
|
||||
{
|
||||
"maxEventRetentionTimeInMin": 500,
|
||||
"maxNumEvents": 150000,
|
||||
"throttle": 100
|
||||
},
|
||||
{
|
||||
"maxEventRetentionTimeInMin": 500,
|
||||
"maxNumEvents": 150000,
|
||||
"throttle": 100
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
meta {
|
||||
name: readerSpecific
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: admin
|
||||
password: Zebra123!
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
meta {
|
||||
name: Get Settings
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{url}}/api/settings
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
returns all settings
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
meta {
|
||||
name: Status
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{url}}/api/stats
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
meta {
|
||||
name: updateSetting
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
patch {
|
||||
url: {{url}}/api/settings/opendock_sync
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"value" : "1",
|
||||
"active": "true"
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
|
||||
docs {
|
||||
Allows the changing of a setting based on the parameter.
|
||||
|
||||
* when a setting that is being changed is a feature there will be some backgound logic that will stop that features processes and no long work.
|
||||
|
||||
* when the setting is being changed is system the entire app will do a full restart
|
||||
|
||||
* when a seeting is being changed and is standard nothing will happen until the next action is completed. example someone prints a label and you changed the default to 120 second from 90 seconds
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
meta {
|
||||
name: Active Jobs
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{url}}/api/utils/croner
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
meta {
|
||||
name: Change job status
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
patch {
|
||||
url: {{url}}/api/utils/croner/stop
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"name": "open-dock-monitor"
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
@@ -12,48 +12,36 @@ services:
|
||||
#- "${VITE_PORT:-4200}:4200"
|
||||
- "3600:3000"
|
||||
dns:
|
||||
- 10.193.9.250
|
||||
- 10.193.9.251 # your internal DNS server
|
||||
dns_search:
|
||||
- alpla.net # or your internal search suffix
|
||||
- 10.44.9.250
|
||||
- 10.44.9.251 # your internal DNS server
|
||||
- 1.1.1.1
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- LOG_LEVEL=info
|
||||
- EXTERNAL_URL=http://192.168.8.222:3600
|
||||
- DATABASE_HOST=host.docker.internal # if running on the same docker then do this
|
||||
- DATABASE_PORT=5433
|
||||
- DATABASE_HOST=postgres # if running on the same docker then do this
|
||||
- DATABASE_PORT=5432
|
||||
- DATABASE_USER=${DATABASE_USER}
|
||||
- DATABASE_PASSWORD=${DATABASE_PASSWORD}
|
||||
- DATABASE_DB=${DATABASE_DB}
|
||||
- PROD_SERVER=${PROD_SERVER}
|
||||
- PROD_SERVER=10.75.9.56 #${PROD_SERVER}
|
||||
- PROD_PLANT_TOKEN=${PROD_PLANT_TOKEN}
|
||||
- PROD_USER=${PROD_USER}
|
||||
- PROD_PASSWORD=${PROD_PASSWORD}
|
||||
- GP_SERVER=10.193.9.31
|
||||
- SQL_PORT=1433
|
||||
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
|
||||
- BETTER_AUTH_URL=${URL}
|
||||
# for all host including prod servers, plc's, printers, or other de
|
||||
# extra_hosts:
|
||||
# - "${PROD_SERVER}:${PROD_IP}"
|
||||
- OPENDOCK_URL=${OPENDOCK_URL}
|
||||
- OPENDOCK_PASSWORD=${OPENDOCK_PASSWORD}
|
||||
- DEFAULT_DOCK=${DEFAULT_DOCK}
|
||||
- DEFAULT_LOAD_TYPE=${DEFAULT_LOAD_TYPE}
|
||||
- DEFAULT_CARRIER=${DEFAULT_CARRIER}
|
||||
|
||||
# networks:
|
||||
# - default
|
||||
# - logisticsNetwork
|
||||
# #- mlan1
|
||||
# networks:
|
||||
# logisticsNetwork:
|
||||
# driver: macvlan
|
||||
# driver_opts:
|
||||
# parent: eth0
|
||||
# ipam:
|
||||
# config:
|
||||
# - subnet: ${LOGISTICS_NETWORK}
|
||||
# gateway: ${LOGISTICS_GATEWAY}
|
||||
#for all host including prod servers, plc's, printers, or other de
|
||||
networks:
|
||||
- docker-network
|
||||
|
||||
# mlan1:
|
||||
# driver: macvlan
|
||||
# driver_opts:
|
||||
# parent: eth0
|
||||
# ipam:
|
||||
# config:
|
||||
# - subnet: ${MLAN1_NETWORK}
|
||||
# gateway: ${MLAN1_GATEWAY}
|
||||
networks:
|
||||
docker-network:
|
||||
external: true
|
||||
21
frontend/package-lock.json
generated
21
frontend/package-lock.json
generated
@@ -19,6 +19,8 @@
|
||||
"better-auth": "^1.5.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"lucide-react": "^0.577.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"radix-ui": "^1.4.3",
|
||||
@@ -6016,6 +6018,25 @@
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns-tz": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz",
|
||||
"integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"date-fns": "^3.0.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
|
||||
@@ -34,7 +34,9 @@
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"zod": "^4.3.6"
|
||||
"zod": "^4.3.6",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-tz": "^3.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.36.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { Bell, Logs, Settings } from "lucide-react";
|
||||
import { Bell, Logs, Server, Settings } from "lucide-react";
|
||||
|
||||
import {
|
||||
SidebarGroup,
|
||||
@@ -40,6 +40,14 @@ export default function AdminSidebar({ session }: any) {
|
||||
module: "admin",
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
title: "Servers",
|
||||
url: "/admin/servers",
|
||||
icon: Server,
|
||||
role: ["systemAdmin", "admin"],
|
||||
module: "admin",
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
title: "Logs",
|
||||
url: "/admin/logs",
|
||||
|
||||
@@ -1,22 +1,55 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import socket from "@/lib/socket.io";
|
||||
|
||||
export function useSocketRoom<T>(roomId: string) {
|
||||
type RoomUpdatePayload<T> = {
|
||||
roomId: string;
|
||||
payloads: T[];
|
||||
};
|
||||
|
||||
type RoomErrorPayload = {
|
||||
roomId?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export function useSocketRoom<T>(
|
||||
roomId: string,
|
||||
getKey?: (item: T) => string | number,
|
||||
) {
|
||||
const [data, setData] = useState<T[]>([]);
|
||||
const [info, setInfo] = useState(
|
||||
"No data yet — join the room to start receiving",
|
||||
);
|
||||
|
||||
const clearRoom = useCallback(
|
||||
(id?: string | number) => {
|
||||
if (id !== undefined && getKey) {
|
||||
setData((prev) => prev.filter((item) => getKey(item) !== id));
|
||||
setInfo(`Removed item ${id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
setData([]);
|
||||
setInfo("Room data cleared");
|
||||
},
|
||||
[getKey],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
function handleConnect() {
|
||||
socket.emit("join-room", roomId);
|
||||
setInfo(`Joined room: ${roomId}`);
|
||||
}
|
||||
|
||||
function handleUpdate(payload: any) {
|
||||
function handleUpdate(payload: RoomUpdatePayload<T>) {
|
||||
// protects against other room updates hitting this hook
|
||||
if (payload.roomId !== roomId) return;
|
||||
|
||||
setData((prev) => [...payload.payloads, ...prev]);
|
||||
setInfo("");
|
||||
}
|
||||
|
||||
function handleError(err: any) {
|
||||
function handleError(err: RoomErrorPayload) {
|
||||
if (err.roomId && err.roomId !== roomId) return;
|
||||
setInfo(err.message ?? "Room error");
|
||||
}
|
||||
|
||||
@@ -31,6 +64,7 @@ export function useSocketRoom<T>(roomId: string) {
|
||||
// If already connected, join immediately
|
||||
if (socket.connected) {
|
||||
socket.emit("join-room", roomId);
|
||||
setInfo(`Joined room: ${roomId}`);
|
||||
}
|
||||
|
||||
return () => {
|
||||
@@ -42,5 +76,5 @@ export function useSocketRoom<T>(roomId: string) {
|
||||
};
|
||||
}, [roomId]);
|
||||
|
||||
return { data, info };
|
||||
return { data, info, clearRoom };
|
||||
}
|
||||
|
||||
22
frontend/src/lib/queries/servers.ts
Normal file
22
frontend/src/lib/queries/servers.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
|
||||
export function servers() {
|
||||
return queryOptions({
|
||||
queryKey: ["servers"],
|
||||
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/servers");
|
||||
|
||||
return data.data;
|
||||
};
|
||||
@@ -105,6 +105,7 @@ export default function LstTable({
|
||||
</TableBody>
|
||||
</Table>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
<ScrollBar orientation="vertical" />
|
||||
</ScrollArea>
|
||||
<div className="flex items-center justify-end space-x-2 py-4">
|
||||
<Button
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Route as IndexRouteImport } from './routes/index'
|
||||
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 AdminNotificationsRouteImport } from './routes/admin/notifications'
|
||||
import { Route as AdminLogsRouteImport } from './routes/admin/logs'
|
||||
import { Route as authLoginRouteImport } from './routes/(auth)/login'
|
||||
@@ -46,6 +47,11 @@ const AdminSettingsRoute = AdminSettingsRouteImport.update({
|
||||
path: '/admin/settings',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AdminServersRoute = AdminServersRouteImport.update({
|
||||
id: '/admin/servers',
|
||||
path: '/admin/servers',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AdminNotificationsRoute = AdminNotificationsRouteImport.update({
|
||||
id: '/admin/notifications',
|
||||
path: '/admin/notifications',
|
||||
@@ -83,6 +89,7 @@ export interface FileRoutesByFullPath {
|
||||
'/login': typeof authLoginRoute
|
||||
'/admin/logs': typeof AdminLogsRoute
|
||||
'/admin/notifications': typeof AdminNotificationsRoute
|
||||
'/admin/servers': typeof AdminServersRoute
|
||||
'/admin/settings': typeof AdminSettingsRoute
|
||||
'/docs/$': typeof DocsSplatRoute
|
||||
'/docs/': typeof DocsIndexRoute
|
||||
@@ -96,6 +103,7 @@ export interface FileRoutesByTo {
|
||||
'/login': typeof authLoginRoute
|
||||
'/admin/logs': typeof AdminLogsRoute
|
||||
'/admin/notifications': typeof AdminNotificationsRoute
|
||||
'/admin/servers': typeof AdminServersRoute
|
||||
'/admin/settings': typeof AdminSettingsRoute
|
||||
'/docs/$': typeof DocsSplatRoute
|
||||
'/docs': typeof DocsIndexRoute
|
||||
@@ -110,6 +118,7 @@ export interface FileRoutesById {
|
||||
'/(auth)/login': typeof authLoginRoute
|
||||
'/admin/logs': typeof AdminLogsRoute
|
||||
'/admin/notifications': typeof AdminNotificationsRoute
|
||||
'/admin/servers': typeof AdminServersRoute
|
||||
'/admin/settings': typeof AdminSettingsRoute
|
||||
'/docs/$': typeof DocsSplatRoute
|
||||
'/docs/': typeof DocsIndexRoute
|
||||
@@ -125,6 +134,7 @@ export interface FileRouteTypes {
|
||||
| '/login'
|
||||
| '/admin/logs'
|
||||
| '/admin/notifications'
|
||||
| '/admin/servers'
|
||||
| '/admin/settings'
|
||||
| '/docs/$'
|
||||
| '/docs/'
|
||||
@@ -138,6 +148,7 @@ export interface FileRouteTypes {
|
||||
| '/login'
|
||||
| '/admin/logs'
|
||||
| '/admin/notifications'
|
||||
| '/admin/servers'
|
||||
| '/admin/settings'
|
||||
| '/docs/$'
|
||||
| '/docs'
|
||||
@@ -151,6 +162,7 @@ export interface FileRouteTypes {
|
||||
| '/(auth)/login'
|
||||
| '/admin/logs'
|
||||
| '/admin/notifications'
|
||||
| '/admin/servers'
|
||||
| '/admin/settings'
|
||||
| '/docs/$'
|
||||
| '/docs/'
|
||||
@@ -165,6 +177,7 @@ export interface RootRouteChildren {
|
||||
authLoginRoute: typeof authLoginRoute
|
||||
AdminLogsRoute: typeof AdminLogsRoute
|
||||
AdminNotificationsRoute: typeof AdminNotificationsRoute
|
||||
AdminServersRoute: typeof AdminServersRoute
|
||||
AdminSettingsRoute: typeof AdminSettingsRoute
|
||||
DocsSplatRoute: typeof DocsSplatRoute
|
||||
DocsIndexRoute: typeof DocsIndexRoute
|
||||
@@ -210,6 +223,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AdminSettingsRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/admin/servers': {
|
||||
id: '/admin/servers'
|
||||
path: '/admin/servers'
|
||||
fullPath: '/admin/servers'
|
||||
preLoaderRoute: typeof AdminServersRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/admin/notifications': {
|
||||
id: '/admin/notifications'
|
||||
path: '/admin/notifications'
|
||||
@@ -261,6 +281,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
authLoginRoute: authLoginRoute,
|
||||
AdminLogsRoute: AdminLogsRoute,
|
||||
AdminNotificationsRoute: AdminNotificationsRoute,
|
||||
AdminServersRoute: AdminServersRoute,
|
||||
AdminSettingsRoute: AdminSettingsRoute,
|
||||
DocsSplatRoute: DocsSplatRoute,
|
||||
DocsIndexRoute: DocsIndexRoute,
|
||||
|
||||
251
frontend/src/routes/admin/servers.tsx
Normal file
251
frontend/src/routes/admin/servers.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
import { 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 {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "../../components/ui/tooltip";
|
||||
import { useSocketRoom } from "../../hooks/socket.io.hook";
|
||||
import { authClient } from "../../lib/auth-client";
|
||||
import { servers } from "../../lib/queries/servers";
|
||||
import LstTable from "../../lib/tableStuff/LstTable";
|
||||
import SearchableHeader from "../../lib/tableStuff/SearchableHeader";
|
||||
import SkellyTable from "../../lib/tableStuff/SkellyTable";
|
||||
|
||||
export const Route = createFileRoute("/admin/servers")({
|
||||
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 ServerTable = () => {
|
||||
const { data, refetch } = useSuspenseQuery(servers());
|
||||
const columnHelper = createColumnHelper<any>();
|
||||
const okToUpdate = ["localhost", "usmcd1olp082"];
|
||||
const columns = [
|
||||
columnHelper.accessor("name", {
|
||||
header: ({ column }) => (
|
||||
<SearchableHeader column={column} title="Name" searchable={true} />
|
||||
),
|
||||
filterFn: "includesString",
|
||||
cell: (i) => i.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("greatPlainsPlantCode", {
|
||||
header: ({ column }) => (
|
||||
<SearchableHeader column={column} title="GP Code" />
|
||||
),
|
||||
cell: (i) => <span>{i.getValue().toUpperCase()}</span>,
|
||||
}),
|
||||
columnHelper.accessor("server", {
|
||||
header: ({ column }) => (
|
||||
<SearchableHeader column={column} title="server" />
|
||||
),
|
||||
cell: (i) => <span>{i.getValue().toUpperCase()}</span>,
|
||||
}),
|
||||
columnHelper.accessor("idAddress", {
|
||||
header: ({ column }) => (
|
||||
<SearchableHeader column={column} title="IP Address" />
|
||||
),
|
||||
cell: (i) => <span>{i.getValue()}</span>,
|
||||
}),
|
||||
];
|
||||
|
||||
if (okToUpdate.includes(window.location.hostname)) {
|
||||
columns.push(
|
||||
columnHelper.accessor("lastUpdated", {
|
||||
header: ({ column }) => (
|
||||
<SearchableHeader column={column} title="Last Update" />
|
||||
),
|
||||
cell: (i) => <span>{format(i.getValue(), "M/d/yyyy HH:mm")}</span>,
|
||||
}),
|
||||
columnHelper.accessor("buildNumber", {
|
||||
header: ({ column }) => (
|
||||
<SearchableHeader column={column} title="Build" />
|
||||
),
|
||||
cell: (i) => <span>{i.getValue()}</span>,
|
||||
}),
|
||||
columnHelper.accessor("update", {
|
||||
header: ({ column }) => (
|
||||
<SearchableHeader column={column} title="Update" 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 onToggle = async () => {
|
||||
setActiveToggle(true);
|
||||
toast.success(
|
||||
`${i.row.original.name} just started the upgrade monitor logs for errors.`,
|
||||
);
|
||||
try {
|
||||
const res = await axios.post(
|
||||
`/lst/api/admin/build/updateServer`,
|
||||
{
|
||||
server: i.row.original.server,
|
||||
destination: i.row.original.serverLoc,
|
||||
token: i.row.original.plantToken,
|
||||
},
|
||||
{ withCredentials: true },
|
||||
);
|
||||
|
||||
if (res.data.success) {
|
||||
toast.success(
|
||||
`${i.row.original.name} has completed its upgrade.`,
|
||||
);
|
||||
refetch();
|
||||
setActiveToggle(false);
|
||||
}
|
||||
} catch (error) {
|
||||
setActiveToggle(false);
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
disabled={activeToggle}
|
||||
onClick={() => onToggle()}
|
||||
>
|
||||
{activeToggle ? (
|
||||
<span>
|
||||
<Spinner />
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
<CircleFadingArrowUp />
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return <LstTable data={data} columns={columns} />;
|
||||
};
|
||||
|
||||
function RouteComponent() {
|
||||
const { data: logs = [], clearRoom } = useSocketRoom<any>("admin:build");
|
||||
|
||||
const columnHelper = createColumnHelper<any>();
|
||||
|
||||
console.log(window.location);
|
||||
const logColumns = [
|
||||
columnHelper.accessor("timestamp", {
|
||||
header: ({ column }) => (
|
||||
<SearchableHeader column={column} title="Time" searchable={false} />
|
||||
),
|
||||
filterFn: "includesString",
|
||||
cell: (i) => format(i.getValue(), "M/d/yyyy HH:mm"),
|
||||
}),
|
||||
columnHelper.accessor("message", {
|
||||
header: ({ column }) => (
|
||||
<SearchableHeader column={column} title="Message" />
|
||||
),
|
||||
cell: (i) => (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
{i.getValue()?.length > 250 ? (
|
||||
<span>{i.getValue().slice(0, 250)}...</span>
|
||||
) : (
|
||||
<span>{i.getValue()}</span>
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{i.getValue()}</TooltipContent>
|
||||
</Tooltip>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("clearLog", {
|
||||
header: ({ column }) => (
|
||||
<SearchableHeader column={column} title="Clear" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const x = row.original;
|
||||
return (
|
||||
<Button
|
||||
size="icon"
|
||||
variant={"destructive"}
|
||||
onClick={() => clearRoom(x.timestamp)}
|
||||
>
|
||||
<Trash />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
}),
|
||||
];
|
||||
const triggerBuild = async () => {
|
||||
try {
|
||||
const res = await axios.post(
|
||||
`/lst/api/admin/build/release`,
|
||||
|
||||
{
|
||||
withCredentials: true,
|
||||
},
|
||||
);
|
||||
|
||||
if (res.data.success) {
|
||||
toast.success(res.data.message);
|
||||
}
|
||||
|
||||
if (!res.data.success) {
|
||||
toast.error(res.data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
//toast.error(err?.message);
|
||||
}
|
||||
};
|
||||
//console.log(logs);
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-1 justify-end">
|
||||
<Button onClick={triggerBuild}>Trigger Build</Button>
|
||||
<Button onClick={() => clearRoom()}>Clear Logs</Button>
|
||||
</div>
|
||||
<div className="flex gap-1 w-full">
|
||||
<div className="w-full">
|
||||
<Suspense fallback={<SkellyTable />}>
|
||||
<ServerTable />
|
||||
</Suspense>
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<LstTable data={logs} columns={logColumns} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
lstMobile/package-lock.json
generated
32
lstMobile/package-lock.json
generated
@@ -39,7 +39,8 @@
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-worklets": "0.7.2",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"zod": "^4.3.6"
|
||||
"zod": "^4.3.6",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "~19.2.2",
|
||||
@@ -13898,6 +13899,35 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "5.0.12",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz",
|
||||
"integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18.0.0",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=18.0.0",
|
||||
"use-sync-external-store": ">=1.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,8 @@
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-worklets": "0.7.2",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"zod": "^4.3.6"
|
||||
"zod": "^4.3.6",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "~19.2.2",
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Tabs } from 'expo-router'
|
||||
import React from 'react'
|
||||
import { colors } from '../../stlyes/global'
|
||||
import { Home,Settings } from 'lucide-react-native'
|
||||
|
||||
export default function TabLayout() {
|
||||
return (
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
tabBarStyle:{
|
||||
|
||||
},
|
||||
tabBarActiveTintColor: 'black',
|
||||
tabBarInactiveTintColor: colors.textSecondary,
|
||||
}}
|
||||
>
|
||||
<Tabs.Screen
|
||||
name='index'
|
||||
options={{
|
||||
title:'Home',
|
||||
tabBarIcon: ({color, size})=>(
|
||||
<Home color={color} size={size}/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name='config'
|
||||
options={{
|
||||
title: 'Config',
|
||||
tabBarIcon: ({color, size})=> (
|
||||
<Settings size={size} color={color}/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
// app/config.tsx
|
||||
import { useEffect, useState } from "react";
|
||||
import { View, Text, TextInput, Button, Alert } from "react-native";
|
||||
import { useRouter } from "expo-router";
|
||||
import { AppConfig, getConfig, saveConfig } from "../../lib/storage";
|
||||
import Constants from "expo-constants";
|
||||
|
||||
export default function Config() {
|
||||
const [serverUrl, setServerUrl] = useState("");
|
||||
const [scannerId, setScannerId] = useState("");
|
||||
const [config, setConfig] = useState<AppConfig | null>(null)
|
||||
const [loading, setLoading] = useState(true);
|
||||
const router = useRouter()
|
||||
|
||||
const version = Constants.expoConfig?.version;
|
||||
const build = Constants.expoConfig?.android?.versionCode ?? 1;
|
||||
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
const existing = await getConfig();
|
||||
|
||||
if (existing) {
|
||||
setServerUrl(existing.serverUrl);
|
||||
setScannerId(existing.scannerId);
|
||||
setConfig(existing)
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
loadConfig();
|
||||
}, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!serverUrl.trim() || !scannerId.trim()) {
|
||||
Alert.alert("Missing info", "Please fill in both fields.");
|
||||
return;
|
||||
}
|
||||
|
||||
await saveConfig({
|
||||
serverUrl: serverUrl.trim(),
|
||||
scannerId: scannerId.trim(),
|
||||
});
|
||||
|
||||
Alert.alert("Saved", "Config saved to device.");
|
||||
//router.replace("/");
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <Text>Loading config...</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, padding: 16, gap: 12 }}>
|
||||
<View style={{alignItems: "center", margin: 10}}>
|
||||
<Text style={{ fontSize: 20, fontWeight: "600"}}>LST Scanner Config</Text>
|
||||
</View>
|
||||
|
||||
|
||||
<Text>Server IP</Text>
|
||||
<TextInput
|
||||
value={serverUrl}
|
||||
onChangeText={setServerUrl}
|
||||
placeholder="192.168.1.1"
|
||||
autoCapitalize="none"
|
||||
keyboardType="numeric"
|
||||
style={{ borderWidth: 1, padding: 10, borderRadius: 8 }}
|
||||
/>
|
||||
|
||||
<Text>Server port</Text>
|
||||
<TextInput
|
||||
value={scannerId}
|
||||
onChangeText={setScannerId}
|
||||
placeholder="3000"
|
||||
autoCapitalize="characters"
|
||||
keyboardType="numeric"
|
||||
style={{ borderWidth: 1, padding: 10, borderRadius: 8, }}
|
||||
/>
|
||||
|
||||
<View style={{flexDirection: 'row',justifyContent: 'center', padding: 3}}>
|
||||
<Button title="Save Config" onPress={handleSave} />
|
||||
</View>
|
||||
|
||||
|
||||
<View style={{ marginTop: "auto", alignItems: "center", padding: 10 }}>
|
||||
<Text style={{ fontSize: 12, color: "#666" }}>
|
||||
LST Scanner v{version}-{build}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
import * as Application from "expo-application";
|
||||
import * as Device from "expo-device";
|
||||
import { useRouter } from "expo-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
Platform,
|
||||
ScrollView,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native";
|
||||
import HomeHeader from "../../components/HomeHeader";
|
||||
import { type AppConfig, getConfig, hasValidConfig } from "../../lib/storage";
|
||||
import {
|
||||
evaluateVersion,
|
||||
type ServerVersionInfo,
|
||||
type StartupStatus,
|
||||
} from "../../lib/versionValidation";
|
||||
import { globalStyles } from "../../stlyes/global";
|
||||
import axios from 'axios'
|
||||
|
||||
export default function Index() {
|
||||
const [config, setConfig] = useState<AppConfig | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [startupStatus, setStartupStatus] = useState<StartupStatus>({state: "checking"});
|
||||
const [serverInfo, setServerInfo] = useState<ServerVersionInfo>()
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const versionName = Application.nativeApplicationVersion ?? "unknown";
|
||||
const versionCode = Number(Application.nativeBuildVersion ?? "0");
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const startUp = async () => {
|
||||
try {
|
||||
const savedConfig = await getConfig();
|
||||
|
||||
if (!hasValidConfig(savedConfig)) {
|
||||
router.replace("/config");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isMounted) return;
|
||||
setConfig(savedConfig);
|
||||
|
||||
// temp while testing
|
||||
const appBuildCode = 1;
|
||||
|
||||
try {
|
||||
const res = await axios.get(`http://${savedConfig?.serverUrl}:${savedConfig?.scannerId}/lst/api/mobile/version`);
|
||||
console.log(res)
|
||||
const server = (await res.data) as ServerVersionInfo;
|
||||
|
||||
if (!isMounted) return;
|
||||
|
||||
const result = evaluateVersion(appBuildCode, server);
|
||||
setStartupStatus(result);
|
||||
setServerInfo(server)
|
||||
|
||||
if (result.state === "warning") {
|
||||
Alert.alert("Update available", result.message);
|
||||
}
|
||||
} catch {
|
||||
if (!isMounted) return;
|
||||
setStartupStatus({ state: "offline" });
|
||||
}
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
startUp();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
|
||||
}, [router]);
|
||||
|
||||
if (loading) {
|
||||
return <Text>Validating Configs.</Text>;
|
||||
}
|
||||
|
||||
if (startupStatus.state === "checking") {
|
||||
return <Text>Checking device and server status...</Text>;
|
||||
}
|
||||
|
||||
if (startupStatus.state === "blocked") {
|
||||
return (
|
||||
<View>
|
||||
<Text>Update Required</Text>
|
||||
<Text>This scanner must be updated before it can be used.</Text>
|
||||
<Text>Scan the update code to continue.</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (startupStatus.state === "offline") {
|
||||
// app still renders, but show disconnected state
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView >
|
||||
|
||||
<View style={globalStyles.container}>
|
||||
<HomeHeader />
|
||||
|
||||
<Text>
|
||||
Welcome.{versionName} - {versionCode}
|
||||
</Text>
|
||||
<Text>Running on: {Platform.OS}</Text>
|
||||
<Text>Device model: {Device.modelName}</Text>
|
||||
<Text>Device Brand: {Device.brand}</Text>
|
||||
<Text> OS Version: {Device.osVersion}</Text>
|
||||
<View style={{ flex: 1, padding: 16, gap: 12 }}>
|
||||
<Text style={{ fontSize: 22, fontWeight: "600" }}>Welcome</Text>
|
||||
|
||||
{config ? (
|
||||
<>
|
||||
<Text>Server: {config.serverUrl}</Text>
|
||||
<Text>Scanner: {config.scannerId}</Text>
|
||||
<Text>Server: v{serverInfo?.versionName}</Text>
|
||||
</>
|
||||
) : (
|
||||
<Text>No config found yet.</Text>
|
||||
)}
|
||||
</View></View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Stack } from "expo-router";
|
||||
import {StatusBar} from 'expo-status-bar'
|
||||
import { colors } from "../stlyes/global";
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
|
||||
export default function RootLayout() {
|
||||
return <>
|
||||
<StatusBar style="dark" />
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name='(tabs)' />
|
||||
</Stack>
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
<StatusBar style="dark" />
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="index" />
|
||||
{/* <Stack.Screen name="(tabs)" /> */}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
9
lstMobile/src/app/blocked.tsx
Normal file
9
lstMobile/src/app/blocked.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
export default function blocked() {
|
||||
return (
|
||||
<View>
|
||||
<Text>Blocked</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
72
lstMobile/src/app/index.tsx
Normal file
72
lstMobile/src/app/index.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useRouter } from "expo-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ActivityIndicator, Text, View } from "react-native";
|
||||
import { useAppStore } from "../hooks/useAppStore";
|
||||
import { devDelay } from "../lib/devMode";
|
||||
|
||||
export default function Index() {
|
||||
const router = useRouter();
|
||||
const [message, setMessage] = useState(<Text>Starting app...</Text>);
|
||||
|
||||
const hasHydrated = useAppStore((s) => s.hasHydrated);
|
||||
const serverPort = useAppStore((s) => s.serverPort);
|
||||
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;
|
||||
}
|
||||
|
||||
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");
|
||||
return;
|
||||
}
|
||||
|
||||
setMessage(<Text>Opening LST scan app</Text>);
|
||||
await devDelay(3250);
|
||||
router.replace("/scanner");
|
||||
} catch (error) {
|
||||
console.log("Startup error", error);
|
||||
setMessage(<Text>Something went wrong during startup.</Text>);
|
||||
}
|
||||
};
|
||||
|
||||
startup();
|
||||
}, [hasHydrated, hasValidSetup, serverPort, router]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginTop: 12,
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
<ActivityIndicator size="large" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
32
lstMobile/src/app/scanner.tsx
Normal file
32
lstMobile/src/app/scanner.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
export default function scanner() {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
//justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginTop: 50,
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
159
lstMobile/src/app/setup.tsx
Normal file
159
lstMobile/src/app/setup.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import Constants from "expo-constants";
|
||||
import { useRouter } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import { Alert, Button, Text, TextInput, View } from "react-native";
|
||||
import { useAppStore } from "../hooks/useAppStore";
|
||||
|
||||
export default function setup() {
|
||||
const router = useRouter();
|
||||
const [auth, setAuth] = useState(false);
|
||||
const [pin, setPin] = useState("");
|
||||
const version = Constants.expoConfig?.version;
|
||||
const build = Constants.expoConfig?.android?.versionCode ?? 1;
|
||||
|
||||
const serverIpFromStore = useAppStore((s) => s.serverIp);
|
||||
const serverPortFromStore = useAppStore((s) => s.serverPort);
|
||||
const scannerIdFromStore = useAppStore((s) => s.scannerId);
|
||||
|
||||
const updateAppState = useAppStore((s) => s.updateAppState);
|
||||
|
||||
// local form state
|
||||
const [serverIp, setLocalServerIp] = useState(serverIpFromStore);
|
||||
const [serverPort, setLocalServerPort] = useState(serverPortFromStore);
|
||||
const [scannerId, setScannerId] = useState(scannerIdFromStore);
|
||||
|
||||
const authCheck = () => {
|
||||
if (pin === "6971") {
|
||||
setAuth(true);
|
||||
} else {
|
||||
Alert.alert("Incorrect pin entered please try again");
|
||||
setPin("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!serverIp.trim() || !serverPort.trim()) {
|
||||
Alert.alert("Missing info", "Please fill in both fields.");
|
||||
return;
|
||||
}
|
||||
|
||||
updateAppState({
|
||||
serverIp: serverIp.trim(),
|
||||
serverPort: serverPort.trim(),
|
||||
setupCompleted: true,
|
||||
isRegistered: true,
|
||||
});
|
||||
|
||||
Alert.alert("Saved", "Config saved to device.");
|
||||
//router.replace("/");
|
||||
};
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
//justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginTop: 50,
|
||||
}}
|
||||
>
|
||||
<View style={{ alignItems: "center", margin: 10 }}>
|
||||
<Text style={{ fontSize: 20, fontWeight: "600" }}>
|
||||
LST Scanner Config
|
||||
</Text>
|
||||
</View>
|
||||
{!auth ? (
|
||||
<View>
|
||||
<Text>Pin Number</Text>
|
||||
<TextInput
|
||||
value={pin}
|
||||
onChangeText={setPin}
|
||||
placeholder=""
|
||||
//autoCapitalize="none"
|
||||
keyboardType="numeric"
|
||||
style={{ borderWidth: 1, padding: 10, borderRadius: 8, width: 128 }}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
padding: 3,
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<Button title="Save Config" onPress={authCheck} />
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View>
|
||||
<Text>Server IP</Text>
|
||||
<TextInput
|
||||
value={serverIp}
|
||||
onChangeText={setLocalServerIp}
|
||||
placeholder="192.168.1.1"
|
||||
//autoCapitalize="none"
|
||||
keyboardType="numeric"
|
||||
style={{ borderWidth: 1, padding: 10, borderRadius: 8 }}
|
||||
/>
|
||||
|
||||
<Text>Server port</Text>
|
||||
<TextInput
|
||||
value={serverPort}
|
||||
onChangeText={setLocalServerPort}
|
||||
placeholder="3000"
|
||||
//autoCapitalize="characters"
|
||||
keyboardType="numeric"
|
||||
style={{ borderWidth: 1, padding: 10, borderRadius: 8 }}
|
||||
/>
|
||||
|
||||
{parseInt(serverPort ?? "0", 10) >= 50000 && (
|
||||
<View>
|
||||
<Text>Scanner ID</Text>
|
||||
<Text style={{ width: 250 }}>
|
||||
The ID is required to be able to scan. The scanner will be
|
||||
treated as a normal scanner direct to alplaprod. no extra rules
|
||||
added.
|
||||
</Text>
|
||||
<TextInput
|
||||
value={scannerId}
|
||||
onChangeText={setScannerId}
|
||||
placeholder="0001"
|
||||
autoCapitalize="characters"
|
||||
keyboardType="numeric"
|
||||
style={{ borderWidth: 1, padding: 10, borderRadius: 8 }}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
padding: 3,
|
||||
gap: 3,
|
||||
}}
|
||||
>
|
||||
<Button title="Save Config" onPress={handleSave} />
|
||||
<Button
|
||||
title="Home"
|
||||
onPress={() => {
|
||||
router.push("/");
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
marginTop: "auto",
|
||||
alignItems: "center",
|
||||
padding: 10,
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 12, color: "#666" }}>
|
||||
LST Scanner v{version}-{build}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
151
lstMobile/src/hooks/useAppStore.ts
Normal file
151
lstMobile/src/hooks/useAppStore.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { create } from "zustand";
|
||||
import { createJSONStorage, persist } from "zustand/middleware";
|
||||
|
||||
export type ValidationStatus = "idle" | "pending" | "passed" | "failed";
|
||||
|
||||
export type AppState = {
|
||||
serverIp: string;
|
||||
serverPort: string;
|
||||
scannerId?: string;
|
||||
stageId?: string;
|
||||
deviceName?: string;
|
||||
|
||||
setupCompleted: boolean;
|
||||
isRegistered: boolean;
|
||||
|
||||
lastValidationStatus: ValidationStatus;
|
||||
lastValidationAt?: string;
|
||||
|
||||
appVersion?: string;
|
||||
|
||||
hasHydrated: boolean;
|
||||
};
|
||||
|
||||
type AppActions = {
|
||||
setServerIp: (value: string) => void;
|
||||
setServerPort: (value: string) => void;
|
||||
setScannerId: (value?: string) => void;
|
||||
setStageId: (value?: string) => void;
|
||||
setDeviceName: (value?: string) => void;
|
||||
setSetupCompleted: (value: boolean) => void;
|
||||
setIsRegistered: (value: boolean) => void;
|
||||
setValidationStatus: (status: ValidationStatus, validatedAt?: string) => void;
|
||||
setAppVersion: (value?: string) => void;
|
||||
setHasHydrated: (value: boolean) => void;
|
||||
|
||||
updateAppState: (updates: Partial<AppState>) => void;
|
||||
resetApp: () => void;
|
||||
|
||||
hasValidSetup: () => boolean;
|
||||
canEnterApp: () => boolean;
|
||||
getServerUrl: () => string;
|
||||
};
|
||||
|
||||
export type AppStore = AppState & AppActions;
|
||||
|
||||
const defaultAppState: AppState = {
|
||||
serverIp: "",
|
||||
serverPort: "",
|
||||
scannerId: "0001",
|
||||
stageId: undefined,
|
||||
deviceName: undefined,
|
||||
|
||||
setupCompleted: false,
|
||||
isRegistered: false,
|
||||
|
||||
lastValidationStatus: "idle",
|
||||
lastValidationAt: undefined,
|
||||
|
||||
appVersion: undefined,
|
||||
|
||||
hasHydrated: false,
|
||||
};
|
||||
|
||||
export const useAppStore = create<AppStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
...defaultAppState,
|
||||
|
||||
setServerIp: (value) => set({ serverIp: value }),
|
||||
setServerPort: (value) => set({ serverPort: value }),
|
||||
setScannerId: (value) => set({ scannerId: value }),
|
||||
setStageId: (value) => set({ stageId: value }),
|
||||
setDeviceName: (value) => set({ deviceName: value }),
|
||||
setSetupCompleted: (value) => set({ setupCompleted: value }),
|
||||
setIsRegistered: (value) => set({ isRegistered: value }),
|
||||
|
||||
setValidationStatus: (status, validatedAt) =>
|
||||
set({
|
||||
lastValidationStatus: status,
|
||||
lastValidationAt: validatedAt,
|
||||
}),
|
||||
|
||||
setAppVersion: (value) => set({ appVersion: value }),
|
||||
setHasHydrated: (value) => set({ hasHydrated: value }),
|
||||
|
||||
updateAppState: (updates) =>
|
||||
set((state) => ({
|
||||
...state,
|
||||
...updates,
|
||||
})),
|
||||
|
||||
resetApp: () =>
|
||||
set({
|
||||
...defaultAppState,
|
||||
hasHydrated: true,
|
||||
}),
|
||||
|
||||
hasValidSetup: () => {
|
||||
const state = get();
|
||||
return Boolean(
|
||||
state.serverIp?.trim() &&
|
||||
state.serverPort?.trim() &&
|
||||
state.setupCompleted,
|
||||
);
|
||||
},
|
||||
|
||||
canEnterApp: () => {
|
||||
const state = get();
|
||||
return Boolean(
|
||||
state.serverIp?.trim() &&
|
||||
state.serverPort?.trim() &&
|
||||
state.setupCompleted &&
|
||||
state.isRegistered,
|
||||
);
|
||||
},
|
||||
|
||||
getServerUrl: () => {
|
||||
const { serverIp, serverPort } = get();
|
||||
|
||||
if (!serverIp?.trim() || !serverPort?.trim()) return "";
|
||||
return `http://${serverIp.trim()}:${serverPort.trim()}`;
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "lst_mobile_app_store",
|
||||
storage: createJSONStorage(() => AsyncStorage),
|
||||
|
||||
onRehydrateStorage: () => (state, error) => {
|
||||
if (error) {
|
||||
console.log("Failed to hydrate app state", error);
|
||||
}
|
||||
|
||||
state?.setHasHydrated(true);
|
||||
},
|
||||
|
||||
partialize: (state) => ({
|
||||
serverIp: state.serverIp,
|
||||
serverPort: state.serverPort,
|
||||
scannerId: state.scannerId,
|
||||
stageId: state.stageId,
|
||||
deviceName: state.deviceName,
|
||||
setupCompleted: state.setupCompleted,
|
||||
isRegistered: state.isRegistered,
|
||||
lastValidationStatus: state.lastValidationStatus,
|
||||
lastValidationAt: state.lastValidationAt,
|
||||
appVersion: state.appVersion,
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
1
lstMobile/src/lib/delay.ts
Normal file
1
lstMobile/src/lib/delay.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
|
||||
7
lstMobile/src/lib/devMode.ts
Normal file
7
lstMobile/src/lib/devMode.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { delay } from "./delay";
|
||||
|
||||
export const devDelay = async (ms: number) => {
|
||||
if (__DEV__) {
|
||||
await delay(ms);
|
||||
}
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
|
||||
export type AppConfig = {
|
||||
serverUrl: string;
|
||||
scannerId: string;
|
||||
};
|
||||
|
||||
const CONFIG_KEY = "scanner_app_config";
|
||||
|
||||
export async function saveConfig(config: AppConfig) {
|
||||
|
||||
await AsyncStorage.setItem(CONFIG_KEY, JSON.stringify(config));
|
||||
}
|
||||
|
||||
export async function getConfig(): Promise<AppConfig | null> {
|
||||
const raw = await AsyncStorage.getItem(CONFIG_KEY);
|
||||
|
||||
if (!raw) return null;
|
||||
|
||||
try {
|
||||
return JSON.parse(raw) as AppConfig;
|
||||
} catch (error) {
|
||||
console.log("Error", error)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function hasValidConfig(config: AppConfig | null) {
|
||||
if (!config) return false;
|
||||
|
||||
return Boolean(
|
||||
config.serverUrl?.trim() &&
|
||||
config.scannerId?.trim()
|
||||
);
|
||||
}
|
||||
33
lstMobile/temps/(tabs)/_layout.tsx
Normal file
33
lstMobile/temps/(tabs)/_layout.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Tabs } from "expo-router";
|
||||
import { Home, Settings } from "lucide-react-native";
|
||||
import { colors } from "../../stlyes/global";
|
||||
|
||||
export default function TabLayout() {
|
||||
return (
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
tabBarStyle: {},
|
||||
tabBarActiveTintColor: "black",
|
||||
tabBarInactiveTintColor: colors.textSecondary,
|
||||
}}
|
||||
>
|
||||
<Tabs.Screen
|
||||
name="home"
|
||||
options={{
|
||||
title: "Home",
|
||||
tabBarIcon: ({ color, size }) => <Home color={color} size={size} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="config"
|
||||
options={{
|
||||
title: "Config",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Settings size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
94
lstMobile/temps/(tabs)/config.tsx
Normal file
94
lstMobile/temps/(tabs)/config.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
// app/config.tsx
|
||||
|
||||
import Constants from "expo-constants";
|
||||
import { useRouter } from "expo-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Alert, Button, Text, TextInput, View } from "react-native";
|
||||
|
||||
export default function Config() {
|
||||
const [serverUrl, setServerUrl] = useState("");
|
||||
const [scannerId, setScannerId] = useState("");
|
||||
const [config, setConfig] = useState<AppConfig | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const router = useRouter();
|
||||
|
||||
const version = Constants.expoConfig?.version;
|
||||
const build = Constants.expoConfig?.android?.versionCode ?? 1;
|
||||
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
const existing = await getConfig();
|
||||
|
||||
if (existing) {
|
||||
setServerUrl(existing.serverUrl);
|
||||
setScannerId(existing.scannerId);
|
||||
setConfig(existing);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
loadConfig();
|
||||
}, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!serverUrl.trim() || !scannerId.trim()) {
|
||||
Alert.alert("Missing info", "Please fill in both fields.");
|
||||
return;
|
||||
}
|
||||
|
||||
await saveConfig({
|
||||
serverUrl: serverUrl.trim(),
|
||||
scannerId: scannerId.trim(),
|
||||
});
|
||||
|
||||
Alert.alert("Saved", "Config saved to device.");
|
||||
//router.replace("/");
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <Text>Loading config...</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, padding: 16, gap: 12 }}>
|
||||
<View style={{ alignItems: "center", margin: 10 }}>
|
||||
<Text style={{ fontSize: 20, fontWeight: "600" }}>
|
||||
LST Scanner Config
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text>Server IP</Text>
|
||||
<TextInput
|
||||
value={serverUrl}
|
||||
onChangeText={setServerUrl}
|
||||
placeholder="192.168.1.1"
|
||||
autoCapitalize="none"
|
||||
keyboardType="numeric"
|
||||
style={{ borderWidth: 1, padding: 10, borderRadius: 8 }}
|
||||
/>
|
||||
|
||||
<Text>Server port</Text>
|
||||
<TextInput
|
||||
value={scannerId}
|
||||
onChangeText={setScannerId}
|
||||
placeholder="3000"
|
||||
autoCapitalize="characters"
|
||||
keyboardType="numeric"
|
||||
style={{ borderWidth: 1, padding: 10, borderRadius: 8 }}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{ flexDirection: "row", justifyContent: "center", padding: 3 }}
|
||||
>
|
||||
<Button title="Save Config" onPress={handleSave} />
|
||||
</View>
|
||||
|
||||
<View style={{ marginTop: "auto", alignItems: "center", padding: 10 }}>
|
||||
<Text style={{ fontSize: 12, color: "#666" }}>
|
||||
LST Scanner v{version}-{build}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
43
lstMobile/temps/(tabs)/home.tsx
Normal file
43
lstMobile/temps/(tabs)/home.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import axios from "axios";
|
||||
import * as Application from "expo-application";
|
||||
import * as Device from "expo-device";
|
||||
import { useRouter } from "expo-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Alert, Platform, ScrollView, Text, View } from "react-native";
|
||||
import HomeHeader from "../../components/HomeHeader";
|
||||
import { hasValidSetup, type PersistedAppState } from "../../lib/storage";
|
||||
import {
|
||||
evaluateVersion,
|
||||
type ServerVersionInfo,
|
||||
type StartupStatus,
|
||||
} from "../../lib/versionValidation";
|
||||
import { globalStyles } from "../../stlyes/global";
|
||||
|
||||
export default function Index() {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={globalStyles.container}>
|
||||
<HomeHeader />
|
||||
|
||||
<Text>Welcome. Blake</Text>
|
||||
<Text>Running on: {Platform.OS}</Text>
|
||||
<Text>Device model: {Device.modelName}</Text>
|
||||
<Text>Device Brand: {Device.brand}</Text>
|
||||
<Text> OS Version: {Device.osVersion}</Text>
|
||||
<View style={{ flex: 1, padding: 16, gap: 12 }}>
|
||||
<Text style={{ fontSize: 22, fontWeight: "600" }}>Welcome</Text>
|
||||
|
||||
{/* {config ? (
|
||||
<>
|
||||
<Text>Server: {config.serverUrl}</Text>
|
||||
<Text>Scanner: {config.scannerId}</Text>
|
||||
<Text>Server: v{serverInfo?.versionName}</Text>
|
||||
</>
|
||||
) : (
|
||||
<Text>No config found yet.</Text>
|
||||
)} */}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
17
migrations/0034_groovy_darkhawk.sql
Normal file
17
migrations/0034_groovy_darkhawk.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
CREATE TABLE "server_data" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"server" text,
|
||||
"plant_token" text,
|
||||
"id_address" text,
|
||||
"great_plains_plantCode" numeric,
|
||||
"contact_email" text,
|
||||
"contact_phone" text,
|
||||
"active" boolean DEFAULT true,
|
||||
"server_loc" text,
|
||||
"last_updated" timestamp DEFAULT now(),
|
||||
"build_number" integer,
|
||||
"is_upgrading" boolean DEFAULT false
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "plant_token" ON "server_data" USING btree ("plant_token");
|
||||
1
migrations/0035_icy_harpoon.sql
Normal file
1
migrations/0035_icy_harpoon.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "server_data" RENAME COLUMN "great_plains_plantCode" TO "great_plains_plant_code";
|
||||
1
migrations/0036_easy_magus.sql
Normal file
1
migrations/0036_easy_magus.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "server_data" ADD CONSTRAINT "server_data_server_unique" UNIQUE("server");
|
||||
2
migrations/0037_glamorous_joseph.sql
Normal file
2
migrations/0037_glamorous_joseph.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "server_data" DROP CONSTRAINT "server_data_server_unique";--> statement-breakpoint
|
||||
ALTER TABLE "server_data" ADD CONSTRAINT "server_data_plant_token_unique" UNIQUE("plant_token");
|
||||
1
migrations/0038_special_wildside.sql
Normal file
1
migrations/0038_special_wildside.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "server_data" ALTER COLUMN "plant_token" SET NOT NULL;
|
||||
2
migrations/0039_special_the_leader.sql
Normal file
2
migrations/0039_special_the_leader.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
DROP INDEX "plant_token";--> statement-breakpoint
|
||||
ALTER TABLE "server_data" ALTER COLUMN "great_plains_plant_code" SET DATA TYPE text;
|
||||
21
migrations/0040_rainy_white_tiger.sql
Normal file
21
migrations/0040_rainy_white_tiger.sql
Normal file
@@ -0,0 +1,21 @@
|
||||
CREATE TABLE "deployment_history" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"server_id" uuid,
|
||||
"build_number" integer NOT NULL,
|
||||
"status" text NOT NULL,
|
||||
"message" text,
|
||||
"created_at" timestamp DEFAULT now()
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "app_stats" (
|
||||
"id" text PRIMARY KEY DEFAULT 'primary' NOT NULL,
|
||||
"current_build" integer DEFAULT 1 NOT NULL,
|
||||
"last_build_at" timestamp,
|
||||
"last_deploy_at" timestamp,
|
||||
"building" boolean DEFAULT false NOT NULL,
|
||||
"updating" boolean DEFAULT false NOT NULL,
|
||||
"last_updated" timestamp DEFAULT now(),
|
||||
"meta" jsonb DEFAULT '{}'::jsonb
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DROP TABLE "stats" CASCADE;
|
||||
1883
migrations/meta/0034_snapshot.json
Normal file
1883
migrations/meta/0034_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1883
migrations/meta/0035_snapshot.json
Normal file
1883
migrations/meta/0035_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1891
migrations/meta/0036_snapshot.json
Normal file
1891
migrations/meta/0036_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1891
migrations/meta/0037_snapshot.json
Normal file
1891
migrations/meta/0037_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1891
migrations/meta/0038_snapshot.json
Normal file
1891
migrations/meta/0038_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1875
migrations/meta/0039_snapshot.json
Normal file
1875
migrations/meta/0039_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user