12 Commits

Author SHA1 Message Date
87f738702a docs(notifcations): docs for intro, notifcations, reprint added
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m25s
2026-04-10 21:35:12 -05:00
38a0b65e94 refactor(connection): corrected the connection to the old system 2026-04-10 21:33:55 -05:00
9a0ef8e51a refactor(notification): blocking added 2026-04-10 21:33:26 -05:00
dcb3f2dd13 refactor(server): added in serverCrash email 2026-04-10 21:32:25 -05:00
e47ea9ec52 ci(agent): added in jeff city 2026-04-10 21:31:57 -05:00
ca3425d327 docs(env example): updated the file 2026-04-10 21:30:46 -05:00
3bf024cfc9 refactor(agent): changed to have the test servers on there own push for better testing
production servers will soon pull a build from git rather and push the zip so splitting things up
now
2026-04-10 14:12:02 -05:00
9d39c13510 refactor(puchase): changes how the error handling works so a better email can be sent 2026-04-10 13:58:30 -05:00
c9eb59e2ad refactor(reprint): new query added to deactivate the old notifcation so no chance of duplicates 2026-04-10 13:57:52 -05:00
b0e5fd7999 feat(migrate): quality alert migrated 2026-04-10 13:57:15 -05:00
07ebf88806 refactor(templates): corrections for new notify process on critcal errors 2026-04-10 10:33:01 -05:00
79e653efa3 refactor(logging): when notify is true send the error to systemAdmins 2026-04-10 10:32:20 -05:00
41 changed files with 2641 additions and 70 deletions

View File

@@ -1,32 +1,51 @@
NODE_ENV=development
# Server # Server
PORT=3000 PORT=3000
URL=http://localhost:3000 URL=http://localhost:3000
TIMEZONE=America/New_York
TCP_PORT=2222
# authentication # Better auth Secret
BETTER_AUTH_SECRET="" BETTER_AUTH_SECRET=
RESET_EXPIRY_SECONDS=3600 RESET_EXPIRY_SECONDS=3600
# logging # logging
LOG_LEVEL=debug LOG_LEVEL=
# prodServer # SMTP password
PROD_SERVER=usmcd1vms036 SMTP_PASSWORD=
PROD_PLANT_TOKEN=test3
PROD_USER=alplaprod # opendock
PROD_PASSWORD=password OPENDOCK_URL=https://neutron.opendock.com
OPENDOCK_PASSWORD=
DEFAULT_DOCK=
DEFAULT_LOAD_TYPE=
DEFAULT_CARRIER=
# prodServer when runing on an actual prod server use localhost this way we dont go out and back in.
PROD_SERVER=
PROD_PLANT_TOKEN=
PROD_USER=
PROD_PASSWORD=
# Tech user for alplaprod api
TEC_API_KEY=
# AD STUFF
# this is mainly used for purchase stuff to reference reqs
LDAP_URL=
# postgres connection # postgres connection
DATABASE_HOST=localhost DATABASE_HOST=localhost
DATABASE_PORT=5433 DATABASE_PORT=5432
DATABASE_USER=user DATABASE_USER=
DATABASE_PASSWORD=password DATABASE_PASSWORD=
DATABASE_DB=lst_dev DATABASE_DB=
# how is the app running server or client when in client mode you must provide the server # Gp connection
APP_RUNNING_IN=server GP_USER=
SERVER_NAME=localhost GP_PASSWORD=
#dev stuff # how often to check for new/updated queries in min
GITEA_TOKEN="" QUERY_TIME_TYPE=m #valid options are m, h
EMAIL_USER="" QUERY_CHECK=1
EMAIL_PASSWORD=""

View File

@@ -65,6 +65,7 @@
"onnotice", "onnotice",
"opendock", "opendock",
"opendocks", "opendocks",
"palletizer",
"ppoo", "ppoo",
"preseed", "preseed",
"prodlabels", "prodlabels",

View File

@@ -5,6 +5,7 @@ import { db } from "../db/db.controller.js";
import { logs } from "../db/schema/logs.schema.js"; import { logs } from "../db/schema/logs.schema.js";
import { emitToRoom } from "../socket.io/roomEmitter.socket.js"; import { emitToRoom } from "../socket.io/roomEmitter.socket.js";
import { tryCatch } from "../utils/trycatch.utils.js"; import { tryCatch } from "../utils/trycatch.utils.js";
import { notifySystemIssue } from "./logger.notify.js";
//import build from "pino-abstract-transport"; //import build from "pino-abstract-transport";
export const logLevel = process.env.LOG_LEVEL || "info"; export const logLevel = process.env.LOG_LEVEL || "info";
@@ -45,6 +46,10 @@ const dbStream = new Writable({
console.error(res.error); console.error(res.error);
} }
if (obj.notify) {
notifySystemIssue(obj);
}
if (obj.room) { if (obj.room) {
emitToRoom(obj.room, res.data ? res.data[0] : obj); emitToRoom(obj.room, res.data ? res.data[0] : obj);
} }

View File

@@ -0,0 +1,44 @@
/**
* For all logging that has notify set to true well send an email to the system admins, if we have a discord webhook set well send it there as well
*/
import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { user } from "../db/schema/auth.schema.js";
import { sendEmail } from "../utils/sendEmail.utils.js";
type NotifyData = {
module: string;
submodule: string;
hostname: string;
msg: string;
stack: unknown[];
};
export const notifySystemIssue = async (data: NotifyData) => {
// build the email out
const formattedError = Array.isArray(data.stack)
? data.stack.map((e: any) => e.error || e)
: data.stack;
const sysAdmin = await db
.select()
.from(user)
.where(eq(user.role, "systemAdmin"));
await sendEmail({
email: sysAdmin.map((r) => r.email).join("; ") ?? "cowchmonkey@gmail.com", // change to pull in system admin emails
subject: `${data.hostname} has encountered a critical issue.`,
template: "serverCritialIssue",
context: {
plant: data.hostname,
module: data.module,
subModule: data.submodule,
message: data.msg,
error: JSON.stringify(formattedError, null, 2),
},
});
// TODO: add discord
};

View File

@@ -0,0 +1,96 @@
import { eq } from "drizzle-orm";
import { type Response, Router } from "express";
import { db } from "../db/db.controller.js";
import { notifications } from "../db/schema/notifications.schema.js";
import { auth } from "../utils/auth.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.post("/", async (req, res: Response) => {
const hasPermissions = await auth.api.userHasPermission({
body: {
//userId: req?.user?.id,
role: req.user?.roles as any,
permissions: {
notifications: ["readAll"], // This must match the structure in your access control
},
},
});
if (!hasPermissions) {
return apiReturn(res, {
success: false,
level: "error",
module: "notification",
subModule: "post",
message: `You do not have permissions to be here`,
data: [],
status: 400,
});
}
const { data: nName, error: nError } = await tryCatch(
db
.select()
.from(notifications)
.where(eq(notifications.name, req.body.name)),
);
if (nError) {
return apiReturn(res, {
success: false,
level: "error",
module: "notification",
subModule: "get",
message: `There was an error getting the notifications `,
data: [nError],
status: 400,
});
}
const { data: sub, error: sError } = await tryCatch(
db
.select()
.from(notifications)
.where(eq(notifications.name, req.body.name)),
);
if (sError) {
return apiReturn(res, {
success: false,
level: "error",
module: "notification",
subModule: "get",
message: `There was an error getting the subs `,
data: [sError],
status: 400,
});
}
const emailString = [
...new Set(
sub.flatMap((e: any) =>
e.emails?.map((email: any) => email.trim().toLowerCase()),
),
),
].join(";");
console.log(emailString);
const { default: runFun } = await import(
`./notification.${req.body.name.trim()}.js`
);
const manual = await runFun(nName[0], "blake.matthes@alpla.com");
return apiReturn(res, {
success: true,
level: "info",
module: "notification",
subModule: "post",
message: `Manual Trigger ran`,
data: manual ?? [],
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,114 @@
import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { notifications } from "../db/schema/notifications.schema.js";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { delay } from "../utils/delay.utils.js";
import { returnFunc } from "../utils/returnHelper.utils.js";
import { sendEmail } from "../utils/sendEmail.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { v2QueryRun } from "../utils/pgConnectToLst.utils.js";
let shutoffv1 = false
const func = async (data: any, emails: string) => {
// TODO: remove this disable once all 17 plants are on this new lst
if (!shutoffv1){
v2QueryRun(`update public.notifications set active = false where name = '${data.name}'`)
shutoffv1 = true
}
const { data: l, error: le } = (await tryCatch(
db.select().from(notifications).where(eq(notifications.id, data.id)),
)) as any;
if (le) {
return returnFunc({
success: false,
level: "error",
module: "notification",
subModule: "query",
message: `${data.name} encountered an error while trying to get initial info`,
data: le as any,
notify: true,
});
}
// search the query db for the query by name
const sqlQuery = sqlQuerySelector(`${data.name}`) as SqlQuery;
// create the ignore audit logs ids
// get get the latest blocking order id that was sent
const blockingOrderId = l[0].options[0].lastBlockingOrderIdSent ?? 69;
// run the check
const { data: queryRun, error } = await tryCatch(
prodQuery(
sqlQuery.query.replace("[lastBlocking]", blockingOrderId),
`Running notification query: ${l[0].name}`,
),
);
if (error) {
return returnFunc({
success: false,
level: "error",
module: "notification",
subModule: "query",
message: `Data for: ${l[0].name} encountered an error while trying to get it`,
data: error as any,
notify: true,
});
}
if (queryRun.data.length > 0) {
for (const bo of queryRun.data) {
const sentEmail = await sendEmail({
email: emails,
subject: bo.subject,
template: "qualityBlocking",
context: {
items: bo,
},
});
if (!sentEmail?.success) {
return returnFunc({
success: false,
level: "error",
module: "notification",
subModule: "email",
message: `${l[0].name} failed to send the email`,
data: sentEmail?.data as any,
notify: true,
});
}
await delay(1500);
const { error: dbe } = await tryCatch(
db
.update(notifications)
.set({ options: [{ lastBlockingOrderIdSent: bo.blockingNumber }] })
.where(eq(notifications.id, data.id)),
);
if (dbe) {
return returnFunc({
success: false,
level: "error",
module: "notification",
subModule: "query",
message: `Data for: ${l[0].name} encountered an error while trying to get it`,
data: dbe as any,
notify: true,
});
}
}
}
};
export default func;

View File

@@ -9,9 +9,16 @@ import {
import { returnFunc } from "../utils/returnHelper.utils.js"; import { returnFunc } from "../utils/returnHelper.utils.js";
import { sendEmail } from "../utils/sendEmail.utils.js"; import { sendEmail } from "../utils/sendEmail.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js"; import { tryCatch } from "../utils/trycatch.utils.js";
import { v2QueryRun } from "../utils/pgConnectToLst.utils.js";
let shutoffv1 = false
const func = async (data: any, emails: string) => {
// TODO: remove this disable once all 17 plants are on this new lst
if (!shutoffv1){
v2QueryRun(`update public.notifications set active = false where name = '${data.name}'`)
shutoffv1 = true
}
const reprint = async (data: any, emails: string) => {
// TODO: do the actual logic for the notification.
const { data: l, error: le } = (await tryCatch( const { data: l, error: le } = (await tryCatch(
db.select().from(notifications).where(eq(notifications.id, data.id)), db.select().from(notifications).where(eq(notifications.id, data.id)),
)) as any; )) as any;
@@ -23,7 +30,7 @@ const reprint = async (data: any, emails: string) => {
module: "notification", module: "notification",
subModule: "query", subModule: "query",
message: `${data.name} encountered an error while trying to get initial info`, message: `${data.name} encountered an error while trying to get initial info`,
data: [le], data: le as any,
notify: true, notify: true,
}); });
} }
@@ -52,7 +59,7 @@ const reprint = async (data: any, emails: string) => {
module: "notification", module: "notification",
subModule: "query", subModule: "query",
message: `Data for: ${l[0].name} encountered an error while trying to get it`, message: `Data for: ${l[0].name} encountered an error while trying to get it`,
data: [error], data: error as any,
notify: true, notify: true,
}); });
} }
@@ -73,7 +80,7 @@ const reprint = async (data: any, emails: string) => {
module: "notification", module: "notification",
subModule: "query", subModule: "query",
message: `Data for: ${l[0].name} encountered an error while trying to get it`, message: `Data for: ${l[0].name} encountered an error while trying to get it`,
data: [dbe], data: dbe as any,
notify: true, notify: true,
}); });
} }
@@ -90,26 +97,17 @@ const reprint = async (data: any, emails: string) => {
}); });
if (!sentEmail?.success) { if (!sentEmail?.success) {
// sendEmail({
// email: "Blake.matths@alpla.com",
// subject: `${os.hostname()} failed to run ${data[0]?.name}.`,
// template: "serverCrash",
// context: {
// error: sentEmail?.data,
// plant: `${os.hostname()}`,
// },
// });
return returnFunc({ return returnFunc({
success: false, success: false,
level: "error", level: "error",
module: "notification", module: "notification",
subModule: "email", subModule: "email",
message: `${l[0].name} failed to send the email`, message: `${l[0].name} failed to send the email`,
data: [sentEmail?.data], data: sentEmail?.data as any,
notify: true, notify: true,
}); });
} }
} }
}; };
export default reprint; export default func;

View File

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

View File

@@ -22,7 +22,7 @@ const note: NewNotification[] = [
"Checks for new blocking orders that have been entered, recommend to get the most recent order in here before activating.", "Checks for new blocking orders that have been entered, recommend to get the most recent order in here before activating.",
active: false, active: false,
interval: "10", interval: "10",
options: [{ sentBlockingOrders: [{ timeStamp: "0", blockingOrder: 1 }] }], options: [{ lastBlockingOrderIdSent: 1 }],
}, },
{ {
name: "alplaPurchaseHistory", name: "alplaPurchaseHistory",

View File

@@ -0,0 +1,44 @@
use [test1_AlplaPROD2.0_Read]
SELECT
'Alert! new blocking order: #' + cast(bo.HumanReadableId as varchar) + ' - ' + bo.ArticleVariantDescription as subject
,cast(bo.[HumanReadableId] as varchar) as blockingNumber
,bo.[ArticleVariantDescription] as article
,cast(bo.[CustomerHumanReadableId] as varchar) + ' - ' + bo.[CustomerDescription] as customer
,convert(varchar(10), bo.[BlockingDate], 101) + ' ' + convert(varchar(5), bo.[BlockingDate], 108) as blockingDate
,cast(ArticleVariantHumanReadableId as varchar) + ' - ' + ArticleVariantDescription as av
,case when bo.Remark = '' or bo.Remark is NULL then 'Please reach out to quality for the reason this was placed on hold as a remark was not entered during the blocking processs' else bo.Remark end as remark
,cast(FORMAT(TotalAmountOfPieces, '###,###') as varchar) + ' / ' + cast(LoadingUnit as varchar) as peicesAndLoadingUnits
,bo.ProductionLotHumanReadableId as lotNumber
,cast(osd.IdBlockingDefectsGroup as varchar) + ' - ' + osd.Description as mainDefectGroup
,cast(df.HumanReadableId as varchar) + ' - ' + os.Description as mainDefect
,lot.MachineLocation as line
--,*
FROM [blocking].[BlockingOrder] (nolock) as bo
/*** get the defect details ***/
join
[blocking].[BlockingDefect] (nolock) AS df
on df.id = bo.MainDefectId
/*** pull description from 1.0 ***/
left join
[AlplaPROD_test1].[dbo].[T_BlockingDefects] (nolock) as os
on os.IdGlobalBlockingDefect = df.HumanReadableId
/*** join in 1.0 defect group ***/
left join
[AlplaPROD_test1].[dbo].[T_BlockingDefectsGroups] (nolock) as osd
on osd.IdBlockingDefectsGroup = os.IdBlockingDefectsGroup
left join
[productionControlling].[ProducedLot] (nolock) as lot
on lot.id = bo.ProductionLotId
where
bo.[BlockingDate] between getdate() - 2 and getdate() + 3 and
bo.BlockingTrigger = 1 -- so we only get the ir blocking and not coa
--and HumanReadableId NOT IN ([sentBlockingOrders])
and bo.HumanReadableId > [lastBlocking]

View File

@@ -20,8 +20,8 @@ export const gpReqCheck = async (data: GpStatus[]) => {
module: "purchase", module: "purchase",
subModule: "query", subModule: "query",
message: `Error getting alpla purchase info`, message: `Error getting alpla purchase info`,
data: [gpReqCheck.message], data: gpReqCheck.message as any,
notify: false, notify: true,
}); });
} }
@@ -30,7 +30,7 @@ export const gpReqCheck = async (data: GpStatus[]) => {
const result = await gpQuery( const result = await gpQuery(
gpReqCheck.query.replace( gpReqCheck.query.replace(
"[reqsToCheck]", "[reqsToCheck]",
data.map((r) => `'${r.req}'`).join(", ") ?? "", data.map((r) => `'${r.req}'`).join(", ") ?? "xo",
), ),
"Get req info", "Get req info",
); );
@@ -55,7 +55,7 @@ export const gpReqCheck = async (data: GpStatus[]) => {
[Requisition Number] as req [Requisition Number] as req
,case when [Workflow Status] = 'recall' then 'returned' else [Workflow Status] end as approvedStatus ,case when [Workflow Status] = 'recall' then 'returned' else [Workflow Status] end as approvedStatus
--,* --,*
from [dbo].[PurchaseRequisitions] where [Requisition Number] in (${missing1Reqs.map((r) => `'${r}'`).join(", ")})`, from [dbo].[PurchaseRequisitions] where [Requisition Number] in (${missing1Reqs.map((r) => `'${r}'`).join(", ") ?? "xo"})`,
"validate req is not in recall", "validate req is not in recall",
); );
@@ -76,7 +76,7 @@ export const gpReqCheck = async (data: GpStatus[]) => {
,PONUMBER ,PONUMBER
,reqStatus='converted' ,reqStatus='converted'
,* ,*
from alpla.dbo.sop60100 (nolock) where sopnumbe in (${missing2Reqs.map((r) => `'${r}'`).join(", ")})`, from alpla.dbo.sop60100 (nolock) where sopnumbe in (${missing2Reqs.map((r) => `'${r}'`).join(", ") ?? "xo"})`,
"Get release info", "Get release info",
); );
@@ -111,7 +111,15 @@ export const gpReqCheck = async (data: GpStatus[]) => {
})); }));
return updateData; return updateData;
} catch (error) { } catch (error: any) {
log.error({ stack: error }); return returnFunc({
success: false,
level: "error",
module: "purchase",
subModule: "gpChecks",
message: error.message,
data: error.stack as any,
notify: true,
});
} }
}; };

View File

@@ -39,8 +39,8 @@ export const monitorAlplaPurchase = async () => {
module: "purchase", module: "purchase",
subModule: "query", subModule: "query",
message: `Error getting alpla purchase info`, message: `Error getting alpla purchase info`,
data: [sqlQuery.message], data: sqlQuery.message as any,
notify: false, notify: true,
}); });
} }
@@ -78,7 +78,7 @@ export const monitorAlplaPurchase = async () => {
if (error) { if (error) {
log.error( log.error(
{ error }, { error, notify: true },
"There was an error adding alpla purchase history", "There was an error adding alpla purchase history",
); );
} }
@@ -86,8 +86,10 @@ export const monitorAlplaPurchase = async () => {
await delay(500); await delay(500);
} }
} catch (e) { } catch (e) {
log.error({ error: e }, "Error occurred while running the monitor job"); log.error(
log.error({ error: e }, "Error occurred while running the monitor job"); { error: e, notify: true },
"Error occurred while running the monitor job",
);
return; return;
} }
@@ -104,7 +106,7 @@ export const monitorAlplaPurchase = async () => {
// if theres no reqs just end meow // if theres no reqs just end meow
if (errorReq) { if (errorReq) {
log.error( log.error(
{ stack: errorReq }, { stack: errorReq, notify: true },
"There was an error getting history data", "There was an error getting history data",
); );
return; return;

View File

@@ -15,6 +15,7 @@ import { monitorAlplaPurchase } from "./purchase/purchase.controller.js";
import { setupSocketIORoutes } from "./socket.io/serverSetup.js"; import { setupSocketIORoutes } from "./socket.io/serverSetup.js";
import { baseSettingValidationCheck } from "./system/settingsBase.controller.js"; import { baseSettingValidationCheck } from "./system/settingsBase.controller.js";
import { createCronJob } from "./utils/croner.utils.js"; import { createCronJob } from "./utils/croner.utils.js";
import { sendEmail } from "./utils/sendEmail.utils.js";
const port = Number(process.env.PORT) || 3000; const port = Number(process.env.PORT) || 3000;
export let systemSettings: Setting[] = []; export let systemSettings: Setting[] = [];
@@ -62,6 +63,23 @@ const start = async () => {
startNotifications(); startNotifications();
}, 5 * 1000); }, 5 * 1000);
process.on("uncaughtException", async (err) => {
console.error("Uncaught Exception:", err);
//await closePool();
const emailData = {
email: "blake.matthes@alpla.com", // should be moved to the db so it can be reused.
subject: `${os.hostname()} has just encountered a crash.`,
template: "serverCrash",
context: {
error: err,
plant: `${os.hostname()}`,
},
};
await sendEmail(emailData);
//process.exit(1);
});
server.listen(port, async () => { server.listen(port, async () => {
log.info( log.info(
`Listening on http://${os.hostname()}:${port}${baseUrl}, logging in ${process.env.LOG_LEVEL}, current ENV ${process.env.NODE_ENV ? process.env.NODE_ENV : "development"}`, `Listening on http://${os.hostname()}:${port}${baseUrl}, logging in ${process.env.LOG_LEVEL}, current ENV ${process.env.NODE_ENV ? process.env.NODE_ENV : "development"}`,

View File

@@ -0,0 +1,73 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{{!-- <link rel="stylesheet" href="styles/styles.css" /> --}}
<style>
.email-wrapper {
max-width: 80%; /* Limit width to 80% of the window */
margin: 0 auto; /* Center the content horizontally */
}
.email-table {
width: 100%;
border-collapse: collapse;
}
.email-table td {
vertical-align: top;
padding: 10px;
border: 1px solid #000;
border-radius: 25px; /* Rounded corners */
background-color: #f0f0f0; /* Optional: Add a background color */
}
.email-table h2 {
margin: 0;
}
.remarks {
border: 1px solid black;
padding: 10px;
background-color: #f0f0f0;
border-radius: 25px;
}
</style>
</head>
<body>
<div class="email-wrapper">
<p>All,</p>
<p>Please see the new blocking order that was created.</p>
<div>
<div class="email-table">
<table>
<tr>
<td>
<p><strong>Blocking number: </strong>{{items.blockingNumber}}</p>
<p><strong>Blocking Date: </strong>{{items.blockingDate}}</p>
<p><strong>Article: </strong>{{items.av}}</p>
<p><strong>Production Lot: </strong>{{items.lotNumber}}</p>
<p><strong>Line: </strong>{{items.line}}</p>
</td>
<td>
<p><strong>Customer: </strong>{{items.customer}}</p>
<p><strong>Blocked pieces /LUs: </strong>{{items.peicesAndLoadingUnits}}</p>
<p><strong>Main defect group: </strong>{{items.mainDefectGroup}}</p>
<p><strong>Main defect: </strong>{{items.mainDefect}}</p>
</td>
</tr>
</table>
</div>
</div>
<div class="remarks">
<h4>Remarks:</h4>
<p>{{items.remark}}</p>
</div>
</div>
<br>
<p>For further questions please reach out to quality.</p>
<p>Thank you,</p>
<p>Quality Department</p>
</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,35 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{{!--<title>Order Summary</title> --}}
{{> styles}}
<style>
pre {
background-color: #f8f9fa;
color: #d63384;
padding: 10px;
border-radius: 5px;
white-space: pre-wrap;
font-family: monospace;
}
</style>
{{!-- <link rel="stylesheet" href="styles/styles.css" /> --}}
</head>
<body>
<h3>{{plant}},<br/> Has encountered an unexpected error.</h1>
<p>
Please see below the stack error from the crash.
</p>
<hr/>
<div>
<h3>Error Message: </h3>
<p>{{error.message}}</p>
</div>
<hr/>
<div>
<h3>Stack trace</h3>
<pre>{{{error.stack}}}</pre>
</div>
</body>
</html>

View File

@@ -0,0 +1,36 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{{!--<title>Order Summary</title> --}}
{{> styles}}
<style>
pre {
background-color: #f8f9fa;
color: #d63384;
padding: 10px;
border-radius: 5px;
white-space: pre-wrap;
font-family: monospace;
}
</style>
{{!-- <link rel="stylesheet" href="styles/styles.css" /> --}}
</head>
<body>
<h3>{{plant}},<br/> Has encountered an error.</h1>
<p>
The below error came from Module: {{module}}, Submodule: {{submodule}}.
</p>
<p>The error below is considered to be critical and should be addressed</p>
<hr/>
<div>
<h3>Error Message: </h3>
<p>{{message}}</p>
</div>
<hr/>
<div>
<h3>Stack trace</h3>
<pre>{{{error}}}</pre>
</div>
</body>
</html>

View File

@@ -0,0 +1,41 @@
import pkg from "pg";
const { Pool } = pkg;
const baseConfig = {
host: process.env.DATABASE_HOST ?? "localhost",
port: parseInt(process.env.DATABASE_PORT ?? "5433", 10),
user: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
};
// Pools (one per DB)
const v1Pool = new Pool({
...baseConfig,
database: "lst",
});
const v2Pool = new Pool({
...baseConfig,
database: "lst_db",
});
// Query helpers
export const v1QueryRun = async (query: string, params?: any[]) => {
try {
const res = await v1Pool.query(query, params);
return res;
} catch (err) {
console.error("V1 query error:", err);
throw err;
}
};
export const v2QueryRun = async (query: string, params?: any[]) => {
try {
const res = await v2Pool.query(query, params);
return res;
} catch (err) {
console.error("V2 query error:", err);
throw err;
}
};

View File

@@ -33,7 +33,8 @@ interface Data<T = unknown[]> {
| "delete" | "delete"
| "printing" | "printing"
| "gpSql" | "gpSql"
| "email"; | "email"
| "gpChecks";
level: "info" | "error" | "debug" | "fatal"; level: "info" | "error" | "debug" | "fatal";
message: string; message: string;
room?: string; room?: string;

View File

@@ -1,5 +1,5 @@
vars { vars {
url: http://localhost:3600/lst url: http://localhost:3000/lst
readerIp: 10.44.14.215 readerIp: 10.44.14.215
} }
vars:secret [ vars:secret [

View File

@@ -14,7 +14,7 @@ body:json {
{ {
"userId":"m6AbQXFwOXoX3YKLfwWgq2LIdDqS5jqv", "userId":"m6AbQXFwOXoX3YKLfwWgq2LIdDqS5jqv",
"notificationId": "0399eb2a-39df-48b7-9f1c-d233cec94d2e", "notificationId": "0399eb2a-39df-48b7-9f1c-d233cec94d2e",
"emails": ["blake.mattes@alpla.com","cowchmonkey@gmail.com"] "emails": ["blake.matthes@alpla.com","blake.matthes@alpla.com"]
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -26,6 +26,8 @@
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"shadcn": "^4.0.8", "shadcn": "^4.0.8",
"socket.io-client": "^4.8.3", "socket.io-client": "^4.8.3",
"sonner": "^2.0.7", "sonner": "^2.0.7",
@@ -36,6 +38,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.36.0", "@eslint/js": "^9.36.0",
"@tailwindcss/typography": "^0.5.19",
"@tanstack/router-plugin": "^1.166.7", "@tanstack/router-plugin": "^1.166.7",
"@types/react": "^19.1.13", "@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9", "@types/react-dom": "^19.1.9",

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1,105 @@
import { Link, useRouterState } from "@tanstack/react-router";
import { ChevronRight } from "lucide-react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "../ui/collapsible";
import {
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
useSidebar,
} from "../ui/sidebar";
const docs = [
{
title: "Notifications",
url: "/intro",
//icon,
isActive: window.location.pathname.includes("notifications") ?? false,
items: [
{
title: "Reprints",
url: "/reprints",
},
{
title: "New Blocking order",
url: "/qualityBlocking",
},
],
},
];
export default function DocBar() {
const { setOpen } = useSidebar();
const pathname = useRouterState({
select: (s) => s.location.pathname,
});
const isNotifications = pathname.includes("notifications");
return (
<SidebarGroup>
<SidebarGroupLabel>Docs</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem key={"docs"}>
<SidebarMenuButton asChild>
<Link to={"/docs"} onClick={() => setOpen(false)}>
{/* <item.icon /> */}
<span>{"Intro"}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
<SidebarMenu>
{docs.map((item) => (
<Collapsible
key={item.title}
asChild
defaultOpen={isNotifications}
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={item.title}>
<Link
to={"/docs/$"}
params={{ _splat: `notifications${item.url}` }}
>
{item.title}
</Link>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild>
<Link
to={"/docs/$"}
params={{ _splat: `notifications${subItem.url}` }}
>
{subItem.title}
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
);
}

View File

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

View File

@@ -0,0 +1,76 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
)
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
className
)}
{...props}
/>
)
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
className
)}
{...props}
/>
)
}
function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-action"
className={cn("absolute top-2 right-2", className)}
{...props}
/>
)
}
export { Alert, AlertTitle, AlertDescription, AlertAction }

View File

@@ -0,0 +1,31 @@
import { Collapsible as CollapsiblePrimitive } from "radix-ui"
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
}
function CollapsibleTrigger({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
)
}
function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
)
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

View File

@@ -0,0 +1,62 @@
export default function into() {
return (
<div className="mx-auto w-full max-w-4xl px-6 py-8">
<h1 className="text-3xl underline p-2">Notifications</h1>
<p className="p-2">
All notifications are a subscription based, please open the menu and
select the notification you would like to know more info about
</p>
<hr />
<p>To subscribe to a notification</p>
<ol className="list-decimal list-inside">
<li>Click on your profile</li>
<img
src="/lst/app/imgs/docs/notifications/lt_profile.png"
alt="Reprint notification example"
className="m-2 rounded-lg border-2"
/>
<li>Click account</li>
<li>Select the notification you would like to subscribe to.</li>
<img
src="/lst/app/imgs/docs/notifications/lt_notification_select.png"
alt="Reprint notification example"
className="m-2 rounded-lg border-2"
/>
<li>
If you want to have more people on the notification you can add more
emails by clicking the add email button.{" "}
<p className="text-sm underline">
Please note that each user can subscribe on there own so you do not
need to add others unless you want to add them.
</p>
</li>
<li>When you are ready click subscribe</li>
</ol>
<br />
<p className="">
NOTE: you can select the same notification and add more people or just
your self only, when you do this it will override you current
subscription and add / remove the emails
</p>
<hr className="m-2" />
<div>
<p>
The table at the bottom of your profile is where all of your current
subscriptions will be at.
</p>
<p>
Clicking the trash can will remove the notifications from sending you
emails
</p>
<img
src="/lst/app/imgs/docs/notifications/lt_notification_table.png"
alt="Reprint notification example"
className="m-2 rounded-lg border-2"
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,19 @@
export default function reprints() {
return (
<div className="mx-auto w-full max-w-4xl px-6 py-8">
<h1 className="text-3xl underline p-2">Quality Blocking</h1>
<p className="p-2">
When a new blocking order is created a new alert will be sent out to all
users subscribed. if there are multiple blocking orders created between
checks you can expect to get multiple emails. below you will see an
example of a blocking email that is sent out
</p>
<img
src="/lst/app/imgs/docs/notifications/lt_qualityBlocking.png"
alt="Reprint notification example"
className="m-2 rounded-lg border-2"
/>
</div>
);
}

View File

@@ -0,0 +1,18 @@
export default function reprints() {
return (
<div className="mx-auto w-full max-w-4xl px-6 py-8">
<h1 className="text-3xl underline p-2">Reprints</h1>
<p className="p-2">
The reprint alert will monitor for labels that have been printed within
a defined time. when a label is printed in the defined time an email
will sent out that looks similar to the below
</p>
<img
src="/lst/app/imgs/docs/notifications/lt_reprints.png"
alt="Reprint notification example"
className="m-2 rounded-lg border-2"
/>
</div>
);
}

26
frontend/src/lib/docs.ts Normal file
View File

@@ -0,0 +1,26 @@
import type { ComponentType } from "react";
const modules = import.meta.glob("../docs/**/*.tsx", {
eager: true,
});
type DocModule = {
default: ComponentType;
};
const docsMap: Record<string, ComponentType> = {};
for (const path in modules) {
const mod = modules[path] as DocModule;
const slug = path
.replace("../docs/", "")
.replace(".tsx", "");
// "notifications/intro"
docsMap[slug] = mod.default;
}
export function getDoc(slug: string) {
return docsMap[slug];
}

View File

@@ -11,6 +11,8 @@
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as AboutRouteImport } from './routes/about' import { Route as AboutRouteImport } from './routes/about'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as DocsIndexRouteImport } from './routes/docs/index'
import { Route as DocsSplatRouteImport } from './routes/docs/$'
import { Route as AdminSettingsRouteImport } from './routes/admin/settings' import { Route as AdminSettingsRouteImport } from './routes/admin/settings'
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'
@@ -29,6 +31,16 @@ const IndexRoute = IndexRouteImport.update({
path: '/', path: '/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const DocsIndexRoute = DocsIndexRouteImport.update({
id: '/docs/',
path: '/docs/',
getParentRoute: () => rootRouteImport,
} as any)
const DocsSplatRoute = DocsSplatRouteImport.update({
id: '/docs/$',
path: '/docs/$',
getParentRoute: () => rootRouteImport,
} as any)
const AdminSettingsRoute = AdminSettingsRouteImport.update({ const AdminSettingsRoute = AdminSettingsRouteImport.update({
id: '/admin/settings', id: '/admin/settings',
path: '/admin/settings', path: '/admin/settings',
@@ -72,6 +84,8 @@ export interface FileRoutesByFullPath {
'/admin/logs': typeof AdminLogsRoute '/admin/logs': typeof AdminLogsRoute
'/admin/notifications': typeof AdminNotificationsRoute '/admin/notifications': typeof AdminNotificationsRoute
'/admin/settings': typeof AdminSettingsRoute '/admin/settings': typeof AdminSettingsRoute
'/docs/$': typeof DocsSplatRoute
'/docs/': typeof DocsIndexRoute
'/user/profile': typeof authUserProfileRoute '/user/profile': typeof authUserProfileRoute
'/user/resetpassword': typeof authUserResetpasswordRoute '/user/resetpassword': typeof authUserResetpasswordRoute
'/user/signup': typeof authUserSignupRoute '/user/signup': typeof authUserSignupRoute
@@ -83,6 +97,8 @@ export interface FileRoutesByTo {
'/admin/logs': typeof AdminLogsRoute '/admin/logs': typeof AdminLogsRoute
'/admin/notifications': typeof AdminNotificationsRoute '/admin/notifications': typeof AdminNotificationsRoute
'/admin/settings': typeof AdminSettingsRoute '/admin/settings': typeof AdminSettingsRoute
'/docs/$': typeof DocsSplatRoute
'/docs': typeof DocsIndexRoute
'/user/profile': typeof authUserProfileRoute '/user/profile': typeof authUserProfileRoute
'/user/resetpassword': typeof authUserResetpasswordRoute '/user/resetpassword': typeof authUserResetpasswordRoute
'/user/signup': typeof authUserSignupRoute '/user/signup': typeof authUserSignupRoute
@@ -95,6 +111,8 @@ export interface FileRoutesById {
'/admin/logs': typeof AdminLogsRoute '/admin/logs': typeof AdminLogsRoute
'/admin/notifications': typeof AdminNotificationsRoute '/admin/notifications': typeof AdminNotificationsRoute
'/admin/settings': typeof AdminSettingsRoute '/admin/settings': typeof AdminSettingsRoute
'/docs/$': typeof DocsSplatRoute
'/docs/': typeof DocsIndexRoute
'/(auth)/user/profile': typeof authUserProfileRoute '/(auth)/user/profile': typeof authUserProfileRoute
'/(auth)/user/resetpassword': typeof authUserResetpasswordRoute '/(auth)/user/resetpassword': typeof authUserResetpasswordRoute
'/(auth)/user/signup': typeof authUserSignupRoute '/(auth)/user/signup': typeof authUserSignupRoute
@@ -108,6 +126,8 @@ export interface FileRouteTypes {
| '/admin/logs' | '/admin/logs'
| '/admin/notifications' | '/admin/notifications'
| '/admin/settings' | '/admin/settings'
| '/docs/$'
| '/docs/'
| '/user/profile' | '/user/profile'
| '/user/resetpassword' | '/user/resetpassword'
| '/user/signup' | '/user/signup'
@@ -119,6 +139,8 @@ export interface FileRouteTypes {
| '/admin/logs' | '/admin/logs'
| '/admin/notifications' | '/admin/notifications'
| '/admin/settings' | '/admin/settings'
| '/docs/$'
| '/docs'
| '/user/profile' | '/user/profile'
| '/user/resetpassword' | '/user/resetpassword'
| '/user/signup' | '/user/signup'
@@ -130,6 +152,8 @@ export interface FileRouteTypes {
| '/admin/logs' | '/admin/logs'
| '/admin/notifications' | '/admin/notifications'
| '/admin/settings' | '/admin/settings'
| '/docs/$'
| '/docs/'
| '/(auth)/user/profile' | '/(auth)/user/profile'
| '/(auth)/user/resetpassword' | '/(auth)/user/resetpassword'
| '/(auth)/user/signup' | '/(auth)/user/signup'
@@ -142,6 +166,8 @@ export interface RootRouteChildren {
AdminLogsRoute: typeof AdminLogsRoute AdminLogsRoute: typeof AdminLogsRoute
AdminNotificationsRoute: typeof AdminNotificationsRoute AdminNotificationsRoute: typeof AdminNotificationsRoute
AdminSettingsRoute: typeof AdminSettingsRoute AdminSettingsRoute: typeof AdminSettingsRoute
DocsSplatRoute: typeof DocsSplatRoute
DocsIndexRoute: typeof DocsIndexRoute
authUserProfileRoute: typeof authUserProfileRoute authUserProfileRoute: typeof authUserProfileRoute
authUserResetpasswordRoute: typeof authUserResetpasswordRoute authUserResetpasswordRoute: typeof authUserResetpasswordRoute
authUserSignupRoute: typeof authUserSignupRoute authUserSignupRoute: typeof authUserSignupRoute
@@ -163,6 +189,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/docs/': {
id: '/docs/'
path: '/docs'
fullPath: '/docs/'
preLoaderRoute: typeof DocsIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/docs/$': {
id: '/docs/$'
path: '/docs/$'
fullPath: '/docs/$'
preLoaderRoute: typeof DocsSplatRouteImport
parentRoute: typeof rootRouteImport
}
'/admin/settings': { '/admin/settings': {
id: '/admin/settings' id: '/admin/settings'
path: '/admin/settings' path: '/admin/settings'
@@ -222,6 +262,8 @@ const rootRouteChildren: RootRouteChildren = {
AdminLogsRoute: AdminLogsRoute, AdminLogsRoute: AdminLogsRoute,
AdminNotificationsRoute: AdminNotificationsRoute, AdminNotificationsRoute: AdminNotificationsRoute,
AdminSettingsRoute: AdminSettingsRoute, AdminSettingsRoute: AdminSettingsRoute,
DocsSplatRoute: DocsSplatRoute,
DocsIndexRoute: DocsIndexRoute,
authUserProfileRoute: authUserProfileRoute, authUserProfileRoute: authUserProfileRoute,
authUserResetpasswordRoute: authUserResetpasswordRoute, authUserResetpasswordRoute: authUserResetpasswordRoute,
authUserSignupRoute: authUserSignupRoute, authUserSignupRoute: authUserSignupRoute,

View File

@@ -0,0 +1,31 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { getDoc } from "../../lib/docs";
export const Route = createFileRoute("/docs/$")({
component: RouteComponent,
});
function RouteComponent() {
const { _splat } = Route.useParams();
const slug = _splat || "";
const Doc = getDoc(slug);
if (!Doc) {
return (
<div>
<p>
You Have reached a doc page that dose not seem to exist please
validate and come back
</p>
<Link to="/docs">Docs Home</Link>
</div>
);
}
return (
<div className="mx-auto w-full max-w-4xl px-6 py-8">
<Doc />
</div>
);
}

View File

@@ -0,0 +1,100 @@
import { createFileRoute, Link } from "@tanstack/react-router";
export const Route = createFileRoute("/docs/")({
component: RouteComponent,
});
function RouteComponent() {
return (
<div className="mx-auto w-full max-w-4xl px-6 py-8">
<h1 className="text-3xl underline p-2">Logistics Support Tool Intro</h1>
<h2 className="text-2xl shadow-2xl p-2">What is lst</h2>
<p className="p-2">
Lst is a logistics support tool, and aid to ALPLAprod All data in here
is just to be treated as an aid and can still be completed manually in
alplaprod. These docs are here to help show what LST has to offer as
well as the manual process via alpla prod.
</p>
<hr />
<h2 className="text-2xl shadow-2xl p-2">What dose LST offer</h2>
<ul className="list-disc list-inside">
<li>One click print</li>
<ul className="list-disc list-inside indent-8">
<li>Controls printing of labels</li>
<li>devices that can be used</li>
<ul className="list-disc list-inside indent-16">
<li>Printer control</li>
<li>plc control</li>
<li>ame palletizer control</li>
</ul>
<li>considers more business logic than alplaprod</li>
<ul className="list-disc list-inside indent-16">
<li>
enough material is needed in the system to create the next pallet
</li>
<li>this will be the same for packaging as well.</li>
</ul>
<li>special processes</li>
<ul className="list-disc list-inside indent-16">
<li>in-house delivery triggered once booked in</li>
<li>stop gap on printing labels at specific times</li>
<li>per line delay in printing</li>
</ul>
</ul>
<li>Silos Management</li>
<ul className="list-disc list-inside indent-8">
<li>Silo adjustments per location</li>
<ul className="list-disc list-inside indent-16">
<li>Charts for the last 10 adjustments</li>
<li>Historical data</li>
<li>Comments on per adjustment</li>
<li>Automatic email for more than 5% deviation</li>
</ul>
<li>Attach silo</li>
<ul className="list-disc list-inside indent-16">
<li>Only shows machines not attached to this silo</li>
</ul>
<li>Detach silo</li>
<ul className="list-disc list-inside indent-16">
Only shows machines that are attached to the silo.
</ul>
</ul>
<li>TMS integration</li>
<ul className="list-disc list-inside indent-8">
<li>integration with TI to auto add in orders</li>
<ul className="list-disc list-inside indent-16">
<li>orders are based on a time defined per plant.</li>
<li>carriers can be auto set.</li>
</ul>
</ul>
<li>
<Link
to={"/docs/$"}
params={{ _splat: "notifications/intro" }}
className="underline"
>
Notifications
</Link>
</li>
<ul className="list-disc list-inside indent-8">
<li>Automated alerts</li>
<li>Subscription based</li>
<li>Processes notifications</li>
</ul>
<li>Datamart</li>
<ul className="list-disc list-inside indent-8">
<li>queries that can be pulled via excel</li>
<li>queries are created to allow better views for the plants</li>
<li>Faster customer reports</li>
</ul>
<li>Fake EDI (Demand Management)</li>
<ul className="list-disc list-inside indent-8">
<li>Orders in (standard template)</li>
<li>Customer specific orders templates per plant</li>
<li>Forecast (standard Template)</li>
<li>Customer specific forecast per plant</li>
</ul>
</ul>
</div>
);
}

View File

@@ -4,6 +4,7 @@ import { tanstackRouter } from "@tanstack/router-plugin/vite";
import react from "@vitejs/plugin-react-swc"; import react from "@vitejs/plugin-react-swc";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [

View File

@@ -27,21 +27,15 @@ $Servers = @(
token = "usday1" token = "usday1"
loc = "D$\LST_V3" loc = "D$\LST_V3"
}, },
[PSCustomObject]@{
server = "usmcd1vms036"
token = "test1"
loc = "E$\LST_V3"
},
[PSCustomObject]@{
server = "usiow1vms036"
token = "test2"
loc = "E$\LST_V3"
}
,
[PSCustomObject]@{ [PSCustomObject]@{
server = "usweb1vms006" server = "usweb1vms006"
token = "usweb1" token = "usweb1"
loc = "D$\LST_V3" loc = "D$\LST_V3"
},
[PSCustomObject]@{
server = "usjci1vms006"
token = "usjci1"
loc = "D$\LST_V3"
} }
#@{ server = "usbet1vms006"; token = "usbet1";loc = "C$\Users\adm_matthes01\Desktop\lst_backend"; } #@{ server = "usbet1vms006"; token = "usbet1";loc = "C$\Users\adm_matthes01\Desktop\lst_backend"; }
#@{ server = "usbow1vms006"; token = "usbow1"; loc = "C$\Users\adm_matthes01\Desktop\lst_backend" ; } #@{ server = "usbow1vms006"; token = "usbow1"; loc = "C$\Users\adm_matthes01\Desktop\lst_backend" ; }
@@ -86,9 +80,10 @@ function Show-Menu {
Write-Host "===============================" Write-Host "==============================="
Write-Host "1. Build app" Write-Host "1. Build app"
Write-Host "2. Deploy New Release" Write-Host "2. Deploy New Release"
Write-Host "3. Upgrade Node" Write-Host "3. Deploy Test Servers"
Write-Host "4. Update Postgres" Write-Host "4. Upgrade Node"
Write-Host "5. Exit" Write-Host "5. Update Postgres"
Write-Host "6. Exit"
Write-Host "" Write-Host ""
} }
@@ -345,7 +340,7 @@ function Update-Server {
Start-Sleep -Seconds 3 Start-Sleep -Seconds 3
Write-Host "Install/update completed." Write-Host "Install/update completed."
# do the migrations # do the migrations
Push-Location $LocalPath # Push-Location $LocalPath
Write-Host "Running migrations" Write-Host "Running migrations"
npm run dev:db:migrate npm run dev:db:migrate
Start-Sleep -Seconds 3 Start-Sleep -Seconds 3
@@ -406,6 +401,45 @@ do {
} }
} }
"3" { "3" {
$TestServers = @(
[PSCustomObject]@{
server = "usmcd1vms036"
token = "test1"
loc = "E$\LST_V3"
},
[PSCustomObject]@{
server = "usiow1vms036"
token = "test2"
loc = "E$\LST_V3"
}
)
$testServer = Select-Server -List $TestServers
if($testServer -eq "all") {
Write-Host "Updating all servers"
for ($i = 0; $i -lt $TestServers.Count; $i++) {
Write-Host "Updating $($TestServers[$i].server)"
Update-Server -Server $TestServers[$i].server -Destination $TestServers[$i].loc -Token $TestServers[$i].token
Start-Sleep -Seconds 1
}
Read-Host -Prompt "Press Enter to continue..."
}
if ($testServer -ne "all") {
Write-Host "You selected $($testServer.server)"
# do the update to the server.
# copy to the server
Update-Server -Server $testServer.server -Destination $testServer.loc -Token $testServer.token
# stop service
# extract zip
# run update check
# run migration
# start service backup
Read-Host -Prompt "Press Enter to continue..."
}
}
"4" {
Write-Host "Choose Server to upgrade node on" Write-Host "Choose Server to upgrade node on"
$server = Select-Server -List $Servers $server = Select-Server -List $Servers
@@ -430,7 +464,7 @@ do {
Read-Host -Prompt "Press Enter to continue..." Read-Host -Prompt "Press Enter to continue..."
} }
} }
"4" { "5" {
Write-Host "Choose Server to upgrade postgres on" Write-Host "Choose Server to upgrade postgres on"
$server = Select-Server -List $Servers $server = Select-Server -List $Servers
@@ -456,7 +490,7 @@ do {
} }
} }
"5" { "6" {
Write-Host "Exiting..." Write-Host "Exiting..."
exit exit
} }