66 Commits

Author SHA1 Message Date
7c31b43a4a fix(app): emit.maxlistener issue
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m27s
BREAKING CHANGE: moved teh middleware to call the api hits to the main app and removed from
everywhere else

closes #18
2026-05-11 13:25:43 -05:00
85e96f5ed1 fix(scanner): logut out corrections
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m24s
refs #17
2026-05-11 07:59:17 -05:00
6b515c608f chore(release): 0.0.2-alpha.10
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m12s
Release and Build Image / release (push) Successful in 16s
2026-05-08 15:09:49 -05:00
d8869b103b fix(scan user): typo 2026-05-08 15:08:33 -05:00
1dba774abc chore(server): removed a console log that shouldnt be there
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 1m10s
2026-05-08 15:06:08 -05:00
505d7cea5d refactor(scan): bump in build and style update 2026-05-08 15:05:47 -05:00
1ff5e5032f test(scanusers): added in scan users as test 2026-05-08 15:05:09 -05:00
5fa70da90c chore(file): name changes.. spelled wrong 2026-05-08 15:04:31 -05:00
0459cd788a fix(spelling): corrected the spelling on the file 2026-05-08 15:03:53 -05:00
7d7d991122 fix(schema): typo in add_date 2026-05-08 15:03:33 -05:00
2721bb2a3b feat(api hits): added in api hits for monitoring 2026-05-08 15:03:03 -05:00
4424c742d2 refactor(analyitics): finished analyitics as a base 2026-05-08 15:02:34 -05:00
6d8499bfb8 ci(templates): force useage 2026-05-08 15:01:44 -05:00
9edafc9d28 feat(analytics): added in backend anaylitics 2026-05-07 10:20:50 -05:00
e9b0101095 ci(template): bug in getting the template to work correctly
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m28s
2026-05-07 09:01:15 -05:00
ca885fb01a ci(templates): added in templates for the repo to make it more easy to manage and add in new ideas
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m24s
2026-05-07 08:50:06 -05:00
edb3668548 refactor(scanner): added toasts in to make it look better
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m25s
2026-05-06 19:42:52 -05:00
87803eed43 feat(scanner): added in lanechecks 2026-05-06 19:42:22 -05:00
e61038e004 chore(release): 0.0.2-alpha.9
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m42s
Release and Build Image / release (push) Successful in 28s
2026-05-06 13:34:30 -05:00
d99449ddc4 test(scanner): lane check
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m41s
2026-05-06 13:30:58 -05:00
3552ca31f9 build(builds): changed to ip as its on the same server
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 4s
2026-05-06 12:27:20 -05:00
b578f05d64 build(release): bypass cloudflare upload limit
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 3m43s
2026-05-06 12:17:42 -05:00
4ca74de279 refactor(mobile): valildation of server after each scan
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 3m40s
2026-05-06 12:10:14 -05:00
12412536d1 refactor(scanner): finished login stuff for current routes 2026-05-06 12:09:47 -05:00
a38e2e0339 refactor(scanner): added in running number 2026-05-06 12:09:09 -05:00
8c253a90b6 chore(release): 0.0.2-alpha.8
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 4m12s
Release and Build Image / release (push) Failing after 2m19s
2026-05-06 05:08:27 -05:00
ba30281e59 feat(mobile): auth added in
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-05-06 05:07:16 -05:00
2ad78e22f1 chore(release): 0.0.2-alpha.7
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 4m3s
Release and Build Image / release (push) Failing after 2m30s
2026-05-05 19:50:58 -05:00
518c0a8c19 refactor(scanner): format changes
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-05-05 19:50:02 -05:00
cd13360cfb feat(intial auth): intial auth setup for the scanner
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-05-05 19:48:36 -05:00
4e0cf8c54c refactor(docker compose): changed to have the correct url that will be used as this is for auth 2026-05-05 14:52:36 -05:00
36995e9fb4 refactor(gp connection): added in gp ip into env if not there use static name for dns
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 8m37s
2026-05-05 13:15:52 -05:00
30ffd843c7 feat(mobile): update notifications and more error handling added
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m24s
2026-04-30 17:02:21 -05:00
bb6155c969 refactor(mobile): more look and feel work
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m17s
2026-04-28 19:49:07 -05:00
7d2f048932 feat(mobile): shadcn like and tailwind added to make things look yummy
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m21s
2026-04-27 21:23:40 -05:00
649ae1ee9f feat(mobile): new route for the ehs launcher 2026-04-27 21:22:59 -05:00
8446dbc955 feat(servers): added iowa ebm
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m45s
2026-04-26 19:51:49 -05:00
0b7318f856 fix(mobile): typo for version checking 2026-04-26 19:51:12 -05:00
bddc9aca0d refactor(mobile): moved the versioning lookup at at the mobile folder plus renamed 2026-04-26 18:33:28 -05:00
77b4533dea feat(scanner): more work on the scanner and can now scan to prod no lst right now
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m41s
2026-04-25 18:13:07 -05:00
83a542d1b7 build(scripts): changing how the relase works so it purposly builds before it trys to release
this is to help prevent errors in the build and release stuff in git
2026-04-23 07:48:13 -05:00
4855412733 chore(release): 0.0.2-alpha.6
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m20s
Release and Build Image / release (push) Successful in 14s
2026-04-23 07:24:45 -05:00
251970ec8f chore(release): 0.0.2-alpha.1
All checks were successful
Release and Build Image / release (push) Successful in 2m36s
2026-04-23 07:24:12 -05:00
f7ea5f709e chore(release): 0.0.2-alpha.0
All checks were successful
Release and Build Image / release (push) Successful in 2m51s
2026-04-23 07:24:02 -05:00
3d3c2aa964 chore(release): 0.0.1
All checks were successful
Release and Build Image / release (push) Successful in 2m39s
2026-04-23 07:23:49 -05:00
781025dca0 fix(frontend): lingering import crashed us 2026-04-23 07:23:25 -05:00
a593bb2baa chore(doc remove): removed a doc and put it in the real area for docs 2026-04-23 07:23:01 -05:00
759f96b0b6 chore(release): 0.0.1-alpha.5
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 1m36s
Release and Build Image / release (push) Failing after 38s
2026-04-23 07:11:50 -05:00
de5df2b00b chore(scripts): added in a helper to remove old stuff
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-04-23 07:10:53 -05:00
4d53af0338 feat(servers): added marked tree in to the mix 2026-04-23 07:10:27 -05:00
f7276ca2d7 feat(oidc): added in so we could use an oidc to login as well :D 2026-04-23 07:09:49 -05:00
d6328ab764 fix(gp): weird issue with db username and password
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m41s
2026-04-22 06:39:22 -05:00
a6d53f0266 refactor(sql): changed sql connection to ip:port
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m42s
this change was needed for when we run in docker so we can connect to the servers
2026-04-22 05:40:38 -05:00
7962463927 refactor(server): server updates can now only be done from a dev pc
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m54s
2026-04-21 19:01:52 -05:00
f716de1a58 chore(clean): removed bruno api a proper api doc will be added to lst later 2026-04-21 19:01:21 -05:00
88cef2a56c refactor(servers): added mcd and stp1 2026-04-21 19:00:30 -05:00
cb00addee9 feat(admin): moved server build/update to full app
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m27s
2026-04-21 07:36:04 -05:00
b832d7aa1e fix(datamart): fixes to correct how we handle activations of new features and legacy queries 2026-04-20 08:49:24 -05:00
32517d0c98 fix(inventory): changes to accruatly adjust the query and check the feature set 2026-04-20 07:25:33 -05:00
82f8369640 refactor(scanner): more basic work to get the scanner just running
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m33s
2026-04-19 17:20:57 -05:00
3734d9daac feat(lstmobile): intial scanner setup kinda working
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m7s
2026-04-17 16:47:09 -05:00
a1eeadeec4 fix(psi): refactor psi queries 2026-04-17 16:46:44 -05:00
3639c1b77c fix(logistics): purchasing monitoring was going off every 5th min instead of every 5 min 2026-04-17 14:47:23 -05:00
cfbc156517 fix(logistics): historical issue where it was being really weird 2026-04-17 08:02:44 -05:00
fb3cd85b41 fix(ocp): fixes to make sure we always hav printer.data as an array or dont do anything 2026-04-15 09:20:08 -05:00
5b1c88546f fix(datamart): if we do not have 2.0 warehousing activate we need to use legacy 2026-04-15 08:45:48 -05:00
269 changed files with 59900 additions and 1189 deletions

View File

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

View File

@@ -0,0 +1,66 @@
---
name: Bug Report
about: Report something that is broken or not working correctly
title: "[BUG] "
ref: "main"
labels:
- bug
---
# Summary
Briefly explain the issue.
---
# Steps To Reproduce
1. Go to ...
2. Click ...
3. Scan ...
4. Error occurs ...
---
# Expected Behavior
What should have happened?
---
# Actual Behavior
What actually happened instead?
---
# Severity
- [ ] Low
- [ ] Medium
- [ ] High
- [ ] Critical
---
# Environment
Example:
- Production
- Development
- Zebra Scanner
- Mobile Device
- Windows Server
- Docker
---
# Logs / Screenshots
Paste logs or upload screenshots here.
```txt
Paste logs here

View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -0,0 +1,47 @@
---
name: Enhancement
about: Improve or refine an existing feature
title: "[ENHANCEMENT] "
ref: "main"
labels:
- enhancement
---
# Existing Feature
What current feature or workflow is being improved?
Example:
- Notifications
- Scanner Login
- Release Monitor
- Printing
- Auth
---
# Proposed Improvement
Describe the improvement.
---
# Expected Benefit
Why would this improvement help?
---
# Impact
- [ ] Small
- [ ] Medium
- [ ] Large
---
# Additional Notes
Anything else worth mentioning.

View File

@@ -0,0 +1,40 @@
---
name: Feature Request
about: Suggest a brand new feature or module
title: "[FEATURE] "
ref: "main"
labels:
- feature
---
# Problem Statement
What problem are you trying to solve?
---
# Proposed Solution
Describe the feature you would like added.
---
# Alternatives Considered
Any other ideas, workarounds, or approaches?
---
# Priority
- [ ] Nice to Have
- [ ] Medium Priority
- [ ] High Priority
- [ ] Critical
---
# Additional Context
Add mockups, screenshots, examples, or notes here.

View File

@@ -12,20 +12,20 @@ jobs:
steps: steps:
- name: Checkout (local) - name: Checkout (local)
run: | run: |
git clone https://git.tuffraid.net/cowch/lst_v3.git . git clone http://10.75.9.150:3100/cowch/lst_v3.git .
git checkout ${{ gitea.sha }} git checkout ${{ gitea.sha }}
- name: Login to registry - name: Login to registry
run: echo "${{ secrets.PASSWORD }}" | docker login git.tuffraid.net -u "cowch" --password-stdin run: echo "${{ secrets.PASSWORD }}" | docker login 10.75.9.150:3100 -u "cowch" --password-stdin
- name: Build image - name: Build image
run: | run: |
docker build \ docker build \
-t git.tuffraid.net/cowch/lst_v3:latest \ -t 10.75.9.150:3100/cowch/lst_v3:latest \
-t git.tuffraid.net/cowch/lst_v3:${{ gitea.sha }} \ -t 10.75.9.150:3100/cowch/lst_v3:${{ gitea.sha }} \
. .
- name: Push - name: Push
run: | run: |
docker push git.tuffraid.net/cowch/lst_v3:latest docker push 10.75.9.150:3100/cowch/lst_v3:latest
docker push git.tuffraid.net/cowch/lst_v3:${{ gitea.sha }} docker push 10.75.9.150:3100/cowch/lst_v3:${{ gitea.sha }}

View File

@@ -14,12 +14,12 @@ jobs:
# Examples: # Examples:
# http://gitea.internal.lan:3000 # http://gitea.internal.lan:3000
# https://gitea-origin.yourdomain.local # https://gitea-origin.yourdomain.local
GITEA_INTERNAL_URL: "https://git.tuffraid.net" GITEA_INTERNAL_URL: "http://10.75.9.150:3100" #"https://git.tuffraid.net"
# Internal/origin registry host. Usually same host as above, but without protocol. # Internal/origin registry host. Usually same host as above, but without protocol.
# Example: # Example:
# gitea.internal:3000 # gitea.internal:3000
REGISTRY_HOST: "git.tuffraid.net" REGISTRY_HOST: "10.75.9.150:3100" #"git.tuffraid.net"
steps: steps:
- name: Check out repository - name: Check out repository

2
.gitignore vendored
View File

@@ -5,6 +5,7 @@ builds
.buildNumber .buildNumber
temp temp
brunoApi brunoApi
downloads
.scriptCreds .scriptCreds
node-v24.14.0-x64.msi node-v24.14.0-x64.msi
postgresql-17.9-2-windows-x64.exe postgresql-17.9-2-windows-x64.exe
@@ -148,3 +149,4 @@ dist
.yarn/install-state.gz .yarn/install-state.gz
.pnp.* .pnp.*
frontend/.tanstack/tmp/2249110e-da91fb0b1b87b6c4cc3e2c2cd25037fd

View File

@@ -1,6 +1,6 @@
{ {
"editor.defaultFormatter": "biomejs.biome", "editor.defaultFormatter": "biomejs.biome",
"workbench.colorTheme": "Default Dark+", "workbench.colorTheme": "Dark+",
"terminal.integrated.env.windows": {}, "terminal.integrated.env.windows": {},
"editor.formatOnSave": true, "editor.formatOnSave": true,
"typescript.preferences.importModuleSpecifier": "relative", "typescript.preferences.importModuleSpecifier": "relative",

View File

@@ -1,5 +1,158 @@
# All Changes to LST can be found below. # All Changes to LST can be found below.
## [0.0.2-alpha.10](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.9...v0.0.2-alpha.10) (2026-05-08)
### 🌟 Enhancements
* **analytics:** added in backend anaylitics ([9edafc9](https://git.tuffraid.net/cowch/lst_v3/commits/9edafc9d2810f339d197c10dfc6a037b3352d81f))
* **api hits:** added in api hits for monitoring ([2721bb2](https://git.tuffraid.net/cowch/lst_v3/commits/2721bb2a3bf1f829591d26a0716f74c4f7fc0c79))
* **scanner:** added in lanechecks ([87803ee](https://git.tuffraid.net/cowch/lst_v3/commits/87803eed43069b73de3f66e6524bb45da9c46334))
### 🐛 Bug fixes
* **scan user:** typo ([d8869b1](https://git.tuffraid.net/cowch/lst_v3/commits/d8869b103b80e4208b3928a370a9524ef33d25cd))
* **schema:** typo in add_date ([7d7d991](https://git.tuffraid.net/cowch/lst_v3/commits/7d7d9911223905d6767b87d2471b6607a90f1ea7))
* **spelling:** corrected the spelling on the file ([0459cd7](https://git.tuffraid.net/cowch/lst_v3/commits/0459cd788aaad6ac54a67e23f798ce5e5a437394))
### 📝 Chore
* **file:** name changes.. spelled wrong ([5fa70da](https://git.tuffraid.net/cowch/lst_v3/commits/5fa70da90ca290ee45088e9c8eb06ba48a6677af))
* **server:** removed a console log that shouldnt be there ([1dba774](https://git.tuffraid.net/cowch/lst_v3/commits/1dba774abc54bf20850c3f26d49926e86d59712d))
### 🛠️ Code Refactor
* **analyitics:** finished analyitics as a base ([4424c74](https://git.tuffraid.net/cowch/lst_v3/commits/4424c742d24dc230b2bc1782e33535184c378cf0))
* **scan:** bump in build and style update ([505d7ce](https://git.tuffraid.net/cowch/lst_v3/commits/505d7cea5d2f52fc4a3ec1edff1878be703c4034))
* **scanner:** added toasts in to make it look better ([edb3668](https://git.tuffraid.net/cowch/lst_v3/commits/edb366854825f4c24ab5d77cf88759465d067f00))
### 📝 Testing Code
* **scanusers:** added in scan users as test ([1ff5e50](https://git.tuffraid.net/cowch/lst_v3/commits/1ff5e5032f9c8bf81f972dc99d6c86ba8d3936c6))
### 📈 Project changes
* **template:** bug in getting the template to work correctly ([e9b0101](https://git.tuffraid.net/cowch/lst_v3/commits/e9b01010954624aed738cd6e4b82fccbba195cc4))
* **templates:** added in templates for the repo to make it more easy to manage and add in new ideas ([ca885fb](https://git.tuffraid.net/cowch/lst_v3/commits/ca885fb01a3c8bc22694c2e05269c43fcd4de70e))
* **templates:** force useage ([6d8499b](https://git.tuffraid.net/cowch/lst_v3/commits/6d8499bfb85f7b9131b1ec7b31a17c4256d0f0cf))
## [0.0.2-alpha.9](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.8...v0.0.2-alpha.9) (2026-05-06)
### 🛠️ Code Refactor
* **mobile:** valildation of server after each scan ([4ca74de](https://git.tuffraid.net/cowch/lst_v3/commits/4ca74de2795cea7244e38697d16afe2822164ed6))
* **scanner:** added in running number ([a38e2e0](https://git.tuffraid.net/cowch/lst_v3/commits/a38e2e033977b725538e9a9046098d94194d549e))
* **scanner:** finished login stuff for current routes ([1241253](https://git.tuffraid.net/cowch/lst_v3/commits/12412536d10981013053c39d156c6c9cb0babd11))
### 📝 Testing Code
* **scanner:** lane check ([d99449d](https://git.tuffraid.net/cowch/lst_v3/commits/d99449ddc4e2777c1b0fe9189ba0a7c01fe1dd8f))
### 📈 Project Builds
* **builds:** changed to ip as its on the same server ([3552ca3](https://git.tuffraid.net/cowch/lst_v3/commits/3552ca31f9f7b3bcbe557a145e7eb154bfdae79c))
* **release:** bypass cloudflare upload limit ([b578f05](https://git.tuffraid.net/cowch/lst_v3/commits/b578f05d6482f9b6f30febeee6ab0b708a70f68b))
## [0.0.2-alpha.8](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.7...v0.0.2-alpha.8) (2026-05-06)
### 🌟 Enhancements
* **mobile:** auth added in ([ba30281](https://git.tuffraid.net/cowch/lst_v3/commits/ba30281e59040513a036fb7413e372457d04a7c8))
## [0.0.2-alpha.7](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.6...v0.0.2-alpha.7) (2026-05-06)
### 🌟 Enhancements
* **intial auth:** intial auth setup for the scanner ([cd13360](https://git.tuffraid.net/cowch/lst_v3/commits/cd13360cfb931daca50fd7b111e1c8f8ab09a909))
* **mobile:** new route for the ehs launcher ([649ae1e](https://git.tuffraid.net/cowch/lst_v3/commits/649ae1ee9f245a9b5d308ea8a636357bf72b1e34))
* **mobile:** shadcn like and tailwind added to make things look yummy ([7d2f048](https://git.tuffraid.net/cowch/lst_v3/commits/7d2f048932b77269568149de34351840b75486e2))
* **mobile:** update notifications and more error handling added ([30ffd84](https://git.tuffraid.net/cowch/lst_v3/commits/30ffd843c725da79ed035e2d9564f60a6babcda8))
* **scanner:** more work on the scanner and can now scan to prod no lst right now ([77b4533](https://git.tuffraid.net/cowch/lst_v3/commits/77b4533dea8314fd4fb81a597995cabd041fe188))
* **servers:** added iowa ebm ([8446dbc](https://git.tuffraid.net/cowch/lst_v3/commits/8446dbc955462235b9df35c501354761661e4f6a))
### 🐛 Bug fixes
* **mobile:** typo for version checking ([0b7318f](https://git.tuffraid.net/cowch/lst_v3/commits/0b7318f8566d15414edd3cd67c89fa5346058ab0))
### 🛠️ Code Refactor
* **docker compose:** changed to have the correct url that will be used as this is for auth ([4e0cf8c](https://git.tuffraid.net/cowch/lst_v3/commits/4e0cf8c54c4dfd68edba7e733518846a47c55064))
* **gp connection:** added in gp ip into env if not there use static name for dns ([36995e9](https://git.tuffraid.net/cowch/lst_v3/commits/36995e9fb42cfa1b72c096b8860866d70b86e70c))
* **mobile:** more look and feel work ([bb6155c](https://git.tuffraid.net/cowch/lst_v3/commits/bb6155c9692220542a52664848abf0b9eee91a43))
* **mobile:** moved the versioning lookup at at the mobile folder plus renamed ([bddc9ac](https://git.tuffraid.net/cowch/lst_v3/commits/bddc9aca0d2da2b2f53dec1250276d7a076a8601))
* **scanner:** format changes ([518c0a8](https://git.tuffraid.net/cowch/lst_v3/commits/518c0a8c19a4bff0b757bbd06ca5460d3565d8bd))
### 📈 Project Builds
* **scripts:** changing how the relase works so it purposly builds before it trys to release ([83a542d](https://git.tuffraid.net/cowch/lst_v3/commits/83a542d1b7beafe394949c001917f2b25056fac2))
## [0.0.2-alpha.6](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.1...v0.0.2-alpha.6) (2026-04-23)
## [0.0.2-alpha.1](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.0...v0.0.2-alpha.1) (2026-04-23)
## [0.0.2-alpha.0](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.1...v0.0.2-alpha.0) (2026-04-23)
## [0.0.1](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.1-alpha.5...v0.0.1) (2026-04-23)
### 🐛 Bug fixes
* **frontend:** lingering import crashed us ([781025d](https://git.tuffraid.net/cowch/lst_v3/commits/781025dca00e9dd4b2ad9b283be944ed91bbc1e5))
### 📝 Chore
* **doc remove:** removed a doc and put it in the real area for docs ([a593bb2](https://git.tuffraid.net/cowch/lst_v3/commits/a593bb2baafd0166a178b80cd76dd8862f240e11))
## [0.0.1-alpha.5](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.1-alpha.4...v0.0.1-alpha.5) (2026-04-23)
### 🌟 Enhancements
* **admin:** moved server build/update to full app ([cb00add](https://git.tuffraid.net/cowch/lst_v3/commits/cb00addee96b3ecccf2694f85cb7882cac9c7e3d))
* **lstmobile:** intial scanner setup kinda working ([3734d9d](https://git.tuffraid.net/cowch/lst_v3/commits/3734d9daac143ad8fb4404c59990bc4f546f365b))
* **oidc:** added in so we could use an oidc to login as well :D ([f7276ca](https://git.tuffraid.net/cowch/lst_v3/commits/f7276ca2d722e30da65bbead23dc9bd57df25aa7))
* **servers:** added marked tree in to the mix ([4d53af0](https://git.tuffraid.net/cowch/lst_v3/commits/4d53af033876d81e0d38c148c15cb0af6f3d5bf0))
### 🐛 Bug fixes
* **datamart:** fixes to correct how we handle activations of new features and legacy queries ([b832d7a](https://git.tuffraid.net/cowch/lst_v3/commits/b832d7aa1ecd063be1bbb7e969617fc7a6376ffa))
* **datamart:** if we do not have 2.0 warehousing activate we need to use legacy ([5b1c885](https://git.tuffraid.net/cowch/lst_v3/commits/5b1c88546ff9a42dc572450fe05ad68015edb627))
* **gp:** weird issue with db username and password ([d6328ab](https://git.tuffraid.net/cowch/lst_v3/commits/d6328ab764c3626aef99727b873003384951d299))
* **inventory:** changes to accruatly adjust the query and check the feature set ([32517d0](https://git.tuffraid.net/cowch/lst_v3/commits/32517d0c98c42a0f0f60135b4a9951c4090ccd58))
* **logistics:** historical issue where it was being really weird ([cfbc156](https://git.tuffraid.net/cowch/lst_v3/commits/cfbc1565172f7c2e27f0a1593fe8e99b00d91bb7))
* **logistics:** purchasing monitoring was going off every 5th min instead of every 5 min ([3639c1b](https://git.tuffraid.net/cowch/lst_v3/commits/3639c1b77c597a94816bfedd0892f0c8980c6403))
* **ocp:** fixes to make sure we always hav printer.data as an array or dont do anything ([fb3cd85](https://git.tuffraid.net/cowch/lst_v3/commits/fb3cd85b411315cac0abd22d050ee88929754833))
* **psi:** refactor psi queries ([a1eeade](https://git.tuffraid.net/cowch/lst_v3/commits/a1eeadeec438f7c5c6d31f190fee5c22f83dc6b0))
### 📝 Chore
* **clean:** removed bruno api a proper api doc will be added to lst later ([f716de1](https://git.tuffraid.net/cowch/lst_v3/commits/f716de1a58a4a4c02d9a0a375444ceecea4a018b))
* **scripts:** added in a helper to remove old stuff ([de5df2b](https://git.tuffraid.net/cowch/lst_v3/commits/de5df2b00b1c6fe7c53d6ea075b4cf7e0fb845f9))
### 🛠️ Code Refactor
* **scanner:** more basic work to get the scanner just running ([82f8369](https://git.tuffraid.net/cowch/lst_v3/commits/82f8369640b2b0ff63dd640dc0aa0609a42c7dda))
* **servers:** added mcd and stp1 ([88cef2a](https://git.tuffraid.net/cowch/lst_v3/commits/88cef2a56c390b692866658ce519e59ffeaf4c17))
* **server:** server updates can now only be done from a dev pc ([7962463](https://git.tuffraid.net/cowch/lst_v3/commits/7962463927c4c5d2e12db9a0dd536b0f29fc65b2))
* **sql:** changed sql connection to ip:port ([a6d53f0](https://git.tuffraid.net/cowch/lst_v3/commits/a6d53f0266f1edc3f3946cd1f07d893c8a98d9c7))
## [0.0.1-alpha.4](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.1-alpha.3...v0.0.1-alpha.4) (2026-04-15) ## [0.0.1-alpha.4](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.1-alpha.3...v0.0.1-alpha.4) (2026-04-15)

View File

@@ -0,0 +1,38 @@
/**
* To be able to run this we need to set our dev pc in the .env.
* if its empty just ignore it. this will just be the double catch
*/
import { Router } from "express";
import { build, building } from "../utils/build.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const router = Router();
router.post("/release", async (_, res) => {
if (!building) {
build();
return apiReturn(res, {
success: true,
level: "info",
module: "admin",
subModule: "build",
message: `The build has been triggered see logs for progress of the current build.`,
data: [],
status: 200,
});
} else {
return apiReturn(res, {
success: false,
level: "error",
module: "admin",
subModule: "build",
message: `There is a build in progress already please check the logs for on going progress.`,
data: [],
status: 200,
});
}
});
export default router;

View File

@@ -0,0 +1,18 @@
import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import build from "./admin.build.js";
import update from "./admin.updateServer.js";
export const setupAdminRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(`${baseUrl}/api/admin/build`, requireAuth, build);
app.use(
`${baseUrl}/api/admin/build`,
requireAuth,
update,
);
// all other system should be under /api/system/*
};

View File

@@ -0,0 +1,86 @@
/**
* To be able to run this we need to set our dev pc in the .env.
* if its empty just ignore it. this will just be the double catch
*/
import { Router } from "express";
import z from "zod";
import { building } from "../utils/build.utils.js";
import { runUpdate, updating } from "../utils/deployApp.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const updateServer = z.object({
server: z.string(),
destination: z.string(),
token: z.string().min(5, "Plant tokens should be at least 5 characters long"),
});
const router = Router();
type Update = {
success: boolean;
message: string;
};
router.post("/updateServer", async (req, res) => {
try {
const validated = updateServer.parse(req.body);
if (!updating && !building) {
const update = (await runUpdate({
server: validated.server,
destination: validated.destination,
token: validated.token,
})) as Update;
return apiReturn(res, {
success: update.success,
level: update.success ? "info" : "error",
module: "admin",
subModule: "update",
message: update.message,
data: [],
status: 200,
});
} else {
return apiReturn(res, {
success: false,
level: "error",
module: "admin",
subModule: "update",
message: `${validated.server}: ${validated.token} is already being updated, or is currently building the app.`,
data: [],
status: 200,
});
}
} catch (err) {
if (err instanceof z.ZodError) {
const flattened = z.flattenError(err);
// return res.status(400).json({
// error: "Validation failed",
// details: flattened,
// });
return apiReturn(res, {
success: false,
level: "error", //connect.success ? "info" : "error",
module: "routes",
subModule: "auth",
message: "Validation failed",
data: [flattened.fieldErrors],
status: 400, //connect.success ? 200 : 400,
});
}
return apiReturn(res, {
success: false,
level: "error", //connect.success ? "info" : "error",
module: "routes",
subModule: "auth",
message: "Internal Server Error creating user",
data: [err],
status: 400, //connect.success ? 200 : 400,
});
}
});
export default router;

View File

@@ -3,7 +3,9 @@ import { fileURLToPath } from "node:url";
import { toNodeHandler } from "better-auth/node"; import { toNodeHandler } from "better-auth/node";
import express from "express"; import express from "express";
import morgan from "morgan"; import morgan from "morgan";
import { umamiConfig } from "./configs/umami.config.js";
import { createLogger } from "./logger/logger.controller.js"; import { createLogger } from "./logger/logger.controller.js";
import { routeHitMiddleware } from "./middleware/routeHit.middleware.js";
import { setupRoutes } from "./routeHandler.routes.js"; import { setupRoutes } from "./routeHandler.routes.js";
import { auth } from "./utils/auth.utils.js"; import { auth } from "./utils/auth.utils.js";
import { lstCors } from "./utils/cors.utils.js"; import { lstCors } from "./utils/cors.utils.js";
@@ -29,10 +31,27 @@ const createApp = async () => {
app.use(morgan("dev")); app.use(morgan("dev"));
app.set("trust proxy", true); app.set("trust proxy", true);
app.use(lstCors()); app.use(lstCors());
app.use(routeHitMiddleware);
app.all(`${baseUrl}/api/auth/*splat`, toNodeHandler(auth)); app.all(`${baseUrl}/api/auth/*splat`, toNodeHandler(auth));
app.use(express.json()); app.use(express.json());
setupRoutes(baseUrl, app); setupRoutes(baseUrl, app);
app.get(`${baseUrl}/api/lst-config.js`, (_, res) => {
res.type("application/javascript");
res.setHeader("Cache-Control", "no-store");
res.send(`
window.LST_CONFIG = {
appName: ${JSON.stringify(umamiConfig.appName ?? "LST")},
site: ${JSON.stringify(umamiConfig.site ?? "unknown")},
server: ${JSON.stringify(umamiConfig.server ?? "unknown")},
appVersion: ${JSON.stringify(umamiConfig.appVersion ?? "dev")},
umamiHost: ${JSON.stringify(umamiConfig.umamiHost ?? "")},
umamiWebsiteId: ${JSON.stringify(umamiConfig.umamiWebsiteId ?? "")}
};
`);
});
app.use( app.use(
`${baseUrl}/app`, `${baseUrl}/app`,
express.static(join(__dirname, "../frontend/dist")), express.static(join(__dirname, "../frontend/dist")),

View File

@@ -1,9 +1,11 @@
import type { Express } from "express"; import type { Express } from "express";
import login from "./login.route.js"; import login from "./login.route.js";
import register from "./register.route.js"; import register from "./register.route.js";
export const setupAuthRoutes = (baseUrl: string, app: Express) => { export const setupAuthRoutes = (baseUrl: string, app: Express) => {
//setup all the routes //setup all the routes
app.use(`${baseUrl}/api/authentication/login`, login); app.use(`${baseUrl}/api/authentication/login`, login);
app.use(`${baseUrl}/api/authentication/register`, register); app.use(`${baseUrl}/api/authentication/register`, register);
}; };

View File

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

View File

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

View File

@@ -0,0 +1,21 @@
export type UmamiRuntimeConfig = {
appName: string;
site: string;
server: string;
appVersion: string;
umamiHost: string;
umamiWebsiteId: string;
};
export const umamiConfig: UmamiRuntimeConfig = {
appName: process.env.APP_NAME ?? "LST",
site: process.env.URL ?? "unknown",
server: process.env.PROD_PLANT_TOKEN ?? "unknown", // could also be server name based on our setup.
appVersion: process.env.NODE_ENV ?? "dev",
umamiHost: process.env.UMAMI_HOST ?? "",
umamiWebsiteId: process.env.UMAMI_WEBSITE_ID ?? "",
};
export function isUmamiEnabled() {
return Boolean(umamiConfig.umamiHost && umamiConfig.umamiWebsiteId);
}

View File

@@ -95,7 +95,39 @@ export const runDatamartQuery = async (data: Data) => {
notify: false, notify: false,
}); });
} }
const sqlQuery = sqlQuerySelector(`datamart.${data.name}`) as SqlQuery;
const featureQ = sqlQuerySelector(`featureCheck`) as SqlQuery;
const { data: fd, error: fe } = await tryCatch(
prodQuery(featureQ.query, `Running feature check`),
);
if (fe) {
return returnFunc({
success: false,
level: "error",
module: "datamart",
subModule: "query",
message: `feature check failed`,
data: fe as any,
notify: false,
});
}
// for queries that will need to be ran on legacy until we get the plant updated need to go in here
const doubleQueries = ["inventory"];
let queryFile = "";
if (doubleQueries.includes(data.name)) {
queryFile = `datamart.${
fd.data[0].activated > 0 ? data.name : `legacy.${data.name}`
}`;
} else {
queryFile = `datamart.${data.name}`;
}
const sqlQuery = sqlQuerySelector(queryFile) as SqlQuery;
// checking if warehousing is as it will start to effect a lot of queries for plants that are not on 2.
const getDataMartInfo = datamartData.filter((x) => x.endpoint === data.name); const getDataMartInfo = datamartData.filter((x) => x.endpoint === data.name);
@@ -141,7 +173,19 @@ export const runDatamartQuery = async (data: Data) => {
case "deliveryByDateRange": case "deliveryByDateRange":
datamartQuery = datamartQuery datamartQuery = datamartQuery
.replace("[startDate]", `${data.options.startDate}`) .replace("[startDate]", `${data.options.startDate}`)
.replace("[endDate]", `${data.options.endDate}`); .replace("[endDate]", `${data.options.endDate}`)
.replace(
"--and r.ArticleHumanReadableId in ([articles]) ",
data.options.articles
? `and r.ArticleHumanReadableId in (${data.options.articles})`
: "--and r.ArticleHumanReadableId in ([articles]) ",
)
.replace(
"and DeliveredQuantity > 0",
data.options.all
? "--and DeliveredQuantity > 0"
: "and DeliveredQuantity > 0",
);
break; break;
case "customerInventory": case "customerInventory":
@@ -170,23 +214,15 @@ export const runDatamartQuery = async (data: Data) => {
"--,l.MachineLocation,l.MachineName,l.ProductionLotRunningNumber as lot", "--,l.MachineLocation,l.MachineName,l.ProductionLotRunningNumber as lot",
`${data.options.lots ? `,l.MachineLocation,l.MachineName,l.ProductionLotRunningNumber as lot` : `--,l.MachineLocation,l.MachineName,l.ProductionLotRunningNumber as lot`}`, `${data.options.lots ? `,l.MachineLocation,l.MachineName,l.ProductionLotRunningNumber as lot` : `--,l.MachineLocation,l.MachineName,l.ProductionLotRunningNumber as lot`}`,
) )
.replaceAll(
"--,l.MachineLocation,l.MachineName,l.ProductionLotRunningNumber",
`${data.options.lots ? `,l.MachineLocation,l.MachineName,l.ProductionLotRunningNumber` : `--,l.MachineLocation,l.MachineName,l.ProductionLotRunningNumber`}`,
)
.replaceAll( .replaceAll(
"--,l.WarehouseDescription,l.LaneDescription", "--,l.WarehouseDescription,l.LaneDescription",
`${data.options.locations ? `,l.WarehouseDescription,l.LaneDescription` : `--,l.WarehouseDescription,l.LaneDescription`}`, `${data.options.locations ? `,l.WarehouseDescription,l.LaneDescription` : `--,l.WarehouseDescription,l.LaneDescription`}`,
); );
// adding in a test for historical check.
if (data.options.historical) {
datamartQuery = datamartQuery
.replace(
"--,l.ProductionLotRunningNumber as lot,l.warehousehumanreadableid as warehouseId,l.WarehouseDescription as warehouseDescription,l.lanehumanreadableid as locationId,l.lanedescription as laneDescription",
",l.ProductionLotRunningNumber as lot,l.warehousehumanreadableid as warehouseId,l.WarehouseDescription as warehouseDescription,l.lanehumanreadableid as locationId,l.lanedescription as laneDescription",
)
.replace(
"--,l.ProductionLotRunningNumber,l.warehousehumanreadableid,l.WarehouseDescription,l.lanehumanreadableid,l.lanedescription",
",l.ProductionLotRunningNumber,l.warehousehumanreadableid,l.WarehouseDescription,l.lanehumanreadableid,l.lanedescription",
);
}
break; break;
case "fakeEDIUpdate": case "fakeEDIUpdate":
datamartQuery = datamartQuery.replace( datamartQuery = datamartQuery.replace(
@@ -213,16 +249,13 @@ export const runDatamartQuery = async (data: Data) => {
); );
break; break;
case "psiDeliveryData": case "psiDeliveryData":
datamartQuery = datamartQuery datamartQuery = datamartQuery
.replace("[startDate]", `${data.options.startDate}`) .replace("[startDate]", `${data.options.startDate}`)
.replace("[endDate]", `${data.options.endDate}`) .replace("[endDate]", `${data.options.endDate}`)
.replace( .replace(
"and IdArtikelVarianten in ([articles])", "[articles]",
data.options.articles data.options.articles ? `${data.options.articles}` : "[articles]",
? `and IdArtikelVarianten in (${data.options.articles})`
: "--and IdArtikelVarianten in ([articles])",
); );
break; break;
case "productionData": case "productionData":

View File

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

View File

@@ -0,0 +1,21 @@
import { integer, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
export const analytics = pgTable("analytics", {
id: uuid("id").defaultRandom().primaryKey(),
createdAt: timestamp("created_at").defaultNow().notNull(),
method: text("method").notNull(),
routePattern: text("route_pattern").notNull(),
actualPath: text("actual_path").notNull(),
statusCode: integer("status_code").notNull(),
durationMs: integer("duration_ms").notNull(),
module: text("module"),
userId: text("user_id"),
userEmail: text("user_email"),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
});

View File

@@ -0,0 +1,10 @@
import { integer, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
export const deploymentHistory = pgTable("deployment_history", {
id: uuid("id").defaultRandom().primaryKey(),
serverId: uuid("server_id"),
buildNumber: integer("build_number").notNull(),
status: text("status").notNull(), // started, success, failed
message: text("message"),
createdAt: timestamp("created_at").defaultNow(),
});

View File

@@ -0,0 +1,33 @@
import {
date,
integer,
pgTable,
text,
timestamp,
uuid,
} from "drizzle-orm/pg-core";
export const analyticsDaily = pgTable("analytics_daily", {
id: uuid("id").defaultRandom().primaryKey(),
businessDate: date("business_date").notNull(),
method: text("method").notNull(),
routePattern: text("route_pattern").notNull(),
module: text("module").notNull(),
totalHits: integer("total_hits").notNull(),
uniqueUsers: integer("unique_users").notNull(),
successCount: integer("success_count").notNull(),
errorCount: integer("error_count").notNull(),
avgDurationMs: integer("avg_duration_ms").notNull(),
maxDurationMs: integer("max_duration_ms").notNull(),
firstHitAt: timestamp("first_hit_at").notNull(),
lastHitAt: timestamp("last_hit_at").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});

View File

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

View File

@@ -0,0 +1,22 @@
import { jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type z from "zod";
export const scanLog = pgTable("scan_log", {
id: uuid("id").defaultRandom().primaryKey(),
user: text("user"),
scannerId: text("scanner_id"),
message: text("message").notNull(),
prompt: text("prompt"),
commandDescription: text("command_description"),
runningNumber: text("running_number").default("0"),
status: text("status"),
lines: jsonb("lines").default([]),
add_Date: timestamp("add_date").defaultNow(),
});
export const scanLogSchema = createSelectSchema(scanLog);
export const newScanLogSchema = createInsertSchema(scanLog);
export type Printer = z.infer<typeof scanLogSchema>;
export type NewPrinter = z.infer<typeof newScanLogSchema>;

View File

@@ -0,0 +1,40 @@
import {
boolean,
integer,
pgTable,
text,
timestamp,
uuid,
} from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type z from "zod";
export const serverData = pgTable(
"server_data",
{
server_id: uuid("id").defaultRandom().primaryKey(),
name: text("name").notNull(),
server: text("server"),
plantToken: text("plant_token").notNull().unique(),
idAddress: text("id_address"),
greatPlainsPlantCode: text("great_plains_plant_code"),
contactEmail: text("contact_email"),
contactPhone: text("contact_phone"),
active: boolean("active").default(true),
serverLoc: text("server_loc"),
lastUpdated: timestamp("last_updated").defaultNow(),
buildNumber: integer("build_number"),
isUpgrading: boolean("is_upgrading").default(false),
},
// (table) => [
// // uniqueIndex('emailUniqueIndex').on(sql`lower(${table.email})`),
// uniqueIndex("plant_token").on(table.plantToken),
// ],
);
export const serverDataSchema = createSelectSchema(serverData);
export const newServerDataSchema = createInsertSchema(serverData);
export type ServerDataSchema = z.infer<typeof serverDataSchema>;
export type NewServerData = z.infer<typeof newServerDataSchema>;

View File

@@ -1,10 +1,27 @@
import type { InferSelectModel } from "drizzle-orm"; import {
import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; boolean,
integer,
jsonb,
pgTable,
text,
timestamp,
} from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type z from "zod";
export const serverStats = pgTable("stats", { export const appStats = pgTable("app_stats", {
id: text("id").primaryKey().default("serverStats"), id: text("id").primaryKey().default("primary"),
build: integer("build").notNull().default(1), currentBuild: integer("current_build").notNull().default(1),
lastUpdate: timestamp("last_update").defaultNow(), lastBuildAt: timestamp("last_build_at"),
lastDeployAt: timestamp("last_deploy_at"),
building: boolean("building").notNull().default(false),
updating: boolean("updating").notNull().default(false),
lastUpdated: timestamp("last_updated").defaultNow(),
meta: jsonb("meta").$type<Record<string, unknown>>().default({}),
}); });
export type ServerStats = InferSelectModel<typeof serverStats>; export const appStatsSchema = createSelectSchema(appStats);
export const newAppStatsSchema = createInsertSchema(appStats, {});
export type AppStats = z.infer<typeof appStatsSchema>;
export type NewAppStats = z.infer<typeof newAppStatsSchema>;

View File

@@ -1,5 +1,6 @@
import { type Express, Router } from "express"; import { type Express, Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js"; import { requireAuth } from "../middleware/auth.middleware.js";
import restart from "./gpSqlRestart.route.js"; import restart from "./gpSqlRestart.route.js";
import start from "./gpSqlStart.route.js"; import start from "./gpSqlStart.route.js";
import stop from "./gpSqlStop.route.js"; import stop from "./gpSqlStop.route.js";

View File

@@ -13,7 +13,9 @@ let attempt = 0;
const maxAttempts = 10; const maxAttempts = 10;
export const connectGPSql = async () => { export const connectGPSql = async () => {
const serverUp = await checkHostnamePort(`USMCD1VMS011:1433`); const serverUp = await checkHostnamePort(
`${process.env.GP_SERVER ?? "usmcd1vms011"}:1433`,
);
if (!serverUp) { if (!serverUp) {
// we will try to reconnect // we will try to reconnect
connected = false; connected = false;
@@ -53,13 +55,14 @@ export const connectGPSql = async () => {
notify: false, notify: false,
}); });
} catch (error) { } catch (error) {
console.log(error);
reconnectToSql; reconnectToSql;
return returnFunc({ return returnFunc({
success: false, success: false,
level: "error", level: "error",
module: "system", module: "system",
subModule: "db", subModule: "db",
message: "Failed to connect to the prod sql server.", message: "Failed to connect to the gp sql server.",
data: [error], data: [error],
notify: false, notify: false,
}); });
@@ -118,7 +121,9 @@ export const reconnectToSql = async () => {
await new Promise((res) => setTimeout(res, delayStart)); await new Promise((res) => setTimeout(res, delayStart));
const serverUp = await checkHostnamePort(`${process.env.PROD_SERVER}:1433`); const serverUp = await checkHostnamePort(
`${process.env.GP_SERVER ?? "usmcd1vms011"}:1433`,
);
if (!serverUp) { if (!serverUp) {
delayStart = Math.min(delayStart * 2, 30000); // exponential backoff until up to 30000 delayStart = Math.min(delayStart * 2, 30000); // exponential backoff until up to 30000

View File

@@ -66,7 +66,10 @@ const historicalInvImport = async () => {
const { data: inv, error: invError } = await tryCatch( const { data: inv, error: invError } = await tryCatch(
//prodQuery(sqlQuery.query, "Inventory data"), //prodQuery(sqlQuery.query, "Inventory data"),
runDatamartQuery({ name: "inventory", options: { historical: "x" } }), runDatamartQuery({
name: "inventory",
options: { lots: "x", locations: "x" },
}),
); );
const { data: av, error: avError } = (await tryCatch( const { data: av, error: avError } = (await tryCatch(

View File

@@ -0,0 +1,83 @@
// routeHit.middleware.ts
import type { NextFunction, Request, Response } from "express";
import {
createRouteHit,
shouldIgnoreRoute,
} from "../utils/analyticRouteHits.utils.js";
export function routeHitMiddleware(
req: Request,
res: Response,
next: NextFunction,
) {
const start = performance.now();
res.on("finish", () => {
const actualPath = getActualPath(req);
if (shouldIgnoreRoute(actualPath)) {
return;
}
const durationMs = Math.round(performance.now() - start);
const routePattern = getRoutePattern(req) as string;
const module = getModuleName(req);
void createRouteHit({
method: req.method,
routePattern,
actualPath,
statusCode: res.statusCode,
durationMs,
module,
// adjust these names to your Better Auth/session shape
userId: req.user?.id ?? null,
userEmail: req.user?.email ?? null,
ipAddress: req.ip ?? null,
userAgent: req.get("user-agent") ?? null,
}).catch((err) => {
console.error("Failed to save route hit", err);
});
});
next();
}
function getActualPath(req: Request) {
return req.originalUrl.split("?")[0] ?? req.path ?? "unknown";
}
function getRoutePattern(req: Request) {
const baseUrl = req.baseUrl || "";
const routePath = req.route?.path;
if (typeof routePath === "string") {
return `${baseUrl}${routePath}`;
}
return getActualPath(req);
}
function getModuleName(req: Request) {
const path = req.originalUrl.split("?")[0];
if (path?.includes("/printers")) return "printers";
if (path?.includes("/releases")) return "releases";
if (path?.includes("/quality")) return "quality";
if (path?.includes("/scanner")) return "scanner";
if (path?.includes("/settings")) return "settings";
if (path?.includes("/users")) return "users";
if (path?.includes("/mobile")) return "mobile";
if (path?.includes("/servers")) return "servers";
if (path?.includes("/logistics")) return "servers";
if (path?.includes("/ocp")) return "ocp";
if (path?.includes("/auth")) return "auth";
if (path?.includes("/datamart")) return "datamart";
if (path?.includes("/opendock")) return "opendock";
return "unknown";
}

View File

@@ -0,0 +1,54 @@
import { eq } from "drizzle-orm";
import { Router } from "express";
import { db } from "../db/db.controller.js";
import { scanUser } from "../db/schema/scanUsers.js";
import { settings } from "../db/schema/settings.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const r = Router();
// scanners that are dedicated to specific users.
const SPECIAL_SCANNERS = [69, 98];
const buildAllowedScannerIds = (scannerCount: number) => {
const generatedIds = Array.from({ length: scannerCount }, (_, i) => i + 1);
return Array.from(new Set([...generatedIds, ...SPECIAL_SCANNERS])).sort(
(a, b) => a - b,
);
};
r.get("/", async (_, res) => {
// get the scan users and setting
const scanusers = await db.select().from(scanUser);
const scannerIdSetting = await db
.select()
.from(settings)
.where(eq(settings.name, "scannerIds"));
const usedScannerIds = scanusers.map((x) => Number(x.scannerId));
const allowedScannerIds = buildAllowedScannerIds(
Number(scannerIdSetting[0]?.value ?? 0),
);
const availableScannerIds = allowedScannerIds.filter(
(id) => !usedScannerIds.includes(id),
);
const data = availableScannerIds.map((id) => ({
label: `${id}`,
value: id,
}));
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "scanner",
message: `There are ${availableScannerIds.length} scanner id's`,
data,
status: 200,
});
});
export default r;

View File

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

View File

@@ -0,0 +1,34 @@
import { Router } from "express";
import { runProdApi } from "../utils/prodEndpoint.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const router = Router();
router.post("/", async (req, res) => {
const body = req.body;
const lane = body.lane.split("#");
console.log(lane[2]);
const laneData = await runProdApi({
method: "post",
endpoint: "/public/v1.1/Warehousing/GetWarehouseUnits",
data: [
{
laneIds: [lane[2]],
},
],
});
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "lane check",
message: `all data for lane Id: ${lane}`,
data: laneData?.data ?? [],
status: 200,
});
});
export default router;

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,40 @@
import fs from "node:fs";
import { and, eq } from "drizzle-orm";
import { Router } from "express";
import path from "path";
import { db } from "../db/db.controller.js";
import { settings } from "../db/schema/settings.schema.js";
const router = Router();
const projectRoot = path.resolve("./lstMobile"); // adjust as needed
const appJsonPath = path.join(projectRoot, "app.json");
router.get("/", async (req, res) => {
const baseUrl = `${req.protocol}://${req.get("host")}`;
const mobileSettings = await db
.select()
.from(settings)
.where(
and(
eq(settings.moduleName, "mobile"),
eq(settings.settingType, "standard"),
),
);
const raw = fs.readFileSync(appJsonPath, "utf-8");
const config = JSON.parse(raw);
const exp = config.expo;
res.json({
packageName: exp.android?.package,
versionName: exp.version,
versionCode: exp.android?.versionCode,
minSupportedVersionCode: exp?.android?.minSupportedVersionCode ?? 0,
downloadUrl: `${baseUrl}/lst/api/mobile/apk/latest`,
settings: mobileSettings,
});
});
export default router;

View File

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

View File

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

View File

@@ -62,7 +62,7 @@ export const printerSync = async () => {
}); });
} }
if (printers?.success) { if (printers?.success && Array.isArray(printers.data)) {
const ignorePrinters = ["pdf24", "standard"]; const ignorePrinters = ["pdf24", "standard"];
const validPrinters = const validPrinters =

View File

@@ -1,6 +1,7 @@
import { type Express, Router } from "express"; import { type Express, Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js"; import { requireAuth } from "../middleware/auth.middleware.js";
import { featureCheck } from "../middleware/featureActive.middleware.js"; import { featureCheck } from "../middleware/featureActive.middleware.js";
import listener from "./ocp.printer.listener.js"; import listener from "./ocp.printer.listener.js";
import update from "./ocp.printer.update.js"; import update from "./ocp.printer.update.js";

View File

@@ -1,6 +1,7 @@
import { type Express, Router } from "express"; import { type Express, Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js"; import { requireAuth } from "../middleware/auth.middleware.js";
import { featureCheck } from "../middleware/featureActive.middleware.js"; import { featureCheck } from "../middleware/featureActive.middleware.js";
import getApt from "./opendockGetRelease.route.js"; import getApt from "./opendockGetRelease.route.js";
export const setupOpendockRoutes = (baseUrl: string, app: Express) => { export const setupOpendockRoutes = (baseUrl: string, app: Express) => {

View File

@@ -1,5 +1,6 @@
import { type Express, Router } from "express"; import { type Express, Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js"; import { requireAuth } from "../middleware/auth.middleware.js";
import restart from "./prodSqlRestart.route.js"; import restart from "./prodSqlRestart.route.js";
import start from "./prodSqlStart.route.js"; import start from "./prodSqlStart.route.js";
import stop from "./prodSqlStop.route.js"; import stop from "./prodSqlStop.route.js";

View File

@@ -13,12 +13,12 @@ r.[ArticleHumanReadableId]
,ea.JournalNummer as BOL_Number ,ea.JournalNummer as BOL_Number
,[ReleaseConfirmationState] ,[ReleaseConfirmationState]
,[PlanningState] ,[PlanningState]
--,format(r.[OrderDate], 'yyyy-MM-dd HH:mm') as OrderDate ,format(r.[OrderDate], 'yyyy-MM-dd HH:mm') as OrderDate
,r.[OrderDate] --,r.[OrderDate]
--,FORMAT(r.[DeliveryDate], 'yyyy-MM-dd HH:mm') as DeliveryDate ,FORMAT(r.[DeliveryDate], 'yyyy-MM-dd HH:mm') as DeliveryDate
,r.[DeliveryDate] --,r.[DeliveryDate]
--,FORMAT(r.[LoadingDate], 'yyyy-MM-dd HH:mm') as LoadingDate ,FORMAT(r.[LoadingDate], 'yyyy-MM-dd HH:mm') as LoadingDate
,r.[LoadingDate] --,r.[LoadingDate]
,[Quantity] ,[Quantity]
,[DeliveredQuantity] ,[DeliveredQuantity]
,r.[AdditionalInformation1] ,r.[AdditionalInformation1]
@@ -66,9 +66,9 @@ ROW_NUMBER() OVER (PARTITION BY IdJournal ORDER BY add_date DESC) AS RowNum
zz.IdLieferschein = ea.IdJournal zz.IdLieferschein = ea.IdJournal
where where
--r.ArticleHumanReadableId in ([articles])
--r.ReleaseNumber = 1452 --r.ReleaseNumber = 1452
r.DeliveryDate between @StartDate AND @EndDate r.DeliveryDate between @StartDate AND @EndDate
and DeliveredQuantity > 0 and DeliveredQuantity > 0
--and r.ArticleHumanReadableId in ([articles])
--and Journalnummer = 169386 --and Journalnummer = 169386

View File

@@ -21,9 +21,6 @@ ArticleHumanReadableId as article
/** data mart include location data **/ /** data mart include location data **/
--,l.WarehouseDescription,l.LaneDescription --,l.WarehouseDescription,l.LaneDescription
/** historical section **/
--,l.ProductionLotRunningNumber as lot,l.warehousehumanreadableid as warehouseId,l.WarehouseDescription as warehouseDescription,l.lanehumanreadableid as locationId,l.lanedescription as laneDescription
,articleTypeName ,articleTypeName
FROM [warehousing].[WarehouseUnit] as l (nolock) FROM [warehousing].[WarehouseUnit] as l (nolock)
@@ -58,7 +55,4 @@ ArticleTypeName
/** data mart include location data **/ /** data mart include location data **/
--,l.WarehouseDescription,l.LaneDescription --,l.WarehouseDescription,l.LaneDescription
/** historical section **/
--,l.ProductionLotRunningNumber,l.warehousehumanreadableid,l.WarehouseDescription,l.lanehumanreadableid,l.lanedescription
order by ArticleHumanReadableId order by ArticleHumanReadableId

View File

@@ -0,0 +1,48 @@
select
x.idartikelVarianten as article,
x.ArtikelVariantenAlias as alias
--x.Lfdnr as RunningNumber,
,round(sum(EinlagerungsMengeVPKSum),2) as total_pallets
,sum(EinlagerungsMengeSum) as total_palletQTY
,round(sum(VerfuegbareMengeVPKSum),0) as available_Pallets
,sum(VerfuegbareMengeSum) as available_QTY
,sum(case when c.Description LIKE '%COA%' then GesperrteMengeVPKSum else 0 end) as coa_Pallets
,sum(case when c.Description LIKE '%COA%' then GesperrteMengeSum else 0 end) as coa_QTY
,sum(case when c.Description NOT LIKE '%COA%' or x.IdMainDefect = -1 then GesperrteMengeVPKSum else 0 end) as held_Pallets
,sum(case when c.Description NOT LIKE '%COA%' or x.IdMainDefect = -1 then GesperrteMengeSum else 0 end) as held_QTY
,sum(case when x.WarenLagerLagerTyp = 8 then VerfuegbareMengeSum else 0 end) as consignment_qty
,IdProdPlanung as lot
----,IdAdressen,
,x.AdressBez
,x.IdLagerAbteilung as locationId
,x.LagerAbteilungKurzBez as laneDescription
,x.IdWarenlager as warehouseId
,x.WarenLagerKurzBez as warehouseDescription
--,*
from [AlplaPROD_test1].dbo.[V_LagerPositionenBarcodes] (nolock) x
left join
[AlplaPROD_test1].dbo.T_EtikettenGedruckt as l(nolock) on
x.Lfdnr = l.Lfdnr AND l.Lfdnr > 1
left join
(SELECT *
FROM [AlplaPROD_test1].[dbo].[T_BlockingDefects] where Active = 1) as c
on x.IdMainDefect = c.IdBlockingDefect
/*
The data below will be controlled by the user in excell by default everything will be passed over
IdAdressen = 3
*/
where /*IdArtikelTyp = 1 and */x.IdWarenlager not in (6, 1)
group by x.idartikelVarianten, ArtikelVariantenAlias, c.Description
--,IdAdressen
,x.AdressBez
,IdProdPlanung
,x.IdLagerAbteilung
,x.LagerAbteilungKurzBez
,x.IdWarenlager
,x.WarenLagerKurzBez
--, x.Lfdnr
order by x.IdArtikelVarianten

View File

@@ -5,19 +5,75 @@ move this over to the delivery date range query once we have the shift data mapp
update the psi stuff on this as well. update the psi stuff on this as well.
**/ **/
declare @start_date nvarchar(30) = '[startDate]' --'2025-01-01' DECLARE @StartDate DATE = '[startDate]' -- 2025-1-1
declare @end_date nvarchar(30) = '[endDate]' --'2025-08-09' DECLARE @EndDate DATE = '[endDate]' -- 2025-1-31
SELECT
r.[ArticleHumanReadableId]
,[ReleaseNumber]
,h.CustomerOrderNumber
,x.CustomerLineItemNumber
,[CustomerReleaseNumber]
,[ReleaseState]
,[DeliveryState]
,ea.JournalNummer as BOL_Number
,[ReleaseConfirmationState]
,[PlanningState]
--,format(r.[OrderDate], 'yyyy-MM-dd HH:mm') as OrderDate
,r.[OrderDate]
--,FORMAT(r.[DeliveryDate], 'yyyy-MM-dd HH:mm') as DeliveryDate
,r.[DeliveryDate]
--,FORMAT(r.[LoadingDate], 'yyyy-MM-dd HH:mm') as LoadingDate
,r.[LoadingDate]
,[Quantity]
,[DeliveredQuantity]
,r.[AdditionalInformation1]
,r.[AdditionalInformation2]
,[TradeUnits]
,[LoadingUnits]
,[Trucks]
,[LoadingToleranceType]
,[SalesPrice]
,[Currency]
,[QuantityUnit]
,[SalesPriceRemark]
,r.[Remark]
,[Irradiated]
,r.[CreatedByEdi]
,[DeliveryAddressHumanReadableId]
,DeliveryAddressDescription
,[CustomerArtNo]
,[TotalPrice]
,r.[ArticleAlias]
FROM [order].[Release] (nolock) as r
select IdArtikelVarianten, left join
ArtikelVariantenBez, [order].LineItem as x on
sum(Menge) totalDelivered,
case when convert(time, upd_date) between '00:00' and '07:00' then convert(date, upd_date - 1) else convert(date, upd_date) end as ShippingDate
from dbo.V_LadePlanungenLadeAuftragAbruf (nolock) r.LineItemId = x.id
where upd_date between CONVERT(datetime, @start_date + ' 7:00') and CONVERT(datetime, @end_date + ' 7:00') left join
and IdArtikelVarianten in ([articles]) [order].Header as h on
x.HeaderId = h.id
group by IdArtikelVarianten, upd_date, --bol stuff
ArtikelVariantenBez left join
AlplaPROD_test1.dbo.V_LadePlanungenLadeAuftragAbruf (nolock) as zz
on zz.AbrufIdAuftragsAbruf = r.ReleaseNumber
left join
(select * from (SELECT
ROW_NUMBER() OVER (PARTITION BY IdJournal ORDER BY add_date DESC) AS RowNum
,*
FROM [AlplaPROD_test1].[dbo].[T_Lieferungen] (nolock)) x
where RowNum = 1) as ea on
zz.IdLieferschein = ea.IdJournal
where
r.ArticleHumanReadableId in ([articles])
--r.ReleaseNumber = 1452
and r.DeliveryDate between @StartDate AND @EndDate
--and DeliveredQuantity > 0
--and Journalnummer = 169386

View File

@@ -0,0 +1,11 @@
SELECT count(*) as activated
FROM [test1_AlplaPROD2.0_Read].[support].[FeatureActivation]
where feature in (108,7)
/*
as more features get activated and need to have this checked to include the new endpoints add here so we can check this.
108 = waste
7 = warehousing
*/

View File

@@ -45,7 +45,7 @@ export const monitorAlplaPurchase = async () => {
} }
if (purchaseMonitor[0]?.active) { if (purchaseMonitor[0]?.active) {
createCronJob("purchaseMonitor", "0 */5 * * * *", async () => { createCronJob("purchaseMonitor", "0 5 * * * *", async () => {
try { try {
const result = await prodQuery( const result = await prodQuery(
sqlQuery.query.replace( sqlQuery.query.replace(

View File

@@ -1,10 +1,11 @@
import type { Express } from "express"; import type { Express } from "express";
import { setupAdminRoutes } from "./admin/admin.routes.js";
import { setupAuthRoutes } from "./auth/auth.routes.js"; import { setupAuthRoutes } from "./auth/auth.routes.js";
// import the routes and route setups // import the routes and route setups
import { setupApiDocsRoutes } from "./configs/scaler.config.js"; import { setupApiDocsRoutes } from "./configs/scaler.config.js";
import { setupDatamartRoutes } from "./datamart/datamart.routes.js"; import { setupDatamartRoutes } from "./datamart/datamart.routes.js";
import { setupGPSqlRoutes } from "./gpSql/gpSql.routes.js"; import { setupGPSqlRoutes } from "./gpSql/gpSql.routes.js";
import { setupMobileRoutes } from "./mobile/mobile.routes.js";
import { setupNotificationRoutes } from "./notification/notification.routes.js"; import { setupNotificationRoutes } from "./notification/notification.routes.js";
import { setupOCPRoutes } from "./ocp/ocp.routes.js"; import { setupOCPRoutes } from "./ocp/ocp.routes.js";
import { setupOpendockRoutes } from "./opendock/opendock.routes.js"; import { setupOpendockRoutes } from "./opendock/opendock.routes.js";
@@ -16,6 +17,7 @@ import { setupUtilsRoutes } from "./utils/utils.routes.js";
export const setupRoutes = (baseUrl: string, app: Express) => { export const setupRoutes = (baseUrl: string, app: Express) => {
//routes that are on by default //routes that are on by default
setupSystemRoutes(baseUrl, app); setupSystemRoutes(baseUrl, app);
setupAdminRoutes(baseUrl, app);
setupApiDocsRoutes(baseUrl, app); setupApiDocsRoutes(baseUrl, app);
setupProdSqlRoutes(baseUrl, app); setupProdSqlRoutes(baseUrl, app);
setupGPSqlRoutes(baseUrl, app); setupGPSqlRoutes(baseUrl, app);
@@ -26,4 +28,5 @@ export const setupRoutes = (baseUrl: string, app: Express) => {
setupNotificationRoutes(baseUrl, app); setupNotificationRoutes(baseUrl, app);
setupOCPRoutes(baseUrl, app); setupOCPRoutes(baseUrl, app);
setupTCPRoutes(baseUrl, app); setupTCPRoutes(baseUrl, app);
setupMobileRoutes(baseUrl, app);
}; };

View File

@@ -15,8 +15,14 @@ import { opendockSocketMonitor } from "./opendock/opendockSocketMonitor.utils.js
import { connectProdSql } from "./prodSql/prodSqlConnection.controller.js"; import { connectProdSql } from "./prodSql/prodSqlConnection.controller.js";
import { monitorAlplaPurchase } from "./purchase/purchase.controller.js"; import { monitorAlplaPurchase } from "./purchase/purchase.controller.js";
import { setupSocketIORoutes } from "./socket.io/serverSetup.js"; import { setupSocketIORoutes } from "./socket.io/serverSetup.js";
import { serversChecks } from "./system/serverData.controller.js";
import { baseSettingValidationCheck } from "./system/settingsBase.controller.js"; import { baseSettingValidationCheck } from "./system/settingsBase.controller.js";
import { startTCPServer } from "./tcpServer/tcp.server.js"; import { startTCPServer } from "./tcpServer/tcp.server.js";
import {
aggregateRouteHitsForBusinessDay,
cleanupOldRouteHits,
runRouteHitAnalyticsCron,
} from "./utils/analyticRouteHits.utils.js";
import { createCronJob } from "./utils/croner.utils.js"; import { createCronJob } from "./utils/croner.utils.js";
import { sendEmail } from "./utils/sendEmail.utils.js"; import { sendEmail } from "./utils/sendEmail.utils.js";
@@ -67,9 +73,16 @@ const start = async () => {
createCronJob("logsCleanup", "0 15 5 * * *", () => dbCleanup("logs", 120)); createCronJob("logsCleanup", "0 15 5 * * *", () => dbCleanup("logs", 120));
historicalSchedule(); historicalSchedule();
createCronJob("aggregateHits", "0 0 7 * * *", async () =>
runRouteHitAnalyticsCron(),
);
createCronJob("cleanHitsUp", "0 0 7 * * *", () => cleanupOldRouteHits());
// one shots only needed to run on server startups // one shots only needed to run on server startups
createNotifications(); createNotifications();
startNotifications(); startNotifications();
serversChecks();
aggregateRouteHitsForBusinessDay();
}, 5 * 1000); }, 5 * 1000);
process.on("uncaughtException", async (err) => { process.on("uncaughtException", async (err) => {

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,205 @@
import { sql } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import {
type NewServerData,
serverData,
} from "../db/schema/serverData.schema.js";
import { createLogger } from "../logger/logger.controller.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const servers: NewServerData[] = [
{
name: "Test server 1",
server: "USMCD1VMS036",
plantToken: "test3",
idAddress: "10.193.0.56",
greatPlainsPlantCode: "00",
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
{
name: "Test server 2",
server: "USIOW1VMS036",
plantToken: "test2",
idAddress: "10.75.0.56",
greatPlainsPlantCode: "00",
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
{
name: "Lima",
server: "USLIM1VMS006",
plantToken: "uslim1",
idAddress: "10.53.0.26", // port opened 3000 2222
greatPlainsPlantCode: "50",
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
{
name: "Houston",
server: "ushou1VMS006",
plantToken: "ushou1",
idAddress: "10.195.0.26",
greatPlainsPlantCode: "20",
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
{
name: "Dayton",
server: "usday1VMS006",
plantToken: "usday1",
idAddress: "10.44.0.56", // ports opened 3000 and 2222
greatPlainsPlantCode: "80",
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
{
name: "West Bend",
server: "usweb1VMS006",
plantToken: "usweb1",
idAddress: "10.80.0.26",
greatPlainsPlantCode: "65",
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
{
name: "Jeff City",
server: "usjci1VMS006",
plantToken: "usjci",
idAddress: "10.167.0.26",
greatPlainsPlantCode: "40",
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
{
name: "Sherman",
server: "usshe1vms006",
plantToken: "usshe1",
idAddress: "10.205.0.26",
greatPlainsPlantCode: "21",
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
{
name: "McDonough",
server: "USMCD1VMS006",
plantToken: "usmcd1",
idAddress: "10.193.0.26",
greatPlainsPlantCode: "10",
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
{
name: "St. Peters",
server: "USSTP1VMS006",
plantToken: "usstp1",
idAddress: "10.37.0.26",
greatPlainsPlantCode: "45",
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
{
name: "Marked Tree",
server: "USMAR1VMS006",
plantToken: "usmar1",
idAddress: "10.206.9.26", // 3000,2222 requested REQ0236838
greatPlainsPlantCode: "90",
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
{
name: "Iowa City EBM",
server: "USIOW1VMS006",
plantToken: "usiow1",
idAddress: "10.75.0.26",
greatPlainsPlantCode: "30",
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
{
name: "Bowling Green 1",
server: "USBOW1VMS006",
plantToken: "usbow1",
idAddress: "10.25.0.26", // 3000 is open REQ0236527 2222 already open
greatPlainsPlantCode: "55",
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
{
name: "Bethlehem",
server: "USBET1VMS006",
plantToken: "usbet1",
idAddress: "10.25.0.26",
greatPlainsPlantCode: "75",
contactEmail: "",
contactPhone: "",
serverLoc: "D$\\LST_V3",
buildNumber: 1,
},
];
// notes here for when it comes time to pull in all the server address info [test1_AlplaPROD2.0_Read].[masterData].[Plant] has everything from every server :D
export const serversChecks = async () => {
const log = createLogger({ module: "system", subModule: "serverData" });
const { data, error } = await tryCatch(
db
.insert(serverData)
.values(servers)
.onConflictDoUpdate({
target: serverData.plantToken,
set: {
server: sql`excluded.server`,
name: sql`excluded.name`,
idAddress: sql`excluded."id_address"`,
greatPlainsPlantCode: sql`excluded.great_plains_plant_code`,
contactEmail: sql`excluded."contact_email"`,
contactPhone: sql`excluded.contact_phone`,
serverLoc: sql`excluded.server_loc`,
},
})
.returning(),
);
if (error) {
log.error(
{ error: error },
"There was an error when adding or updating the servers.",
);
}
if (data) {
log.info({}, "All Servers were added/updated");
}
};
// Communication from logistic network to logisticsSupportTool (for printers and scanners)
// network justification
// scanners and printers are on dhcp

View File

@@ -0,0 +1,43 @@
import { type Response, Router } from "express";
import { db } from "../db/db.controller.js";
import { serverData } from "../db/schema/serverData.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
// export const updateSetting = async (setting: Setting) => {
// // TODO: when the setting is a feature setting we will need to have it run each kill switch on the crons well just stop them and during a reset it just wont start them
// // TODO: when the setting is a system we will need to force an app restart
// // TODO: when the setting is standard we don't do anything.
// };
const r = Router();
r.get("/", async (_, res: Response) => {
const { data: sName, error: sError } = await tryCatch(
db.select().from(serverData).orderBy(serverData.name),
);
if (sError) {
return apiReturn(res, {
success: false,
level: "error",
module: "system",
subModule: "serverData",
message: `There was an error getting the servers `,
data: [sError],
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "system",
subModule: "serverData",
message: `All current servers`,
data: sName ?? [],
status: 200,
});
});
export default r;

View File

@@ -76,6 +76,16 @@ const newSettings: NewSetting[] = [
roles: ["admin"], roles: ["admin"],
seedVersion: 1, seedVersion: 1,
}, },
{
name: "mobile",
value: "0",
active: false,
description: "LST Android Mobile app",
moduleName: "mobile",
settingType: "feature",
roles: ["admin"],
seedVersion: 1,
},
// standard settings // standard settings
{ {
@@ -304,6 +314,49 @@ const newSettings: NewSetting[] = [
roles: ["admin"], roles: ["admin"],
seedVersion: 1, seedVersion: 1,
}, },
{
name: "laneCheck",
value: "0",
active: false,
description:
"Allows the driver to scan a lane and see what is in the lane and details about each pallet.",
moduleName: "mobile",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "dockScan",
value: "0",
active: false,
description:
"Enables dock door scanning, must have a dock scanner setup for this to work.",
moduleName: "mobile",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "cycleCounting",
value: "0",
active: false,
description: "Enables a cycle count to be triggered from the scanner.",
moduleName: "mobile",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "scannerIds",
value: "10",
active: false,
description:
"How many scanners ids are setup for this, there should be a lst_scanner instance created.",
moduleName: "mobile",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
]; ];
export const baseSettingValidationCheck = async () => { export const baseSettingValidationCheck = async () => {

View File

@@ -27,7 +27,7 @@ router.get("/", async (_, res) => {
? sqlServerStats?.data[0].UptimeSeconds ? sqlServerStats?.data[0].UptimeSeconds
: [], : [],
eomFGPkgSheetVersion: 1, // this is the excel file version when we have a change to the macro we want to grab this eomFGPkgSheetVersion: 1, // this is the excel file version when we have a change to the macro we want to grab this
masterMacroFile: 1, masterMacroFile: 1.1,
tcpServerOnline: isServerRunning, tcpServerOnline: isServerRunning,
sqlServerConnected: prodSql, sqlServerConnected: prodSql,
gpServerConnected: gpSql, gpServerConnected: gpSql,

View File

@@ -1,5 +1,7 @@
import type { Express } from "express"; import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js"; import { requireAuth } from "../middleware/auth.middleware.js";
import getServers from "./serverData.route.js";
import getSettings from "./settings.route.js"; import getSettings from "./settings.route.js";
import updSetting from "./settingsUpdate.route.js"; import updSetting from "./settingsUpdate.route.js";
import stats from "./stats.route.js"; import stats from "./stats.route.js";
@@ -8,6 +10,7 @@ export const setupSystemRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this //stats will be like this as we dont need to change this
app.use(`${baseUrl}/api/stats`, stats); app.use(`${baseUrl}/api/stats`, stats);
app.use(`${baseUrl}/api/settings`, getSettings); app.use(`${baseUrl}/api/settings`, getSettings);
app.use(`${baseUrl}/api/servers`, getServers);
app.use(`${baseUrl}/api/settings`, requireAuth, updSetting); app.use(`${baseUrl}/api/settings`, requireAuth, updSetting);
// all other system should be under /api/system/* // all other system should be under /api/system/*

View File

@@ -1,14 +1,21 @@
import type { Express } from "express"; import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js"; import { requireAuth } from "../middleware/auth.middleware.js";
import restart from "./tcpRestart.route.js"; import restart from "./tcpRestart.route.js";
import start from "./tcpStart.route.js"; import start from "./tcpStart.route.js";
import stop from "./tcpStop.route.js"; import stop from "./tcpStop.route.js";
export const setupTCPRoutes = (baseUrl: string, app: Express) => { export const setupTCPRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this //stats will be like this as we dont need to change this
app.use(`${baseUrl}/api/tcp/start`, requireAuth, start); app.use(`${baseUrl}/api/tcp/start`, requireAuth, start);
app.use(`${baseUrl}/api/tcp/stop`, requireAuth, stop); app.use(`${baseUrl}/api/tcp/stop`, requireAuth, stop);
app.use(`${baseUrl}/api/tcp/restart`, requireAuth, restart); app.use(
`${baseUrl}/api/tcp/restart`,
requireAuth,
restart,
);
// all other system should be under /api/system/* // all other system should be under /api/system/*
}; };

View File

@@ -0,0 +1,141 @@
import { and, count, countDistinct, gte, lt, sql } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { analytics } from "../db/schema/analytics.schema.js";
import { analyticsDaily } from "../db/schema/dailyAnalytics.schema.js";
export const ignoredRoutePrefixes = [
"/health",
"/favicon.ico",
"/socket.io",
"/lst/api/ws",
"/lst-config.js",
];
export function shouldIgnoreRoute(path: string) {
return ignoredRoutePrefixes.some((prefix) => path.startsWith(prefix));
}
type CreateRouteHitInput = {
method: string;
routePattern: string;
actualPath: string;
statusCode: number;
durationMs: number;
module?: string | null;
userId?: string | null;
userEmail?: string | null;
ipAddress?: string | null;
userAgent?: string | null;
};
export async function createRouteHit(input: CreateRouteHitInput) {
await db.insert(analytics).values(input);
}
function getPreviousBusinessDayWindow(date = new Date()) {
const end = new Date(date);
end.setHours(7, 0, 0, 0);
const start = new Date(end);
start.setDate(start.getDate() - 1);
const businessDate = start.toISOString().slice(0, 10);
return {
start,
end,
businessDate,
};
}
export async function runRouteHitAnalyticsCron(): Promise<void> {
const result = await aggregateRouteHitsForBusinessDay();
await cleanupOldRouteHits();
console.log("Route hit analytics aggregated", result);
}
export async function aggregateRouteHitsForBusinessDay() {
const { start, end, businessDate } = getPreviousBusinessDayWindow();
const rows = await db
.select({
businessDate: sql<string>`${businessDate}`,
method: analytics.method,
routePattern: analytics.routePattern,
module: sql<string>`COALESCE(${analytics.module}, 'unknown')`,
totalHits: count(),
uniqueUsers: countDistinct(analytics.userId),
successCount: sql<number>`
COUNT(*) FILTER (WHERE ${analytics.statusCode} < 400)
`,
errorCount: sql<number>`
COUNT(*) FILTER (WHERE ${analytics.statusCode} >= 400)
`,
avgDurationMs: sql<number>`
COALESCE(ROUND(AVG(${analytics.durationMs})), 0)
`,
maxDurationMs: sql<number>`
COALESCE(MAX(${analytics.durationMs}), 0)
`,
firstHitAt: sql<Date>`
COALESCE(MIN(${analytics.createdAt}), NOW())
`,
lastHitAt: sql<Date>`
COALESCE(MAX(${analytics.createdAt}), NOW())
`,
})
.from(analytics)
.where(and(gte(analytics.createdAt, start), lt(analytics.createdAt, end)))
.groupBy(
analytics.method,
analytics.routePattern,
sql`COALESCE(${analytics.module}, 'unknown')`,
);
if (rows.length === 0) {
return {
businessDate,
inserted: 0,
};
}
await db
.insert(analyticsDaily)
.values(rows)
.onConflictDoUpdate({
target: [
analyticsDaily.businessDate,
analyticsDaily.method,
analyticsDaily.routePattern,
analyticsDaily.module,
],
set: {
totalHits: sql`excluded.total_hits`,
uniqueUsers: sql`excluded.unique_users`,
successCount: sql`excluded.success_count`,
errorCount: sql`excluded.error_count`,
avgDurationMs: sql`excluded.avg_duration_ms`,
maxDurationMs: sql`excluded.max_duration_ms`,
firstHitAt: sql`excluded.first_hit_at`,
lastHitAt: sql`excluded.last_hit_at`,
updatedAt: sql`now()`,
},
});
return {
businessDate,
inserted: rows.length,
};
}
export async function cleanupOldRouteHits() {
await db
.delete(analytics)
.where(lt(analytics.createdAt, sql`now() - interval '4 days'`));
}

View File

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

View File

@@ -0,0 +1,91 @@
import { spawn } from "node:child_process";
import { createLogger } from "../logger/logger.controller.js";
import { emitToRoom } from "../socket.io/roomEmitter.socket.js";
import { updateAppStats } from "./updateAppStats.utils.js";
import { zipBuild } from "./zipper.utils.js";
export const emitBuildLog = (message: string, level = "info") => {
const payload = {
type: "build",
level,
message,
timestamp: new Date().toISOString(),
};
//console.log(`[BUILD][${level.toUpperCase()}] ${message}`);
emitToRoom("admin:build", payload as any);
if (payload.level === "info") {
log.info({ stack: payload }, payload.message);
}
// if (log) {
// log(payload);
// }
};
export let building = false;
const log = createLogger({ module: "utils", subModule: "builds" });
export const build = async () => {
const appDir = process.env.DEV_DIR ?? "";
return new Promise((resolve) => {
building = true;
updateAppStats({
lastUpdated: new Date(),
building: true,
});
emitBuildLog(`Starting build in: ${appDir}`);
const child = spawn("npm", ["run", "build"], {
cwd: appDir,
shell: true,
});
child.stdout.on("data", (data) => {
const lines = data.toString().split(/\r?\n/);
for (const line of lines) {
if (line.trim() !== "") {
emitBuildLog(line, "info");
}
}
});
child.stderr.on("data", (data) => {
const lines = data.toString().split(/\r?\n/);
for (const line of lines) {
if (line.trim() !== "") {
emitBuildLog(line, "error");
}
}
});
child.on("close", (code) => {
if (code === 0) {
emitBuildLog("Build completed successfully.", "info");
building = false;
zipBuild();
resolve(true);
} else {
building = false;
updateAppStats({
lastUpdated: new Date(),
building: false,
});
emitBuildLog(`Build failed with code ${code}`, "error");
//reject(new Error(`Build failed with code ${code}`));
}
});
child.on("error", (err) => {
building = false;
updateAppStats({
lastUpdated: new Date(),
building: false,
});
emitBuildLog(`Process error: ${err.message}`, "error");
// reject(err);
});
});
};

View File

@@ -9,6 +9,7 @@ export const allowedOrigins = [
"http://localhost:4000", "http://localhost:4000",
"http://localhost:4001", "http://localhost:4001",
"http://localhost:5500", "http://localhost:5500",
"http://localhost:8081",
"https://admin.socket.io", "https://admin.socket.io",
"https://electron-socket-io-playground.vercel.app", "https://electron-socket-io-playground.vercel.app",
`${process.env.URL}`, `${process.env.URL}`,

123
backend/utils/deployApp.ts Normal file
View File

@@ -0,0 +1,123 @@
import { spawn } from "node:child_process";
import { eq, sql } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { serverData } from "../db/schema/serverData.schema.js";
import { appStats } from "../db/schema/stats.schema.js";
//import { createLogger } from "../logger/logger.controller.js";
import { emitBuildLog } from "./build.utils.js";
import { returnFunc } from "./returnHelper.utils.js";
// const log = createLogger({ module: "utils", subModule: "deploy" });
export let updating = false;
const updateServerBuildNumber = async (token: string) => {
// get the current build
const buildNum = await db.select().from(appStats);
// update the build now
await db
.update(serverData)
.set({ buildNumber: buildNum[0]?.currentBuild, lastUpdated: sql`NOW()` })
.where(eq(serverData.plantToken, token));
};
export const runUpdate = ({
server,
destination,
token,
}: {
server: string;
destination: string;
token: string;
}) => {
return new Promise((resolve, reject) => {
updating = true;
const scriptPath = process.env.UPDATE_SCRIPT_PATH;
if (!scriptPath) {
return returnFunc({
success: true,
level: "error",
module: "utils",
subModule: "deploy",
message: "UPDATE_SCRIPT_PATH please make sure you have this set.",
data: [],
notify: true,
room: "admin",
});
}
const args = [
"-ExecutionPolicy",
"Bypass",
"-File",
scriptPath,
"-Server",
server,
"-Destination",
destination,
"-Token",
token,
"-ADM_USER",
process.env.DEV_USER ?? "",
"-ADM_PASSWORD",
process.env.DEV_PASSWORD ?? "",
"-AppDir",
process.env.DEV_DIR ?? "",
];
emitBuildLog(`Starting update for ${server}`);
const child = spawn("powershell.exe", args, {
shell: false,
});
child.stdout.on("data", (data) => {
const lines = data.toString().split(/\r?\n/);
for (const line of lines) {
if (line.trim()) {
emitBuildLog(line);
}
}
});
child.stderr.on("data", (data) => {
const lines = data.toString().split(/\r?\n/);
for (const line of lines) {
if (line.trim()) {
emitBuildLog(line, "error");
}
}
});
child.on("close", (code) => {
if (code === 0) {
emitBuildLog(`Update completed for ${server}`);
updating = false;
updateServerBuildNumber(token);
resolve({
success: true,
message: `Update completed for ${server}`,
data: [],
});
} else {
emitBuildLog(`Update failed for ${server} (code ${code})`, "error");
updating = false;
reject({
success: false,
message: `Update failed for ${server} (code ${code})`,
data: [],
});
}
});
child.on("error", (err) => {
emitBuildLog(`Process error: ${err.message}`, "error");
updating = false;
reject({
success: false,
message: `${server}: Encountered an error while processing: ${err.message} `,
data: err,
});
});
});
};

View File

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

View File

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

View File

@@ -0,0 +1,61 @@
import { isUmamiEnabled, umamiConfig } from "../configs/umami.config.js";
type TrackLstEventInput = {
eventName: string;
eventData?: Record<string, unknown>;
url?: string;
hostname?: string;
};
export async function trackLstEvent({
eventName,
eventData,
url = "/backend",
hostname = umamiConfig.server,
}: TrackLstEventInput): Promise<void> {
if (!isUmamiEnabled()) return;
try {
await fetch(`${umamiConfig.umamiHost}/api/send`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": "LST-Backend",
},
body: JSON.stringify({
type: "event",
payload: {
website: umamiConfig.umamiWebsiteId,
name: eventName,
url,
hostname,
language: "en-US",
screen: "backend",
data: {
app: umamiConfig.appName,
site: umamiConfig.site,
server: umamiConfig.server,
appVersion: umamiConfig.appVersion,
source: "backend",
...eventData,
},
},
}),
});
} catch (err) {
console.error("Failed to send Umami backend event", err);
}
}
/*
await trackLstEvent({
eventName: "label_print_completed",
url: "/backend/printers",
eventData: {
module: "printers",
printerName,
labelType,
},
});
*/

View File

@@ -0,0 +1,17 @@
import { db } from "../db/db.controller.js";
import { appStats } from "../db/schema/stats.schema.js";
export const updateAppStats = async (
data: Partial<typeof appStats.$inferInsert>,
) => {
await db
.insert(appStats)
.values({
id: "primary",
...data,
})
.onConflictDoUpdate({
target: appStats.id,
set: data,
});
};

View File

@@ -1,4 +1,5 @@
import type { Express } from "express"; import type { Express } from "express";
import getActiveJobs from "./cronerActiveJobs.route.js"; import getActiveJobs from "./cronerActiveJobs.route.js";
import jobStatusChange from "./cronerStatusChange.route.js"; import jobStatusChange from "./cronerStatusChange.route.js";
export const setupUtilsRoutes = (baseUrl: string, app: Express) => { export const setupUtilsRoutes = (baseUrl: string, app: Express) => {

View File

@@ -0,0 +1,177 @@
import fs from "node:fs";
import fsp from "node:fs/promises";
import path from "node:path";
import archiver from "archiver";
import { createLogger } from "../logger/logger.controller.js";
import { emitBuildLog } from "./build.utils.js";
import { updateAppStats } from "./updateAppStats.utils.js";
const log = createLogger({ module: "utils", subModule: "zip" });
const exists = async (target: string) => {
try {
await fsp.access(target);
return true;
} catch {
return false;
}
};
const getNextBuildNumber = async (buildNumberFile: string) => {
if (!(await exists(buildNumberFile))) {
await fsp.writeFile(buildNumberFile, "1", "utf8");
return 1;
}
const raw = await fsp.readFile(buildNumberFile, "utf8");
const current = Number.parseInt(raw.trim(), 10);
if (Number.isNaN(current) || current < 1) {
await fsp.writeFile(buildNumberFile, "1", "utf8");
return 1;
}
const next = current + 1;
await fsp.writeFile(buildNumberFile, String(next), "utf8");
// update the server with the next build number
await updateAppStats({
currentBuild: next,
lastBuildAt: new Date(),
building: true,
});
return next;
};
const cleanupOldBuilds = async (buildFolder: string, maxBuilds: number) => {
const entries = await fsp.readdir(buildFolder, { withFileTypes: true });
const zipFiles: { fullPath: string; name: string; mtimeMs: number }[] = [];
for (const entry of entries) {
if (!entry.isFile()) continue;
if (!/^LSTV3-\d+\.zip$/i.test(entry.name)) continue;
const fullPath = path.join(buildFolder, entry.name);
const stat = await fsp.stat(fullPath);
zipFiles.push({
fullPath,
name: entry.name,
mtimeMs: stat.mtimeMs,
});
}
zipFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);
const toRemove = zipFiles.slice(maxBuilds);
for (const file of toRemove) {
await fsp.rm(file.fullPath, { force: true });
emitBuildLog(`Removed old build: ${file.name}`);
}
};
export const zipBuild = async () => {
const appDir = process.env.DEV_DIR ?? "";
const maxBuilds = Number(process.env.MAX_BUILDS ?? 5);
if (!appDir) {
log.error({ notify: true }, "Forgot to add in the dev dir into the env");
return;
}
const includesFile = path.join(appDir, ".includes");
const buildNumberFile = path.join(appDir, ".buildNumber");
const buildFolder = path.join(appDir, "builds");
const tempFolder = path.join(appDir, "temp", "zip-temp");
if (!(await exists(includesFile))) {
log.error({ notify: true }, "Missing .includes file common");
return;
}
await fsp.mkdir(buildFolder, { recursive: true });
const buildNumber = await getNextBuildNumber(buildNumberFile);
const zipFileName = `LSTV3-${buildNumber}.zip`;
const zipFile = path.join(buildFolder, zipFileName);
// make the folders in case they are not created already
emitBuildLog(`Using build number: ${buildNumber}`);
if (await exists(tempFolder)) {
await fsp.rm(tempFolder, { recursive: true, force: true });
}
await fsp.mkdir(tempFolder, { recursive: true });
const includes = (await fsp.readFile(includesFile, "utf8"))
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
emitBuildLog(`Preparing zip from ${includes.length} include entries`);
for (const relPath of includes) {
const source = path.join(appDir, relPath);
const dest = path.join(tempFolder, relPath);
if (!(await exists(source))) {
emitBuildLog(`Skipping missing path: ${relPath}`, "error");
continue;
}
const stat = await fsp.stat(source);
await fsp.mkdir(path.dirname(dest), { recursive: true });
if (stat.isDirectory()) {
emitBuildLog(`Copying folder: ${relPath}`);
await fsp.cp(source, dest, { recursive: true });
} else {
emitBuildLog(`Copying file: ${relPath}`);
await fsp.copyFile(source, dest);
}
}
// if something crazy happens and we get the same build lets just reuse it
// if (await exists(zipFile)) {
// await fsp.rm(zipFile, { force: true });
// }
emitBuildLog(`Creating zip: ${zipFile}`);
await new Promise<void>((resolve, reject) => {
const output = fs.createWriteStream(zipFile);
const archive = archiver("zip", { zlib: { level: 9 } });
output.on("close", () => resolve());
output.on("error", reject);
archive.on("error", reject);
archive.pipe(output);
// zip contents of temp folder, not temp folder itself
archive.directory(tempFolder, false);
archive.finalize();
});
await fsp.rm(tempFolder, { recursive: true, force: true });
emitBuildLog(`Zip completed successfully: ${zipFile}`);
await cleanupOldBuilds(buildFolder, maxBuilds);
await updateAppStats({
lastUpdated: new Date(),
building: false,
});
return {
success: true,
buildNumber,
zipFile,
zipFileName,
};
};

View File

@@ -1,37 +0,0 @@
meta {
name: Login
type: http
seq: 1
}
post {
url: {{url}}/api/auth/sign-in/email
body: json
auth: inherit
}
headers {
Origin: http://localhost:3000
}
body:json {
{
"email": "blake.matthes@alpla.com",
"password": "nova0511"
}
}
script:post-response {
// // grab the raw Set-Cookie header
// const cookies = res.headers["set-cookie"];
// const sessionCookie = cookies[0].split(";")[0];
// // Save it as an environment variable
// bru.setEnvVar("session_cookie", sessionCookie);
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -1,35 +0,0 @@
meta {
name: Register
type: http
seq: 2
}
post {
url: {{url}}/api/authentication/register
body: json
auth: inherit
}
body:json {
{
"name":"Blake", // option when in the frontend as we will pass over as username if not added
"username": "matthes01",
"email": "blake.matthes@alpla.com",
"password": "nova0511"
}
}
script:post-response {
// // grab the raw Set-Cookie header
// const cookies = res.headers["set-cookie"];
// const sessionCookie = cookies[0].split(";")[0];
// // Save it as an environment variable
// bru.setEnvVar("session_cookie", sessionCookie);
}
settings {
encodeUrl: true
timeout: 0
}

View File

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

View File

@@ -1,16 +0,0 @@
meta {
name: getSession
type: http
seq: 3
}
get {
url: {{url}}/api/auth/get-session
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}

View File

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

View File

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

View File

@@ -1,16 +0,0 @@
meta {
name: Get queries
type: http
seq: 1
}
get {
url: {{url}}/api/datamart
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -1,24 +0,0 @@
meta {
name: Run Query
type: http
seq: 2
}
get {
url: {{url}}/api/datamart/:name?historical=x
body: none
auth: inherit
}
params:query {
historical: x
}
params:path {
name: inventory
}
settings {
encodeUrl: true
timeout: 0
}

View File

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

View File

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

View File

@@ -1,20 +0,0 @@
meta {
name: Get All notifications.
type: http
seq: 1
}
get {
url: {{url}}/api/notification
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}
docs {
Passing all as a query param will return all queries active and none active
}

View File

@@ -1,24 +0,0 @@
meta {
name: Subscribe to notification
type: http
seq: 2
}
post {
url: {{url}}/api/notification/sub
body: json
auth: inherit
}
body:json {
{
"userId":"m6AbQXFwOXoX3YKLfwWgq2LIdDqS5jqv",
"notificationId": "0399eb2a-39df-48b7-9f1c-d233cec94d2e",
"emails": ["blake.matthes@alpla.com","blake.matthes@alpla.com"]
}
}
settings {
encodeUrl: true
timeout: 0
}

View File

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

View File

@@ -1,24 +0,0 @@
meta {
name: remove sub notification
type: http
seq: 4
}
delete {
url: {{url}}/api/notification/sub
body: json
auth: inherit
}
body:json {
{
"userId":"0kHd6Kkdub4GW6rK1qa1yjWwqXtvykqT",
"notificationId": "0399eb2a-39df-48b7-9f1c-d233cec94d2e",
"emails": ["blake.mattes@alpla.com"]
}
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -1,16 +0,0 @@
meta {
name: subscriptions
type: http
seq: 5
}
get {
url: {{url}}/api/notification/sub
body: json
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -1,31 +0,0 @@
meta {
name: update notification
type: http
seq: 6
}
patch {
url: {{url}}/api/notification/:id
body: json
auth: inherit
}
params:path {
id: 0399eb2a-39df-48b7-9f1c-d233cec94d2e
}
body:json {
{
"active" : true,
"options": []
}
}
settings {
encodeUrl: true
timeout: 0
}
docs {
Passing all as a query param will return all queries active and none active
}

View File

@@ -1,24 +0,0 @@
meta {
name: update sub notification
type: http
seq: 3
}
patch {
url: {{url}}/api/notification/sub
body: json
auth: inherit
}
body:json {
{
"userId":"m6AbQXFwOXoX3YKLfwWgq2LIdDqS5jqv",
"notificationId": "0399eb2a-39df-48b7-9f1c-d233cec94d2e",
"emails": ["cowchmonkey@gmail.com"]
}
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -1,22 +0,0 @@
meta {
name: Printer Listenter
type: http
seq: 1
}
post {
url: {{url}}/api/ocp/printer/listener/line_1
body: json
auth: inherit
}
body:json {
{
"message":"xnvjdhhgsdfr"
}
}
settings {
encodeUrl: true
timeout: 0
}

View File

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

View File

@@ -1,16 +0,0 @@
meta {
name: GetApt
type: http
seq: 1
}
get {
url: {{url}}/api/opendock
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -1,16 +0,0 @@
meta {
name: Sql Start
type: http
seq: 4
}
post {
url: {{url}}/api/system/prodsql/start
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -1,16 +0,0 @@
meta {
name: Sql restart
type: http
seq: 4
}
post {
url: {{url}}/api/system/prodsql/restart
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -1,16 +0,0 @@
meta {
name: Sql stop
type: http
seq: 4
}
post {
url: {{url}}/api/system/prodsql/stop
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}

View File

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

View File

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

View File

@@ -1,20 +0,0 @@
meta {
name: reader
type: http
seq: 2
}
post {
url: https://usday1prod.alpla.net/lst/old/api/rfid/mgtevents/line3.1
body: json
auth: inherit
}
body:json {
{}
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -1,20 +0,0 @@
meta {
name: Config
type: http
seq: 2
}
get {
url: https://{{readerIp}}/cloud/config
body: none
auth: bearer
}
auth:bearer {
token: {{token}}
}
settings {
encodeUrl: true
timeout: 0
}

Some files were not shown because too many files have changed in this diff Show More