71 Commits

Author SHA1 Message Date
8dfcbc5720 chore(release): 0.0.1-alpha.2
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m29s
Release and Build Image / release (push) Successful in 17s
2026-04-08 16:13:38 -05:00
103ae77e9f build(release): docker and release corrections
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-04-08 16:12:54 -05:00
beeccc6e8d chore(release): 0.0.1-alpha.1
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
Release and Build Image / release (push) Failing after 15s
2026-04-08 15:58:21 -05:00
0880298cf5 refactor(opendock refactor on how releases are posted): this was a bug maybe just a better refactory
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-04-08 15:57:20 -05:00
34b0abac36 feat(puchase history): purhcase history changed to long running no notification 2026-04-08 15:55:25 -05:00
28c226ddbc build(agent): added westbend into the flow 2026-04-07 22:33:38 -05:00
42861cc69e feat(purchase): historical data capture for alpla purchase 2026-04-07 22:33:11 -05:00
5f3d683a13 refactor(notification): reprint - removed a console log as it shouldnt bc there 2026-04-06 16:41:39 -05:00
a17787e852 feat(notification): reprint added
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m6s
2026-04-06 16:01:06 -05:00
5865ac3b99 feat(notification): base notifcaiton sub and admin compelted
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m59s
can now sub to a notification and user can remove them selfs plus an admin can remove,updates to add
new emails are good as well
2026-04-06 12:59:30 -05:00
637de857f9 feat(user notifications): added the ability for users to sub to notifications and add multi email 2026-04-06 09:29:46 -05:00
3ecf5fb916 refactor(userprofile): changes to have the table be blank and say nothing subscribed
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m32s
later we will leave this off the profile and add it once at least one notification is subscribed
2026-04-05 20:50:27 -05:00
92ba3ef512 docs(readme): updated progress data
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m18s
2026-04-05 20:44:49 -05:00
7d6c2db89c style(notifcaion): style changes to the notificaion card and started the table
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m49s
2026-04-03 17:16:58 -05:00
74262beb65 refactor(notification): select menu looks propper now 2026-04-03 17:16:31 -05:00
f3b8dd94e5 refactor(queries): changed dev version to be 1500ms vs 5000ms 2026-04-03 17:16:02 -05:00
0059b9b850 build(changelog): reset the change log after all crap testing 2026-04-03 17:15:22 -05:00
1ad789b2b9 chore(release): 0.1.0-alpha.12
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m45s
Release and Build Image / release (push) Successful in 10s
2026-04-03 16:54:44 -05:00
079478f932 fix(typo): more dam typos 2026-04-03 16:54:29 -05:00
d6d5b451cd chore(release): 0.1.0-alpha.11
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m45s
Release and Build Image / release (push) Successful in 10s
2026-04-03 16:49:20 -05:00
76747cf917 fix(release): typo that caused errors 2026-04-03 16:49:12 -05:00
6e85991062 refactor(release): changes to only have the changelog in the release 2026-04-03 16:43:17 -05:00
98e408cb85 chore(release): 0.1.0-alpha.10
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m48s
Release and Build Image / release (push) Successful in 1m22s
2026-04-03 15:30:02 -05:00
ed052dff3c refactor(changelog): reverted back to commit-chagnelog, like more than changeset for solo dev 2026-04-03 15:29:49 -05:00
8f59bba614 chore(release): 0.1.0-alpha.9
All checks were successful
Release and Build Image / release (push) Successful in 1m52s
2026-04-03 15:22:26 -05:00
fb2c5609aa chore(release): version packages
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m46s
Release and Build Image / release (push) Successful in 1m20s
2026-04-03 13:06:52 -05:00
17aed6cb89 fix(lala): something here 2026-04-03 13:06:14 -05:00
b02b93b83f chore(release): version packages
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m50s
Release and Build Image / release (push) Successful in 1m26s
2026-04-03 12:51:52 -05:00
9ceba8b5bb fix(i suck): more learning experance 2026-04-03 12:51:11 -05:00
2c0dbf95c7 chore(release): version packages
Some checks failed
Build and Push LST Docker Image / docker (push) Successful in 1m50s
Release and Build Image / release (push) Failing after 1m22s
2026-04-03 12:44:43 -05:00
860207a60b fix(build): typo 2026-04-03 12:44:16 -05:00
5c6460012a chore(release): version packages
Some checks failed
Build and Push LST Docker Image / docker (push) Successful in 1m54s
Release and Build Image / release (push) Failing after 1m43s
2026-04-03 12:37:54 -05:00
be1d4081e0 docs(sop): added more info 2026-04-03 12:37:13 -05:00
83a94cacf3 fix(build): type in how we pushed the header over
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m20s
2026-04-03 12:33:20 -05:00
0ce3790675 chore(release): version packages
Some checks failed
Build and Push LST Docker Image / docker (push) Successful in 1m51s
Release and Build Image / release (push) Failing after 1m23s
2026-04-03 12:23:13 -05:00
5854889eb5 refactor(build): added in more info to the relase section 2026-04-03 12:22:26 -05:00
4caaf74569 chore(release): version packages
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m49s
Release and Build Image / release (push) Successful in 1m22s
2026-04-03 12:09:59 -05:00
fe889ca757 fix(build): issue with how i wrote the release token
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-04-03 12:08:57 -05:00
699c124b0e chore(release): version packages
Some checks failed
Build and Push LST Docker Image / docker (push) Successful in 1m42s
Release and Build Image / release (push) Failing after 6s
2026-04-03 11:56:40 -05:00
7d55c5f431 refactor(build): changes to the way we do release so it builds as well
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m21s
2026-04-03 11:54:41 -05:00
c4fd74fc93 chore(release): version packages
Some checks failed
Build and Push LST Docker Image / docker (push) Successful in 1m44s
Create Gitea Release / release (push) Failing after 17s
2026-04-03 11:42:52 -05:00
3775760734 fix(wrelease): forgot to save
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m18s
2026-04-03 11:41:27 -05:00
643d12ff18 refactor(build): changes to auto release when we cahnge version
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-04-03 11:40:09 -05:00
82eaa23da7 chore(release): version packages
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m57s
2026-04-03 11:18:25 -05:00
b18d1ced6d build(`build): added a personal sop to the setup until we move it
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m54s
2026-04-03 11:17:09 -05:00
69c5cf87fd fix(docker): fixes to allow an external url more easy
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 12s
when running in docker we might be using a different url thats not predefined in the cors so we want
to allow 1 more
2026-04-03 10:49:57 -05:00
1fadf0ad25 testing the docker runner
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m28s
2026-04-03 10:15:18 -05:00
beae6eb648 lots of changes with docker
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m57s
2026-04-03 09:51:52 -05:00
82ab735982 add gitea docker workflow
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-04-03 09:51:02 -05:00
dbd56c1b50 helper command set to correct drive now 2026-03-27 18:31:16 -05:00
037a473ab7 added dayton in 2026-03-27 18:31:02 -05:00
32998d417f table and query work 2026-03-27 18:30:50 -05:00
ddcb7e76a3 fixed imports on several files 2026-03-25 06:56:19 -05:00
191cb2b698 changed limas folder after migration 2026-03-25 06:56:01 -05:00
2021141967 notification added in with subs :D 2026-03-20 23:43:52 -05:00
751c8f21ab bug fixes on user 2026-03-20 09:32:17 -05:00
85073c19d2 user forms added 2026-03-19 17:44:17 -05:00
6b8d7b53d0 login form created 2026-03-18 19:14:08 -05:00
e025d0f5cc added iowa test server to the mix 2026-03-18 12:22:14 -05:00
e67e9e6d72 more logging stuff 2026-03-18 12:22:00 -05:00
2846b9cb0d logs route behind protected route and menu 2026-03-16 20:59:05 -05:00
5db2a7fe75 frontend added and socket io 2026-03-16 18:07:23 -05:00
81dc575b4f socket io stuff entered 2026-03-12 15:05:37 -05:00
bf7d765989 correction to monitor opendock activation 2026-03-11 16:23:04 -05:00
4f24fe4660 agent finished and updates servers 2026-03-10 16:41:40 -05:00
68d13b03d3 agent starting :D 2026-03-01 14:10:19 -06:00
c3379919b9 intial setting and auth intergrated 2026-02-24 15:53:58 -06:00
326c2e125c socket setup 2026-02-20 16:54:01 -06:00
880902c478 added bruno 2026-02-20 12:19:32 -06:00
100c9ff9be moved brunoapi stuff inside the app 2026-02-20 12:17:51 -06:00
a8af021621 added opendock apt check route 2026-02-20 12:17:39 -06:00
252 changed files with 36737 additions and 10806 deletions

View File

@@ -1,8 +0,0 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

View File

@@ -1,11 +0,0 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}

View File

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

View File

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

View File

@@ -0,0 +1,229 @@
name: Release and Build Image
on:
push:
tags:
- "v*"
jobs:
release:
runs-on: ubuntu-latest
env:
# Internal/origin Gitea URL. Do NOT use the Cloudflare fronted URL here.
# Examples:
# http://gitea.internal.lan:3000
# https://gitea-origin.yourdomain.local
GITEA_INTERNAL_URL: "https://git.tuffraid.net"
# Internal/origin registry host. Usually same host as above, but without protocol.
# Example:
# gitea.internal:3000
REGISTRY_HOST: "git.tuffraid.net"
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Prepare release metadata
shell: bash
run: |
set -euo pipefail
TAG="${GITHUB_REF_NAME:-${GITHUB_REF##refs/tags/}}"
VERSION="${TAG#v}"
IMAGE_NAME="${REGISTRY_HOST}/${{ gitea.repository }}"
echo "TAG=$TAG" >> "$GITHUB_ENV"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
echo "IMAGE_NAME=$IMAGE_NAME" >> "$GITHUB_ENV"
if [[ "$TAG" == *-* ]]; then
echo "PRERELEASE=true" >> "$GITHUB_ENV"
else
echo "PRERELEASE=false" >> "$GITHUB_ENV"
fi
echo "Resolved TAG=$TAG"
echo "Resolved VERSION=$VERSION"
echo "Resolved IMAGE_NAME=$IMAGE_NAME"
- name: Log in to Gitea container registry
shell: bash
env:
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
set -euo pipefail
echo "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" -u "$REGISTRY_USERNAME" --password-stdin
- name: Build Docker image
shell: bash
run: |
set -euo pipefail
docker build \
-t "$IMAGE_NAME:$TAG" \
-t "$IMAGE_NAME:latest" \
.
- name: Push version tag
shell: bash
run: |
set -euo pipefail
docker push "$IMAGE_NAME:$TAG"
- name: Push latest tag
if: ${{ !contains(env.TAG, '-') }}
shell: bash
run: |
set -euo pipefail
docker push "$IMAGE_NAME:latest"
- name: Push prerelease channel tag
if: ${{ contains(env.TAG, '-') }}
shell: bash
env:
TAG: ${{ env.TAG }}
run: |
set -euo pipefail
CHANNEL="${TAG#*-}"
CHANNEL="${CHANNEL%%.*}"
echo "Resolved prerelease channel: $CHANNEL"
docker tag "$IMAGE_NAME:$TAG" "$IMAGE_NAME:$CHANNEL"
docker push "$IMAGE_NAME:$CHANNEL"
- name: Extract matching CHANGELOG section
shell: bash
env:
VERSION: ${{ env.VERSION }}
run: |
set -euo pipefail
python3 - <<'PY'
import os
import re
from pathlib import Path
version = os.environ["VERSION"]
changelog_path = Path("CHANGELOG.md")
if not changelog_path.exists():
Path("release_body.md").write_text(f"Release {version}\n", encoding="utf-8")
raise SystemExit(0)
text = changelog_path.read_text(encoding="utf-8")
# Matches headings like:
# ## [0.1.0]
# ## 0.1.0
# ## [0.1.0-alpha.1]
pattern = re.compile(
rf"^##\s+\[?{re.escape(version)}\]?[^\n]*\n(.*?)(?=^##\s+\[?[^\n]+|\Z)",
re.MULTILINE | re.DOTALL,
)
match = pattern.search(text)
if match:
body = match.group(1).strip()
else:
body = f"Release {version}"
if not body:
body = f"Release {version}"
Path("release_body.md").write_text(body + "\n", encoding="utf-8")
print("----- release_body.md -----")
print(body)
print("---------------------------")
PY
- name: Create Gitea release
shell: bash
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GITEA_REPOSITORY: ${{ gitea.repository }}
GITEA_INTERNAL_URL: ${{ env.GITEA_INTERNAL_URL }}
TAG: ${{ env.TAG }}
PRERELEASE: ${{ env.PRERELEASE }}
run: |
set -euo pipefail
python3 - <<'PY'
import json
import os
import urllib.request
import urllib.error
from pathlib import Path
tag = os.environ["TAG"]
prerelease = os.environ["PRERELEASE"].lower() == "true"
server_url = os.environ["GITEA_INTERNAL_URL"].rstrip("/")
repo = os.environ["GITEA_REPOSITORY"]
token = os.environ["RELEASE_TOKEN"]
body = Path("release_body.md").read_text(encoding="utf-8").strip()
# Check if the release already exists for this tag
get_url = f"{server_url}/api/v1/repos/{repo}/releases/tags/{tag}"
get_req = urllib.request.Request(
get_url,
method="GET",
headers={
"Authorization": f"token {token}",
"Accept": "application/json",
"User-Agent": "lst-release-workflow/1.0",
},
)
existing_release = None
try:
with urllib.request.urlopen(get_req) as resp:
existing_release = json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as e:
if e.code != 404:
details = e.read().decode("utf-8", errors="replace")
print("Failed checking existing release:")
print(details)
raise
payload = {
"tag_name": tag,
"name": tag,
"body": body,
"draft": False,
"prerelease": prerelease,
}
data = json.dumps(payload).encode("utf-8")
if existing_release:
release_id = existing_release["id"]
url = f"{server_url}/api/v1/repos/{repo}/releases/{release_id}"
method = "PATCH"
print(f"Release already exists for tag {tag}, updating release id {release_id}")
else:
url = f"{server_url}/api/v1/repos/{repo}/releases"
method = "POST"
print(f"No release exists for tag {tag}, creating a new one")
req = urllib.request.Request(
url,
data=data,
method=method,
headers={
"Authorization": f"token {token}",
"Content-Type": "application/json",
"Accept": "application/json",
"User-Agent": "lst-release-workflow/1.0",
},
)
try:
with urllib.request.urlopen(req) as resp:
print(resp.read().decode("utf-8"))
except urllib.error.HTTPError as e:
details = e.read().decode("utf-8", errors="replace")
print("Release create/update failed:")
print(details)
raise
PY

10
.gitignore vendored
View File

@@ -1,6 +1,16 @@
# ---> Node
testFiles
builds
.includes
.buildNumber
temp
brunoApi
.scriptCreds
node-v24.14.0-x64.msi
postgresql-17.9-2-windows-x64.exe
VSCodeUserSetup-x64-1.112.0.exe
nssm.exe
# Logs
logs
*.log

View File

@@ -11,7 +11,7 @@
{ "type": "ci", "hidden": false, "section": "📈 Project changes" },
{ "type": "build", "hidden": false, "section": "📈 Project Builds" }
],
"commitUrlFormat": "https://git.tuffraid.net/cowch/lst/commits/{{hash}}",
"compareUrlFormat": "https://git.tuffraid.net/cowch/lst/compare/{{previousTag}}...{{currentTag}}",
"commitUrlFormat": "https://git.tuffraid.net/cowch/lst_v3/commits/{{hash}}",
"compareUrlFormat": "https://git.tuffraid.net/cowch/lst_v3/compare/{{previousTag}}...{{currentTag}}",
"header": "# All Changes to LST can be found below.\n"
}

View File

@@ -10,6 +10,7 @@
"\tmessage: \"${5:Failed to connect to the prod sql server.}\",",
"\tdata: ${6:[]},",
"\tnotify: ${7:false},",
"\troom: ${8:''},",
"});"
],
"description": "Insert a returnFunc template"

13
.vscode/settings.json vendored
View File

@@ -3,6 +3,8 @@
"workbench.colorTheme": "Default Dark+",
"terminal.integrated.env.windows": {},
"editor.formatOnSave": true,
"typescript.preferences.importModuleSpecifier": "relative",
"javascript.preferences.importModuleSpecifier": "relative",
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
@@ -52,14 +54,23 @@
"alpla",
"alplamart",
"alplaprod",
"alplapurchase",
"bookin",
"Datamart",
"dotenvx",
"dyco",
"intiallally",
"manadatory",
"OCME",
"onnotice",
"opendock",
"opendocks",
"ppoo",
"prodlabels"
"preseed",
"prodlabels",
"prolink",
"Skelly",
"trycatch"
],
"gitea.token": "8456def90e1c651a761a8711763d6ef225d6b2db",
"gitea.instanceURL": "https://git.tuffraid.net",

View File

@@ -1,7 +1,76 @@
# lst_v3
# All Changes to LST can be found below.
## 1.0.1
## [0.0.1-alpha.2](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.1-alpha.1...v0.0.1-alpha.2) (2026-04-08)
### Patch Changes
- cf18e94: core stuff
### 📈 Project Builds
* **release:** docker and release corrections ([103ae77](https://git.tuffraid.net/cowch/lst_v3/commits/103ae77e9f82fc008a8ae143b6feccc3ce802f8c))
## [0.0.1-alpha.1](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.1-alpha.0...v0.0.1-alpha.1) (2026-04-08)
* **notifcaion:** style changes to the notificaion card and started the table ([7d6c2db](https://git.tuffraid.net/cowch/lst_v3/commits/7d6c2db89cae1f137f126f5814dccd373f7ccb76))
### 🌟 Enhancements
* **notification:** base notifcaiton sub and admin compelted ([5865ac3](https://git.tuffraid.net/cowch/lst_v3/commits/5865ac3b99d60005c4245740369b0e0789c8fbbd))
* **notification:** reprint added ([a17787e](https://git.tuffraid.net/cowch/lst_v3/commits/a17787e85217f1fa4a5e5389e29c33ec09c286c5))
* **puchase history:** purhcase history changed to long running no notification ([34b0aba](https://git.tuffraid.net/cowch/lst_v3/commits/34b0abac36f645d0fe5f508881ddbef81ff04b7c))
* **purchase:** historical data capture for alpla purchase ([42861cc](https://git.tuffraid.net/cowch/lst_v3/commits/42861cc69e8d4aba5a9670aaed55417efda2b505))
* **user notifications:** added the ability for users to sub to notifications and add multi email ([637de85](https://git.tuffraid.net/cowch/lst_v3/commits/637de857f99499a41f7175181523f5d809d95d7e))
### 🐛 Bug fixes
* **build:** issue with how i wrote the release token ([fe889ca](https://git.tuffraid.net/cowch/lst_v3/commits/fe889ca75731af08c42ec714b7f2abf17cd1ee40))
* **build:** type in how we pushed the header over ([83a94ca](https://git.tuffraid.net/cowch/lst_v3/commits/83a94cacf3fc87287cdc0c0cc861b339e72e4b83))
* **build:** typo ([860207a](https://git.tuffraid.net/cowch/lst_v3/commits/860207a60b6e04b15736cba631be6c7eab74d020))
* **i suck:** more learning experance ([9ceba8b](https://git.tuffraid.net/cowch/lst_v3/commits/9ceba8b5bba17959f27b16b28f50a83c044863fb))
* **lala:** something here ([17aed6c](https://git.tuffraid.net/cowch/lst_v3/commits/17aed6cb89f8220570f6c66f78dba6bb202c1aaa))
* **release:** typo that caused errors ([76747cf](https://git.tuffraid.net/cowch/lst_v3/commits/76747cf91738bd0d0530afcf7b4f51f0db11ca98))
* **typo:** more dam typos ([079478f](https://git.tuffraid.net/cowch/lst_v3/commits/079478f93217dea31c9a1e8ffed85d2381a6977d))
* **wrelease:** forgot to save ([3775760](https://git.tuffraid.net/cowch/lst_v3/commits/377576073449e95d315defb913dc317759cc3f43))
### 📝 Chore
* **release:** 0.1.0-alpha.10 ([98e408c](https://git.tuffraid.net/cowch/lst_v3/commits/98e408cb8577da18e24821b55474198439434f3e))
* **release:** 0.1.0-alpha.11 ([d6d5b45](https://git.tuffraid.net/cowch/lst_v3/commits/d6d5b451cd9aeba642ef94654ca20f4acd0b827c))
* **release:** 0.1.0-alpha.12 ([1ad789b](https://git.tuffraid.net/cowch/lst_v3/commits/1ad789b2b91a20a2f5a8dc9e6f39af2e19ec9cdc))
* **release:** 0.1.0-alpha.9 ([8f59bba](https://git.tuffraid.net/cowch/lst_v3/commits/8f59bba614a8eaa3105bb56f0db36013d5e68485))
* **release:** version packages ([fb2c560](https://git.tuffraid.net/cowch/lst_v3/commits/fb2c5609aa12ea7823783c364d5bd029c48a64bd))
* **release:** version packages ([b02b93b](https://git.tuffraid.net/cowch/lst_v3/commits/b02b93b83f488fbcee6d24db080ad0d1fe1c5f59))
* **release:** version packages ([2c0dbf9](https://git.tuffraid.net/cowch/lst_v3/commits/2c0dbf95c7b8dfd2c98b476d3f44bc8929668c88))
* **release:** version packages ([5c64600](https://git.tuffraid.net/cowch/lst_v3/commits/5c6460012aa70d336fbc9702240b4f19262a6b41))
* **release:** version packages ([0ce3790](https://git.tuffraid.net/cowch/lst_v3/commits/0ce3790675bc408762eafe76cbd5ab496fd06e73))
* **release:** version packages ([4caaf74](https://git.tuffraid.net/cowch/lst_v3/commits/4caaf745693d4df847aefd3721ac5d0ae792114a))
* **release:** version packages ([699c124](https://git.tuffraid.net/cowch/lst_v3/commits/699c124b0efba8282e436210619504bda8878e90))
* **release:** version packages ([c4fd74f](https://git.tuffraid.net/cowch/lst_v3/commits/c4fd74fc93226cffd9e39602f507a05cd8ea628b))
### 📚 Documentation
* **readme:** updated progress data ([92ba3ef](https://git.tuffraid.net/cowch/lst_v3/commits/92ba3ef5121afd0d82d4f40a5a985e1fdc081011))
* **sop:** added more info ([be1d408](https://git.tuffraid.net/cowch/lst_v3/commits/be1d4081e07b0982b355a270b7850a852a4398f5))
### 🛠️ Code Refactor
* **build:** added in more info to the relase section ([5854889](https://git.tuffraid.net/cowch/lst_v3/commits/5854889eb5398feebda50a5d256ce7aec39ce112))
* **build:** changes to auto release when we cahnge version ([643d12f](https://git.tuffraid.net/cowch/lst_v3/commits/643d12ff182827e724e1569a583bd625a0d1dd0c))
* **build:** changes to the way we do release so it builds as well ([7d55c5f](https://git.tuffraid.net/cowch/lst_v3/commits/7d55c5f43173edb48d8709adcb972b7d8fbc3ebd))
* **changelog:** reverted back to commit-chagnelog, like more than changeset for solo dev ([ed052df](https://git.tuffraid.net/cowch/lst_v3/commits/ed052dff3c81a7064660a7d25685e0505065252c))
* **notification:** reprint - removed a console log as it shouldnt bc there ([5f3d683](https://git.tuffraid.net/cowch/lst_v3/commits/5f3d683a13c831229674166cced699e373131316))
* **notification:** select menu looks propper now ([74262be](https://git.tuffraid.net/cowch/lst_v3/commits/74262beb6596ddc971971cc9214a2688accf3a8e))
* **opendock refactor on how releases are posted:** this was a bug maybe just a better refactory ([0880298](https://git.tuffraid.net/cowch/lst_v3/commits/0880298cf53d83e487c706e73854e0874ae2d9da))
* **queries:** changed dev version to be 1500ms vs 5000ms ([f3b8dd9](https://git.tuffraid.net/cowch/lst_v3/commits/f3b8dd94e5ebae0cc4dd0a2689a19051942e94b8))
* **release:** changes to only have the changelog in the release ([6e85991](https://git.tuffraid.net/cowch/lst_v3/commits/6e8599106298ed13febd069d6fda8b354efb5b7b))
* **userprofile:** changes to have the table be blank and say nothing subscribed ([3ecf5fb](https://git.tuffraid.net/cowch/lst_v3/commits/3ecf5fb916d5dc1b1ffb224e2142d94f7a9cb126))
### 📈 Project Builds
* **agent:** added westbend into the flow ([28c226d](https://git.tuffraid.net/cowch/lst_v3/commits/28c226ddbc37ab85cd6a9a6aec091def3e5623d6))
* **changelog:** reset the change log after all crap testing ([0059b9b](https://git.tuffraid.net/cowch/lst_v3/commits/0059b9b850c9647695a3fecaf5927c2e3ee7b192))

View File

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

View File

@@ -7,7 +7,7 @@
Quick summary of current rewrite/migration goal.
- **Phase:** Backend rewrite
- **Last updated:** 2024-05-01
- **Last updated:** 2026-04-06
---
@@ -16,9 +16,9 @@ Quick summary of current rewrite/migration goal.
| Feature | Description | Status |
|----------|--------------|--------|
| User Authentication | ~~Login~~, ~~Signup~~, API Key | 🟨 In Progress |
| User Profile | Edit profile, upload avatar | ⏳ Not Started |
| User Profile | ~~Edit profile~~, upload avatar | 🟨 In Progress |
| User Admin | Edit user, create user, remove user, alplaprod user integration | ⏳ Not Started |
| Notifications | Subscribe, Create, Update, Remove, Manual Trigger | ⏳ Not Started |
| Notifications | ~~Subscribe~~, ~~Create~~, ~~Update~~, ~~~~Remove~~, Manual Trigger | 🟨 In Progress |
| Datamart | Create, Update, Run, Deactivate | 🔧 In Progress |
| Frontend | Analytics and charts | ⏳ Not Started |
| Docs | Instructions and trouble shooting | ⏳ Not Started |
@@ -44,7 +44,7 @@ _Status legend:_
How to run the current version of the app.
```bash
git clone https://github.com/youruser/yourrepo.git
cd yourrepo
git clone https://git.tuffraid.net/cowch/lst_v3.git
cd lst_v3
npm install
npm run dev

View File

@@ -1,3 +1,5 @@
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { toNodeHandler } from "better-auth/node";
import express from "express";
import morgan from "morgan";
@@ -20,15 +22,34 @@ const createApp = async () => {
baseUrl = "/lst";
}
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// well leave this active so we can monitor it to validate
app.use(morgan("tiny"));
app.use(morgan("dev"));
app.set("trust proxy", true);
app.all(`${baseUrl}api/auth/*splat`, toNodeHandler(auth));
app.use(express.json());
app.use(lstCors());
app.all(`${baseUrl}/api/auth/*splat`, toNodeHandler(auth));
app.use(express.json());
setupRoutes(baseUrl, app);
log.info("Express app created");
app.use(
`${baseUrl}/app`,
express.static(join(__dirname, "../frontend/dist")),
);
app.get(`${baseUrl}/app/*splat`, (_, res) => {
res.sendFile(join(__dirname, "../frontend/dist/index.html"));
});
app.all("*foo", (_, res) => {
res.status(400).json({
message:
"You have encountered a route that dose not exist, please check the url and try again",
});
});
log.info("Lst app created");
return { app, baseUrl };
};

View File

@@ -54,7 +54,8 @@ const signin = z.union([
const r = Router();
r.post("/", async (req, res) => {
let login: unknown;
let login: unknown | any;
try {
const validated = signin.parse(req.body);
if ("email" in validated) {
@@ -92,6 +93,27 @@ r.post("/", async (req, res) => {
password: validated.password,
},
headers: fromNodeHeaders(req.headers),
asResponse: true,
});
if (login.status === 401) {
return apiReturn(res, {
success: false,
level: "error", //connect.success ? "info" : "error",
module: "routes",
subModule: "auth",
message: `Incorrect username or password please try again`,
data: [],
status: 401, //connect.success ? 200 : 400,
});
}
login.headers.forEach((value: string, key: string) => {
if (key.toLowerCase() === "set-cookie") {
res.append("set-cookie", value);
} else {
res.setHeader(key, value);
}
});
}

View File

@@ -1,5 +1,5 @@
import { APIError } from "better-auth";
import { count, sql } from "drizzle-orm";
import { count, eq, sql } from "drizzle-orm";
import { Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
@@ -58,7 +58,10 @@ r.post("/", async (req, res) => {
// if we have no users yet lets make this new one the admin
if (userCount === 0) {
// make this user an admin
await db.update(user).set({ role: "admin", updatedAt: sql`NOW()` });
await db
.update(user)
.set({ role: "admin", updatedAt: sql`NOW()` })
.where(eq(user.id, newUser.user.id));
}
apiReturn(res, {
@@ -78,7 +81,7 @@ r.post("/", async (req, res) => {
// details: flattened,
// });
apiReturn(res, {
return apiReturn(res, {
success: false,
level: "error", //connect.success ? "info" : "error",
module: "routes",
@@ -90,7 +93,7 @@ r.post("/", async (req, res) => {
}
if (err instanceof APIError) {
apiReturn(res, {
return apiReturn(res, {
success: false,
level: "error", //connect.success ? "info" : "error",
module: "routes",
@@ -101,7 +104,7 @@ r.post("/", async (req, res) => {
});
}
apiReturn(res, {
return apiReturn(res, {
success: false,
level: "error", //connect.success ? "info" : "error",
module: "routes",

View File

@@ -12,6 +12,7 @@ import type { OpenAPIV3_1 } from "openapi-types";
import { cronerActiveJobs } from "../scaler/cronerActiveJobs.spec.js";
import { cronerStatusChange } from "../scaler/cronerStatusChange.spec.js";
import { prodLoginSpec } from "../scaler/login.spec.js";
import { openDockApt } from "../scaler/opendockGetRelease.spec.js";
import { prodRestartSpec } from "../scaler/prodSqlRestart.spec.js";
import { prodStartSpec } from "../scaler/prodSqlStart.spec.js";
import { prodStopSpec } from "../scaler/prodSqlStop.spec.js";
@@ -33,6 +34,7 @@ export const openApiBase: OpenAPIV3_1.Document = {
description: "Development server",
},
],
components: {
securitySchemes: {
bearerAuth: {
@@ -87,6 +89,10 @@ export const openApiBase: OpenAPIV3_1.Document = {
name: "Utils",
description: "All routes related to the utilities on the server",
},
{
name: "Open Dock",
description: "All routes related to the opendock on the server",
},
// { name: "TMS", description: "TMS integration" },
],
paths: {}, // Will be populated
@@ -121,6 +127,7 @@ export const setupApiDocsRoutes = (baseUrl: string, app: Express) => {
//...mergedDatamart,
...cronerActiveJobs,
...cronerStatusChange,
...openDockApt,
// Add more specs here as you build features
},
@@ -134,7 +141,9 @@ export const setupApiDocsRoutes = (baseUrl: string, app: Express) => {
apiReference({
url: `${baseUrl}/api/docs.json`,
theme: "purple",
darkMode: true,
persistAuth: true,
authentication: {
securitySchemes: {
httpBasic: {

View File

@@ -0,0 +1,38 @@
import {
integer,
jsonb,
pgTable,
text,
timestamp,
uuid,
} from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type { z } from "zod";
export const alplaPurchaseHistory = pgTable("alpla_purchase_history", {
id: uuid("id").defaultRandom().primaryKey(),
apo: integer("apo"),
revision: integer("revision"),
confirmed: integer("confirmed"),
status: integer("status"),
statusText: text("status_text"),
journalNum: integer("journal_num"),
add_date: timestamp("add_date").defaultNow(),
add_user: text("add_user"),
upd_user: text("upd_user"),
upd_date: timestamp("upd_date").defaultNow(),
remark: text("remark"),
approvedStatus: text("approved_status").default("pending"),
position: jsonb("position").default([]),
createdAt: timestamp("created_at").defaultNow(),
});
export const alplaPurchaseHistorySchema =
createSelectSchema(alplaPurchaseHistory);
export const newAlplaPurchaseHistorySchema =
createInsertSchema(alplaPurchaseHistory);
export type AlplaPurchaseHistory = z.infer<typeof alplaPurchaseHistorySchema>;
export type NewAlplaPurchaseHistory = z.infer<
typeof newAlplaPurchaseHistorySchema
>;

View File

@@ -0,0 +1,29 @@
import {
boolean,
jsonb,
pgTable,
text,
uniqueIndex,
uuid,
} from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type z from "zod";
export const notifications = pgTable(
"notifications",
{
id: uuid("id").defaultRandom().primaryKey(),
name: text("name").notNull(),
description: text("description").notNull(),
active: boolean("active").default(false),
interval: text("interval").default("5"),
options: jsonb("options").default([]),
},
(table) => [uniqueIndex("notify_name").on(table.name)],
);
export const notificationSchema = createSelectSchema(notifications);
export const newNotificationSchema = createInsertSchema(notifications);
export type Notification = z.infer<typeof notificationSchema>;
export type NewNotification = z.infer<typeof newNotificationSchema>;

View File

@@ -0,0 +1,30 @@
import { pgTable, text, unique, uuid } from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type z from "zod";
import { user } from "./auth.schema.js";
import { notifications } from "./notifications.schema.js";
export const notificationSub = pgTable(
"notification_sub",
{
id: uuid("id").defaultRandom().primaryKey(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
notificationId: uuid("notification_id")
.notNull()
.references(() => notifications.id, { onDelete: "cascade" }),
emails: text("emails").array().default([]),
},
(table) => ({
userNotificationUnique: unique(
"notification_sub_user_notification_unique",
).on(table.userId, table.notificationId),
}),
);
export const notificationSubSchema = createSelectSchema(notificationSub);
export const newNotificationSubSchema = createInsertSchema(notificationSub);
export type NotificationSub = z.infer<typeof notificationSubSchema>;
export type NewNotificationSub = z.infer<typeof newNotificationSubSchema>;

View File

@@ -1,4 +1,5 @@
import {
index,
integer,
jsonb,
pgTable,
@@ -9,14 +10,23 @@ import {
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type { z } from "zod";
export const opendockApt = pgTable("opendock_apt", {
id: uuid("id").defaultRandom().primaryKey(),
release: integer("release").unique(),
openDockAptId: text("open_dock_apt_id").notNull(),
appointment: jsonb("appointment").default([]),
upd_date: timestamp("upd_date").defaultNow(),
createdAt: timestamp("created_at").defaultNow(),
});
export const opendockApt = pgTable(
"opendock_apt",
{
id: uuid("id").defaultRandom().primaryKey(),
release: integer("release").notNull().unique(),
openDockAptId: text("open_dock_apt_id").notNull(),
appointment: jsonb("appointment").notNull().default([]),
upd_date: timestamp("upd_date").notNull().defaultNow(),
createdAt: timestamp("created_at").notNull().defaultNow(),
},
(table) => ({
releaseIdx: index("opendock_apt_release_idx").on(table.release),
openDockAptIdIdx: index("opendock_apt_opendock_id_idx").on(
table.openDockAptId,
),
}),
);
export const opendockAptSchema = createSelectSchema(opendockApt);
export const newOpendockAptSchema = createInsertSchema(opendockApt);

View File

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

View File

@@ -1,5 +1,6 @@
import {
boolean,
integer,
jsonb,
pgEnum,
pgTable,
@@ -13,9 +14,9 @@ import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import { z } from "zod";
export const settingType = pgEnum("setting_type", [
"feature",
"system",
"standard",
"feature", // when changed deals with triggering the croner related to this
"system", // when changed fires a system restart but this should be rare and all these settings should be in the env
"standard", // will be effected by the next process, either croner or manual trigger
]);
export const settings = pgTable(
@@ -27,8 +28,9 @@ export const settings = pgTable(
description: text("description"),
moduleName: text("moduleName"), // what part of lst dose it belong to this is used to split the settings out later
active: boolean("active").default(true),
roles: jsonb("roles").notNull().default(["systemAdmin"]), // role or roles to see this goes along with the moduleName, need to have a x role in module to see this setting.
roles: jsonb("roles").$type<string[]>().notNull().default(["systemAdmin"]), // role or roles to see this goes along with the moduleName, need to have a x role in module to see this setting.
settingType: settingType(),
seedVersion: integer("seed_version").default(1), // this is intended for if we want to update the settings.
add_User: text("add_User").default("LST_System").notNull(),
add_Date: timestamp("add_Date").defaultNow(),
upd_user: text("upd_User").default("LST_System").notNull(),

View File

@@ -0,0 +1,10 @@
import type { InferSelectModel } from "drizzle-orm";
import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core";
export const serverStats = pgTable("stats", {
id: text("id").primaryKey().default("serverStats"),
build: integer("build").notNull().default(1),
lastUpdate: timestamp("last_update").defaultNow(),
});
export type ServerStats = InferSelectModel<typeof serverStats>;

View File

@@ -1,7 +1,9 @@
import { Writable } from "node:stream";
import pino, { type Logger } from "pino";
import { db } from "../db/db.controller.js";
import { logs } from "../db/schema/logs.schema.js";
import { emitToRoom } from "../socket.io/roomEmitter.socket.js";
import { tryCatch } from "../utils/trycatch.utils.js";
//import build from "pino-abstract-transport";
@@ -26,20 +28,27 @@ const dbStream = new Writable({
const levelName = pinoLogLevels[obj.level] || "unknown";
const res = await tryCatch(
db.insert(logs).values({
level: levelName,
module: obj?.module?.toLowerCase(),
subModule: obj?.subModule?.toLowerCase(),
hostname: obj?.hostname?.toLowerCase(),
message: obj.msg,
stack: obj?.stack,
}),
db
.insert(logs)
.values({
level: levelName,
module: obj?.module?.toLowerCase(),
subModule: obj?.subModule?.toLowerCase(),
hostname: obj?.hostname?.toLowerCase(),
message: obj.msg,
stack: obj?.stack,
})
.returning(),
);
if (res.error) {
console.error(res.error);
}
if (obj.room) {
emitToRoom(obj.room, res.data ? res.data[0] : obj);
}
emitToRoom("logs", res.data ? res.data[0] : obj);
callback();
} catch (err) {
console.error("DB log insert error:", err);
@@ -48,31 +57,34 @@ const dbStream = new Writable({
},
});
// ✅ Multistream setup
const streams = [
{
stream: pino.transport({
target: "pino-pretty",
options: {
colorize: true,
singleLine: true,
},
}),
},
{
level: "info",
stream: dbStream,
},
];
const rootLogger: Logger = pino(
{
level: logLevel,
redact: { paths: ["email", "password"], remove: true },
},
pino.multistream(streams),
pino.multistream([
{
level: logLevel,
stream: pino.transport({
target: "pino-pretty",
options: {
colorize: true,
singleLine: true,
},
}),
},
{
level: logLevel,
stream: dbStream,
},
]),
);
/**
*
*
* example data to put in as a reference
* rooms logs | labels | etc
*/
export const createLogger = (bindings: Record<string, unknown>): Logger => {
return rootLogger.child(bindings);
};

View File

@@ -2,27 +2,55 @@ import { fromNodeHeaders } from "better-auth/node";
import type { NextFunction, Request, Response } from "express";
import { auth } from "../utils/auth.utils.js";
declare global {
namespace Express {
interface Request {
user?: {
id: string;
email?: string;
roles?: string | null | undefined; //Record<string, string[]>;
username?: string | null | undefined;
};
}
}
}
// function toWebHeaders(nodeHeaders: Request["headers"]): Headers {
// const h = new Headers();
// for (const [key, value] of Object.entries(nodeHeaders)) {
// if (Array.isArray(value)) {
// value.forEach((v) => h.append(key, v));
// } else if (value !== undefined) {
// h.set(key, value);
// }
// }
// return h;
// }
export const requireAuth = async (
req: Request,
res: Response,
next: NextFunction,
) => {
// TODO: add the real auth stuff in later.
try {
const session = await auth.api.getSession({
headers: fromNodeHeaders(req.headers),
//query: { disableCookieCache: true },
});
if (!session) {
//return res.status(401).json({ error: "Unauthorized" });
console.info("not auth of course");
return res.status(401).json({ error: "Unauthorized" });
}
// attach session to request for later use
(req as any).session = session;
console.info(
"Just passing the middleware and reminder that we need to add the real stuff in.",
);
//console.log(session);
req.user = {
id: session.user.id,
email: session.user.email,
roles: session.user.role,
username: session.user.username,
};
next();
} catch {
return res.status(401).json({ error: "Unauthorized" });

View File

@@ -0,0 +1,52 @@
import type { NextFunction, Request, Response } from "express";
import { auth } from "../utils/auth.utils.js";
type PermissionMap = Record<string, string[]>;
declare global {
namespace Express {
interface Request {
authz?: {
success: boolean;
permissions: PermissionMap;
};
}
}
}
function normalizeRoles(roles: unknown): string {
if (Array.isArray(roles)) return roles.join(",");
if (typeof roles === "string") return roles;
return "";
}
export function requirePermission(permissions: PermissionMap) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const role = normalizeRoles(req.user?.roles) as any;
const result = await auth.api.userHasPermission({
body: {
role,
permissions,
},
});
req.authz = {
success: !!result?.success,
permissions,
};
if (!result?.success) {
return res.status(403).json({
ok: false,
message: "You do not have permission to perform this action.",
});
}
next();
} catch (error) {
next(error);
}
};
}

View File

@@ -0,0 +1,37 @@
import { and, eq } from "drizzle-orm";
import type { NextFunction, Request, Response } from "express";
import { db } from "../db/db.controller.js";
import { settings } from "../db/schema/settings.schema.js";
import { tryCatch } from "../utils/trycatch.utils.js";
/**
*
* @param moduleName name of the module we are checking if is enabled or not.
*/
export const featureCheck = (moduleName: string) => {
// get the features from the settings
return async (_req: Request, res: Response, next: NextFunction) => {
const { data: sData, error: sError } = await tryCatch(
db
.select()
.from(settings)
.where(
and(
eq(settings.settingType, "feature"),
eq(settings.name, moduleName),
),
),
);
if (sError) {
return res.status(403).json({ error: "Internal Error" });
}
if (!sData?.length || !sData[0]?.active) {
return res.status(403).json({ error: "Feature disabled" });
}
next();
};
};

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

@@ -0,0 +1,153 @@
import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { notifications } from "../db/schema/notifications.schema.js";
import { notificationSub } from "../db/schema/notifications.sub.schema.js";
import { createLogger } from "../logger/logger.controller.js";
import { minutesToCron } from "../utils/croner.minConvert.js";
import { createCronJob, stopCronJob } from "../utils/croner.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const log = createLogger({ module: "notifications", subModule: "start" });
export const startNotifications = async () => {
// get active notification
const { data, error } = await tryCatch(
db.select().from(notifications).where(eq(notifications.active, true)),
);
if (error) {
log.error(
{ error: error },
"There was an error when getting notifications.",
);
return;
}
if (data) {
if (data.length === 0) {
log.info(
{},
"There are know currently active notifications to start up.",
);
return;
}
// get the subs and see if we have any subs currently so we can fire up the notification
const { data: sub, error: subError } = await tryCatch(
db.select().from(notificationSub),
);
if (subError) {
log.error(
{ error: error },
"There was an error when getting subscriptions.",
);
return;
}
if (sub.length === 0) {
log.info({}, "There are know currently active subscriptions.");
return;
}
const emailString = [
...new Set(
sub.flatMap((e) =>
e.emails?.map((email) => email.trim().toLowerCase()),
),
),
].join(";");
for (const n of data) {
createCronJob(
n.name,
minutesToCron(parseInt(n.interval ?? "15", 10)),
async () => {
try {
const { default: runFun } = await import(
`./notification.${n.name.trim()}.js`
);
await runFun(n, emailString);
} catch (error) {
log.error(
{ error: error },
"There was an error starting the notification",
);
}
},
);
}
}
};
export const modifiedNotification = async (id: string) => {
// when a notifications subscribed to, updated, deleted we want to get the info and rerun the startup on the single notification.
const { data, error } = await tryCatch(
db.select().from(notifications).where(eq(notifications.id, id)),
);
if (error) {
log.error(
{ error: error },
"There was an error when getting notifications.",
);
return;
}
if (data) {
if (!data[0]?.active) {
stopCronJob(data[0]?.name ?? "");
return;
}
// get the subs for the specific id as we only want to up the modified one
const { data: sub, error: subError } = await tryCatch(
db
.select()
.from(notificationSub)
.where(eq(notificationSub.notificationId, id)),
);
if (subError) {
log.error(
{ error: error },
"There was an error when getting subscriptions.",
);
return;
}
if (sub.length === 0) {
log.info({}, "There are know currently active subscriptions.");
stopCronJob(data[0]?.name ?? "");
return;
}
const emailString = [
...new Set(
sub.flatMap((e) =>
e.emails?.map((email) => email.trim().toLowerCase()),
),
),
].join(";");
createCronJob(
data[0].name,
minutesToCron(parseInt(data[0].interval ?? "15", 10)),
async () => {
try {
const { default: runFun } = await import(
`./notification.${data[0]?.name.trim()}.js`
);
await runFun(data[0], emailString);
} catch (error) {
log.error(
{ error: error },
"There was an error starting the notification",
);
}
},
);
}
};

View File

@@ -0,0 +1,106 @@
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 reprint = async (data: any, emails: string) => {
// TODO: do the actual logic for the notification.
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,
});
}
}
};
export default reprint;

View File

@@ -0,0 +1,55 @@
import { eq } from "drizzle-orm";
import { type Response, Router } from "express";
import { db } from "../db/db.controller.js";
import { notifications } from "../db/schema/notifications.schema.js";
import { auth } from "../utils/auth.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (req, res: Response) => {
const hasPermissions = await auth.api.userHasPermission({
body: {
//userId: req?.user?.id,
role: req.user?.roles as any,
permissions: {
notifications: ["readAll"], // This must match the structure in your access control
},
},
});
const { data: nName, error: nError } = await tryCatch(
db
.select()
.from(notifications)
.where(
!hasPermissions.success ? eq(notifications.active, true) : undefined,
)
.orderBy(notifications.name),
);
if (nError) {
return apiReturn(res, {
success: false,
level: "error",
module: "notification",
subModule: "get",
message: `There was an error getting the notifications `,
data: [nError],
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "notification",
subModule: "get",
message: `All current notifications`,
data: nName ?? [],
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,20 @@
import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import getNotifications from "./notification.route.js";
import updateNote from "./notification.update.route.js";
import deleteSub from "./notificationSub.delete.route.js";
import subs from "./notificationSub.get.route.js";
import newSub from "./notificationSub.post.route.js";
import updateSub from "./notificationSub.update.route.js";
export const setupNotificationRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(`${baseUrl}/api/notification`, requireAuth, getNotifications);
app.use(`${baseUrl}/api/notification`, requireAuth, updateNote);
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/*
};

View File

@@ -0,0 +1,81 @@
import { eq } from "drizzle-orm";
import { type Response, Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import { notifications } from "../db/schema/notifications.schema.js";
import { requirePermission } from "../middleware/auth.requiredPerms.middleware.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { modifiedNotification } from "./notification.controller.js";
const r = Router();
const updateNote = z.object({
description: z.string().optional(),
active: z.boolean().optional(),
interval: z.string().optional(),
options: z.array(z.record(z.string(), z.unknown())).optional(),
});
r.patch(
"/:id",
requirePermission({ notifications: ["update"] }),
async (req, res: Response) => {
const { id } = req.params;
try {
const validated = updateNote.parse(req.body);
const { data: nName, error: nError } = await tryCatch(
db
.update(notifications)
.set(validated)
.where(eq(notifications.id, id as string))
.returning(),
);
await modifiedNotification(id as string);
if (nError) {
return apiReturn(res, {
success: false,
level: "error",
module: "notification",
subModule: "update",
message: `There was an error getting the notifications `,
data: [nError],
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "notification",
subModule: "update",
message: `Notification was updated`,
data: nName ?? [],
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: "notification",
message: "Validation failed",
data: [flattened.fieldErrors],
status: 400, //connect.success ? 200 : 400,
});
}
}
},
);
export default r;

View File

@@ -0,0 +1,103 @@
import { and, eq } from "drizzle-orm";
import { type Response, Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import { notificationSub } from "../db/schema/notifications.sub.schema.js";
import { auth } from "../utils/auth.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { modifiedNotification } from "./notification.controller.js";
const newSubscribe = z.object({
userId: z.string().describe("User id."),
notificationId: z.string().describe("Notification id"),
});
const r = Router();
r.delete("/", async (req, res: Response) => {
const hasPermissions = await auth.api.userHasPermission({
body: {
//userId: req?.user?.id,
role: req.user?.roles as any,
permissions: {
notifications: ["readAll"], // This must match the structure in your access control
},
},
});
try {
const validated = newSubscribe.parse(req.body);
const { data, error } = await tryCatch(
db
.delete(notificationSub)
.where(
and(
eq(
notificationSub.userId,
hasPermissions ? validated.userId : (req?.user?.id ?? ""),
), // allows the admin to delete this
//eq(notificationSub.userId, req?.user?.id ?? ""),
eq(notificationSub.notificationId, validated.notificationId),
),
)
.returning(),
);
await modifiedNotification(validated.notificationId);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "notification",
subModule: "post",
message: `There was an error deleting the subscription `,
data: [error],
status: 400,
});
}
if (data.length <= 0) {
return apiReturn(res, {
success: false,
level: "info",
module: "notification",
subModule: "post",
message: `Subscription was not deleted invalid data sent over`,
data: data ?? [],
status: 200,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "notification",
subModule: "post",
message: `Subscription deleted`,
data: 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: "notification",
message: "Validation failed",
data: [flattened.fieldErrors],
status: 400, //connect.success ? 200 : 400,
});
}
}
});
export default r;

View File

@@ -0,0 +1,61 @@
import { eq } from "drizzle-orm";
import { type Response, Router } from "express";
import { db } from "../db/db.controller.js";
import { notificationSub } from "../db/schema/notifications.sub.schema.js";
import { auth } from "../utils/auth.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (req, res: Response) => {
const { userId } = req.query;
const hasPermissions = await auth.api.userHasPermission({
body: {
//userId: req?.user?.id,
role: req.user?.roles as any,
permissions: {
notifications: ["readAll"], // This must match the structure in your access control
},
},
});
if (userId) {
hasPermissions.success = false;
}
const { data, error } = await tryCatch(
db
.select()
.from(notificationSub)
.where(
!hasPermissions.success
? eq(notificationSub.userId, `${req?.user?.id ?? ""}`)
: undefined,
),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "notification",
subModule: "post",
message: `There was an error getting subscriptions `,
data: [error],
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "notification",
subModule: "post",
message: `Subscriptions`,
data: data ?? [],
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,92 @@
import { type Response, Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import { notificationSub } from "../db/schema/notifications.sub.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { modifiedNotification } from "./notification.controller.js";
const newSubscribe = z.object({
emails: z
.email()
.array()
.describe("An array of emails"),
userId: z.string().describe("User id."),
notificationId: z
.string()
.describe("Notification id"),
});
const r = Router();
r.post("/", async (req, res: Response) => {
try {
const validated = newSubscribe.parse(req.body);
const emails = validated.emails
.map((e) => e.trim().toLowerCase())
.filter(Boolean);
const uniqueEmails = [...new Set(emails)];
const { data, error } = await tryCatch(
db
.insert(notificationSub)
.values({
userId: req?.user?.id ?? "",
notificationId: validated.notificationId,
emails: uniqueEmails,
})
.onConflictDoUpdate({
target: [notificationSub.userId, notificationSub.notificationId],
set: { emails: uniqueEmails },
})
.returning(),
);
await modifiedNotification(validated.notificationId);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "notification",
subModule: "post",
message: `There was an error getting the notifications `,
data: [error],
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "notification",
subModule: "post",
message: `Subscribed to notification`,
data: 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: "notification",
message: "Validation failed",
data: [flattened.fieldErrors],
status: 400, //connect.success ? 200 : 400,
});
}
}
});
export default r;

View File

@@ -0,0 +1,84 @@
import { and, eq } from "drizzle-orm";
import { type Response, Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import { notificationSub } from "../db/schema/notifications.sub.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { modifiedNotification } from "./notification.controller.js";
const newSubscribe = z.object({
emails: z.email().array().describe("An array of emails"),
userId: z.string().describe("User id."),
notificationId: z.string().describe("Notification id"),
});
const r = Router();
r.patch("/", async (req, res: Response) => {
try {
const validated = newSubscribe.parse(req.body);
const emails = validated.emails
.map((e) => e.trim().toLowerCase())
.filter(Boolean);
const uniqueEmails = [...new Set(emails)];
const { data, error } = await tryCatch(
db
.update(notificationSub)
.set({ emails: uniqueEmails })
.where(
and(
eq(notificationSub.userId, validated.userId),
eq(notificationSub.notificationId, validated.notificationId),
),
)
.returning(),
);
await modifiedNotification(validated.notificationId);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "notification",
subModule: "update",
message: `There was an error updating the notifications `,
data: [error],
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "notification",
subModule: "update",
message: `Subscription updated`,
data: 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: "notification",
message: "Validation failed",
data: [flattened.fieldErrors],
status: 400, //connect.success ? 200 : 400,
});
}
}
});
export default r;

View File

@@ -0,0 +1,70 @@
import { sql } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import {
type NewNotification,
notifications,
} from "../db/schema/notifications.schema.js";
import { createLogger } from "../logger/logger.controller.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const note: NewNotification[] = [
{
name: "reprintLabels",
description:
"Monitors the labels that are printed and returns a there data, if one falls withing the time frame.",
active: false,
interval: "10",
options: [{ auditId: [0] }],
},
{
name: "qualityBlocking",
description:
"Checks for new blocking orders that have been entered, recommend to get the most recent order in here before activating.",
active: false,
interval: "10",
options: [{ sentBlockingOrders: [{ timeStamp: "0", blockingOrder: 1 }] }],
},
{
name: "alplaPurchaseHistory",
description:
"Will check the alpla purchase data for any changes, if the req has not been sent already then we will send this, for a po or fresh order we will ignore. ",
active: false,
interval: "5",
options: [
{ sentReqs: [{ timeStamp: "0", req: 1, approved: false }] },
{ sentAPOs: [{ timeStamp: "0", apo: 1 }] },
{ sentRCT: [{ timeStamp: "0", rct: 1 }] },
],
},
];
export const createNotifications = async () => {
const log = createLogger({ module: "notifications", subModule: "create" });
const { data, error } = await tryCatch(
db
.insert(notifications)
.values(note)
.onConflictDoUpdate({
target: notifications.name,
set: {
description: sql`excluded.description`,
},
// where: sql`
// settings.seed_version IS NULL
// OR settings.seed_version < excluded.seed_version
// `,
})
.returning(),
);
if (error) {
log.error(
{ error: error },
"There was an error when adding or updating the notifications.",
);
}
if (data) {
log.info({}, "All notifications were added/updated");
}
};

View File

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

View File

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

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

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

View File

@@ -1,23 +0,0 @@
import { io } from "socket.io-client";
// NOTE: we assume "accessToken" was already obtained earlier via a call to '/auth/login'.
// get the
const accessToken =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3MTkyNTQyZS01NzQ5LTRlZTgtYjdjZS0zNTQ4ZjA0NGQwOWIiLCJqdGkiOiI1NzE5ZmQ2OS02NTVkLTQ1MjctYTJjOC1hZWNhMjU0MTQ2MDEiLCJpZCI6IjcxOTI1NDJlLTU3NDktNGVlOC1iN2NlLTM1NDhmMDQ0ZDA5YiIsImVtYWlsIjoiYmxha2UubWF0dGhlc0BhbHBsYS5jb20iLCJvcmdJZCI6IjI2YTE4NjlmLTYwNDktNDM3Mi04ZWMzLTVkZDZlNDIzZjJmNiIsImNvbXBhbnlJZCI6bnVsbCwicm9sZSI6InJvbGVfb3duZXIiLCJpc0VtYWlsVmVyaWZpZWQiOnRydWUsImludmFsaWRMb2dpbkF0dGVtcHRzIjpudWxsLCJpYXQiOjE3NzA4MTE5MTEsImV4cCI6MTc3MTA3MTExMX0.jLHOSIF5RHUGjwq8WvycYxD9HK8_677O6sgRUZeYdUQ";
const baseSubspaceUrl = "wss://subspace.opendock.com";
const url = `${baseSubspaceUrl}?token=${accessToken}`;
const socket = io(url, { transports: ["websocket"] }); // Enforce 'websocket' transport only.
// socket.on("heartbeat", (data) => {
// console.log(data);
// });
socket.on("connection", () => {
console.log("Connected");
});
socket.on("create-Appointment", (data) => {
console.log("appt create:", data);
});

View File

@@ -0,0 +1,393 @@
import axios from "axios";
import { addHours } from "date-fns";
import { formatInTimeZone } from "date-fns-tz";
import { eq, sql } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { opendockApt } from "../db/schema/opendock.schema.js";
import { settings } from "../db/schema/settings.schema.js";
import { createLogger } from "../logger/logger.controller.js";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { createCronJob } from "../utils/croner.utils.js";
import { delay } from "../utils/delay.utils.js";
import { returnFunc } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { getToken, odToken } from "./opendock.utils.js";
type Releases = {
ReleaseNumber: number;
DeliveryState: number;
DeliveryDate: Date;
LineItemHumanReadableId: number;
ArticleAlias: string;
LoadingUnits: string;
Quantity: number;
LineItemArticleWeight: number;
CustomerReleaseNumber: string;
};
const timeZone = process.env.TIMEZONE as string;
const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;
const log = createLogger({ module: "opendock", subModule: "releaseMonitor" });
// making the cron more safe when it comes to buffer stuff
let opendockSyncRunning = false;
let lastCheck = formatInTimeZone(
new Date().toISOString(),
timeZone,
"yyyy-MM-dd HH:mm:ss",
);
// const lastCheck = formatInTimeZone(
// new Date().toISOString(),
// `America/New_York`, //TODO: Pull timezone from the .env last as process.env.TIME_ZONE is not working so need to figure itout
// "yyyy-MM-dd HH:mm:ss",
// );
//const queue: unknown[] = [];
//const isProcessing: boolean = false;
// const parseDbDate = (value: string | Date) => {
// if (value instanceof Date) return value;
// // normalize "2026-04-08 13:10:43.280" -> "2026-04-08T13:10:43.280"
// const normalized = value.replace(" ", "T");
// // interpret that wall-clock time as America/New_York
// return fromZonedTime(normalized, timeZone);
// };
const postRelease = async (release: Releases) => {
if (!odToken.odToken) {
log.info({}, "Getting Auth Token");
await getToken();
}
if (
new Date(odToken.tokenDate || Date.now()).getTime() <
Date.now() - TWENTY_FOUR_HOURS
) {
log.info({}, "Refreshing Auth Token");
await getToken();
}
/**
* ReleaseState
* 0 = open
* 1 = planned
* 2 = CustomCanceled
* 4 = internally canceled
*/
/**
* DeliveryState
* 0 = open
* 1 = inprogress
* 2 = loading
* 3 = partly shipped
* 4 = delivered
*/
const newDockApt = {
status:
release.DeliveryState === 0 || release.DeliveryState === 1
? "Scheduled"
: release.DeliveryState === 2
? "InProgress"
: release.DeliveryState === 3 // this will consider finished and if a correction needs made to the bol we need to cancel and reactivate the order
? "Completed"
: release.DeliveryState === 4 && "Completed",
userId: process.env.DEFAULT_CARRIER, // this should be the carrierid
loadTypeId: process.env.DEFAULT_LOAD_TYPE, // well get this and make it a default one
dockId: process.env.DEFAULT_DOCK, // this the warehouse we want it in to start out
refNumbers: [release.ReleaseNumber],
//refNumber: release.ReleaseNumber,
start: release.DeliveryDate,
end: addHours(release.DeliveryDate, 1),
notes: "",
ccEmails: [""],
muteNotifications: true,
metadata: {
externalValidationFailed: false,
externalValidationErrorMessage: null,
},
units: null,
customFields: [
{
name: "strArticle",
type: "str",
label: "Article",
value: `${release.LineItemHumanReadableId} - ${release.ArticleAlias}`,
description: "What bottle are we sending ",
placeholder: "",
dropDownValues: [],
minLengthOrValue: 1,
hiddenFromCarrier: false,
requiredForCarrier: false,
requiredForWarehouse: false,
},
{
name: "intPallet Count",
type: "int",
label: "Pallet Count",
value: parseInt(release.LoadingUnits, 10), // do we really want to update this if its partial load as it should have been the full amount?
description: "How many pallets",
placeholder: "22",
dropDownValues: [],
minLengthOrValue: 1,
hiddenFromCarrier: false,
requiredForCarrier: false,
requiredForWarehouse: false,
},
{
name: "strTotal Weight",
type: "str",
label: "Total Weight",
value: `${(((release.Quantity * release.LineItemArticleWeight) / 1000) * 2.20462).toFixed(2)}`,
description: "What is the total weight of the load",
placeholder: "",
dropDownValues: [],
minLengthOrValue: 1,
hiddenFromCarrier: false,
requiredForCarrier: false,
requiredForWarehouse: false,
},
{
name: "strCustomer ReleaseNumber",
type: "str",
label: "Customer Release Number",
value: `${release.CustomerReleaseNumber}`,
description: "What is the customer release number",
placeholder: "",
dropDownValues: [],
minLengthOrValue: 1,
hiddenFromCarrier: false,
requiredForCarrier: false,
requiredForWarehouse: false,
},
],
};
// TODO: pull the current added releases from the db and if one matches then we want to get its id and run the update vs create
const { data: existingApt, error: aptError } = await tryCatch(
db
.select()
.from(opendockApt)
.where(eq(opendockApt.release, release.ReleaseNumber))
.limit(1),
);
if (aptError) {
log.error({ error: aptError }, "Error getting apt data");
// TODO: send an error email on this one as it will cause issues
return;
}
const existing = existingApt[0];
//console.log(releaseCheck);
if (existing) {
const id = existing.openDockAptId;
try {
const response = await axios.patch(
`${process.env.OPENDOCK_URL}/appointment/${id}`,
newDockApt,
{
headers: {
"content-type": "application/json; charset=utf-8",
Authorization: `Bearer ${odToken.odToken}`,
},
},
);
if (response.status === 400) {
log.error({}, response.data.data.message);
return;
}
// update the release in the db leaving as insert just incase something weird happened
try {
await db
.insert(opendockApt)
.values({
release: release.ReleaseNumber,
openDockAptId: response.data.data.id,
appointment: response.data.data,
})
.onConflictDoUpdate({
target: opendockApt.release,
set: {
openDockAptId: response.data.data.id,
appointment: response.data.data,
upd_date: sql`NOW()`,
},
})
.returning();
log.info({}, `${release.ReleaseNumber} was updated`);
} catch (e) {
log.error(
{ error: e },
`Error updating the release: ${release.ReleaseNumber}`,
);
}
// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
} catch (e: any) {
//console.info(newDockApt);
log.error(
{ error: e.response.data },
`An error has occurred during patching of the release: ${release.ReleaseNumber}`,
);
return;
}
} else {
try {
const response = await axios.post(
`${process.env.OPENDOCK_URL}/appointment`,
newDockApt,
{
headers: {
"content-type": "application/json; charset=utf-8",
Authorization: `Bearer ${odToken.odToken}`,
},
},
);
// we need the id,release#,status from this response, store it in lst, check if we have a release so we can just update it.
// this will be utilized when we are listening for the changes to the apts. that way we can update the state to arrived. we will run our own checks on this guy during the incoming messages.
if (response.status === 400) {
log.error({}, response.data.data.message);
return;
}
// the response to make it simple we want response.data.id, response.data.relNumber, status will be defaulted to Scheduled if we created it here.
// TODO: add this release data to our db. but save it in json format and well parse it out. that way we future proof it and have everything in here vs just a few things
//console.info(response.data.data, "Was Created");
try {
await db
.insert(opendockApt)
.values({
release: release.ReleaseNumber,
openDockAptId: response.data.data.id,
appointment: response.data.data,
})
.onConflictDoUpdate({
target: opendockApt.release,
set: {
openDockAptId: response.data.data.id,
appointment: response.data.data,
upd_date: sql`NOW()`,
},
})
.returning();
log.info({}, `${release.ReleaseNumber} was created`);
} catch (e) {
log.error({ error: e }, "Error creating new release");
}
// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
} catch (e: any) {
log.error(
{ error: e?.response?.data },
"Error posting new release to opendock",
);
return;
}
}
await delay(750); // rate limit protection
};
export const monitorReleaseChanges = async () => {
// TODO: validate if the setting for opendocks is active and start / stop the system based on this
// if it changes we set to false and the next loop will stop.
const openDockMonitor = await db
.select()
.from(settings)
.where(eq(settings.name, "opendock_sync"));
// console.info("Starting release monitor", lastCheck);
const sqlQuery = sqlQuerySelector(`releaseChecks`) as SqlQuery;
if (!sqlQuery.success) {
return returnFunc({
success: false,
level: "error",
module: "datamart",
subModule: "query",
message: `Error getting releaseChecks info`,
data: [sqlQuery.message],
notify: false,
});
}
if (openDockMonitor[0]?.active) {
// const BUFFER_MS =
// Math.floor(parseInt(openDockMonitor[0]?.value, 10) || 30) * 1.5 * 1000; // this should be >= to the interval we set in the cron TODO: should pull the buffer from the setting and give it an extra 10% then round to nearest int.
createCronJob(
"opendock_sync",
`*/${parseInt(openDockMonitor[0]?.value, 10) || 30} * * * * *`,
async () => {
if (opendockSyncRunning) {
log.warn(
{},
"Skipping opendock_sync because previous run is still active",
);
return;
}
opendockSyncRunning = true;
try {
// set this to the latest time.
const result = await prodQuery(
sqlQuery.query.replace("[dateCheck]", `'${lastCheck}'`),
"Get release info",
);
log.debug(
{ lastCheck },
`${result.data.length} Changes to a release have been made`,
);
if (result.data.length) {
for (const release of result.data) {
await postRelease(release);
// add a 2 seconds to account for a massive influx of orders and when we dont finish in 1 go it wont try to grab the same amount again
const nDate = new Date(release.Upd_Date);
nDate.setSeconds(nDate.getSeconds() + 2);
lastCheck = formatInTimeZone(
nDate.toISOString(),
"UTC",
"yyyy-MM-dd HH:mm:ss",
);
log.debug({ lastCheck }, "Changes to a release have been made");
await delay(500);
}
}
} catch (e) {
console.error(
{ error: e },
"Error occurred while running the monitor job",
);
log.error(
{ error: e },
"Error occurred while running the monitor job",
);
} finally {
opendockSyncRunning = false;
}
},
"monitorReleaseChanges",
);
}
};

View File

@@ -0,0 +1,19 @@
import { type Express, Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { featureCheck } from "../middleware/featureActive.middleware.js";
import getApt from "./opendockGetRelease.route.js";
export const setupOpendockRoutes = (baseUrl: string, app: Express) => {
//setup all the routes
// Apply auth to entire router
const router = Router();
// is the feature even on?
router.use(featureCheck("opendock_sync"));
// we need to make sure we are authenticated to see the releases
router.use(requireAuth);
router.use(getApt);
app.use(`${baseUrl}/api/opendock`, router);
};

View File

@@ -0,0 +1,35 @@
import axios from "axios";
import { createLogger } from "../logger/logger.controller.js";
type ODToken = {
odToken: string | null;
tokenDate: Date | null;
};
export let odToken: ODToken = {
odToken: null,
tokenDate: new Date(),
};
export const getToken = async () => {
const log = createLogger({ module: "opendock", subModule: "releaseMonitor" });
try {
const { status, data } = await axios.post(
`${process.env.OPENDOCK_URL}/auth/login`,
{
email: "blake.matthes@alpla.com",
password: process.env.OPENDOCK_PASSWORD,
},
);
if (status === 400) {
log.error(data.message);
return;
}
odToken = { odToken: data.access_token, tokenDate: new Date() };
log.info({ odToken }, "Token added");
} catch (e) {
log.error({ error: e }, "Error getting/refreshing token");
}
};

View File

@@ -0,0 +1,40 @@
import { desc, gte, sql } from "drizzle-orm";
import { Router } from "express";
import { db } from "../db/db.controller.js";
import { opendockApt } from "../db/schema/opendock.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (_, res) => {
//const limit
const daysCreated = 30;
const { data } = await tryCatch(
db
.select()
.from(opendockApt)
.where(
gte(
opendockApt.createdAt,
sql.raw(`NOW() - INTERVAL '${daysCreated} days'`),
),
)
.orderBy(desc(opendockApt.createdAt))
.limit(500),
);
apiReturn(res, {
success: true,
level: "info",
module: "opendock",
subModule: "apt",
message: `The first ${data?.length} Apt(s) that were created in the last ${daysCreated} `,
data: data ?? [],
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,69 @@
import { io, type Socket } from "socket.io-client";
import { createLogger } from "../logger/logger.controller.js";
import { systemSettings } from "../server.js";
import { getToken, odToken } from "./opendock.utils.js";
const log = createLogger({ module: "opendock", subModule: "releaseMonitor" });
const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;
let socket: Socket | null = null;
export const opendockSocketMonitor = async () => {
// checking if we actaully want to run this
if (!systemSettings.filter((n) => n.name === "opendock_sync")[0]?.active) {
log.info({}, "Opendock is not active");
}
if (!odToken.odToken) {
log.info({}, "Getting Auth Token");
await getToken();
}
if (
new Date(odToken.tokenDate || Date.now()).getTime() <
Date.now() - TWENTY_FOUR_HOURS
) {
log.info({}, "Refreshing Auth Token");
await getToken();
}
const baseSubspaceUrl = "wss://subspace.staging.opendock.com";
const url = `${baseSubspaceUrl}?token=${odToken.odToken}`;
socket = io(url, { transports: ["websocket"] }); // Enforce 'websocket' transport only.
socket.on("connect", () => {
console.log("Connected");
});
// socket.on("heartbeat", (data) => {
// console.log(data);
// });
socket.on("create-Appointment", () => {
//console.log("appt create:", data);
});
socket.on("update-Appointment", () => {
//console.log("appt update:", data);
});
socket.on("error", (data) => {
console.log("Error:", data);
});
// socket.onAny((event, ...args) => {
// console.log("Received event:", event, args);
// });
};
export const killOpendockSocket = () => {
if (!socket) {
console.log("No active socket to kill");
return;
}
console.log("🛑 Killing socket connection...");
socket.removeAllListeners(); // optional but clean
socket.disconnect();
socket = null;
console.log("✅ Socket killed");
};

View File

@@ -1,493 +0,0 @@
import axios from "axios";
import { addHours } from "date-fns";
import { formatInTimeZone } from "date-fns-tz";
import { sql } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { opendockApt } from "../db/schema/opendock.schema.js";
import { createLogger } from "../logger/logger.controller.js";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { createCronJob } from "../utils/croner.utils.js";
import { delay } from "../utils/delay.utils.js";
import { returnFunc } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
let lastCheck = formatInTimeZone(
new Date().toISOString(),
"America/New_York",
"yyyy-MM-dd HH:mm:ss",
);
//const queue: unknown[] = [];
//const isProcessing: boolean = false;
type Releases = {
ReleaseNumber: number;
DeliveryState: number;
DeliveryDate: Date;
LineItemHumanReadableId: number;
ArticleAlias: string;
LoadingUnits: string;
Quantity: number;
LineItemArticleWeight: number;
CustomerReleaseNumber: string;
};
type ODToken = {
odToken: string | null;
tokenDate: Date | null;
};
let odToken: ODToken = {
odToken: null,
tokenDate: new Date(),
};
const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;
const log = createLogger({ module: "opendock", subModule: "releaseMonitor" });
const postRelease = async (release: Releases) => {
if (!odToken.odToken) {
log.info("Getting Auth Token");
await getToken();
}
if (
new Date(odToken.tokenDate || Date.now()).getTime() <
Date.now() - TWENTY_FOUR_HOURS
) {
log.info("Refreshing Auth Token");
await getToken();
}
/**
* ReleaseState
* 0 = open
* 1 = planned
* 2 = CustomCanceled
* 4 = internally canceled
*/
/**
* DeliveryState
* 0 = open
* 1 = inprogress
* 2 = loading
* 3 = partly shipped
* 4 = delivered
*/
const newDockApt = {
status:
release.DeliveryState === 0 || release.DeliveryState === 1
? "Scheduled"
: release.DeliveryState === 2
? "InProgress"
: release.DeliveryState === 3 // this will consider finished and if a correction needs made to the bol we need to cancel and reactivate the order
? "Completed"
: release.DeliveryState === 4 && "Completed",
userId: "2629b4f6-0003-472d-8b26-66a69ce5ac50", // this should be the carrierid
loadTypeId: "0aa7988e-b17b-4f10-acdd-3d029b44a773", // well get this and make it a default one
dockId: "00ba4386-ce5a-4dd1-9356-6e6d10a24609", // this the warehouse we want it in to start out
refNumbers: [release.ReleaseNumber],
refNumber: release.ReleaseNumber,
start: release.DeliveryDate,
end: addHours(release.DeliveryDate, 1),
notes: "",
ccEmails: [""],
muteNotifications: true,
metadata: {
externalValidationFailed: false,
externalValidationErrorMessage: null,
},
units: null,
customFields: [
{
name: "strArticle",
type: "str",
label: "Article",
value: `${release.LineItemHumanReadableId} - ${release.ArticleAlias}`,
description: "What bottle are we sending ",
placeholder: "",
dropDownValues: [],
minLengthOrValue: 1,
hiddenFromCarrier: false,
requiredForCarrier: false,
requiredForWarehouse: false,
},
{
name: "intPallet Count",
type: "int",
label: "Pallet Count",
value: parseInt(release.LoadingUnits, 10),
description: "How many pallets",
placeholder: "22",
dropDownValues: [],
minLengthOrValue: 1,
hiddenFromCarrier: false,
requiredForCarrier: false,
requiredForWarehouse: false,
},
{
name: "strTotal Weight",
type: "str",
label: "Total Weight",
value: `${(((release.Quantity * release.LineItemArticleWeight) / 1000) * 2.20462).toFixed(2)}`,
description: "What is the total weight of the load",
placeholder: "",
dropDownValues: [],
minLengthOrValue: 1,
hiddenFromCarrier: false,
requiredForCarrier: false,
requiredForWarehouse: false,
},
{
name: "strCustomer ReleaseNumber",
type: "str",
label: "Customer Release Number",
value: `${release.CustomerReleaseNumber}`,
description: "What is the customer release number",
placeholder: "",
dropDownValues: [],
minLengthOrValue: 1,
hiddenFromCarrier: false,
requiredForCarrier: false,
requiredForWarehouse: false,
},
],
};
// TODO: pull the current added releases from the db and if one matches then we want to get its id and run the update vs create
const { data: apt, error: aptError } = await tryCatch(
db.select().from(opendockApt),
);
if (aptError) {
log.error({ error: aptError }, "Error getting apt data");
// TODO: send an error email on this one as it will cause issues
return;
}
const releaseCheck = apt.filter((r) => r.release === release.ReleaseNumber);
//console.log(releaseCheck);
if (releaseCheck.length > 0) {
const id = releaseCheck[0]?.openDockAptId;
try {
const response = await axios.patch(
`${process.env.OPENDOCK_URL}/appointment/${id}`,
newDockApt,
{
headers: {
"content-type": "application/json; charset=utf-8",
Authorization: `Bearer ${odToken.odToken}`,
},
},
);
if (response.status === 400) {
log.error({}, response.data.data.message);
return;
}
// update the release in the db leaving as insert just incase something weird happened
try {
await db
.insert(opendockApt)
.values({
release: release.ReleaseNumber,
openDockAptId: response.data.data.id,
appointment: response.data.data,
})
.onConflictDoUpdate({
target: opendockApt.release,
set: { appointment: response.data.data, upd_date: sql`NOW()` },
})
.returning();
log.info(`${release.ReleaseNumber} was updated`);
} catch (e) {
log.error(e);
}
} catch (e: any) {
//console.info(newDockApt);
log.error(e.response.data);
return;
}
} else {
try {
const response = await axios.post(
`${process.env.OPENDOCK_URL}/appointment`,
newDockApt,
{
headers: {
"content-type": "application/json; charset=utf-8",
Authorization: `Bearer ${odToken}`,
},
},
);
// we need the id,release#,status from this response, store it in lst, check if we have a release so we can just update it.
// this will be utilized when we are listening for the changes to the apts. that way we can update the state to arrived. we will run our own checks on this guy during the incoming messages.
if (response.status === 400) {
log.error(response.data.data.message);
return;
}
// the response to make it simple we want response.data.id, response.data.relNumber, status will be defaulted to Scheduled if we created it here.
// TODO: add this release data to our db. but save it in json format and well parse it out. that way we future proof it and have everything in here vs just a few things
//console.info(response.data.data, "Was Created");
try {
await db
.insert(opendockApt)
.values({
release: release.ReleaseNumber,
openDockAptId: response.data.data.id,
appointment: response.data.data,
})
.onConflictDoUpdate({
target: opendockApt.id,
set: { appointment: response.data.data, upd_date: sql`NOW()` },
})
.returning();
log.info(`${release.ReleaseNumber} was created`);
} catch (e) {
log.error(e);
}
} catch (e: any) {
log.error(e.response.data);
return;
}
}
await delay(500); // rate limit protection
};
export const monitorReleaseChanges = async () => {
// TODO: validate if the setting for opendocks is active and start / stop the system based on this
// if it changes we set to false and the next loop will stop.
const openDockMonitor = true;
// console.info("Starting release monitor", lastCheck);
const sqlQuery = sqlQuerySelector(`releaseChecks`) as SqlQuery;
if (!sqlQuery.success) {
return returnFunc({
success: false,
level: "error",
module: "datamart",
subModule: "query",
message: `Error getting releaseChecks info`,
data: [sqlQuery.message],
notify: false,
});
}
if (openDockMonitor) {
createCronJob("open-dock-monitor", "*/15 * * * * *", async () => {
try {
const result = await prodQuery(
sqlQuery.query.replace("[dateCheck]", `'${lastCheck}'`),
"Get release info",
);
if (result.data.length) {
for (const release of result.data) {
await postRelease(release);
lastCheck = formatInTimeZone(
new Date(release.Upd_Date).toISOString(),
"UTC",
"yyyy-MM-dd HH:mm:ss",
);
await delay(500);
}
}
} catch (e) {
console.error(e);
log.error({ error: e }, "Monitor error");
}
});
}
// run the main game loop
// while (openDockSetting) {
// try {
// const result = await prodQuery(
// sqlQuery.query.replace("[dateCheck]", `'${lastCheck}'`),
// "Get release info",
// );
// if (result.data.length) {
// for (const release of result.data) {
// // potentially move this to a buffer table to easy up on memory
// await postRelease(release);
// // Move checkpoint AFTER successful post
// lastCheck = formatInTimeZone(
// new Date(release.Upd_Date).toISOString(),
// "UTC",
// "yyyy-MM-dd HH:mm:ss",
// );
// await delay(500);
// }
// }
// } catch (e) {
// console.error("Monitor error:", e);
// }
// await delay(15 * 1000); // making this 15 seconds as we would really only see issues if we have a mass burst.
// }
};
const getToken = async () => {
try {
const { status, data } = await axios.post(
`${process.env.OPENDOCK_URL}/auth/login`,
{
email: "blake.matthes@alpla.com",
password: process.env.OPENDOCK_PASSWORD,
},
);
if (status === 400) {
log.error(data.message);
return;
}
odToken = { odToken: data.access_token, tokenDate: new Date() };
log.info("Token added");
} catch (e) {
log.error(e);
}
};
// export const monitorReleaseChanges = async () => {
// console.log("Starting release monitor", lastCheck);
// setInterval(async () => {
// try {
// const result = await prodQuery(
// releaseQuery.replace("[dateCheck]", `'${lastCheck}'`),
// "get last release change",
// );
// //console.log(releaseQuery.replace("[dateCheck]", `'${lastCheck}'`));
// if (result.data.length > 0) {
// console.log(
// formatInTimeZone(
// result.data[result.data.length - 1].Upd_Date,
// "UTC",
// "yyyy-MM-dd HH:mm:ss",
// ),
// lastCheck,
// );
// lastCheck = formatInTimeZone(
// result.data[result.data.length - 1].Upd_Date,
// "UTC",
// "yyyy-MM-dd HH:mm:ss",
// );
// const releases = result.data;
// for (let i = 0; i < releases.length; i++) {
// const newDockApt = {
// status: "Scheduled",
// userId: "ee956455-e193-47fc-b53b-dff30fabdf4b", // this should be the carrierid
// loadTypeId: "0aa7988e-b17b-4f10-acdd-3d029b44a773", // well get this and make it a default one
// dockId: "00ba4386-ce5a-4dd1-9356-6e6d10a24609", // this the warehouse we want it in to start out
// refNumbers: [releases[i].ReleaseNumber],
// refNumber: releases[i].ReleaseNumber,
// start: releases[i].DeliveryDate,
// end: addHours(releases[i].DeliveryDate, 1),
// notes: "",
// ccEmails: [""],
// muteNotifications: true,
// metadata: {
// externalValidationFailed: false,
// externalValidationErrorMessage: null,
// },
// units: null,
// customFields: [
// {
// name: "strArticle",
// type: "str",
// label: "Article",
// value: `${releases[i].LineItemHumanReadableId} - ${releases[i].ArticleAlias}`,
// description: "What bottle are we sending ",
// placeholder: "",
// dropDownValues: [],
// minLengthOrValue: 1,
// hiddenFromCarrier: false,
// requiredForCarrier: false,
// requiredForWarehouse: false,
// },
// {
// name: "intPallet Count",
// type: "int",
// label: "Pallet Count",
// value: parseInt(releases[i].LoadingUnits, 10),
// description: "How many pallets",
// placeholder: "22",
// dropDownValues: [],
// minLengthOrValue: 1,
// hiddenFromCarrier: false,
// requiredForCarrier: false,
// requiredForWarehouse: false,
// },
// {
// name: "strTotal Weight",
// type: "str",
// label: "Total Weight",
// value: `${(((releases[i].Quantity * releases[i].LineItemArticleWeight) / 1000) * 2.20462).toFixed(2)}`,
// description: "What is the total weight of the load",
// placeholder: "",
// dropDownValues: [],
// minLengthOrValue: 1,
// hiddenFromCarrier: false,
// requiredForCarrier: false,
// requiredForWarehouse: false,
// },
// {
// name: "strCustomer ReleaseNumber",
// type: "str",
// label: "Customer Release Number",
// value: `${releases[i].CustomerReleaseNumber}`,
// description: "What is the customer release number",
// placeholder: "",
// dropDownValues: [],
// minLengthOrValue: 1,
// hiddenFromCarrier: false,
// requiredForCarrier: false,
// requiredForWarehouse: false,
// },
// ],
// };
// //console.log(newDockApt);
// const newDockResult = await axios.post(
// "https://neutron.staging.opendock.com/appointment",
// newDockApt,
// {
// headers: {
// "content-type": "application/json; charset=utf-8",
// },
// },
// );
// console.log(newDockResult.statusText);
// await delay(500);
// }
// }
// } catch (e) {
// console.log(e);
// }
// }, 5 * 1000);
// };

View File

@@ -58,7 +58,7 @@ export const prodQuery = async (queryToRun: string, name: string) => {
return {
success: true,
message: `Query results for: ${name}`,
data: result.recordset,
data: result.recordset ?? [],
};
} catch (error: unknown) {
const err = error as SqlError;

View File

@@ -0,0 +1,63 @@
use AlplaPROD_test1
declare @intervalCheck as int = '[interval]'
/*
Monitors alpla purchase for thing new. this will not update unless the order status is updated.
this means if a user just reopens the order it will update but everything changed in the position will not be updated until the user reorders or cancels the po
*/
select
IdBestellung as apo
,po.revision as revision
,po.Bestaetigt as confirmed
,po.status
,case po.Status
when 1 then 'Created'
when 2 then 'Ordered'
when 22 then 'Reopened'
when 11 then 'Reopened'
when 4 then 'Planned'
when 5 then 'Partly Delivered'
when 6 then 'Delivered'
when 7 then 'Canceled'
when 8 then 'Closed'
else 'Unknown' end as statusText
,po.IdJournal as journalNum -- use this to validate if we used it already.
,po.Add_User as add_user
,po.Add_Date as add_date
,po.Upd_User as upd_user
,po.Upd_Date as upd_Date
,po.Bemerkung as remark
,po.IdJournal as journal -- use this to validate if we used it already.
,isnull((
select
o.IdArtikelVarianten as av
,a.Bezeichnung as alias
,Lieferdatum as deliveryDate
,cast(BestellMenge as decimal(18,2)) as qty
,cast(BestellMengeVPK as decimal(18,0)) as pkg
,cast(PreisProEinheit as decimal(18,0)) as price
,PositionsStatus
,case PositionsStatus
when 1 then 'Created'
when 2 then 'Ordered'
when 22 then 'Reopened'
when 4 then 'Planned'
when 5 then 'Partly Delivered'
when 6 then 'Delivered'
when 7 then 'Canceled'
when 8 then 'Closed'
else 'Unknown' end as statusText
,o.upd_user
,o.upd_date
from T_Bestellpositionen (nolock) as o
left join
T_Artikelvarianten as a on
a.IdArtikelvarianten = o.IdArtikelVarianten
where o.IdBestellung = po.IdBestellung
for json path
), '[]') as position
--,*
from T_Bestellungen (nolock) as po
where po.Upd_Date > dateadd(MINUTE, -@intervalCheck, getdate())

View File

@@ -0,0 +1,28 @@
use [test1_AlplaPROD2.0_Read]
SELECT
--JSON_VALUE(content, '$.EntityId') as labelId
a.id
,ActorName
,FORMAT(PrintDate, 'yyyy-MM-dd HH:mm') as printDate
,FORMAT(CreatedDateTime, 'yyyy-MM-dd HH:mm') createdDateTime
,l.ArticleHumanReadableId as av
,l.ArticleDescription as alias
,PrintedCopies
,p.name as printerName
,RunningNumber
--,*
FROM [support].[AuditLog] (nolock) as a
left join
[labelling].[InternalLabel] (nolock) as l on
l.id = JSON_VALUE(content, '$.EntityId')
left join
[masterData].[printer] (nolock) as p on
p.id = l.PrinterId
where message like '%reprint%'
and CreatedDateTime > DATEADD(minute, -[intervalCheck], SYSDATETIMEOFFSET())
and a.id > [ignoreList]
order by CreatedDateTime desc

View File

@@ -0,0 +1,97 @@
/**
* This will monitor alpla purchase
*/
import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import {
alplaPurchaseHistory,
type NewAlplaPurchaseHistory,
} from "../db/schema/alplapurchase.schema.js";
import { settings } from "../db/schema/settings.schema.js";
import { createLogger } from "../logger/logger.controller.js";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { createCronJob } from "../utils/croner.utils.js";
import { delay } from "../utils/delay.utils.js";
import { returnFunc } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const log = createLogger({ module: "purchase", subModule: "purchaseMonitor" });
export const monitorAlplaPurchase = async () => {
const purchaseMonitor = await db
.select()
.from(settings)
.where(eq(settings.name, "purchaseMonitor"));
const sqlQuery = sqlQuerySelector(`alplapurchase`) as SqlQuery;
if (!sqlQuery.success) {
return returnFunc({
success: false,
level: "error",
module: "purchase",
subModule: "query",
message: `Error getting alpla purchase info`,
data: [sqlQuery.message],
notify: false,
});
}
if (purchaseMonitor[0]?.active) {
createCronJob("purchaseMonitor", "0 */5 * * * *", async () => {
try {
const result = await prodQuery(
sqlQuery.query.replace(
"[interval]",
`${purchaseMonitor[0]?.value || "5"}`,
),
"Get release info",
);
log.debug(
{},
`There are ${result.data.length} pending to be updated from the last ${purchaseMonitor[0]?.value}`,
);
if (result.data.length) {
const convertedData = result.data.map((i) => ({
...i,
position: JSON.parse(i.position),
})) as NewAlplaPurchaseHistory;
const { data, error } = await tryCatch(
db.insert(alplaPurchaseHistory).values(convertedData).returning(),
);
if (data) {
log.debug(
{ data },
"New data was just added to alpla purchase history",
);
}
if (error) {
log.error(
{ error },
"There was an error adding alpla purchase history",
);
}
await delay(500);
}
} catch (e) {
console.error(
{ error: e },
"Error occurred while running the monitor job",
);
log.error({ error: e }, "Error occurred while running the monitor job");
return;
}
});
}
};

View File

@@ -0,0 +1,188 @@
{
"GPIO-LED": {
"GPODefaults": {
"1": "HIGH",
"2": "HIGH",
"3": "HIGH",
"4": "HIGH"
},
"LEDDefaults": {
"3": "GREEN"
},
"TAG_READ": [
{
"pin": 1,
"state": "HIGH",
"type": "GPO"
}
]
},
"READER-GATEWAY": {
"batching": [
{
"maxPayloadSizePerReport": 256000,
"reportingInterval": 2000
}
],
"endpointConfig": {
"data": {
"event": {
"connections": [
{
"additionalOptions": {
"batching": {
"maxPayloadSizePerReport": 256000,
"reportingInterval": 2000
},
"retention": {
"maxEventRetentionTimeInMin": 500,
"maxNumEvents": 150000,
"throttle": 100
}
},
"description": "",
"name": "LST",
"options": {
"URL": "https://usday1prod.alpla.net/lst/old/api/rfid/taginfo/line3.4",
"security": {
"CACertificateFileLocation": "",
"authenticationOptions": {
"privateKeyFileLocation": "/readerconfig/ssl/server.key",
"publicKeyFileLocation": "/readerconfig/ssl/server.crt"
},
"authenticationType": "NONE",
"verifyHost": false,
"verifyPeer": false
}
},
"type": "httpPost"
}
]
}
}
},
"managementEventConfig": {
"errors": {
"antenna": false,
"cpu": {
"reportIntervalInSec": 1800,
"threshold": 90
},
"database": true,
"flash": {
"reportIntervalInSec": 1800,
"threshold": 90
},
"ntp": true,
"radio": true,
"radio_control": true,
"ram": {
"reportIntervalInSec": 1800,
"threshold": 90
},
"reader_gateway": true,
"userApp": {
"reportIntervalInSec": 1800,
"threshold": 120
}
},
"gpiEvents": true,
"gpoEvents": true,
"heartbeat": {
"fields": {
"radio_control": [
"ANTENNAS",
"RADIO_ACTIVITY",
"RADIO_CONNECTION",
"CPU",
"RAM",
"UPTIME",
"NUM_ERRORS",
"NUM_WARNINGS",
"NUM_TAG_READS",
"NUM_TAG_READS_PER_ANTENNA",
"NUM_DATA_MESSAGES_TXED",
"NUM_RADIO_PACKETS_RXED"
],
"reader_gateway": [
"NUM_DATA_MESSAGES_RXED",
"NUM_MANAGEMENT_EVENTS_TXED",
"NUM_DATA_MESSAGES_TXED",
"NUM_DATA_MESSAGES_RETAINED",
"NUM_DATA_MESSAGES_DROPPED",
"CPU",
"RAM",
"UPTIME",
"NUM_ERRORS",
"NUM_WARNINGS",
"INTERFACE_CONNECTION_STATUS",
"NOLOCKQ_DEPTH"
],
"system": [
"CPU",
"FLASH",
"NTP",
"RAM",
"SYSTEMTIME",
"TEMPERATURE",
"UPTIME",
"GPO",
"GPI",
"POWER_NEGOTIATION",
"POWER_SOURCE",
"MAC_ADDRESS",
"HOSTNAME"
],
"userDefined": null,
"userapps": [
"STATUS",
"CPU",
"RAM",
"UPTIME",
"NUM_DATA_MESSAGES_RXED",
"NUM_DATA_MESSAGES_TXED",
"INCOMING_DATA_BUFFER_PERCENTAGE_REMAINING",
"OUTGOING_DATA_BUFFER_PERCENTAGE_REMAINING"
]
},
"interval": 60
},
"userappEvents": true,
"warnings": {
"cpu": {
"reportIntervalInSec": 1800,
"threshold": 80
},
"database": true,
"flash": {
"reportIntervalInSec": 1800,
"threshold": 80
},
"ntp": true,
"radio_api": true,
"radio_control": true,
"ram": {
"reportIntervalInSec": 1800,
"threshold": 80
},
"reader_gateway": true,
"temperature": {
"ambient": 75,
"pa": 105
},
"userApp": {
"reportIntervalInSec": 1800,
"threshold": 60
}
}
},
"retention": [
{
"maxEventRetentionTimeInMin": 500,
"maxNumEvents": 150000,
"throttle": 100
}
]
},
"xml": "<?xml version='1.0'?>\n<Motorola xmlns:Falcon='http://www.motorola.com/RFID/Readers/Config/Falcon' xmlns='http://www.motorola.com/RFID/Readers/Config/Falcon'>\n<Config>\n<AppVersion major='3' minor='28' build='1' maintenance='0'/>\n<CommConfig EnabledStacks='IPV4' DisableRAPktProcessing='1' EnableDHCPv6='1' IPv6StaticIPAddr='fe80::1' IPv6SubnetMask='64' IPv6StaticGateway='::' IPv6DNSIP='fe80::20' DHCP='1' IPAddr='10.44.14.39' Mask='255.255.255.0' Gateway='10.44.14.252' DNS='10.44.9.250' DomainSearch='example.com' HttpRunning='2' TelnetActive='2' FtpActive='2' usbMode='0' WatchdogEnabled='1' AvahiEnabled='1' NetBIOSEnabled='0' RDMPAgentEnabled='1' SerialConTimeout='0' SNTP='0.0.0.0' SNTPHostName='pool.ntp.org' sntpHostDisplayMode='0' llrpClientMode='0' llrpSecureMode='0' llrpSecureModeValidatePeer='0' llrpPort='5084' llrpHostIP='192.168.127.2' allowllrpConnOverride='0' shouldReconnect='1'/>\n<Bluetooth discoverable='0' pairable='0' PincodeEnabled='0' passkey='165CB22DA5BE7BBEFB77709DD0A94B03FB77709DD0A94B03FB77709DD0A94B03FB77709DD0A94B03FB77709DD0A94B03FB77709DD0A94B03FB77709DD0A94B03' startIP='192.168.0.2' endIP='192.168.0.3'/>\n<WirelessConfig essid='' autoconnect='0'/>\n<RegionConfig RFCountry='United States/Canada' RFRegulatory='US FCC 15' RFScanMode='0' LBTEnable='0' ChannelData='FFFFFFFFFFFFFFFF'/>\n<SnmpConfig snmpVersion='1' heartbeat='1'/>\n<SyslogConfig RemoteIp='0.0.0.0' RemotePort='514' LogMinSeverity='7' ApplyFilter='0' MinimumSeverity='7' ProcessFilter='rmserver.elf,llrpserver.elf,snmpextagent.elf,RDMPAgent'/>\n<UserList>\n<User name='admin' PSWD='$6$weLpDwlv$utr0AwgPIae2O4Gln4cQ2IJJblXye412Xqni0V.ahIFKUOCEDGjzZ4ttthhrw7rmmQYsCXKwA9znyqPkAT.IL/'/>\n<User name='rfidadm' PSWD='15491'/>\n</UserList>\n<IPReader name='FX96007AF832 FX9600 RFID Reader' desc='FX96007AF832 Advanced Reader' flags='0' MonoStatic='0' CheckAntenna='1' gpiDebounceTime='0' gpioMapping='0' idleModeTimeOut='0' diagMode='0' extDiagMode='0' contact='Zebra Technologies Corporation' PowerNegotiation='0' PowerNegotiationProtocol='0' allowGuestLogin='1' configureHostName='0'>\n<ReadPoint name='Read Point 1' flags='0' CableLossPerHundredFt='10' CableLength='10'/>\n<ReadPoint name='Read Point 2' flags='0' CableLossPerHundredFt='10' CableLength='10'/>\n<ReadPoint name='Read Point 3' flags='1' CableLossPerHundredFt='10' CableLength='10'/>\n<ReadPoint name='Read Point 4' flags='1' CableLossPerHundredFt='10' CableLength='10'/>\n</IPReader>\n<SerialPortConf Mode='0' Baudrate='115200' Databits='8' Parity='none' Stopbits='1' Flowcontrol='hardware' TagMetaData='0' InventoryControl='0' IsAutostart='0'/>\n<FXConnectConfig FXConnectMode='0' TagMetaData='0' InventoryControl='None' HeartBeatPeriod='0' IsAutostart='0' PreFilterMode='0' PreFilters='None'/>\n<ProfinetConfig virtualDAP='1'/>\n<NodeJSPortConf Portnumber='8001'/>\n</Config>\n<MOTOROLA_LLRP_CONFIG><LLRP_READER_CONFIG />\n</MOTOROLA_LLRP_CONFIG>\n<IOT_CONNECT_CONFIG><OPERATING_MODE />\n</IOT_CONNECT_CONFIG>\n<RadioProfileData><RadioRegisterData Address='0' Data='00'/>\n</RadioProfileData>\n<CustomProfileData ForceEAPMode='0' FIPS_MODE_ENABLED='0' MaxNumberOfTagsBuffered='512'/>\n</Motorola >\n"
}

View File

@@ -0,0 +1,206 @@
{
"GPIO-LED": {
"GPODefaults": {
"1": "HIGH",
"2": "HIGH",
"3": "HIGH",
"4": "HIGH"
},
"LEDDefaults": {
"3": "GREEN"
},
"TAG_READ": [
{
"pin": 1,
"state": "HIGH",
"type": "GPO"
}
]
},
"READER-GATEWAY": {
"batching": [
{
"maxPayloadSizePerReport": 256000,
"reportingInterval": 2000
}
],
"endpointConfig": {
"data": {
"event": {
"connections": [
{
"additionalOptions": {
"retention": {
"maxEventRetentionTimeInMin": 500,
"maxNumEvents": 150000,
"throttle": 100
}
},
"description": "",
"name": "lst",
"options": {
"URL": "http://usday1vms006:3100/api/rfid/taginfo/wrapper1",
"security": {
"CACertificateFileLocation": "",
"authenticationOptions": {},
"authenticationType": "NONE",
"verifyHost": false,
"verifyPeer": false
}
},
"type": "httpPost"
},
{
"additionalOptions": {
"retention": {
"maxEventRetentionTimeInMin": 500,
"maxNumEvents": 150000,
"throttle": 100
}
},
"description": "",
"name": "mgt",
"options": {
"URL": "http://usday1vms006:3100/api/rfid/mgtevents/wrapper1",
"security": {
"CACertificateFileLocation": "",
"authenticationOptions": {},
"authenticationType": "NONE",
"verifyHost": false,
"verifyPeer": false
}
},
"type": "httpPost"
}
]
}
}
},
"interfaces": {
"tagDataInterface1": "lst",
"managementEventsInterface": "mgt"
},
"managementEventConfig": {
"errors": {
"antenna": false,
"cpu": {
"reportIntervalInSec": 1800,
"threshold": 90
},
"database": true,
"flash": {
"reportIntervalInSec": 1800,
"threshold": 90
},
"ntp": true,
"radio": true,
"radio_control": true,
"ram": {
"reportIntervalInSec": 1800,
"threshold": 90
},
"reader_gateway": true,
"userApp": {
"reportIntervalInSec": 1800,
"threshold": 120
}
},
"gpiEvents": true,
"gpoEvents": true,
"heartbeat": {
"fields": {
"radio_control": [
"ANTENNAS",
"RADIO_ACTIVITY",
"RADIO_CONNECTION",
"CPU",
"RAM",
"UPTIME",
"NUM_ERRORS",
"NUM_WARNINGS",
"NUM_TAG_READS",
"NUM_TAG_READS_PER_ANTENNA",
"NUM_DATA_MESSAGES_TXED",
"NUM_RADIO_PACKETS_RXED"
],
"reader_gateway": [
"NUM_DATA_MESSAGES_RXED",
"NUM_MANAGEMENT_EVENTS_TXED",
"NUM_DATA_MESSAGES_TXED",
"NUM_DATA_MESSAGES_RETAINED",
"NUM_DATA_MESSAGES_DROPPED",
"CPU",
"RAM",
"UPTIME",
"NUM_ERRORS",
"NUM_WARNINGS",
"INTERFACE_CONNECTION_STATUS",
"NOLOCKQ_DEPTH"
],
"system": [
"CPU",
"FLASH",
"NTP",
"RAM",
"SYSTEMTIME",
"TEMPERATURE",
"UPTIME",
"GPO",
"GPI",
"POWER_NEGOTIATION",
"POWER_SOURCE",
"MAC_ADDRESS",
"HOSTNAME"
],
"userDefined": null,
"userapps": [
"STATUS",
"CPU",
"RAM",
"UPTIME",
"NUM_DATA_MESSAGES_RXED",
"NUM_DATA_MESSAGES_TXED",
"INCOMING_DATA_BUFFER_PERCENTAGE_REMAINING",
"OUTGOING_DATA_BUFFER_PERCENTAGE_REMAINING"
]
},
"interval": 60
},
"userappEvents": true,
"warnings": {
"cpu": {
"reportIntervalInSec": 1800,
"threshold": 80
},
"database": true,
"flash": {
"reportIntervalInSec": 1800,
"threshold": 80
},
"ntp": true,
"radio_api": true,
"radio_control": true,
"ram": {
"reportIntervalInSec": 1800,
"threshold": 80
},
"reader_gateway": true,
"temperature": {
"ambient": 75,
"pa": 105
},
"userApp": {
"reportIntervalInSec": 1800,
"threshold": 60
}
}
},
"retention": [
{
"maxEventRetentionTimeInMin": 500,
"maxNumEvents": 150000,
"throttle": 100
}
]
}
}

View File

@@ -1,8 +1,12 @@
import type { Express } from "express";
import { setupAuthRoutes } from "./auth/auth.routes.js";
// import the routes and route setups
import { setupApiDocsRoutes } from "./configs/scaler.config.js";
import { setupDatamartRoutes } from "./datamart/datamart.routes.js";
import { setupNotificationRoutes } from "./notification/notification.routes.js";
import { setupOCPRoutes } from "./ocp/ocp.routes.js";
import { setupOpendockRoutes } from "./opendock/opendock.routes.js";
import { setupProdSqlRoutes } from "./prodSql/prodSql.routes.js";
import { setupSystemRoutes } from "./system/system.routes.js";
import { setupUtilsRoutes } from "./utils/utils.routes.js";
@@ -15,13 +19,7 @@ export const setupRoutes = (baseUrl: string, app: Express) => {
setupDatamartRoutes(baseUrl, app);
setupAuthRoutes(baseUrl, app);
setupUtilsRoutes(baseUrl, app);
// routes that get activated if the module is set to activated.
app.all("*foo", (_, res) => {
res.status(400).json({
message:
"You have encountered a route that dose not exist, please check the url and try again",
});
});
setupOpendockRoutes(baseUrl, app);
setupNotificationRoutes(baseUrl, app);
setupOCPRoutes(baseUrl, app);
};

View File

@@ -0,0 +1,36 @@
import type { OpenAPIV3_1 } from "openapi-types";
export const openDockApt: OpenAPIV3_1.PathsObject = {
"/api/opendock": {
get: {
summary: "Open Dock apt",
description: "Returns the last 30 days of apt(s).",
tags: ["Open Dock"],
responses: {
"200": {
description: "Jobs returned",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: {
type: "boolean",
format: "boolean",
example: true,
},
message: {
type: "string",
format: "string",
example:
"The first 5 Apt(s) that were created in the last 30 days",
},
},
},
},
},
},
},
},
},
};

View File

@@ -1,39 +1,65 @@
import { createServer } from "node:http";
import os from "node:os";
import createApp from "./app.js";
import { db } from "./db/db.controller.js";
import { dbCleanup } from "./db/dbCleanup.controller.js";
import { type Setting, settings } from "./db/schema/settings.schema.js";
import { createLogger } from "./logger/logger.controller.js";
import { monitorReleaseChanges } from "./opendock/releaseMonitor.utils.js";
import { startNotifications } from "./notification/notification.controller.js";
import { createNotifications } from "./notification/notifications.master.js";
import { monitorReleaseChanges } from "./opendock/openDockRreleaseMonitor.utils.js";
import { opendockSocketMonitor } from "./opendock/opendockSocketMonitor.utils.js";
import { connectProdSql } from "./prodSql/prodSqlConnection.controller.js";
import { monitorAlplaPurchase } from "./purchase/purchase.controller.js";
import { setupSocketIORoutes } from "./socket.io/serverSetup.js";
import { baseSettingValidationCheck } from "./system/settingsBase.controller.js";
import { createCronJob } from "./utils/croner.utils.js";
const port = Number(process.env.PORT) || 3000;
export let systemSettings: Setting[] = [];
const start = async () => {
const log = createLogger({ module: "system", subModule: "main start" });
// triggering long lived processes
connectProdSql();
// start long live processes
setTimeout(() => {
monitorReleaseChanges(); // this is od monitoring the db for all new releases
createCronJob("JobAuditLogCleanUp", "* 0 5 * * * *", () =>
dbCleanup("jobs", 30),
);
createCronJob("logsCleanup", "* 15 5 * * * *", () => dbCleanup("logs", 30));
createCronJob("opendockAptCleanup", "* 30 5 * * * *", () =>
dbCleanup("opendockApt", 90),
);
}, 5 * 1000);
const { app, baseUrl } = await createApp();
const server = createServer(app);
setupSocketIORoutes(baseUrl, server);
const log = createLogger({ module: "system", subModule: "main start" });
// triggering long lived processes
connectProdSql();
// trigger startup processes these must run before anything else can run
await baseSettingValidationCheck();
systemSettings = await db.select().from(settings);
//when starting up long lived features the name must match the setting name.
// also we always want to have long lived processes inside a setting check.
setTimeout(() => {
if (systemSettings.filter((n) => n.name === "opendock_sync")[0]?.active) {
log.info({}, "Opendock is active");
monitorReleaseChanges(); // this is od monitoring the db for all new releases
opendockSocketMonitor();
createCronJob("opendockAptCleanup", "0 30 5 * * *", () =>
dbCleanup("opendockApt", 90),
);
}
if (systemSettings.filter((n) => n.name === "purchaseMonitor")[0]?.active) {
monitorAlplaPurchase();
}
// these jobs below are system jobs and should run no matter what.
createCronJob("JobAuditLogCleanUp", "0 0 5 * * *", () =>
dbCleanup("jobs", 30),
);
createCronJob("logsCleanup", "0 15 5 * * *", () => dbCleanup("logs", 120));
// one shots only needed to run on server startups
createNotifications();
startNotifications();
}, 5 * 1000);
server.listen(port, async () => {
log.info(
`Listening on http://${os.hostname()}:${port}${baseUrl}, logging in ${process.env.LOG_LEVEL}, current ENV ${process.env.NODE_ENV ? process.env.NODE_ENV : "development"}`,

View File

@@ -0,0 +1,8 @@
import type { RoomId } from "./types.socket.js";
export const MAX_HISTORY = 50;
export const FLUSH_INTERVAL = 100; // 50ms change higher if needed
export const roomHistory = new Map<RoomId, unknown[]>();
export const roomBuffers = new Map<RoomId, any[]>();
export const roomFlushTimers = new Map<RoomId, NodeJS.Timeout>();

View File

@@ -0,0 +1,39 @@
import { desc } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { logs } from "../db/schema/logs.schema.js";
import type { RoomId } from "./types.socket.js";
type RoomDefinition<T = unknown> = {
seed: (limit: number) => Promise<T[]>;
};
export const protectedRooms: any = {
logs: { requiresAuth: true, role: ["admin", "systemAdmin"] },
admin: { requiresAuth: true, role: ["admin", "systemAdmin"] },
};
export const roomDefinition: Record<RoomId, RoomDefinition> = {
logs: {
seed: async (limit) => {
try {
const rows = await db
.select()
.from(logs)
.orderBy(desc(logs.createdAt))
.limit(limit);
return rows; //.reverse();
} catch (e) {
console.error("Failed to seed logs:", e);
return [];
}
},
},
labels: {
seed: async (limit) => {
console.info(limit);
return [];
},
},
};

View File

@@ -0,0 +1,27 @@
// the emitter setup
import type { RoomId } from "./types.socket.js";
let addDataToRoom: ((roomId: RoomId, payload: unknown[]) => void) | null = null;
export const registerEmitter = (
fn: (roomId: RoomId, payload: unknown[]) => void,
) => {
addDataToRoom = fn;
};
export const emitToRoom = (roomId: RoomId, payload: unknown[]) => {
if (!addDataToRoom) {
console.error("Socket emitter not initialized");
return;
}
addDataToRoom(roomId, payload);
};
/*
import { emitToRoom } from "../socket/socketEmitter.js";
// room name
// its payload
emitToRoom("logs", newLogRow);
*/

View File

@@ -0,0 +1,73 @@
import type { Server } from "socket.io";
import { createLogger } from "../logger/logger.controller.js";
import {
FLUSH_INTERVAL,
MAX_HISTORY,
roomBuffers,
roomFlushTimers,
roomHistory,
} from "./roomCache.socket.js";
import { roomDefinition } from "./roomDefinitions.socket.js";
import type { RoomId } from "./types.socket.js";
// get the db data if not exiting already
const log = createLogger({ module: "socket.io", subModule: "roomService" });
export const preseedRoom = async (roomId: RoomId) => {
if (roomHistory.has(roomId)) {
return roomHistory.get(roomId);
}
const roomDef = roomDefinition[roomId];
if (!roomDef) {
log.error({}, `Room ${roomId} is not defined`);
}
const latestData = await roomDef.seed(MAX_HISTORY);
roomHistory.set(roomId, latestData);
return latestData;
};
export const createRoomEmitter = (io: Server) => {
const addDataToRoom = <T>(roomId: RoomId, payload: T) => {
if (!roomHistory.has(roomId)) {
roomHistory.set(roomId, []);
}
const history = roomHistory.get(roomId)!;
history?.push(payload);
if (history?.length > MAX_HISTORY) {
history?.shift();
}
if (!roomBuffers.has(roomId)) {
roomBuffers.set(roomId, []);
}
roomBuffers.get(roomId)!.push(payload);
if (!roomFlushTimers.has(roomId)) {
const timer = setTimeout(() => {
const buffered = roomBuffers.get(roomId) || [];
if (buffered.length > 0) {
io.to(roomId).emit("room-update", {
roomId,
payloads: buffered, // ✅ array now
});
}
roomBuffers.set(roomId, []);
roomFlushTimers.delete(roomId);
}, FLUSH_INTERVAL);
roomFlushTimers.set(roomId, timer);
}
};
return { addDataToRoom };
};

View File

@@ -4,48 +4,137 @@ import type { Server as HttpServer } from "node:http";
import { instrument } from "@socket.io/admin-ui";
import { Server } from "socket.io";
import { createLogger } from "../logger/logger.controller.js";
import { allowedOrigins } from "../utils/cors.utils.js";
import { registerEmitter } from "./roomEmitter.socket.js";
import { createRoomEmitter, preseedRoom } from "./roomService.socket.js";
//const __filename = fileURLToPath(import.meta.url);
//const __dirname = dirname(__filename);
const log = createLogger({ module: "socket.io", subModule: "setup" });
import { auth } from "../utils/auth.utils.js";
//import type { Session, User } from "better-auth"; // adjust if needed
import { protectedRooms } from "./roomDefinitions.socket.js";
// declare module "socket.io" {
// interface Socket {
// user?: User | any;
// session?: Session;
// }
// }
export const setupSocketIORoutes = (baseUrl: string, server: HttpServer) => {
const io = new Server(server, {
path: `${baseUrl}/api/socket.io`,
cors: {
origin: ["http://localhost:3000", "https://admin.socket.io"],
origin: allowedOrigins,
credentials: true,
},
});
// ✅ Create emitter instance
const { addDataToRoom } = createRoomEmitter(io);
registerEmitter(addDataToRoom);
io.use(async (socket, next) => {
try {
//const cookieHeader = socket.handshake.headers.cookie;
const headers = new Headers();
for (const [key, value] of Object.entries(socket.request.headers)) {
if (typeof value === "string") {
headers.set(key, value);
} else if (Array.isArray(value)) {
headers.set(key, value.join(", "));
}
}
const session = await auth.api.getSession({
headers,
});
if (!session) {
return next(); // allow connection, but no auth
}
if (session) {
socket.user = session.user;
socket.session = session as any;
}
next();
} catch (err) {
next();
}
});
io.on("connection", (s) => {
console.info(s.id);
log.info({}, `User connected: ${s.id}`);
s.emit("welcome", {
serverTime: Date.now(),
availableRooms: ["logs", "labels"],
version: "1.0.0",
});
s.on("join-room", async (rn) => {
const config = protectedRooms[rn];
if (config?.requiresAuth && !s.user) {
return s.emit("room-error", {
room: rn,
message: "Authentication required",
});
}
const roles = Array.isArray(config.role) ? config.role : [config.role];
console.log(roles, s.user.role);
//if (config?.role && s.user?.role !== config.role) {
if (config?.role && !roles.includes(s.user?.role)) {
return s.emit("room-error", {
room: rn,
message: `Not authorized to be in room: ${rn}`,
});
}
s.join(rn);
// get room seeded
const history = await preseedRoom(rn);
log.info({}, `User joined ${rn}: ${s.id}`);
// send the intial data
s.emit("room-update", {
roomId: rn,
payloads: history,
initial: true,
});
});
s.on("leave-room", (room) => {
s.leave(room);
log.info({}, `${s.id} left room: ${room}`);
});
});
// admin stuff for socket io
// app.use(
// express.static(
// join(__dirname, "../../../node_modules/@socket.io/admin-ui/dist"),
// ),
// );
io.on("disconnect", (s) => {
log.info({}, "User disconnected:", s.id);
});
// admin stuff
// app.get(baseUrl + "/admindashboard", (_, res) => {
// res.sendFile(
// join(
// __dirname,
// "../../../node_modules/@socket.io/admin-ui/dist/index.js",
// ),
// );
// });
const admin = io.of("/admin");
admin.on("connection", () => {
console.info("Connected to admin userspace");
admin.on("connection", (s) => {
log.info({}, `User connected: ${s.id}`);
});
admin.on("disconnect", (s) => {
log.info({}, "User disconnected:", s.id);
});
instrument(io, {
auth: false,
//namespaceName: "/admin",
});
//setup all the routes
// app.use(`${baseUrl}/api/datamart`, runQuery);
// app.use(`${baseUrl}/api/datamart`, addQuery);
// app.use(`${baseUrl}/api/datamart`, updateQuery);
// just sending a get on datamart will return all the queries that we can call.
};

View File

@@ -0,0 +1 @@
export type RoomId = "logs" | "labels"; //| "alerts" | "metrics";

View File

@@ -0,0 +1,44 @@
import { type Response, Router } from "express";
import { db } from "../db/db.controller.js";
import { settings } from "../db/schema/settings.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(settings).orderBy(settings.name),
);
if (sError) {
return apiReturn(res, {
success: false,
level: "error",
module: "system",
subModule: "settings",
message: `There was an error getting the settings `,
data: [sError],
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "system",
subModule: "settings",
message: `All current settings`,
data: sName ?? [],
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,344 @@
import { sql } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { type NewSetting, settings } from "../db/schema/settings.schema.js";
import { createLogger } from "../logger/logger.controller.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const newSettings: NewSetting[] = [
// feature settings
{
name: "opendock_sync",
value: "15",
active: false,
description: "Dock Scheduling system",
moduleName: "opendock",
settingType: "feature",
roles: ["admin"],
seedVersion: 1,
},
{
name: "ocp",
value: "1",
active: false,
description: "One click print",
moduleName: "ocp",
settingType: "feature",
roles: ["admin"],
seedVersion: 1,
},
{
name: "ocme",
value: "0",
active: false,
description: "Dayton Agv system",
moduleName: "ocme",
settingType: "feature",
roles: ["admin"],
seedVersion: 1,
},
{
name: "demandManagement",
value: "1",
active: false,
description: "Fake EDI System",
moduleName: "demandManagement",
settingType: "feature",
roles: ["admin"],
seedVersion: 1,
},
{
name: "qualityRequest",
value: "0",
active: false,
description: "Quality System",
moduleName: "qualityRequest",
settingType: "feature",
roles: ["admin"],
seedVersion: 1,
},
{
name: "tms",
value: "0",
active: false,
description: "Transport system integration",
moduleName: "tms",
settingType: "feature",
roles: ["admin"],
seedVersion: 1,
},
{
name: "purchaseMonitor",
value: "5",
active: true,
description: "Monitors alpla purchase fo all changes",
moduleName: "purchase",
settingType: "feature",
roles: ["admin"],
seedVersion: 1,
},
// standard settings
{
name: "prolinkCheck",
value: "1",
active: true,
description:
"Will prolink be considered to check if matches, mainly used in plants that do not fully utilize prolink + ocp",
moduleName: "ocp",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "bookin",
value: "1",
active: true,
description: "Will we book in the labels after they are printed.",
moduleName: "ocp",
settingType: "standard",
roles: ["admin", "supervisor"],
seedVersion: 2,
},
{
name: "printDelay",
value: "90",
active: true,
description: "The default time between label printing",
moduleName: "ocp",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "dualPrinting",
value: "0",
active: true,
description: "Are we producing on 2 lines that pack into the 1 packer",
moduleName: "ocp",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "fifoCheck",
value: "45",
active: true,
description:
"This check is used to do a more near fifo check when pulling pallets for the agv's",
moduleName: "ocme",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "dayCheck",
value: "3",
active: true,
description: "how many days +/- to check for shipments in alplaprod",
moduleName: "ocme",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "maxLotsPerTruck",
value: "3",
active: true,
description:
"What is the maximum amount of lots that can be pulled for a truck this",
moduleName: "ocme",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "monitorAddress",
value: "8",
active: true,
description:
"What address(2) are we monitoring for lot restrictions. multiple addresses can be used but should be separated by a comma ,",
moduleName: "ocme",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "ocmeCycleCount",
value: "0",
active: true,
description:
"Are you enabling the system count page? meaning that we will allow a 'manual cycle count' vs full auto",
moduleName: "ocme",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "inhouseDelivery",
value: "0",
active: true,
description:
"This is for in-house plants that deliver direct to the customer, note if book-in is off this will be ignored ",
moduleName: "ocp",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "dycoConnect",
value: "0",
active: true,
description: "Connection to the dyco in dayton for the labeling process",
moduleName: "ocp",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "dycoPrint",
value: "0",
active: true,
description:
"This tells us we want to use the dyco system to print the labels",
moduleName: "ocp",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "strapperCheck",
value: "0",
active: true,
description:
"Strapper alarm check this is more used in tandem with dycoPrint setting",
moduleName: "ocp",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "rfid_ocp",
value: "0",
active: true,
description:
"Enable the rfid pallet mgt system this replaces dycoPrint but can be overridden by dycoPrint",
moduleName: "ocp",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "ocpCycleDelay",
value: "10",
active: true,
description:
"How long is the delay to fire off the printer check status, Defaulting to 10 seconds",
moduleName: "ocp",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "pNgAddress",
value: "139",
active: true,
description:
"This is the P&G address that is used when uploading the new forecast from P&G",
moduleName: "demandManagement",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "zechetti_1",
value: "0",
active: true,
description:
"Active the Zechetti 1 process, to print from and no longer utilize the pc",
moduleName: "ocp",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "zechetti_2",
value: "0",
active: true,
description:
"Active the Zechetti 2 process, to print from and no longer utilize the pc",
moduleName: "ocp",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "checkColor",
value: "0",
active: true,
description:
"During the material check are we going to check the color has enough provided to create the next pallet",
moduleName: "ocp",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "checkPKG",
value: "0",
active: true,
description:
"During the material check are we going to check the packaging has enough provided to create the next pallet",
moduleName: "ocp",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "lotPrintDelay",
value: "0",
active: true,
description:
"Override ride the printer printer delay setting, by default if this is on all printers are controlled by this.",
moduleName: "ocp",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
];
export const baseSettingValidationCheck = async () => {
const log = createLogger({ module: "system", subModule: "settings" });
const { data, error } = await tryCatch(
db
.insert(settings)
.values(newSettings)
.onConflictDoUpdate({
target: settings.name,
set: {
roles: sql`excluded.roles`,
description: sql`excluded.description`,
moduleName: sql`excluded."moduleName"`,
settingType: sql`excluded."settingType"`,
seedVersion: sql`excluded.seed_version`,
upd_user: "LST_System",
upd_date: sql`now()`,
},
where: sql`
settings.seed_version IS NULL
OR settings.seed_version < excluded.seed_version
`,
})
.returning(),
);
if (error) {
log.error(
{ error: error },
"There was an error when adding or updating the settings.",
);
}
if (data) {
log.info({}, "All Settings were added/updated");
}
};

View File

@@ -0,0 +1,55 @@
/**
* When a feature setting gets updated we will handle it here.
* we will stop jobs, stop cycles
*/
import { dbCleanup } from "../db/dbCleanup.controller.js";
import type { Setting } from "../db/schema/settings.schema.js";
import { monitorReleaseChanges } from "../opendock/openDockRreleaseMonitor.utils.js";
import {
killOpendockSocket,
opendockSocketMonitor,
} from "../opendock/opendockSocketMonitor.utils.js";
import { monitorAlplaPurchase } from "../purchase/purchase.controller.js";
import {
createCronJob,
resumeCronJob,
stopCronJob,
} from "../utils/croner.utils.js";
export const featureControl = async (data: Setting) => {
// when a feature is changed to active or deactivated we will update the cron.
if (data.active) {
resumeCronJob(data.name);
} else {
stopCronJob(data.name);
}
// specific setting stuff should have handled like below. what needs turned back on or off.
if (data.name === "opendock_sync" && data.active) {
opendockSocketMonitor();
monitorReleaseChanges();
createCronJob("opendockAptCleanup", "0 30 5 * * *", () =>
dbCleanup("opendockApt", 90),
);
}
if (data.name === "opendock_sync" && !data.active) {
killOpendockSocket();
stopCronJob("opendockAptCleanup");
}
// purchase stuff
if (data.name === "purchaseMonitor" && data.active) {
monitorAlplaPurchase();
}
if (data.name === "purchaseMonitor" && !data.active) {
stopCronJob("purchaseMonitor");
}
// this means the data time has changed
if (data.name === "purchaseMonitor" && data.value) {
monitorAlplaPurchase();
}
};

View File

@@ -0,0 +1,110 @@
import { eq, sql } from "drizzle-orm";
import { type Request, type Response, Router } from "express";
import { db } from "../db/db.controller.js";
import { settings } from "../db/schema/settings.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { featureControl } from "./settingsFeatures.controller.js";
// // 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.patch("/:name", async (req: Request, res: Response) => {
const { name } = req.params;
const updates: Record<string, unknown | null> = {};
// lets see if we even have a setting name
const { data: sName, error: sError } = await tryCatch(
db
.select()
.from(settings)
.where(eq(settings.name, name ?? "")),
);
if (sError) {
return apiReturn(res, {
success: false,
level: "error",
module: "system",
subModule: "settings",
message: `There was an error checking the name of the setting`,
data: [sError],
status: 400,
});
}
if (sName?.length === 0) {
return apiReturn(res, {
success: false,
level: "error",
module: "system",
subModule: "settings",
message: `The setting "${name}" dose not appear to be a valid setting please check the name and try again. `,
data: [],
status: 400,
});
}
// manage the actual setting. we will still do an upsert just in case we strangely get past everything
if (req.body?.value !== undefined) {
updates.value = req.body.value;
}
if (req.body?.description !== undefined) {
updates.description = req.body.description;
}
if (req.body?.moduleName !== undefined) {
updates.moduleName = req.body.moduleName;
}
if (req.body?.active !== undefined) {
updates.active = req.body.active === "true";
}
if (req.body?.roles !== undefined) {
updates.roles = req.body.roles;
}
if (req.body?.settingType !== undefined) {
updates.settingType = req.body.settingType;
}
updates.upd_user = req.user?.username || "lst_user";
updates.upd_date = sql`NOW()`;
const updatedSetting = await db
.update(settings)
.set(updates)
.where(eq(settings.name, name ?? ""))
.returning();
// the switch statment will only run when the setting actually updates
switch (updatedSetting[0]?.settingType) {
case "feature":
await featureControl(updatedSetting[0]);
break;
case "system":
// TODO: add the system control logic in to restart the app if not in dev mode
break;
default:
break;
}
return apiReturn(res, {
success: true,
level: "info",
module: "system",
subModule: "settings",
message: `Setting "${name}" Was just updated. `,
data: updatedSetting,
status: 200,
});
});
export default r;

View File

@@ -16,6 +16,7 @@ router.get("/", async (_, res) => {
res.status(200).json({
status: "ok",
uptime: process.uptime(),
nodeVersion: process.version,
memoryUsage: `Heap: ${(used.heapUsed / 1024 / 1024).toFixed(2)} MB / RSS: ${(
used.rss / 1024 / 1024
).toFixed(2)} MB`,

View File

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

9
backend/types/socket.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
import "socket.io";
import type { Session, User } from "better-auth"; // adjust if needed
declare module "socket.io" {
interface Socket {
user?: User | any;
session?: Session;
}
}

View File

@@ -0,0 +1,26 @@
import { createAccessControl } from "better-auth/plugins/access";
export const statement = {
app: ["read", "create", "share", "update", "delete", "readAll"],
user: ["ban"],
quality: ["read", "create", "share", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"],
} as const;
export const ac = createAccessControl(statement);
export const user = ac.newRole({
app: ["read", "create"],
notifications: ["read", "create"],
});
export const admin = ac.newRole({
app: ["read", "create", "update"],
});
export const systemAdmin = ac.newRole({
app: ["read", "create", "share", "update", "delete", "readAll"],
user: ["ban"],
quality: ["read", "create", "share", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"],
});

View File

@@ -1,17 +1,18 @@
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import {
admin,
apiKey,
createAuthMiddleware,
customSession,
admin as adminPlugin,
// apiKey,
// createAuthMiddleware,
//customSession,
jwt,
lastLoginMethod,
username,
} from "better-auth/plugins";
import { eq } from "drizzle-orm";
//import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import * as rawSchema from "../db/schema/auth.schema.js";
import { ac, admin, systemAdmin, user } from "./auth.permissions.js";
import { allowedOrigins } from "./cors.utils.js";
import { sendEmail } from "./sendEmail.utils.js";
@@ -31,57 +32,60 @@ export const auth = betterAuth({
provider: "pg",
schema,
}),
trustedOrigins: allowedOrigins,
user: {
additionalFields: {
role: {
type: "string",
required: false,
input: false,
},
lastLogin: {
type: "date",
required: true,
//required: false,
input: false,
},
},
},
plugins: [
jwt({ jwt: { expirationTime: "1h" } }),
apiKey(),
admin(),
//apiKey(),
adminPlugin({
ac,
roles: {
admin,
user,
systemAdmin,
},
}),
lastLoginMethod(),
username({
minUsernameLength: 5,
usernameValidator: (username) => {
if (username === "admin") {
if (username === "admin" || username === "root") {
return false;
}
return true;
},
}),
customSession(async ({ user, session }) => {
const roles = await db
.select({ roles: rawSchema.user.role })
.from(rawSchema.user)
.where(eq(rawSchema.user.id, session.id));
return {
roles,
user: {
...user,
//newField: "newField",
},
session,
};
}),
// customSession(async ({ user, session }) => {
// const roles = await db
// .select({ roles: rawSchema.user.role })
// .from(rawSchema.user)
// .where(eq(rawSchema.user.id, session.id));
// return {
// roles,
// user: {
// ...user,
// //newField: "newField",
// },
// session,
// };
// }),
],
trustedOrigins: allowedOrigins,
emailAndPassword: {
enabled: true,
minPasswordLength: 8, // optional config
resetPasswordTokenExpirySeconds: process.env.RESET_EXPIRY_SECONDS, // time in seconds
sendResetPassword: async ({ user, token }) => {
const frontendUrl = `${process.env.BETTER_AUTH_URL}/lst/app/user/resetpassword?token=${token}`;
const frontendUrl = `${process.env.URL}/lst/app/user/resetpassword?token=${token}`;
const expiryMinutes = Math.floor(
parseInt(process.env.RESET_EXPIRY_SECONDS ?? "3600", 10) / 60,
);
@@ -122,16 +126,16 @@ export const auth = betterAuth({
secure: false,
httpOnly: true,
},
hooks: {
after: createAuthMiddleware(async (ctx) => {
if (ctx.path.startsWith("/login")) {
const newSession = ctx.context.newSession;
if (newSession) {
// something here later
}
}
}),
},
// hooks: {
// after: createAuthMiddleware(async (ctx) => {
// if (ctx.path.startsWith("/login")) {
// const newSession = ctx.context.newSession;
// if (newSession) {
// // something here later
// }
// }
// }),
// },
events: {
// async onSignInSuccess({ user }: { user: User }) {
// await db

View File

@@ -12,6 +12,10 @@ export const allowedOrigins = [
"https://admin.socket.io",
"https://electron-socket-io-playground.vercel.app",
`${process.env.URL}`,
`http://${process.env.PROD_SERVER}:3000`,
`http://${process.env.PROD_SERVER}:3100`, // temp
`http://usmcd1olp082:3000`,
`${process.env.EXTERNAL_URL}`, // internal docker
];
export const lstCors = () => {
return cors({

View File

@@ -0,0 +1,38 @@
import { createLogger } from "../logger/logger.controller.js";
const minTime = 5;
export const minutesToCron = (minutes: number): string => {
const log = createLogger({ module: "system", subModule: "croner" });
if (minutes < minTime) {
log.error(
{},
`Conversion of less then ${minTime} min and is not allowed on this server`,
);
}
// Every X minutes (under 60)
if (minutes < 60) {
return `*/${minutes} * * * *`;
}
// Every X hours (clean division)
if (minutes % 60 === 0 && minutes < 1440) {
const hours = minutes / 60;
return `0 0 */${hours} * * *`;
}
// Every day
if (minutes === 1440) {
return `0 0 8 * * *`; // 8am
}
// Every X days (clean division)
if (minutes % 1440 === 0) {
const days = minutes / 1440;
return `0 0 0 */${days} * *`;
}
// Fallback (not cleanly divisible)
// Cron can't represent this perfectly → run every X minutes as approximation
return `0 */${minutes} * * * *`;
};

View File

@@ -18,9 +18,12 @@ export interface JobInfo {
// Store running cronjobs
export const runningCrons: Record<string, Cron> = {};
const activeRuns = new Set<string>();
const log = createLogger({ module: "system", subModule: "croner" });
const cronStats: Record<string, { created: number; replaced: number }> = {};
// how to se the times
// * ┌──────────────── (optional) second (0 - 59) \n
// * ┌──────────────── (optional) second (0 - 59)
// * │ ┌────────────── minute (0 - 59)
// * │ │ ┌──────────── hour (0 - 23)
// * │ │ │ ┌────────── day of month (1 - 31)
@@ -30,23 +33,43 @@ export const runningCrons: Record<string, Cron> = {};
// * │ │ │ │ │ │ ┌──── (optional) year (1 - 9999)
// * │ │ │ │ │ │ │
// * * 05 * * * * *
// note that when leaving * anywhere means ever part of that. IE * in the seconds part will run every second inside that min
/**
*
* @param name Name of the job we want to run
* @param schedule Cron expression (example: `*\/5 * * * * *`)
* @param task Async function that will run
* @param source we can add where it came from to assist in getting this tracked down, more for debugging
*/
export const createCronJob = async (
name: string,
schedule: string, // cron string with 8 8 IE: */5 * * * * * every 5th second
task: () => Promise<void>, // what function are we passing over
source = "unknown",
) => {
// get the timezone based on the os timezone set
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const log = createLogger({ module: "system", subModule: "croner" });
// initial go so just store it this is more for debugging to see if something crazy keeps happening
if (!cronStats[name]) {
cronStats[name] = { created: 0, replaced: 0 };
}
// Destroy existing job if it exist
if (runningCrons[name]) {
cronStats[name].replaced += 1;
log.warn(
{
job: name,
source,
oldSchedule: runningCrons[name].getPattern?.(),
newSchedule: schedule,
replaceCount: cronStats[name].replaced,
},
`Cron job "${name}" already existed and is being replaced`,
);
runningCrons[name].stop();
}
@@ -59,6 +82,13 @@ export const createCronJob = async (
name: name,
},
async () => {
if (activeRuns.has(name)) {
log.warn({ jobName: name }, "Skipping overlapping cron execution");
return;
}
activeRuns.add(name);
const startedAt = new Date();
const start = Date.now();
@@ -89,14 +119,19 @@ export const createCronJob = async (
.where(eq(jobAuditLog.id, executionId));
} catch (e: any) {
if (executionId) {
await db.update(jobAuditLog).set({
finishedAt: new Date(),
durationMs: Date.now() - start,
status: "error",
errorMessage: e.message,
errorStack: e.stack,
});
await db
.update(jobAuditLog)
.set({
finishedAt: new Date(),
durationMs: Date.now() - start,
status: "error",
errorMessage: e.message,
errorStack: e.stack,
})
.where(eq(jobAuditLog.id, executionId));
}
} finally {
activeRuns.delete(name);
}
},
);
@@ -124,11 +159,19 @@ export const removeCronJob = (name: string) => {
export const stopCronJob = (name: string) => {
if (runningCrons[name]) {
runningCrons[name].pause();
log.info(
{},
`${name} was just stopped either manually or due to a setting change.`,
);
}
};
export const resumeCronJob = (name: string) => {
if (runningCrons[name]) {
runningCrons[name].resume();
log.info(
{},
`${name} was just restarted either manually or due to a setting change.`,
);
}
};

View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{{!-- <link rel="stylesheet" href="styles/styles.css" /> --}}
{{> styles}}
</head>
<body>
<p>All,</p>
<p>The below labels have been reprinted.</p>
<table >
<thead>
<tr>
<th>AV</th>
<th>Description</th>
<th>Label Number</th>
<th>Date Added</th>
<th>Date Reprinted</th>
<th>Who printed/Updated</th>
<th>What printer it came from</th>
</tr>
</thead>
<tbody>
{{#each items}}
<tr>
<td>{{av}}</td>
<td>{{alias}}</td>
<td>{{RunningNumber}}</td>
<td>{{printDate}}</td>
<td>{{createdDateTime}}</td>
<td>{{ActorName}}</td>
<td>{{printerName}}</td>
</tr>
{{/each}}
</tbody>
</table>
<div>
<p>Thank you,</p>
<p>LST Team</p>
</div>
</body>
</html>

View File

@@ -1,9 +1,18 @@
import type { Response } from "express";
import { createLogger } from "../logger/logger.controller.js";
interface Data {
interface Data<T = unknown[]> {
success: boolean;
module: "system" | "ocp" | "routes" | "datamart" | "utils";
module:
| "system"
| "ocp"
| "routes"
| "datamart"
| "utils"
| "opendock"
| "notification"
| "email"
| "purchase";
subModule:
| "db"
| "labeling"
@@ -13,10 +22,20 @@ interface Data {
| "sendmail"
| "auth"
| "datamart"
| "jobs";
| "jobs"
| "apt"
| "settings"
| "get"
| "update"
| "delete"
| "post"
| "notification"
| "delete"
| "printing";
level: "info" | "error" | "debug" | "fatal";
message: string;
data?: unknown[];
room?: string;
data?: T;
notify?: boolean;
}
@@ -36,20 +55,21 @@ interface Data {
*/
export const returnFunc = (data: Data) => {
const notify = data.notify ? data.notify : false;
const room = data.room ?? data.room;
const log = createLogger({ module: data.module, subModule: data.subModule });
// handle the logging part
switch (data.level) {
case "info":
log.info({ notify: notify }, data.message);
log.info({ notify: notify, room }, data.message);
break;
case "error":
log.error({ notify: notify, error: data.data }, data.message);
log.error({ notify: notify, error: data.data, room }, data.message);
break;
case "debug":
log.debug({ notify: notify }, data.message);
log.debug({ notify: notify, room }, data.message);
break;
case "fatal":
log.fatal({ notify: notify }, data.message);
log.fatal({ notify: notify, room }, data.message);
}
// api section to return

View File

@@ -1,12 +1,9 @@
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import type { Transporter } from "nodemailer";
import nodemailer from "nodemailer";
import type Mail from "nodemailer/lib/mailer/index.js";
import type { Address } from "nodemailer/lib/mailer/index.js";
import type SMTPTransport from "nodemailer/lib/smtp-transport/index.js";
import hbs from "nodemailer-express-handlebars";
import { returnFunc } from "./returnHelper.utils.js";
import { tryCatch } from "./trycatch.utils.js";
@@ -24,62 +21,36 @@ interface EmailData {
}
export const sendEmail = async (data: EmailData) => {
let transporter: Transporter;
let fromEmail: string | Address;
const fromEmail: string = `DoNotReply@mail.alpla.com`;
const transporter: Transporter = nodemailer.createTransport({
host: "smtp.azurecomm.net",
port: 587,
//rejectUnauthorized: false,
tls: {
minVersion: "TLSv1.2",
},
auth: {
user: "donotreply@mail.alpla.com",
pass: process.env.SMTP_PASSWORD,
},
debug: true,
});
if (os.hostname().includes("OLP")) {
transporter = nodemailer.createTransport({
service: "gmail",
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASSWORD, // The 16-character App Password
},
//debug: true,
});
// update the from email
fromEmail = process.env.EMAIL_USER as string;
// update the from email
fromEmail = `donotreply@alpla.com`;
} else {
//create the servers smtp config
let host = `${os.hostname().replace("VMS006", "")}-smtp.alpla.net`;
if (os.hostname().includes("VMS036")) {
host = "USMCD1-smtp.alpla.net";
}
transporter = nodemailer.createTransport({
host: host,
port: 25,
rejectUnauthorized: false,
//secure: false,
// auth: {
// user: "alplaprod",
// pass: "obelix",
// },
debug: true,
} as SMTPTransport.Options);
// update the from email
fromEmail = `donotreply@alpla.com`;
}
// create the handlebars view
// creating the handlbar options
const viewPath = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"./mailViews",
"../utils/mailViews/",
);
const handlebarOptions = {
viewEngine: {
extname: ".hbs",
defaultLayout: "",
//layoutsDir: path.resolve(viewPath, "layouts"), // Path to layouts directory
defaultLayout: "", // Specify the default layout
partialsDir: viewPath,
},
viewPath: viewPath,
extName: ".hbs",
extName: ".hbs", // File extension for Handlebars templates
};
transporter.use("compile", hbs(handlebarOptions));
@@ -89,7 +60,7 @@ export const sendEmail = async (data: EmailData) => {
to: data.email,
subject: data.subject,
//text: "You will have a reset token here and only have 30min to click the link before it expires.",
//html: emailTemplate("Blake's Test", "This is an example with css"),
//html: emailTemplate("BlakesTest", "This is an example with css"),
template: data.template, // Name of the Handlebars template (e.g., 'welcome.hbs')
context: data.context,
};

View File

@@ -8,7 +8,7 @@
},
"files": {
"ignoreUnknown": false,
"includes": ["**", "!!**/dist","!!**/frontend", "!!**/lst_docs"]
"includes": ["**", "!!**/dist", "!!**/lst_docs"]
},
"formatter": {
"enabled": true,

37
brunoApi/auth/Login.bru Normal file
View File

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

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

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

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

View File

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

9
brunoApi/bruno.json Normal file
View File

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

3
brunoApi/collection.bru Normal file
View File

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

View File

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

View File

@@ -0,0 +1,20 @@
meta {
name: Run Query
type: http
seq: 2
}
get {
url: {{url}}/api/datamart/:name
body: none
auth: inherit
}
params:path {
name: activeArticles
}
settings {
encodeUrl: true
timeout: 0
}

View File

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

View File

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

View File

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

@@ -0,0 +1,24 @@
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.mattes@alpla.com","cowchmonkey@gmail.com"]
}
}
settings {
encodeUrl: true
timeout: 0
}

View File

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

View File

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

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

View File

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

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

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

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

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

View File

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

View File

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

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