18 Commits

Author SHA1 Message Date
4855412733 chore(release): 0.0.2-alpha.6
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m20s
Release and Build Image / release (push) Successful in 14s
2026-04-23 07:24:45 -05:00
251970ec8f chore(release): 0.0.2-alpha.1
All checks were successful
Release and Build Image / release (push) Successful in 2m36s
2026-04-23 07:24:12 -05:00
f7ea5f709e chore(release): 0.0.2-alpha.0
All checks were successful
Release and Build Image / release (push) Successful in 2m51s
2026-04-23 07:24:02 -05:00
3d3c2aa964 chore(release): 0.0.1
All checks were successful
Release and Build Image / release (push) Successful in 2m39s
2026-04-23 07:23:49 -05:00
781025dca0 fix(frontend): lingering import crashed us 2026-04-23 07:23:25 -05:00
a593bb2baa chore(doc remove): removed a doc and put it in the real area for docs 2026-04-23 07:23:01 -05:00
759f96b0b6 chore(release): 0.0.1-alpha.5
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 1m36s
Release and Build Image / release (push) Failing after 38s
2026-04-23 07:11:50 -05:00
de5df2b00b chore(scripts): added in a helper to remove old stuff
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-04-23 07:10:53 -05:00
4d53af0338 feat(servers): added marked tree in to the mix 2026-04-23 07:10:27 -05:00
f7276ca2d7 feat(oidc): added in so we could use an oidc to login as well :D 2026-04-23 07:09:49 -05:00
d6328ab764 fix(gp): weird issue with db username and password
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m41s
2026-04-22 06:39:22 -05:00
a6d53f0266 refactor(sql): changed sql connection to ip:port
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m42s
this change was needed for when we run in docker so we can connect to the servers
2026-04-22 05:40:38 -05:00
7962463927 refactor(server): server updates can now only be done from a dev pc
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m54s
2026-04-21 19:01:52 -05:00
f716de1a58 chore(clean): removed bruno api a proper api doc will be added to lst later 2026-04-21 19:01:21 -05:00
88cef2a56c refactor(servers): added mcd and stp1 2026-04-21 19:00:30 -05:00
cb00addee9 feat(admin): moved server build/update to full app
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m27s
2026-04-21 07:36:04 -05:00
b832d7aa1e fix(datamart): fixes to correct how we handle activations of new features and legacy queries 2026-04-20 08:49:24 -05:00
32517d0c98 fix(inventory): changes to accruatly adjust the query and check the feature set 2026-04-20 07:25:33 -05:00
96 changed files with 15934 additions and 1062 deletions

View File

@@ -50,3 +50,11 @@ GP_PASSWORD=
# how often to check for new/updated queries in min # how often to check for new/updated queries in min
QUERY_TIME_TYPE=m #valid options are m, h QUERY_TIME_TYPE=m #valid options are m, h
QUERY_CHECK=1 QUERY_CHECK=1
# Oauth setup
PROVIDER=""
CLIENT_ID=""
CLIENT_SECRET=""
CLIENT_SCOPES="openid profile email groups"
DISCOVERY_URL=""

1
.gitignore vendored
View File

@@ -149,3 +149,4 @@ dist
.yarn/install-state.gz .yarn/install-state.gz
.pnp.* .pnp.*
frontend/.tanstack/tmp/2249110e-da91fb0b1b87b6c4cc3e2c2cd25037fd

View File

@@ -1,5 +1,59 @@
# All Changes to LST can be found below. # All Changes to LST can be found below.
## [0.0.2-alpha.6](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.1...v0.0.2-alpha.6) (2026-04-23)
## [0.0.2-alpha.1](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.0...v0.0.2-alpha.1) (2026-04-23)
## [0.0.2-alpha.0](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.1...v0.0.2-alpha.0) (2026-04-23)
## [0.0.1](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.1-alpha.5...v0.0.1) (2026-04-23)
### 🐛 Bug fixes
* **frontend:** lingering import crashed us ([781025d](https://git.tuffraid.net/cowch/lst_v3/commits/781025dca00e9dd4b2ad9b283be944ed91bbc1e5))
### 📝 Chore
* **doc remove:** removed a doc and put it in the real area for docs ([a593bb2](https://git.tuffraid.net/cowch/lst_v3/commits/a593bb2baafd0166a178b80cd76dd8862f240e11))
## [0.0.1-alpha.5](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.1-alpha.4...v0.0.1-alpha.5) (2026-04-23)
### 🌟 Enhancements
* **admin:** moved server build/update to full app ([cb00add](https://git.tuffraid.net/cowch/lst_v3/commits/cb00addee96b3ecccf2694f85cb7882cac9c7e3d))
* **lstmobile:** intial scanner setup kinda working ([3734d9d](https://git.tuffraid.net/cowch/lst_v3/commits/3734d9daac143ad8fb4404c59990bc4f546f365b))
* **oidc:** added in so we could use an oidc to login as well :D ([f7276ca](https://git.tuffraid.net/cowch/lst_v3/commits/f7276ca2d722e30da65bbead23dc9bd57df25aa7))
* **servers:** added marked tree in to the mix ([4d53af0](https://git.tuffraid.net/cowch/lst_v3/commits/4d53af033876d81e0d38c148c15cb0af6f3d5bf0))
### 🐛 Bug fixes
* **datamart:** fixes to correct how we handle activations of new features and legacy queries ([b832d7a](https://git.tuffraid.net/cowch/lst_v3/commits/b832d7aa1ecd063be1bbb7e969617fc7a6376ffa))
* **datamart:** if we do not have 2.0 warehousing activate we need to use legacy ([5b1c885](https://git.tuffraid.net/cowch/lst_v3/commits/5b1c88546ff9a42dc572450fe05ad68015edb627))
* **gp:** weird issue with db username and password ([d6328ab](https://git.tuffraid.net/cowch/lst_v3/commits/d6328ab764c3626aef99727b873003384951d299))
* **inventory:** changes to accruatly adjust the query and check the feature set ([32517d0](https://git.tuffraid.net/cowch/lst_v3/commits/32517d0c98c42a0f0f60135b4a9951c4090ccd58))
* **logistics:** historical issue where it was being really weird ([cfbc156](https://git.tuffraid.net/cowch/lst_v3/commits/cfbc1565172f7c2e27f0a1593fe8e99b00d91bb7))
* **logistics:** purchasing monitoring was going off every 5th min instead of every 5 min ([3639c1b](https://git.tuffraid.net/cowch/lst_v3/commits/3639c1b77c597a94816bfedd0892f0c8980c6403))
* **ocp:** fixes to make sure we always hav printer.data as an array or dont do anything ([fb3cd85](https://git.tuffraid.net/cowch/lst_v3/commits/fb3cd85b411315cac0abd22d050ee88929754833))
* **psi:** refactor psi queries ([a1eeade](https://git.tuffraid.net/cowch/lst_v3/commits/a1eeadeec438f7c5c6d31f190fee5c22f83dc6b0))
### 📝 Chore
* **clean:** removed bruno api a proper api doc will be added to lst later ([f716de1](https://git.tuffraid.net/cowch/lst_v3/commits/f716de1a58a4a4c02d9a0a375444ceecea4a018b))
* **scripts:** added in a helper to remove old stuff ([de5df2b](https://git.tuffraid.net/cowch/lst_v3/commits/de5df2b00b1c6fe7c53d6ea075b4cf7e0fb845f9))
### 🛠️ Code Refactor
* **scanner:** more basic work to get the scanner just running ([82f8369](https://git.tuffraid.net/cowch/lst_v3/commits/82f8369640b2b0ff63dd640dc0aa0609a42c7dda))
* **servers:** added mcd and stp1 ([88cef2a](https://git.tuffraid.net/cowch/lst_v3/commits/88cef2a56c390b692866658ce519e59ffeaf4c17))
* **server:** server updates can now only be done from a dev pc ([7962463](https://git.tuffraid.net/cowch/lst_v3/commits/7962463927c4c5d2e12db9a0dd536b0f29fc65b2))
* **sql:** changed sql connection to ip:port ([a6d53f0](https://git.tuffraid.net/cowch/lst_v3/commits/a6d53f0266f1edc3f3946cd1f07d893c8a98d9c7))
## [0.0.1-alpha.4](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.1-alpha.3...v0.0.1-alpha.4) (2026-04-15) ## [0.0.1-alpha.4](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.1-alpha.3...v0.0.1-alpha.4) (2026-04-15)

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

View 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/*
};

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

View File

@@ -1,10 +1,16 @@
import type sql from "mssql"; import type sql from "mssql";
// TODO : Remove this later and get it onto the env
const username = "gpviewer"; const username = "gpviewer";
const password = "gp$$ViewOnly!"; const password = "gp$$ViewOnly!";
const port = process.env.SQL_PORT
? Number.parseInt(process.env.SQL_PORT, 10)
: undefined;
export const gpSqlConfig: sql.config = { export const gpSqlConfig: sql.config = {
server: `USMCD1VMS011`, server: `${process.env.GP_SERVER ?? "USMCD1VMS011"}`,
port: port,
database: `ALPLA`, database: `ALPLA`,
user: username, user: username,
password: password, password: password,

View File

@@ -1,7 +1,13 @@
import type sql from "mssql"; import type sql from "mssql";
const port = process.env.SQL_PORT
? Number.parseInt(process.env.SQL_PORT, 10)
: undefined;
export const prodSqlConfig: sql.config = { export const prodSqlConfig: sql.config = {
server: `${process.env.PROD_SERVER}`, server: `${process.env.PROD_SERVER}`,
database: `AlplaPROD_${process.env.PROD_PLANT_TOKEN}_cus`, database: `AlplaPROD_${process.env.PROD_PLANT_TOKEN}_cus`,
port: port,
user: process.env.PROD_USER, user: process.env.PROD_USER,
password: process.env.PROD_PASSWORD, password: process.env.PROD_PASSWORD,
options: { options: {

View File

@@ -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 // 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 doubleQueries = ["inventory"];
const sqlQuery = sqlQuerySelector( let queryFile = "";
`datamart.${fd.data[0].activated > 0 && !doubleQueries.includes(data.name) ? data.name : `legacy.${data.name}`}`,
) as SqlQuery;
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. // 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); const getDataMartInfo = datamartData.filter((x) => x.endpoint === data.name);
@@ -172,6 +179,12 @@ export const runDatamartQuery = async (data: Data) => {
data.options.articles data.options.articles
? `and r.ArticleHumanReadableId in (${data.options.articles})` ? `and r.ArticleHumanReadableId in (${data.options.articles})`
: "--and r.ArticleHumanReadableId in ([articles]) ", : "--and r.ArticleHumanReadableId in ([articles]) ",
)
.replace(
"and DeliveredQuantity > 0",
data.options.all
? "--and DeliveredQuantity > 0"
: "and DeliveredQuantity > 0",
); );
break; break;
@@ -201,10 +214,15 @@ export const runDatamartQuery = async (data: Data) => {
"--,l.MachineLocation,l.MachineName,l.ProductionLotRunningNumber as lot", "--,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`}`, `${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( .replaceAll(
"--,l.WarehouseDescription,l.LaneDescription", "--,l.WarehouseDescription,l.LaneDescription",
`${data.options.locations ? `,l.WarehouseDescription,l.LaneDescription` : `--,l.WarehouseDescription,l.LaneDescription`}`, `${data.options.locations ? `,l.WarehouseDescription,l.LaneDescription` : `--,l.WarehouseDescription,l.LaneDescription`}`,
); );
break; break;
case "fakeEDIUpdate": case "fakeEDIUpdate":
datamartQuery = datamartQuery.replace( datamartQuery = datamartQuery.replace(
@@ -231,7 +249,6 @@ export const runDatamartQuery = async (data: Data) => {
); );
break; break;
case "psiDeliveryData": case "psiDeliveryData":
datamartQuery = datamartQuery datamartQuery = datamartQuery
.replace("[startDate]", `${data.options.startDate}`) .replace("[startDate]", `${data.options.startDate}`)

View 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(),
});

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

View File

@@ -1,10 +1,27 @@
import type { InferSelectModel } from "drizzle-orm"; import {
import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; 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", { export const appStats = pgTable("app_stats", {
id: text("id").primaryKey().default("serverStats"), id: text("id").primaryKey().default("primary"),
build: integer("build").notNull().default(1), currentBuild: integer("current_build").notNull().default(1),
lastUpdate: timestamp("last_update").defaultNow(), 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>;

View File

@@ -53,13 +53,14 @@ export const connectGPSql = async () => {
notify: false, notify: false,
}); });
} catch (error) { } catch (error) {
console.log(error);
reconnectToSql; reconnectToSql;
return returnFunc({ return returnFunc({
success: false, success: false,
level: "error", level: "error",
module: "system", module: "system",
subModule: "db", subModule: "db",
message: "Failed to connect to the prod sql server.", message: "Failed to connect to the gp sql server.",
data: [error], data: [error],
notify: false, notify: false,
}); });

View File

@@ -1,5 +1,5 @@
import type { Express } from "express"; import type { Express } from "express";
import { setupAdminRoutes } from "./admin/admin.routes.js";
import { setupAuthRoutes } from "./auth/auth.routes.js"; import { setupAuthRoutes } from "./auth/auth.routes.js";
// import the routes and route setups // import the routes and route setups
import { setupApiDocsRoutes } from "./configs/scaler.config.js"; 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) => { export const setupRoutes = (baseUrl: string, app: Express) => {
//routes that are on by default //routes that are on by default
setupSystemRoutes(baseUrl, app); setupSystemRoutes(baseUrl, app);
setupAdminRoutes(baseUrl, app);
setupApiDocsRoutes(baseUrl, app); setupApiDocsRoutes(baseUrl, app);
setupProdSqlRoutes(baseUrl, app); setupProdSqlRoutes(baseUrl, app);
setupGPSqlRoutes(baseUrl, app); setupGPSqlRoutes(baseUrl, app);

View File

@@ -15,6 +15,7 @@ import { opendockSocketMonitor } from "./opendock/opendockSocketMonitor.utils.js
import { connectProdSql } from "./prodSql/prodSqlConnection.controller.js"; import { connectProdSql } from "./prodSql/prodSqlConnection.controller.js";
import { monitorAlplaPurchase } from "./purchase/purchase.controller.js"; import { monitorAlplaPurchase } from "./purchase/purchase.controller.js";
import { setupSocketIORoutes } from "./socket.io/serverSetup.js"; import { setupSocketIORoutes } from "./socket.io/serverSetup.js";
import { serversChecks } from "./system/serverData.controller.js";
import { baseSettingValidationCheck } from "./system/settingsBase.controller.js"; import { baseSettingValidationCheck } from "./system/settingsBase.controller.js";
import { startTCPServer } from "./tcpServer/tcp.server.js"; import { startTCPServer } from "./tcpServer/tcp.server.js";
import { createCronJob } from "./utils/croner.utils.js"; import { createCronJob } from "./utils/croner.utils.js";
@@ -70,6 +71,7 @@ const start = async () => {
// one shots only needed to run on server startups // one shots only needed to run on server startups
createNotifications(); createNotifications();
startNotifications(); startNotifications();
serversChecks();
}, 5 * 1000); }, 5 * 1000);
process.on("uncaughtException", async (err) => { process.on("uncaughtException", async (err) => {

View File

@@ -9,7 +9,7 @@ type RoomDefinition<T = unknown> = {
export const protectedRooms: any = { export const protectedRooms: any = {
logs: { requiresAuth: true, role: ["admin", "systemAdmin"] }, logs: { requiresAuth: true, role: ["admin", "systemAdmin"] },
admin: { requiresAuth: true, role: ["admin", "systemAdmin"] }, //admin: { requiresAuth: false, role: ["admin", "systemAdmin"] },
}; };
export const roomDefinition: Record<RoomId, RoomDefinition> = { export const roomDefinition: Record<RoomId, RoomDefinition> = {
@@ -36,4 +36,16 @@ export const roomDefinition: Record<RoomId, RoomDefinition> = {
return []; return [];
}, },
}, },
admin: {
seed: async (limit) => {
console.info(limit);
return [];
},
},
"admin:build": {
seed: async (limit) => {
console.info(limit);
return [];
},
},
}; };

View File

@@ -88,14 +88,12 @@ export const setupSocketIORoutes = (baseUrl: string, server: HttpServer) => {
}); });
} }
const roles = Array.isArray(config.role) ? config.role : [config.role]; const roles = Array.isArray(config?.role) ? config?.role : [config?.role];
console.log(roles, s.user.role);
//if (config?.role && s.user?.role !== config.role) { //if (config?.role && s.user?.role !== config.role) {
if (config?.role && !roles.includes(s.user?.role)) { if (config?.role && !roles.includes(s.user?.role)) {
return s.emit("room-error", { return s.emit("room-error", {
room: rn, roomId: rn,
message: `Not authorized to be in room: ${rn}`, message: `Not authorized to be in room: ${rn}`,
}); });
} }

View File

@@ -1 +1 @@
export type RoomId = "logs" | "labels"; //| "alerts" | "metrics"; export type RoomId = "logs" | "labels" | "admin" | "admin:build"; //| "alerts" | "metrics";

View File

@@ -0,0 +1,172 @@
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: 1,
},
{
name: "St. Peters",
server: "USSTP1VMS006",
plantToken: "usstp1",
idAddress: "10.37.0.26",
greatPlainsPlantCode: "45",
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
{
name: "Marked Tree",
server: "USMAR1VMS006",
plantToken: "usmar1",
idAddress: "10.206.9.26",
greatPlainsPlantCode: "90",
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
];
// notes here for when it comes time to pull in all the server address info [test1_AlplaPROD2.0_Read].[masterData].[Plant] has everything from every server :D
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");
}
};
// Communication from logistic network to logisticsSupportTool (for printers and scanners)
// network justification
// scanners and printers are on dhcp

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

View File

@@ -27,7 +27,7 @@ router.get("/", async (_, res) => {
? sqlServerStats?.data[0].UptimeSeconds ? 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 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, tcpServerOnline: isServerRunning,
sqlServerConnected: prodSql, sqlServerConnected: prodSql,
gpServerConnected: gpSql, gpServerConnected: gpSql,

View File

@@ -1,5 +1,6 @@
import type { Express } from "express"; import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js"; import { requireAuth } from "../middleware/auth.middleware.js";
import getServers from "./serverData.route.js";
import getSettings from "./settings.route.js"; import getSettings from "./settings.route.js";
import updSetting from "./settingsUpdate.route.js"; import updSetting from "./settingsUpdate.route.js";
import stats from "./stats.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/stats`, stats);
app.use(`${baseUrl}/api/mobile`, mobile); app.use(`${baseUrl}/api/mobile`, mobile);
app.use(`${baseUrl}/api/settings`, getSettings); app.use(`${baseUrl}/api/settings`, getSettings);
app.use(`${baseUrl}/api/servers`, getServers);
app.use(`${baseUrl}/api/settings`, requireAuth, updSetting); app.use(`${baseUrl}/api/settings`, requireAuth, updSetting);
// all other system should be under /api/system/* // all other system should be under /api/system/*

View File

@@ -2,6 +2,7 @@ import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { import {
admin as adminPlugin, admin as adminPlugin,
genericOAuth,
// apiKey, // apiKey,
// createAuthMiddleware, // createAuthMiddleware,
//customSession, //customSession,
@@ -16,6 +17,46 @@ import { ac, admin, systemAdmin, user } from "./auth.permissions.js";
import { allowedOrigins } from "./cors.utils.js"; import { allowedOrigins } from "./cors.utils.js";
import { sendEmail } from "./sendEmail.utils.js"; import { sendEmail } from "./sendEmail.utils.js";
function decodeJwtPayload<T = Record<string, unknown>>(jwt: string): T {
const parts = jwt.split(".");
if (parts.length < 2) {
throw new Error("Invalid JWT");
}
const payload = parts[1]?.replace(/-/g, "+").replace(/_/g, "/");
const padded = payload?.padEnd(
payload.length + ((4 - (payload.length % 4)) % 4),
"=",
);
const json = Buffer.from(padded ?? "", "base64").toString("utf8");
return JSON.parse(json) as T;
}
function normalizeGroups(groups?: unknown): string[] {
if (!Array.isArray(groups)) return [];
return groups
.filter((g): g is string => typeof g === "string")
.map((g) => g.trim().toLowerCase())
.filter((g) => g.length > 0);
}
type VoidAuthClaims = {
sub: string;
name?: string;
preferred_username?: string;
email?: string;
email_verified?: boolean;
groups?: string[];
picture?: string;
iss?: string;
aud?: string;
exp?: number;
iat?: number;
};
export const schema = { export const schema = {
user: rawSchema.user, user: rawSchema.user,
session: rawSchema.session, session: rawSchema.session,
@@ -25,9 +66,73 @@ export const schema = {
apiKey: rawSchema.apikey, // 🔑 rename to apiKey apiKey: rawSchema.apikey, // 🔑 rename to apiKey
}; };
const hasOAuth =
Boolean(process.env.PROVIDER) &&
Boolean(process.env.CLIENT_ID) &&
Boolean(process.env.CLIENT_SECRET) &&
Boolean(process.env.DISCOVERY_URL);
if (!hasOAuth) {
console.warn("Missing oauth data.");
}
const oauthPlugins = hasOAuth
? [
genericOAuth({
config: [
{
providerId: process.env.PROVIDER!,
clientId: process.env.CLIENT_ID!,
clientSecret: process.env.CLIENT_SECRET!,
discoveryUrl: process.env.DISCOVERY_URL!,
scopes: (process.env.CLIENT_SCOPES ?? "")
.split(/[,\s]+/)
.filter(Boolean),
pkce: true,
requireIssuerValidation: true,
redirectURI: `${process.env.URL}/lst/api/auth/oauth2/callback/${process.env.PROVIDER!}`,
getUserInfo: async (tokens) => {
if (!tokens.idToken) {
throw new Error("VoidAuth did not return an idToken");
}
const claims = decodeJwtPayload<VoidAuthClaims>(tokens.idToken);
const groups = normalizeGroups(claims.groups);
return {
id: claims.sub,
email: claims.email ?? "",
name:
claims.name ??
claims.preferred_username ??
claims.email ??
"Unknown User",
image: claims.picture ?? null,
emailVerified: Boolean(claims.email_verified),
groups,
username: claims.preferred_username ?? null,
} as any;
},
mapProfileToUser: async (profile) => {
return {
name: profile.name,
role: profile.groups?.includes("lst_admins")
? "systemAdmin"
: profile.groups?.includes("admins")
? "admin"
: "user",
};
},
},
],
}),
]
: [];
export const auth = betterAuth({ export const auth = betterAuth({
appName: "lst", appName: "lst",
baseURL: process.env.URL, baseURL: `${process.env.URL}/lst/api/auth`,
database: drizzleAdapter(db, { database: drizzleAdapter(db, {
provider: "pg", provider: "pg",
schema, schema,
@@ -42,6 +147,14 @@ export const auth = betterAuth({
}, },
}, },
}, },
account: {
encryptOAuthTokens: true,
updateAccountOnSignIn: true,
accountLinking: {
enabled: true,
trustedProviders: ["voidauth"],
},
},
plugins: [ plugins: [
jwt({ jwt: { expirationTime: "1h" } }), jwt({ jwt: { expirationTime: "1h" } }),
//apiKey(), //apiKey(),
@@ -63,6 +176,7 @@ export const auth = betterAuth({
return true; return true;
}, },
}), }),
...oauthPlugins,
// customSession(async ({ user, session }) => { // customSession(async ({ user, session }) => {
// const roles = await db // const roles = await db
@@ -121,7 +235,7 @@ export const auth = betterAuth({
}, },
}, },
cookie: { cookie: {
path: "/lst/app", path: "/lst",
sameSite: "lax", sameSite: "lax",
secure: false, secure: false,
httpOnly: true, httpOnly: true,

View 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
View 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,
});
});
});
};

View File

@@ -14,7 +14,8 @@ export interface ReturnHelper<T = unknown[]> {
| "email" | "email"
| "purchase" | "purchase"
| "tcp" | "tcp"
| "logistics"; | "logistics"
| "admin";
subModule: string; subModule: string;
level: "info" | "error" | "debug" | "fatal" | "warn"; level: "info" | "error" | "debug" | "fatal" | "warn";

View 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,
});
};

View 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,
};
};

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -1,8 +0,0 @@
meta {
name: auth
seq: 5
}
auth {
mode: inherit
}

View File

@@ -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
}

View File

@@ -1,9 +0,0 @@
{
"version": "1",
"name": "lst_v3",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}

View File

@@ -1,3 +0,0 @@
docs {
All Api endpoints to the logistics support tool
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -1,8 +0,0 @@
meta {
name: datamart
seq: 2
}
auth {
mode: inherit
}

View File

@@ -1,7 +0,0 @@
vars {
url: http://localhost:3000/lst
readerIp: 10.44.14.215
}
vars:secret [
token
]

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -1,8 +0,0 @@
meta {
name: notifications
seq: 7
}
auth {
mode: inherit
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -1,8 +0,0 @@
meta {
name: ocp
seq: 9
}
auth {
mode: inherit
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -1,8 +0,0 @@
meta {
name: prodSql
seq: 6
}
auth {
mode: inherit
}

View File

@@ -1,8 +0,0 @@
meta {
name: rfidReaders
seq: 8
}
auth {
mode: inherit
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -1,12 +0,0 @@
meta {
name: readerSpecific
}
auth {
mode: basic
}
auth:basic {
username: admin
password: Zebra123!
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -12,48 +12,36 @@ services:
#- "${VITE_PORT:-4200}:4200" #- "${VITE_PORT:-4200}:4200"
- "3600:3000" - "3600:3000"
dns: dns:
- 10.193.9.250 - 10.44.9.250
- 10.193.9.251 # your internal DNS server - 10.44.9.251 # your internal DNS server
dns_search: - 1.1.1.1
- alpla.net # or your internal search suffix
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- LOG_LEVEL=info - LOG_LEVEL=info
- EXTERNAL_URL=http://192.168.8.222:3600 - EXTERNAL_URL=http://192.168.8.222:3600
- DATABASE_HOST=host.docker.internal # if running on the same docker then do this - DATABASE_HOST=postgres # if running on the same docker then do this
- DATABASE_PORT=5433 - DATABASE_PORT=5432
- DATABASE_USER=${DATABASE_USER} - DATABASE_USER=${DATABASE_USER}
- DATABASE_PASSWORD=${DATABASE_PASSWORD} - DATABASE_PASSWORD=${DATABASE_PASSWORD}
- DATABASE_DB=${DATABASE_DB} - DATABASE_DB=${DATABASE_DB}
- PROD_SERVER=${PROD_SERVER} - PROD_SERVER=10.75.9.56 #${PROD_SERVER}
- PROD_PLANT_TOKEN=${PROD_PLANT_TOKEN} - PROD_PLANT_TOKEN=${PROD_PLANT_TOKEN}
- PROD_USER=${PROD_USER} - PROD_USER=${PROD_USER}
- PROD_PASSWORD=${PROD_PASSWORD} - PROD_PASSWORD=${PROD_PASSWORD}
- GP_SERVER=10.193.9.31
- SQL_PORT=1433
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
- BETTER_AUTH_URL=${URL} - BETTER_AUTH_URL=${URL}
- OPENDOCK_URL=${OPENDOCK_URL}
- OPENDOCK_PASSWORD=${OPENDOCK_PASSWORD}
- DEFAULT_DOCK=${DEFAULT_DOCK}
- DEFAULT_LOAD_TYPE=${DEFAULT_LOAD_TYPE}
- DEFAULT_CARRIER=${DEFAULT_CARRIER}
#for all host including prod servers, plc's, printers, or other de #for all host including prod servers, plc's, printers, or other de
# extra_hosts: networks:
# - "${PROD_SERVER}:${PROD_IP}" - docker-network
# networks: networks:
# - default docker-network:
# - logisticsNetwork external: true
# #- mlan1
# networks:
# logisticsNetwork:
# driver: macvlan
# driver_opts:
# parent: eth0
# ipam:
# config:
# - subnet: ${LOGISTICS_NETWORK}
# gateway: ${LOGISTICS_GATEWAY}
# mlan1:
# driver: macvlan
# driver_opts:
# parent: eth0
# ipam:
# config:
# - subnet: ${MLAN1_NETWORK}
# gateway: ${MLAN1_GATEWAY}

View File

@@ -19,6 +19,8 @@
"better-auth": "^1.5.5", "better-auth": "^1.5.5",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"lucide-react": "^0.577.0", "lucide-react": "^0.577.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
@@ -6016,6 +6018,25 @@
"node": ">= 12" "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": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",

View File

@@ -34,7 +34,9 @@
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.1", "tailwindcss": "^4.2.1",
"tw-animate-css": "^1.4.0", "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": { "devDependencies": {
"@eslint/js": "^9.36.0", "@eslint/js": "^9.36.0",

View File

@@ -1,5 +1,5 @@
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { Bell, Logs, Settings } from "lucide-react"; import { Bell, Logs, Server, Settings } from "lucide-react";
import { import {
SidebarGroup, SidebarGroup,
@@ -40,6 +40,14 @@ export default function AdminSidebar({ session }: any) {
module: "admin", module: "admin",
active: true, active: true,
}, },
{
title: "Servers",
url: "/admin/servers",
icon: Server,
role: ["systemAdmin", "admin"],
module: "admin",
active: true,
},
{ {
title: "Logs", title: "Logs",
url: "/admin/logs", url: "/admin/logs",

View File

@@ -1,22 +1,55 @@
import { useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import socket from "@/lib/socket.io"; 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 [data, setData] = useState<T[]>([]);
const [info, setInfo] = useState( const [info, setInfo] = useState(
"No data yet — join the room to start receiving", "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(() => { useEffect(() => {
function handleConnect() { function handleConnect() {
socket.emit("join-room", roomId); 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]); 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"); setInfo(err.message ?? "Room error");
} }
@@ -31,6 +64,7 @@ export function useSocketRoom<T>(roomId: string) {
// If already connected, join immediately // If already connected, join immediately
if (socket.connected) { if (socket.connected) {
socket.emit("join-room", roomId); socket.emit("join-room", roomId);
setInfo(`Joined room: ${roomId}`);
} }
return () => { return () => {
@@ -42,5 +76,5 @@ export function useSocketRoom<T>(roomId: string) {
}; };
}, [roomId]); }, [roomId]);
return { data, info }; return { data, info, clearRoom };
} }

View File

@@ -1,4 +1,4 @@
import { adminClient } from "better-auth/client/plugins"; import { adminClient, genericOAuthClient } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react"; import { createAuthClient } from "better-auth/react";
import { ac, admin, systemAdmin, user } from "./auth-permissions"; import { ac, admin, systemAdmin, user } from "./auth-permissions";
@@ -13,6 +13,7 @@ export const authClient = createAuthClient({
systemAdmin, systemAdmin,
}, },
}), }),
genericOAuthClient(),
], ],
}); });

View 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;
};

View File

@@ -105,6 +105,7 @@ export default function LstTable({
</TableBody> </TableBody>
</Table> </Table>
<ScrollBar orientation="horizontal" /> <ScrollBar orientation="horizontal" />
<ScrollBar orientation="vertical" />
</ScrollArea> </ScrollArea>
<div className="flex items-center justify-end space-x-2 py-4"> <div className="flex items-center justify-end space-x-2 py-4">
<Button <Button

View File

@@ -14,6 +14,7 @@ import { Route as IndexRouteImport } from './routes/index'
import { Route as DocsIndexRouteImport } from './routes/docs/index' import { Route as DocsIndexRouteImport } from './routes/docs/index'
import { Route as DocsSplatRouteImport } from './routes/docs/$' import { Route as DocsSplatRouteImport } from './routes/docs/$'
import { Route as AdminSettingsRouteImport } from './routes/admin/settings' 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 AdminNotificationsRouteImport } from './routes/admin/notifications'
import { Route as AdminLogsRouteImport } from './routes/admin/logs' import { Route as AdminLogsRouteImport } from './routes/admin/logs'
import { Route as authLoginRouteImport } from './routes/(auth)/login' import { Route as authLoginRouteImport } from './routes/(auth)/login'
@@ -46,6 +47,11 @@ const AdminSettingsRoute = AdminSettingsRouteImport.update({
path: '/admin/settings', path: '/admin/settings',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const AdminServersRoute = AdminServersRouteImport.update({
id: '/admin/servers',
path: '/admin/servers',
getParentRoute: () => rootRouteImport,
} as any)
const AdminNotificationsRoute = AdminNotificationsRouteImport.update({ const AdminNotificationsRoute = AdminNotificationsRouteImport.update({
id: '/admin/notifications', id: '/admin/notifications',
path: '/admin/notifications', path: '/admin/notifications',
@@ -83,6 +89,7 @@ export interface FileRoutesByFullPath {
'/login': typeof authLoginRoute '/login': typeof authLoginRoute
'/admin/logs': typeof AdminLogsRoute '/admin/logs': typeof AdminLogsRoute
'/admin/notifications': typeof AdminNotificationsRoute '/admin/notifications': typeof AdminNotificationsRoute
'/admin/servers': typeof AdminServersRoute
'/admin/settings': typeof AdminSettingsRoute '/admin/settings': typeof AdminSettingsRoute
'/docs/$': typeof DocsSplatRoute '/docs/$': typeof DocsSplatRoute
'/docs/': typeof DocsIndexRoute '/docs/': typeof DocsIndexRoute
@@ -96,6 +103,7 @@ export interface FileRoutesByTo {
'/login': typeof authLoginRoute '/login': typeof authLoginRoute
'/admin/logs': typeof AdminLogsRoute '/admin/logs': typeof AdminLogsRoute
'/admin/notifications': typeof AdminNotificationsRoute '/admin/notifications': typeof AdminNotificationsRoute
'/admin/servers': typeof AdminServersRoute
'/admin/settings': typeof AdminSettingsRoute '/admin/settings': typeof AdminSettingsRoute
'/docs/$': typeof DocsSplatRoute '/docs/$': typeof DocsSplatRoute
'/docs': typeof DocsIndexRoute '/docs': typeof DocsIndexRoute
@@ -110,6 +118,7 @@ export interface FileRoutesById {
'/(auth)/login': typeof authLoginRoute '/(auth)/login': typeof authLoginRoute
'/admin/logs': typeof AdminLogsRoute '/admin/logs': typeof AdminLogsRoute
'/admin/notifications': typeof AdminNotificationsRoute '/admin/notifications': typeof AdminNotificationsRoute
'/admin/servers': typeof AdminServersRoute
'/admin/settings': typeof AdminSettingsRoute '/admin/settings': typeof AdminSettingsRoute
'/docs/$': typeof DocsSplatRoute '/docs/$': typeof DocsSplatRoute
'/docs/': typeof DocsIndexRoute '/docs/': typeof DocsIndexRoute
@@ -125,6 +134,7 @@ export interface FileRouteTypes {
| '/login' | '/login'
| '/admin/logs' | '/admin/logs'
| '/admin/notifications' | '/admin/notifications'
| '/admin/servers'
| '/admin/settings' | '/admin/settings'
| '/docs/$' | '/docs/$'
| '/docs/' | '/docs/'
@@ -138,6 +148,7 @@ export interface FileRouteTypes {
| '/login' | '/login'
| '/admin/logs' | '/admin/logs'
| '/admin/notifications' | '/admin/notifications'
| '/admin/servers'
| '/admin/settings' | '/admin/settings'
| '/docs/$' | '/docs/$'
| '/docs' | '/docs'
@@ -151,6 +162,7 @@ export interface FileRouteTypes {
| '/(auth)/login' | '/(auth)/login'
| '/admin/logs' | '/admin/logs'
| '/admin/notifications' | '/admin/notifications'
| '/admin/servers'
| '/admin/settings' | '/admin/settings'
| '/docs/$' | '/docs/$'
| '/docs/' | '/docs/'
@@ -165,6 +177,7 @@ export interface RootRouteChildren {
authLoginRoute: typeof authLoginRoute authLoginRoute: typeof authLoginRoute
AdminLogsRoute: typeof AdminLogsRoute AdminLogsRoute: typeof AdminLogsRoute
AdminNotificationsRoute: typeof AdminNotificationsRoute AdminNotificationsRoute: typeof AdminNotificationsRoute
AdminServersRoute: typeof AdminServersRoute
AdminSettingsRoute: typeof AdminSettingsRoute AdminSettingsRoute: typeof AdminSettingsRoute
DocsSplatRoute: typeof DocsSplatRoute DocsSplatRoute: typeof DocsSplatRoute
DocsIndexRoute: typeof DocsIndexRoute DocsIndexRoute: typeof DocsIndexRoute
@@ -210,6 +223,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AdminSettingsRouteImport preLoaderRoute: typeof AdminSettingsRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/admin/servers': {
id: '/admin/servers'
path: '/admin/servers'
fullPath: '/admin/servers'
preLoaderRoute: typeof AdminServersRouteImport
parentRoute: typeof rootRouteImport
}
'/admin/notifications': { '/admin/notifications': {
id: '/admin/notifications' id: '/admin/notifications'
path: '/admin/notifications' path: '/admin/notifications'
@@ -261,6 +281,7 @@ const rootRouteChildren: RootRouteChildren = {
authLoginRoute: authLoginRoute, authLoginRoute: authLoginRoute,
AdminLogsRoute: AdminLogsRoute, AdminLogsRoute: AdminLogsRoute,
AdminNotificationsRoute: AdminNotificationsRoute, AdminNotificationsRoute: AdminNotificationsRoute,
AdminServersRoute: AdminServersRoute,
AdminSettingsRoute: AdminSettingsRoute, AdminSettingsRoute: AdminSettingsRoute,
DocsSplatRoute: DocsSplatRoute, DocsSplatRoute: DocsSplatRoute,
DocsIndexRoute: DocsIndexRoute, DocsIndexRoute: DocsIndexRoute,

View File

@@ -1,4 +1,5 @@
import { Link, useNavigate } from "@tanstack/react-router"; import { Link, useNavigate } from "@tanstack/react-router";
import { Cat } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
Card, Card,
@@ -9,13 +10,23 @@ import {
} from "@/components/ui/card"; } from "@/components/ui/card";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
import { useAppForm } from "@/lib/formSutff"; import { useAppForm } from "@/lib/formSutff";
import { Button } from "../../../components/ui/button";
import socket from "../../../lib/socket.io"; import socket from "../../../lib/socket.io";
export default function LoginForm({ redirectPath }: { redirectPath: string }) { export default function LoginForm({ redirectPath }: { redirectPath: string }) {
const loginEmail = localStorage.getItem("loginEmail") || ""; const loginEmail = localStorage.getItem("loginEmail") || "";
const rememberMe = localStorage.getItem("rememberMe") === "true"; const rememberMe = localStorage.getItem("rememberMe") === "true";
const navigate = useNavigate(); const navigate = useNavigate();
const oauthLogin = async () => {
await authClient.signIn.oauth2({
providerId: "voidauth",
callbackURL: "/lst/app",
errorCallbackURL: "/lst/app/login",
});
};
const form = useAppForm({ const form = useAppForm({
defaultValues: { defaultValues: {
email: loginEmail, email: loginEmail,
@@ -26,7 +37,7 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
// set remember me incase we want it later // set remember me incase we want it later
if (value.rememberMe) { if (value.rememberMe) {
localStorage.setItem("rememberMe", value.rememberMe.toString()); localStorage.setItem("rememberMe", value.rememberMe.toString());
localStorage.setItem("loginEmail", value.email); localStorage.setItem("loginEmail", value.email.toLocaleLowerCase());
} else { } else {
localStorage.removeItem("rememberMe"); localStorage.removeItem("rememberMe");
localStorage.removeItem("loginEmail"); localStorage.removeItem("loginEmail");
@@ -62,7 +73,17 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
<div> <div>
<Card className="p-3 w-96"> <Card className="p-3 w-96">
<CardHeader> <CardHeader>
<CardTitle>Login to your account</CardTitle> <CardTitle>
<div className="flex flex-row justify-center">
<Button onClick={oauthLogin} size="lg" variant="ghost">
<Cat />
</Button>
<span className="mt-2">Login to your account</span>{" "}
<Button size="lg" variant="ghost">
<Cat />
</Button>
</div>
</CardTitle>
<CardDescription> <CardDescription>
Enter your username and password below Enter your username and password below
</CardDescription> </CardDescription>
@@ -76,12 +97,19 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
> >
<form.AppField name="email"> <form.AppField name="email">
{(field) => ( {(field) => (
<field.InputField label="Email" inputType="email" required /> <field.InputField
label="Email"
inputType="email"
required={rememberMe}
/>
)} )}
</form.AppField> </form.AppField>
<form.AppField name="password"> <form.AppField name="password">
{(field) => ( {(field) => (
<field.InputPasswordField label="Password" required={true} /> <field.InputPasswordField
label="Password"
required={rememberMe}
/>
)} )}
</form.AppField> </form.AppField>
@@ -98,7 +126,7 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
</Link> </Link>
</div> </div>
<div className="flex justify-end mt-2"> <div className="flex justify-between mt-2 ">
<form.AppForm> <form.AppForm>
<form.SubmitButton>Login</form.SubmitButton> <form.SubmitButton>Login</form.SubmitButton>
</form.AppForm> </form.AppForm>

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

View File

@@ -40,7 +40,7 @@ export default function Index() {
</Text>, </Text>,
); );
await devDelay(1500); await devDelay(1500);
router.replace("/setup"); router.replace("/scanner");
return; return;
} }

View File

@@ -13,12 +13,14 @@ export default function setup() {
const serverIpFromStore = useAppStore((s) => s.serverIp); const serverIpFromStore = useAppStore((s) => s.serverIp);
const serverPortFromStore = useAppStore((s) => s.serverPort); const serverPortFromStore = useAppStore((s) => s.serverPort);
const scannerIdFromStore = useAppStore((s) => s.scannerId);
const updateAppState = useAppStore((s) => s.updateAppState); const updateAppState = useAppStore((s) => s.updateAppState);
// local form state // local form state
const [serverIp, setLocalServerIp] = useState(serverIpFromStore); const [serverIp, setLocalServerIp] = useState(serverIpFromStore);
const [serverPort, setLocalServerPort] = useState(serverPortFromStore); const [serverPort, setLocalServerPort] = useState(serverPortFromStore);
const [scannerId, setScannerId] = useState(scannerIdFromStore);
const authCheck = () => { const authCheck = () => {
if (pin === "6971") { if (pin === "6971") {
@@ -98,7 +100,7 @@ export default function setup() {
value={serverPort} value={serverPort}
onChangeText={setLocalServerPort} onChangeText={setLocalServerPort}
placeholder="3000" placeholder="3000"
autoCapitalize="characters" //autoCapitalize="characters"
keyboardType="numeric" keyboardType="numeric"
style={{ borderWidth: 1, padding: 10, borderRadius: 8 }} style={{ borderWidth: 1, padding: 10, borderRadius: 8 }}
/> />
@@ -107,8 +109,9 @@ export default function setup() {
<View> <View>
<Text>Scanner ID</Text> <Text>Scanner ID</Text>
<Text style={{ width: 250 }}> <Text style={{ width: 250 }}>
This is needed as you will be redirected to the standard scanner The ID is required to be able to scan. The scanner will be
with no rules except the rules that alplaprod puts in treated as a normal scanner direct to alplaprod. no extra rules
added.
</Text> </Text>
<TextInput <TextInput
value={scannerId} value={scannerId}

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

View File

@@ -0,0 +1 @@
ALTER TABLE "server_data" RENAME COLUMN "great_plains_plantCode" TO "great_plains_plant_code";

View File

@@ -0,0 +1 @@
ALTER TABLE "server_data" ADD CONSTRAINT "server_data_server_unique" UNIQUE("server");

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

View File

@@ -0,0 +1 @@
ALTER TABLE "server_data" ALTER COLUMN "plant_token" SET NOT NULL;

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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -239,6 +239,55 @@
"when": 1776256060808, "when": 1776256060808,
"tag": "0033_elite_adam_warlock", "tag": "0033_elite_adam_warlock",
"breakpoints": true "breakpoints": true
},
{
"idx": 34,
"version": "7",
"when": 1776732155543,
"tag": "0034_groovy_darkhawk",
"breakpoints": true
},
{
"idx": 35,
"version": "7",
"when": 1776733278288,
"tag": "0035_icy_harpoon",
"breakpoints": true
},
{
"idx": 36,
"version": "7",
"when": 1776733364021,
"tag": "0036_easy_magus",
"breakpoints": true
},
{
"idx": 37,
"version": "7",
"when": 1776733842142,
"tag": "0037_glamorous_joseph",
"breakpoints": true
},
{
"idx": 38,
"version": "7",
"when": 1776733879132,
"tag": "0038_special_wildside",
"breakpoints": true
},
{
"idx": 39,
"version": "7",
"when": 1776734237129,
"tag": "0039_special_the_leader",
"breakpoints": true
},
{
"idx": 40,
"version": "7",
"when": 1776770845947,
"tag": "0040_rainy_white_tiger",
"breakpoints": true
} }
] ]
} }

846
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "lst_v3", "name": "lst_v3",
"version": "0.0.1-alpha.4", "version": "0.0.2-alpha.6",
"description": "The tool that supports us in our everyday alplaprod", "description": "The tool that supports us in our everyday alplaprod",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@@ -39,6 +39,7 @@
"@biomejs/biome": "2.4.8", "@biomejs/biome": "2.4.8",
"@commitlint/cli": "^20.5.0", "@commitlint/cli": "^20.5.0",
"@commitlint/config-conventional": "^20.5.0", "@commitlint/config-conventional": "^20.5.0",
"@types/archiver": "^7.0.0",
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/morgan": "^1.9.10", "@types/morgan": "^1.9.10",
@@ -66,6 +67,7 @@
"@dotenvx/dotenvx": "^1.57.0", "@dotenvx/dotenvx": "^1.57.0",
"@scalar/express-api-reference": "^0.9.4", "@scalar/express-api-reference": "^0.9.4",
"@socket.io/admin-ui": "^0.5.1", "@socket.io/admin-ui": "^0.5.1",
"archiver": "^7.0.1",
"axios": "^1.13.6", "axios": "^1.13.6",
"better-auth": "^1.5.5", "better-auth": "^1.5.5",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",

View File

@@ -1,132 +0,0 @@
docker build -t git.tuffraid.net/cowch/lst_v3:latest .
docker push git.tuffraid.net/cowch/lst_v3:latest
docker compose pull && docker compose up -d --force-recreate
How to choose the bump
Use this rule:
patch = bug fix, small safe improvement
minor = new feature, backward compatible
major = breaking change
Changesets uses semver bump ty
### daily process
npm commit
- when closing a issue at the end add
Use one of these in the commit body or PR description:
- - Closes #123
- - Fixes #123
- - Resolves #123
Common ones:
- - Closes #123
- - Fixes #123
- - Resolves #123
Reference an issue without closing it
Use:
- - Refs #123
- - Related to #123
- - See #123
Good safe one:
- - Refs #123
Good example commit
Subject:
- - fix(cors): normalize external url origin
Body:
- - Refs #42
Or if this should close it:
- - Closes #42
# Release flow
npm run changeset:add
Pick one:
- patch = bug fix
- minor = new feature, non-breaking
- major = breaking change
Edit the generated .md file in .changeset it will be randomly named and add anything else in here from all the commits that are new to this release
Recommended release command
npm run changeset:version
stage the change log file
git commit -m "chore(release): version packages"
git tag v0.0.1-alpha.0 change this to the same version thats in the pkg.json
then push it
git push
git push --tags
### release type
when we want to go from alpha to normal well do
npx changeset pre enter alpha
npx changeset pre enter rc
go to full production
npx changeset pre exit
npx changeset version
### Steps will make it cleaner later
Daily work
1. Stage files
2. npm run commit
3. Add issue keyword if needed
4. git push when ready
Release flow
1. npx changeset
2. pick patch/minor/major
3. edit the generated md file with better notes
4. npx changeset version
5. git add .
6. git commit -m "chore(release): version packages"
7. git tag vX.X.X
8. git push
9. git push --tags
# normal work
stage files
npm run commit
# if releasing
npm run commit
npm run release -- --prerelease alpha
git push
git push --tags
git add .
git commit -m "chore(release): version packages"
git tag v0.0.1-alpha.0
git push
git push --tags

View File

@@ -16,6 +16,7 @@ param (
# server migrations get - reminder to add to old version in pkg "start:lst": "cd lstV2 && npm start", # server migrations get - reminder to add to old version in pkg "start:lst": "cd lstV2 && npm start",
# powershell.exe -ExecutionPolicy Bypass -File .\scripts\services.ps1 -serviceName "LST_app" -option "install" -appPath "D:\LST" -description "Logistics Support Tool" -command "run start" # powershell.exe -ExecutionPolicy Bypass -File .\scripts\services.ps1 -serviceName "LST_app" -option "install" -appPath "D:\LST" -description "Logistics Support Tool" -command "run start"
# powershell.exe -ExecutionPolicy Bypass -File .\scripts\services.ps1 -serviceName "LSTV2" -option "install" -appPath "D:\LST" -description "Logistics Support Tool" -command "run start:lst" # powershell.exe -ExecutionPolicy Bypass -File .\scripts\services.ps1 -serviceName "LSTV2" -option "install" -appPath "D:\LST" -description "Logistics Support Tool" -command "run start:lst"
# powershell.exe -ExecutionPolicy Bypass -File .\scripts\services.ps1 -serviceName "LST_ctl" -option "delete" -appPath "D:\LST" -description "Logistics Support Tool" -command "run start:lst"
$nssmPath = $AppPath + "\nssm.exe" $nssmPath = $AppPath + "\nssm.exe"
$npmPath = "C:\Program Files\nodejs\npm.cmd" # Path to npm.cmd $npmPath = "C:\Program Files\nodejs\npm.cmd" # Path to npm.cmd

225
scripts/updateServer.ps1 Normal file
View File

@@ -0,0 +1,225 @@
param(
[string]$Server,
[string]$Destination,
[string]$Token,
[string]$ADM_USER,
[string]$ADM_PASSWORD,
[string]$AppDir
)
# $credFile = Join-Path $AppDir ".scriptCreds"
# $credData = @{}
# Get-Content $credFile | ForEach-Object {
# if ($_ -match "=") {
# $key, $value = $_ -split "=", 2
# $credData[$key.Trim()] = $value.Trim()
# }
# }
$username = $ADM_USER
$password = $ADM_PASSWORD
$securePass = ConvertTo-SecureString $password -AsPlainText -Force
$credentials = New-Object System.Management.Automation.PSCredential($username, $securePass)
function Update-Server {
param (
[string]$Destination,
[string]$Server,
[string]$Token
)
$buildFile = Join-Path $AppDir ".buildNumber"
$BuildNumber = 1
$BuildFolder = Join-Path $AppDir "builds"
if (Test-Path $BuildFile) {
$content = Get-Content $BuildFile | Select-Object -First 1
$num = $content.Trim() -as [int] # safe cast
if ($num) {
$BuildNumber = $num + 1
}
else {
$BuildNumber = 1
}
}
# Get The current Build we have zipped up
$BuildNumber = ([int]$BuildNumber - 1).ToString()
# copy the latest build over
Write-Host "Forcing the removal of the mapped drive."
Get-PSDrive -Name "z" -ErrorAction SilentlyContinue | Remove-PSDrive -Force
try {
New-PSDrive -Name "z" -PSProvider FileSystem -Root "\\$Server\$Destination" -Credential $credentials
# Create the update folder if it doesn't exist
if (-not (Test-Path -Path "\\$Server\$Destination")) {
New-Item -ItemType Directory -Path "\\$Server\$Destination" -Force
}
# Copying files to the server
Write-Host "Copying files to $($Server)"
$zipFile = Join-Path $BuildFolder "LSTV3-$BuildNumber.zip"
Copy-Item -Path $zipFile -Destination "z:\" -Force
Write-Host "Files copied to $($Server)"
}
catch {
Write-Host "Error: $_"
}
finally {
# Remove the mapped drive after copying
if (Get-PSDrive -Name "z" -ErrorAction SilentlyContinue) {
Write-Host "Removing mapped drive..."
Remove-PSDrive -Name "z"
}
}
Write-Host "Updating the app to LSTV3-$BuildNumber.zip"
# do the stop services, unzip, and restart service and pool
$AppUpdate = {
param ($Server, $Token, $Destination, $BuildFile)
function Fix-Env {
$envFile = ".env"
if (-not (Test-Path $envFile)) {
Write-Host ".env not found, creating..."
New-Item -ItemType File -Path $envFile | Out-Null
}
$envContent = Get-Content $envFile -Raw
if ([string]::IsNullOrWhiteSpace($envContent)) {
$envContent = ""
}
$envVarsToAdd = @{
"PROVIDER" = "voidauth"
"CLIENT_ID" = "crIVcUilFWIS6ME3"
"CLIENT_SECRET" = "zsJeyjMN2yDDqfyzSsh96OtlA2714F5d"
"CLIENT_SCOPES" = "openid profile email groups"
"DISCOVERY_URL" = "https://auth.tuffraid.net/oidc/.well-known/openid-configuration"
}
$linesToAppend = @()
foreach ($key in $envVarsToAdd.Keys) {
$escapedKey = [regex]::Escape($key)
if ($envContent -notmatch "(?m)^$escapedKey=") {
$linesToAppend += "$key=$($envVarsToAdd[$key])"
Write-Host "Adding missing env: $key"
}
else {
Write-Host "Env exists, skipping add: $key"
}
}
###### to replace the values of mistakens or something fun where we need to fix across all 17 servers put it here.
$envVarsToReplace = @{
# "PORT" = "3000"
#"URL" = "https://$($Token)prod.alpla.net/lst"
}
foreach ($key in $envVarsToReplace.Keys) {
$value = $envVarsToReplace[$key]
$escapedKey = [regex]::Escape($key)
if ($envContent -match "(?m)^$escapedKey=") {
Write-Host "Replacing env: $key -> $value"
$envContent = $envContent -replace "(?m)^$escapedKey=.*", "$key=$value"
}
else {
Write-Host "Env not found for replace, skipping: $key"
}
}
if ($linesToAppend.Count -gt 0) {
if ($envContent.Length -gt 0 -and -not $envContent.EndsWith("`n")) {
$envContent += "`r`n"
}
$envContent += "`r`n# ---- VoidAuth Config ----`r`n"
$envContent += ($linesToAppend -join "`r`n")
Write-Host "Appending new env vars."
}
Set-Content -Path $envFile -Value $envContent
Write-Host "Env update completed."
}
#convert everything to the server fun
$LocalPath = $Destination -replace '\$', ':'
$BuildFileLoc = "$LocalPath\$BuildFile"
Write-Host "Updating the app to $($BuildFile)"
Write-Host "Stopping the services to do the updates, pkgs and db changes."
$app_name = "LSTV3_app$(if ($Token -eq "usiow2") { "_2" })"
# TODO: add in the iis reset later
Write-Host "Stopping $($app_name)"
Stop-Service -DisplayName $app_name -Force
Start-Sleep -Seconds 1
Write-Host "Unzipping the folder..."
try {
# Expand the archive
Expand-Archive -Path $BuildFileLoc -DestinationPath $LocalPath -Force
}
catch {
Write-Host "Error: $_"
exit 1 # Exit with a non-zero code if there's an error
}
# Delete the zip file after extraction
Write-Host "Deleting the zip file..."
Remove-Item -Path $BuildFileLoc -Force
Start-Sleep -Seconds 1
try {
# do the install/update
Push-Location $LocalPath
Write-Host "Running install/update in: $LocalPath"
npm install --omit=dev
Start-Sleep -Seconds 3
Write-Host "Install/update completed."
# update the env to include the new and missing things silly people and wanting things fixed :(
Fix-Env #-Path $LocalPath
# do the migrations
# Push-Location $LocalPath
Write-Host "Running migrations"
npm run dev:db:migrate
Start-Sleep -Seconds 3
Write-Host "Migrations Completed."
}
catch {
Write-Host "Migration: $_"
}
finally {
Pop-Location
}
Write-Host "Starting $($app_name)"
Start-Service -DisplayName $app_name -ErrorAction Stop
Start-Sleep -Seconds 1
#Write-Host "Update completed on $($Server)-$($Token)"
}
Invoke-Command -ComputerName $Server -ScriptBlock $AppUpdate -ArgumentList $Server, $Token, $Destination, "LSTV3-$BuildNumber.zip" -Credential $credentials
}
Update-Server -Server $Server -Destination $Destination -Token $Token