11 Commits

Author SHA1 Message Date
82eaa23da7 chore(release): version packages
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m57s
2026-04-03 11:18:25 -05:00
b18d1ced6d build(`build): added a personal sop to the setup until we move it
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m54s
2026-04-03 11:17:09 -05:00
69c5cf87fd fix(docker): fixes to allow an external url more easy
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 12s
when running in docker we might be using a different url thats not predefined in the cors so we want
to allow 1 more
2026-04-03 10:49:57 -05:00
1fadf0ad25 testing the docker runner
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m28s
2026-04-03 10:15:18 -05:00
beae6eb648 lots of changes with docker
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m57s
2026-04-03 09:51:52 -05:00
82ab735982 add gitea docker workflow
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-04-03 09:51:02 -05:00
dbd56c1b50 helper command set to correct drive now 2026-03-27 18:31:16 -05:00
037a473ab7 added dayton in 2026-03-27 18:31:02 -05:00
32998d417f table and query work 2026-03-27 18:30:50 -05:00
ddcb7e76a3 fixed imports on several files 2026-03-25 06:56:19 -05:00
191cb2b698 changed limas folder after migration 2026-03-25 06:56:01 -05:00
90 changed files with 4426 additions and 190 deletions

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json",
"$schema": "https://unpkg.com/@changesets/config/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],

View File

@@ -0,0 +1,5 @@
---
"lst_v3": patch
---
build stuff

11
.changeset/pre.json Normal file
View File

@@ -0,0 +1,11 @@
{
"mode": "pre",
"tag": "alpha",
"initialVersions": {
"lst_v3": "1.0.1"
},
"changesets": [
"neat-years-unite",
"soft-onions-appear"
]
}

View File

@@ -0,0 +1,5 @@
---
"lst_v3": patch
---
external url added for docker

View File

@@ -6,4 +6,7 @@ Dockerfile
docker-compose.yml
npm-debug.log
builds
testFiles
testFiles
nssm.exe
postgresql-17.9-2-windows-x64.exe
VSCodeUserSetup-x64-1.112.0.msi

View File

@@ -0,0 +1,31 @@
name: Build and Push LST Docker Image
on:
push:
branches:
- main
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout (local)
run: |
git clone https://git.tuffraid.net/cowch/lst_v3.git .
git checkout ${{ gitea.sha }}
- name: Login to registry
run: echo "${{ secrets.PASSWORD }}" | docker login git.tuffraid.net -u "cowch" --password-stdin
- name: Build image
run: |
docker build \
-t git.tuffraid.net/cowch/lst_v3:latest \
-t git.tuffraid.net/cowch/lst_v3:${{ gitea.sha }} \
.
- name: Push
run: |
docker push git.tuffraid.net/cowch/lst_v3:latest
docker push git.tuffraid.net/cowch/lst_v3:${{ gitea.sha }}

4
.gitignore vendored
View File

@@ -6,6 +6,10 @@ builds
temp
.scriptCreds
node-v24.14.0-x64.msi
postgresql-17.9-2-windows-x64.exe
VSCodeUserSetup-x64-1.112.0.exe
nssm.exe
# Logs
logs
*.log

View File

@@ -3,6 +3,8 @@
"workbench.colorTheme": "Default Dark+",
"terminal.integrated.env.windows": {},
"editor.formatOnSave": true,
"typescript.preferences.importModuleSpecifier": "relative",
"javascript.preferences.importModuleSpecifier": "relative",
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
@@ -65,6 +67,7 @@
"preseed",
"prodlabels",
"prolink",
"Skelly",
"trycatch"
],
"gitea.token": "8456def90e1c651a761a8711763d6ef225d6b2db",

View File

@@ -1,5 +1,12 @@
# lst_v3
## 1.0.2-alpha.0
### Patch Changes
- build stuff
- external url added for docker
## 1.0.1
### Patch Changes

View File

@@ -9,10 +9,13 @@ WORKDIR /app
# Copy package files
COPY . .
# Install production dependencies only
# build backend
RUN npm ci
RUN npm run build:docker
RUN npm run build
# build frontend
RUN npm --prefix frontend ci
RUN npm --prefix frontend run build
###########
# Stage 2 #
@@ -33,6 +36,9 @@ RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
COPY --from=build /app/frontend/dist ./frontend/dist
# TODO add in drizzle migrates
ENV RUNNING_IN_DOCKER=true
EXPOSE 3000

View File

@@ -0,0 +1,6 @@
import { integer, pgTable, text } from "drizzle-orm/pg-core";
export const opendockApt = pgTable("printer_log", {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
name: text("name").notNull(),
});

View File

@@ -1,10 +1,10 @@
import { notificationSub } from "db/schema/notifications.sub.schema.js";
import { eq } from "drizzle-orm";
import { createLogger } from "logger/logger.controller.js";
import { minutesToCron } from "utils/croner.minConvert.js";
import { createCronJob, stopCronJob } from "utils/croner.utils.js";
import { db } from "../db/db.controller.js";
import { notifications } from "../db/schema/notifications.schema.js";
import { notificationSub } from "../db/schema/notifications.sub.schema.js";
import { createLogger } from "../logger/logger.controller.js";
import { minutesToCron } from "../utils/croner.minConvert.js";
import { createCronJob, stopCronJob } from "../utils/croner.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const log = createLogger({ module: "notifications", subModule: "start" });

View File

@@ -1,6 +1,10 @@
const reprint = (data: any, emails: string) => {
// TODO: do the actual logic for the notification.
console.log(data);
console.log(emails);
// TODO send the error to systemAdmin users so they do not always need to be on the notifications.
// these errors are defined per notification.
};
export default reprint;

View File

@@ -1,8 +1,9 @@
import { notifications } from "db/schema/notifications.schema.js";
import { eq } from "drizzle-orm";
import { type Response, Router } from "express";
import { auth } from "utils/auth.utils.js";
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";

View File

@@ -1,8 +1,8 @@
import { notifications } from "db/schema/notifications.schema.js";
import { eq } from "drizzle-orm";
import { type Response, Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import { notifications } from "../db/schema/notifications.schema.js";
import { requirePermission } from "../middleware/auth.requiredPerms.middleware.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";

View File

@@ -1,23 +1,16 @@
import { notificationSub } from "db/schema/notifications.sub.schema.js";
import { and, eq } from "drizzle-orm";
import { type Response, Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import { notificationSub } from "../db/schema/notifications.sub.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { modifiedNotification } from "./notification.controller.js";
const newSubscribe = z.object({
emails: z
.email()
.array()
.describe("An array of emails"),
emails: z.email().array().describe("An array of emails"),
userId: z.string().describe("User id."),
notificationId: z
.string()
.describe("Notification id"),
notificationId: z.string().describe("Notification id"),
});
const r = Router();

View File

@@ -1,14 +1,16 @@
import { notificationSub } from "db/schema/notifications.sub.schema.js";
import { eq } from "drizzle-orm";
import { type Response, Router } from "express";
import { auth } from "utils/auth.utils.js";
import { db } from "../db/db.controller.js";
import { notificationSub } from "../db/schema/notifications.sub.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.get("/", async (req, res: Response) => {
const { userId } = req.query;
const hasPermissions = await auth.api.userHasPermission({
body: {
//userId: req?.user?.id,
@@ -24,7 +26,7 @@ r.get("/", async (req, res: Response) => {
.select()
.from(notificationSub)
.where(
!hasPermissions.success
userId || !hasPermissions.success
? eq(notificationSub.userId, `${req?.user?.id ?? ""}`)
: undefined,
),
@@ -47,7 +49,7 @@ r.get("/", async (req, res: Response) => {
level: "info",
module: "notification",
subModule: "post",
message: `Subscription deleted`,
message: `Subscriptions`,
data: data ?? [],
status: 200,
});

View File

@@ -1,7 +1,7 @@
import { notificationSub } from "db/schema/notifications.sub.schema.js";
import { type Response, Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import { notificationSub } from "../db/schema/notifications.sub.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { modifiedNotification } from "./notification.controller.js";

View File

@@ -1,8 +1,8 @@
import { notificationSub } from "db/schema/notifications.sub.schema.js";
import { and, eq } from "drizzle-orm";
import { type Response, Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import { notificationSub } from "../db/schema/notifications.sub.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { modifiedNotification } from "./notification.controller.js";

View File

@@ -1,11 +1,11 @@
import { db } from "db/db.controller.js";
import { sql } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import {
type NewNotification,
notifications,
} from "db/schema/notifications.schema.js";
import { sql } from "drizzle-orm";
import { tryCatch } from "utils/trycatch.utils.js";
} from "../db/schema/notifications.schema.js";
import { createLogger } from "../logger/logger.controller.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const note: NewNotification[] = [
{

View File

@@ -0,0 +1,36 @@
/**
* the route that listens for the printers post.
*
* and http-post alert should be setup on each printer pointing to at min you will want to make the alert for
* pause printer, you can have all on here as it will also monitor and do things on all messages
*
* http://{serverIP}:2222/lst/api/ocp/printer/listener/{printerName}
*
* the messages will be sent over to the db for logging as well as specific ones will do something
*
* pause will validate if can print
* close head will repause the printer so it wont print a label
* power up will just repause the printer so it wont print a label
*/
import { Router } from "express";
import { apiReturn } from "../utils/returnHelper.utils.js";
const r = Router();
r.post("/printer/listener/:printer", async (req, res) => {
const { printer: printerName } = req.params;
console.log(req.body);
return apiReturn(res, {
success: true,
level: "info",
module: "ocp",
subModule: "printing",
message: `${printerName} just passed over a message`,
data: req.body ?? [],
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,19 @@
/**
* this will do a prod sync, update or add alerts to the printer, validate the next pm intervale as well as head replacement.
*
* if a printer is upcoming on a pm or head replacement send to the plant to address.
*
* a trigger on the printer table will have the ability to run this as well
*
* heat beats on all assigned printers
*
* printer status will live here this will be how we manage all the levels of status like 3 paused, 1 printing, 8 error, 10 power up, etc...
*/
export const printerManager = async () => {};
export const printerHeartBeat = async () => {
// heat heats will be defaulted to 60 seconds no reason to allow anything else
};
//export const printerStatus = async (statusNr: number, printerId: number) => {};

22
backend/ocp/ocp.routes.ts Normal file
View File

@@ -0,0 +1,22 @@
import { type Express, Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { featureCheck } from "../middleware/featureActive.middleware.js";
import listener from "./ocp.printer.listener.js";
export const setupOCPRoutes = (baseUrl: string, app: Express) => {
//setup all the routes
const router = Router();
// is the feature even on?
router.use(featureCheck("ocp"));
// non auth routes up here
router.use(listener);
// auth routes below here
router.use(requireAuth);
//router.use("");
app.use(`${baseUrl}/api/ocp`, router);
};

View File

@@ -0,0 +1,188 @@
{
"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
}
],
"endpointConfig": {
"data": {
"event": {
"connections": [
{
"additionalOptions": {
"batching": {
"maxPayloadSizePerReport": 256000,
"reportingInterval": 2000
},
"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": {
"privateKeyFileLocation": "/readerconfig/ssl/server.key",
"publicKeyFileLocation": "/readerconfig/ssl/server.crt"
},
"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"
],
"userDefined": null,
"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
}
]
},
"xml": "<?xml version='1.0'?>\n<Motorola xmlns:Falcon='http://www.motorola.com/RFID/Readers/Config/Falcon' xmlns='http://www.motorola.com/RFID/Readers/Config/Falcon'>\n<Config>\n<AppVersion major='3' minor='28' build='1' maintenance='0'/>\n<CommConfig EnabledStacks='IPV4' DisableRAPktProcessing='1' EnableDHCPv6='1' IPv6StaticIPAddr='fe80::1' IPv6SubnetMask='64' IPv6StaticGateway='::' IPv6DNSIP='fe80::20' DHCP='1' IPAddr='10.44.14.39' Mask='255.255.255.0' Gateway='10.44.14.252' DNS='10.44.9.250' DomainSearch='example.com' HttpRunning='2' TelnetActive='2' FtpActive='2' usbMode='0' WatchdogEnabled='1' AvahiEnabled='1' NetBIOSEnabled='0' RDMPAgentEnabled='1' SerialConTimeout='0' SNTP='0.0.0.0' SNTPHostName='pool.ntp.org' sntpHostDisplayMode='0' llrpClientMode='0' llrpSecureMode='0' llrpSecureModeValidatePeer='0' llrpPort='5084' llrpHostIP='192.168.127.2' allowllrpConnOverride='0' shouldReconnect='1'/>\n<Bluetooth discoverable='0' pairable='0' PincodeEnabled='0' passkey='165CB22DA5BE7BBEFB77709DD0A94B03FB77709DD0A94B03FB77709DD0A94B03FB77709DD0A94B03FB77709DD0A94B03FB77709DD0A94B03FB77709DD0A94B03' startIP='192.168.0.2' endIP='192.168.0.3'/>\n<WirelessConfig essid='' autoconnect='0'/>\n<RegionConfig RFCountry='United States/Canada' RFRegulatory='US FCC 15' RFScanMode='0' LBTEnable='0' ChannelData='FFFFFFFFFFFFFFFF'/>\n<SnmpConfig snmpVersion='1' heartbeat='1'/>\n<SyslogConfig RemoteIp='0.0.0.0' RemotePort='514' LogMinSeverity='7' ApplyFilter='0' MinimumSeverity='7' ProcessFilter='rmserver.elf,llrpserver.elf,snmpextagent.elf,RDMPAgent'/>\n<UserList>\n<User name='admin' PSWD='$6$weLpDwlv$utr0AwgPIae2O4Gln4cQ2IJJblXye412Xqni0V.ahIFKUOCEDGjzZ4ttthhrw7rmmQYsCXKwA9znyqPkAT.IL/'/>\n<User name='rfidadm' PSWD='15491'/>\n</UserList>\n<IPReader name='FX96007AF832 FX9600 RFID Reader' desc='FX96007AF832 Advanced Reader' flags='0' MonoStatic='0' CheckAntenna='1' gpiDebounceTime='0' gpioMapping='0' idleModeTimeOut='0' diagMode='0' extDiagMode='0' contact='Zebra Technologies Corporation' PowerNegotiation='0' PowerNegotiationProtocol='0' allowGuestLogin='1' configureHostName='0'>\n<ReadPoint name='Read Point 1' flags='0' CableLossPerHundredFt='10' CableLength='10'/>\n<ReadPoint name='Read Point 2' flags='0' CableLossPerHundredFt='10' CableLength='10'/>\n<ReadPoint name='Read Point 3' flags='1' CableLossPerHundredFt='10' CableLength='10'/>\n<ReadPoint name='Read Point 4' flags='1' CableLossPerHundredFt='10' CableLength='10'/>\n</IPReader>\n<SerialPortConf Mode='0' Baudrate='115200' Databits='8' Parity='none' Stopbits='1' Flowcontrol='hardware' TagMetaData='0' InventoryControl='0' IsAutostart='0'/>\n<FXConnectConfig FXConnectMode='0' TagMetaData='0' InventoryControl='None' HeartBeatPeriod='0' IsAutostart='0' PreFilterMode='0' PreFilters='None'/>\n<ProfinetConfig virtualDAP='1'/>\n<NodeJSPortConf Portnumber='8001'/>\n</Config>\n<MOTOROLA_LLRP_CONFIG><LLRP_READER_CONFIG />\n</MOTOROLA_LLRP_CONFIG>\n<IOT_CONNECT_CONFIG><OPERATING_MODE />\n</IOT_CONNECT_CONFIG>\n<RadioProfileData><RadioRegisterData Address='0' Data='00'/>\n</RadioProfileData>\n<CustomProfileData ForceEAPMode='0' FIPS_MODE_ENABLED='0' MaxNumberOfTagsBuffered='512'/>\n</Motorola >\n"
}

View File

@@ -0,0 +1,206 @@
{
"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
}
],
"endpointConfig": {
"data": {
"event": {
"connections": [
{
"additionalOptions": {
"retention": {
"maxEventRetentionTimeInMin": 500,
"maxNumEvents": 150000,
"throttle": 100
}
},
"description": "",
"name": "lst",
"options": {
"URL": "http://usday1vms006:3100/api/rfid/taginfo/wrapper1",
"security": {
"CACertificateFileLocation": "",
"authenticationOptions": {},
"authenticationType": "NONE",
"verifyHost": false,
"verifyPeer": false
}
},
"type": "httpPost"
},
{
"additionalOptions": {
"retention": {
"maxEventRetentionTimeInMin": 500,
"maxNumEvents": 150000,
"throttle": 100
}
},
"description": "",
"name": "mgt",
"options": {
"URL": "http://usday1vms006:3100/api/rfid/mgtevents/wrapper1",
"security": {
"CACertificateFileLocation": "",
"authenticationOptions": {},
"authenticationType": "NONE",
"verifyHost": false,
"verifyPeer": false
}
},
"type": "httpPost"
}
]
}
}
},
"interfaces": {
"tagDataInterface1": "lst",
"managementEventsInterface": "mgt"
},
"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"
],
"userDefined": null,
"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
}
]
}
}

View File

@@ -1,9 +1,11 @@
import type { Express } from "express";
import { setupNotificationRoutes } from "notification/notification.routes.js";
import { setupAuthRoutes } from "./auth/auth.routes.js";
// import the routes and route setups
import { setupApiDocsRoutes } from "./configs/scaler.config.js";
import { setupDatamartRoutes } from "./datamart/datamart.routes.js";
import { setupNotificationRoutes } from "./notification/notification.routes.js";
import { setupOCPRoutes } from "./ocp/ocp.routes.js";
import { setupOpendockRoutes } from "./opendock/opendock.routes.js";
import { setupProdSqlRoutes } from "./prodSql/prodSql.routes.js";
import { setupSystemRoutes } from "./system/system.routes.js";
@@ -19,4 +21,5 @@ export const setupRoutes = (baseUrl: string, app: Express) => {
setupUtilsRoutes(baseUrl, app);
setupOpendockRoutes(baseUrl, app);
setupNotificationRoutes(baseUrl, app);
setupOCPRoutes(baseUrl, app);
};

View File

@@ -1,12 +1,12 @@
import { createServer } from "node:http";
import os from "node:os";
import { startNotifications } from "notification/notification.controller.js";
import { createNotifications } from "notification/notifications.master.js";
import createApp from "./app.js";
import { db } from "./db/db.controller.js";
import { dbCleanup } from "./db/dbCleanup.controller.js";
import { type Setting, settings } from "./db/schema/settings.schema.js";
import { createLogger } from "./logger/logger.controller.js";
import { startNotifications } from "./notification/notification.controller.js";
import { createNotifications } from "./notification/notifications.master.js";
import { monitorReleaseChanges } from "./opendock/openDockRreleaseMonitor.utils.js";
import { opendockSocketMonitor } from "./opendock/opendockSocketMonitor.utils.js";
import { connectProdSql } from "./prodSql/prodSqlConnection.controller.js";

View File

@@ -8,8 +8,8 @@ type RoomDefinition<T = unknown> = {
};
export const protectedRooms: any = {
logs: { requiresAuth: true, role: "admin" },
admin: { requiresAuth: true, role: "admin" },
logs: { requiresAuth: true, role: ["admin", "systemAdmin"] },
admin: { requiresAuth: true, role: ["admin", "systemAdmin"] },
};
export const roomDefinition: Record<RoomId, RoomDefinition> = {

View File

@@ -13,9 +13,9 @@ import { createRoomEmitter, preseedRoom } from "./roomService.socket.js";
//const __dirname = dirname(__filename);
const log = createLogger({ module: "socket.io", subModule: "setup" });
import { auth } from "../utils/auth.utils.js";
//import type { Session, User } from "better-auth"; // adjust if needed
import { protectedRooms } from "./roomDefinitions.socket.js";
import { auth } from "../utils/auth.utils.js";
// declare module "socket.io" {
// interface Socket {
@@ -88,7 +88,12 @@ export const setupSocketIORoutes = (baseUrl: string, server: HttpServer) => {
});
}
if (config?.role && s.user?.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 && !roles.includes(s.user?.role)) {
return s.emit("room-error", {
room: rn,
message: `Not authorized to be in room: ${rn}`,

View File

@@ -9,7 +9,7 @@ const newSettings: NewSetting[] = [
{
name: "opendock_sync",
value: "0",
active: true,
active: false,
description: "Dock Scheduling system",
moduleName: "opendock",
settingType: "feature",
@@ -19,7 +19,7 @@ const newSettings: NewSetting[] = [
{
name: "ocp",
value: "1",
active: true,
active: false,
description: "One click print",
moduleName: "ocp",
settingType: "feature",
@@ -29,7 +29,7 @@ const newSettings: NewSetting[] = [
{
name: "ocme",
value: "0",
active: true,
active: false,
description: "Dayton Agv system",
moduleName: "ocme",
settingType: "feature",
@@ -39,7 +39,7 @@ const newSettings: NewSetting[] = [
{
name: "demandManagement",
value: "1",
active: true,
active: false,
description: "Fake EDI System",
moduleName: "demandManagement",
settingType: "feature",
@@ -49,7 +49,7 @@ const newSettings: NewSetting[] = [
{
name: "qualityRequest",
value: "0",
active: true,
active: false,
description: "Quality System",
moduleName: "qualityRequest",
settingType: "feature",
@@ -59,7 +59,7 @@ const newSettings: NewSetting[] = [
{
name: "tms",
value: "0",
active: true,
active: false,
description: "Transport system integration",
moduleName: "tms",
settingType: "feature",

View File

@@ -15,6 +15,7 @@ export const allowedOrigins = [
`http://${process.env.PROD_SERVER}:3000`,
`http://${process.env.PROD_SERVER}:3100`, // temp
`http://usmcd1olp082:3000`,
`${process.env.EXTERNAL_URL}`, // internal docker
];
export const lstCors = () => {
return cors({

View File

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

View File

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

View File

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

View File

@@ -10,14 +10,6 @@ get {
auth: inherit
}
body:json {
{
"userId":"0kHd6Kkdub4GW6rK1qa1yjWwqXtvykqT",
"notificationId": "0399eb2a-39df-48b7-9f1c-d233cec94d2e",
"emails": ["blake.mattes@alpla.com"]
}
}
settings {
encodeUrl: true
timeout: 0

View File

@@ -16,8 +16,8 @@ params:path {
body:json {
{
"active" : false,
"options": [{"prodId": 5}]
"active" : true,
"options": []
}
}

View File

@@ -0,0 +1,22 @@
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
}

8
brunoApi/ocp/folder.bru Normal file
View File

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

View File

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

View File

@@ -0,0 +1,20 @@
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

@@ -0,0 +1,20 @@
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

@@ -0,0 +1,32 @@
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

@@ -0,0 +1,237 @@
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

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

View File

@@ -1,16 +1,21 @@
services:
app:
build:
context: .
dockerfile: Dockerfile
lst:
image: git.tuffraid.net/cowch/lst_v3:latest
container_name: lst_app
restart: unless-stopped
# app:
# build:
# context: .
# dockerfile: Dockerfile
# container_name: lst_app
ports:
#- "${VITE_PORT:-4200}:4200"
- "3600:3000"
environment:
- NODE_ENV=production
- LOG_LEVEL=info
- DATABASE_HOST=host.docker.internal
- EXTERNAL_URL=192.168.8.222:3600
- DATABASE_HOST=host.docker.internal # if running on the same docker then do this
- DATABASE_PORT=5433
- DATABASE_USER=${DATABASE_USER}
- DATABASE_PASSWORD=${DATABASE_PASSWORD}
@@ -21,7 +26,6 @@ services:
- PROD_PASSWORD=${PROD_PASSWORD}
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
- BETTER_AUTH_URL=${URL}
restart: unless-stopped
# for all host including prod servers, plc's, printers, or other de
# extra_hosts:
# - "${PROD_SERVER}:${PROD_IP}"

View File

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

View File

@@ -1,69 +0,0 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

View File

@@ -69,7 +69,7 @@ export default function Header() {
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Link to="/user/profile">Profile</Link>
<Link to="/user/profile">Account</Link>
</DropdownMenuItem>
{/* <DropdownMenuItem>Billing</DropdownMenuItem>

View File

@@ -11,17 +11,27 @@ import {
useSidebar,
} from "../ui/sidebar";
export default function AdminSidebar() {
// type AdminSidebarProps = {
// session: {
// user: {
// name?: string | null;
// email?: string | null;
// role?: string | string[];
// };
// } | null;
//};
export default function AdminSidebar({ session }: any) {
const { setOpen } = useSidebar();
const items = [
// {
// title: "Users",
// url: "/admin/users",
// icon: User,
// role: ["systemAdmin", "admin"],
// module: "admin",
// active: true,
// },
{
title: "Settings",
url: "/admin/settings",
icon: Logs,
role: ["systemAdmin"],
module: "admin",
active: true,
},
{
title: "Logs",
url: "/admin/logs",
@@ -53,14 +63,18 @@ export default function AdminSidebar() {
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<Link to={item.url} onClick={() => setOpen(false)}>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<>
{item.role.includes(session.user.role) && (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<Link to={item.url} onClick={() => setOpen(false)}>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)}
</>
))}
</SidebarMenu>
</SidebarGroupContent>

View File

@@ -10,6 +10,7 @@ import AdminSidebar from "./AdminBar";
export function AppSidebar() {
const { data: session } = useSession();
return (
<Sidebar
variant="sidebar"
@@ -20,7 +21,11 @@ export function AppSidebar() {
<SidebarMenu>
<SidebarMenuItem>
<SidebarContent>
{session && session.user.role === "admin" && <AdminSidebar />}
{session &&
(session.user.role === "admin" ||
session.user.role === "systemAdmin") && (
<AdminSidebar session={session} />
)}
</SidebarContent>
</SidebarMenuItem>
</SidebarMenu>

View File

@@ -0,0 +1,53 @@
import * as React from "react"
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
data-orientation={orientation}
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="relative flex-1 rounded-full bg-border"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }

View File

@@ -0,0 +1,190 @@
import * as React from "react"
import { Select as SelectPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return (
<SelectPrimitive.Group
data-slot="select-group"
className={cn("scroll-my-1 p-1", className)}
{...props}
/>
)
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "item-aligned",
align = "center",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
data-align-trigger={position === "item-aligned"}
className={cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className )}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
data-position={position}
className={cn(
"data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)",
position === "popper" && ""
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="pointer-events-none" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<ChevronUpIcon
/>
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<ChevronDownIcon
/>
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View File

@@ -0,0 +1,31 @@
import * as React from "react"
import { Switch as SwitchPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Switch({
className,
size = "default",
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root> & {
size?: "sm" | "default"
}) {
return (
<SwitchPrimitive.Root
data-slot="switch"
data-size={size}
className={cn(
"peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
/>
</SwitchPrimitive.Root>
)
}
export { Switch }

View File

@@ -0,0 +1,114 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
)
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@@ -0,0 +1,88 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Tabs as TabsPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Tabs({
className,
orientation = "horizontal",
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
data-orientation={orientation}
className={cn(
"group/tabs flex gap-2 data-horizontal:flex-col",
className
)}
{...props}
/>
)
}
const tabsListVariants = cva(
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
{
variants: {
variant: {
default: "bg-muted",
line: "gap-1 bg-transparent",
},
},
defaultVariants: {
variant: "default",
},
}
)
function TabsList({
className,
variant = "default",
...props
}: React.ComponentProps<typeof TabsPrimitive.List> &
VariantProps<typeof tabsListVariants>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
data-variant={variant}
className={cn(tabsListVariants({ variant }), className)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
className
)}
{...props}
/>
)
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 text-sm outline-none", className)}
{...props}
/>
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }

View File

@@ -0,0 +1,44 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Toggle as TogglePrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
const toggleVariants = cva(
"group/toggle inline-flex items-center justify-center gap-1 rounded-lg text-sm font-medium whitespace-nowrap transition-all outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-transparent",
outline: "border border-input bg-transparent hover:bg-muted",
},
size: {
default: "h-8 min-w-8 px-2",
sm: "h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-1.5 text-[0.8rem]",
lg: "h-9 min-w-9 px-2.5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Toggle({
className,
variant = "default",
size = "default",
...props
}: React.ComponentProps<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive.Root
data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Toggle, toggleVariants }

View File

@@ -0,0 +1,87 @@
import { Trash2 } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "../../components/ui/button";
import { useFieldContext } from ".";
import { FieldErrors } from "./Errors.Field";
type DynamicInputField = {
name?: string;
label: string;
inputType: "text" | "email" | "password" | "number" | "username";
required?: boolean;
description?: string;
addLabel?: string;
placeholder?: string;
disabled?: boolean;
};
const autoCompleteMap: Record<string, string> = {
email: "email",
password: "current-password",
text: "off",
username: "username",
};
export const DynamicInputField = ({
label,
inputType = "text",
required = false,
description,
addLabel,
}: DynamicInputField) => {
const field = useFieldContext<any>();
const values = Array.isArray(field.state.value) ? field.state.value : [];
return (
<div className="grid gap-3 mt-2">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1">
<Label>{label}</Label>
{description ? (
<p className="text-sm text-muted-foreground">{description}</p>
) : null}
</div>
<Button
type="button"
variant="secondary"
onClick={() => {
field.pushValue("");
}}
>
{addLabel}
</Button>
</div>
<div className="grid gap-3">
{values.map((_: string, index: number) => (
<div key={`${field.name}-${index}`} className="grid gap-2">
<div className="flex items-center gap-2">
<Label htmlFor={field.name}>{label}</Label>
<Input
id={field.name}
autoComplete={autoCompleteMap[inputType] ?? "off"}
value={field.state.value?.[index] ?? ""}
onChange={(e) => field.replaceValue(index, e.target.value)}
onBlur={field.handleBlur}
type={inputType}
required={required}
/>
{values.length > 1 ? (
<Button
type="button"
size={"icon"}
variant="destructive"
onClick={() => field.removeValue(index)}
>
<Trash2 className="w-32 h-32" />
</Button>
) : null}
<FieldErrors meta={field.state.meta} />
</div>
</div>
))}
</div>
</div>
);
};

View File

@@ -6,7 +6,7 @@ import { FieldErrors } from "./Errors.Field";
type InputFieldProps = {
label: string;
inputType: string;
required: boolean;
required?: boolean;
};
const autoCompleteMap: Record<string, string> = {
@@ -16,7 +16,11 @@ const autoCompleteMap: Record<string, string> = {
username: "username",
};
export const InputField = ({ label, inputType, required }: InputFieldProps) => {
export const InputField = ({
label,
inputType,
required = false,
}: InputFieldProps) => {
const field = useFieldContext<any>();
return (

View File

@@ -0,0 +1,57 @@
import { Label } from "../../components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../components/ui/select";
import { useFieldContext } from ".";
import { FieldErrors } from "./Errors.Field";
type SelectOption = {
value: string;
label: string;
};
type SelectFieldProps = {
label: string;
options: SelectOption[];
placeholder?: string;
};
export const SelectField = ({
label,
options,
placeholder,
}: SelectFieldProps) => {
const field = useFieldContext<string>();
return (
<div className="grid gap-3">
<div className="grid gap-3">
<Label htmlFor={field.name}>{label}</Label>
<Select
value={field.state.value}
onValueChange={(value) => field.handleChange(value)}
>
<SelectTrigger
id={field.name}
onBlur={field.handleBlur}
className="w-min-2/3 w-max-fit"
>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<FieldErrors meta={field.state.meta} />
</div>
);
};

View File

@@ -1,13 +1,19 @@
import { useStore } from "@tanstack/react-form";
import { Button } from "@/components/ui/button";
import { useFormContext } from ".";
import { Spinner } from "@/components/ui/spinner";
import { useFormContext } from ".";
type SubmitButtonProps = {
children: React.ReactNode;
variant?: "default" | "secondary" | "destructive";
className?: string;
};
export const SubmitButton = ({ children }: SubmitButtonProps) => {
export const SubmitButton = ({
children,
variant = "default",
className,
}: SubmitButtonProps) => {
const form = useFormContext();
const [isSubmitting] = useStore(form.store, (state) => [
@@ -17,10 +23,19 @@ export const SubmitButton = ({ children }: SubmitButtonProps) => {
return (
<div className="">
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? <><Spinner data-icon="inline-start" /> Submitting </> : <>{children}</>
}
<Button
type="submit"
disabled={isSubmitting}
variant={variant}
className={className}
>
{isSubmitting ? (
<>
<Spinner data-icon="inline-start" /> Submitting{" "}
</>
) : (
<>{children}</>
)}
</Button>
</div>
);

View File

@@ -0,0 +1,29 @@
import { Label } from "../../components/ui/label";
import { Switch } from "../../components/ui/switch";
import { useFieldContext } from ".";
type SwitchField = {
trueLabel: string;
falseLabel: string;
};
export const SwitchField = ({
trueLabel = "True",
falseLabel = "False",
}: SwitchField) => {
const field = useFieldContext<boolean>();
const checked = field.state.value ?? false;
return (
<div className="flex items-center space-x-2">
<Switch
id={field.name}
checked={checked}
onCheckedChange={field.handleChange}
onBlur={field.handleBlur}
/>
<Label htmlFor={field.name}>{checked ? trueLabel : falseLabel}</Label>
</div>
);
};

View File

@@ -1,8 +1,11 @@
import { createFormHook, createFormHookContexts } from "@tanstack/react-form";
import { CheckboxField } from "./CheckBox.Field";
import { DynamicInputField } from "./DynamicInput.Field";
import { InputField } from "./Input.Field";
import { InputPasswordField } from "./InputPassword.Field";
import { SelectField } from "./Select.Field";
import { SubmitButton } from "./SubmitButton";
import { SwitchField } from "./Switch.Field";
export const { fieldContext, useFieldContext, formContext, useFormContext } =
createFormHookContexts();
@@ -11,11 +14,13 @@ export const { useAppForm } = createFormHook({
fieldComponents: {
InputField,
InputPasswordField,
//SelectField,
SelectField,
CheckboxField,
//DateField,
//TextArea,
//Searchable,
SwitchField,
DynamicInputField,
},
formComponents: { SubmitButton },
fieldContext,

View File

@@ -0,0 +1,22 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import axios from "axios";
export function getSettings() {
return queryOptions({
queryKey: ["getSettings"],
queryFn: () => fetch(),
staleTime: 5000,
refetchOnWindowFocus: true,
placeholderData: keepPreviousData,
});
}
const fetch = async () => {
if (window.location.hostname === "localhost") {
await new Promise((res) => setTimeout(res, 5000));
}
const { data } = await axios.get("/lst/api/settings");
return data.data;
};

View File

@@ -0,0 +1,24 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import axios from "axios";
export function notificationSubs(userId?: string) {
return queryOptions({
queryKey: ["notificationSubs"],
queryFn: () => fetch(userId),
staleTime: 5000,
refetchOnWindowFocus: true,
placeholderData: keepPreviousData,
});
}
const fetch = async (userId?: string) => {
if (window.location.hostname === "localhost") {
await new Promise((res) => setTimeout(res, 5000));
}
const { data } = await axios.get(
`/lst/api/notification/sub${userId ? `?userId=${userId}` : ""}`,
);
return data.data;
};

View File

@@ -0,0 +1,22 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import axios from "axios";
export function notifications() {
return queryOptions({
queryKey: ["notifications"],
queryFn: () => fetch(),
staleTime: 5000,
refetchOnWindowFocus: true,
placeholderData: keepPreviousData,
});
}
const fetch = async () => {
if (window.location.hostname === "localhost") {
await new Promise((res) => setTimeout(res, 5000));
}
const { data } = await axios.get("/lst/api/notification");
return data.data;
};

View File

@@ -0,0 +1,70 @@
import { useEffect, useRef, useState } from "react";
import { Input } from "../../components/ui/input";
type EditableCell = {
value: string | number | null | undefined;
id: string;
field: string;
className?: string;
onSubmit: (args: { id: string; field: string; value: string }) => void;
};
export default function EditableCellInput({
value,
id,
field,
className = "w-32",
onSubmit,
}: EditableCell) {
const initialValue = String(value ?? "");
const [localValue, setLocalValue] = useState(initialValue);
const submitting = useRef(false);
useEffect(() => {
setLocalValue(initialValue);
}, [initialValue]);
const handleSubmit = (nextValue: string) => {
const trimmedValue = nextValue.trim();
if (trimmedValue === initialValue) return;
onSubmit({
id,
field,
value: trimmedValue,
});
};
return (
<Input
value={localValue}
className={className}
onChange={(e) => setLocalValue(e.currentTarget.value)}
onBlur={(e) => {
if (submitting.current) return;
submitting.current = true;
handleSubmit(e.currentTarget.value);
setTimeout(() => {
submitting.current = false;
}, 100);
}}
onKeyDown={(e) => {
if (e.key !== "Enter") return;
e.preventDefault();
if (submitting.current) return;
submitting.current = true;
handleSubmit(e.currentTarget.value);
e.currentTarget.blur();
setTimeout(() => {
submitting.current = false;
}, 100);
}}
/>
);
}

View File

@@ -0,0 +1,129 @@
import {
type ColumnFiltersState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
type SortingState,
useReactTable,
} from "@tanstack/react-table";
import React, { useState } from "react";
import { Button } from "../../components/ui/button";
import { ScrollArea, ScrollBar } from "../../components/ui/scroll-area";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../../components/ui/table";
import { cn } from "../utils";
type LstTableType = {
className?: string;
tableClassName?: string;
data: any;
columns: any;
};
export default function LstTable({
className = "",
tableClassName = "",
data,
columns,
}: LstTableType) {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
//console.log(data);
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
//renderSubComponent: ({ row }: { row: any }) => <ExpandedRow row={row} />,
//getRowCanExpand: () => true,
filterFns: {},
state: {
sorting,
columnFilters,
},
});
return (
<div className={className}>
<ScrollArea className="w-full rounded-md border whitespace-nowrap">
<Table className={cn("w-full", tableClassName)}>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
<React.Fragment key={row.id}>
<TableRow data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
</React.Fragment>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<ScrollBar orientation="horizontal" />
</ScrollArea>
<div className="flex items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,67 @@
import type { Column } from "@tanstack/react-table";
import { ArrowDown, ArrowUp, Search } from "lucide-react";
import { useState } from "react";
import { Button } from "../../components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "../../components/ui/dropdown-menu";
import { Input } from "../../components/ui/input";
import { cn } from "../utils";
type SearchableHeaderProps<TData> = {
column: Column<TData, unknown>;
title: string;
searchable?: boolean;
};
export default function SearchableHeader<TData>({
column,
title,
searchable = false,
}: SearchableHeaderProps<TData>) {
const [open, setOpen] = useState(false);
return (
<div className="flex items-center gap-1">
<Button
variant="ghost"
className="px-2"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
<span className="flex flex-row items-center gap-2">
{title}
{column.getIsSorted() === "asc" ? (
<ArrowUp className="h-4 w-4" />
) : column.getIsSorted() === "desc" ? (
<ArrowDown className="h-4 w-4" />
) : null}
</span>
</Button>
{searchable && (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<Search
className={cn(
"h-4 w-4",
column.getFilterValue() ? "text-primary" : "",
)}
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56 p-2">
<Input
autoFocus
value={(column.getFilterValue() as string) ?? ""}
onChange={(e) => column.setFilterValue(e.target.value)}
placeholder={`Search ${title.toLowerCase()}...`}
className="h-8"
/>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
);
}

View File

@@ -0,0 +1,40 @@
import { Skeleton } from "../../components/ui/skeleton";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../../components/ui/table";
type TableSkelly = {
rows?: number;
columns?: number;
};
export default function SkellyTable({ rows = 5, columns = 4 }: TableSkelly) {
return (
<div className="rounded-md border">
<Table>
<TableHeader>
{Array.from({ length: columns }).map((_, i) => (
<TableHead key={i}>
<Skeleton className="h-4 w-[80px]" />
</TableHead>
))}
</TableHeader>
<TableBody>
{Array.from({ length: rows }).map((_, r) => (
<TableRow key={r}>
{Array.from({ length: columns }).map((_, c) => (
<TableCell key={c}>
<Skeleton className="h-4 w-full max-w-[120px]" />
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}

View File

@@ -11,6 +11,7 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as AboutRouteImport } from './routes/about'
import { Route as IndexRouteImport } from './routes/index'
import { Route as AdminSettingsRouteImport } from './routes/admin/settings'
import { Route as AdminLogsRouteImport } from './routes/admin/logs'
import { Route as authLoginRouteImport } from './routes/(auth)/login'
import { Route as authUserSignupRouteImport } from './routes/(auth)/user.signup'
@@ -27,6 +28,11 @@ const IndexRoute = IndexRouteImport.update({
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const AdminSettingsRoute = AdminSettingsRouteImport.update({
id: '/admin/settings',
path: '/admin/settings',
getParentRoute: () => rootRouteImport,
} as any)
const AdminLogsRoute = AdminLogsRouteImport.update({
id: '/admin/logs',
path: '/admin/logs',
@@ -58,6 +64,7 @@ export interface FileRoutesByFullPath {
'/about': typeof AboutRoute
'/login': typeof authLoginRoute
'/admin/logs': typeof AdminLogsRoute
'/admin/settings': typeof AdminSettingsRoute
'/user/profile': typeof authUserProfileRoute
'/user/resetpassword': typeof authUserResetpasswordRoute
'/user/signup': typeof authUserSignupRoute
@@ -67,6 +74,7 @@ export interface FileRoutesByTo {
'/about': typeof AboutRoute
'/login': typeof authLoginRoute
'/admin/logs': typeof AdminLogsRoute
'/admin/settings': typeof AdminSettingsRoute
'/user/profile': typeof authUserProfileRoute
'/user/resetpassword': typeof authUserResetpasswordRoute
'/user/signup': typeof authUserSignupRoute
@@ -77,6 +85,7 @@ export interface FileRoutesById {
'/about': typeof AboutRoute
'/(auth)/login': typeof authLoginRoute
'/admin/logs': typeof AdminLogsRoute
'/admin/settings': typeof AdminSettingsRoute
'/(auth)/user/profile': typeof authUserProfileRoute
'/(auth)/user/resetpassword': typeof authUserResetpasswordRoute
'/(auth)/user/signup': typeof authUserSignupRoute
@@ -88,6 +97,7 @@ export interface FileRouteTypes {
| '/about'
| '/login'
| '/admin/logs'
| '/admin/settings'
| '/user/profile'
| '/user/resetpassword'
| '/user/signup'
@@ -97,6 +107,7 @@ export interface FileRouteTypes {
| '/about'
| '/login'
| '/admin/logs'
| '/admin/settings'
| '/user/profile'
| '/user/resetpassword'
| '/user/signup'
@@ -106,6 +117,7 @@ export interface FileRouteTypes {
| '/about'
| '/(auth)/login'
| '/admin/logs'
| '/admin/settings'
| '/(auth)/user/profile'
| '/(auth)/user/resetpassword'
| '/(auth)/user/signup'
@@ -116,6 +128,7 @@ export interface RootRouteChildren {
AboutRoute: typeof AboutRoute
authLoginRoute: typeof authLoginRoute
AdminLogsRoute: typeof AdminLogsRoute
AdminSettingsRoute: typeof AdminSettingsRoute
authUserProfileRoute: typeof authUserProfileRoute
authUserResetpasswordRoute: typeof authUserResetpasswordRoute
authUserSignupRoute: typeof authUserSignupRoute
@@ -137,6 +150,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/admin/settings': {
id: '/admin/settings'
path: '/admin/settings'
fullPath: '/admin/settings'
preLoaderRoute: typeof AdminSettingsRouteImport
parentRoute: typeof rootRouteImport
}
'/admin/logs': {
id: '/admin/logs'
path: '/admin/logs'
@@ -180,6 +200,7 @@ const rootRouteChildren: RootRouteChildren = {
AboutRoute: AboutRoute,
authLoginRoute: authLoginRoute,
AdminLogsRoute: AdminLogsRoute,
AdminSettingsRoute: AdminSettingsRoute,
authUserProfileRoute: authUserProfileRoute,
authUserResetpasswordRoute: authUserResetpasswordRoute,
authUserSignupRoute: authUserSignupRoute,

View File

@@ -79,7 +79,9 @@ export default function ChangePassword() {
<div className="flex justify-end mt-6">
<form.AppForm>
<form.SubmitButton>Update Profile</form.SubmitButton>
<form.SubmitButton variant="destructive">
Update Password
</form.SubmitButton>
</form.AppForm>
</div>
</form>

View File

@@ -9,6 +9,7 @@ import {
} from "@/components/ui/card";
import { authClient } from "@/lib/auth-client";
import { useAppForm } from "@/lib/formSutff";
import socket from "../../../lib/socket.io";
export default function LoginForm({ redirectPath }: { redirectPath: string }) {
const loginEmail = localStorage.getItem("loginEmail") || "";
@@ -47,8 +48,12 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
return;
}
toast.success(`Welcome back ${login.data?.user.name}`);
if (login.data) {
socket.disconnect();
socket.connect();
}
} catch (error) {
console.log(error);
console.error(error);
}
},
});

View File

@@ -0,0 +1,87 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../../../components/ui/card";
import { useAppForm } from "../../../lib/formSutff";
import { notificationSubs } from "../../../lib/queries/notificationSubs";
import { notifications } from "../../../lib/queries/notifications";
export default function NotificationsSubCard({ user }: any) {
const { data } = useSuspenseQuery(notifications());
const { data: ns } = useSuspenseQuery(notificationSubs(user.id));
const form = useAppForm({
defaultValues: {
notificationId: "",
emails: [user.email],
},
onSubmit: async ({ value }) => {
const postD = { ...value, userId: user.id };
console.log(postD);
},
});
let n: any = [];
if (data) {
n = data.map((i: any) => ({
label: i.name,
value: i.id,
}));
}
console.log(ns);
return (
<div>
<Card className="p-3 w-128">
<CardHeader>
<CardTitle>Notifications</CardTitle>
<CardDescription>
All currently active notifications you can subscribe to. selecting a
notification will give you a brief description on how it works
</CardDescription>
</CardHeader>
<CardContent>
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<div>
<form.AppField name="notificationId">
{(field) => (
<field.SelectField
label="Notifications"
placeholder="Select Notification"
options={n}
/>
)}
</form.AppField>
</div>
<form.AppField name="emails" mode="array">
{(field) => (
<field.DynamicInputField
label="Notification Emails"
description="Add more email addresses for notification delivery."
inputType="email"
addLabel="Add Email"
//initialValue={session?.user.email}
/>
)}
</form.AppField>
<div className="flex justify-end mt-6">
<form.AppForm>
<form.SubmitButton>Subscribe</form.SubmitButton>
</form.AppForm>
</div>
</form>
</CardContent>
</Card>
</div>
);
}

View File

@@ -25,7 +25,7 @@ function RouteComponent() {
const redirectPath = search.redirect ?? "/";
return (
<div className="flex justify-center mt-10">
<div className="flex justify-center mt-2">
<LoginForm redirectPath={redirectPath} />
</div>
);

View File

@@ -1,4 +1,5 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
import { Suspense } from "react";
import { toast } from "sonner";
import {
Card,
@@ -9,7 +10,9 @@ import {
} from "@/components/ui/card";
import { authClient, useSession } from "@/lib/auth-client";
import { useAppForm } from "@/lib/formSutff";
import { Spinner } from "../../components/ui/spinner";
import ChangePassword from "./-components/ChangePassword";
import NotificationsSubCard from "./-components/NotificationsSubCard";
export const Route = createFileRoute("/(auth)/user/profile")({
beforeLoad: async () => {
@@ -54,7 +57,7 @@ function RouteComponent() {
},
});
return (
<div className="flex justify-center mt-2 gap-2">
<div className="flex justify-center flex-col pt-4 gap-2 lg:flex-row">
<div>
<Card className="p-6 w-96">
<CardHeader>
@@ -93,6 +96,26 @@ function RouteComponent() {
<div>
<ChangePassword />
</div>
<div>
<Suspense
fallback={
<Card className="p-3 w-96">
<CardHeader>
<CardTitle>Notifications</CardTitle>
</CardHeader>
<CardContent>
<div className="flex justify-center m-auto">
<div>
<Spinner className="size-32" />
</div>
</div>
</CardContent>
</Card>
}
>
{session && <NotificationsSubCard user={session.user} />}
</Suspense>
</div>
</div>
);
}

View File

@@ -3,7 +3,7 @@ import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import { Toaster } from "sonner";
import Header from "@/components/Header";
import { AppSidebar } from "@/components/Sidebar/sidebar";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import { SidebarProvider } from "@/components/ui/sidebar";
import { ThemeProvider } from "@/lib/theme-provider";
const RootLayout = () => (
@@ -11,12 +11,15 @@ const RootLayout = () => (
<ThemeProvider>
<SidebarProvider className="flex flex-col" defaultOpen={false}>
<Header />
<div className="flex flex-1">
<div className="relative min-h-[calc(100svh-var(--header-height))]">
<AppSidebar />
<SidebarInset>
<Outlet />
</SidebarInset>
<main className="w-full p-4">
<div className="mx-auto w-full max-w-7xl">
<Outlet />
</div>
</main>
</div>
<Toaster expand richColors closeButton />

View File

@@ -0,0 +1,92 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import axios from "axios";
import { toast } from "sonner";
import { Card, CardDescription, CardHeader } from "../../../components/ui/card";
import { useAppForm } from "../../../lib/formSutff";
import { getSettings } from "../../../lib/queries/getSettings";
type Setting = {
id: string;
name: string;
description?: string;
value: string;
active: boolean;
inputType: "text" | "boolean" | "number" | "select";
options?: string[];
};
export default function FeatureCard({ item }: { item: Setting }) {
const { refetch } = useSuspenseQuery(getSettings());
const form = useAppForm({
defaultValues: {
value: item.value ?? "",
active: item.active,
},
onSubmit: async ({ value }) => {
try {
// adding this in as my base as i need to see timers working
if (window.location.hostname === "localhost") {
await new Promise((res) => setTimeout(res, 1000));
}
const { data } = await axios.patch(`/lst/api/settings/${item.name}`, {
value: value.value,
active: value.active ? "true" : "false",
}, {
withCredentials: true,
});
refetch();
toast.success(
<div>
<p>{data.message}</p>
<p>
This was a feature setting so{" "}
{value.active
? "processes related to this will start working on there next interval"
: "processes related to this will stop working on there next interval"}
</p>
</div>,
);
} catch (error) {
console.error(error);
}
},
});
return (
<Card className="p-2 w-96">
<CardHeader>
<p>{item.name}</p>
<CardDescription>
<p>{item.description}</p>
</CardDescription>
</CardHeader>
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
void form.handleSubmit();
}}
>
<div className="flex justify-end mt-2 flex-col gap-4">
<form.AppField name="value">
{(field) => (
<field.InputField label="Setting Value" inputType="string" />
)}
</form.AppField>
<div className="flex flex-row justify-between">
<form.AppField name="active">
{(field) => (
<field.SwitchField trueLabel="Active" falseLabel="Deactivate" />
)}
</form.AppField>
<form.AppForm>
<form.SubmitButton>Update</form.SubmitButton>
</form.AppForm>
</div>
</div>
</form>
</Card>
);
}

View File

@@ -0,0 +1,11 @@
import FeatureCard from "./FeatureCard";
export default function FeatureSettings({ data }: any) {
return (
<div className=" flex flex-wrap gap-2">
{data.map((i: any) => (
<FeatureCard key={i.name} item={i} />
))}
</div>
);
}

View File

@@ -5,6 +5,7 @@ import { authClient } from "@/lib/auth-client";
export const Route = createFileRoute("/admin/logs")({
beforeLoad: async ({ location }) => {
const { data: session } = await authClient.getSession();
const allowedRole = ["admin", "systemAdmin"];
if (!session?.user) {
throw redirect({
@@ -15,7 +16,7 @@ export const Route = createFileRoute("/admin/logs")({
});
}
if (session.user.role !== "admin") {
if (!allowedRole.includes(session.user.role as string)) {
throw redirect({
to: "/",
});

View File

@@ -0,0 +1,209 @@
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { createColumnHelper } from "@tanstack/react-table";
import axios from "axios";
import { Suspense, useMemo } from "react";
import { toast } from "sonner";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "../../components/ui/card";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "../../components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "../../components/ui/tooltip";
import { authClient } from "../../lib/auth-client";
import { getSettings } from "../../lib/queries/getSettings";
import EditableCellInput from "../../lib/tableStuff/EditableCellInput";
import LstTable from "../../lib/tableStuff/LstTable";
import SearchableHeader from "../../lib/tableStuff/SearchableHeader";
import SkellyTable from "../../lib/tableStuff/SkellyTable";
import FeatureSettings from "./-components/FeatureSettings";
type Settings = {
settings_id: string;
name: string;
active: boolean;
value: string;
description: string;
moduleName: string;
roles: string[];
};
const updateSettings = async (
id: string,
data: Record<string, string | number | boolean | null>,
) => {
console.log(id, data);
try {
const res = await axios.patch(`/lst/api/settings/${id}`, data, {
withCredentials: true,
});
toast.success(`Setting just updated`);
return res;
} catch (err) {
toast.error("Error in updating the settings");
return err;
}
};
export const Route = createFileRoute("/admin/settings")({
beforeLoad: async ({ location }) => {
const { data: session } = await authClient.getSession();
const allowedRole = ["systemAdmin"];
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,
});
function SettingsTableCard() {
const { data, refetch } = useSuspenseQuery(getSettings());
const columnHelper = createColumnHelper<Settings>();
const updateSetting = useMutation({
mutationFn: ({
id,
field,
value,
}: {
id: string;
field: string;
value: string | number | boolean | null;
}) => updateSettings(id, { [field]: value }),
onSuccess: () => {
// refetch or update cache
refetch();
},
});
const column = [
columnHelper.accessor("name", {
header: ({ column }) => (
<SearchableHeader column={column} title="Name" searchable={true} />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("description", {
header: ({ column }) => (
<SearchableHeader column={column} title="Description" />
),
cell: (i) => (
<Tooltip>
<TooltipTrigger>
{i.getValue().length > 25 ? (
<span>{i.getValue().slice(0, 25)}...</span>
) : (
<span>{i.getValue()}</span>
)}
</TooltipTrigger>
<TooltipContent>{i.getValue()}</TooltipContent>
</Tooltip>
),
}),
columnHelper.accessor("value", {
header: ({ column }) => (
<SearchableHeader column={column} title="Value" />
),
filterFn: "includesString",
cell: ({ row, getValue }) => (
<EditableCellInput
value={getValue()}
id={row.original.name}
field="value"
onSubmit={({ id, field, value }) => {
updateSetting.mutate({ id, field, value });
}}
/>
),
}),
];
const { standardSettings, featureSettings, systemSetting } = useMemo(() => {
return {
standardSettings: data.filter(
(setting: any) => setting.settingType === "standard",
),
featureSettings: data.filter(
(setting: any) => setting.settingType === "feature",
),
systemSetting: data.filter(
(setting: any) => setting.settingType === "system",
),
};
}, [data]);
return (
<>
<TabsContent value="feature">
<FeatureSettings data={featureSettings} />
</TabsContent>
<TabsContent value="system">
<LstTable data={systemSetting} columns={column} />
</TabsContent>
<TabsContent value="standard">
<LstTable data={standardSettings} columns={column} />
</TabsContent>
</>
);
}
function RouteComponent() {
return (
<div className="space-y-6">
<div className="space-y-2">
<h1 className="text-2xl font-semibold">Settings</h1>
<p className="text-sm text-muted-foreground">
Manage your settings and related data.
</p>
</div>
<Card>
<CardHeader>
<CardTitle>System Settings</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="standard" className="w-full">
<TabsList>
<TabsTrigger value="feature">Features</TabsTrigger>
<TabsTrigger value="system">System</TabsTrigger>
<TabsTrigger value="standard">Standard</TabsTrigger>
</TabsList>
<Suspense fallback={<SkellyTable />}>
<SettingsTableCard />
</Suspense>
</Tabs>
</CardContent>
</Card>
</div>
);
}

View File

@@ -18,10 +18,42 @@ function Index() {
if (isPending)
return <div className="flex justify-center mt-10">Loading...</div>;
// if (!session) return <button>Sign In</button>
let url: string;
if (window.location.origin.includes("localhost")) {
url = `https://www.youtube.com/watch?v=dQw4w9WgXcQ`;
} else if (window.location.origin.includes("vms006")) {
url = `https://${window.location.hostname.replace("vms006", "prod.alpla.net/")}lst/app/old/ocp`;
} else {
url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ";
}
return (
<div className="flex justify-center mt-10">
<h3 className="w-2xl text-3xl">Welcome Home!</h3>
<div className="flex justify-center m-10 flex-col">
<h3 className="w-2xl text-3xl">Welcome Lst - V3</h3>
<br></br>
<p>
This is active in your plant today due to having warehousing activated
and new functions needed to be introduced, you should be still using LST
as you were before
</p>
<br></br>
<p>
If you dont know why you are here and looking for One Click Print{" "}
<a href={`${url}`} target="_blank" rel="noopener">
<b>
<strong>Click</strong>
</b>
</a>
<a
href={`https://www.youtube.com/watch?v=dQw4w9WgXcQ`}
target="_blank"
rel="noopener"
>
<b>
<strong> Here</strong>
</b>
</a>
</p>
</div>
);
}

View File

@@ -0,0 +1,4 @@
CREATE TABLE "printer_log" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "printer_log_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"name" text NOT NULL
);

File diff suppressed because it is too large Load Diff

View File

@@ -134,6 +134,13 @@
"when": 1774032587305,
"tag": "0018_lowly_wallow",
"breakpoints": true
},
{
"idx": 19,
"version": "7",
"when": 1775159956510,
"tag": "0019_large_thunderbird",
"breakpoints": true
}
]
}

271
package-lock.json generated
View File

@@ -63,6 +63,7 @@
"@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.8",
"commitizen": "^4.3.1",
"cpy-cli": "^7.0.0",
"cz-conventional-changelog": "^3.3.0",
"npm-check-updates": "^19.6.5",
"openapi-types": "^12.1.3",
@@ -2388,6 +2389,19 @@
"url": "https://ko-fi.com/dangreen"
}
},
"node_modules/@sindresorhus/merge-streams": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
"integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@socket.io/admin-ui": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@socket.io/admin-ui/-/admin-ui-0.5.1.tgz",
@@ -3838,6 +3852,23 @@
"node": ">=6.6.0"
}
},
"node_modules/copy-file": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/copy-file/-/copy-file-11.1.0.tgz",
"integrity": "sha512-X8XDzyvYaA6msMyAM575CUoygY5b44QzLcGRKsK3MFmXcOvQa518dNPLsKYwkYsn72g3EiW+LE0ytd/FlqWmyw==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.11",
"p-event": "^6.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cors": {
"version": "2.8.6",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
@@ -3900,6 +3931,178 @@
"typescript": ">=5"
}
},
"node_modules/cpy": {
"version": "13.2.1",
"resolved": "https://registry.npmjs.org/cpy/-/cpy-13.2.1.tgz",
"integrity": "sha512-/H2B3WW9gccZJKjKoDZsIrDU3MkkHlxgheT82hUbInC5fEdi4+54zyYpFueZT9pLfr5ObrtgN4MsYYrmTmHzeg==",
"dev": true,
"license": "MIT",
"dependencies": {
"copy-file": "^11.1.0",
"globby": "^16.1.0",
"junk": "^4.0.1",
"micromatch": "^4.0.8",
"p-filter": "^4.1.0",
"p-map": "^7.0.4"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cpy-cli": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/cpy-cli/-/cpy-cli-7.0.0.tgz",
"integrity": "sha512-uGCdhIxGfZcPXidCuT8w1jBknVXFx0un7NLjzqBZcdnkIWtLUnWMPk5TC37ceoVjwASLSNsRtTXXNTuFIyE2ng==",
"dev": true,
"license": "MIT",
"dependencies": {
"cpy": "^13.2.0",
"globby": "^16.1.0",
"meow": "^14.0.0"
},
"bin": {
"cpy": "cli.js"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cpy-cli/node_modules/globby": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-16.2.0.tgz",
"integrity": "sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@sindresorhus/merge-streams": "^4.0.0",
"fast-glob": "^3.3.3",
"ignore": "^7.0.5",
"is-path-inside": "^4.0.0",
"slash": "^5.1.0",
"unicorn-magic": "^0.4.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cpy-cli/node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/cpy-cli/node_modules/meow": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/meow/-/meow-14.1.0.tgz",
"integrity": "sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cpy-cli/node_modules/slash": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
"integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cpy/node_modules/globby": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-16.2.0.tgz",
"integrity": "sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@sindresorhus/merge-streams": "^4.0.0",
"fast-glob": "^3.3.3",
"ignore": "^7.0.5",
"is-path-inside": "^4.0.0",
"slash": "^5.1.0",
"unicorn-magic": "^0.4.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cpy/node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/cpy/node_modules/p-filter": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-filter/-/p-filter-4.1.0.tgz",
"integrity": "sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-map": "^7.0.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cpy/node_modules/p-map": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz",
"integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cpy/node_modules/slash": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
"integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
@@ -6427,6 +6630,19 @@
"node": ">=8"
}
},
"node_modules/is-path-inside": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz",
"integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-plain-obj": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
@@ -6637,6 +6853,19 @@
"npm": ">=6"
}
},
"node_modules/junk": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/junk/-/junk-4.0.1.tgz",
"integrity": "sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
@@ -7467,6 +7696,22 @@
"dev": true,
"license": "MIT"
},
"node_modules/p-event": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/p-event/-/p-event-6.0.1.tgz",
"integrity": "sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-timeout": "^6.1.2"
},
"engines": {
"node": ">=16.17"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-filter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz",
@@ -7519,6 +7764,19 @@
"node": ">=6"
}
},
"node_modules/p-timeout": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz",
"integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
@@ -9256,6 +9514,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/unicorn-magic": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz",
"integrity": "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "lst_v3",
"version": "1.0.1",
"version": "1.0.2-alpha.0",
"description": "The tool that supports us in our everyday alplaprod",
"main": "index.js",
"scripts": {
@@ -13,23 +13,21 @@
"build": "rimraf dist && npm run dev:db:generate && npm run dev:db:migrate && npm run build:app && npm run build:copySql && cd frontend && npm run build",
"build:app": "tsc",
"agent": "powershell -ExecutionPolicy Bypass -File scripts/agentController.ps1",
"build:docker": "docker compose up --force-recreate --build -d",
"build:copySql": "xcopy backend\\prodSql\\queries dist\\prodSql\\queries\\ /E /I /Y ",
"build:docker": "rimraf dist && npm run build:app && npm run build:copySql",
"build:copySql": "cpy \"backend/prodSql/queries/**/*\" dist/prodSql/queries --parents",
"lint": "tsc && biome lint",
"start": "npm run start:server",
"start:server": "dotenvx run -f .env -- node dist/server.js",
"start:docker": "node dist/server.js",
"commit": "cz",
"changeset": "changeset",
"version": "changeset version",
"release": "dotenvx run -f .env -- npm run version && git push --follow-tags && node scripts/create-release.js",
"specCheck": "node scripts/check-route-specs.mjs"
"specCheck": "node scripts/check-route-specs.mjs",
"commit": "cz",
"changeset": "changeset",
"changeset:add": "changeset",
"changeset:version": "changeset version",
"changeset:status": "changeset status --verbose"
},
"workspaces": [
"backend",
"agent",
"shared"
],
"repository": {
"type": "git",
"url": "https://git.tuffraid.net/cowch/lst_v3.git"
@@ -57,6 +55,7 @@
"@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.8",
"commitizen": "^4.3.1",
"cpy-cli": "^7.0.0",
"cz-conventional-changelog": "^3.3.0",
"npm-check-updates": "^19.6.5",
"openapi-types": "^12.1.3",

View File

@@ -15,7 +15,17 @@ $Servers = @(
[PSCustomObject]@{
server = "uslim1vms006"
token = "uslim1"
loc = "E$\LST_V3"
loc = "D$\LST_V3"
},
[PSCustomObject]@{
server = "ushou1vms006"
token = "ushou1"
loc = "D$\LST_V3"
},
[PSCustomObject]@{
server = "usday1vms006"
token = "usday1"
loc = "D$\LST_V3"
},
[PSCustomObject]@{
server = "usmcd1vms036"
@@ -74,8 +84,9 @@ function Show-Menu {
Write-Host "==============================="
Write-Host "1. Build app"
Write-Host "2. Deploy New Release"
Write-Host "3. Restart Service"
Write-Host "4. Exit"
Write-Host "3. Upgrade Node"
Write-Host "4. Update Postgres"
Write-Host "5. Exit"
Write-Host ""
}
@@ -393,9 +404,57 @@ do {
}
}
"3" {
Write-Host "Restart selected"
Write-Host "Choose Server to upgrade node on"
$server = Select-Server -List $Servers
if($server -eq "all") {
Write-Host "Updating all servers"
for ($i = 0; $i -lt $Servers.Count; $i++) {
Write-Host "Updating $($Servers[$i].server)"
# Update-Server -Server $Servers[$i].server -Destination $Servers[$i].loc -Token $Servers[$i].token
Start-Sleep -Seconds 1
}
Read-Host -Prompt "Press Enter to continue..."
}
if ($server -ne "all") {
Write-Host "You selected $($server.server)"
# Update-Server -Server $server.server -Destination $server.loc -Token $server.token
# validate we have a node file to install in the folder
# stop service
# do update script on the server
# delete the .exe file
Read-Host -Prompt "Press Enter to continue..."
}
}
"4" {
Write-Host "Choose Server to upgrade postgres on"
$server = Select-Server -List $Servers
if($server -eq "all") {
Write-Host "Updating all servers"
for ($i = 0; $i -lt $Servers.Count; $i++) {
Write-Host "Updating $($Servers[$i].server)"
# Update-Server -Server $Servers[$i].server -Destination $Servers[$i].loc -Token $Servers[$i].token
Start-Sleep -Seconds 1
}
Read-Host -Prompt "Press Enter to continue..."
}
if ($server -ne "all") {
Write-Host "You selected $($server.server)"
# Update-Server -Server $server.server -Destination $server.loc -Token $server.token
# validate we have a postgres file to install in the folder
# stop service
# do update script on the server
# delete the .exe file
Read-Host -Prompt "Press Enter to continue..."
}
}
"5" {
Write-Host "Exiting..."
exit
}

113
scripts/dockerscripts.md Normal file
View File

@@ -0,0 +1,113 @@
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 v1.0.1 this will be the new version
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

View File

@@ -11,7 +11,7 @@ param (
)
# Example string to run with the parameters in it.
# .\scripts\services.ps1 -serviceName "LSTV3_app" -option "install" -appPath "E:\LST_V3" -description "Logistics Support Tool" -command "run start"
# .\scripts\services.ps1 -serviceName "LSTV3_app" -option "install" -appPath "D:\LST_V3" -description "Logistics Support Tool" -command "run start"
$nssmPath = $AppPath + "\nssm.exe"
$npmPath = "C:\Program Files\nodejs\npm.cmd" # Path to npm.cmd

View File

@@ -18,7 +18,7 @@
"exactOptionalPropertyTypes": true,
"baseUrl": "./backend",
"paths": {
"@/*": ["src/*"]
"@/*": ["*"]
},
"esModuleInterop": true,
"skipLibCheck": true,
@@ -27,8 +27,7 @@
//"allowImportingTsExtensions": true,
"noEmit": false
},
"include": ["backend/**/*"
],
"include": ["backend/**/*"],
"exclude": [
"node_modules",
"frontend",