Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20d5e70d65 | ||
|
|
68b6fc81bd | ||
|
|
16947cedf5 | ||
|
|
eca0cb57f9 | ||
|
|
011cbd23b2 | ||
|
|
19e18b4ebf | ||
|
|
ffef6313d4 | ||
|
|
7b4daf8754 | ||
|
|
fdeda6c77a | ||
|
|
5acaf144fb | ||
|
|
926032e83b | ||
|
|
8fc9b27f55 | ||
|
|
9a8cff3227 | ||
|
|
e40bd1f6a3 | ||
|
|
6c982050da | ||
|
|
b40f5b788c | ||
|
|
2c3fdf5544 | ||
|
|
74dd29b87b | ||
|
|
8a07fb781c | ||
|
|
79f17236db | ||
|
|
83ab60ec2d | ||
|
|
cd2b00b704 | ||
|
|
7817ce3d8e | ||
|
|
82b2012f50 | ||
|
|
bb4eb2964b | ||
|
|
a6a02a15fe | ||
|
|
bf5b602fd1 | ||
|
|
4d12a51b05 | ||
|
|
e0bea6c6e0 | ||
|
|
2f1b738164 | ||
|
|
3639dd231a | ||
|
|
86f55069e6 | ||
|
|
5faa7f3646 | ||
|
|
bc52256301 | ||
|
|
9e66cdd7ea | ||
|
|
931c9cedf1 | ||
|
|
83a51db265 | ||
|
|
1b09d1494e | ||
|
|
02fe842995 | ||
|
|
a464c845c2 | ||
|
|
5a69f58770 | ||
|
|
48c243f624 | ||
|
|
7af0717b1b | ||
|
|
9977d390ac | ||
|
|
ea5e801718 | ||
|
|
7d54481f75 | ||
|
|
fbc6a9362e |
11
.env.example
11
.env.example
@@ -26,4 +26,13 @@ AUTH_PASSWORD_HASH=
|
|||||||
AUTH_ENABLED=false
|
AUTH_ENABLED=false
|
||||||
AUTH_SETUP_COMPLETED=false
|
AUTH_SETUP_COMPLETED=false
|
||||||
JWT_SECRET=
|
JWT_SECRET=
|
||||||
DATABASE_URL="file:./data/database.sqlite"
|
DATABASE_URL="file:/opt/ProxmoxVE-Local/data/settings.db"
|
||||||
|
AUTO_SYNC_ENABLED=false
|
||||||
|
SYNC_INTERVAL_TYPE=
|
||||||
|
SYNC_INTERVAL_PREDEFINED=
|
||||||
|
AUTO_DOWNLOAD_NEW=
|
||||||
|
AUTO_UPDATE_EXISTING=
|
||||||
|
NOTIFICATION_ENABLED=
|
||||||
|
APPRISE_URLS=
|
||||||
|
LAST_AUTO_SYNC=
|
||||||
|
SYNC_INTERVAL_CRON=
|
||||||
29
.github/workflows/publish_release.yml
vendored
29
.github/workflows/publish_release.yml
vendored
@@ -46,35 +46,6 @@ jobs:
|
|||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
git commit -m "chore: add VERSION $version" --allow-empty
|
git commit -m "chore: add VERSION $version" --allow-empty
|
||||||
|
|
||||||
|
|
||||||
- name: Sync upstream JSONs
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
tmp_dir=$(mktemp -d)
|
|
||||||
api_url="https://api.github.com/repos/community-scripts/ProxmoxVE/contents/frontend/public/json"
|
|
||||||
# Fetch file list (no subfolders)
|
|
||||||
curl -sSL -H "Authorization: token $GH_TOKEN" "$api_url" \
|
|
||||||
| jq -r '.[] | select(.type=="file") | .name' > "$tmp_dir/files.txt"
|
|
||||||
|
|
||||||
# Download each file
|
|
||||||
while IFS= read -r name; do
|
|
||||||
curl -sSL -H "Authorization: token $GH_TOKEN" \
|
|
||||||
"https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/frontend/public/json/$name" \
|
|
||||||
-o "$tmp_dir/$name"
|
|
||||||
done < "$tmp_dir/files.txt"
|
|
||||||
|
|
||||||
mkdir -p json
|
|
||||||
rsync -a --delete "$tmp_dir/" json/
|
|
||||||
|
|
||||||
# Stage and amend commit to include JSON updates (and VERSION)
|
|
||||||
git add json VERSION
|
|
||||||
if ! git diff --cached --quiet; then
|
|
||||||
git commit --amend --no-edit
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
- name: Push changes
|
- name: Push changes
|
||||||
run: |
|
run: |
|
||||||
git push --force-with-lease --set-upstream origin "update-version-${{ steps.draft.outputs.tag_name }}"
|
git push --force-with-lease --set-upstream origin "update-version-${{ steps.draft.outputs.tag_name }}"
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -37,6 +37,9 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
update.log
|
||||||
|
server.log
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
|
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
|
||||||
.env
|
.env
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Frigate",
|
|
||||||
"slug": "frigate",
|
|
||||||
"categories": [
|
|
||||||
15
|
|
||||||
],
|
|
||||||
"date_created": "2024-05-02",
|
|
||||||
"type": "ct",
|
|
||||||
"updateable": false,
|
|
||||||
"privileged": true,
|
|
||||||
"interface_port": 5000,
|
|
||||||
"documentation": "https://docs.frigate.video/",
|
|
||||||
"website": "https://frigate.video/",
|
|
||||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/frigate.webp",
|
|
||||||
"config_path": "",
|
|
||||||
"description": "Frigate is an open source NVR built around real-time AI object detection. All processing is performed locally on your own hardware, and your camera feeds never leave your home.",
|
|
||||||
"install_methods": [
|
|
||||||
{
|
|
||||||
"type": "default",
|
|
||||||
"script": "ct/frigate.sh",
|
|
||||||
"resources": {
|
|
||||||
"cpu": 4,
|
|
||||||
"ram": 4096,
|
|
||||||
"hdd": 20,
|
|
||||||
"os": "debian",
|
|
||||||
"version": "11"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"default_credentials": {
|
|
||||||
"username": null,
|
|
||||||
"password": null
|
|
||||||
},
|
|
||||||
"notes": [
|
|
||||||
{
|
|
||||||
"text": "Discussions (explore more advanced methods): `https://github.com/tteck/Proxmox/discussions/2711`",
|
|
||||||
"type": "info"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "go2rtc Interface port:`1984`",
|
|
||||||
"type": "info"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"documentation": "https://docs.openarchiver.com/",
|
"documentation": "https://docs.openarchiver.com/",
|
||||||
"config_path": "/opt/openarchiver/.env",
|
"config_path": "/opt/openarchiver/.env",
|
||||||
"website": "https://openarchiver.com/",
|
"website": "https://openarchiver.com/",
|
||||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/OpenArchiver.webp",
|
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/open-archiver.webp",
|
||||||
"description": "Open Archiver is a secure, self-hosted email archiving solution, and it's completely open source. Get an email archiver that enables full-text search across email and attachments. Create a permanent, searchable, and compliant mail archive from Google Workspace, Microsoft 35, and any IMAP server.",
|
"description": "Open Archiver is a secure, self-hosted email archiving solution, and it's completely open source. Get an email archiver that enables full-text search across email and attachments. Create a permanent, searchable, and compliant mail archive from Google Workspace, Microsoft 35, and any IMAP server.",
|
||||||
"install_methods": [
|
"install_methods": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,14 +1,89 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"name": "apache/cassandra",
|
||||||
|
"version": "5.0.6-tentative",
|
||||||
|
"date": "2025-10-21T11:42:35Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "evcc-io/evcc",
|
||||||
|
"version": "0.209.3",
|
||||||
|
"date": "2025-10-21T10:53:07Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "grafana/grafana",
|
||||||
|
"version": "v11.5.10",
|
||||||
|
"date": "2025-10-21T10:46:47Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "zabbix/zabbix",
|
||||||
|
"version": "7.0.20rc1",
|
||||||
|
"date": "2025-10-21T09:14:14Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "n8n-io/n8n",
|
||||||
|
"version": "n8n@1.115.3",
|
||||||
|
"date": "2025-10-14T14:40:17Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VictoriaMetrics/VictoriaMetrics",
|
||||||
|
"version": "pmm-6401-v1.128.0",
|
||||||
|
"date": "2025-10-21T08:30:52Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "crowdsecurity/crowdsec",
|
||||||
|
"version": "v1.7.1",
|
||||||
|
"date": "2025-10-15T10:44:03Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "meilisearch/meilisearch",
|
||||||
|
"version": "prototype-v1.24.0.ignore-embedding-failures-2",
|
||||||
|
"date": "2025-10-21T07:36:22Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jackett/Jackett",
|
||||||
|
"version": "v0.24.166",
|
||||||
|
"date": "2025-10-21T05:54:33Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "openobserve/openobserve",
|
||||||
|
"version": "v0.16.0-rc1",
|
||||||
|
"date": "2025-10-21T00:37:47Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "jeedom/core",
|
||||||
|
"version": "4.4.20",
|
||||||
|
"date": "2025-10-21T00:27:05Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "steveiliop56/tinyauth",
|
||||||
|
"version": "v4.0.1",
|
||||||
|
"date": "2025-10-15T16:53:55Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "documenso/documenso",
|
||||||
|
"version": "v1.13.0",
|
||||||
|
"date": "2025-10-21T00:21:04Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "goauthentik/authentik",
|
"name": "goauthentik/authentik",
|
||||||
"version": "version/2025.10.0-rc1",
|
"version": "version/2025.10.0-rc2",
|
||||||
"date": "2025-10-20T23:43:29Z"
|
"date": "2025-10-21T00:19:36Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TwiN/gatus",
|
||||||
|
"version": "v5.27.1",
|
||||||
|
"date": "2025-10-21T00:01:36Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "henrygd/beszel",
|
"name": "henrygd/beszel",
|
||||||
"version": "v0.14.1",
|
"version": "v0.14.1",
|
||||||
"date": "2025-10-20T22:10:56Z"
|
"date": "2025-10-20T22:10:56Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "keycloak/keycloak",
|
||||||
|
"version": "26.4.1",
|
||||||
|
"date": "2025-10-16T07:21:53Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "booklore-app/booklore",
|
"name": "booklore-app/booklore",
|
||||||
"version": "v1.8.1",
|
"version": "v1.8.1",
|
||||||
@@ -44,11 +119,6 @@
|
|||||||
"version": "v1.91.0-pre",
|
"version": "v1.91.0-pre",
|
||||||
"date": "2025-10-20T16:18:51Z"
|
"date": "2025-10-20T16:18:51Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "meilisearch/meilisearch",
|
|
||||||
"version": "prototype-v1.24.0.ignore-embedding-failures-1",
|
|
||||||
"date": "2025-10-20T16:17:45Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "louislam/uptime-kuma",
|
"name": "louislam/uptime-kuma",
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
@@ -69,16 +139,6 @@
|
|||||||
"version": "v1.71.2",
|
"version": "v1.71.2",
|
||||||
"date": "2025-10-20T15:25:52Z"
|
"date": "2025-10-20T15:25:52Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "openobserve/openobserve",
|
|
||||||
"version": "v0.16.0-rc1",
|
|
||||||
"date": "2025-10-20T13:16:20Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "crowdsecurity/crowdsec",
|
|
||||||
"version": "v1.7.1",
|
|
||||||
"date": "2025-10-15T10:44:03Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Graylog2/graylog2-server",
|
"name": "Graylog2/graylog2-server",
|
||||||
"version": "7.0.0-rc.1",
|
"version": "7.0.0-rc.1",
|
||||||
@@ -94,21 +154,11 @@
|
|||||||
"version": "v2025.10.4",
|
"version": "v2025.10.4",
|
||||||
"date": "2025-10-20T10:45:54Z"
|
"date": "2025-10-20T10:45:54Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "apache/cassandra",
|
|
||||||
"version": "cassandra-4.0.19",
|
|
||||||
"date": "2025-10-20T09:08:49Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "jupyter/notebook",
|
"name": "jupyter/notebook",
|
||||||
"version": "@jupyter-notebook/ui-components@7.5.0-beta.1",
|
"version": "@jupyter-notebook/ui-components@7.5.0-beta.1",
|
||||||
"date": "2025-10-20T07:01:38Z"
|
"date": "2025-10-20T07:01:38Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "Jackett/Jackett",
|
|
||||||
"version": "v0.24.159",
|
|
||||||
"date": "2025-10-20T05:53:23Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "inventree/InvenTree",
|
"name": "inventree/InvenTree",
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
@@ -134,16 +184,6 @@
|
|||||||
"version": "v1.0.0-beta27",
|
"version": "v1.0.0-beta27",
|
||||||
"date": "2025-10-20T00:38:13Z"
|
"date": "2025-10-20T00:38:13Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "jeedom/core",
|
|
||||||
"version": "4.4.20",
|
|
||||||
"date": "2025-10-20T00:27:05Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "steveiliop56/tinyauth",
|
|
||||||
"version": "v4.0.1",
|
|
||||||
"date": "2025-10-15T16:53:55Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "seriousm4x/UpSnap",
|
"name": "seriousm4x/UpSnap",
|
||||||
"version": "5.2.3",
|
"version": "5.2.3",
|
||||||
@@ -259,11 +299,6 @@
|
|||||||
"version": "v25.4",
|
"version": "v25.4",
|
||||||
"date": "2025-10-09T10:27:01Z"
|
"date": "2025-10-09T10:27:01Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "TwiN/gatus",
|
|
||||||
"version": "v5.27.0",
|
|
||||||
"date": "2025-10-18T02:44:26Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "9001/copyparty",
|
"name": "9001/copyparty",
|
||||||
"version": "v1.19.17",
|
"version": "v1.19.17",
|
||||||
@@ -284,11 +319,6 @@
|
|||||||
"version": "v13.0.1",
|
"version": "v13.0.1",
|
||||||
"date": "2025-10-17T18:54:16Z"
|
"date": "2025-10-17T18:54:16Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "keycloak/keycloak",
|
|
||||||
"version": "26.4.1",
|
|
||||||
"date": "2025-10-16T07:21:53Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "grokability/snipe-it",
|
"name": "grokability/snipe-it",
|
||||||
"version": "v8.3.4",
|
"version": "v8.3.4",
|
||||||
@@ -459,11 +489,6 @@
|
|||||||
"version": "2025.10.0",
|
"version": "2025.10.0",
|
||||||
"date": "2025-10-14T19:07:37Z"
|
"date": "2025-10-14T19:07:37Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "evcc-io/evcc",
|
|
||||||
"version": "0.209.2",
|
|
||||||
"date": "2025-10-14T18:55:44Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "crafty-controller/crafty-4",
|
"name": "crafty-controller/crafty-4",
|
||||||
"version": "v4.5.5",
|
"version": "v4.5.5",
|
||||||
@@ -484,11 +509,6 @@
|
|||||||
"version": "v1.140.0",
|
"version": "v1.140.0",
|
||||||
"date": "2025-10-14T15:57:12Z"
|
"date": "2025-10-14T15:57:12Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "n8n-io/n8n",
|
|
||||||
"version": "n8n@1.115.3",
|
|
||||||
"date": "2025-10-14T14:40:17Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "rogerfar/rdt-client",
|
"name": "rogerfar/rdt-client",
|
||||||
"version": "v2.0.119",
|
"version": "v2.0.119",
|
||||||
@@ -619,11 +639,6 @@
|
|||||||
"version": "11.0.1",
|
"version": "11.0.1",
|
||||||
"date": "2025-10-09T12:34:15Z"
|
"date": "2025-10-09T12:34:15Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "documenso/documenso",
|
|
||||||
"version": "v1.12.10",
|
|
||||||
"date": "2025-10-09T04:32:35Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "rabbitmq/rabbitmq-server",
|
"name": "rabbitmq/rabbitmq-server",
|
||||||
"version": "v4.1.4",
|
"version": "v4.1.4",
|
||||||
@@ -674,11 +689,6 @@
|
|||||||
"version": "v0.15.1",
|
"version": "v0.15.1",
|
||||||
"date": "2025-10-07T20:30:56Z"
|
"date": "2025-10-07T20:30:56Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "VictoriaMetrics/VictoriaMetrics",
|
|
||||||
"version": "pmm-6401-v1.127.0",
|
|
||||||
"date": "2025-10-07T14:31:32Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "thecfu/scraparr",
|
"name": "thecfu/scraparr",
|
||||||
"version": "v2.2.5",
|
"version": "v2.2.5",
|
||||||
@@ -769,11 +779,6 @@
|
|||||||
"version": "v5.41.4",
|
"version": "v5.41.4",
|
||||||
"date": "2025-09-30T22:26:11Z"
|
"date": "2025-09-30T22:26:11Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "zabbix/zabbix",
|
|
||||||
"version": "7.4.3",
|
|
||||||
"date": "2025-09-30T21:49:53Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "mongodb/mongo",
|
"name": "mongodb/mongo",
|
||||||
"version": "r8.2.1",
|
"version": "r8.2.1",
|
||||||
@@ -884,11 +889,6 @@
|
|||||||
"version": "v2.0.10",
|
"version": "v2.0.10",
|
||||||
"date": "2025-09-24T08:33:37Z"
|
"date": "2025-09-24T08:33:37Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "grafana/grafana",
|
|
||||||
"version": "v12.2.0",
|
|
||||||
"date": "2025-09-23T23:47:02Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "postgres/postgres",
|
"name": "postgres/postgres",
|
||||||
"version": "REL_18_0",
|
"version": "REL_18_0",
|
||||||
|
|||||||
396
package-lock.json
generated
396
package-lock.json
generated
@@ -22,12 +22,16 @@
|
|||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/addon-web-links": "^0.11.0",
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
|
"axios": "^1.7.9",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cron-validator": "^1.2.0",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lucide-react": "^0.546.0",
|
"lucide-react": "^0.546.0",
|
||||||
"next": "^15.5.6",
|
"next": "^15.5.6",
|
||||||
|
"node-cron": "^3.0.3",
|
||||||
"node-pty": "^1.0.0",
|
"node-pty": "^1.0.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
@@ -37,7 +41,7 @@
|
|||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"strip-ansi": "^7.1.2",
|
"strip-ansi": "^7.1.2",
|
||||||
"superjson": "^2.2.1",
|
"superjson": "^2.2.3",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"ws": "^8.18.3",
|
"ws": "^8.18.3",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
@@ -51,20 +55,21 @@
|
|||||||
"@types/bcryptjs": "^3.0.0",
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"@types/better-sqlite3": "^7.6.8",
|
"@types/better-sqlite3": "^7.6.8",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^24.9.0",
|
"@types/node": "^24.9.1",
|
||||||
|
"@types/node-cron": "^3.0.11",
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.2.2",
|
||||||
"@vitejs/plugin-react": "^5.0.2",
|
"@vitejs/plugin-react": "^5.0.2",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
"@vitest/ui": "^3.2.4",
|
"@vitest/ui": "^3.2.4",
|
||||||
"eslint": "^9.38.0",
|
"eslint": "^9.38.0",
|
||||||
"eslint-config-next": "^15.5.6",
|
"eslint-config-next": "^16.0.0",
|
||||||
"jsdom": "^27.0.1",
|
"jsdom": "^27.0.1",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.1",
|
"prettier-plugin-tailwindcss": "^0.7.1",
|
||||||
"prisma": "^6.17.1",
|
"prisma": "^6.18.0",
|
||||||
"tailwindcss": "^4.1.15",
|
"tailwindcss": "^4.1.16",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.2",
|
||||||
"typescript-eslint": "^8.46.2",
|
"typescript-eslint": "^8.46.2",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
@@ -1833,9 +1838,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@next/eslint-plugin-next": {
|
"node_modules/@next/eslint-plugin-next": {
|
||||||
"version": "15.5.6",
|
"version": "16.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.0.0.tgz",
|
||||||
"integrity": "sha512-YxDvsT2fwy1j5gMqk3ppXlsgDopHnkM4BoxSVASbvvgh5zgsK8lvWerDzPip8k3WVzsTZ1O7A7si1KNfN4OZfQ==",
|
"integrity": "sha512-IB7RzmmtrPOrpAgEBR1PIQPD0yea5lggh5cq54m51jHjjljU80Ia+czfxJYMlSDl1DPvpzb8S9TalCc0VMo9Hw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2059,66 +2064,66 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/config": {
|
"node_modules/@prisma/config": {
|
||||||
"version": "6.17.1",
|
"version": "6.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.18.0.tgz",
|
||||||
"integrity": "sha512-fs8wY6DsvOCzuiyWVckrVs1LOcbY4LZNz8ki4uUIQ28jCCzojTGqdLhN2Jl5lDnC1yI8/gNIKpsWDM8pLhOdwA==",
|
"integrity": "sha512-rgFzspCpwsE+q3OF/xkp0fI2SJ3PfNe9LLMmuSVbAZ4nN66WfBiKqJKo/hLz3ysxiPQZf8h1SMf2ilqPMeWATQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"c12": "3.1.0",
|
"c12": "3.1.0",
|
||||||
"deepmerge-ts": "7.1.5",
|
"deepmerge-ts": "7.1.5",
|
||||||
"effect": "3.16.12",
|
"effect": "3.18.4",
|
||||||
"empathic": "2.0.0"
|
"empathic": "2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/debug": {
|
"node_modules/@prisma/debug": {
|
||||||
"version": "6.17.1",
|
"version": "6.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.18.0.tgz",
|
||||||
"integrity": "sha512-Vf7Tt5Wh9XcndpbmeotuqOMLWPTjEKCsgojxXP2oxE1/xYe7PtnP76hsouG9vis6fctX+TxgmwxTuYi/+xc7dQ==",
|
"integrity": "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines": {
|
"node_modules/@prisma/engines": {
|
||||||
"version": "6.17.1",
|
"version": "6.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.18.0.tgz",
|
||||||
"integrity": "sha512-D95Ik3GYZkqZ8lSR4EyFOJ/tR33FcYRP8kK61o+WMsyD10UfJwd7+YielflHfKwiGodcqKqoraWw8ElAgMDbPw==",
|
"integrity": "sha512-i5RzjGF/ex6AFgqEe2o1IW8iIxJGYVQJVRau13kHPYEL1Ck8Zvwuzamqed/1iIljs5C7L+Opiz5TzSsUebkriA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.17.1",
|
"@prisma/debug": "6.18.0",
|
||||||
"@prisma/engines-version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac",
|
"@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f",
|
||||||
"@prisma/fetch-engine": "6.17.1",
|
"@prisma/fetch-engine": "6.18.0",
|
||||||
"@prisma/get-platform": "6.17.1"
|
"@prisma/get-platform": "6.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines-version": {
|
"node_modules/@prisma/engines-version": {
|
||||||
"version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac",
|
"version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f.tgz",
|
||||||
"integrity": "sha512-17140E3huOuD9lMdJ9+SF/juOf3WR3sTJMVyyenzqUPbuH+89nPhSWcrY+Mf7tmSs6HvaO+7S+HkELinn6bhdg==",
|
"integrity": "sha512-T7Af4QsJQnSgWN1zBbX+Cha5t4qjHRxoeoWpK4JugJzG/ipmmDMY5S+O0N1ET6sCBNVkf6lz+Y+ZNO9+wFU8pQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/fetch-engine": {
|
"node_modules/@prisma/fetch-engine": {
|
||||||
"version": "6.17.1",
|
"version": "6.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.18.0.tgz",
|
||||||
"integrity": "sha512-AYZiHOs184qkDMiTeshyJCtyL4yERkjfTkJiSJdYuSfc24m94lTNL5+GFinZ6vVz+ktX4NJzHKn1zIFzGTWrWg==",
|
"integrity": "sha512-TdaBvTtBwP3IoqVYoGIYpD4mWlk0pJpjTJjir/xLeNWlwog7Sl3bD2J0jJ8+5+q/6RBg+acb9drsv5W6lqae7A==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.17.1",
|
"@prisma/debug": "6.18.0",
|
||||||
"@prisma/engines-version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac",
|
"@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f",
|
||||||
"@prisma/get-platform": "6.17.1"
|
"@prisma/get-platform": "6.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/get-platform": {
|
"node_modules/@prisma/get-platform": {
|
||||||
"version": "6.17.1",
|
"version": "6.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.18.0.tgz",
|
||||||
"integrity": "sha512-AKEn6fsfz0r482S5KRDFlIGEaq9wLNcgalD1adL+fPcFFblIKs1sD81kY/utrHdqKuVC6E1XSRpegDK3ZLL4Qg==",
|
"integrity": "sha512-uXNJCJGhxTCXo2B25Ta91Rk1/Nmlqg9p7G9GKh8TPhxvAyXCvMNQoogj4JLEUy+3ku8g59cpyQIKFhqY2xO2bg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.17.1"
|
"@prisma/debug": "6.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/primitive": {
|
"node_modules/@radix-ui/primitive": {
|
||||||
@@ -2976,13 +2981,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@rushstack/eslint-patch": {
|
|
||||||
"version": "1.13.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.13.0.tgz",
|
|
||||||
"integrity": "sha512-2ih5qGw5SZJ+2fLZxP6Lr6Na2NTIgPRL/7Kmyuw0uIyBQnuhQ8fi8fzUTd38eIQmqp+GYLC00cI6WgtqHxBwmw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@standard-schema/spec": {
|
"node_modules/@standard-schema/spec": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||||
@@ -3070,6 +3068,13 @@
|
|||||||
"tailwindcss": "4.1.15"
|
"tailwindcss": "4.1.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/node/node_modules/tailwindcss": {
|
||||||
|
"version": "4.1.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.15.tgz",
|
||||||
|
"integrity": "sha512-k2WLnWkYFkdpRv+Oby3EBXIyQC8/s1HOFMBUViwtAh6Z5uAozeUSMQlIsn/c6Q2iJzqG6aJT3wdPaRNj70iYxQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/oxide": {
|
"node_modules/@tailwindcss/oxide": {
|
||||||
"version": "4.1.15",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.15.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.15.tgz",
|
||||||
@@ -3385,6 +3390,13 @@
|
|||||||
"tailwindcss": "4.1.15"
|
"tailwindcss": "4.1.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/postcss/node_modules/tailwindcss": {
|
||||||
|
"version": "4.1.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.15.tgz",
|
||||||
|
"integrity": "sha512-k2WLnWkYFkdpRv+Oby3EBXIyQC8/s1HOFMBUViwtAh6Z5uAozeUSMQlIsn/c6Q2iJzqG6aJT3wdPaRNj70iYxQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/typography": {
|
"node_modules/@tailwindcss/typography": {
|
||||||
"version": "0.5.19",
|
"version": "0.5.19",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
|
||||||
@@ -3731,14 +3743,21 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.9.0",
|
"version": "24.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
|
||||||
"integrity": "sha512-MKNwXh3seSK8WurXF7erHPJ2AONmMwkI7zAMrXZDPIru8jRqkk6rGDBVbw4mLwfqA+ZZliiDPg05JQ3uW66tKQ==",
|
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/node-cron": {
|
||||||
|
"version": "3.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz",
|
||||||
|
"integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/prismjs": {
|
"node_modules/@types/prismjs": {
|
||||||
"version": "1.26.5",
|
"version": "1.26.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
|
||||||
@@ -4891,6 +4910,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/available-typed-arrays": {
|
"node_modules/available-typed-arrays": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||||
@@ -4917,6 +4942,17 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.12.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
|
||||||
|
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.4",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/axobject-query": {
|
"node_modules/axobject-query": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||||
@@ -5066,6 +5102,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/c12/node_modules/dotenv": {
|
||||||
|
"version": "16.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||||
|
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cac": {
|
"node_modules/cac": {
|
||||||
"version": "6.7.14",
|
"version": "6.7.14",
|
||||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||||
@@ -5099,7 +5148,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@@ -5323,6 +5371,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/comma-separated-tokens": {
|
"node_modules/comma-separated-tokens": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
||||||
@@ -5365,20 +5425,26 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/copy-anything": {
|
"node_modules/copy-anything": {
|
||||||
"version": "3.0.5",
|
"version": "4.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
|
||||||
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
|
"integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-what": "^4.1.8"
|
"is-what": "^5.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.13"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/mesqueeb"
|
"url": "https://github.com/sponsors/mesqueeb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cron-validator": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cron-validator/-/cron-validator-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-wGcJ9FCy65iaU6egSH8b5dZYJF7GU/3Jh06wzaT9lsa5dbqExjljmu+0cJ8cpKn+vUyZa/EM4WAxeLR6SypJXw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -5630,6 +5696,15 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dequal": {
|
"node_modules/dequal": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
@@ -5697,10 +5772,9 @@
|
|||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.6.1",
|
"version": "17.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
|
||||||
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -5713,7 +5787,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.1",
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
@@ -5741,9 +5814,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/effect": {
|
"node_modules/effect": {
|
||||||
"version": "3.16.12",
|
"version": "3.18.4",
|
||||||
"resolved": "https://registry.npmjs.org/effect/-/effect-3.16.12.tgz",
|
"resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz",
|
||||||
"integrity": "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==",
|
"integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5875,7 +5948,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -5885,7 +5957,6 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -5930,7 +6001,6 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0"
|
"es-errors": "^1.3.0"
|
||||||
@@ -5943,7 +6013,6 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@@ -6112,25 +6181,24 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-config-next": {
|
"node_modules/eslint-config-next": {
|
||||||
"version": "15.5.6",
|
"version": "16.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.0.0.tgz",
|
||||||
"integrity": "sha512-cGr3VQlPsZBEv8rtYp4BpG1KNXDqGvPo9VC1iaCgIA11OfziC/vczng+TnAS3WpRIR3Q5ye/6yl+CRUuZ1fPGg==",
|
"integrity": "sha512-DWKT1YAO9ex2rK0/EeiPpKU++ghTiG59z6m08/ReLRECOYIaEv17maSCYT8zmFQLwIrY5lhJ+iaJPQdT4sJd4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/eslint-plugin-next": "15.5.6",
|
"@next/eslint-plugin-next": "16.0.0",
|
||||||
"@rushstack/eslint-patch": "^1.10.3",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
|
|
||||||
"@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
|
|
||||||
"eslint-import-resolver-node": "^0.3.6",
|
"eslint-import-resolver-node": "^0.3.6",
|
||||||
"eslint-import-resolver-typescript": "^3.5.2",
|
"eslint-import-resolver-typescript": "^3.5.2",
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.0",
|
"eslint-plugin-jsx-a11y": "^6.10.0",
|
||||||
"eslint-plugin-react": "^7.37.0",
|
"eslint-plugin-react": "^7.37.0",
|
||||||
"eslint-plugin-react-hooks": "^5.0.0"
|
"eslint-plugin-react-hooks": "^7.0.0",
|
||||||
|
"globals": "16.4.0",
|
||||||
|
"typescript-eslint": "^8.46.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": "^7.23.0 || ^8.0.0 || ^9.0.0",
|
"eslint": ">=9.0.0",
|
||||||
"typescript": ">=3.3.1"
|
"typescript": ">=3.3.1"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
@@ -6139,6 +6207,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eslint-config-next/node_modules/globals": {
|
||||||
|
"version": "16.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz",
|
||||||
|
"integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eslint-import-resolver-node": {
|
"node_modules/eslint-import-resolver-node": {
|
||||||
"version": "0.3.9",
|
"version": "0.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
|
||||||
@@ -6342,13 +6423,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-react-hooks": {
|
"node_modules/eslint-plugin-react-hooks": {
|
||||||
"version": "5.2.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.0.tgz",
|
||||||
"integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==",
|
"integrity": "sha512-fNXaOwvKwq2+pXiRpXc825Vd63+KM4DLL40Rtlycb8m7fYpp6efrTp1sa6ZbP/Ap58K2bEKFXRmhURE+CJAQWw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/core": "^7.24.4",
|
||||||
|
"@babel/parser": "^7.24.4",
|
||||||
|
"hermes-parser": "^0.25.1",
|
||||||
|
"zod": "^3.22.4 || ^4.0.0",
|
||||||
|
"zod-validation-error": "^3.0.3 || ^4.0.0"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
|
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
|
||||||
@@ -6677,6 +6765,26 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
|
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/for-each": {
|
"node_modules/for-each": {
|
||||||
"version": "0.3.5",
|
"version": "0.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||||
@@ -6710,6 +6818,22 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/format": {
|
"node_modules/format": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
|
||||||
@@ -6737,7 +6861,6 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
@@ -6798,7 +6921,6 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.2",
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
@@ -6832,7 +6954,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dunder-proto": "^1.0.1",
|
"dunder-proto": "^1.0.1",
|
||||||
@@ -6985,7 +7106,6 @@
|
|||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -7064,7 +7184,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -7077,7 +7196,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-symbols": "^1.0.3"
|
"has-symbols": "^1.0.3"
|
||||||
@@ -7093,7 +7211,6 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
@@ -7172,6 +7289,23 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hermes-estree": {
|
||||||
|
"version": "0.25.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
|
||||||
|
"integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/hermes-parser": {
|
||||||
|
"version": "0.25.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
|
||||||
|
"integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"hermes-estree": "0.25.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/highlight.js": {
|
"node_modules/highlight.js": {
|
||||||
"version": "10.7.3",
|
"version": "10.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
|
||||||
@@ -7815,12 +7949,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-what": {
|
"node_modules/is-what": {
|
||||||
"version": "4.1.16",
|
"version": "5.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz",
|
||||||
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
|
"integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.13"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/mesqueeb"
|
"url": "https://github.com/sponsors/mesqueeb"
|
||||||
@@ -8625,7 +8759,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -9507,6 +9640,27 @@
|
|||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/min-indent": {
|
"node_modules/min-indent": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||||
@@ -9693,6 +9847,18 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-cron": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"uuid": "8.3.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-fetch-native": {
|
"node_modules/node-fetch-native": {
|
||||||
"version": "1.6.7",
|
"version": "1.6.7",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
||||||
@@ -10294,15 +10460,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prisma": {
|
"node_modules/prisma": {
|
||||||
"version": "6.17.1",
|
"version": "6.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.18.0.tgz",
|
||||||
"integrity": "sha512-ac6h0sM1Tg3zu8NInY+qhP/S9KhENVaw9n1BrGKQVFu05JT5yT5Qqqmb8tMRIE3ZXvVj4xcRA5yfrsy4X7Yy5g==",
|
"integrity": "sha512-bXWy3vTk8mnRmT+SLyZBQoC2vtV9Z8u7OHvEu+aULYxwiop/CPiFZ+F56KsNRNf35jw+8wcu8pmLsjxpBxAO9g==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/config": "6.17.1",
|
"@prisma/config": "6.18.0",
|
||||||
"@prisma/engines": "6.17.1"
|
"@prisma/engines": "6.18.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"prisma": "build/index.js"
|
"prisma": "build/index.js"
|
||||||
@@ -10357,6 +10523,12 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
@@ -11769,12 +11941,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/superjson": {
|
"node_modules/superjson": {
|
||||||
"version": "2.2.2",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.3.tgz",
|
||||||
"integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==",
|
"integrity": "sha512-ay3d+LW/S6yppKoTz3Bq4mG0xrS5bFwfWEBmQfbC7lt5wmtk+Obq0TxVuA9eYRirBTQb1K3eEpBRHMQEo0WyVw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"copy-anything": "^3.0.2"
|
"copy-anything": "^4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
@@ -11824,9 +11996,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.1.15",
|
"version": "4.1.16",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.15.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz",
|
||||||
"integrity": "sha512-k2WLnWkYFkdpRv+Oby3EBXIyQC8/s1HOFMBUViwtAh6Z5uAozeUSMQlIsn/c6Q2iJzqG6aJT3wdPaRNj70iYxQ==",
|
"integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tapable": {
|
"node_modules/tapable": {
|
||||||
@@ -12475,6 +12647,15 @@
|
|||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vfile": {
|
"node_modules/vfile": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
|
||||||
@@ -13071,6 +13252,19 @@
|
|||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/zod-validation-error": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/zwitch": {
|
"node_modules/zwitch": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
||||||
|
|||||||
15
package.json
15
package.json
@@ -36,12 +36,16 @@
|
|||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/addon-web-links": "^0.11.0",
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
|
"axios": "^1.7.9",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cron-validator": "^1.2.0",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lucide-react": "^0.546.0",
|
"lucide-react": "^0.546.0",
|
||||||
"next": "^15.5.6",
|
"next": "^15.5.6",
|
||||||
|
"node-cron": "^3.0.3",
|
||||||
"node-pty": "^1.0.0",
|
"node-pty": "^1.0.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
@@ -51,7 +55,7 @@
|
|||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"strip-ansi": "^7.1.2",
|
"strip-ansi": "^7.1.2",
|
||||||
"superjson": "^2.2.1",
|
"superjson": "^2.2.3",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"ws": "^8.18.3",
|
"ws": "^8.18.3",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
@@ -65,20 +69,21 @@
|
|||||||
"@types/bcryptjs": "^3.0.0",
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"@types/better-sqlite3": "^7.6.8",
|
"@types/better-sqlite3": "^7.6.8",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^24.9.0",
|
"@types/node": "^24.9.1",
|
||||||
|
"@types/node-cron": "^3.0.11",
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.2.2",
|
||||||
"@vitejs/plugin-react": "^5.0.2",
|
"@vitejs/plugin-react": "^5.0.2",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
"@vitest/ui": "^3.2.4",
|
"@vitest/ui": "^3.2.4",
|
||||||
"eslint": "^9.38.0",
|
"eslint": "^9.38.0",
|
||||||
"eslint-config-next": "^15.5.6",
|
"eslint-config-next": "^16.0.0",
|
||||||
"jsdom": "^27.0.1",
|
"jsdom": "^27.0.1",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.1",
|
"prettier-plugin-tailwindcss": "^0.7.1",
|
||||||
"prisma": "^6.17.1",
|
"prisma": "^6.18.0",
|
||||||
"tailwindcss": "^4.1.15",
|
"tailwindcss": "^4.1.16",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.2",
|
||||||
"typescript-eslint": "^8.46.2",
|
"typescript-eslint": "^8.46.2",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
|
|||||||
@@ -439,17 +439,14 @@ advanced_settings() {
|
|||||||
exit_script
|
exit_script
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if CT_ID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Container ID" 8 58 "$NEXTID" --title "CONTAINER ID" 3>&1 1>&2 2>&3); then
|
if CT_ID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Container ID" 8 58 "$NEXTID" --title "CONTAINER ID" 3>&1 1>&2 2>&3); then
|
||||||
if [ -z "$CT_ID" ]; then
|
if [ -z "$CT_ID" ]; then
|
||||||
CT_ID="$NEXTID"
|
CT_ID="$NEXTID"
|
||||||
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}"
|
|
||||||
else
|
|
||||||
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}"
|
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
exit_script
|
exit_script
|
||||||
fi
|
fi
|
||||||
|
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
if CT_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$NSAPP" --title "HOSTNAME" 3>&1 1>&2 2>&3); then
|
if CT_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$NSAPP" --title "HOSTNAME" 3>&1 1>&2 2>&3); then
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@
|
|||||||
"documentation": "https://docs.bunkerweb.io/latest/",
|
"documentation": "https://docs.bunkerweb.io/latest/",
|
||||||
"website": "https://www.bunkerweb.io/",
|
"website": "https://www.bunkerweb.io/",
|
||||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/bunkerweb.webp",
|
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/bunkerweb.webp",
|
||||||
"config_path": "/opt/bunkerweb/variables.env",
|
"config_path": "/etc/bunkerweb/variables.env",
|
||||||
"description": "BunkerWeb is a security-focused web server that enhances web application protection. It guards against common web vulnerabilities like SQL injection, XSS, and CSRF. It features simple setup and configuration using a YAML file, customizable security rules, and provides detailed logs for traffic monitoring and threat detection.",
|
"description": "BunkerWeb is a security-focused web server that enhances web application protection. It guards against common web vulnerabilities like SQL injection, XSS, and CSRF. It features simple setup and configuration using a YAML file, customizable security rules, and provides detailed logs for traffic monitoring and threat detection.",
|
||||||
"install_methods": [
|
"install_methods": [
|
||||||
{
|
{
|
||||||
|
|||||||
48
scripts/json/execute.json
Normal file
48
scripts/json/execute.json
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"name": "PVE LXC Execute Command",
|
||||||
|
"slug": "lxc-execute",
|
||||||
|
"categories": [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"date_created": "2025-09-18",
|
||||||
|
"type": "pve",
|
||||||
|
"updateable": false,
|
||||||
|
"privileged": false,
|
||||||
|
"interface_port": null,
|
||||||
|
"documentation": null,
|
||||||
|
"website": null,
|
||||||
|
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/proxmox.webp",
|
||||||
|
"config_path": "",
|
||||||
|
"description": "This script allows administrators to execute a custom command inside one or multiple LXC containers on a Proxmox VE node. Containers can be selectively excluded via an interactive checklist. If a container is stopped, the script will automatically start it, run the command, and then shut it down again. Only Debian and Ubuntu based containers are supported.",
|
||||||
|
"install_methods": [
|
||||||
|
{
|
||||||
|
"type": "default",
|
||||||
|
"script": "tools/pve/execute.sh",
|
||||||
|
"resources": {
|
||||||
|
"cpu": null,
|
||||||
|
"ram": null,
|
||||||
|
"hdd": null,
|
||||||
|
"os": null,
|
||||||
|
"version": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default_credentials": {
|
||||||
|
"username": null,
|
||||||
|
"password": null
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
{
|
||||||
|
"text": "Execute within the Proxmox shell.",
|
||||||
|
"type": "info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Non-Debian/Ubuntu containers will be skipped automatically.",
|
||||||
|
"type": "info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Stopped containers will be started temporarily to run the command, then shut down again.",
|
||||||
|
"type": "warning"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"ram": 2048,
|
"ram": 2048,
|
||||||
"hdd": 10,
|
"hdd": 10,
|
||||||
"os": "debian",
|
"os": "debian",
|
||||||
"version": "12"
|
"version": "13"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"documentation": "https://github.com/HydroshieldMKII/Guardian/blob/main/README.md",
|
"documentation": "https://github.com/HydroshieldMKII/Guardian/blob/main/README.md",
|
||||||
"config_path": "/opt/guardian/.env",
|
"config_path": "/opt/guardian/.env",
|
||||||
"website": "https://github.com/HydroshieldMKII/Guardian",
|
"website": "https://github.com/HydroshieldMKII/Guardian",
|
||||||
"logo": null,
|
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/guardian-plex.webp",
|
||||||
"description": "Guardian is a lightweight companion app for Plex that lets you monitor, approve or block devices in real time. It helps you enforce per-user or global policies, stop unwanted sessions automatically and grant temporary access - all through a simple web interface.",
|
"description": "Guardian is a lightweight companion app for Plex that lets you monitor, approve or block devices in real time. It helps you enforce per-user or global policies, stop unwanted sessions automatically and grant temporary access - all through a simple web interface.",
|
||||||
"install_methods": [
|
"install_methods": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"resources": {
|
"resources": {
|
||||||
"cpu": 2,
|
"cpu": 2,
|
||||||
"ram": 2048,
|
"ram": 2048,
|
||||||
"hdd": 8,
|
"hdd": 16,
|
||||||
"os": "ubuntu",
|
"os": "ubuntu",
|
||||||
"version": "24.04"
|
"version": "24.04"
|
||||||
}
|
}
|
||||||
|
|||||||
40
scripts/json/jotty.json
Normal file
40
scripts/json/jotty.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "jotty",
|
||||||
|
"slug": "jotty",
|
||||||
|
"categories": [
|
||||||
|
12
|
||||||
|
],
|
||||||
|
"date_created": "2025-10-21",
|
||||||
|
"type": "ct",
|
||||||
|
"updateable": true,
|
||||||
|
"privileged": false,
|
||||||
|
"interface_port": 3000,
|
||||||
|
"documentation": "https://github.com/fccview/jotty/blob/main/README.md",
|
||||||
|
"website": "https://github.com/fccview/jotty",
|
||||||
|
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/jotty.webp",
|
||||||
|
"config_path": "/opt/jotty/.env",
|
||||||
|
"description": "A simple, self-hosted app for your checklists and notes. Tired of bloated, cloud-based to-do apps? jotty is a lightweight alternative for managing your personal checklists and notes. It's built with Next.js 14, is easy to deploy, and keeps all your data on your own server.",
|
||||||
|
"install_methods": [
|
||||||
|
{
|
||||||
|
"type": "default",
|
||||||
|
"script": "ct/jotty.sh",
|
||||||
|
"resources": {
|
||||||
|
"cpu": 2,
|
||||||
|
"ram": 3072,
|
||||||
|
"hdd": 6,
|
||||||
|
"os": "debian",
|
||||||
|
"version": "13"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default_credentials": {
|
||||||
|
"username": null,
|
||||||
|
"password": null
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
{
|
||||||
|
"text": "jotty was previously named rwMarkable",
|
||||||
|
"type": "info"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"ram": 2048,
|
"ram": 2048,
|
||||||
"hdd": 8,
|
"hdd": 8,
|
||||||
"os": "debian",
|
"os": "debian",
|
||||||
"version": "12"
|
"version": "13"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,186 +1,186 @@
|
|||||||
{
|
{
|
||||||
"categories": [
|
"categories": [
|
||||||
{
|
{
|
||||||
"name": "Proxmox & Virtualization",
|
"name": "Proxmox & Virtualization",
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"sort_order": 1.0,
|
"sort_order": 1.0,
|
||||||
"description": "Tools and scripts to manage Proxmox VE and virtualization platforms effectively.",
|
"description": "Tools and scripts to manage Proxmox VE and virtualization platforms effectively.",
|
||||||
"icon": "server"
|
"icon": "server"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Operating Systems",
|
"name": "Operating Systems",
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"sort_order": 2.0,
|
"sort_order": 2.0,
|
||||||
"description": "Scripts for deploying and managing various operating systems.",
|
"description": "Scripts for deploying and managing various operating systems.",
|
||||||
"icon": "monitor"
|
"icon": "monitor"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Containers & Docker",
|
"name": "Containers & Docker",
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"sort_order": 3.0,
|
"sort_order": 3.0,
|
||||||
"description": "Solutions for containerization using Docker and related technologies.",
|
"description": "Solutions for containerization using Docker and related technologies.",
|
||||||
"icon": "box"
|
"icon": "box"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Network & Firewall",
|
"name": "Network & Firewall",
|
||||||
"id": 4,
|
"id": 4,
|
||||||
"sort_order": 4.0,
|
"sort_order": 4.0,
|
||||||
"description": "Enhance network security and configure firewalls with ease.",
|
"description": "Enhance network security and configure firewalls with ease.",
|
||||||
"icon": "shield"
|
"icon": "shield"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Adblock & DNS",
|
"name": "Adblock & DNS",
|
||||||
"id": 5,
|
"id": 5,
|
||||||
"sort_order": 5.0,
|
"sort_order": 5.0,
|
||||||
"description": "Optimize your network with DNS and ad-blocking solutions.",
|
"description": "Optimize your network with DNS and ad-blocking solutions.",
|
||||||
"icon": "ban"
|
"icon": "ban"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Authentication & Security",
|
"name": "Authentication & Security",
|
||||||
"id": 6,
|
"id": 6,
|
||||||
"sort_order": 6.0,
|
"sort_order": 6.0,
|
||||||
"description": "Secure your infrastructure with authentication and security tools.",
|
"description": "Secure your infrastructure with authentication and security tools.",
|
||||||
"icon": "lock"
|
"icon": "lock"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Backup & Recovery",
|
"name": "Backup & Recovery",
|
||||||
"id": 7,
|
"id": 7,
|
||||||
"sort_order": 7.0,
|
"sort_order": 7.0,
|
||||||
"description": "Reliable backup and recovery scripts to protect your data.",
|
"description": "Reliable backup and recovery scripts to protect your data.",
|
||||||
"icon": "archive"
|
"icon": "archive"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Databases",
|
"name": "Databases",
|
||||||
"id": 8,
|
"id": 8,
|
||||||
"sort_order": 8.0,
|
"sort_order": 8.0,
|
||||||
"description": "Deploy and manage robust database systems with ease.",
|
"description": "Deploy and manage robust database systems with ease.",
|
||||||
"icon": "database"
|
"icon": "database"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Monitoring & Analytics",
|
"name": "Monitoring & Analytics",
|
||||||
"id": 9,
|
"id": 9,
|
||||||
"sort_order": 9.0,
|
"sort_order": 9.0,
|
||||||
"description": "Monitor system performance and analyze data seamlessly.",
|
"description": "Monitor system performance and analyze data seamlessly.",
|
||||||
"icon": "bar-chart"
|
"icon": "bar-chart"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Dashboards & Frontends",
|
"name": "Dashboards & Frontends",
|
||||||
"id": 10,
|
"id": 10,
|
||||||
"sort_order": 10.0,
|
"sort_order": 10.0,
|
||||||
"description": "Create interactive dashboards and user-friendly frontends.",
|
"description": "Create interactive dashboards and user-friendly frontends.",
|
||||||
"icon": "layout"
|
"icon": "layout"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Files & Downloads",
|
"name": "Files & Downloads",
|
||||||
"id": 11,
|
"id": 11,
|
||||||
"sort_order": 11.0,
|
"sort_order": 11.0,
|
||||||
"description": "Manage file sharing and downloading solutions efficiently.",
|
"description": "Manage file sharing and downloading solutions efficiently.",
|
||||||
"icon": "download"
|
"icon": "download"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Documents & Notes",
|
"name": "Documents & Notes",
|
||||||
"id": 12,
|
"id": 12,
|
||||||
"sort_order": 12.0,
|
"sort_order": 12.0,
|
||||||
"description": "Organize and manage documents and note-taking tools.",
|
"description": "Organize and manage documents and note-taking tools.",
|
||||||
"icon": "file-text"
|
"icon": "file-text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Media & Streaming",
|
"name": "Media & Streaming",
|
||||||
"id": 13,
|
"id": 13,
|
||||||
"sort_order": 13.0,
|
"sort_order": 13.0,
|
||||||
"description": "Stream and manage media effortlessly across devices.",
|
"description": "Stream and manage media effortlessly across devices.",
|
||||||
"icon": "play"
|
"icon": "play"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "*Arr Suite",
|
"name": "*Arr Suite",
|
||||||
"id": 14,
|
"id": 14,
|
||||||
"sort_order": 14.0,
|
"sort_order": 14.0,
|
||||||
"description": "Automated media management with the popular *Arr suite tools.",
|
"description": "Automated media management with the popular *Arr suite tools.",
|
||||||
"icon": "tv"
|
"icon": "tv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "NVR & Cameras",
|
"name": "NVR & Cameras",
|
||||||
"id": 15,
|
"id": 15,
|
||||||
"sort_order": 15.0,
|
"sort_order": 15.0,
|
||||||
"description": "Manage network video recorders and camera setups.",
|
"description": "Manage network video recorders and camera setups.",
|
||||||
"icon": "camera"
|
"icon": "camera"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "IoT & Smart Home",
|
"name": "IoT & Smart Home",
|
||||||
"id": 16,
|
"id": 16,
|
||||||
"sort_order": 16.0,
|
"sort_order": 16.0,
|
||||||
"description": "Control and automate IoT devices and smart home systems.",
|
"description": "Control and automate IoT devices and smart home systems.",
|
||||||
"icon": "home"
|
"icon": "home"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "ZigBee, Z-Wave & Matter",
|
"name": "ZigBee, Z-Wave & Matter",
|
||||||
"id": 17,
|
"id": 17,
|
||||||
"sort_order": 17.0,
|
"sort_order": 17.0,
|
||||||
"description": "Solutions for ZigBee, Z-Wave, and Matter-based device management.",
|
"description": "Solutions for ZigBee, Z-Wave, and Matter-based device management.",
|
||||||
"icon": "radio"
|
"icon": "radio"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "MQTT & Messaging",
|
"name": "MQTT & Messaging",
|
||||||
"id": 18,
|
"id": 18,
|
||||||
"sort_order": 18.0,
|
"sort_order": 18.0,
|
||||||
"description": "Set up reliable messaging and MQTT-based communication systems.",
|
"description": "Set up reliable messaging and MQTT-based communication systems.",
|
||||||
"icon": "message-circle"
|
"icon": "message-circle"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Automation & Scheduling",
|
"name": "Automation & Scheduling",
|
||||||
"id": 19,
|
"id": 19,
|
||||||
"sort_order": 19.0,
|
"sort_order": 19.0,
|
||||||
"description": "Automate tasks and manage scheduling with powerful tools.",
|
"description": "Automate tasks and manage scheduling with powerful tools.",
|
||||||
"icon": "clock"
|
"icon": "clock"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "AI / Coding & Dev-Tools",
|
"name": "AI / Coding & Dev-Tools",
|
||||||
"id": 20,
|
"id": 20,
|
||||||
"sort_order": 20.0,
|
"sort_order": 20.0,
|
||||||
"description": "Leverage AI and developer tools for smarter coding workflows.",
|
"description": "Leverage AI and developer tools for smarter coding workflows.",
|
||||||
"icon": "code"
|
"icon": "code"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Webservers & Proxies",
|
"name": "Webservers & Proxies",
|
||||||
"id": 21,
|
"id": 21,
|
||||||
"sort_order": 21.0,
|
"sort_order": 21.0,
|
||||||
"description": "Deploy and configure web servers and proxy solutions.",
|
"description": "Deploy and configure web servers and proxy solutions.",
|
||||||
"icon": "globe"
|
"icon": "globe"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Bots & ChatOps",
|
"name": "Bots & ChatOps",
|
||||||
"id": 22,
|
"id": 22,
|
||||||
"sort_order": 22.0,
|
"sort_order": 22.0,
|
||||||
"description": "Enhance collaboration with bots and ChatOps integrations.",
|
"description": "Enhance collaboration with bots and ChatOps integrations.",
|
||||||
"icon": "bot"
|
"icon": "bot"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Finance & Budgeting",
|
"name": "Finance & Budgeting",
|
||||||
"id": 23,
|
"id": 23,
|
||||||
"sort_order": 23.0,
|
"sort_order": 23.0,
|
||||||
"description": "Track expenses and manage budgets efficiently.",
|
"description": "Track expenses and manage budgets efficiently.",
|
||||||
"icon": "dollar-sign"
|
"icon": "dollar-sign"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Gaming & Leisure",
|
"name": "Gaming & Leisure",
|
||||||
"id": 24,
|
"id": 24,
|
||||||
"sort_order": 24.0,
|
"sort_order": 24.0,
|
||||||
"description": "Scripts for gaming servers and leisure-related tools.",
|
"description": "Scripts for gaming servers and leisure-related tools.",
|
||||||
"icon": "gamepad-2"
|
"icon": "gamepad-2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Business & ERP",
|
"name": "Business & ERP",
|
||||||
"id": 25,
|
"id": 25,
|
||||||
"sort_order": 25.0,
|
"sort_order": 25.0,
|
||||||
"description": "Streamline business operations with ERP and management tools.",
|
"description": "Streamline business operations with ERP and management tools.",
|
||||||
"icon": "building"
|
"icon": "building"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Miscellaneous",
|
"name": "Miscellaneous",
|
||||||
"id": 0,
|
"id": 0,
|
||||||
"sort_order": 99.0,
|
"sort_order": 99.0,
|
||||||
"description": "General scripts and tools that don't fit into other categories.",
|
"description": "General scripts and tools that don't fit into other categories.",
|
||||||
"icon": "more-horizontal"
|
"icon": "more-horizontal"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"ram": 1024,
|
"ram": 1024,
|
||||||
"hdd": 4,
|
"hdd": 4,
|
||||||
"os": "debian",
|
"os": "debian",
|
||||||
"version": "13"
|
"version": "12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"ram": 2048,
|
"ram": 2048,
|
||||||
"hdd": 8,
|
"hdd": 8,
|
||||||
"os": "debian",
|
"os": "debian",
|
||||||
"version": "13"
|
"version": "12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"ram": 2048,
|
"ram": 2048,
|
||||||
"hdd": 6,
|
"hdd": 6,
|
||||||
"os": "debian",
|
"os": "debian",
|
||||||
"version": "13"
|
"version": "12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"ram": 1024,
|
"ram": 1024,
|
||||||
"hdd": 4,
|
"hdd": 4,
|
||||||
"os": "debian",
|
"os": "debian",
|
||||||
"version": "13"
|
"version": "12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
40
scripts/json/open-archiver.json
Normal file
40
scripts/json/open-archiver.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "Open-Archiver",
|
||||||
|
"slug": "open-archiver",
|
||||||
|
"categories": [
|
||||||
|
7
|
||||||
|
],
|
||||||
|
"date_created": "2025-10-18",
|
||||||
|
"type": "ct",
|
||||||
|
"updateable": true,
|
||||||
|
"privileged": false,
|
||||||
|
"interface_port": 3000,
|
||||||
|
"documentation": "https://docs.openarchiver.com/",
|
||||||
|
"config_path": "/opt/openarchiver/.env",
|
||||||
|
"website": "https://openarchiver.com/",
|
||||||
|
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/open-archiver.webp",
|
||||||
|
"description": "Open Archiver is a secure, self-hosted email archiving solution, and it's completely open source. Get an email archiver that enables full-text search across email and attachments. Create a permanent, searchable, and compliant mail archive from Google Workspace, Microsoft 35, and any IMAP server.",
|
||||||
|
"install_methods": [
|
||||||
|
{
|
||||||
|
"type": "default",
|
||||||
|
"script": "ct/open-archiver.sh",
|
||||||
|
"resources": {
|
||||||
|
"cpu": 2,
|
||||||
|
"ram": 3072,
|
||||||
|
"hdd": 8,
|
||||||
|
"os": "debian",
|
||||||
|
"version": "13"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default_credentials": {
|
||||||
|
"username": null,
|
||||||
|
"password": null
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
{
|
||||||
|
"text": "Data directory is: `/opt/openarchiver-data`. If you have a lot of email, you might consider mounting external storage to this directory.",
|
||||||
|
"type": "info"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"ram": 8192,
|
"ram": 8192,
|
||||||
"hdd": 25,
|
"hdd": 25,
|
||||||
"os": "debian",
|
"os": "debian",
|
||||||
"version": "13"
|
"version": "12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
"type": "default",
|
"type": "default",
|
||||||
"script": "ct/paperless-ai.sh",
|
"script": "ct/paperless-ai.sh",
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": 2,
|
"cpu": 4,
|
||||||
"ram": 2048,
|
"ram": 4096,
|
||||||
"hdd": 20,
|
"hdd": 20,
|
||||||
"os": "debian",
|
"os": "debian",
|
||||||
"version": "13"
|
"version": "13"
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"ram": 1024,
|
"ram": 1024,
|
||||||
"hdd": 4,
|
"hdd": 4,
|
||||||
"os": "debian",
|
"os": "debian",
|
||||||
"version": "13"
|
"version": "12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"ram": 512,
|
"ram": 512,
|
||||||
"hdd": 2,
|
"hdd": 2,
|
||||||
"os": "debian",
|
"os": "debian",
|
||||||
"version": "13"
|
"version": "12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,7 @@
|
|||||||
"ram": 2048,
|
"ram": 2048,
|
||||||
"hdd": 5,
|
"hdd": 5,
|
||||||
"os": "Debian",
|
"os": "Debian",
|
||||||
"version": "12"
|
"version": "13"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
11
server.js
11
server.js
@@ -8,6 +8,11 @@ import stripAnsi from 'strip-ansi';
|
|||||||
import { spawn as ptySpawn } from 'node-pty';
|
import { spawn as ptySpawn } from 'node-pty';
|
||||||
import { getSSHExecutionService } from './src/server/ssh-execution-service.js';
|
import { getSSHExecutionService } from './src/server/ssh-execution-service.js';
|
||||||
import { getDatabase } from './src/server/database-prisma.js';
|
import { getDatabase } from './src/server/database-prisma.js';
|
||||||
|
import { initializeAutoSync, setupGracefulShutdown } from './src/server/lib/autoSyncInit.js';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
// Load environment variables from .env file
|
||||||
|
dotenv.config();
|
||||||
// Fallback minimal global error handlers for Node runtime (avoid TS import)
|
// Fallback minimal global error handlers for Node runtime (avoid TS import)
|
||||||
function registerGlobalErrorHandlers() {
|
function registerGlobalErrorHandlers() {
|
||||||
if (registerGlobalErrorHandlers._registered) return;
|
if (registerGlobalErrorHandlers._registered) return;
|
||||||
@@ -976,5 +981,11 @@ app.prepare().then(() => {
|
|||||||
.listen(port, hostname, () => {
|
.listen(port, hostname, () => {
|
||||||
console.log(`> Ready on http://${hostname}:${port}`);
|
console.log(`> Ready on http://${hostname}:${port}`);
|
||||||
console.log(`> WebSocket server running on ws://${hostname}:${port}/ws/script-execution`);
|
console.log(`> WebSocket server running on ws://${hostname}:${port}/ws/script-execution`);
|
||||||
|
|
||||||
|
// Initialize auto-sync service
|
||||||
|
initializeAutoSync();
|
||||||
|
|
||||||
|
// Setup graceful shutdown handlers
|
||||||
|
setupGracefulShutdown();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
|
|
||||||
> pve-scripts-local@0.1.0 start
|
|
||||||
> node server.js
|
|
||||||
|
|
||||||
> Ready on http://0.0.0.0:3000
|
|
||||||
> WebSocket server running on ws://0.0.0.0:3000/ws/script-execution
|
|
||||||
@@ -356,7 +356,7 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr
|
|||||||
}
|
}
|
||||||
}, [selectedCategory]);
|
}, [selectedCategory]);
|
||||||
|
|
||||||
const handleCardClick = (scriptCard: { slug: string }) => {
|
const handleCardClick = (scriptCard: ScriptCardType) => {
|
||||||
// All scripts are GitHub scripts, open modal
|
// All scripts are GitHub scripts, open modal
|
||||||
setSelectedSlug(scriptCard.slug);
|
setSelectedSlug(scriptCard.slug);
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Toggle } from './ui/toggle';
|
|||||||
import { ContextualHelpIcon } from './ContextualHelpIcon';
|
import { ContextualHelpIcon } from './ContextualHelpIcon';
|
||||||
import { useTheme } from './ThemeProvider';
|
import { useTheme } from './ThemeProvider';
|
||||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||||
|
import { api } from '~/trpc/react';
|
||||||
|
|
||||||
interface GeneralSettingsModalProps {
|
interface GeneralSettingsModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -16,7 +17,7 @@ interface GeneralSettingsModalProps {
|
|||||||
export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalProps) {
|
export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalProps) {
|
||||||
useRegisterModal(isOpen, { id: 'general-settings-modal', allowEscape: true, onClose });
|
useRegisterModal(isOpen, { id: 'general-settings-modal', allowEscape: true, onClose });
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
const [activeTab, setActiveTab] = useState<'general' | 'github' | 'auth'>('general');
|
const [activeTab, setActiveTab] = useState<'general' | 'github' | 'auth' | 'auto-sync'>('general');
|
||||||
const [githubToken, setGithubToken] = useState('');
|
const [githubToken, setGithubToken] = useState('');
|
||||||
const [saveFilter, setSaveFilter] = useState(false);
|
const [saveFilter, setSaveFilter] = useState(false);
|
||||||
const [savedFilters, setSavedFilters] = useState<any>(null);
|
const [savedFilters, setSavedFilters] = useState<any>(null);
|
||||||
@@ -34,6 +35,21 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
|
|||||||
const [authSetupCompleted, setAuthSetupCompleted] = useState(false);
|
const [authSetupCompleted, setAuthSetupCompleted] = useState(false);
|
||||||
const [authLoading, setAuthLoading] = useState(false);
|
const [authLoading, setAuthLoading] = useState(false);
|
||||||
|
|
||||||
|
// Auto-sync state
|
||||||
|
const [autoSyncEnabled, setAutoSyncEnabled] = useState(false);
|
||||||
|
const [syncIntervalType, setSyncIntervalType] = useState<'predefined' | 'custom'>('predefined');
|
||||||
|
const [syncIntervalPredefined, setSyncIntervalPredefined] = useState('1hour');
|
||||||
|
const [syncIntervalCron, setSyncIntervalCron] = useState('');
|
||||||
|
const [autoDownloadNew, setAutoDownloadNew] = useState(false);
|
||||||
|
const [autoUpdateExisting, setAutoUpdateExisting] = useState(false);
|
||||||
|
const [notificationEnabled, setNotificationEnabled] = useState(false);
|
||||||
|
const [appriseUrls, setAppriseUrls] = useState<string[]>([]);
|
||||||
|
const [appriseUrlsText, setAppriseUrlsText] = useState('');
|
||||||
|
const [lastAutoSync, setLastAutoSync] = useState('');
|
||||||
|
const [lastAutoSyncError, setLastAutoSyncError] = useState<string | null>(null);
|
||||||
|
const [lastAutoSyncErrorTime, setLastAutoSyncErrorTime] = useState<string | null>(null);
|
||||||
|
const [cronValidationError, setCronValidationError] = useState('');
|
||||||
|
|
||||||
// Load existing settings when modal opens
|
// Load existing settings when modal opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
@@ -42,6 +58,7 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
|
|||||||
void loadSavedFilters();
|
void loadSavedFilters();
|
||||||
void loadAuthCredentials();
|
void loadAuthCredentials();
|
||||||
void loadColorCodingSetting();
|
void loadColorCodingSetting();
|
||||||
|
void loadAutoSyncSettings();
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
@@ -278,6 +295,140 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Auto-sync functions
|
||||||
|
const loadAutoSyncSettings = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/settings/auto-sync');
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json() as { settings: any };
|
||||||
|
const settings = data.settings;
|
||||||
|
if (settings) {
|
||||||
|
setAutoSyncEnabled(settings.autoSyncEnabled ?? false);
|
||||||
|
setSyncIntervalType(settings.syncIntervalType ?? 'predefined');
|
||||||
|
setSyncIntervalPredefined(settings.syncIntervalPredefined ?? '1hour');
|
||||||
|
setSyncIntervalCron(settings.syncIntervalCron ?? '');
|
||||||
|
setAutoDownloadNew(settings.autoDownloadNew ?? false);
|
||||||
|
setAutoUpdateExisting(settings.autoUpdateExisting ?? false);
|
||||||
|
setNotificationEnabled(settings.notificationEnabled ?? false);
|
||||||
|
setAppriseUrls(settings.appriseUrls ?? []);
|
||||||
|
setAppriseUrlsText((settings.appriseUrls ?? []).join('\n'));
|
||||||
|
setLastAutoSync(settings.lastAutoSync ?? '');
|
||||||
|
setLastAutoSyncError(settings.lastAutoSyncError ?? null);
|
||||||
|
setLastAutoSyncErrorTime(settings.lastAutoSyncErrorTime ?? null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading auto-sync settings:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveAutoSyncSettings = async () => {
|
||||||
|
setIsSaving(true);
|
||||||
|
setMessage(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/settings/auto-sync', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
autoSyncEnabled,
|
||||||
|
syncIntervalType,
|
||||||
|
syncIntervalPredefined,
|
||||||
|
syncIntervalCron,
|
||||||
|
autoDownloadNew,
|
||||||
|
autoUpdateExisting,
|
||||||
|
notificationEnabled,
|
||||||
|
appriseUrls: appriseUrls
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setMessage({ type: 'success', text: 'Auto-sync settings saved successfully!' });
|
||||||
|
setTimeout(() => setMessage(null), 3000);
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
setMessage({ type: 'error', text: errorData.error ?? 'Failed to save auto-sync settings' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving auto-sync settings:', error);
|
||||||
|
setMessage({ type: 'error', text: 'Failed to save auto-sync settings' });
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAppriseUrlsChange = (text: string) => {
|
||||||
|
setAppriseUrlsText(text);
|
||||||
|
const urls = text.split('\n').filter(url => url.trim() !== '');
|
||||||
|
setAppriseUrls(urls);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateCronExpression = (cron: string) => {
|
||||||
|
if (!cron.trim()) {
|
||||||
|
setCronValidationError('');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic cron validation - you might want to use a library like cron-validator
|
||||||
|
const cronRegex = /^(\*|([0-5]?\d)) (\*|([01]?\d|2[0-3])) (\*|([012]?\d|3[01])) (\*|([0]?\d|1[0-2])) (\*|([0-6]))$/;
|
||||||
|
const isValid = cronRegex.test(cron);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
setCronValidationError('Invalid cron expression format');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCronValidationError('');
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCronChange = (cron: string) => {
|
||||||
|
setSyncIntervalCron(cron);
|
||||||
|
validateCronExpression(cron);
|
||||||
|
};
|
||||||
|
|
||||||
|
const testNotification = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/settings/auto-sync', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ testNotification: true })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setMessage({ type: 'success', text: 'Test notification sent successfully!' });
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
setMessage({ type: 'error', text: errorData.error ?? 'Failed to send test notification' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending test notification:', error);
|
||||||
|
setMessage({ type: 'error', text: 'Failed to send test notification' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerManualSync = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/settings/auto-sync', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ triggerManualSync: true })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setMessage({ type: 'success', text: 'Manual sync triggered successfully!' });
|
||||||
|
// Reload settings to get updated last sync time
|
||||||
|
await loadAutoSyncSettings();
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
setMessage({ type: 'error', text: errorData.error ?? 'Failed to trigger manual sync' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error triggering manual sync:', error);
|
||||||
|
setMessage({ type: 'error', text: 'Failed to trigger manual sync' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -340,6 +491,18 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
|
|||||||
>
|
>
|
||||||
Authentication
|
Authentication
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => setActiveTab('auto-sync')}
|
||||||
|
variant="ghost"
|
||||||
|
size="null"
|
||||||
|
className={`py-3 sm:py-4 px-1 border-b-2 font-medium text-sm w-full sm:w-auto ${
|
||||||
|
activeTab === 'auto-sync'
|
||||||
|
? 'border-primary text-primary'
|
||||||
|
: 'border-transparent text-muted-foreground hover:text-foreground hover:border-border'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Auto-Sync
|
||||||
|
</Button>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -623,6 +786,302 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'auto-sync' && (
|
||||||
|
<div className="space-y-4 sm:space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-base sm:text-lg font-medium text-foreground mb-3 sm:mb-4">Auto-Sync Settings</h3>
|
||||||
|
<p className="text-sm sm:text-base text-muted-foreground mb-4">
|
||||||
|
Configure automatic synchronization of scripts with configurable intervals and notifications.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Enable Auto-Sync */}
|
||||||
|
<div className="p-4 border border-border rounded-lg mb-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-foreground mb-1">Enable Auto-Sync</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">Automatically sync JSON files from GitHub at specified intervals</p>
|
||||||
|
</div>
|
||||||
|
<Toggle
|
||||||
|
checked={autoSyncEnabled}
|
||||||
|
onCheckedChange={async (checked) => {
|
||||||
|
setAutoSyncEnabled(checked);
|
||||||
|
|
||||||
|
// Auto-save when toggle changes
|
||||||
|
try {
|
||||||
|
// If syncIntervalType is custom but no cron expression, fallback to predefined
|
||||||
|
const effectiveSyncIntervalType = (syncIntervalType === 'custom' && !syncIntervalCron)
|
||||||
|
? 'predefined'
|
||||||
|
: syncIntervalType;
|
||||||
|
|
||||||
|
const response = await fetch('/api/settings/auto-sync', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
autoSyncEnabled: checked,
|
||||||
|
syncIntervalType: effectiveSyncIntervalType,
|
||||||
|
syncIntervalPredefined: effectiveSyncIntervalType === 'predefined' ? syncIntervalPredefined : undefined,
|
||||||
|
syncIntervalCron: effectiveSyncIntervalType === 'custom' ? syncIntervalCron : undefined,
|
||||||
|
autoDownloadNew,
|
||||||
|
autoUpdateExisting,
|
||||||
|
notificationEnabled,
|
||||||
|
appriseUrls: appriseUrls
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Update local state to reflect the effective sync interval type
|
||||||
|
if (effectiveSyncIntervalType !== syncIntervalType) {
|
||||||
|
setSyncIntervalType(effectiveSyncIntervalType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving auto-sync toggle:', error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={isSaving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sync Interval */}
|
||||||
|
{autoSyncEnabled && (
|
||||||
|
<div className="p-4 border border-border rounded-lg mb-4">
|
||||||
|
<h4 className="font-medium text-foreground mb-3">Sync Interval</h4>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex space-x-4">
|
||||||
|
<label className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="syncIntervalType"
|
||||||
|
value="predefined"
|
||||||
|
checked={syncIntervalType === 'predefined'}
|
||||||
|
onChange={(e) => setSyncIntervalType(e.target.value as 'predefined' | 'custom')}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
Predefined
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="syncIntervalType"
|
||||||
|
value="custom"
|
||||||
|
checked={syncIntervalType === 'custom'}
|
||||||
|
onChange={(e) => setSyncIntervalType(e.target.value as 'predefined' | 'custom')}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
Custom Cron
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{syncIntervalType === 'predefined' && (
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
value={syncIntervalPredefined}
|
||||||
|
onChange={(e) => setSyncIntervalPredefined(e.target.value)}
|
||||||
|
className="w-full p-2 border border-border rounded-md bg-background"
|
||||||
|
>
|
||||||
|
<option value="15min">Every 15 minutes</option>
|
||||||
|
<option value="30min">Every 30 minutes</option>
|
||||||
|
<option value="1hour">Every hour</option>
|
||||||
|
<option value="6hours">Every 6 hours</option>
|
||||||
|
<option value="12hours">Every 12 hours</option>
|
||||||
|
<option value="24hours">Every 24 hours</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{syncIntervalType === 'custom' && (
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
placeholder="0 */6 * * * (every 6 hours)"
|
||||||
|
value={syncIntervalCron}
|
||||||
|
onChange={(e) => handleCronChange(e.target.value)}
|
||||||
|
className="w-full"
|
||||||
|
autoFocus
|
||||||
|
onFocus={() => setCronValidationError('')}
|
||||||
|
/>
|
||||||
|
{cronValidationError && (
|
||||||
|
<p className="text-sm text-red-500 mt-1">{cronValidationError}</p>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Format: minute hour day month weekday. See{' '}
|
||||||
|
<a href="https://crontab.guru" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
|
||||||
|
crontab.guru
|
||||||
|
</a>{' '}
|
||||||
|
for examples
|
||||||
|
</p>
|
||||||
|
<div className="mt-2 p-2 bg-muted rounded text-xs">
|
||||||
|
<p className="font-medium mb-1">Common examples:</p>
|
||||||
|
<ul className="space-y-1 text-muted-foreground">
|
||||||
|
<li>• <code>* * * * *</code> - Every minute</li>
|
||||||
|
<li>• <code>0 * * * *</code> - Every hour</li>
|
||||||
|
<li>• <code>0 */6 * * *</code> - Every 6 hours</li>
|
||||||
|
<li>• <code>0 0 * * *</code> - Every day at midnight</li>
|
||||||
|
<li>• <code>0 0 * * 0</code> - Every Sunday at midnight</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Auto-Download Options */}
|
||||||
|
{autoSyncEnabled && (
|
||||||
|
<div className="p-4 border border-border rounded-lg mb-4">
|
||||||
|
<h4 className="font-medium text-foreground mb-3">Auto-Download Options</h4>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h5 className="font-medium text-foreground">Auto-download new scripts</h5>
|
||||||
|
<p className="text-sm text-muted-foreground">Automatically download scripts that haven't been downloaded yet</p>
|
||||||
|
</div>
|
||||||
|
<Toggle
|
||||||
|
checked={autoDownloadNew}
|
||||||
|
onCheckedChange={setAutoDownloadNew}
|
||||||
|
disabled={isSaving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h5 className="font-medium text-foreground">Auto-update existing scripts</h5>
|
||||||
|
<p className="text-sm text-muted-foreground">Automatically update scripts that have newer versions available</p>
|
||||||
|
</div>
|
||||||
|
<Toggle
|
||||||
|
checked={autoUpdateExisting}
|
||||||
|
onCheckedChange={setAutoUpdateExisting}
|
||||||
|
disabled={isSaving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Notifications */}
|
||||||
|
{autoSyncEnabled && (
|
||||||
|
<div className="p-4 border border-border rounded-lg mb-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-foreground">Enable Notifications</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">Send notifications when sync completes</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
If you want any other notification service, please open an issue on the GitHub repository.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Toggle
|
||||||
|
checked={notificationEnabled}
|
||||||
|
onCheckedChange={setNotificationEnabled}
|
||||||
|
disabled={isSaving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{notificationEnabled && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="apprise-urls" className="block text-sm font-medium text-foreground mb-1">
|
||||||
|
Apprise URLs
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="apprise-urls"
|
||||||
|
placeholder="http://YOUR_APPRISE_SERVER/notify/apprise "
|
||||||
|
value={appriseUrlsText}
|
||||||
|
onChange={(e) => handleAppriseUrlsChange(e.target.value)}
|
||||||
|
className="w-full p-2 border border-border rounded-md bg-background h-24 resize-none"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
One URL per line. Supports Discord, Telegram, Email, Slack, and more via{' '}
|
||||||
|
<a href="https://github.com/caronc/apprise" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
|
||||||
|
Apprise
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={testNotification}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={appriseUrls.length === 0}
|
||||||
|
>
|
||||||
|
Test Notification
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Status and Actions */}
|
||||||
|
{autoSyncEnabled && (
|
||||||
|
<div className="p-4 border border-border rounded-lg mb-4">
|
||||||
|
<h4 className="font-medium text-foreground mb-3">Status & Actions</h4>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{lastAutoSync && (
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Last sync: {new Date(lastAutoSync).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{lastAutoSyncError && (
|
||||||
|
<div className="p-3 bg-error/10 text-error-foreground border border-error/20 rounded-md">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<svg className="w-4 h-4 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">Last sync error:</p>
|
||||||
|
<p className="text-sm mt-1">{lastAutoSyncError}</p>
|
||||||
|
{lastAutoSyncErrorTime && (
|
||||||
|
<p className="text-xs mt-1 opacity-75">
|
||||||
|
{new Date(lastAutoSyncErrorTime).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={triggerManualSync}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Trigger Sync Now
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={saveAutoSyncSettings}
|
||||||
|
disabled={isSaving || (syncIntervalType === 'custom' && !!cronValidationError)}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{isSaving ? 'Saving...' : 'Save Settings'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Message Display */}
|
||||||
|
{message && (
|
||||||
|
<div className={`p-3 rounded-md text-sm ${
|
||||||
|
message.type === 'success'
|
||||||
|
? 'bg-success/10 text-success-foreground border border-success/20'
|
||||||
|
: 'bg-error/10 text-error-foreground border border-error/20'
|
||||||
|
}`}>
|
||||||
|
{message.text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import { HelpCircle, Server, Settings, RefreshCw, Package, HardDrive, FolderOpen, Search, Download } from 'lucide-react';
|
import { HelpCircle, Server, Settings, RefreshCw, Clock, Package, HardDrive, FolderOpen, Search, Download } from 'lucide-react';
|
||||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||||
|
|
||||||
interface HelpModalProps {
|
interface HelpModalProps {
|
||||||
@@ -11,7 +11,7 @@ interface HelpModalProps {
|
|||||||
initialSection?: string;
|
initialSection?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type HelpSection = 'server-settings' | 'general-settings' | 'sync-button' | 'available-scripts' | 'downloaded-scripts' | 'installed-scripts' | 'lxc-settings' | 'update-system';
|
type HelpSection = 'server-settings' | 'general-settings' | 'sync-button' | 'auto-sync' | 'available-scripts' | 'downloaded-scripts' | 'installed-scripts' | 'lxc-settings' | 'update-system';
|
||||||
|
|
||||||
export function HelpModal({ isOpen, onClose, initialSection = 'server-settings' }: HelpModalProps) {
|
export function HelpModal({ isOpen, onClose, initialSection = 'server-settings' }: HelpModalProps) {
|
||||||
useRegisterModal(isOpen, { id: 'help-modal', allowEscape: true, onClose });
|
useRegisterModal(isOpen, { id: 'help-modal', allowEscape: true, onClose });
|
||||||
@@ -23,6 +23,7 @@ export function HelpModal({ isOpen, onClose, initialSection = 'server-settings'
|
|||||||
{ id: 'server-settings' as HelpSection, label: 'Server Settings', icon: Server },
|
{ id: 'server-settings' as HelpSection, label: 'Server Settings', icon: Server },
|
||||||
{ id: 'general-settings' as HelpSection, label: 'General Settings', icon: Settings },
|
{ id: 'general-settings' as HelpSection, label: 'General Settings', icon: Settings },
|
||||||
{ id: 'sync-button' as HelpSection, label: 'Sync Button', icon: RefreshCw },
|
{ id: 'sync-button' as HelpSection, label: 'Sync Button', icon: RefreshCw },
|
||||||
|
{ id: 'auto-sync' as HelpSection, label: 'Auto-Sync', icon: Clock },
|
||||||
{ id: 'available-scripts' as HelpSection, label: 'Available Scripts', icon: Package },
|
{ id: 'available-scripts' as HelpSection, label: 'Available Scripts', icon: Package },
|
||||||
{ id: 'downloaded-scripts' as HelpSection, label: 'Downloaded Scripts', icon: HardDrive },
|
{ id: 'downloaded-scripts' as HelpSection, label: 'Downloaded Scripts', icon: HardDrive },
|
||||||
{ id: 'installed-scripts' as HelpSection, label: 'Installed Scripts', icon: FolderOpen },
|
{ id: 'installed-scripts' as HelpSection, label: 'Installed Scripts', icon: FolderOpen },
|
||||||
@@ -185,6 +186,101 @@ export function HelpModal({ isOpen, onClose, initialSection = 'server-settings'
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case 'auto-sync':
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-foreground mb-4">Auto-Sync</h3>
|
||||||
|
<p className="text-muted-foreground mb-6">
|
||||||
|
Configure automatic synchronization of scripts with configurable intervals and notifications.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="p-4 border border-border rounded-lg">
|
||||||
|
<h4 className="font-medium text-foreground mb-2">What Is Auto-Sync?</h4>
|
||||||
|
<p className="text-sm text-muted-foreground mb-2">
|
||||||
|
Auto-sync automatically synchronizes script metadata from the ProxmoxVE GitHub repository at specified intervals,
|
||||||
|
and optionally downloads/updates scripts and sends notifications.
|
||||||
|
</p>
|
||||||
|
<ul className="text-sm text-muted-foreground space-y-1">
|
||||||
|
<li>• <strong>Automatic JSON Sync:</strong> Downloads latest script metadata periodically</li>
|
||||||
|
<li>• <strong>Auto-Download:</strong> Automatically download new scripts when available</li>
|
||||||
|
<li>• <strong>Auto-Update:</strong> Automatically update existing scripts to newer versions</li>
|
||||||
|
<li>• <strong>Notifications:</strong> Send notifications when sync completes</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 border border-border rounded-lg">
|
||||||
|
<h4 className="font-medium text-foreground mb-2">Sync Intervals</h4>
|
||||||
|
<ul className="text-sm text-muted-foreground space-y-2">
|
||||||
|
<li>• <strong>Predefined:</strong> Choose from common intervals (15min, 30min, 1hour, 6hours, 12hours, 24hours)</li>
|
||||||
|
<li>• <strong>Custom Cron:</strong> Use cron expressions for advanced scheduling</li>
|
||||||
|
<li>• <strong>Examples:</strong>
|
||||||
|
<ul className="ml-4 mt-1 space-y-1">
|
||||||
|
<li>• <code>0 */6 * * *</code> - Every 6 hours</li>
|
||||||
|
<li>• <code>0 0 * * *</code> - Daily at midnight</li>
|
||||||
|
<li>• <code>0 9 * * 1</code> - Every Monday at 9 AM</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 border border-border rounded-lg">
|
||||||
|
<h4 className="font-medium text-foreground mb-2">Auto-Download Options</h4>
|
||||||
|
<ul className="text-sm text-muted-foreground space-y-2">
|
||||||
|
<li>• <strong>Auto-download new scripts:</strong> Automatically download scripts that haven't been downloaded yet</li>
|
||||||
|
<li>• <strong>Auto-update existing scripts:</strong> Automatically update scripts that have newer versions available</li>
|
||||||
|
<li>• <strong>Selective Control:</strong> Enable/disable each option independently</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 border border-border rounded-lg">
|
||||||
|
<h4 className="font-medium text-foreground mb-2">Notifications (Apprise)</h4>
|
||||||
|
<p className="text-sm text-muted-foreground mb-2">
|
||||||
|
Send notifications when sync completes using Apprise, which supports 80+ notification services.
|
||||||
|
If you want any other notification service, please open an issue on the GitHub repository.
|
||||||
|
</p>
|
||||||
|
<ul className="text-sm text-muted-foreground space-y-2">
|
||||||
|
<li>• <strong>Apprise Server:</strong> <code>http://YOUR_APPRISE_SERVER/notify/apprise</code></li>
|
||||||
|
</ul>
|
||||||
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
|
See the <a href="https://github.com/caronc/apprise" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Apprise documentation</a> for more supported services.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 border border-border rounded-lg">
|
||||||
|
<h4 className="font-medium text-foreground mb-2">Setup Guide</h4>
|
||||||
|
<ol className="text-sm text-muted-foreground space-y-2 list-decimal list-inside">
|
||||||
|
<li>Enable auto-sync in the General Settings → Auto-Sync tab</li>
|
||||||
|
<li>Choose your sync interval (predefined or custom cron)</li>
|
||||||
|
<li>Configure auto-download options if desired</li>
|
||||||
|
<li>Set up notifications by adding Apprise URLs</li>
|
||||||
|
<li>Test your notification setup using the "Test Notification" button</li>
|
||||||
|
<li>Save your settings to activate auto-sync</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 border border-border rounded-lg">
|
||||||
|
<h4 className="font-medium text-foreground mb-2">Cron Expression Help</h4>
|
||||||
|
<p className="text-sm text-muted-foreground mb-2">
|
||||||
|
Cron expressions have 5 fields: minute hour day month weekday
|
||||||
|
</p>
|
||||||
|
<ul className="text-sm text-muted-foreground space-y-1">
|
||||||
|
<li>• <strong>Minute:</strong> 0-59 or *</li>
|
||||||
|
<li>• <strong>Hour:</strong> 0-23 or *</li>
|
||||||
|
<li>• <strong>Day:</strong> 1-31 or *</li>
|
||||||
|
<li>• <strong>Month:</strong> 1-12 or *</li>
|
||||||
|
<li>• <strong>Weekday:</strong> 0-6 (Sunday=0) or *</li>
|
||||||
|
</ul>
|
||||||
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
|
Use <a href="https://crontab.guru" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">crontab.guru</a> to test and learn cron expressions.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
case 'available-scripts':
|
case 'available-scripts':
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|||||||
@@ -574,7 +574,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const handleCardClick = (scriptCard: { slug: string }) => {
|
const handleCardClick = (scriptCard: ScriptCardType) => {
|
||||||
// All scripts are GitHub scripts, open modal
|
// All scripts are GitHub scripts, open modal
|
||||||
setSelectedSlug(scriptCard.slug);
|
setSelectedSlug(scriptCard.slug);
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
|
|||||||
396
src/app/api/settings/auto-sync/route.ts
Normal file
396
src/app/api/settings/auto-sync/route.ts
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { isValidCron } from 'cron-validator';
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const settings = await request.json();
|
||||||
|
|
||||||
|
if (!settings || typeof settings !== 'object') {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Settings object is required' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle test notification request
|
||||||
|
if (settings.testNotification) {
|
||||||
|
return await handleTestNotification();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle manual sync trigger
|
||||||
|
if (settings.triggerManualSync) {
|
||||||
|
return await handleManualSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields for settings save
|
||||||
|
const requiredFields = [
|
||||||
|
'autoSyncEnabled',
|
||||||
|
'syncIntervalType',
|
||||||
|
'autoDownloadNew',
|
||||||
|
'autoUpdateExisting',
|
||||||
|
'notificationEnabled'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const field of requiredFields) {
|
||||||
|
if (!(field in settings)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: `Missing required field: ${field}` },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate sync interval type
|
||||||
|
if (!['predefined', 'custom'].includes(settings.syncIntervalType)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'syncIntervalType must be "predefined" or "custom"' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate predefined interval
|
||||||
|
if (settings.syncIntervalType === 'predefined') {
|
||||||
|
const validIntervals = ['15min', '30min', '1hour', '6hours', '12hours', '24hours'];
|
||||||
|
if (!validIntervals.includes(settings.syncIntervalPredefined)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Invalid predefined interval' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate custom cron expression
|
||||||
|
if (settings.syncIntervalType === 'custom') {
|
||||||
|
if (!settings.syncIntervalCron || typeof settings.syncIntervalCron !== 'string' || settings.syncIntervalCron.trim() === '') {
|
||||||
|
// Fallback to predefined if custom is selected but no cron expression
|
||||||
|
settings.syncIntervalType = 'predefined';
|
||||||
|
settings.syncIntervalPredefined = settings.syncIntervalPredefined || '1hour';
|
||||||
|
settings.syncIntervalCron = '';
|
||||||
|
} else if (!isValidCron(settings.syncIntervalCron, { seconds: false })) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Invalid cron expression' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Apprise URLs if notifications are enabled
|
||||||
|
if (settings.notificationEnabled && settings.appriseUrls) {
|
||||||
|
try {
|
||||||
|
// Handle both array and JSON string formats
|
||||||
|
let urls;
|
||||||
|
if (Array.isArray(settings.appriseUrls)) {
|
||||||
|
urls = settings.appriseUrls;
|
||||||
|
} else if (typeof settings.appriseUrls === 'string') {
|
||||||
|
urls = JSON.parse(settings.appriseUrls);
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Apprise URLs must be an array or JSON string' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(urls)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Apprise URLs must be an array' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic URL validation
|
||||||
|
for (const url of urls) {
|
||||||
|
if (typeof url !== 'string' || url.trim() === '') {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'All Apprise URLs must be non-empty strings' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Invalid JSON format for Apprise URLs' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path to the .env file
|
||||||
|
const envPath = path.join(process.cwd(), '.env');
|
||||||
|
|
||||||
|
// Read existing .env file
|
||||||
|
let envContent = '';
|
||||||
|
if (fs.existsSync(envPath)) {
|
||||||
|
envContent = fs.readFileSync(envPath, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-sync settings to add/update
|
||||||
|
const autoSyncSettings = {
|
||||||
|
'AUTO_SYNC_ENABLED': settings.autoSyncEnabled ? 'true' : 'false',
|
||||||
|
'SYNC_INTERVAL_TYPE': settings.syncIntervalType,
|
||||||
|
'SYNC_INTERVAL_PREDEFINED': settings.syncIntervalPredefined || '',
|
||||||
|
'SYNC_INTERVAL_CRON': settings.syncIntervalCron || '',
|
||||||
|
'AUTO_DOWNLOAD_NEW': settings.autoDownloadNew ? 'true' : 'false',
|
||||||
|
'AUTO_UPDATE_EXISTING': settings.autoUpdateExisting ? 'true' : 'false',
|
||||||
|
'NOTIFICATION_ENABLED': settings.notificationEnabled ? 'true' : 'false',
|
||||||
|
'APPRISE_URLS': Array.isArray(settings.appriseUrls) ? JSON.stringify(settings.appriseUrls) : (settings.appriseUrls || '[]'),
|
||||||
|
'LAST_AUTO_SYNC': settings.lastAutoSync || '',
|
||||||
|
'LAST_AUTO_SYNC_ERROR': settings.lastAutoSyncError || '',
|
||||||
|
'LAST_AUTO_SYNC_ERROR_TIME': settings.lastAutoSyncErrorTime || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update or add each setting
|
||||||
|
for (const [key, value] of Object.entries(autoSyncSettings)) {
|
||||||
|
const regex = new RegExp(`^${key}=.*$`, 'm');
|
||||||
|
const settingLine = `${key}="${value}"`;
|
||||||
|
|
||||||
|
if (regex.test(envContent)) {
|
||||||
|
// Replace existing setting
|
||||||
|
envContent = envContent.replace(regex, settingLine);
|
||||||
|
} else {
|
||||||
|
// Add new setting
|
||||||
|
envContent += (envContent.endsWith('\n') ? '' : '\n') + `${settingLine}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write back to .env file
|
||||||
|
fs.writeFileSync(envPath, envContent);
|
||||||
|
|
||||||
|
// Reschedule auto-sync service with new settings
|
||||||
|
try {
|
||||||
|
const { getAutoSyncService, setAutoSyncService } = await import('../../../../server/lib/autoSyncInit.js');
|
||||||
|
let autoSyncService = getAutoSyncService();
|
||||||
|
|
||||||
|
// If no global instance exists, create one
|
||||||
|
if (!autoSyncService) {
|
||||||
|
const { AutoSyncService } = await import('../../../../server/services/autoSyncService.js');
|
||||||
|
autoSyncService = new AutoSyncService();
|
||||||
|
setAutoSyncService(autoSyncService);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the global service instance with new settings
|
||||||
|
autoSyncService.saveSettings(settings);
|
||||||
|
|
||||||
|
if (settings.autoSyncEnabled) {
|
||||||
|
autoSyncService.scheduleAutoSync();
|
||||||
|
} else {
|
||||||
|
autoSyncService.stopAutoSync();
|
||||||
|
// Ensure the service is completely stopped and won't restart
|
||||||
|
autoSyncService.isRunning = false;
|
||||||
|
// Also stop the global service instance if it exists
|
||||||
|
const { stopAutoSync: stopGlobalAutoSync } = await import('../../../../server/lib/autoSyncInit.js');
|
||||||
|
stopGlobalAutoSync();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error rescheduling auto-sync service:', error);
|
||||||
|
// Don't fail the request if rescheduling fails
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Auto-sync settings saved successfully'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving auto-sync settings:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to save auto-sync settings' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
// Path to the .env file
|
||||||
|
const envPath = path.join(process.cwd(), '.env');
|
||||||
|
|
||||||
|
if (!fs.existsSync(envPath)) {
|
||||||
|
return NextResponse.json({
|
||||||
|
settings: {
|
||||||
|
autoSyncEnabled: false,
|
||||||
|
syncIntervalType: 'predefined',
|
||||||
|
syncIntervalPredefined: '1hour',
|
||||||
|
syncIntervalCron: '',
|
||||||
|
autoDownloadNew: false,
|
||||||
|
autoUpdateExisting: false,
|
||||||
|
notificationEnabled: false,
|
||||||
|
appriseUrls: [],
|
||||||
|
lastAutoSync: '',
|
||||||
|
lastAutoSyncError: null,
|
||||||
|
lastAutoSyncErrorTime: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read .env file and extract auto-sync settings
|
||||||
|
const envContent = fs.readFileSync(envPath, 'utf8');
|
||||||
|
|
||||||
|
const settings = {
|
||||||
|
autoSyncEnabled: getEnvValue(envContent, 'AUTO_SYNC_ENABLED') === 'true',
|
||||||
|
syncIntervalType: getEnvValue(envContent, 'SYNC_INTERVAL_TYPE') || 'predefined',
|
||||||
|
syncIntervalPredefined: getEnvValue(envContent, 'SYNC_INTERVAL_PREDEFINED') || '1hour',
|
||||||
|
syncIntervalCron: getEnvValue(envContent, 'SYNC_INTERVAL_CRON') || '',
|
||||||
|
autoDownloadNew: getEnvValue(envContent, 'AUTO_DOWNLOAD_NEW') === 'true',
|
||||||
|
autoUpdateExisting: getEnvValue(envContent, 'AUTO_UPDATE_EXISTING') === 'true',
|
||||||
|
notificationEnabled: getEnvValue(envContent, 'NOTIFICATION_ENABLED') === 'true',
|
||||||
|
appriseUrls: (() => {
|
||||||
|
try {
|
||||||
|
const urlsValue = getEnvValue(envContent, 'APPRISE_URLS') || '[]';
|
||||||
|
return JSON.parse(urlsValue);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
lastAutoSync: getEnvValue(envContent, 'LAST_AUTO_SYNC') || '',
|
||||||
|
lastAutoSyncError: getEnvValue(envContent, 'LAST_AUTO_SYNC_ERROR') || null,
|
||||||
|
lastAutoSyncErrorTime: getEnvValue(envContent, 'LAST_AUTO_SYNC_ERROR_TIME') || null
|
||||||
|
};
|
||||||
|
|
||||||
|
return NextResponse.json({ settings });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading auto-sync settings:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to read auto-sync settings' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to handle test notification
|
||||||
|
async function handleTestNotification() {
|
||||||
|
try {
|
||||||
|
// Load current settings
|
||||||
|
const envPath = path.join(process.cwd(), '.env');
|
||||||
|
|
||||||
|
if (!fs.existsSync(envPath)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'No auto-sync settings found' },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const envContent = fs.readFileSync(envPath, 'utf8');
|
||||||
|
const notificationEnabled = getEnvValue(envContent, 'NOTIFICATION_ENABLED') === 'true';
|
||||||
|
const appriseUrls = (() => {
|
||||||
|
try {
|
||||||
|
const urlsValue = getEnvValue(envContent, 'APPRISE_URLS') || '[]';
|
||||||
|
return JSON.parse(urlsValue);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (!notificationEnabled) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Notifications are not enabled' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!appriseUrls || appriseUrls.length === 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'No Apprise URLs configured' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send test notification using the auto-sync service
|
||||||
|
const { AutoSyncService } = await import('../../../../server/services/autoSyncService.js');
|
||||||
|
const autoSyncService = new AutoSyncService();
|
||||||
|
const result = await autoSyncService.testNotification();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Test notification sent successfully'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: result.message },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending test notification:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to send test notification' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to handle manual sync trigger
|
||||||
|
async function handleManualSync() {
|
||||||
|
try {
|
||||||
|
// Load current settings
|
||||||
|
const envPath = path.join(process.cwd(), '.env');
|
||||||
|
|
||||||
|
if (!fs.existsSync(envPath)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'No auto-sync settings found' },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const envContent = fs.readFileSync(envPath, 'utf8');
|
||||||
|
const autoSyncEnabled = getEnvValue(envContent, 'AUTO_SYNC_ENABLED') === 'true';
|
||||||
|
|
||||||
|
if (!autoSyncEnabled) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Auto-sync is not enabled' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger manual sync using the auto-sync service
|
||||||
|
const { AutoSyncService } = await import('../../../../server/services/autoSyncService.js');
|
||||||
|
const autoSyncService = new AutoSyncService();
|
||||||
|
const result = await autoSyncService.executeAutoSync() as any;
|
||||||
|
|
||||||
|
if (result && result.success) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Manual sync completed successfully',
|
||||||
|
result
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: result.message },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error triggering manual sync:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to trigger manual sync' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to extract value from .env content
|
||||||
|
function getEnvValue(envContent: string, key: string): string {
|
||||||
|
// Try to match the pattern with quotes around the value (handles nested quotes)
|
||||||
|
const regex = new RegExp(`^${key}="(.+)"$`, 'm');
|
||||||
|
let match = regex.exec(envContent);
|
||||||
|
|
||||||
|
if (match && match[1]) {
|
||||||
|
let value = match[1];
|
||||||
|
// Remove extra quotes that might be around JSON values
|
||||||
|
if (value.startsWith('"') && value.endsWith('"')) {
|
||||||
|
value = value.slice(1, -1);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to match without quotes (fallback)
|
||||||
|
const regexNoQuotes = new RegExp(`^${key}=([^\\s]*)$`, 'm');
|
||||||
|
match = regexNoQuotes.exec(envContent);
|
||||||
|
if (match && match[1]) {
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { scriptManager } from "~/server/lib/scripts";
|
|||||||
import { githubJsonService } from "~/server/services/githubJsonService";
|
import { githubJsonService } from "~/server/services/githubJsonService";
|
||||||
import { localScriptsService } from "~/server/services/localScripts";
|
import { localScriptsService } from "~/server/services/localScripts";
|
||||||
import { scriptDownloaderService } from "~/server/services/scriptDownloader";
|
import { scriptDownloaderService } from "~/server/services/scriptDownloader";
|
||||||
|
import { AutoSyncService } from "~/server/services/autoSyncService";
|
||||||
import type { ScriptCard } from "~/types/script";
|
import type { ScriptCard } from "~/types/script";
|
||||||
|
|
||||||
export const scriptsRouter = createTRPCRouter({
|
export const scriptsRouter = createTRPCRouter({
|
||||||
@@ -114,6 +115,18 @@ export const scriptsRouter = createTRPCRouter({
|
|||||||
.input(z.object({ slug: z.string() }))
|
.input(z.object({ slug: z.string() }))
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
|
console.log('getScriptBySlug called with slug:', input.slug);
|
||||||
|
console.log('githubJsonService methods:', Object.getOwnPropertyNames(Object.getPrototypeOf(githubJsonService)));
|
||||||
|
console.log('githubJsonService.getScriptBySlug type:', typeof githubJsonService.getScriptBySlug);
|
||||||
|
|
||||||
|
if (typeof githubJsonService.getScriptBySlug !== 'function') {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'getScriptBySlug method is not available on githubJsonService',
|
||||||
|
script: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const script = await githubJsonService.getScriptBySlug(input.slug);
|
const script = await githubJsonService.getScriptBySlug(input.slug);
|
||||||
if (!script) {
|
if (!script) {
|
||||||
return {
|
return {
|
||||||
@@ -124,6 +137,7 @@ export const scriptsRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
return { success: true, script };
|
return { success: true, script };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error in getScriptBySlug:', error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : 'Failed to fetch script',
|
error: error instanceof Error ? error.message : 'Failed to fetch script',
|
||||||
@@ -457,5 +471,121 @@ export const scriptsRouter = createTRPCRouter({
|
|||||||
message: 'Failed to check Proxmox VE status'
|
message: 'Failed to check Proxmox VE status'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Auto-sync settings and operations
|
||||||
|
getAutoSyncSettings: publicProcedure
|
||||||
|
.query(async () => {
|
||||||
|
try {
|
||||||
|
const autoSyncService = new AutoSyncService();
|
||||||
|
const settings = autoSyncService.loadSettings();
|
||||||
|
return { success: true, settings };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting auto-sync settings:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to get auto-sync settings',
|
||||||
|
settings: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
saveAutoSyncSettings: publicProcedure
|
||||||
|
.input(z.object({
|
||||||
|
autoSyncEnabled: z.boolean(),
|
||||||
|
syncIntervalType: z.enum(['predefined', 'custom']),
|
||||||
|
syncIntervalPredefined: z.string().optional(),
|
||||||
|
syncIntervalCron: z.string().optional(),
|
||||||
|
autoDownloadNew: z.boolean(),
|
||||||
|
autoUpdateExisting: z.boolean(),
|
||||||
|
notificationEnabled: z.boolean(),
|
||||||
|
appriseUrls: z.array(z.string()).optional()
|
||||||
|
}))
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
// Use the global auto-sync service instance
|
||||||
|
const { getAutoSyncService, setAutoSyncService } = await import('~/server/lib/autoSyncInit');
|
||||||
|
let autoSyncService = getAutoSyncService();
|
||||||
|
|
||||||
|
// If no global instance exists, create one
|
||||||
|
if (!autoSyncService) {
|
||||||
|
const { AutoSyncService } = await import('~/server/services/autoSyncService');
|
||||||
|
autoSyncService = new AutoSyncService();
|
||||||
|
setAutoSyncService(autoSyncService);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save settings to both .env file and service instance
|
||||||
|
autoSyncService.saveSettings(input);
|
||||||
|
|
||||||
|
// Reschedule auto-sync if enabled
|
||||||
|
if (input.autoSyncEnabled) {
|
||||||
|
autoSyncService.scheduleAutoSync();
|
||||||
|
console.log('Auto-sync rescheduled with new settings');
|
||||||
|
} else {
|
||||||
|
autoSyncService.stopAutoSync();
|
||||||
|
// Ensure the service is completely stopped and won't restart
|
||||||
|
autoSyncService.isRunning = false;
|
||||||
|
console.log('Auto-sync stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, message: 'Auto-sync settings saved successfully' };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving auto-sync settings:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to save auto-sync settings'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
testNotification: publicProcedure
|
||||||
|
.mutation(async () => {
|
||||||
|
try {
|
||||||
|
const autoSyncService = new AutoSyncService();
|
||||||
|
const result = await autoSyncService.testNotification();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error testing notification:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof Error ? error.message : 'Failed to test notification'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
triggerManualAutoSync: publicProcedure
|
||||||
|
.mutation(async () => {
|
||||||
|
try {
|
||||||
|
const autoSyncService = new AutoSyncService();
|
||||||
|
const result = await autoSyncService.executeAutoSync();
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Manual auto-sync completed successfully',
|
||||||
|
result
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in manual auto-sync:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to execute manual auto-sync',
|
||||||
|
result: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
getAutoSyncStatus: publicProcedure
|
||||||
|
.query(async () => {
|
||||||
|
try {
|
||||||
|
const autoSyncService = new AutoSyncService();
|
||||||
|
const status = autoSyncService.getStatus();
|
||||||
|
return { success: true, status };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting auto-sync status:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to get auto-sync status',
|
||||||
|
status: null
|
||||||
|
};
|
||||||
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
84
src/server/lib/autoSyncInit.js
Normal file
84
src/server/lib/autoSyncInit.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { AutoSyncService } from '../services/autoSyncService.js';
|
||||||
|
|
||||||
|
let autoSyncService = null;
|
||||||
|
let isInitialized = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize auto-sync service and schedule cron job if enabled
|
||||||
|
*/
|
||||||
|
export function initializeAutoSync() {
|
||||||
|
if (isInitialized) {
|
||||||
|
console.log('Auto-sync service already initialized, skipping...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Initializing auto-sync service...');
|
||||||
|
autoSyncService = new AutoSyncService();
|
||||||
|
isInitialized = true;
|
||||||
|
console.log('AutoSyncService instance created');
|
||||||
|
|
||||||
|
// Load settings and schedule if enabled
|
||||||
|
const settings = autoSyncService.loadSettings();
|
||||||
|
console.log('Settings loaded:', settings);
|
||||||
|
|
||||||
|
if (settings.autoSyncEnabled) {
|
||||||
|
console.log('Auto-sync is enabled, scheduling cron job...');
|
||||||
|
autoSyncService.scheduleAutoSync();
|
||||||
|
console.log('Cron job scheduled');
|
||||||
|
} else {
|
||||||
|
console.log('Auto-sync is disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Auto-sync service initialized successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize auto-sync service:', error);
|
||||||
|
console.error('Error stack:', error.stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop auto-sync service and clean up cron jobs
|
||||||
|
*/
|
||||||
|
export function stopAutoSync() {
|
||||||
|
try {
|
||||||
|
if (autoSyncService) {
|
||||||
|
console.log('Stopping auto-sync service...');
|
||||||
|
autoSyncService.stopAutoSync();
|
||||||
|
autoSyncService = null;
|
||||||
|
isInitialized = false;
|
||||||
|
console.log('Auto-sync service stopped');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error stopping auto-sync service:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the auto-sync service instance
|
||||||
|
*/
|
||||||
|
export function getAutoSyncService() {
|
||||||
|
return autoSyncService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the auto-sync service instance (for external management)
|
||||||
|
*/
|
||||||
|
export function setAutoSyncService(service) {
|
||||||
|
autoSyncService = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Graceful shutdown handler
|
||||||
|
*/
|
||||||
|
export function setupGracefulShutdown() {
|
||||||
|
const shutdown = (signal) => {
|
||||||
|
console.log(`Received ${signal}, shutting down gracefully...`);
|
||||||
|
stopAutoSync();
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||||
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||||
|
process.on('SIGUSR2', () => shutdown('SIGUSR2')); // For nodemon
|
||||||
|
}
|
||||||
72
src/server/lib/autoSyncInit.ts
Normal file
72
src/server/lib/autoSyncInit.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { AutoSyncService } from '~/server/services/autoSyncService';
|
||||||
|
|
||||||
|
let autoSyncService: AutoSyncService | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize auto-sync service and schedule cron job if enabled
|
||||||
|
*/
|
||||||
|
export function initializeAutoSync(): void {
|
||||||
|
try {
|
||||||
|
console.log('Initializing auto-sync service...');
|
||||||
|
autoSyncService = new AutoSyncService();
|
||||||
|
|
||||||
|
// Load settings and schedule if enabled
|
||||||
|
const settings = autoSyncService.loadSettings();
|
||||||
|
|
||||||
|
if (settings.autoSyncEnabled) {
|
||||||
|
console.log('Auto-sync is enabled, scheduling cron job...');
|
||||||
|
autoSyncService.scheduleAutoSync();
|
||||||
|
} else {
|
||||||
|
console.log('Auto-sync is disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Auto-sync service initialized successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize auto-sync service:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop auto-sync service and clean up cron jobs
|
||||||
|
*/
|
||||||
|
export function stopAutoSync(): void {
|
||||||
|
try {
|
||||||
|
if (autoSyncService) {
|
||||||
|
console.log('Stopping auto-sync service...');
|
||||||
|
autoSyncService.stopAutoSync();
|
||||||
|
autoSyncService = null;
|
||||||
|
console.log('Auto-sync service stopped');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error stopping auto-sync service:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the auto-sync service instance
|
||||||
|
*/
|
||||||
|
export function getAutoSyncService(): AutoSyncService | null {
|
||||||
|
return autoSyncService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the auto-sync service instance (for external management)
|
||||||
|
*/
|
||||||
|
export function setAutoSyncService(service: AutoSyncService | null): void {
|
||||||
|
autoSyncService = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Graceful shutdown handler
|
||||||
|
*/
|
||||||
|
export function setupGracefulShutdown(): void {
|
||||||
|
const shutdown = (signal: string) => {
|
||||||
|
console.log(`Received ${signal}, shutting down gracefully...`);
|
||||||
|
stopAutoSync();
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||||
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||||
|
process.on('SIGUSR2', () => shutdown('SIGUSR2')); // For nodemon
|
||||||
|
}
|
||||||
@@ -25,6 +25,25 @@ export class ScriptManager {
|
|||||||
// Initialize lazily to avoid accessing env vars during module load
|
// Initialize lazily to avoid accessing env vars during module load
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely handle file modification time, providing fallback for invalid dates
|
||||||
|
* @param mtime - The file modification time from fs.stat
|
||||||
|
* @returns Date - Valid date or current date as fallback
|
||||||
|
*/
|
||||||
|
private safeMtime(mtime: Date): Date {
|
||||||
|
try {
|
||||||
|
// Check if the date is valid
|
||||||
|
if (!mtime || isNaN(mtime.getTime())) {
|
||||||
|
console.warn('Invalid mtime detected, using current time as fallback');
|
||||||
|
return new Date();
|
||||||
|
}
|
||||||
|
return mtime;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error processing mtime:', error);
|
||||||
|
return new Date();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private initializeConfig() {
|
private initializeConfig() {
|
||||||
if (this.scriptsDir === null) {
|
if (this.scriptsDir === null) {
|
||||||
// Handle both absolute and relative paths for testing
|
// Handle both absolute and relative paths for testing
|
||||||
@@ -63,7 +82,7 @@ export class ScriptManager {
|
|||||||
path: filePath,
|
path: filePath,
|
||||||
extension,
|
extension,
|
||||||
size: stats.size,
|
size: stats.size,
|
||||||
lastModified: stats.mtime,
|
lastModified: this.safeMtime(stats.mtime),
|
||||||
executable
|
executable
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -125,7 +144,7 @@ export class ScriptManager {
|
|||||||
path: filePath,
|
path: filePath,
|
||||||
extension,
|
extension,
|
||||||
size: stats.size,
|
size: stats.size,
|
||||||
lastModified: stats.mtime,
|
lastModified: this.safeMtime(stats.mtime),
|
||||||
executable,
|
executable,
|
||||||
logo,
|
logo,
|
||||||
slug
|
slug
|
||||||
@@ -212,7 +231,7 @@ export class ScriptManager {
|
|||||||
path: filePath,
|
path: filePath,
|
||||||
extension,
|
extension,
|
||||||
size: stats.size,
|
size: stats.size,
|
||||||
lastModified: stats.mtime,
|
lastModified: this.safeMtime(stats.mtime),
|
||||||
executable,
|
executable,
|
||||||
logo,
|
logo,
|
||||||
slug
|
slug
|
||||||
|
|||||||
123
src/server/services/appriseService.js
Normal file
123
src/server/services/appriseService.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export class AppriseService {
|
||||||
|
constructor() {
|
||||||
|
this.baseUrl = 'http://localhost:8080'; // Default Apprise API URL
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send notification via Apprise
|
||||||
|
* @param {string} title - Notification title
|
||||||
|
* @param {string} body - Notification body
|
||||||
|
* @param {string[]} urls - Array of Apprise URLs
|
||||||
|
*/
|
||||||
|
async sendNotification(title, body, urls) {
|
||||||
|
if (!urls || urls.length === 0) {
|
||||||
|
throw new Error('No Apprise URLs provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Format the notification as form data (Apprise API expects form data)
|
||||||
|
const formData = new URLSearchParams();
|
||||||
|
formData.append('body', body || '');
|
||||||
|
formData.append('title', title || 'PVE Scripts Local');
|
||||||
|
formData.append('tags', 'all');
|
||||||
|
|
||||||
|
// Send to each URL
|
||||||
|
const results = [];
|
||||||
|
for (const url of urls) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(url, formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
timeout: 10000 // 10 second timeout
|
||||||
|
});
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
url,
|
||||||
|
success: true,
|
||||||
|
status: response.status
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error(`Failed to send notification to ${url}:`, errorMessage);
|
||||||
|
results.push({
|
||||||
|
url,
|
||||||
|
success: false,
|
||||||
|
error: errorMessage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any notifications succeeded
|
||||||
|
const successCount = results.filter(r => r.success).length;
|
||||||
|
if (successCount === 0) {
|
||||||
|
throw new Error('All notification attempts failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Notification sent to ${successCount}/${urls.length} services`,
|
||||||
|
results
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Apprise notification failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test notification to a single URL
|
||||||
|
* @param {string} url - Apprise URL to test
|
||||||
|
*/
|
||||||
|
async testUrl(url) {
|
||||||
|
try {
|
||||||
|
await this.sendNotification('Test', 'This is a test notification', [url]);
|
||||||
|
return { success: true, message: 'Test notification sent successfully' };
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
return { success: false, message: errorMessage };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate Apprise URL format
|
||||||
|
* @param {string} url - URL to validate
|
||||||
|
*/
|
||||||
|
validateUrl(url) {
|
||||||
|
if (!url || typeof url !== 'string') {
|
||||||
|
return { valid: false, error: 'URL is required' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic URL validation
|
||||||
|
try {
|
||||||
|
new URL(url);
|
||||||
|
} catch {
|
||||||
|
return { valid: false, error: 'Invalid URL format' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for common Apprise URL patterns
|
||||||
|
const apprisePatterns = [
|
||||||
|
/^discord:\/\//,
|
||||||
|
/^tgram:\/\//,
|
||||||
|
/^mailto:\/\//,
|
||||||
|
/^slack:\/\//,
|
||||||
|
/^https?:\/\//
|
||||||
|
];
|
||||||
|
|
||||||
|
const isValidAppriseUrl = apprisePatterns.some(pattern => pattern.test(url));
|
||||||
|
|
||||||
|
if (!isValidAppriseUrl) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: 'URL does not match known Apprise service patterns'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const appriseService = new AppriseService();
|
||||||
758
src/server/services/autoSyncService.js
Normal file
758
src/server/services/autoSyncService.js
Normal file
@@ -0,0 +1,758 @@
|
|||||||
|
import cron from 'node-cron';
|
||||||
|
import { githubJsonService } from './githubJsonService.js';
|
||||||
|
import { scriptDownloaderService } from './scriptDownloader.js';
|
||||||
|
import { appriseService } from './appriseService.js';
|
||||||
|
import { readFile, writeFile, readFileSync, writeFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import cronValidator from 'cron-validator';
|
||||||
|
|
||||||
|
// Global lock to prevent multiple autosync instances from running simultaneously
|
||||||
|
let globalAutoSyncLock = false;
|
||||||
|
|
||||||
|
export class AutoSyncService {
|
||||||
|
constructor() {
|
||||||
|
this.cronJob = null;
|
||||||
|
this.isRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely convert a date to ISO string, handling invalid dates
|
||||||
|
* @param {Date} date - The date to convert
|
||||||
|
* @returns {string} - ISO string or fallback timestamp
|
||||||
|
*/
|
||||||
|
safeToISOString(date) {
|
||||||
|
try {
|
||||||
|
// Check if the date is valid
|
||||||
|
if (!date || isNaN(date.getTime())) {
|
||||||
|
console.warn('Invalid date provided to safeToISOString, using current time as fallback');
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
return date.toISOString();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error converting date to ISO string:', error instanceof Error ? error.message : String(error));
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load auto-sync settings from .env file
|
||||||
|
*/
|
||||||
|
loadSettings() {
|
||||||
|
try {
|
||||||
|
const envPath = join(process.cwd(), '.env');
|
||||||
|
const envContent = readFileSync(envPath, 'utf8');
|
||||||
|
|
||||||
|
/** @type {{
|
||||||
|
* autoSyncEnabled: boolean;
|
||||||
|
* syncIntervalType: string;
|
||||||
|
* syncIntervalPredefined?: string;
|
||||||
|
* syncIntervalCron?: string;
|
||||||
|
* autoDownloadNew: boolean;
|
||||||
|
* autoUpdateExisting: boolean;
|
||||||
|
* notificationEnabled: boolean;
|
||||||
|
* appriseUrls?: string[];
|
||||||
|
* lastAutoSync?: string;
|
||||||
|
* lastAutoSyncError?: string;
|
||||||
|
* lastAutoSyncErrorTime?: string;
|
||||||
|
* }} */
|
||||||
|
const settings = {
|
||||||
|
autoSyncEnabled: false,
|
||||||
|
syncIntervalType: 'predefined',
|
||||||
|
syncIntervalPredefined: '1hour',
|
||||||
|
syncIntervalCron: '',
|
||||||
|
autoDownloadNew: false,
|
||||||
|
autoUpdateExisting: false,
|
||||||
|
notificationEnabled: false,
|
||||||
|
appriseUrls: [],
|
||||||
|
lastAutoSync: '',
|
||||||
|
lastAutoSyncError: '',
|
||||||
|
lastAutoSyncErrorTime: ''
|
||||||
|
};
|
||||||
|
const lines = envContent.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const [key, ...valueParts] = line.split('=');
|
||||||
|
if (key && valueParts.length > 0) {
|
||||||
|
let value = valueParts.join('=').trim();
|
||||||
|
// Remove surrounding quotes if present
|
||||||
|
if (value.startsWith('"') && value.endsWith('"')) {
|
||||||
|
value = value.slice(1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (key.trim()) {
|
||||||
|
case 'AUTO_SYNC_ENABLED':
|
||||||
|
settings.autoSyncEnabled = value === 'true';
|
||||||
|
break;
|
||||||
|
case 'SYNC_INTERVAL_TYPE':
|
||||||
|
settings.syncIntervalType = value;
|
||||||
|
break;
|
||||||
|
case 'SYNC_INTERVAL_PREDEFINED':
|
||||||
|
settings.syncIntervalPredefined = value;
|
||||||
|
break;
|
||||||
|
case 'SYNC_INTERVAL_CRON':
|
||||||
|
settings.syncIntervalCron = value;
|
||||||
|
break;
|
||||||
|
case 'AUTO_DOWNLOAD_NEW':
|
||||||
|
settings.autoDownloadNew = value === 'true';
|
||||||
|
break;
|
||||||
|
case 'AUTO_UPDATE_EXISTING':
|
||||||
|
settings.autoUpdateExisting = value === 'true';
|
||||||
|
break;
|
||||||
|
case 'NOTIFICATION_ENABLED':
|
||||||
|
settings.notificationEnabled = value === 'true';
|
||||||
|
break;
|
||||||
|
case 'APPRISE_URLS':
|
||||||
|
try {
|
||||||
|
settings.appriseUrls = JSON.parse(value || '[]');
|
||||||
|
} catch {
|
||||||
|
settings.appriseUrls = [];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'LAST_AUTO_SYNC':
|
||||||
|
settings.lastAutoSync = value;
|
||||||
|
break;
|
||||||
|
case 'LAST_AUTO_SYNC_ERROR':
|
||||||
|
settings.lastAutoSyncError = value;
|
||||||
|
break;
|
||||||
|
case 'LAST_AUTO_SYNC_ERROR_TIME':
|
||||||
|
settings.lastAutoSyncErrorTime = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading auto-sync settings:', error);
|
||||||
|
return {
|
||||||
|
autoSyncEnabled: false,
|
||||||
|
syncIntervalType: 'predefined',
|
||||||
|
syncIntervalPredefined: '1hour',
|
||||||
|
syncIntervalCron: '',
|
||||||
|
autoDownloadNew: false,
|
||||||
|
autoUpdateExisting: false,
|
||||||
|
notificationEnabled: false,
|
||||||
|
appriseUrls: [],
|
||||||
|
lastAutoSync: '',
|
||||||
|
lastAutoSyncError: '',
|
||||||
|
lastAutoSyncErrorTime: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save auto-sync settings to .env file
|
||||||
|
* @param {Object} settings - Settings object
|
||||||
|
* @param {boolean} settings.autoSyncEnabled
|
||||||
|
* @param {string} settings.syncIntervalType
|
||||||
|
* @param {string} [settings.syncIntervalPredefined]
|
||||||
|
* @param {string} [settings.syncIntervalCron]
|
||||||
|
* @param {boolean} settings.autoDownloadNew
|
||||||
|
* @param {boolean} settings.autoUpdateExisting
|
||||||
|
* @param {boolean} settings.notificationEnabled
|
||||||
|
* @param {Array<string>} [settings.appriseUrls]
|
||||||
|
* @param {string} [settings.lastAutoSync]
|
||||||
|
* @param {string} [settings.lastAutoSyncError]
|
||||||
|
* @param {string} [settings.lastAutoSyncErrorTime]
|
||||||
|
*/
|
||||||
|
saveSettings(settings) {
|
||||||
|
try {
|
||||||
|
const envPath = join(process.cwd(), '.env');
|
||||||
|
let envContent = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
envContent = readFileSync(envPath, 'utf8');
|
||||||
|
} catch {
|
||||||
|
// .env file doesn't exist, create it
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = envContent.split('\n');
|
||||||
|
const newLines = [];
|
||||||
|
const settingsMap = {
|
||||||
|
'AUTO_SYNC_ENABLED': settings.autoSyncEnabled.toString(),
|
||||||
|
'SYNC_INTERVAL_TYPE': settings.syncIntervalType,
|
||||||
|
'SYNC_INTERVAL_PREDEFINED': settings.syncIntervalPredefined || '',
|
||||||
|
'SYNC_INTERVAL_CRON': settings.syncIntervalCron || '',
|
||||||
|
'AUTO_DOWNLOAD_NEW': settings.autoDownloadNew.toString(),
|
||||||
|
'AUTO_UPDATE_EXISTING': settings.autoUpdateExisting.toString(),
|
||||||
|
'NOTIFICATION_ENABLED': settings.notificationEnabled.toString(),
|
||||||
|
'APPRISE_URLS': JSON.stringify(settings.appriseUrls || []),
|
||||||
|
'LAST_AUTO_SYNC': settings.lastAutoSync || '',
|
||||||
|
'LAST_AUTO_SYNC_ERROR': settings.lastAutoSyncError || '',
|
||||||
|
'LAST_AUTO_SYNC_ERROR_TIME': settings.lastAutoSyncErrorTime || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const existingKeys = new Set();
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
|
||||||
|
// Skip empty lines and comments
|
||||||
|
if (!trimmedLine || trimmedLine.startsWith('#')) {
|
||||||
|
newLines.push(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const equalIndex = trimmedLine.indexOf('=');
|
||||||
|
if (equalIndex === -1) {
|
||||||
|
// Line doesn't contain '=', keep as is
|
||||||
|
newLines.push(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = trimmedLine.substring(0, equalIndex).trim();
|
||||||
|
if (key && key in settingsMap) {
|
||||||
|
// Replace existing setting
|
||||||
|
// @ts-ignore - Dynamic property access is safe here
|
||||||
|
newLines.push(`${key}=${settingsMap[key]}`);
|
||||||
|
existingKeys.add(key);
|
||||||
|
} else {
|
||||||
|
// Keep other settings as is
|
||||||
|
newLines.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any missing settings
|
||||||
|
for (const [key, value] of Object.entries(settingsMap)) {
|
||||||
|
if (!existingKeys.has(key)) {
|
||||||
|
newLines.push(`${key}=${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(envPath, newLines.join('\n'));
|
||||||
|
console.log('Auto-sync settings saved successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving auto-sync settings:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule auto-sync cron job
|
||||||
|
*/
|
||||||
|
scheduleAutoSync() {
|
||||||
|
this.stopAutoSync(); // Stop any existing job
|
||||||
|
|
||||||
|
const settings = this.loadSettings();
|
||||||
|
if (!settings.autoSyncEnabled) {
|
||||||
|
console.log('Auto-sync is disabled, not scheduling cron job');
|
||||||
|
this.isRunning = false; // Ensure we're completely stopped
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there's already a global autosync running
|
||||||
|
if (globalAutoSyncLock) {
|
||||||
|
console.log('Auto-sync is already running globally, not scheduling new cron job');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cronExpression;
|
||||||
|
|
||||||
|
if (settings.syncIntervalType === 'custom') {
|
||||||
|
cronExpression = settings.syncIntervalCron;
|
||||||
|
} else {
|
||||||
|
// Convert predefined intervals to cron expressions
|
||||||
|
const intervalMap = {
|
||||||
|
'15min': '*/15 * * * *',
|
||||||
|
'30min': '*/30 * * * *',
|
||||||
|
'1hour': '0 * * * *',
|
||||||
|
'6hours': '0 */6 * * *',
|
||||||
|
'12hours': '0 */12 * * *',
|
||||||
|
'24hours': '0 0 * * *'
|
||||||
|
};
|
||||||
|
// @ts-ignore - Dynamic key access is safe here
|
||||||
|
cronExpression = intervalMap[settings.syncIntervalPredefined] || '0 * * * *';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate cron expression (5-field format for node-cron)
|
||||||
|
if (!cronValidator.isValidCron(cronExpression, { seconds: false })) {
|
||||||
|
console.error('Invalid cron expression:', cronExpression);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Scheduling auto-sync with cron expression: ${cronExpression}`);
|
||||||
|
|
||||||
|
this.cronJob = cron.schedule(cronExpression, async () => {
|
||||||
|
// Check global lock first
|
||||||
|
if (globalAutoSyncLock) {
|
||||||
|
console.log('Auto-sync already running globally, skipping cron execution...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isRunning) {
|
||||||
|
console.log('Auto-sync already running locally, skipping...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Double-check that autosync is still enabled before executing
|
||||||
|
const currentSettings = this.loadSettings();
|
||||||
|
if (!currentSettings.autoSyncEnabled) {
|
||||||
|
console.log('Auto-sync has been disabled, stopping and destroying cron job');
|
||||||
|
this.stopAutoSync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional check: if cronJob is null, it means it was stopped
|
||||||
|
if (!this.cronJob) {
|
||||||
|
console.log('Cron job was stopped, skipping execution');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Starting scheduled auto-sync...');
|
||||||
|
await this.executeAutoSync();
|
||||||
|
}, {
|
||||||
|
scheduled: true,
|
||||||
|
timezone: 'UTC'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Auto-sync cron job scheduled successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop auto-sync cron job
|
||||||
|
*/
|
||||||
|
stopAutoSync() {
|
||||||
|
if (this.cronJob) {
|
||||||
|
this.cronJob.stop();
|
||||||
|
this.cronJob.destroy();
|
||||||
|
this.cronJob = null;
|
||||||
|
this.isRunning = false;
|
||||||
|
console.log('Auto-sync cron job stopped and destroyed');
|
||||||
|
} else {
|
||||||
|
console.log('No active cron job to stop');
|
||||||
|
this.isRunning = false; // Ensure isRunning is false even if no cron job
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute auto-sync process
|
||||||
|
*/
|
||||||
|
async executeAutoSync() {
|
||||||
|
// Check global lock first
|
||||||
|
if (globalAutoSyncLock) {
|
||||||
|
console.log('Auto-sync already running globally, skipping...');
|
||||||
|
return { success: false, message: 'Auto-sync already running globally' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isRunning) {
|
||||||
|
console.log('Auto-sync already running locally, skipping...');
|
||||||
|
return { success: false, message: 'Auto-sync already running locally' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set global lock
|
||||||
|
globalAutoSyncLock = true;
|
||||||
|
this.isRunning = true;
|
||||||
|
const startTime = new Date();
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Starting auto-sync execution...');
|
||||||
|
|
||||||
|
// Step 1: Sync JSON files
|
||||||
|
console.log('Syncing JSON files...');
|
||||||
|
const syncResult = await githubJsonService.syncJsonFiles();
|
||||||
|
|
||||||
|
if (!syncResult.success) {
|
||||||
|
throw new Error(`JSON sync failed: ${syncResult.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = {
|
||||||
|
jsonSync: syncResult,
|
||||||
|
newScripts: /** @type {any[]} */ ([]),
|
||||||
|
updatedScripts: /** @type {any[]} */ ([]),
|
||||||
|
errors: /** @type {string[]} */ ([])
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 2: Auto-download/update scripts if enabled
|
||||||
|
const settings = this.loadSettings();
|
||||||
|
|
||||||
|
if (settings.autoDownloadNew || settings.autoUpdateExisting) {
|
||||||
|
console.log('Processing synced JSON files for script downloads...');
|
||||||
|
|
||||||
|
// Only process scripts for files that were actually synced
|
||||||
|
if (syncResult.syncedFiles && syncResult.syncedFiles.length > 0) {
|
||||||
|
console.log(`Processing ${syncResult.syncedFiles.length} synced JSON files for script downloads...`);
|
||||||
|
|
||||||
|
// Get scripts only for the synced files
|
||||||
|
const localScriptsService = await import('./localScripts.js');
|
||||||
|
const syncedScripts = [];
|
||||||
|
|
||||||
|
for (const filename of syncResult.syncedFiles) {
|
||||||
|
try {
|
||||||
|
// Extract slug from filename (remove .json extension)
|
||||||
|
const slug = filename.replace('.json', '');
|
||||||
|
const script = await localScriptsService.localScriptsService.getScriptBySlug(slug);
|
||||||
|
if (script) {
|
||||||
|
syncedScripts.push(script);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Error loading script from ${filename}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${syncedScripts.length} scripts from synced JSON files`);
|
||||||
|
|
||||||
|
// Filter to only truly NEW scripts (not previously downloaded)
|
||||||
|
const newScripts = [];
|
||||||
|
const existingScripts = [];
|
||||||
|
|
||||||
|
for (const script of syncedScripts) {
|
||||||
|
try {
|
||||||
|
// Validate script object
|
||||||
|
if (!script || !script.slug) {
|
||||||
|
console.warn('Invalid script object found, skipping:', script);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDownloaded = await scriptDownloaderService.isScriptDownloaded(script);
|
||||||
|
if (!isDownloaded) {
|
||||||
|
newScripts.push(script);
|
||||||
|
} else {
|
||||||
|
existingScripts.push(script);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Error checking script ${script?.slug || 'unknown'}:`, error);
|
||||||
|
// Treat as new script if we can't check
|
||||||
|
if (script && script.slug) {
|
||||||
|
newScripts.push(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${newScripts.length} new scripts and ${existingScripts.length} existing scripts from synced files`);
|
||||||
|
|
||||||
|
// Download new scripts
|
||||||
|
if (settings.autoDownloadNew && newScripts.length > 0) {
|
||||||
|
console.log(`Auto-downloading ${newScripts.length} new scripts...`);
|
||||||
|
const downloaded = [];
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
for (const script of newScripts) {
|
||||||
|
try {
|
||||||
|
const result = await scriptDownloaderService.loadScript(script);
|
||||||
|
if (result.success) {
|
||||||
|
downloaded.push(script); // Store full script object for category grouping
|
||||||
|
console.log(`Downloaded script: ${script.name || script.slug}`);
|
||||||
|
} else {
|
||||||
|
errors.push(`${script.name || script.slug}: ${result.message}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
|
errors.push(`${script.name || script.slug}: ${errorMsg}`);
|
||||||
|
console.error(`Failed to download script ${script.slug}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.newScripts = downloaded;
|
||||||
|
results.errors.push(...errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing scripts
|
||||||
|
if (settings.autoUpdateExisting && existingScripts.length > 0) {
|
||||||
|
console.log(`Auto-updating ${existingScripts.length} existing scripts...`);
|
||||||
|
const updated = [];
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
for (const script of existingScripts) {
|
||||||
|
try {
|
||||||
|
// Always update existing scripts when auto-update is enabled
|
||||||
|
const result = await scriptDownloaderService.loadScript(script);
|
||||||
|
if (result.success) {
|
||||||
|
updated.push(script); // Store full script object for category grouping
|
||||||
|
console.log(`Updated script: ${script.name || script.slug}`);
|
||||||
|
} else {
|
||||||
|
errors.push(`${script.name || script.slug}: ${result.message}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
|
errors.push(`${script.name || script.slug}: ${errorMsg}`);
|
||||||
|
console.error(`Failed to update script ${script.slug}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.updatedScripts = updated;
|
||||||
|
results.errors.push(...errors);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('No JSON files were synced, skipping script download/update');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Auto-download/update disabled, skipping script processing');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Send notifications if enabled
|
||||||
|
if (settings.notificationEnabled && settings.appriseUrls && settings.appriseUrls.length > 0) {
|
||||||
|
console.log('Sending success notifications...');
|
||||||
|
await this.sendSyncNotification(results);
|
||||||
|
console.log('Success notifications sent');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Update last sync time and clear any previous errors
|
||||||
|
const lastSyncTime = this.safeToISOString(new Date());
|
||||||
|
const updatedSettings = {
|
||||||
|
...settings,
|
||||||
|
lastAutoSync: lastSyncTime,
|
||||||
|
lastAutoSyncError: '' // Clear any previous errors on successful sync
|
||||||
|
};
|
||||||
|
this.saveSettings(updatedSettings);
|
||||||
|
|
||||||
|
const duration = new Date().getTime() - startTime.getTime();
|
||||||
|
console.log(`Auto-sync completed successfully in ${duration}ms`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Auto-sync completed successfully',
|
||||||
|
results,
|
||||||
|
duration
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Auto-sync execution failed:', error);
|
||||||
|
|
||||||
|
// Check if it's a rate limit error
|
||||||
|
const isRateLimitError = error instanceof Error && error.name === 'RateLimitError';
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
|
||||||
|
// Send error notification if enabled
|
||||||
|
const settings = this.loadSettings();
|
||||||
|
if (settings.notificationEnabled && settings.appriseUrls && settings.appriseUrls.length > 0) {
|
||||||
|
try {
|
||||||
|
const notificationTitle = isRateLimitError ? 'Auto-Sync Rate Limited' : 'Auto-Sync Failed';
|
||||||
|
const notificationMessage = isRateLimitError
|
||||||
|
? `GitHub API rate limit exceeded. Please set a GITHUB_TOKEN in your .env file for higher rate limits. Error: ${errorMessage}`
|
||||||
|
: `Auto-sync failed with error: ${errorMessage}`;
|
||||||
|
|
||||||
|
await appriseService.sendNotification(
|
||||||
|
notificationTitle,
|
||||||
|
notificationMessage,
|
||||||
|
settings.appriseUrls || []
|
||||||
|
);
|
||||||
|
} catch (notifError) {
|
||||||
|
console.error('Failed to send error notification:', notifError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the error in settings for UI display
|
||||||
|
const errorSettings = this.loadSettings();
|
||||||
|
const errorToStore = isRateLimitError
|
||||||
|
? `GitHub API rate limit exceeded. Please set a GITHUB_TOKEN in your .env file for higher rate limits.`
|
||||||
|
: errorMessage;
|
||||||
|
|
||||||
|
const updatedErrorSettings = {
|
||||||
|
...errorSettings,
|
||||||
|
lastAutoSyncError: errorToStore,
|
||||||
|
lastAutoSyncErrorTime: this.safeToISOString(new Date())
|
||||||
|
};
|
||||||
|
this.saveSettings(updatedErrorSettings);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: errorToStore,
|
||||||
|
error: errorMessage,
|
||||||
|
isRateLimitError
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
this.isRunning = false;
|
||||||
|
globalAutoSyncLock = false; // Release global lock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load categories from metadata.json
|
||||||
|
*/
|
||||||
|
loadCategories() {
|
||||||
|
try {
|
||||||
|
const metadataPath = join(process.cwd(), 'scripts', 'json', 'metadata.json');
|
||||||
|
const metadataContent = readFileSync(metadataPath, 'utf8');
|
||||||
|
const metadata = JSON.parse(metadataContent);
|
||||||
|
return metadata.categories || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading categories:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group scripts by category
|
||||||
|
* @param {Array<any>} scripts - Array of script objects
|
||||||
|
* @param {Array<any>} categories - Array of category objects
|
||||||
|
*/
|
||||||
|
groupScriptsByCategory(scripts, categories) {
|
||||||
|
const categoryMap = new Map();
|
||||||
|
categories.forEach(cat => categoryMap.set(cat.id, cat.name));
|
||||||
|
|
||||||
|
const grouped = new Map();
|
||||||
|
|
||||||
|
scripts.forEach(script => {
|
||||||
|
// Validate script object
|
||||||
|
if (!script || !script.name) {
|
||||||
|
console.warn('Invalid script object in groupScriptsByCategory, skipping:', script);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scriptCategories = script.categories || [0]; // Default to Miscellaneous (id: 0)
|
||||||
|
scriptCategories.forEach((/** @type {number} */ catId) => {
|
||||||
|
const categoryName = categoryMap.get(catId) || 'Miscellaneous';
|
||||||
|
if (!grouped.has(categoryName)) {
|
||||||
|
grouped.set(categoryName, []);
|
||||||
|
}
|
||||||
|
grouped.get(categoryName).push(script.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return grouped;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send notification about sync results
|
||||||
|
* @param {Object} results - Sync results object
|
||||||
|
*/
|
||||||
|
async sendSyncNotification(results) {
|
||||||
|
const settings = this.loadSettings();
|
||||||
|
|
||||||
|
if (!settings.notificationEnabled || !settings.appriseUrls?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = 'ProxmoxVE-Local - Auto-Sync Completed';
|
||||||
|
let body = `Auto-sync completed successfully.\n\n`;
|
||||||
|
|
||||||
|
// Add JSON sync info
|
||||||
|
// @ts-ignore - Dynamic property access
|
||||||
|
if (results.jsonSync) {
|
||||||
|
// @ts-ignore - Dynamic property access
|
||||||
|
const syncedCount = results.jsonSync.count || 0;
|
||||||
|
// @ts-ignore - Dynamic property access
|
||||||
|
const syncedFiles = results.jsonSync.syncedFiles || [];
|
||||||
|
|
||||||
|
// Calculate up-to-date count (total files - synced files)
|
||||||
|
// We can't easily get total file count from the sync result, so just show synced count
|
||||||
|
if (syncedCount > 0) {
|
||||||
|
body += `JSON Files: ${syncedCount} synced\n`;
|
||||||
|
} else {
|
||||||
|
body += `JSON Files: All up-to-date\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore - Dynamic property access
|
||||||
|
if (results.jsonSync.errors?.length > 0) {
|
||||||
|
// @ts-ignore - Dynamic property access
|
||||||
|
body += `JSON Errors: ${results.jsonSync.errors.length}\n`;
|
||||||
|
}
|
||||||
|
body += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load categories for grouping
|
||||||
|
const categories = this.loadCategories();
|
||||||
|
|
||||||
|
// @ts-ignore - Dynamic property access
|
||||||
|
if (results.newScripts?.length > 0) {
|
||||||
|
// @ts-ignore - Dynamic property access
|
||||||
|
body += `New scripts downloaded: ${results.newScripts.length}\n`;
|
||||||
|
|
||||||
|
// Group new scripts by category
|
||||||
|
// @ts-ignore - Dynamic property access
|
||||||
|
const newScriptsGrouped = this.groupScriptsByCategory(results.newScripts, categories);
|
||||||
|
|
||||||
|
// Sort categories by name for consistent ordering
|
||||||
|
const sortedCategories = Array.from(newScriptsGrouped.keys()).sort();
|
||||||
|
|
||||||
|
sortedCategories.forEach(categoryName => {
|
||||||
|
const scripts = newScriptsGrouped.get(categoryName);
|
||||||
|
body += `\n**${categoryName}:**\n`;
|
||||||
|
scripts.forEach((/** @type {string} */ scriptName) => {
|
||||||
|
body += `• ${scriptName}\n`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
body += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore - Dynamic property access
|
||||||
|
if (results.updatedScripts?.length > 0) {
|
||||||
|
// @ts-ignore - Dynamic property access
|
||||||
|
body += `Scripts updated: ${results.updatedScripts.length}\n`;
|
||||||
|
|
||||||
|
// Group updated scripts by category
|
||||||
|
// @ts-ignore - Dynamic property access
|
||||||
|
const updatedScriptsGrouped = this.groupScriptsByCategory(results.updatedScripts, categories);
|
||||||
|
|
||||||
|
// Sort categories by name for consistent ordering
|
||||||
|
const sortedCategories = Array.from(updatedScriptsGrouped.keys()).sort();
|
||||||
|
|
||||||
|
sortedCategories.forEach(categoryName => {
|
||||||
|
const scripts = updatedScriptsGrouped.get(categoryName);
|
||||||
|
body += `\n**${categoryName}:**\n`;
|
||||||
|
scripts.forEach((/** @type {string} */ scriptName) => {
|
||||||
|
body += `• ${scriptName}\n`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
body += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore - Dynamic property access
|
||||||
|
if (results.errors?.length > 0) {
|
||||||
|
// @ts-ignore - Dynamic property access
|
||||||
|
body += `Script errors encountered: ${results.errors.length}\n`;
|
||||||
|
// @ts-ignore - Dynamic property access
|
||||||
|
body += `• ${results.errors.slice(0, 5).join('\n• ')}\n`;
|
||||||
|
// @ts-ignore - Dynamic property access
|
||||||
|
if (results.errors.length > 5) {
|
||||||
|
// @ts-ignore - Dynamic property access
|
||||||
|
body += `• ... and ${results.errors.length - 5} more errors\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore - Dynamic property access
|
||||||
|
if (results.newScripts?.length === 0 && results.updatedScripts?.length === 0 && results.errors?.length === 0) {
|
||||||
|
body += 'No script changes detected.';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await appriseService.sendNotification(title, body, settings.appriseUrls);
|
||||||
|
console.log('Sync notification sent successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to send sync notification:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test notification
|
||||||
|
*/
|
||||||
|
async testNotification() {
|
||||||
|
const settings = this.loadSettings();
|
||||||
|
|
||||||
|
if (!settings.notificationEnabled || !settings.appriseUrls?.length) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Notifications not enabled or no Apprise URLs configured'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await appriseService.sendNotification(
|
||||||
|
'ProxmoxVE-Local - Test Notification',
|
||||||
|
'This is a test notification from PVE Scripts Local auto-sync feature.',
|
||||||
|
settings.appriseUrls
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Test notification sent successfully'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Failed to send test notification: ${error instanceof Error ? error.message : String(error)}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get auto-sync status
|
||||||
|
*/
|
||||||
|
getStatus() {
|
||||||
|
return {
|
||||||
|
isRunning: this.isRunning,
|
||||||
|
hasCronJob: !!this.cronJob,
|
||||||
|
lastSync: this.loadSettings().lastAutoSync
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,14 +29,24 @@ export class GitHubService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async fetchFromGitHub<T>(endpoint: string): Promise<T> {
|
private async fetchFromGitHub<T>(endpoint: string): Promise<T> {
|
||||||
const response = await fetch(`${this.baseUrl}${endpoint}`, {
|
const headers: HeadersInit = {
|
||||||
headers: {
|
'Accept': 'application/vnd.github.v3+json',
|
||||||
'Accept': 'application/vnd.github.v3+json',
|
'User-Agent': 'PVEScripts-Local/1.0',
|
||||||
'User-Agent': 'PVEScripts-Local/1.0',
|
};
|
||||||
},
|
|
||||||
});
|
// Add GitHub token authentication if available
|
||||||
|
if (env.GITHUB_TOKEN) {
|
||||||
|
headers.Authorization = `token ${env.GITHUB_TOKEN}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${this.baseUrl}${endpoint}`, { headers });
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
if (response.status === 403) {
|
||||||
|
const error = new Error(`GitHub API rate limit exceeded. Consider setting GITHUB_TOKEN for higher limits. Status: ${response.status} ${response.statusText}`);
|
||||||
|
error.name = 'RateLimitError';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
6
src/server/services/githubJsonService.js
Normal file
6
src/server/services/githubJsonService.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// JavaScript wrapper for githubJsonService.ts
|
||||||
|
// This allows the JavaScript autoSyncService.js to import the TypeScript service
|
||||||
|
|
||||||
|
import { githubJsonService } from './githubJsonService.ts';
|
||||||
|
|
||||||
|
export { githubJsonService };
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { writeFile, mkdir } from 'fs/promises';
|
import { writeFile, mkdir, readdir } from 'fs/promises';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { env } from '~/env.js';
|
import { env } from '../../env.js';
|
||||||
import type { Script, ScriptCard, GitHubFile } from '~/types/script';
|
import type { Script, ScriptCard, GitHubFile } from '../../types/script';
|
||||||
|
|
||||||
export class GitHubJsonService {
|
export class GitHubJsonService {
|
||||||
private baseUrl: string | null = null;
|
private baseUrl: string | null = null;
|
||||||
@@ -41,14 +41,25 @@ export class GitHubJsonService {
|
|||||||
|
|
||||||
private async fetchFromGitHub<T>(endpoint: string): Promise<T> {
|
private async fetchFromGitHub<T>(endpoint: string): Promise<T> {
|
||||||
this.initializeConfig();
|
this.initializeConfig();
|
||||||
const response = await fetch(`${this.baseUrl!}${endpoint}`, {
|
|
||||||
headers: {
|
const headers: HeadersInit = {
|
||||||
'Accept': 'application/vnd.github.v3+json',
|
'Accept': 'application/vnd.github.v3+json',
|
||||||
'User-Agent': 'PVEScripts-Local/1.0',
|
'User-Agent': 'PVEScripts-Local/1.0',
|
||||||
},
|
};
|
||||||
});
|
|
||||||
|
// Add GitHub token authentication if available
|
||||||
|
if (env.GITHUB_TOKEN) {
|
||||||
|
headers.Authorization = `token ${env.GITHUB_TOKEN}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${this.baseUrl!}${endpoint}`, { headers });
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
if (response.status === 403) {
|
||||||
|
const error = new Error(`GitHub API rate limit exceeded. Consider setting GITHUB_TOKEN for higher limits. Status: ${response.status} ${response.statusText}`);
|
||||||
|
error.name = 'RateLimitError';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,8 +70,22 @@ export class GitHubJsonService {
|
|||||||
this.initializeConfig();
|
this.initializeConfig();
|
||||||
const rawUrl = `https://raw.githubusercontent.com/${this.extractRepoPath()}/${this.branch!}/${filePath}`;
|
const rawUrl = `https://raw.githubusercontent.com/${this.extractRepoPath()}/${this.branch!}/${filePath}`;
|
||||||
|
|
||||||
const response = await fetch(rawUrl);
|
const headers: HeadersInit = {
|
||||||
|
'User-Agent': 'PVEScripts-Local/1.0',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add GitHub token authentication if available (for raw files, use token in URL or header)
|
||||||
|
if (env.GITHUB_TOKEN) {
|
||||||
|
headers.Authorization = `token ${env.GITHUB_TOKEN}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(rawUrl, { headers });
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
if (response.status === 403) {
|
||||||
|
const error = new Error(`GitHub rate limit exceeded while downloading ${filePath}. Consider setting GITHUB_TOKEN for higher limits. Status: ${response.status} ${response.statusText}`);
|
||||||
|
error.name = 'RateLimitError';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
throw new Error(`Failed to download ${filePath}: ${response.status} ${response.statusText}`);
|
throw new Error(`Failed to download ${filePath}: ${response.status} ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,48 +210,90 @@ export class GitHubJsonService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncJsonFiles(): Promise<{ success: boolean; message: string; count: number }> {
|
async syncJsonFiles(): Promise<{ success: boolean; message: string; count: number; syncedFiles: string[] }> {
|
||||||
try {
|
try {
|
||||||
// Get all scripts from GitHub (1 API call + raw downloads)
|
console.log('Starting fast incremental JSON sync...');
|
||||||
const scripts = await this.getAllScripts();
|
|
||||||
|
|
||||||
// Save scripts to local directory
|
// Get file list from GitHub
|
||||||
await this.saveScriptsLocally(scripts);
|
console.log('Fetching file list from GitHub...');
|
||||||
|
const githubFiles = await this.getJsonFiles();
|
||||||
|
console.log(`Found ${githubFiles.length} JSON files in repository`);
|
||||||
|
|
||||||
|
// Get local files
|
||||||
|
const localFiles = await this.getLocalJsonFiles();
|
||||||
|
console.log(`Found ${localFiles.length} files in local directory`);
|
||||||
|
console.log(`Found ${localFiles.filter(f => f.endsWith('.json')).length} local JSON files`);
|
||||||
|
|
||||||
|
// Compare and find files that need syncing
|
||||||
|
const filesToSync = this.findFilesToSync(githubFiles, localFiles);
|
||||||
|
console.log(`Found ${filesToSync.length} files that need syncing`);
|
||||||
|
|
||||||
|
if (filesToSync.length === 0) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'All JSON files are up to date',
|
||||||
|
count: 0,
|
||||||
|
syncedFiles: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download and save only the files that need syncing
|
||||||
|
const syncedFiles = await this.syncSpecificFiles(filesToSync);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Successfully synced ${scripts.length} scripts from GitHub using 1 API call + raw downloads`,
|
message: `Successfully synced ${syncedFiles.length} JSON files from GitHub`,
|
||||||
count: scripts.length
|
count: syncedFiles.length,
|
||||||
|
syncedFiles
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error syncing JSON files:', error);
|
console.error('JSON sync failed:', error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: `Failed to sync JSON files: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
message: `Failed to sync JSON files: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
count: 0
|
count: 0,
|
||||||
|
syncedFiles: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async saveScriptsLocally(scripts: Script[]): Promise<void> {
|
private async getLocalJsonFiles(): Promise<string[]> {
|
||||||
this.initializeConfig();
|
this.initializeConfig();
|
||||||
try {
|
try {
|
||||||
// Ensure the directory exists
|
const files = await readdir(this.localJsonDirectory!);
|
||||||
await mkdir(this.localJsonDirectory!, { recursive: true });
|
return files.filter(f => f.endsWith('.json'));
|
||||||
|
} catch {
|
||||||
// Save each script as a JSON file
|
return [];
|
||||||
for (const script of scripts) {
|
|
||||||
const filename = `${script.slug}.json`;
|
|
||||||
const filePath = join(this.localJsonDirectory!, filename);
|
|
||||||
const content = JSON.stringify(script, null, 2);
|
|
||||||
await writeFile(filePath, content, 'utf-8');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving scripts locally:', error);
|
|
||||||
throw new Error('Failed to save scripts locally');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private findFilesToSync(githubFiles: GitHubFile[], localFiles: string[]): GitHubFile[] {
|
||||||
|
const localFileSet = new Set(localFiles);
|
||||||
|
// Return only files that don't exist locally
|
||||||
|
return githubFiles.filter(ghFile => !localFileSet.has(ghFile.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async syncSpecificFiles(filesToSync: GitHubFile[]): Promise<string[]> {
|
||||||
|
this.initializeConfig();
|
||||||
|
const syncedFiles: string[] = [];
|
||||||
|
|
||||||
|
await mkdir(this.localJsonDirectory!, { recursive: true });
|
||||||
|
|
||||||
|
for (const file of filesToSync) {
|
||||||
|
try {
|
||||||
|
const script = await this.downloadJsonFile(file.path);
|
||||||
|
const filename = `${script.slug}.json`;
|
||||||
|
const filePath = join(this.localJsonDirectory!, filename);
|
||||||
|
await writeFile(filePath, JSON.stringify(script, null, 2), 'utf-8');
|
||||||
|
syncedFiles.push(filename);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to sync ${file.name}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return syncedFiles;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Singleton instance
|
// Singleton instance
|
||||||
|
|||||||
6
src/server/services/localScripts.js
Normal file
6
src/server/services/localScripts.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// JavaScript wrapper for localScripts.ts
|
||||||
|
// This allows the JavaScript autoSyncService.js to import the TypeScript service
|
||||||
|
|
||||||
|
import { localScriptsService } from './localScripts.ts';
|
||||||
|
|
||||||
|
export { localScriptsService };
|
||||||
297
src/server/services/scriptDownloader.js
Normal file
297
src/server/services/scriptDownloader.js
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
// Real JavaScript implementation for script downloading
|
||||||
|
import { join } from 'path';
|
||||||
|
import { writeFile, mkdir, access } from 'fs/promises';
|
||||||
|
|
||||||
|
export class ScriptDownloaderService {
|
||||||
|
constructor() {
|
||||||
|
this.scriptsDirectory = null;
|
||||||
|
this.repoUrl = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeConfig() {
|
||||||
|
if (this.scriptsDirectory === null) {
|
||||||
|
this.scriptsDirectory = join(process.cwd(), 'scripts');
|
||||||
|
// Get REPO_URL from environment or use default
|
||||||
|
this.repoUrl = process.env.REPO_URL || 'https://github.com/community-scripts/ProxmoxVE';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensureDirectoryExists(dirPath) {
|
||||||
|
try {
|
||||||
|
await mkdir(dirPath, { recursive: true });
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== 'EEXIST') {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadFileFromGitHub(filePath) {
|
||||||
|
this.initializeConfig();
|
||||||
|
if (!this.repoUrl) {
|
||||||
|
throw new Error('REPO_URL environment variable is not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract repo path from URL
|
||||||
|
const match = /github\.com\/([^\/]+)\/([^\/]+)/.exec(this.repoUrl);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error('Invalid GitHub repository URL');
|
||||||
|
}
|
||||||
|
const [, owner, repo] = match;
|
||||||
|
|
||||||
|
const url = `https://raw.githubusercontent.com/${owner}/${repo}/main/${filePath}`;
|
||||||
|
|
||||||
|
console.log(`Downloading from GitHub: ${url}`);
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to download ${filePath}: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyScriptContent(content) {
|
||||||
|
// Replace the build.func source line
|
||||||
|
const oldPattern = /source <\(curl -fsSL https:\/\/raw\.githubusercontent\.com\/community-scripts\/ProxmoxVE\/main\/misc\/build\.func\)/g;
|
||||||
|
const newPattern = 'SCRIPT_DIR="$(dirname "$0")" \nsource "$SCRIPT_DIR/../core/build.func"';
|
||||||
|
|
||||||
|
return content.replace(oldPattern, newPattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadScript(script) {
|
||||||
|
this.initializeConfig();
|
||||||
|
try {
|
||||||
|
const files = [];
|
||||||
|
|
||||||
|
// Ensure directories exist
|
||||||
|
await this.ensureDirectoryExists(join(this.scriptsDirectory, 'ct'));
|
||||||
|
await this.ensureDirectoryExists(join(this.scriptsDirectory, 'install'));
|
||||||
|
await this.ensureDirectoryExists(join(this.scriptsDirectory, 'tools'));
|
||||||
|
await this.ensureDirectoryExists(join(this.scriptsDirectory, 'vm'));
|
||||||
|
|
||||||
|
if (script.install_methods?.length) {
|
||||||
|
for (const method of script.install_methods) {
|
||||||
|
if (method.script) {
|
||||||
|
const scriptPath = method.script;
|
||||||
|
const fileName = scriptPath.split('/').pop();
|
||||||
|
|
||||||
|
if (fileName) {
|
||||||
|
// Download from GitHub
|
||||||
|
console.log(`Downloading script file: ${scriptPath}`);
|
||||||
|
const content = await this.downloadFileFromGitHub(scriptPath);
|
||||||
|
|
||||||
|
// Determine target directory based on script path
|
||||||
|
let targetDir;
|
||||||
|
let finalTargetDir;
|
||||||
|
let filePath;
|
||||||
|
|
||||||
|
if (scriptPath.startsWith('ct/')) {
|
||||||
|
targetDir = 'ct';
|
||||||
|
finalTargetDir = targetDir;
|
||||||
|
// Modify the content for CT scripts
|
||||||
|
const modifiedContent = this.modifyScriptContent(content);
|
||||||
|
filePath = join(this.scriptsDirectory, targetDir, fileName);
|
||||||
|
await writeFile(filePath, modifiedContent, 'utf-8');
|
||||||
|
} else if (scriptPath.startsWith('tools/')) {
|
||||||
|
targetDir = 'tools';
|
||||||
|
// Preserve subdirectory structure for tools scripts
|
||||||
|
const subPath = scriptPath.replace('tools/', '');
|
||||||
|
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
|
||||||
|
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
|
||||||
|
// Ensure the subdirectory exists
|
||||||
|
await this.ensureDirectoryExists(join(this.scriptsDirectory, finalTargetDir));
|
||||||
|
filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
|
||||||
|
await writeFile(filePath, content, 'utf-8');
|
||||||
|
} else if (scriptPath.startsWith('vm/')) {
|
||||||
|
targetDir = 'vm';
|
||||||
|
// Preserve subdirectory structure for VM scripts
|
||||||
|
const subPath = scriptPath.replace('vm/', '');
|
||||||
|
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
|
||||||
|
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
|
||||||
|
// Ensure the subdirectory exists
|
||||||
|
await this.ensureDirectoryExists(join(this.scriptsDirectory, finalTargetDir));
|
||||||
|
filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
|
||||||
|
await writeFile(filePath, content, 'utf-8');
|
||||||
|
} else if (scriptPath.startsWith('vw/')) {
|
||||||
|
targetDir = 'vw';
|
||||||
|
// Preserve subdirectory structure for VW scripts
|
||||||
|
const subPath = scriptPath.replace('vw/', '');
|
||||||
|
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
|
||||||
|
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
|
||||||
|
// Ensure the subdirectory exists
|
||||||
|
await this.ensureDirectoryExists(join(this.scriptsDirectory, finalTargetDir));
|
||||||
|
filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
|
||||||
|
await writeFile(filePath, content, 'utf-8');
|
||||||
|
} else {
|
||||||
|
// Handle other script types (fallback to ct directory)
|
||||||
|
targetDir = 'ct';
|
||||||
|
finalTargetDir = targetDir;
|
||||||
|
const modifiedContent = this.modifyScriptContent(content);
|
||||||
|
filePath = join(this.scriptsDirectory, targetDir, fileName);
|
||||||
|
await writeFile(filePath, modifiedContent, 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
files.push(`${finalTargetDir}/${fileName}`);
|
||||||
|
console.log(`Successfully downloaded: ${finalTargetDir}/${fileName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only download install script for CT scripts
|
||||||
|
const hasCtScript = script.install_methods?.some(method => method.script?.startsWith('ct/'));
|
||||||
|
if (hasCtScript) {
|
||||||
|
const installScriptName = `${script.slug}-install.sh`;
|
||||||
|
try {
|
||||||
|
console.log(`Downloading install script: install/${installScriptName}`);
|
||||||
|
const installContent = await this.downloadFileFromGitHub(`install/${installScriptName}`);
|
||||||
|
const localInstallPath = join(this.scriptsDirectory, 'install', installScriptName);
|
||||||
|
await writeFile(localInstallPath, installContent, 'utf-8');
|
||||||
|
files.push(`install/${installScriptName}`);
|
||||||
|
console.log(`Successfully downloaded: install/${installScriptName}`);
|
||||||
|
} catch (error) {
|
||||||
|
// Install script might not exist, that's okay
|
||||||
|
console.log(`Install script not found: install/${installScriptName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Successfully loaded ${files.length} script(s) for ${script.name}`,
|
||||||
|
files
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading script:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof Error ? error.message : 'Failed to load script',
|
||||||
|
files: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async isScriptDownloaded(script) {
|
||||||
|
if (!script.install_methods?.length) return false;
|
||||||
|
|
||||||
|
// Check if ALL script files are downloaded
|
||||||
|
for (const method of script.install_methods) {
|
||||||
|
if (method.script) {
|
||||||
|
const scriptPath = method.script;
|
||||||
|
const fileName = scriptPath.split('/').pop();
|
||||||
|
|
||||||
|
if (fileName) {
|
||||||
|
// Determine target directory based on script path
|
||||||
|
let targetDir;
|
||||||
|
let finalTargetDir;
|
||||||
|
let filePath;
|
||||||
|
|
||||||
|
if (scriptPath.startsWith('ct/')) {
|
||||||
|
targetDir = 'ct';
|
||||||
|
finalTargetDir = targetDir;
|
||||||
|
filePath = join(this.scriptsDirectory, targetDir, fileName);
|
||||||
|
} else if (scriptPath.startsWith('tools/')) {
|
||||||
|
targetDir = 'tools';
|
||||||
|
const subPath = scriptPath.replace('tools/', '');
|
||||||
|
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
|
||||||
|
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
|
||||||
|
filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
|
||||||
|
} else if (scriptPath.startsWith('vm/')) {
|
||||||
|
targetDir = 'vm';
|
||||||
|
const subPath = scriptPath.replace('vm/', '');
|
||||||
|
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
|
||||||
|
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
|
||||||
|
filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
|
||||||
|
} else if (scriptPath.startsWith('vw/')) {
|
||||||
|
targetDir = 'vw';
|
||||||
|
const subPath = scriptPath.replace('vw/', '');
|
||||||
|
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
|
||||||
|
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
|
||||||
|
filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
|
||||||
|
} else {
|
||||||
|
targetDir = 'ct';
|
||||||
|
finalTargetDir = targetDir;
|
||||||
|
filePath = join(this.scriptsDirectory, targetDir, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await import('fs/promises').then(fs => fs.readFile(filePath, 'utf8'));
|
||||||
|
// File exists, continue checking other methods
|
||||||
|
} catch {
|
||||||
|
// File doesn't exist, script is not fully downloaded
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All files exist, script is downloaded
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkScriptExists(script) {
|
||||||
|
this.initializeConfig();
|
||||||
|
const files = [];
|
||||||
|
let ctExists = false;
|
||||||
|
let installExists = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check scripts based on their install methods
|
||||||
|
if (script.install_methods?.length) {
|
||||||
|
for (const method of script.install_methods) {
|
||||||
|
if (method.script) {
|
||||||
|
const scriptPath = method.script;
|
||||||
|
const fileName = scriptPath.split('/').pop();
|
||||||
|
|
||||||
|
if (fileName) {
|
||||||
|
let targetDir;
|
||||||
|
if (scriptPath.startsWith('ct/')) {
|
||||||
|
targetDir = 'ct';
|
||||||
|
} else if (scriptPath.startsWith('tools/')) {
|
||||||
|
targetDir = 'tools';
|
||||||
|
} else if (scriptPath.startsWith('vm/')) {
|
||||||
|
targetDir = 'vm';
|
||||||
|
} else {
|
||||||
|
targetDir = 'ct'; // Default fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = join(this.scriptsDirectory, targetDir, fileName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await access(filePath);
|
||||||
|
files.push(`${targetDir}/${fileName}`);
|
||||||
|
|
||||||
|
if (scriptPath.startsWith('ct/')) {
|
||||||
|
ctExists = true;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// File doesn't exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for install script for CT scripts
|
||||||
|
const hasCtScript = script.install_methods?.some(method => method.script?.startsWith('ct/'));
|
||||||
|
if (hasCtScript) {
|
||||||
|
const installScriptName = `${script.slug}-install.sh`;
|
||||||
|
const installPath = join(this.scriptsDirectory, 'install', installScriptName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await access(installPath);
|
||||||
|
files.push(`install/${installScriptName}`);
|
||||||
|
installExists = true;
|
||||||
|
} catch {
|
||||||
|
// Install script doesn't exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ctExists, installExists, files };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking script existence:', error);
|
||||||
|
return { ctExists: false, installExists: false, files: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const scriptDownloaderService = new ScriptDownloaderService();
|
||||||
@@ -167,6 +167,203 @@ export class ScriptDownloaderService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-download new scripts that haven't been downloaded yet
|
||||||
|
*/
|
||||||
|
async autoDownloadNewScripts(allScripts: Script[]): Promise<{ downloaded: string[]; errors: string[] }> {
|
||||||
|
this.initializeConfig();
|
||||||
|
const downloaded: string[] = [];
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
for (const script of allScripts) {
|
||||||
|
try {
|
||||||
|
// Check if script is already downloaded
|
||||||
|
const isDownloaded = await this.isScriptDownloaded(script);
|
||||||
|
|
||||||
|
if (!isDownloaded) {
|
||||||
|
const result = await this.loadScript(script);
|
||||||
|
if (result.success) {
|
||||||
|
downloaded.push(script.name || script.slug);
|
||||||
|
console.log(`Auto-downloaded new script: ${script.name || script.slug}`);
|
||||||
|
} else {
|
||||||
|
errors.push(`${script.name || script.slug}: ${result.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = `${script.name || script.slug}: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
||||||
|
errors.push(errorMsg);
|
||||||
|
console.error(`Failed to auto-download script ${script.slug}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { downloaded, errors };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-update existing scripts to newer versions
|
||||||
|
*/
|
||||||
|
async autoUpdateExistingScripts(allScripts: Script[]): Promise<{ updated: string[]; errors: string[] }> {
|
||||||
|
this.initializeConfig();
|
||||||
|
const updated: string[] = [];
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
for (const script of allScripts) {
|
||||||
|
try {
|
||||||
|
// Check if script is downloaded
|
||||||
|
const isDownloaded = await this.isScriptDownloaded(script);
|
||||||
|
|
||||||
|
if (isDownloaded) {
|
||||||
|
// Check if update is needed by comparing content
|
||||||
|
const needsUpdate = await this.scriptNeedsUpdate(script);
|
||||||
|
|
||||||
|
if (needsUpdate) {
|
||||||
|
const result = await this.loadScript(script);
|
||||||
|
if (result.success) {
|
||||||
|
updated.push(script.name || script.slug);
|
||||||
|
console.log(`Auto-updated script: ${script.name || script.slug}`);
|
||||||
|
} else {
|
||||||
|
errors.push(`${script.name || script.slug}: ${result.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = `${script.name || script.slug}: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
||||||
|
errors.push(errorMsg);
|
||||||
|
console.error(`Failed to auto-update script ${script.slug}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { updated, errors };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a script is already downloaded
|
||||||
|
*/
|
||||||
|
async isScriptDownloaded(script: Script): Promise<boolean> {
|
||||||
|
if (!script.install_methods?.length) return false;
|
||||||
|
|
||||||
|
// Check if ALL script files are downloaded
|
||||||
|
for (const method of script.install_methods) {
|
||||||
|
if (method.script) {
|
||||||
|
const scriptPath = method.script;
|
||||||
|
const fileName = scriptPath.split('/').pop();
|
||||||
|
|
||||||
|
if (fileName) {
|
||||||
|
// Determine target directory based on script path
|
||||||
|
let targetDir: string;
|
||||||
|
let finalTargetDir: string;
|
||||||
|
let filePath: string;
|
||||||
|
|
||||||
|
if (scriptPath.startsWith('ct/')) {
|
||||||
|
targetDir = 'ct';
|
||||||
|
finalTargetDir = targetDir;
|
||||||
|
filePath = join(this.scriptsDirectory!, targetDir, fileName);
|
||||||
|
} else if (scriptPath.startsWith('tools/')) {
|
||||||
|
targetDir = 'tools';
|
||||||
|
const subPath = scriptPath.replace('tools/', '');
|
||||||
|
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
|
||||||
|
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
|
||||||
|
filePath = join(this.scriptsDirectory!, finalTargetDir, fileName);
|
||||||
|
} else if (scriptPath.startsWith('vm/')) {
|
||||||
|
targetDir = 'vm';
|
||||||
|
const subPath = scriptPath.replace('vm/', '');
|
||||||
|
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
|
||||||
|
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
|
||||||
|
filePath = join(this.scriptsDirectory!, finalTargetDir, fileName);
|
||||||
|
} else if (scriptPath.startsWith('vw/')) {
|
||||||
|
targetDir = 'vw';
|
||||||
|
const subPath = scriptPath.replace('vw/', '');
|
||||||
|
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
|
||||||
|
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
|
||||||
|
filePath = join(this.scriptsDirectory!, finalTargetDir, fileName);
|
||||||
|
} else {
|
||||||
|
targetDir = 'ct';
|
||||||
|
finalTargetDir = targetDir;
|
||||||
|
filePath = join(this.scriptsDirectory!, targetDir, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await readFile(filePath, 'utf8');
|
||||||
|
// File exists, continue checking other methods
|
||||||
|
} catch {
|
||||||
|
// File doesn't exist, script is not fully downloaded
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All files exist, script is downloaded
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a script needs updating by comparing local and remote content
|
||||||
|
*/
|
||||||
|
private async scriptNeedsUpdate(script: Script): Promise<boolean> {
|
||||||
|
if (!script.install_methods?.length) return false;
|
||||||
|
|
||||||
|
for (const method of script.install_methods) {
|
||||||
|
if (method.script) {
|
||||||
|
const scriptPath = method.script;
|
||||||
|
const fileName = scriptPath.split('/').pop();
|
||||||
|
|
||||||
|
if (fileName) {
|
||||||
|
// Determine target directory based on script path
|
||||||
|
let targetDir: string;
|
||||||
|
let finalTargetDir: string;
|
||||||
|
let filePath: string;
|
||||||
|
|
||||||
|
if (scriptPath.startsWith('ct/')) {
|
||||||
|
targetDir = 'ct';
|
||||||
|
finalTargetDir = targetDir;
|
||||||
|
filePath = join(this.scriptsDirectory!, targetDir, fileName);
|
||||||
|
} else if (scriptPath.startsWith('tools/')) {
|
||||||
|
targetDir = 'tools';
|
||||||
|
const subPath = scriptPath.replace('tools/', '');
|
||||||
|
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
|
||||||
|
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
|
||||||
|
filePath = join(this.scriptsDirectory!, finalTargetDir, fileName);
|
||||||
|
} else if (scriptPath.startsWith('vm/')) {
|
||||||
|
targetDir = 'vm';
|
||||||
|
const subPath = scriptPath.replace('vm/', '');
|
||||||
|
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
|
||||||
|
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
|
||||||
|
filePath = join(this.scriptsDirectory!, finalTargetDir, fileName);
|
||||||
|
} else if (scriptPath.startsWith('vw/')) {
|
||||||
|
targetDir = 'vw';
|
||||||
|
const subPath = scriptPath.replace('vw/', '');
|
||||||
|
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
|
||||||
|
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
|
||||||
|
filePath = join(this.scriptsDirectory!, finalTargetDir, fileName);
|
||||||
|
} else {
|
||||||
|
targetDir = 'ct';
|
||||||
|
finalTargetDir = targetDir;
|
||||||
|
filePath = join(this.scriptsDirectory!, targetDir, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read local content
|
||||||
|
const localContent = await readFile(filePath, 'utf8');
|
||||||
|
|
||||||
|
// Download remote content
|
||||||
|
const remoteContent = await this.downloadFileFromGitHub(scriptPath);
|
||||||
|
|
||||||
|
// Compare content (simple string comparison for now)
|
||||||
|
// In a more sophisticated implementation, you might want to compare
|
||||||
|
// file modification times or use content hashing
|
||||||
|
return localContent !== remoteContent;
|
||||||
|
} catch {
|
||||||
|
// If we can't read local or download remote, assume update needed
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
async checkScriptExists(script: Script): Promise<{ ctExists: boolean; installExists: boolean; files: string[] }> {
|
async checkScriptExists(script: Script): Promise<{ ctExists: boolean; installExists: boolean; files: string[] }> {
|
||||||
this.initializeConfig();
|
this.initializeConfig();
|
||||||
const files: string[] = [];
|
const files: string[] = [];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
#21.10.2025
|
#21.10.2025 - @michelroegl-brunner
|
||||||
# Enhanced update script for ProxmoxVE-Local
|
# Enhanced update script for ProxmoxVE-Local
|
||||||
# Fetches latest release from GitHub and backs up data directory
|
# Fetches latest release from GitHub and backs up data directory
|
||||||
|
|
||||||
@@ -551,7 +551,6 @@ update_files() {
|
|||||||
"*.backup"
|
"*.backup"
|
||||||
"*.bak"
|
"*.bak"
|
||||||
"scripts"
|
"scripts"
|
||||||
"prisma/migrations"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Find the actual source directory (strip the top-level directory)
|
# Find the actual source directory (strip the top-level directory)
|
||||||
@@ -622,6 +621,7 @@ update_files() {
|
|||||||
log_success "Application files updated successfully ($files_copied files)"
|
log_success "Application files updated successfully ($files_copied files)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Install dependencies and build
|
# Install dependencies and build
|
||||||
install_and_build() {
|
install_and_build() {
|
||||||
log "Installing dependencies..."
|
log "Installing dependencies..."
|
||||||
@@ -927,7 +927,6 @@ main() {
|
|||||||
# Restore .env and data directory before building
|
# Restore .env and data directory before building
|
||||||
restore_backup_files
|
restore_backup_files
|
||||||
|
|
||||||
|
|
||||||
# Verify database was restored correctly
|
# Verify database was restored correctly
|
||||||
if ! verify_database_restored; then
|
if ! verify_database_restored; then
|
||||||
log_error "Database verification failed, rolling back..."
|
log_error "Database verification failed, rolling back..."
|
||||||
|
|||||||
Reference in New Issue
Block a user