53 Commits

Author SHA1 Message Date
152c1bcba0 Version Bump 0.1.4 2025-11-03 00:35:40 +00:00
5cdfd0f2e3 Change the Start Script and Added NODE_ENV to the env vars also for the dockerized Version 2025-11-03 00:33:23 +00:00
1c6aad0f3a Merge pull request 'fix/db-event-uid-create-unique-index' (#8) from fix/db-event-uid-create-unique-index into dev
Reviewed-on: #8
2025-11-03 00:28:36 +00:00
16593e0281 Fixed Syntax Error 2025-11-03 00:24:42 +00:00
eea37b3df5 Fixing the uid Column isn't unique.
uid is required to be unique for the the Changed Events (with the new Data) to be inserted without creating new Rows.
2025-11-03 00:22:01 +00:00
1a7de55da8 Prints "Optime" and a Diff to Optime for a Events thats not on Optime 2025-11-02 21:11:37 +00:00
2c34fece2c Merge pull request 'feature/notification-more-options' (#7) from feature/notification-more-options into dev
Reviewed-on: #7
2025-11-02 20:56:14 +00:00
4bbda5dcf8 Adding a parameter for the URLs for the Notification URLs of Services. 2025-10-29 23:49:48 +01:00
a57e4efd4c adding a file "config.ts" for adjustable configurations like URLs for Apprise 2025-10-29 23:48:09 +01:00
9ec83d8b87 adding a Helper Function to create QueryStrings for URLs 2025-10-29 23:47:04 +01:00
12e57a97f5 some minor fixes to the logging texts 2025-10-29 23:38:45 +01:00
c69eca5c08 updated README 2025-10-27 20:18:40 +01:00
dc76e14c9d changed start script to add env prod and dev 2025-10-27 20:17:52 +01:00
6e34f30d4a added some scripts for the sqlite db for cleanup 2025-10-27 20:16:53 +01:00
f1bc30a64d exclude any kind of .env from docker and git 2025-10-27 20:11:28 +01:00
d22dbaf971 Event.get_body() does only print the Diff to Optime if its not 00:00 2025-10-27 20:02:21 +01:00
c5c5d872d7 Added Event.toString() 2025-10-27 20:01:29 +01:00
7b594614c6 Merge branch 'version/0.1.3' into dev 2025-10-27 17:59:23 +01:00
608608aa56 Bugfix in sendNotification()
URL for Post Request returned a 404 because there was a " too much
2025-10-27 17:54:30 +01:00
04ef066158 Merge pull request 'Release Version v0.1.2' (#5) from v0.1.2 into main
Reviewed-on: #5
2025-10-26 20:17:20 +00:00
8bcb2618a2 Release Version v0.1.2 2025-10-26 21:15:23 +01:00
1433d37afa Merge pull request 'dev to main - v0.1.2' (#4) from dev into main
Reviewed-on: #4
2025-10-26 14:22:05 +00:00
c51263c947 Added a Workarond for the DST (European Daylight Saving Time (DST)) 2025-10-26 15:14:57 +01:00
8c161c6dc5 just moved the properties of Event up in the Class. 2025-10-26 15:12:41 +01:00
c1ad9c7494 Added env vars to function sendNotification 2025-10-26 14:08:10 +01:00
e9ead4e7bf Moved Function to get a Title and Body of a Event to the Event Class. 2025-10-26 14:07:28 +01:00
420076a8cf Changed Package Name. 2025-10-26 14:06:34 +01:00
d5a1bc9fa7 Added Helper Functions for events.deleteDate. Its stored as integer for unixtime. 2025-10-26 14:05:08 +01:00
76dfde05f7 added more env vars 2025-10-26 14:03:29 +01:00
79b7cfae68 Merge pull request 'dev v0.1.1' (#3) from dev into main
Reviewed-on: #3
2025-10-24 23:45:46 +00:00
d303560f53 rearranged some code.
splited function main() to 2 seperate functions.
2025-10-24 02:55:33 +02:00
eb6525a66f added 'deleteDate' to Events 2025-10-24 02:51:30 +02:00
f974684945 fixed inconsistent module loading with 'require' 2025-10-24 02:49:27 +02:00
b035c9475d Some Cleanup and minor Changes 2025-10-24 02:48:00 +02:00
f648952fa9 added Linting for TypeScript 2025-10-24 02:38:50 +02:00
5546d5511e replaced /etc/environment with /etc/cron-env.sh to work with cron 2025-10-22 19:10:04 +02:00
4e21b1372f added windows style new line clean up to all script files and linux resources. 2025-10-22 19:09:15 +02:00
f33324e9f8 docker-compose.yml cleanup 2025-10-22 19:07:33 +02:00
f9a1919d08 replaced the old Crontab with commands with a script to execute instead. Better logging for docker logs <container name>.
because cron does not execute script with env vars, they get dumped into a file and set for the runtime of the  script run-taks.sh
2025-10-22 16:28:25 +02:00
96e9e79aeb replaced CMD in the docker file with ENTRYPOINT and docker-entrypoint.sh 2025-10-22 16:26:04 +02:00
1729332373 removed Python and the Python Script, added the apprise-api docker container to the Docker-compose to send notificaiton 2025-10-21 23:34:50 +02:00
8c5d6de5a4 fixed not loading the env file for docker 2025-10-21 02:47:36 +02:00
5f735dce6a container healthchecks.. 2025-10-21 02:35:59 +02:00
de11a6934c fixing docker-compose.yml 2025-10-21 02:29:54 +02:00
457a49e754 fuck python, switching to apprise-api as a docker container 2025-10-21 02:22:51 +02:00
92a2c6956a forgot the ENV for bun 2025-10-21 01:26:18 +02:00
1bf297752d more dockerfile fixing 2025-10-21 01:23:41 +02:00
156a2db485 fixing docker file 2025-10-21 01:21:39 +02:00
714738dcba Fixing python... 2025-10-21 01:16:59 +02:00
66212229f5 fixed python dependency and Dockerfile Volume Bind 2025-10-20 23:48:31 +02:00
3167bd7976 added dotenv as python dependency 2025-10-20 23:24:56 +02:00
877d9e37b3 Udpated Dockerfile, changed Crontab for testing purposes 2025-10-20 23:16:35 +02:00
fd0081d4d0 Fixed dockerfile, added a docker-compose.yml, added db init function to run on every startup 2025-10-20 23:12:38 +02:00
36 changed files with 935 additions and 210 deletions

View File

@@ -9,7 +9,7 @@ LICENSE
.vscode .vscode
Makefile Makefile
helm-charts helm-charts
.env .env*
.editorconfig .editorconfig
.idea .idea
coverage* coverage*

View File

@@ -1,9 +1,15 @@
TZ=Europe/Berlin
DB_FILEPATH=./data/db
DB_FILENAME=77th_eventntfy.db
apprise_https=false
apprise_hostname=apprise
apprise_port=8000
notification_mock=true
ntfy_on=true ntfy_on=true
ntfy_username=chiko ntfy_username=chiko
ntfy_password=Blub ntfy_password=Blub
ntfy_host=ntfy.some-service.com ntfy_host=ntfy.some-service.com
ntfy_topic=SomeTopic ntfy_topic=SomeTopic
dc_on=true dc_on=true
dc_webhook=123123123123123/ABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEF dc_webhook=123123123123123/ABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEF
dc_botname=Botname Here dc_botname=Botname Here

1
.eslintignore Normal file
View File

@@ -0,0 +1 @@
.eslintrc.cjs

17
.eslintrc Normal file
View File

@@ -0,0 +1,17 @@
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/no-floating-promises": [
"error",
{
"ignoreVoid": false,
"ignoreIIFE": false
}
]
}
}

17
.eslintrc.js Normal file
View File

@@ -0,0 +1,17 @@
/* eslint-env node */
export default {
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
parserOptions: {
project: "./tsconfig.json",
sourceType: "module"
},
rules: {
"@typescript-eslint/no-floating-promises": "error"
}
};

2
.gitignore vendored
View File

@@ -23,6 +23,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
.env.test.local .env.test.local
.env.production.local .env.production.local
.env.local .env.local
.env.*
# caches # caches
.eslintcache .eslintcache
@@ -37,3 +38,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
*.db *.db
*.sqlite *.sqlite
data

7
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"eslint.enable": true,
"eslint.validate": [
"javascript",
"typescript"
]
}

View File

@@ -1,2 +0,0 @@
1 * * * * bun run ./src/app.ts --today > /dev/null 2>&1
0 * * * * bun run ./src/app.ts > /dev/null 2>&1

View File

@@ -1,12 +1,32 @@
FROM debian:12 AS base FROM debian:12 AS base
ARG BUILD_DATE
ARG VERSION
LABEL build_version="77th_eventcalendarntfy ${VERSION}, Build-date:- ${BUILD_DATE}"
LABEL maintainer="chiko <chiko@xcsone.de>"
ENV TZ=Europe/Berlin
WORKDIR /opt/app WORKDIR /opt/app
RUN apt-get update && \ RUN set -eux && \
# apt-get install -y curl unzip cron ca-certificates python3 python3-pip && \ echo "Updating APT" && \
apt-get install -y curl unzip cron ca-certificates python3 python3-pip && \ apt-get update -y -qq && \
rm -rf /var/lib/apt/lists/* apt-get upgrade -y -qq && \
echo "Installing tools" && \
apt-get install -y -qq \
curl unzip cron ca-certificates logrotate dos2unix tzdata && \
echo "Remove exim" && \
apt-get remove -y -qq exim4 exim4-base exim4-daemon-light && \
echo "Cleaning up" && \
apt-get --yes autoremove --purge -qq && \
apt-get clean --yes -qq && \
rm --recursive --force --verbose /var/lib/apt/lists/* && \
rm --recursive --force --verbose /tmp/* && \
rm --recursive --force --verbose /var/tmp/* && \
rm --recursive --force --verbose /var/cache/apt/archives/* && \
truncate --size 0 /var/log/*log
# install BunJs # install BunJs
RUN curl -fsSL https://bun.com/install | bash RUN curl -fsSL https://bun.com/install | bash
ENV PATH="/root/.bun/bin:$PATH" ENV PATH="/root/.bun/bin:$PATH"
RUN ln -s /root/.bun/bin/bun /usr/local/bin/bun
# install dependencies into temp directory # install dependencies into temp directory
# this will cache them and speed up future builds # this will cache them and speed up future builds
FROM base AS install FROM base AS install
@@ -19,33 +39,26 @@ RUN mkdir -p /temp/prod
COPY package.json bun.lock /temp/prod/ COPY package.json bun.lock /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production RUN cd /temp/prod && bun install --frozen-lockfile --production
# and install python dependencies
# COPY ./requirements.txt .
# RUN python3 -m pip install --break-system-packages -r requirements.txt
# RUN python3 -m pip install -U python-dotenv
# copy node_modules from temp directory # copy node_modules from temp directory
# then copy all (non-ignored) project files into the image # then copy all (non-ignored) project files into the image
FROM base AS prerelease FROM base AS prerelease
COPY --from=install /temp/dev/node_modules node_modules COPY --from=install /temp/dev/node_modules node_modules
COPY . ./ COPY . .
# [optional] tests & build # [optional] tests & build
ENV NODE_ENV=production
# copy production dependencies and source code into final image # copy production dependencies and source code into final image
FROM base AS release FROM base AS release
WORKDIR /opt/app ENV NODE_ENV=production
COPY --from=install /temp/prod/node_modules node_modules COPY --from=install /temp/prod/node_modules node_modules
COPY --from=prerelease /opt/app/src/app.ts . # COPY ./docker/cron-bun-log /etc/logrotate.d/
COPY --from=prerelease /opt/app/package.json . COPY ./docker/Crontab /etc/cron.d/
#COPY --from=prerelease .entrypoint.sh . RUN chmod 0644 /etc/cron.d/Crontab && \
COPY Crontab /etc/cron.d/ chown root:root /etc/cron.d/Crontab
RUN chmod 0644 /etc/cron.d/Crontab COPY . .
COPY . ./ RUN dos2unix \
# USER bun /opt/app/docker/docker-entrypoint.sh \
RUN touch /var/log/cron.log /opt/app/run-task.sh \
# RUN chmod +x entrypoint.sh /etc/cron.d/Crontab
# ENTRYPOINT ["./entrypoint.sh"] RUN chmod +x /opt/app/docker/docker-entrypoint.sh && \
VOLUME /opt/app/data/db chmod +x /opt/app/run-task.sh
CMD bun run ./src/app.ts --today && cron && tail -f /var/log/cron.log ENTRYPOINT [ "/opt/app/docker/docker-entrypoint.sh" ]

View File

@@ -1,15 +1,27 @@
# 77th Event Calender Notifcations # 77th Event Calendar Notifcations
To install dependencies: To install dependencies:
```bashe ```bash
bun install bun install
``` ```
To run: To run:
```bash ```bash
bun run index.ts bun run ./src/app.ts
bun run start
bun run dev
``` ```
This project was created using `bun init` in bun v1.3.0. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. ## Docker
```bash
docker compose build
docker compose up -d
```
## Parameter
### --today
fetch all Events, track all Changes (new, changed and deleted Events) and additionally Send a Notification for todays mission

254
bun.lock
View File

@@ -8,30 +8,282 @@
"minimist": "^1.2.8", "minimist": "^1.2.8",
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.38.0",
"@types/bun": "^1.3.0", "@types/bun": "^1.3.0",
"@types/minimist": "^1.2.5",
"@typescript-eslint/eslint-plugin": "^8.46.2",
"@typescript-eslint/parser": "^8.46.2",
"eslint": "^9.38.0",
"globals": "^16.4.0",
"jiti": "^2.6.1",
"typescript-eslint": "^8.46.2",
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5", "typescript": "^5.9.3",
}, },
}, },
}, },
"packages": { "packages": {
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
"@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.1", "", { "dependencies": { "@eslint/core": "^0.16.0" } }, "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw=="],
"@eslint/core": ["@eslint/core@0.16.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
"@eslint/js": ["@eslint/js@9.38.0", "", {}, "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0", "levn": "^0.4.1" } }, "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
"@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="], "@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/minimist": ["@types/minimist@1.2.5", "", {}, "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag=="],
"@types/node": ["@types/node@24.8.1", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q=="], "@types/node": ["@types/node@24.8.1", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q=="],
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.46.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/type-utils": "8.46.2", "@typescript-eslint/utils": "8.46.2", "@typescript-eslint/visitor-keys": "8.46.2", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.46.2", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.46.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", "@typescript-eslint/typescript-estree": "8.46.2", "@typescript-eslint/visitor-keys": "8.46.2", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.46.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.46.2", "@typescript-eslint/types": "^8.46.2", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.2", "", { "dependencies": { "@typescript-eslint/types": "8.46.2", "@typescript-eslint/visitor-keys": "8.46.2" } }, "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.46.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.46.2", "", { "dependencies": { "@typescript-eslint/types": "8.46.2", "@typescript-eslint/typescript-estree": "8.46.2", "@typescript-eslint/utils": "8.46.2", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.46.2", "", {}, "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.46.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.46.2", "@typescript-eslint/tsconfig-utils": "8.46.2", "@typescript-eslint/types": "8.46.2", "@typescript-eslint/visitor-keys": "8.46.2", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", "@typescript-eslint/typescript-estree": "8.46.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.46.2", "", { "dependencies": { "@typescript-eslint/types": "8.46.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="], "bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@9.38.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.1", "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.38.0", "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw=="],
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="],
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"typescript-eslint": ["typescript-eslint@8.46.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.46.2", "@typescript-eslint/parser": "8.46.2", "@typescript-eslint/typescript-estree": "8.46.2", "@typescript-eslint/utils": "8.46.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg=="],
"undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
"@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
} }
} }

View File

@@ -1,5 +1,6 @@
services: services:
app: app:
image: chiko/77th_eventcalendarntfy:v0.1.4
build: . build: .
volumes: volumes:
- ./data/db:/opt/app/data/db - ./data/db:/opt/app/data/db
@@ -12,7 +13,7 @@ services:
links: links:
- apprise - apprise
apprise: apprise:
image: caronc/apprise:latest image: caronc/apprise:1.2.2
hostname: apprise hostname: apprise
environment: environment:
- APPRISE_WORKER_COUNT=1 - APPRISE_WORKER_COUNT=1
@@ -23,14 +24,10 @@ services:
- ./data/apprise/config:/config - ./data/apprise/config:/config
- ./data/apprise/plugin:/plugin - ./data/apprise/plugin:/plugin
- ./data/apprise/attach:/attach - ./data/apprise/attach:/attach
ports: # ports:
- 8000:8000 #- 8880:8000
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/status"] test: ["CMD", "curl", "-f", "http://localhost:8000/status"]
interval: 30s interval: 5s
timeout: 10s timeout: 3s
retries: 5 retries: 5
# networks:
# default:
# external: true
# name: npm

4
docker/Crontab Normal file
View File

@@ -0,0 +1,4 @@
SHELL=/bin/bash
MAILTO=""
0 8 * * * root . /etc/cron-env.sh && /opt/app/run-task.sh --today >> /proc/1/fd/1 2>&1
*/15 * * * * root . /etc/cron-env.sh && /opt/app/run-task.sh >> /proc/1/fd/1 2>&1

8
docker/cron-bun-log Normal file
View File

@@ -0,0 +1,8 @@
/var/log/cron.log {
daily
rotate 7
compress
missingok
notifempty
copytruncate
}

View File

@@ -0,0 +1,41 @@
#!/bin/bash
# Create or overwrite the cron env file
cat /dev/null > /etc/cron-env.sh
chmod 600 /etc/cron-env.sh
chmod +x /etc/cron-env.sh
# Write the Env Vars into a file for cron. happens during runtime of the container and not build.
# List your environment variables here
env_vars=(
NODE_ENV
TZ
DB_FILEPATH
DB_FILENAME
apprise_https
apprise_hostname
apprise_port
notification_mock
ntfy_on
ntfy_username
ntfy_password
ntfy_host
ntfy_topic
dc_on
dc_webhook
dc_botname
dc_avatar_url
)
for var in "${env_vars[@]}"; do
val="${!var}"
if [ -n "$val" ]; then
# Safely export the variable with proper quoting
printf 'export %s=%q\n' "$var" "$val" >> /etc/cron-env.sh
fi
done
export PATH="/root/.bun/bin:$PATH"
bun run /opt/app/src/app.ts --today
# Start cron in foreground
exec cron -f

View File

@@ -1,6 +0,0 @@
#!/bin/bash
crontab -l > mycron
echo "0 8 * * * bun run ./src/app.ts --today > /dev/null 2>&1" >> mycron
echo "0 * * * * bun run ./src/app.ts > /dev/null 2>&1" >> mycron
crontab mycron
rm mycron

9
eslint.config.ts Normal file
View File

@@ -0,0 +1,9 @@
import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
import { defineConfig } from "eslint/config";
export default defineConfig([
{ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.browser } },
tseslint.configs.recommended,
]);

View File

@@ -1,23 +1,32 @@
{ {
"version": "0.1.0", "version": "0.1.4",
"name": "eventcalender", "name": "77th_eventcalendarnotification",
"module": "./src/app.ts", "module": "./src/app.ts",
"type": "module", "type": "module",
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"@types/bun": "^1.3.0" "@eslint/js": "^9.38.0",
"@types/bun": "^1.3.0",
"@types/minimist": "^1.2.5",
"@typescript-eslint/eslint-plugin": "^8.46.2",
"@typescript-eslint/parser": "^8.46.2",
"eslint": "^9.38.0",
"globals": "^16.4.0",
"jiti": "^2.6.1",
"typescript-eslint": "^8.46.2"
}, },
"scripts": { "scripts": {
"dev": "bun run ./src/app.ts", "start": "bun run ./src/app.ts",
"dev:init": "bun run ./src/app.ts --init", "dev": "NODE_ENV=development bun ./src/app.ts",
"db:init": "bun run ./run/db_init.ts", "db:init": "bun run ./run/db_init.ts",
"db:deleteall": "bun run ./run/db_deleteall.ts", "db:deleteall": "bun run ./run/db_event_deleteall.ts",
"build": "bun build --compile --minify --sourcemap ./src/app.ts --outfile ./build/77th_event_calendar_notification", "db:event:dedup": "bun run ./run/db_event_delete_duplicates.ts",
"build:linux": "bun build --compile --minify --sourcemap --target=bun-linux-arm64 ./src/app.ts --outfile ./build/77th_event_calendar_notification", "build": "bun build --compile --minify --sourcemap ./src/app.ts --outfile ./build/77th_eventcalendarnotification",
"build:linux": "bun build --compile --minify --sourcemap --target=bun-linux-arm64 ./src/app.ts --outfile ./build/77th_eventcalendarnotification",
"docker:build": "docker build -t chiko/77th_eventcalendarntfy:0.1.0 ." "docker:build": "docker build -t chiko/77th_eventcalendarntfy:0.1.0 ."
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5" "typescript": "^5.9.3"
}, },
"dependencies": { "dependencies": {
"dotenv": "^17.2.3", "dotenv": "^17.2.3",

View File

@@ -1,2 +0,0 @@
apprise
python-dotenv

26
run-task.sh Normal file
View File

@@ -0,0 +1,26 @@
#!/bin/bash
# set -e
export PATH="/root/.bun/bin:$PATH"
set -o allexport
. /etc/cron-env.sh || echo "[WARN] Failed to load env" >> /proc/1/fd/2
set +o allexport
log_info() {
echo "[INFO] $(date) $1" >> /proc/1/fd/1
}
log_error() {
echo "[ERROR] $(date) $1" >> /proc/1/fd/2
}
log_info "Starting task with args: $*"
cd /opt/app
if bun run start "$@" >> /proc/1/fd/1 2>> /proc/1/fd/2; then
log_info "Task completed successfully."
else
log_error "Task failed!"
exit 1
fi

View File

@@ -0,0 +1,10 @@
import * as db from "../src/sql";
const query = db.db.query(`DELETE FROM events
WHERE rowid NOT IN (
SELECT MIN(rowid)
FROM events
GROUP BY uid
);`);
query.run();

View File

@@ -0,0 +1,39 @@
import db from "../src/sql";
const run_migration = db.transaction(() => {
// SQL 1: Insert a new user
db.run(`DELETE FROM events
WHERE rowid NOT IN (
SELECT MIN(rowid)
FROM events
GROUP BY uid
);`);
// SQL 2: Update product stock
db.run(`CREATE TABLE events_new (
"event_uid" INTEGER PRIMARY KEY,
"uid" TEXT NOT NULL UNIQUE,
"title" TEXT NOT NULL,
"date_at" DATETIME NOT NULL,
"time_start" TEXT NOT NULL,
"time_end" TEXT NOT NULL,
"posted_by" TEXT NOT NULL,
"location" TEXT NOT NULL,
"event_type" TEXT NOT NULL,
"link" TEXT NOT NULL,
"description" TEXT NOT NULL,
"timezone" TEXT NOT NULL,
"notification" TEXT NOT NULL,
"deleteDate" INTEGER NULL
);`);
// SQL 3: Log the transaction
db.run(`INSERT INTO events_new (event_uid, uid, title, date_at, time_start, time_end, posted_by, location, event_type, link, description, timezone, notification, deleteDate)
SELECT event_uid, uid, title, date_at, time_start, time_end, posted_by, location, event_type, link, description, timezone, notification, deleteDate FROM events;
`);
db.run(`DROP TABLE events;
ALTER TABLE events_new RENAME TO events;`);
});
// Run the transaction
run_migration();

View File

@@ -0,0 +1 @@
CREATE UNIQUE INDEX idx_events_uid ON events(uid);

View File

@@ -0,0 +1,6 @@
DELETE FROM events
WHERE rowid NOT IN (
SELECT MIN(rowid)
FROM events
GROUP BY uid
);

View File

@@ -0,0 +1,4 @@
SELECT uid, COUNT(*) AS count
FROM events
GROUP BY uid
HAVING COUNT(*) > 1;

View File

@@ -0,0 +1,29 @@
DELETE FROM events
WHERE rowid NOT IN (
SELECT MIN(rowid)
FROM events
GROUP BY uid
);
CREATE TABLE events_new (
"event_uid" INTEGER PRIMARY KEY,
"uid" TEXT NOT NULL UNIQUE,
"title" TEXT NOT NULL,
"date_at" DATETIME NOT NULL,
"time_start" TEXT NOT NULL,
"time_end" TEXT NOT NULL,
"posted_by" TEXT NOT NULL,
"location" TEXT NOT NULL,
"event_type" TEXT NOT NULL,
"link" TEXT NOT NULL,
"description" TEXT NOT NULL,
"timezone" TEXT NOT NULL,
"notification" TEXT NOT NULL,
"deleteDate" INTEGER NULL
);
INSERT INTO events_new (event_uid, uid, title, date_at, time_start, time_end, posted_by, location, event_type, link, description, timezone, notification, deleteDate)
SELECT event_uid, uid, title, date_at, time_start, time_end, posted_by, location, event_type, link, description, timezone, notification, deleteDate FROM events;
DROP TABLE events;
ALTER TABLE events_new RENAME TO events;

View File

@@ -1,48 +1,49 @@
import { TEventType } from "./component/event/event.types";
import { db } from "./sql"; import { db } from "./sql";
import { Event, type TEventEntityNew, type TGetEventsOptions } from "./component/event/events"; import { Event, type TEventEntityNew, type TGetEventsOptions } from "./component/event/events";
import { createPlaceholders, getTsNow } from "./util";
import { sendNotification } from "./sendNotification"; import { sendNotification } from "./sendNotification";
import { createPlaceholders, pad_l2 } from "./util"; import minimist from "minimist";
const argv = minimist(process.argv.slice(2))
console.log("App started");
console.dir({argv})
const argv = require('minimist')(process.argv.slice(2)); const TODAY = getTsNow();
console.dir(argv) console.dir({TODAY});
// const TS_TODAY = new Date(); async function events_update_db() {
const events_fetched_currentMonth = await Event.fetch_events( TODAY.year, TODAY.month , -120 );
console.log("events_fetched_currentMonth.length: " + events_fetched_currentMonth.length );
const events_fetched_nextMonth = await Event.fetch_events( TODAY.year, TODAY.month + 1 , -120 );
console.log("events_fetched_nextMonth.length: " + events_fetched_nextMonth.length );
const events_fetched = [...events_fetched_currentMonth, ...events_fetched_nextMonth];
console.log("events_fetched.length: " + events_fetched.length );
function getTsNow() { const events_fetched_list_of_uids = events_fetched.map( event => { return event.uid; });
const now = new Date(); console.dir({events_fetched_list_of_uids} );
const rtn = {
year: now.getFullYear(),
month: now.getMonth() + 1,
day: now.getDate(),
minute: now.getMinutes(),
seconds: now.getSeconds()
}
return rtn;
}
async function main( ) { const events_db_currentMonth = Event.get_events({month: {year: TODAY.year, month: TODAY.month}}, db);
const TODAY = getTsNow(); const events_removed: Event[] = events_db_currentMonth.filter( (ev) => {
const events_currentMonth = await Event.fetch_events( TODAY.year, TODAY.month , -120 ); return ! events_fetched_list_of_uids.includes(ev.uid);
const events_nextMonth = await Event.fetch_events( TODAY.year, TODAY.month + 1 , -120 ); });
const events = [...events_currentMonth, ...events_nextMonth];
// Write to JSON File Section START
// const data = JSON.stringify(events, null, 2);
// const TS = `${TS_TODAY.getFullYear()}-${TS_TODAY.getMonth() + 1}-${TS_TODAY.getDate()}_${TS_TODAY.getHours()}-${TS_TODAY.getMinutes()}-${TS_TODAY.getSeconds()}`;
// await Bun.write(path.join(import.meta.dir, "output", `output_${TS}.json`), data );
// Write to JSON File Section END
const allEventUids = events.map( event => { return event.uid; }); console.dir({events_removed});
const placeholders = createPlaceholders( allEventUids );
events_removed.forEach( ev => {
ev.set_notification("removed", db);
});
const placeholders = createPlaceholders( events_fetched_list_of_uids );
const getAllRelevantEventsQuery = db.query( const getAllRelevantEventsQuery = db.query(
`SELECT * FROM events WHERE uid IN (${placeholders}); ` `SELECT * FROM events WHERE uid IN (${placeholders}) AND deleteDate IS NULL;`
).as(Event ); ).as(Event );
const AllRelevantEvents = getAllRelevantEventsQuery.all(...allEventUids); const AllRelevantEvents = getAllRelevantEventsQuery.all(...events_fetched_list_of_uids);
console.log("AllRelevantEvents.length: " + AllRelevantEvents.length );
const eventsToInsert: TEventEntityNew[] = []; const eventsToInsert: TEventEntityNew[] = [];
for ( const ev of events ) { for ( const ev of events_fetched ) {
console.log("loop ev " + ev.uid + " : " + [ ev.title, ev.date_at ].join( ", " ) );
const found = AllRelevantEvents.find(event => event.uid === ev.uid); const found = AllRelevantEvents.find(event => event.uid === ev.uid);
if ( found ) { if ( found ) {
console.log("loop ev " + ev.uid + " found: " + [ found.title, found.date_at ].join( ", " ) );
if ( if (
found.title != ev.title || found.title != ev.title ||
found.description != ev.description || found.description != ev.description ||
@@ -55,87 +56,59 @@ async function main( ) {
found.timezone != ev.timezone || found.timezone != ev.timezone ||
found.link != ev.link found.link != ev.link
) { ) {
console.log("loop ev " + ev.uid + " different (changed): " + [ ev.title, ev.date_at ].join( ", " ) );
const newEventToInsert: TEventEntityNew = {... ev, notification: "changed"}; const newEventToInsert: TEventEntityNew = {... ev, notification: "changed"};
eventsToInsert.push( newEventToInsert ); eventsToInsert.push( newEventToInsert );
} }
} else { } else {
console.log("loop ev " + ev.uid + " added (new): " + [ ev.title, ev.date_at ].join( ", " ) );
const newEventToInsert: TEventEntityNew = {... ev, notification: "new"}; const newEventToInsert: TEventEntityNew = {... ev, notification: "new"};
eventsToInsert.push( newEventToInsert ); eventsToInsert.push( newEventToInsert );
} }
} }
console.dir({eventsToInsert})
Event.insert( eventsToInsert, db); Event.insert( eventsToInsert, db);
const options: TGetEventsOptions = { }
async function events_check_for_notification() {
const where: TGetEventsOptions = {
notification: ["new", "changed", "removed"],
deleted: false
} }
if (argv.today) { if ( argv.today ) {
options.date = { where.date = {
year: TODAY.year, year: TODAY.year,
month: TODAY.month, month: TODAY.month,
day: TODAY.day day: TODAY.day
} }
} else {
options.notification = ["new", "changed"]
} }
const list_of_events = Event.get_events( options, db ); const list_of_events = Event.get_events( where, db );
console.dir({
list_of_events,
where
});
for ( const ev of list_of_events ) { for ( const ev of list_of_events ) {
const body = [ console.log("loop list_of_events - ev: " + [ ev.uid, ev.title, ev.date_at, "notification: " + ev.notification ].join( ", " ) );
`Title: ${ev.title}`, console.log("loop list_of_events - ev 'title': " + ev.get_title() );
`Location: ${ev.location}`, const notificationOptions = {
`Type: ${ TEventType[ ev.event_type ] }`, ntfy: null,
`Date: ${ev.date_at}`, discord: {
`Time: ${ev.time_start}`, avatar_url: ( process.env.dc_avatar_url as string),
`By: ${ev.posted_by}`, botname: ( process.env.dc_botname as string)
`Link: ${ev.link}`,
].join("\n");
const notification_prefix = ( (event: Event) => {
switch( event.notification) {
case "new":
return "New";
case "changed":
return "Changed";
case "deleted":
return "Deleted";
default:
return null;
} }
} ) ( ev ); };
await sendNotification( ev.get_title(), ev.get_body(), notificationOptions );
const today_prefix = ( (ev: Event) => { if ( ev.notification == "removed" ) {
const now = getTsNow(); ev.set_deleted( db );
const [year, month, day] = ev.date_at.split("-") }
if (
year == String(now.year) &&
month == pad_l2( String(now.month) ) &&
day == pad_l2( String( now.day ) )
) {
return true;
}
return false;
})( ev );
const title = `${today_prefix ? "TODAY " : ""}${notification_prefix ? notification_prefix + ": " : ""} ${ev.title} (${ TEventType[ ev.event_type ] })`;
await fetch("http://apprise:8000/notify", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
urls: [
`ntfys://${process.env.ntfy_username}:${process.env.ntfy_password}@${process.env.ntfy_host}/${process.env.ntfy_topic}${ ev.link ? `?click=${ev.link}`: "?click=https://77th-jsoc.com/#/events" }`,
`discord://${process.env.dc_webhook}?avatar_url=${process.env.dc_avatar_url}&botname=${process.env.dc_botname}`
].join(","),
title: title,
body: body,
format: "text"
})
});
// await sendNotification(
// title,
// body
// // `${ev.link || "https://77th-jsoc.com/#/events"}`
// );
ev.set_notification("done", db); ev.set_notification("done", db);
} }
}
async function main ( ) {
console.log("Excecuting main()");
await events_update_db();
await events_check_for_notification();
}; };
main(); main();

View File

@@ -15,5 +15,6 @@ export type TEvent = {
location: string, location: string,
event_type: keyof typeof TEventType, event_type: keyof typeof TEventType,
timezone: string, timezone: string,
link: string link: string,
deleteDate?: number | null
}; };

View File

@@ -1,6 +1,6 @@
import { Database } from "bun:sqlite"; import { Database } from "bun:sqlite";
import type { TEvent } from "./event.types"; import { TEventType, type TEvent } from "./event.types";
import { transformArray } from "../../util"; import { getTsNow, pad_l2, transformArray, formatTimeDiff, isEuropeanDST, subtractHours } from "../../util";
const BASE_URL = "https://77th-jsoc.com/service.php?action=get_events"; const BASE_URL = "https://77th-jsoc.com/service.php?action=get_events";
@@ -10,38 +10,64 @@ export type TGetEventsOptions = {
year: number, year: number,
month: number, month: number,
day: number day: number
} },
month?: {
year: number,
month: number,
},
deleted?: boolean
} }
export type TEventEntity = TEvent & { export type TEventEntity = TEvent & {
event_uid: number event_uid: number
notification: "new" | "changed" | "deleted" | "done" notification: "new" | "changed" | "removed" | "done"
} }
export type TEventEntityNew = Omit<TEventEntity, "event_uid"> export type TEventEntityNew = Omit<TEventEntity, "event_uid">
export class Event implements TEventEntity { export class Event implements TEventEntity {
static table_name: "events" static table_name: "events"
event_uid: number;
uid: string;
title: string;
description: string;
date_at: string;
time_start: string;
time_end: string;
posted_by: string;
location: string;
event_type: TEventEntity["event_type"];
timezone: string;
link: string;
notification: TEventEntity["notification"];
deleteDate: TEventEntity["deleteDate"];
static createTable (db: Database): void { static createTable (db: Database): void {
const query = db.query(`CREATE TABLE IF NOT EXISTS events ( const query = db.query(`CREATE TABLE IF NOT EXISTS "events" (
event_uid INTEGER PRIMARY KEY, "event_uid" INTEGER PRIMARY KEY,
uid TEXT NOT NULL UNIQUE, "uid" TEXT NOT NULL UNIQUE,
title TEXT NOT NULL, "title" TEXT NOT NULL,
date_at DATETIME NOT NULL, "date_at" DATETIME NOT NULL,
time_start TEXT NOT NULL, "time_start" TEXT NOT NULL,
time_end TEXT NOT NULL, "time_end" TEXT NOT NULL,
posted_by TEXT NOT NULL, "posted_by" TEXT NOT NULL,
location TEXT NOT NULL, "location" TEXT NOT NULL,
event_type TEXT NOT NULL, "event_type" TEXT NOT NULL,
link TEXT NOT NULL, "link" TEXT NOT NULL,
description TEXT NOT NULL, "description" TEXT NOT NULL,
timezone TEXT NOT NULL, "timezone" TEXT NOT NULL,
notification TEXT NOT NULL DEFAULT "new" "notification" TEXT NOT NULL,
"deleteDate" INTEGER NULL
);`); );`);
query.run(); query.run();
} }
static insert ( events: TEventEntityNew[], db: Database ) { static insert ( events: TEventEntityNew[], db: Database ) {
const insert = db.prepare("INSERT OR REPLACE INTO events (uid, title, date_at, time_start, time_end, posted_by, location, event_type, link, description, timezone, notification) VALUES ($uid, $title, $date_at, $time_start, $time_end, $posted_by, $location, $event_type, $link, $description, $timezone, $notification)"); const insert = db.prepare( [
"INSERT OR REPLACE INTO events",
"(uid, title, date_at, time_start, time_end, posted_by, location, event_type, link, description, timezone, notification)",
"VALUES",
"($uid, $title, $date_at, $time_start, $time_end, $posted_by, $location, $event_type, $link, $description, $timezone, $notification)"
].join(" "));
const insertEvents = db.transaction(events => { const insertEvents = db.transaction(events => {
for (const event of events) insert.run(event); for (const event of events) insert.run(event);
return events.length; return events.length;
@@ -71,32 +97,27 @@ export class Event implements TEventEntity {
if (options.date) { if (options.date) {
whereConditions.push(`date_at = "${options.date.year}-${options.date.month}-${options.date.day}"`); whereConditions.push(`date_at = "${options.date.year}-${options.date.month}-${options.date.day}"`);
} }
if ( options.month ) {
whereConditions.push( `strftime('%Y-%m', date_at) = '${options.month.year}-${options.month.month}'`)
}
const where = ( () => { const where = ( () => {
let str = "WHERE "; let str = "WHERE ";
if ( whereConditions.length >= 1 ) { if ( options.deleted === true ) {
str += whereConditions.join(" OR "); str += "deleteDate IS NOT NULL AND ";
} else if ( options.deleted === false ) {
str += "deleteDate IS NULL AND ";
} }
return str; if ( whereConditions.length >= 1 ) {
return str += `( ${ whereConditions.join(" OR ") } )`;
}
return null;
})() })()
const query = db.query(`SELECT * FROM events${ where ? ( " " + where ) : ""};`).as(Event); const query = db.query(`SELECT * FROM events${ where ? ( " " + where ) : ""};`).as(Event);
return query.all(); return query.all();
} }
event_uid: number; constructor(event_uid: number, uid: string, title: string, description: string, date_at: string, time_start: string, time_end: string, posted_by: string, location: string, event_type: TEventEntity["event_type"], timezone: string, link: string, notification: TEventEntity["notification"], deleteDate: TEventEntity["deleteDate"]) {
uid: string;
title: string;
description: string;
date_at: string;
time_start: string;
time_end: string;
posted_by: string;
location: string;
event_type: TEventEntity["event_type"];
timezone: string;
link: string;
notification: TEventEntity["notification"]
constructor(event_uid: number, uid: string, title: string, description: string, date_at: string, time_start: string, time_end: string, posted_by: string, location: string, event_type: TEventEntity["event_type"], timezone: string, link: string, notification: TEventEntity["notification"]) {
this.event_uid = event_uid; this.event_uid = event_uid;
this.uid = uid; this.uid = uid;
this.title = title; this.title = title;
@@ -110,9 +131,29 @@ export class Event implements TEventEntity {
this.timezone = timezone; this.timezone = timezone;
this.link = link; this.link = link;
this.notification = notification; this.notification = notification;
this.deleteDate = deleteDate;
} }
toString() {
return {
event_uid: this.event_uid,
uid: this.uid,
title: this.title,
description: this.description,
date_at: this.date_at,
time_start: this.time_start,
time_end: this.time_end,
posted_by: this.posted_by,
location: this.location,
event_type: this.event_type,
timezone: this.timezone,
link: this.link,
notification: this.notification,
deleteDate: this.deleteDate
}
}
syncWithDb ( db: Database ) { syncWithDb ( db: Database ) {
const query = db.prepare( `SELECT * FROM ${Event.table_name} WHERE event_uid = $event_uid;`).as(Event); const query = db.prepare( `SELECT * FROM events WHERE event_uid = $event_uid;`).as(Event);
const entity = query.get({$event_uid: this.event_uid }); const entity = query.get({$event_uid: this.event_uid });
if ( ! entity ) { throw new Error(`Could not find Event with event_uid ${this.event_uid} in DB!`); } if ( ! entity ) { throw new Error(`Could not find Event with event_uid ${this.event_uid} in DB!`); }
this.uid = entity.uid; this.uid = entity.uid;
@@ -127,6 +168,7 @@ export class Event implements TEventEntity {
this.timezone = entity.timezone; this.timezone = entity.timezone;
this.link = entity.link; this.link = entity.link;
this.notification = entity.notification; this.notification = entity.notification;
this.deleteDate = entity.deleteDate;
return this; return this;
} }
@@ -137,5 +179,75 @@ export class Event implements TEventEntity {
WHERE event_uid = $event_uid;` WHERE event_uid = $event_uid;`
); );
query.get({$notification: newValue, $event_uid: this.event_uid }); query.get({$notification: newValue, $event_uid: this.event_uid });
return this.syncWithDb( db );
}
set_deleted ( db: Database ) {
const query = db.prepare(
`UPDATE events
SET deleteDate = $deleteDate
WHERE event_uid = $event_uid;`
);
query.get({
$deleteDate: Math.floor((new Date()).getTime() / 1000),
$event_uid: this.event_uid
});
return this.syncWithDb( db );
}
get_title() {
const type_of_notification = ( (event: Event) => {
switch ( event.notification ) {
case "new":
return "New";
case "changed":
return "Changed";
case "removed":
return "Removed";
default:
return null;
}
} ) ( this );
const title_prefix_arr = [];
if ( type_of_notification ) title_prefix_arr.push( "<" + type_of_notification + ">" );
if ( this.isEventToday() ) title_prefix_arr.push( "<TODAY>" )
return `${title_prefix_arr.length >= 1 ? ( title_prefix_arr.join(" " ) + " - ") : "" }${this.title} (${ TEventType[ this.event_type ] })`;
}
get_body() {
const BaseTime = new Date(`${this.date_at} 21:00`);
const RelativeEventTime = new Date(`${this.date_at} ${this.get_time_start()}`);
const TimeDiff = formatTimeDiff( BaseTime, RelativeEventTime);
const body = [
`Title: ${this.title}`,
`Date: ${this.date_at}`,
`Time: ${this.get_time_start()} (OP Time${ TimeDiff != "00:00" ? ` ${TimeDiff}` : "" })`,
`Type: ${ TEventType[ this.event_type ] }`,
`Location: ${this.location}`,
`By: ${this.posted_by}`,
`Link: ${this.link}`,
].join("\n");
return body;
}
isEventToday ( ) {
const now = getTsNow();
const [year, month, day] = this.date_at.split("-")
if (
year == String(now.year) &&
month == pad_l2( String(now.month) ) &&
day == pad_l2( String( now.day ) )
) {
return true;
}
return false;
}
get_time_start () {
const date = new Date( `${this.date_at} ${this.time_start}` );
if ( ! isEuropeanDST( date ) ) {
const newDate = subtractHours( date, 1);
const hours = newDate.getHours();
const minutes = newDate.getMinutes();
return `${pad_l2(hours)}:${pad_l2(minutes)}`;
}
return this.time_start;
} }
} }

16
src/config.ts Normal file
View File

@@ -0,0 +1,16 @@
export const config = {
apprise: {
services: {
ntfy: {
url: `ntfys://${process.env.ntfy_username}:${process.env.ntfy_password}@${process.env.ntfy_host}/${process.env.ntfy_topic}`,
defaults: {
}
}
},
urls: [
`ntfys://${process.env.ntfy_username}:${process.env.ntfy_password}@${process.env.ntfy_host}/${process.env.ntfy_topic}`,
`discord://${process.env.dc_webhook}?avatar_url=${process.env.dc_avatar_url}&botname=${process.env.dc_botname}`
]
}
} as const

View File

@@ -1,16 +1,49 @@
import * as Bun from "bun"; import { createQS } from "./util";
export async function sendNotification(title: string, body: string, click?: string | null) { type TSendNotificationOptions = {
const command = [ ntfy: {
"python3", link?: string;
"./src/notification.py", } | null,
`--title=${title}`, discord: {
`--body=${body}`, href?: string
]; avatar_url: string,
if ( click ) { botname: string
command.push(`--click=${click}`); }
}
const proc = Bun.spawn(command);
const text = await proc.stdout.text();
console.log("sendNotification: " + text);
} }
export async function sendNotification( title: string, body: string, options: TSendNotificationOptions ) {
console.dir({
sendNotification: {
title,
body
}
});
const QS = {
ntfy: options.ntfy ? createQS(options.ntfy) : null,
discord: createQS(options.discord)
}
if ( ! ( process.env.notification_mock == "true" ) ) {
const response = await fetch(`${ process.env.apprise_https == "true" ? "https" : "http"}://${process.env.apprise_host ? process.env.apprise_host : "apprise"}:${process.env.apprise_port ? String(process.env.apprise_port) : "80" }/notify`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
urls: [
`ntfys://${process.env.ntfy_username}:${process.env.ntfy_password}@${process.env.ntfy_host}/${process.env.ntfy_topic}${ QS.ntfy ? "?" + QS.ntfy : ""}`,
`discord://${process.env.dc_webhook}?${QS.discord}`
].join(","),
title: title,
body: body,
format: "markdown"
})
});
const responseBody = await response.json();
return responseBody;
} else {
console.dir({
sendNotification: "mocking"
})
}
}

16
src/sendNotificationPy.ts Normal file
View File

@@ -0,0 +1,16 @@
import * as Bun from "bun";
export async function sendNotificationPy(title: string, body: string, click?: string | null) {
const command = [
"python3",
"./src/notification.py",
`--title=${title}`,
`--body=${body}`,
];
if ( click ) {
command.push(`--click=${click}`);
}
const proc = Bun.spawn(command);
const text = await proc.stdout.text();
console.log("sendNotification: " + text);
}

View File

@@ -8,6 +8,8 @@ console.log(db_filepath);
export const db = new Database(db_filepath); export const db = new Database(db_filepath);
export default db;
export function init () { export function init () {
Event.createTable(db); Event.createTable(db);
} }

View File

@@ -26,4 +26,73 @@ export function pad_l2 ( _thing: string | number ): string {
_thing = JSON.stringify(_thing); _thing = JSON.stringify(_thing);
}; };
return _thing.padStart(2, "0"); return _thing.padStart(2, "0");
}
export function getTsNow() {
const now = new Date();
const rtn = {
year: now.getFullYear(),
month: now.getMonth() + 1,
day: now.getDate(),
minute: now.getMinutes(),
seconds: now.getSeconds()
}
return rtn;
}
export function unixToDate( unix_timestamp: number ) { return new Date(unix_timestamp * 1000) }
export function dateToUnix( date: Date ) { return Math.round( date.getTime()/1000 ) }
export function formatTimeDiff(dateA: Date, dateB: Date) {
// Difference in milliseconds
const diffMs = dateB.getTime() - dateA.getTime();
// Get sign (+ or -)
const sign = diffMs < 0 ? "-" : "";
// Convert to absolute minutes
const diffMinutes = Math.floor(Math.abs(diffMs) / 60000);
// Split into hours and minutes
const hours = Math.floor(diffMinutes / 60);
const minutes = diffMinutes % 60;
// Return formatted string
return `${sign}${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`;
}
export function subtractHours(date: Date, hours: number) {
// Create a new Date so we don't mutate the original
return new Date(date.getTime() - hours * 60 * 60 * 1000);
}
// Helper: get last Sunday of a given month
function lastSundayOfMonth(year: number, month: number ) {
const lastDay = new Date(Date.UTC(year, month + 1, 0)); // last day of month
const day = lastDay.getUTCDay(); // 0 = Sunday
const diff = day === 0 ? 0 : day; // how far back to go to reach Sunday
lastDay.setUTCDate(lastDay.getUTCDate() - diff);
return lastDay;
}
export function isEuropeanDST( date: Date ) {
const year = date.getFullYear();
// DST starts: last Sunday in March, 01:00 UTC
const start = lastSundayOfMonth(year, 2); // March (month = 2)
start.setUTCHours(1, 0, 0, 0);
// DST ends: last Sunday in October, 01:00 UTC
const end = lastSundayOfMonth(year, 9); // October (month = 9)
end.setUTCHours(1, 0, 0, 0);
// Return true if within DST period
return date >= start && date < end;
}
export function createQS (params: Record<string, string | number | boolean>): string {
const queryString = Object.entries(params)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join("&");
return queryString;
} }

View File

@@ -1,7 +1,10 @@
{ {
"compilerOptions": { "compilerOptions": {
// Environment setup & latest features // Environment setup & latest features
"lib": ["ESNext"], "lib": [
"ESNext",
"es2015.promise"
],
"target": "ESNext", "target": "ESNext",
"module": "Preserve", "module": "Preserve",
"moduleDetection": "force", "moduleDetection": "force",