mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2026-03-03 17:35:55 +01:00
Compare commits
36 Commits
MickLesk-p
...
tinyauth
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6d7cec2a3 | ||
|
|
f9b59d7634 | ||
|
|
f279536eb7 | ||
|
|
8f34c7cd2e | ||
|
|
f922f669a8 | ||
|
|
2694d60faf | ||
|
|
a326994459 | ||
|
|
4c8d1ef010 | ||
|
|
86a9a0aac6 | ||
|
|
5059ed2320 | ||
|
|
df65c60fe4 | ||
|
|
cff2c90041 | ||
|
|
88d1494e46 | ||
|
|
8b62b8f3c5 | ||
|
|
cd38bc3a65 | ||
|
|
46d25645c2 | ||
|
|
3701737eff | ||
|
|
b39e296684 | ||
|
|
d8a7620c64 | ||
|
|
7d5900de18 | ||
|
|
e8b3b936df | ||
|
|
5c246310f4 | ||
|
|
bdad2cc941 | ||
|
|
f23c33fee7 | ||
|
|
5b207cf5bd | ||
|
|
f20c9e4ec9 | ||
|
|
1398ff8397 | ||
|
|
ebc3512f50 | ||
|
|
564a8136a5 | ||
|
|
00047c95b8 | ||
|
|
9849ce79a7 | ||
|
|
cea9858193 | ||
|
|
ee8ea672ef | ||
|
|
fc59910bd2 | ||
|
|
20ab7bc005 | ||
|
|
17de8e761b |
255
.github/workflows/push-json-to-pocketbase.yml
generated
vendored
Normal file
255
.github/workflows/push-json-to-pocketbase.yml
generated
vendored
Normal file
@@ -0,0 +1,255 @@
|
||||
name: Push JSON changes to PocketBase
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "frontend/public/json/**"
|
||||
|
||||
jobs:
|
||||
push-json:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get changed JSON files with slug
|
||||
id: changed
|
||||
run: |
|
||||
changed=$(git diff --name-only "${{ github.event.before }}" "${{ github.event.after }}" -- frontend/public/json/ | grep '\.json$' || true)
|
||||
with_slug=""
|
||||
for f in $changed; do
|
||||
[[ -f "$f" ]] || continue
|
||||
jq -e '.slug' "$f" >/dev/null 2>&1 && with_slug="$with_slug $f"
|
||||
done
|
||||
with_slug=$(echo $with_slug | xargs -n1)
|
||||
if [[ -z "$with_slug" ]]; then
|
||||
echo "No app JSON files changed (or no files with slug)."
|
||||
echo "count=0" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "$with_slug" > changed_app_jsons.txt
|
||||
echo "count=$(echo "$with_slug" | wc -w)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Push to PocketBase
|
||||
if: steps.changed.outputs.count != '0'
|
||||
env:
|
||||
POCKETBASE_URL: ${{ secrets.POCKETBASE_URL }}
|
||||
POCKETBASE_COLLECTION: ${{ secrets.POCKETBASE_COLLECTION }}
|
||||
POCKETBASE_ADMIN_EMAIL: ${{ secrets.POCKETBASE_ADMIN_EMAIL }}
|
||||
POCKETBASE_ADMIN_PASSWORD: ${{ secrets.POCKETBASE_ADMIN_PASSWORD }}
|
||||
run: |
|
||||
node << 'ENDSCRIPT'
|
||||
(async function() {
|
||||
const fs = require('fs');
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const url = require('url');
|
||||
function request(fullUrl, opts) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
const u = url.parse(fullUrl);
|
||||
const isHttps = u.protocol === 'https:';
|
||||
const body = opts.body;
|
||||
const options = {
|
||||
hostname: u.hostname,
|
||||
port: u.port || (isHttps ? 443 : 80),
|
||||
path: u.path,
|
||||
method: opts.method || 'GET',
|
||||
headers: opts.headers || {}
|
||||
};
|
||||
if (body) options.headers['Content-Length'] = Buffer.byteLength(body);
|
||||
const lib = isHttps ? https : http;
|
||||
const req = lib.request(options, function(res) {
|
||||
let data = '';
|
||||
res.on('data', function(chunk) { data += chunk; });
|
||||
res.on('end', function() {
|
||||
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, statusCode: res.statusCode, body: data });
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
if (body) req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
const raw = process.env.POCKETBASE_URL.replace(/\/$/, '');
|
||||
const apiBase = /\/api$/i.test(raw) ? raw : raw + '/api';
|
||||
const coll = process.env.POCKETBASE_COLLECTION;
|
||||
const files = fs.readFileSync('changed_app_jsons.txt', 'utf8').trim().split(/\s+/).filter(Boolean);
|
||||
const authUrl = apiBase + '/collections/users/auth-with-password';
|
||||
console.log('Auth URL: ' + authUrl);
|
||||
const authBody = JSON.stringify({
|
||||
identity: process.env.POCKETBASE_ADMIN_EMAIL,
|
||||
password: process.env.POCKETBASE_ADMIN_PASSWORD
|
||||
});
|
||||
const authRes = await request(authUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: authBody
|
||||
});
|
||||
if (!authRes.ok) {
|
||||
throw new Error('Auth failed. Tried: ' + authUrl + ' - Verify POST to that URL with body {"identity":"...","password":"..."} works. Response: ' + authRes.body);
|
||||
}
|
||||
const token = JSON.parse(authRes.body).token;
|
||||
const recordsUrl = apiBase + '/collections/' + encodeURIComponent(coll) + '/records';
|
||||
let categoryIdToName = {};
|
||||
try {
|
||||
const metadata = JSON.parse(fs.readFileSync('frontend/public/json/metadata.json', 'utf8'));
|
||||
(metadata.categories || []).forEach(function(cat) { categoryIdToName[cat.id] = cat.name; });
|
||||
} catch (e) { console.warn('Could not load metadata.json:', e.message); }
|
||||
let typeValueToId = {};
|
||||
let categoryNameToPbId = {};
|
||||
try {
|
||||
const typesRes = await request(apiBase + '/collections/z_ref_script_types/records?perPage=500', { headers: { 'Authorization': token } });
|
||||
if (typesRes.ok) {
|
||||
const typesData = JSON.parse(typesRes.body);
|
||||
(typesData.items || []).forEach(function(item) {
|
||||
if (item.type != null) typeValueToId[item.type] = item.id;
|
||||
if (item.name != null) typeValueToId[item.name] = item.id;
|
||||
if (item.value != null) typeValueToId[item.value] = item.id;
|
||||
});
|
||||
}
|
||||
} catch (e) { console.warn('Could not fetch z_ref_script_types:', e.message); }
|
||||
try {
|
||||
const catRes = await request(apiBase + '/collections/script_categories/records?perPage=500', { headers: { 'Authorization': token } });
|
||||
if (catRes.ok) {
|
||||
const catData = JSON.parse(catRes.body);
|
||||
(catData.items || []).forEach(function(item) { if (item.name) categoryNameToPbId[item.name] = item.id; });
|
||||
}
|
||||
} catch (e) { console.warn('Could not fetch script_categories:', e.message); }
|
||||
var noteTypeToId = {};
|
||||
var installMethodTypeToId = {};
|
||||
var osToId = {};
|
||||
var osVersionToId = {};
|
||||
try {
|
||||
const res = await request(apiBase + '/collections/z_ref_note_types/records?perPage=500', { headers: { 'Authorization': token } });
|
||||
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.type != null) noteTypeToId[item.type] = item.id; });
|
||||
} catch (e) { console.warn('z_ref_note_types:', e.message); }
|
||||
try {
|
||||
const res = await request(apiBase + '/collections/z_ref_install_method_types/records?perPage=500', { headers: { 'Authorization': token } });
|
||||
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.type != null) installMethodTypeToId[item.type] = item.id; });
|
||||
} catch (e) { console.warn('z_ref_install_method_types:', e.message); }
|
||||
try {
|
||||
const res = await request(apiBase + '/collections/z_ref_os/records?perPage=500', { headers: { 'Authorization': token } });
|
||||
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.os != null) osToId[item.os] = item.id; });
|
||||
} catch (e) { console.warn('z_ref_os:', e.message); }
|
||||
try {
|
||||
const res = await request(apiBase + '/collections/z_ref_os_version/records?perPage=500&expand=os', { headers: { 'Authorization': token } });
|
||||
if (res.ok) {
|
||||
(JSON.parse(res.body).items || []).forEach(function(item) {
|
||||
var osName = item.expand && item.expand.os && item.expand.os.os != null ? item.expand.os.os : null;
|
||||
if (osName != null && item.version != null) osVersionToId[osName + '|' + item.version] = item.id;
|
||||
});
|
||||
}
|
||||
} catch (e) { console.warn('z_ref_os_version:', e.message); }
|
||||
var notesCollUrl = apiBase + '/collections/script_notes/records';
|
||||
var installMethodsCollUrl = apiBase + '/collections/script_install_methods/records';
|
||||
for (const file of files) {
|
||||
if (!fs.existsSync(file)) continue;
|
||||
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
if (!data.slug) { console.log('Skipping', file, '(no slug)'); continue; }
|
||||
var payload = {
|
||||
name: data.name,
|
||||
slug: data.slug,
|
||||
script_created: data.date_created || data.script_created,
|
||||
script_updated: data.date_created || data.script_updated,
|
||||
updateable: data.updateable,
|
||||
privileged: data.privileged,
|
||||
port: data.interface_port != null ? data.interface_port : data.port,
|
||||
documentation: data.documentation,
|
||||
website: data.website,
|
||||
logo: data.logo,
|
||||
description: data.description,
|
||||
config_path: data.config_path,
|
||||
default_user: (data.default_credentials && data.default_credentials.username) || data.default_user,
|
||||
default_passwd: (data.default_credentials && data.default_credentials.password) || data.default_passwd,
|
||||
is_dev: false
|
||||
};
|
||||
var resolvedType = typeValueToId[data.type];
|
||||
if (resolvedType == null && data.type === 'ct') resolvedType = typeValueToId['lxc'];
|
||||
if (resolvedType) payload.type = resolvedType;
|
||||
var resolvedCats = (data.categories || []).map(function(n) { return categoryNameToPbId[categoryIdToName[n]]; }).filter(Boolean);
|
||||
if (resolvedCats.length) payload.categories = resolvedCats;
|
||||
if (data.version !== undefined) payload.version = data.version;
|
||||
if (data.changelog !== undefined) payload.changelog = data.changelog;
|
||||
if (data.screenshots !== undefined) payload.screenshots = data.screenshots;
|
||||
const filter = "(slug='" + data.slug + "')";
|
||||
const listRes = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1', {
|
||||
headers: { 'Authorization': token }
|
||||
});
|
||||
const list = JSON.parse(listRes.body);
|
||||
const existingId = list.items && list.items[0] && list.items[0].id;
|
||||
async function resolveNotesAndInstallMethods(scriptId) {
|
||||
var noteIds = [];
|
||||
for (var i = 0; i < (data.notes || []).length; i++) {
|
||||
var note = data.notes[i];
|
||||
var typeId = noteTypeToId[note.type];
|
||||
if (typeId == null) continue;
|
||||
var postRes = await request(notesCollUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: note.text || '', type: typeId })
|
||||
});
|
||||
if (postRes.ok) noteIds.push(JSON.parse(postRes.body).id);
|
||||
}
|
||||
var installMethodIds = [];
|
||||
for (var j = 0; j < (data.install_methods || []).length; j++) {
|
||||
var im = data.install_methods[j];
|
||||
var typeId = installMethodTypeToId[im.type];
|
||||
var res = im.resources || {};
|
||||
var osId = osToId[res.os];
|
||||
var osVersionKey = (res.os != null && res.version != null) ? res.os + '|' + res.version : null;
|
||||
var osVersionId = osVersionKey ? osVersionToId[osVersionKey] : null;
|
||||
var imBody = {
|
||||
script: scriptId,
|
||||
resources_cpu: res.cpu != null ? res.cpu : 0,
|
||||
resources_ram: res.ram != null ? res.ram : 0,
|
||||
resources_hdd: res.hdd != null ? res.hdd : 0
|
||||
};
|
||||
if (typeId) imBody.type = typeId;
|
||||
if (osId) imBody.os = osId;
|
||||
if (osVersionId) imBody.os_version = osVersionId;
|
||||
var imPostRes = await request(installMethodsCollUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(imBody)
|
||||
});
|
||||
if (imPostRes.ok) installMethodIds.push(JSON.parse(imPostRes.body).id);
|
||||
}
|
||||
return { noteIds: noteIds, installMethodIds: installMethodIds };
|
||||
}
|
||||
if (existingId) {
|
||||
var resolved = await resolveNotesAndInstallMethods(existingId);
|
||||
payload.notes = resolved.noteIds;
|
||||
payload.install_methods = resolved.installMethodIds;
|
||||
console.log('Updating', file, '(slug=' + data.slug + ')');
|
||||
const r = await request(recordsUrl + '/' + existingId, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!r.ok) throw new Error('PATCH failed: ' + r.body);
|
||||
} else {
|
||||
console.log('Creating', file, '(slug=' + data.slug + ')');
|
||||
const r = await request(recordsUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!r.ok) throw new Error('POST failed: ' + r.body);
|
||||
var scriptId = JSON.parse(r.body).id;
|
||||
var resolved = await resolveNotesAndInstallMethods(scriptId);
|
||||
var patchRes = await request(recordsUrl + '/' + scriptId, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ install_methods: resolved.installMethodIds, notes: resolved.noteIds })
|
||||
});
|
||||
if (!patchRes.ok) throw new Error('PATCH relations failed: ' + patchRes.body);
|
||||
}
|
||||
}
|
||||
console.log('Done.');
|
||||
})().catch(e => { console.error(e); process.exit(1); });
|
||||
ENDSCRIPT
|
||||
shell: bash
|
||||
23
CHANGELOG.md
23
CHANGELOG.md
@@ -410,18 +410,39 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
||||
|
||||
</details>
|
||||
|
||||
## 2026-03-03
|
||||
|
||||
### 🌐 Website
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Revert #11534 PR that messed up search [@BramSuurdje](https://github.com/BramSuurdje) ([#12492](https://github.com/community-scripts/ProxmoxVE/pull/12492))
|
||||
|
||||
## 2026-03-02
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Profilarr ([#12441](https://github.com/community-scripts/ProxmoxVE/pull/12441))
|
||||
- PowerDNS ([#12481](https://github.com/community-scripts/ProxmoxVE/pull/12481))
|
||||
- Profilarr ([#12441](https://github.com/community-scripts/ProxmoxVE/pull/12441))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Tracearr: prepare for imminent v1.4.19 release [@durzo](https://github.com/durzo) ([#12413](https://github.com/community-scripts/ProxmoxVE/pull/12413))
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- Frigate: Bump to v0.17 [@MickLesk](https://github.com/MickLesk) ([#12474](https://github.com/community-scripts/ProxmoxVE/pull/12474))
|
||||
|
||||
- #### 💥 Breaking Changes
|
||||
|
||||
- Migrate: DokPloy, Komodo, Coolify, Dockge, Runtipi to Addons [@MickLesk](https://github.com/MickLesk) ([#12275](https://github.com/community-scripts/ProxmoxVE/pull/12275))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- ref: replace generic exit 1 with specific exit codes in ct & install [@MickLesk](https://github.com/MickLesk) ([#12475](https://github.com/community-scripts/ProxmoxVE/pull/12475))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
@@ -39,7 +39,7 @@ function update_script() {
|
||||
COMPOSE_FILE=$(find /opt/komodo -maxdepth 1 -type f -name '*.compose.yaml' ! -name 'compose.env' | head -n1)
|
||||
if [[ -z "$COMPOSE_FILE" ]]; then
|
||||
msg_error "No valid compose file found in /opt/komodo!"
|
||||
exit 1
|
||||
exit 252
|
||||
fi
|
||||
$STD docker compose -p komodo -f "$COMPOSE_FILE" --env-file /opt/komodo/compose.env pull
|
||||
$STD docker compose -p komodo -f "$COMPOSE_FILE" --env-file /opt/komodo/compose.env up -d
|
||||
|
||||
@@ -35,6 +35,20 @@ function update_script() {
|
||||
$STD service tinyauth stop
|
||||
msg_ok "Service Stopped"
|
||||
|
||||
if [[ -f /opt/tinyauth/.env ]] && ! grep -q "^TINYAUTH_" /opt/tinyauth/.env; then
|
||||
msg_info "Migrating .env to v5 format"
|
||||
sed -i \
|
||||
-e 's/^DATABASE_PATH=/TINYAUTH_DATABASE_PATH=/' \
|
||||
-e 's/^USERS=/TINYAUTH_AUTH_USERS=/' \
|
||||
-e "s/^USERS='/TINYAUTH_AUTH_USERS='/" \
|
||||
-e 's/^APP_URL=/TINYAUTH_APPURL=/' \
|
||||
-e 's/^SECRET=/TINYAUTH_AUTH_SECRET=/' \
|
||||
-e 's/^PORT=/TINYAUTH_SERVER_PORT=/' \
|
||||
-e 's/^ADDRESS=/TINYAUTH_SERVER_ADDRESS=/' \
|
||||
/opt/tinyauth/.env
|
||||
msg_ok "Migrated .env to v5 format"
|
||||
fi
|
||||
|
||||
msg_info "Updating Tinyauth"
|
||||
rm -f /opt/tinyauth/tinyauth
|
||||
curl -fsSL "https://github.com/steveiliop56/tinyauth/releases/download/v${RELEASE}/tinyauth-amd64" -o /opt/tinyauth/tinyauth
|
||||
|
||||
@@ -26,7 +26,7 @@ function update_script() {
|
||||
|
||||
if [[ ! -d /opt/endurain ]]; then
|
||||
msg_error "No ${APP} installation found!"
|
||||
exit 1
|
||||
exit 233
|
||||
fi
|
||||
if check_for_gh_release "endurain" "endurain-project/endurain"; then
|
||||
msg_info "Stopping Service"
|
||||
|
||||
@@ -25,7 +25,7 @@ function update_script() {
|
||||
check_container_resources
|
||||
if ! command -v evcc >/dev/null 2>&1; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit 1
|
||||
exit 233
|
||||
fi
|
||||
|
||||
if [[ -f /etc/apt/sources.list.d/evcc-stable.list ]]; then
|
||||
|
||||
@@ -26,7 +26,7 @@ function update_script() {
|
||||
|
||||
if ! dpkg -s grafana >/dev/null 2>&1; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit 1
|
||||
exit 233
|
||||
fi
|
||||
|
||||
if [[ -f /etc/apt/sources.list.d/grafana.list ]] || [[ ! -f /etc/apt/sources.list.d/grafana.sources ]]; then
|
||||
|
||||
6
ct/headers/powerdns
Normal file
6
ct/headers/powerdns
Normal file
@@ -0,0 +1,6 @@
|
||||
____ ____ _ _______
|
||||
/ __ \____ _ _____ _____/ __ \/ | / / ___/
|
||||
/ /_/ / __ \ | /| / / _ \/ ___/ / / / |/ /\__ \
|
||||
/ ____/ /_/ / |/ |/ / __/ / / /_/ / /| /___/ /
|
||||
/_/ \____/|__/|__/\___/_/ /_____/_/ |_//____/
|
||||
|
||||
@@ -26,7 +26,7 @@ function update_script() {
|
||||
|
||||
if [[ ! -f /etc/itsm-ng/config_db.php ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit 1
|
||||
exit 233
|
||||
fi
|
||||
setup_mariadb
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ function update_script() {
|
||||
|
||||
if [[ -z "$KASM_URL" ]] || [[ -z "$KASM_VERSION" ]]; then
|
||||
msg_error "Unable to detect latest Kasm release URL."
|
||||
exit 1
|
||||
exit 250
|
||||
fi
|
||||
msg_info "Checked for new version"
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ function update_script() {
|
||||
COMPOSE_FILE=$(find /opt/komodo -maxdepth 1 -type f -name '*.compose.yaml' ! -name 'compose.env' | head -n1)
|
||||
if [[ -z "$COMPOSE_FILE" ]]; then
|
||||
msg_error "No valid compose file found in /opt/komodo!"
|
||||
exit 1
|
||||
exit 252
|
||||
fi
|
||||
$STD docker compose -p komodo -f "$COMPOSE_FILE" --env-file /opt/komodo/compose.env pull
|
||||
$STD docker compose -p komodo -f "$COMPOSE_FILE" --env-file /opt/komodo/compose.env up -d
|
||||
|
||||
@@ -26,7 +26,7 @@ function update_script() {
|
||||
|
||||
if ! dpkg -s loki >/dev/null 2>&1; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit 1
|
||||
exit 233
|
||||
fi
|
||||
|
||||
CHOICE=$(msg_menu "Loki Update Options" \
|
||||
|
||||
@@ -44,7 +44,7 @@ function update_script() {
|
||||
echo -e "${TAB}${GATEWAY}${BGN}https://github.com/community-scripts/ProxmoxVE/discussions/9223${CL}"
|
||||
echo -e ""
|
||||
msg_custom "⚠️" "Update aborted. Please migrate your data first."
|
||||
exit 1
|
||||
exit 253
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
68
ct/powerdns.sh
Normal file
68
ct/powerdns.sh
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: Slaviša Arežina (tremor021)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://www.powerdns.com/
|
||||
|
||||
APP="PowerDNS"
|
||||
var_tags="${var_tags:-dns}"
|
||||
var_cpu="${var_cpu:-1}"
|
||||
var_ram="${var_ram:-1024}"
|
||||
var_disk="${var_disk:-4}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
if [[ ! -d /opt/poweradmin ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
msg_info "Updating PowerDNS"
|
||||
$STD apt update
|
||||
$STD apt install -y --only-upgrade pdns-server pdns-backend-sqlite3
|
||||
msg_ok "Updated PowerDNS"
|
||||
|
||||
if check_for_gh_release "poweradmin" "poweradmin/poweradmin"; then
|
||||
msg_info "Backing up Configuration"
|
||||
cp /opt/poweradmin/config/settings.php /opt/poweradmin_settings.php.bak
|
||||
cp /opt/poweradmin/powerdns.db /opt/poweradmin_powerdns.db.bak
|
||||
msg_ok "Backed up Configuration"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "poweradmin" "poweradmin/poweradmin" "tarball"
|
||||
|
||||
msg_info "Updating Poweradmin"
|
||||
cp /opt/poweradmin_settings.php.bak /opt/poweradmin/config/settings.php
|
||||
cp /opt/poweradmin_powerdns.db.bak /opt/poweradmin/powerdns.db
|
||||
rm -rf /opt/poweradmin/install
|
||||
rm -f /opt/poweradmin_settings.php.bak /opt/poweradmin_powerdns.db.bak
|
||||
chown -R www-data:www-data /opt/poweradmin
|
||||
msg_ok "Updated Poweradmin"
|
||||
|
||||
msg_info "Restarting Services"
|
||||
systemctl restart pdns apache2
|
||||
msg_ok "Restarted Services"
|
||||
msg_ok "Updated successfully!"
|
||||
fi
|
||||
exit
|
||||
}
|
||||
|
||||
start
|
||||
build_container
|
||||
description
|
||||
|
||||
msg_ok "Completed Successfully!\n"
|
||||
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
|
||||
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}${CL}"
|
||||
53
ct/tinyauth.sh
Normal file
53
ct/tinyauth.sh
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/steveiliop56/tinyauth
|
||||
|
||||
APP="Tinyauth"
|
||||
var_tags="${var_tags:-auth}"
|
||||
var_cpu="${var_cpu:-1}"
|
||||
var_ram="${var_ram:-512}"
|
||||
var_disk="${var_disk:-4}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
if [[ ! -d /opt/tinyauth ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "tinyauth" "steveiliop56/tinyauth"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop tinyauth
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
fetch_and_deploy_gh_release "tinyauth" "steveiliop56/tinyauth" "singlefile" "latest" "/opt/tinyauth" "tinyauth-amd64"
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start tinyauth
|
||||
msg_ok "Started Service"
|
||||
msg_ok "Updated successfully!"
|
||||
fi
|
||||
exit
|
||||
}
|
||||
|
||||
start
|
||||
build_container
|
||||
description
|
||||
|
||||
msg_ok "Completed successfully!\n"
|
||||
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
|
||||
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:3000${CL}"
|
||||
@@ -75,10 +75,31 @@ if [ -f \$pg_config_file ]; then
|
||||
fi
|
||||
fi
|
||||
systemctl restart postgresql
|
||||
sudo -u postgres psql -c "ALTER USER tracearr WITH SUPERUSER;"
|
||||
EOF
|
||||
chmod +x /data/tracearr/prestart.sh
|
||||
msg_ok "Updated prestart script"
|
||||
|
||||
# check if tailscale is installed
|
||||
if command -v tailscale >/dev/null 2>&1; then
|
||||
# Tracearr runs tailscaled in user mode, disable the service.
|
||||
$STD systemctl disable --now tailscaled
|
||||
$STD systemctl stop tailscaled
|
||||
msg_ok "Tailscale already installed"
|
||||
else
|
||||
msg_info "Installing tailscale"
|
||||
setup_deb822_repo \
|
||||
"tailscale" \
|
||||
"https://pkgs.tailscale.com/stable/$(get_os_info id)/$(get_os_info codename).noarmor.gpg" \
|
||||
"https://pkgs.tailscale.com/stable/$(get_os_info id)/" \
|
||||
"$(get_os_info codename)"
|
||||
$STD apt install -y tailscale
|
||||
# Tracearr runs tailscaled in user mode, disable the service.
|
||||
$STD systemctl disable --now tailscaled
|
||||
$STD systemctl stop tailscaled
|
||||
msg_ok "Installed tailscale"
|
||||
fi
|
||||
|
||||
if check_for_gh_release "tracearr" "connorgallopo/Tracearr"; then
|
||||
msg_info "Stopping Services"
|
||||
systemctl stop tracearr postgresql redis
|
||||
@@ -122,6 +143,8 @@ EOF
|
||||
sed -i "s/^APP_VERSION=.*/APP_VERSION=$(cat /root/.tracearr)/" /data/tracearr/.env
|
||||
chmod 600 /data/tracearr/.env
|
||||
chown -R tracearr:tracearr /data/tracearr
|
||||
mkdir -p /data/backup
|
||||
chown -R tracearr:tracearr /data/backup
|
||||
msg_ok "Configured Tracearr"
|
||||
|
||||
msg_info "Starting services"
|
||||
|
||||
@@ -34,14 +34,14 @@ function update_script() {
|
||||
msg_warn "This requires MANUAL config changes in /etc/vikunja/config.yml."
|
||||
msg_warn "See: https://vikunja.io/changelog/whats-new-in-vikunja-1.0.0/#config-changes"
|
||||
|
||||
read -rp "Continue with update? (y to proceed): " -t 30 CONFIRM1 || exit 1
|
||||
read -rp "Continue with update? (y to proceed): " -t 30 CONFIRM1 || exit 254
|
||||
[[ "$CONFIRM1" =~ ^[yY]$ ]] || exit 0
|
||||
|
||||
echo
|
||||
msg_warn "Vikunja may not start after the update until you manually adjust the config."
|
||||
msg_warn "Details: https://vikunja.io/changelog/whats-new-in-vikunja-1.0.0/#config-changes"
|
||||
|
||||
read -rp "Acknowledge and continue? (y): " -t 30 CONFIRM2 || exit 1
|
||||
read -rp "Acknowledge and continue? (y): " -t 30 CONFIRM2 || exit 254
|
||||
[[ "$CONFIRM2" =~ ^[yY]$ ]] || exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
{
|
||||
"name": "Alpine-Tinyauth",
|
||||
"slug": "alpine-tinyauth",
|
||||
"categories": [
|
||||
6
|
||||
],
|
||||
"date_created": "2025-05-06",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 3000,
|
||||
"documentation": "https://tinyauth.app",
|
||||
"config_path": "/opt/tinyauth/.env",
|
||||
"website": "https://tinyauth.app",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/tinyauth.webp",
|
||||
"description": "Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic provider to all of your docker apps.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/alpine-tinyauth.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 256,
|
||||
"hdd": 2,
|
||||
"os": "alpine",
|
||||
"version": "3.23"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "alpine",
|
||||
"script": "ct/alpine-tinyauth.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 256,
|
||||
"hdd": 2,
|
||||
"os": "alpine",
|
||||
"version": "3.23"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "The default credentials are located in `/opt/tinyauth/credentials.txt`.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generated": "2026-03-02T12:12:21Z",
|
||||
"generated": "2026-03-03T06:17:56Z",
|
||||
"versions": [
|
||||
{
|
||||
"slug": "2fauth",
|
||||
@@ -200,9 +200,9 @@
|
||||
{
|
||||
"slug": "cleanuparr",
|
||||
"repo": "Cleanuparr/Cleanuparr",
|
||||
"version": "v2.7.6",
|
||||
"version": "v2.7.7",
|
||||
"pinned": false,
|
||||
"date": "2026-02-27T19:32:02Z"
|
||||
"date": "2026-03-02T13:08:32Z"
|
||||
},
|
||||
{
|
||||
"slug": "cloudreve",
|
||||
@@ -291,9 +291,9 @@
|
||||
{
|
||||
"slug": "dispatcharr",
|
||||
"repo": "Dispatcharr/Dispatcharr",
|
||||
"version": "v0.20.1",
|
||||
"version": "v0.20.2",
|
||||
"pinned": false,
|
||||
"date": "2026-02-26T21:38:19Z"
|
||||
"date": "2026-03-03T01:40:33Z"
|
||||
},
|
||||
{
|
||||
"slug": "docmost",
|
||||
@@ -382,9 +382,9 @@
|
||||
{
|
||||
"slug": "firefly",
|
||||
"repo": "firefly-iii/firefly-iii",
|
||||
"version": "v6.5.1",
|
||||
"version": "v6.5.2",
|
||||
"pinned": false,
|
||||
"date": "2026-02-27T20:55:55Z"
|
||||
"date": "2026-03-03T05:42:27Z"
|
||||
},
|
||||
{
|
||||
"slug": "fladder",
|
||||
@@ -424,9 +424,9 @@
|
||||
{
|
||||
"slug": "frigate",
|
||||
"repo": "blakeblackshear/frigate",
|
||||
"version": "v0.16.4",
|
||||
"version": "v0.17.0",
|
||||
"pinned": true,
|
||||
"date": "2026-01-29T00:42:14Z"
|
||||
"date": "2026-02-27T03:03:01Z"
|
||||
},
|
||||
{
|
||||
"slug": "gatus",
|
||||
@@ -585,9 +585,9 @@
|
||||
{
|
||||
"slug": "immich-public-proxy",
|
||||
"repo": "alangrainger/immich-public-proxy",
|
||||
"version": "v1.15.3",
|
||||
"version": "v1.15.4",
|
||||
"pinned": false,
|
||||
"date": "2026-02-16T22:54:27Z"
|
||||
"date": "2026-03-02T21:28:06Z"
|
||||
},
|
||||
{
|
||||
"slug": "inspircd",
|
||||
@@ -613,9 +613,9 @@
|
||||
{
|
||||
"slug": "jackett",
|
||||
"repo": "Jackett/Jackett",
|
||||
"version": "v0.24.1247",
|
||||
"version": "v0.24.1261",
|
||||
"pinned": false,
|
||||
"date": "2026-03-02T05:56:37Z"
|
||||
"date": "2026-03-03T05:54:20Z"
|
||||
},
|
||||
{
|
||||
"slug": "jellystat",
|
||||
@@ -872,9 +872,9 @@
|
||||
{
|
||||
"slug": "metube",
|
||||
"repo": "alexta69/metube",
|
||||
"version": "2026.02.27",
|
||||
"version": "2026.03.02",
|
||||
"pinned": false,
|
||||
"date": "2026-02-27T11:47:02Z"
|
||||
"date": "2026-03-02T19:19:10Z"
|
||||
},
|
||||
{
|
||||
"slug": "miniflux",
|
||||
@@ -1149,6 +1149,13 @@
|
||||
"pinned": false,
|
||||
"date": "2026-02-23T19:50:48Z"
|
||||
},
|
||||
{
|
||||
"slug": "powerdns",
|
||||
"repo": "poweradmin/poweradmin",
|
||||
"version": "v4.0.7",
|
||||
"pinned": false,
|
||||
"date": "2026-02-15T20:09:48Z"
|
||||
},
|
||||
{
|
||||
"slug": "privatebin",
|
||||
"repo": "PrivateBin/PrivateBin",
|
||||
@@ -1222,9 +1229,9 @@
|
||||
{
|
||||
"slug": "pulse",
|
||||
"repo": "rcourtman/Pulse",
|
||||
"version": "v5.1.16",
|
||||
"version": "v5.1.17",
|
||||
"pinned": false,
|
||||
"date": "2026-03-01T23:13:09Z"
|
||||
"date": "2026-03-02T20:15:31Z"
|
||||
},
|
||||
{
|
||||
"slug": "pve-scripts-local",
|
||||
@@ -1348,9 +1355,9 @@
|
||||
{
|
||||
"slug": "scanopy",
|
||||
"repo": "scanopy/scanopy",
|
||||
"version": "v0.14.10",
|
||||
"version": "v0.14.11",
|
||||
"pinned": false,
|
||||
"date": "2026-02-28T21:05:12Z"
|
||||
"date": "2026-03-02T08:48:42Z"
|
||||
},
|
||||
{
|
||||
"slug": "scraparr",
|
||||
@@ -1726,9 +1733,9 @@
|
||||
{
|
||||
"slug": "wealthfolio",
|
||||
"repo": "afadil/wealthfolio",
|
||||
"version": "v3.0.0",
|
||||
"version": "v3.0.2",
|
||||
"pinned": false,
|
||||
"date": "2026-02-24T22:37:05Z"
|
||||
"date": "2026-03-03T05:01:49Z"
|
||||
},
|
||||
{
|
||||
"slug": "web-check",
|
||||
|
||||
40
frontend/public/json/powerdns.json
Normal file
40
frontend/public/json/powerdns.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "PowerDNS",
|
||||
"slug": "powerdns",
|
||||
"categories": [
|
||||
5
|
||||
],
|
||||
"date_created": "2026-03-02",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 80,
|
||||
"documentation": "https://doc.powerdns.com/index.html",
|
||||
"config_path": "/opt/poweradmin/config/settings.php",
|
||||
"website": "https://www.powerdns.com/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/powerdns.webp",
|
||||
"description": "The PowerDNS Authoritative Server is a versatile nameserver which supports a large number of backends. These backends can either be plain zone files or be more dynamic in nature. PowerDNS has the concepts of ‘backends’. A backend is a datastore that the server will consult that contains DNS records (and some metadata). The backends range from database backends (MySQL, PostgreSQL) and BIND zone files to co-processes and JSON API’s.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/powerdns.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 1024,
|
||||
"hdd": 4,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "For administrator credentials type: `cat ~/poweradmin.creds` inside LXC.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
51
frontend/public/json/tinyauth.json
Normal file
51
frontend/public/json/tinyauth.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "Tinyauth",
|
||||
"slug": "tinyauth",
|
||||
"categories": [
|
||||
6
|
||||
],
|
||||
"date_created": "2025-05-06",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 3000,
|
||||
"documentation": "https://tinyauth.app",
|
||||
"config_path": "/opt/tinyauth/.env",
|
||||
"website": "https://tinyauth.app",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/tinyauth.webp",
|
||||
"description": "Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic provider to all of your docker apps.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/tinyauth.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 4,
|
||||
"os": "debian",
|
||||
"version": "13"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "alpine",
|
||||
"script": "ct/alpine-tinyauth.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 256,
|
||||
"hdd": 2,
|
||||
"os": "alpine",
|
||||
"version": "3.23"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "The default credentials are located in `/opt/tinyauth/credentials.txt`.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,42 +2,68 @@
|
||||
|
||||
import type { z } from "zod";
|
||||
|
||||
import { githubGist, nord } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||
import { CalendarIcon, Check, Clipboard, Download } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import { useTheme } from "next-themes";
|
||||
import { format } from "date-fns";
|
||||
import { toast } from "sonner";
|
||||
import Image from "next/image";
|
||||
|
||||
import type { Category } from "@/lib/types";
|
||||
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { basePath } from "@/config/site-config";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { fetchCategories } from "@/lib/data";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import type { Script } from "./_schemas/schemas";
|
||||
|
||||
import { ScriptItem } from "../scripts/_components/script-item";
|
||||
import InstallMethod from "./_components/install-method";
|
||||
import { ScriptSchema } from "./_schemas/schemas";
|
||||
import Categories from "./_components/categories";
|
||||
import Note from "./_components/note";
|
||||
|
||||
import { githubGist, nord } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import { ScriptItem } from "../scripts/_components/script-item";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { search } from "@/components/command-menu";
|
||||
import { basePath } from "@/config/site-config";
|
||||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
function search(scripts: Script[], query: string): Script[] {
|
||||
const queryLower = query.toLowerCase().trim();
|
||||
const searchWords = queryLower.split(/\s+/).filter(Boolean);
|
||||
|
||||
return scripts
|
||||
.map((script) => {
|
||||
const nameLower = script.name.toLowerCase();
|
||||
const descriptionLower = (script.description || "").toLowerCase();
|
||||
|
||||
let score = 0;
|
||||
|
||||
for (const word of searchWords) {
|
||||
if (nameLower.includes(word)) {
|
||||
score += 10;
|
||||
}
|
||||
if (descriptionLower.includes(word)) {
|
||||
score += 5;
|
||||
}
|
||||
}
|
||||
|
||||
return { script, score };
|
||||
})
|
||||
.filter(({ score }) => score > 0)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 20)
|
||||
.map(({ script }) => script);
|
||||
}
|
||||
|
||||
const initialScript: Script = {
|
||||
name: "",
|
||||
@@ -77,32 +103,32 @@ export default function JSONGenerator() {
|
||||
|
||||
const selectedCategoryObj = useMemo(
|
||||
() => categories.find(cat => cat.id.toString() === selectedCategory),
|
||||
[categories, selectedCategory]
|
||||
[categories, selectedCategory],
|
||||
);
|
||||
|
||||
const allScripts = useMemo(
|
||||
() => categories.flatMap(cat => cat.scripts || []),
|
||||
[categories]
|
||||
[categories],
|
||||
);
|
||||
|
||||
const scripts = useMemo(() => {
|
||||
const query = searchQuery.trim()
|
||||
const query = searchQuery.trim();
|
||||
|
||||
if (query) {
|
||||
return search(allScripts, query)
|
||||
return search(allScripts, query);
|
||||
}
|
||||
|
||||
if (selectedCategoryObj) {
|
||||
return selectedCategoryObj.scripts || []
|
||||
return selectedCategoryObj.scripts || [];
|
||||
}
|
||||
|
||||
return []
|
||||
return [];
|
||||
}, [allScripts, selectedCategoryObj, searchQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCategories()
|
||||
.then(setCategories)
|
||||
.catch((error) => console.error("Error fetching categories:", error));
|
||||
.catch(error => console.error("Error fetching categories:", error));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -122,11 +148,14 @@ export default function JSONGenerator() {
|
||||
|
||||
if (updated.type === "pve") {
|
||||
scriptPath = `tools/pve/${updated.slug}.sh`;
|
||||
} else if (updated.type === "addon") {
|
||||
}
|
||||
else if (updated.type === "addon") {
|
||||
scriptPath = `tools/addon/${updated.slug}.sh`;
|
||||
} else if (method.type === "alpine") {
|
||||
}
|
||||
else if (method.type === "alpine") {
|
||||
scriptPath = `${updated.type}/alpine-${updated.slug}.sh`;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
scriptPath = `${updated.type}/${updated.slug}.sh`;
|
||||
}
|
||||
|
||||
@@ -145,11 +174,13 @@ export default function JSONGenerator() {
|
||||
}, []);
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
if (!isValid) toast.warning("JSON schema is invalid. Copying anyway.");
|
||||
if (!isValid)
|
||||
toast.warning("JSON schema is invalid. Copying anyway.");
|
||||
navigator.clipboard.writeText(JSON.stringify(script, null, 2));
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
if (isValid) toast.success("Copied metadata to clipboard");
|
||||
if (isValid)
|
||||
toast.success("Copied metadata to clipboard");
|
||||
}, [script]);
|
||||
|
||||
const importScript = (script: Script) => {
|
||||
@@ -166,11 +197,11 @@ export default function JSONGenerator() {
|
||||
setIsValid(true);
|
||||
setZodErrors(null);
|
||||
toast.success("Imported JSON successfully");
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
toast.error("Failed to read or parse the JSON file.");
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileImport = useCallback(() => {
|
||||
const input = document.createElement("input");
|
||||
@@ -180,7 +211,8 @@ export default function JSONGenerator() {
|
||||
input.onchange = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const file = target.files?.[0];
|
||||
if (!file) return;
|
||||
if (!file)
|
||||
return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
@@ -189,7 +221,8 @@ export default function JSONGenerator() {
|
||||
const parsed = JSON.parse(content);
|
||||
importScript(parsed);
|
||||
toast.success("Imported JSON successfully");
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
toast.error("Failed to read the JSON file.");
|
||||
}
|
||||
};
|
||||
@@ -243,7 +276,10 @@ export default function JSONGenerator() {
|
||||
<div className="mt-2 space-y-1">
|
||||
{zodErrors.issues.map((error, index) => (
|
||||
<AlertDescription key={index} className="p-1 text-red-500">
|
||||
{error.path.join(".")} -{error.message}
|
||||
{error.path.join(".")}
|
||||
{" "}
|
||||
-
|
||||
{error.message}
|
||||
</AlertDescription>
|
||||
))}
|
||||
</div>
|
||||
@@ -270,7 +306,7 @@ export default function JSONGenerator() {
|
||||
onOpenChange={setIsImportDialogOpen}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
<DropdownMenuItem onSelect={e => e.preventDefault()}>
|
||||
Import existing script
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
@@ -292,7 +328,7 @@ export default function JSONGenerator() {
|
||||
<SelectValue placeholder="Category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map((category) => (
|
||||
{categories.map(category => (
|
||||
<SelectItem key={category.id} value={category.id.toString()}>
|
||||
{category.name}
|
||||
</SelectItem>
|
||||
@@ -302,40 +338,44 @@ export default function JSONGenerator() {
|
||||
<Input
|
||||
placeholder="Search for a script..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
{!selectedCategory && !searchQuery ? (
|
||||
<p className="text-muted-foreground text-sm text-center">
|
||||
Select a category or search for a script
|
||||
</p>
|
||||
) : scripts.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm text-center">
|
||||
No scripts found
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 auto-rows-min h-64 overflow-y-auto gap-4">
|
||||
{scripts.map(script => (
|
||||
<div
|
||||
key={script.slug}
|
||||
className="p-2 border rounded cursor-pointer hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => {
|
||||
importScript(script);
|
||||
setIsImportDialogOpen(false);
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={script.logo || `/${basePath}/logo.png`}
|
||||
alt={script.name}
|
||||
className="w-full h-12 object-contain mb-2"
|
||||
width={16}
|
||||
height={16}
|
||||
unoptimized
|
||||
/>
|
||||
<p className="text-sm text-center">{script.name}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!selectedCategory && !searchQuery
|
||||
? (
|
||||
<p className="text-muted-foreground text-sm text-center">
|
||||
Select a category or search for a script
|
||||
</p>
|
||||
)
|
||||
: scripts.length === 0
|
||||
? (
|
||||
<p className="text-muted-foreground text-sm text-center">
|
||||
No scripts found
|
||||
</p>
|
||||
)
|
||||
: (
|
||||
<div className="grid grid-cols-3 auto-rows-min h-64 overflow-y-auto gap-4">
|
||||
{scripts.map(script => (
|
||||
<div
|
||||
key={script.slug}
|
||||
className="p-2 border rounded cursor-pointer hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => {
|
||||
importScript(script);
|
||||
setIsImportDialogOpen(false);
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={script.logo || `/${basePath}/logo.png`}
|
||||
alt={script.name}
|
||||
className="w-full h-12 object-contain mb-2"
|
||||
width={16}
|
||||
height={16}
|
||||
unoptimized
|
||||
/>
|
||||
<p className="text-sm text-center">{script.name}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
@@ -348,15 +388,19 @@ export default function JSONGenerator() {
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>
|
||||
Name <span className="text-red-500">*</span>
|
||||
Name
|
||||
{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input placeholder="Example" value={script.name} onChange={(e) => updateScript("name", e.target.value)} />
|
||||
<Input placeholder="Example" value={script.name} onChange={e => updateScript("name", e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>
|
||||
Slug <span className="text-red-500">*</span>
|
||||
Slug
|
||||
{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input placeholder="example" value={script.slug} onChange={(e) => updateScript("slug", e.target.value)} />
|
||||
<Input placeholder="example" value={script.slug} onChange={e => updateScript("slug", e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -366,7 +410,7 @@ export default function JSONGenerator() {
|
||||
<Input
|
||||
placeholder="Full logo URL"
|
||||
value={script.logo || ""}
|
||||
onChange={(e) => updateScript("logo", e.target.value || null)}
|
||||
onChange={e => updateScript("logo", e.target.value || null)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -374,24 +418,28 @@ export default function JSONGenerator() {
|
||||
<Input
|
||||
placeholder="Path to config file"
|
||||
value={script.config_path || ""}
|
||||
onChange={(e) => updateScript("config_path", e.target.value || "")}
|
||||
onChange={e => updateScript("config_path", e.target.value || "")}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>
|
||||
Description <span className="text-red-500">*</span>
|
||||
Description
|
||||
{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
placeholder="Example"
|
||||
value={script.description}
|
||||
onChange={(e) => updateScript("description", e.target.value)}
|
||||
onChange={e => updateScript("description", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Categories script={script} setScript={setScript} categories={categories} />
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<Label>
|
||||
Date Created <span className="text-red-500">*</span>
|
||||
Date Created
|
||||
{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild className="flex-1">
|
||||
@@ -415,7 +463,7 @@ export default function JSONGenerator() {
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<Label>Type</Label>
|
||||
<Select value={script.type} onValueChange={(value) => updateScript("type", value)}>
|
||||
<Select value={script.type} onValueChange={value => updateScript("type", value)}>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue placeholder="Type" />
|
||||
</SelectTrigger>
|
||||
@@ -430,17 +478,17 @@ export default function JSONGenerator() {
|
||||
</div>
|
||||
<div className="w-full flex gap-5">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch checked={script.updateable} onCheckedChange={(checked) => updateScript("updateable", checked)} />
|
||||
<Switch checked={script.updateable} onCheckedChange={checked => updateScript("updateable", checked)} />
|
||||
<label>Updateable</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch checked={script.privileged} onCheckedChange={(checked) => updateScript("privileged", checked)} />
|
||||
<Switch checked={script.privileged} onCheckedChange={checked => updateScript("privileged", checked)} />
|
||||
<label>Privileged</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={script.disable || false}
|
||||
onCheckedChange={(checked) => updateScript("disable", checked)}
|
||||
onCheckedChange={checked => updateScript("disable", checked)}
|
||||
/>
|
||||
<label>Disabled</label>
|
||||
</div>
|
||||
@@ -448,12 +496,14 @@ export default function JSONGenerator() {
|
||||
{script.disable && (
|
||||
<div>
|
||||
<Label>
|
||||
Disable Description <span className="text-red-500">*</span>
|
||||
Disable Description
|
||||
{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
placeholder="Explain why this script is disabled..."
|
||||
value={script.disable_description || ""}
|
||||
onChange={(e) => updateScript("disable_description", e.target.value)}
|
||||
onChange={e => updateScript("disable_description", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -461,18 +511,18 @@ export default function JSONGenerator() {
|
||||
placeholder="Interface Port"
|
||||
type="number"
|
||||
value={script.interface_port || ""}
|
||||
onChange={(e) => updateScript("interface_port", e.target.value ? Number(e.target.value) : null)}
|
||||
onChange={e => updateScript("interface_port", e.target.value ? Number(e.target.value) : null)}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Website URL"
|
||||
value={script.website || ""}
|
||||
onChange={(e) => updateScript("website", e.target.value || null)}
|
||||
onChange={e => updateScript("website", e.target.value || null)}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Documentation URL"
|
||||
value={script.documentation || ""}
|
||||
onChange={(e) => updateScript("documentation", e.target.value || null)}
|
||||
onChange={e => updateScript("documentation", e.target.value || null)}
|
||||
/>
|
||||
</div>
|
||||
<InstallMethod script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
||||
@@ -480,22 +530,20 @@ export default function JSONGenerator() {
|
||||
<Input
|
||||
placeholder="Username"
|
||||
value={script.default_credentials.username || ""}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
updateScript("default_credentials", {
|
||||
...script.default_credentials,
|
||||
username: e.target.value || null,
|
||||
})
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Password"
|
||||
value={script.default_credentials.password || ""}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
updateScript("default_credentials", {
|
||||
...script.default_credentials,
|
||||
password: e.target.value || null,
|
||||
})
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<Note script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
||||
</form>
|
||||
@@ -504,7 +552,7 @@ export default function JSONGenerator() {
|
||||
<Tabs
|
||||
defaultValue="json"
|
||||
className="w-full"
|
||||
onValueChange={(value) => setCurrentTab(value as "json" | "preview")}
|
||||
onValueChange={value => setCurrentTab(value as "json" | "preview")}
|
||||
value={currentTab}
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ArrowRightIcon, Sparkles } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
import type { Category, Script } from "@/lib/types";
|
||||
@@ -21,35 +22,6 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/t
|
||||
import { DialogTitle } from "./ui/dialog";
|
||||
import { Button } from "./ui/button";
|
||||
import { Badge } from "./ui/badge";
|
||||
import Link from "next/link";
|
||||
|
||||
export function search(scripts: Script[], query: string): Script[] {
|
||||
const queryLower = query.toLowerCase().trim();
|
||||
const searchWords = queryLower.split(/\s+/).filter(Boolean);
|
||||
|
||||
return scripts
|
||||
.map(script => {
|
||||
const nameLower = script.name.toLowerCase();
|
||||
const descriptionLower = (script.description || "").toLowerCase();
|
||||
|
||||
let score = 0;
|
||||
|
||||
for (const word of searchWords) {
|
||||
if (nameLower.includes(word)) {
|
||||
score += 10;
|
||||
}
|
||||
if (descriptionLower.includes(word)) {
|
||||
score += 5;
|
||||
}
|
||||
}
|
||||
|
||||
return { script, score };
|
||||
})
|
||||
.filter(({ score }) => score > 0)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 20)
|
||||
.map(({ script }) => script);
|
||||
}
|
||||
|
||||
export function formattedBadge(type: string) {
|
||||
switch (type) {
|
||||
@@ -79,11 +51,9 @@ function getRandomScript(categories: Category[], previouslySelected: Set<string>
|
||||
}
|
||||
|
||||
function CommandMenu() {
|
||||
const [query, setQuery] = React.useState("");
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [links, setLinks] = React.useState<Category[]>([]);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [results, setResults] = React.useState<Script[]>([]);
|
||||
const [selectedScripts, setSelectedScripts] = React.useState<Set<string>>(new Set());
|
||||
const router = useRouter();
|
||||
|
||||
@@ -100,27 +70,6 @@ function CommandMenu() {
|
||||
});
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (query.trim() === "") {
|
||||
fetchSortedCategories();
|
||||
}
|
||||
else {
|
||||
const scriptMap = new Map<string, Script>();
|
||||
|
||||
for (const category of links) {
|
||||
for (const script of category.scripts || []) {
|
||||
if (!scriptMap.has(script.slug)) {
|
||||
scriptMap.set(script.slug, script);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueScripts = Array.from(scriptMap.values());
|
||||
const filteredResults = search(uniqueScripts, query);
|
||||
setResults(filteredResults);
|
||||
}
|
||||
}, [query]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||
@@ -248,46 +197,49 @@ function CommandMenu() {
|
||||
|
||||
<CommandDialog
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
setOpen(open);
|
||||
if (open) {
|
||||
setQuery("");
|
||||
setResults([]);
|
||||
}
|
||||
onOpenChange={setOpen}
|
||||
filter={(value: string, search: string) => {
|
||||
const searchLower = search.toLowerCase().trim();
|
||||
if (!searchLower)
|
||||
return 1;
|
||||
const valueLower = value.toLowerCase();
|
||||
const searchWords = searchLower.split(/\s+/).filter(Boolean);
|
||||
// All search words must appear somewhere in the value (name + description)
|
||||
const allWordsMatch = searchWords.every((word: string) => valueLower.includes(word));
|
||||
return allWordsMatch ? 1 : 0;
|
||||
}}
|
||||
>
|
||||
<DialogTitle className="sr-only">Search scripts</DialogTitle>
|
||||
<CommandInput
|
||||
placeholder="Search for a script..."
|
||||
onValueChange={setQuery}
|
||||
value={query}
|
||||
/>
|
||||
<CommandInput placeholder="Search for a script..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{isLoading ? (
|
||||
"Searching..."
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-6 text-center">
|
||||
<p className="text-sm text-muted-foreground">No scripts match your search.</p>
|
||||
<div className="mt-4">
|
||||
<p className="text-xs text-muted-foreground mb-2">Want to add a new script?</p>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link
|
||||
href={`https://github.com/community-scripts/${basePath}/tree/main/docs/contribution/GUIDE.md`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation <ArrowRightIcon className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isLoading
|
||||
? (
|
||||
"Searching..."
|
||||
)
|
||||
: (
|
||||
<div className="flex flex-col items-center justify-center py-6 text-center">
|
||||
<p className="text-sm text-muted-foreground">No scripts match your search.</p>
|
||||
<div className="mt-4">
|
||||
<p className="text-xs text-muted-foreground mb-2">Want to add a new script?</p>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link
|
||||
href={`https://github.com/community-scripts/${basePath}/tree/main/docs/contribution/GUIDE.md`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation
|
||||
{" "}
|
||||
<ArrowRightIcon className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CommandEmpty>
|
||||
|
||||
{results.length > 0 ? (
|
||||
<CommandGroup heading="Search Results">
|
||||
{results.map(script => (
|
||||
{Object.entries(uniqueScriptsByCategory).map(([categoryName, scripts]) => (
|
||||
<CommandGroup key={`category:${categoryName}`} heading={categoryName}>
|
||||
{scripts.map(script => (
|
||||
<CommandItem
|
||||
key={`script:${script.slug}`}
|
||||
value={`${script.name} ${script.type} ${script.description || ""}`}
|
||||
@@ -320,44 +272,7 @@ function CommandMenu() {
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
) : ( // When no search results, show all scripts grouped by category
|
||||
Object.entries(uniqueScriptsByCategory).map(([categoryName, scripts]) => (
|
||||
<CommandGroup key={`category:${categoryName}`} heading={categoryName}>
|
||||
{scripts.map(script => (
|
||||
<CommandItem
|
||||
key={`script:${script.slug}`}
|
||||
value={`${script.name} ${script.type} ${script.description || ""}`}
|
||||
onSelect={() => {
|
||||
setOpen(false);
|
||||
router.push(`/scripts?id=${script.slug}`);
|
||||
}}
|
||||
tabIndex={0}
|
||||
aria-label={`Open script ${script.name}`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
setOpen(false);
|
||||
router.push(`/scripts?id=${script.slug}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-2" onClick={() => setOpen(false)}>
|
||||
<Image
|
||||
src={script.logo || `/${basePath}/logo.png`}
|
||||
onError={e => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
||||
unoptimized
|
||||
width={16}
|
||||
height={16}
|
||||
alt=""
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
<span>{script.name}</span>
|
||||
<span>{formattedBadge(script.type)}</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))
|
||||
)}
|
||||
))}
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
</>
|
||||
|
||||
@@ -14,7 +14,7 @@ network_check
|
||||
update_os
|
||||
|
||||
read -r -p "${TAB3}Enter PostgreSQL version (15/16/17): " ver
|
||||
[[ $ver =~ ^(15|16|17)$ ]] || { echo "Invalid version"; exit 1; }
|
||||
[[ $ver =~ ^(15|16|17)$ ]] || { echo "Invalid version"; exit 64; }
|
||||
|
||||
msg_info "Installing PostgreSQL ${ver}"
|
||||
$STD apk add --no-cache postgresql${ver} postgresql${ver}-contrib postgresql${ver}-openrc sudo
|
||||
|
||||
@@ -36,9 +36,9 @@ msg_ok "Installed Tinyauth"
|
||||
read -r -p "${TAB3}Enter your Tinyauth subdomain (e.g. https://tinyauth.example.com): " app_url
|
||||
|
||||
cat <<EOF >/opt/tinyauth/.env
|
||||
DATABASE_PATH=/opt/tinyauth/database.db
|
||||
USERS='${USER}'
|
||||
APP_URL=${app_url}
|
||||
TINYAUTH_DATABASE_PATH=/opt/tinyauth/database.db
|
||||
TINYAUTH_AUTH_USERS='${USER}'
|
||||
TINYAUTH_APPURL=${app_url}
|
||||
EOF
|
||||
|
||||
msg_info "Creating Service"
|
||||
|
||||
@@ -25,7 +25,7 @@ case $version in
|
||||
;;
|
||||
*)
|
||||
msg_error "Invalid JDK version selected. Please enter 8, 11, 17 or 21."
|
||||
exit 1
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
@@ -39,7 +39,7 @@ case $version in
|
||||
;;
|
||||
*)
|
||||
msg_error "Invalid JDK version selected. Please enter 11, 17 or 21."
|
||||
exit 1
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
@@ -53,13 +53,13 @@ case $version in
|
||||
;;
|
||||
*)
|
||||
msg_error "Invalid JDK version selected. Please enter 17 or 21."
|
||||
exit 1
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
msg_error "Invalid Tomcat version selected. Please enter 9, 10.1 or 11."
|
||||
exit 1
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ mkdir -p /opt/booklore/dist
|
||||
JAR_PATH=$(find /opt/booklore/booklore-api/build/libs -maxdepth 1 -type f -name "booklore-api-*.jar" ! -name "*plain*" | head -n1)
|
||||
if [[ -z "$JAR_PATH" ]]; then
|
||||
msg_error "Backend JAR not found"
|
||||
exit 1
|
||||
exit 153
|
||||
fi
|
||||
cp "$JAR_PATH" /opt/booklore/dist/app.jar
|
||||
msg_ok "Built Backend"
|
||||
|
||||
@@ -86,7 +86,7 @@ EOF
|
||||
msg_ok "Docker TCP socket available on $socket"
|
||||
else
|
||||
msg_error "Docker failed to restart. Check journalctl -xeu docker.service"
|
||||
exit 1
|
||||
exit 150
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ msg_info "Fetching latest EMQX Enterprise version"
|
||||
LATEST_VERSION=$(curl -fsSL https://www.emqx.com/en/downloads/enterprise | grep -oP '/en/downloads/enterprise/v\K[0-9]+\.[0-9]+\.[0-9]+' | sort -V | tail -n1)
|
||||
if [[ -z "$LATEST_VERSION" ]]; then
|
||||
msg_error "Failed to determine latest EMQX version"
|
||||
exit 1
|
||||
exit 250
|
||||
fi
|
||||
msg_ok "Latest version: v$LATEST_VERSION"
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Authors: MickLesk (CanbiZ)
|
||||
# Co-Authors: remz1337
|
||||
# Authors: MickLesk (CanbiZ) | Co-Authors: remz1337
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://frigate.video/ | Github: https://github.com/blakeblackshear/frigate
|
||||
|
||||
@@ -17,7 +16,7 @@ update_os
|
||||
source /etc/os-release
|
||||
if [[ "$VERSION_ID" != "12" ]]; then
|
||||
msg_error "Frigate requires Debian 12 (Bookworm) due to Python 3.11 dependencies"
|
||||
exit 1
|
||||
exit 238
|
||||
fi
|
||||
|
||||
msg_info "Converting APT sources to DEB822 format"
|
||||
@@ -85,6 +84,7 @@ $STD apt install -y \
|
||||
tclsh \
|
||||
libopenblas-dev \
|
||||
liblapack-dev \
|
||||
libgomp1 \
|
||||
make \
|
||||
moreutils
|
||||
msg_ok "Installed Dependencies"
|
||||
@@ -101,9 +101,16 @@ export NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
|
||||
export TOKENIZERS_PARALLELISM=true
|
||||
export TRANSFORMERS_NO_ADVISORY_WARNINGS=1
|
||||
export OPENCV_FFMPEG_LOGLEVEL=8
|
||||
export PYTHONWARNINGS="ignore:::numpy.core.getlimits"
|
||||
export HAILORT_LOGGER_PATH=NONE
|
||||
export TF_CPP_MIN_LOG_LEVEL=3
|
||||
export TF_CPP_MIN_VLOG_LEVEL=3
|
||||
export TF_ENABLE_ONEDNN_OPTS=0
|
||||
export AUTOGRAPH_VERBOSITY=0
|
||||
export GLOG_minloglevel=3
|
||||
export GLOG_logtostderr=0
|
||||
|
||||
fetch_and_deploy_gh_release "frigate" "blakeblackshear/frigate" "tarball" "v0.16.4" "/opt/frigate"
|
||||
fetch_and_deploy_gh_release "frigate" "blakeblackshear/frigate" "tarball" "v0.17.0" "/opt/frigate"
|
||||
|
||||
msg_info "Building Nginx"
|
||||
$STD bash /opt/frigate/docker/main/build_nginx.sh
|
||||
@@ -138,13 +145,19 @@ install -c -m 644 libusb-1.0.pc /usr/local/lib/pkgconfig
|
||||
ldconfig
|
||||
msg_ok "Built libUSB"
|
||||
|
||||
msg_info "Bootstrapping pip"
|
||||
wget -q https://bootstrap.pypa.io/get-pip.py -O /tmp/get-pip.py
|
||||
sed -i 's/args.append("setuptools")/args.append("setuptools==77.0.3")/' /tmp/get-pip.py
|
||||
$STD python3 /tmp/get-pip.py "pip"
|
||||
rm -f /tmp/get-pip.py
|
||||
msg_ok "Bootstrapped pip"
|
||||
|
||||
msg_info "Installing Python Dependencies"
|
||||
$STD pip3 install -r /opt/frigate/docker/main/requirements.txt
|
||||
msg_ok "Installed Python Dependencies"
|
||||
|
||||
msg_info "Building Python Wheels (Patience)"
|
||||
mkdir -p /wheels
|
||||
sed -i 's|^SQLITE3_VERSION=.*|SQLITE3_VERSION="version-3.46.0"|g' /opt/frigate/docker/main/build_pysqlite3.sh
|
||||
$STD bash /opt/frigate/docker/main/build_pysqlite3.sh
|
||||
for i in {1..3}; do
|
||||
$STD pip3 wheel --wheel-dir=/wheels -r /opt/frigate/docker/main/requirements-wheels.txt --default-timeout=300 --retries=3 && break
|
||||
@@ -152,7 +165,7 @@ for i in {1..3}; do
|
||||
done
|
||||
msg_ok "Built Python Wheels"
|
||||
|
||||
NODE_VERSION="22" NODE_MODULE="yarn" setup_nodejs
|
||||
NODE_VERSION="20" setup_nodejs
|
||||
|
||||
msg_info "Downloading Inference Models"
|
||||
mkdir -p /models /openvino-model
|
||||
@@ -183,6 +196,10 @@ $STD pip3 install -U /wheels/*.whl
|
||||
ldconfig
|
||||
msg_ok "Installed HailoRT Runtime"
|
||||
|
||||
msg_info "Installing MemryX Runtime"
|
||||
$STD bash /opt/frigate/docker/main/install_memryx.sh
|
||||
msg_ok "Installed MemryX Runtime"
|
||||
|
||||
msg_info "Installing OpenVino"
|
||||
$STD pip3 install -r /opt/frigate/docker/main/requirements-ov.txt
|
||||
msg_ok "Installed OpenVino"
|
||||
@@ -209,6 +226,8 @@ $STD make version
|
||||
cd /opt/frigate/web
|
||||
$STD npm install
|
||||
$STD npm run build
|
||||
mv /opt/frigate/web/dist/BASE_PATH/monacoeditorwork/* /opt/frigate/web/dist/assets/
|
||||
rm -rf /opt/frigate/web/dist/BASE_PATH
|
||||
cp -r /opt/frigate/web/dist/* /opt/frigate/web/
|
||||
sed -i '/^s6-svc -O \.$/s/^/#/' /opt/frigate/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/run
|
||||
msg_ok "Built Frigate Application"
|
||||
@@ -224,6 +243,19 @@ echo "tmpfs /tmp/cache tmpfs defaults 0 0" >>/etc/fstab
|
||||
cat <<EOF >/etc/frigate.env
|
||||
DEFAULT_FFMPEG_VERSION="7.0"
|
||||
INCLUDED_FFMPEG_VERSIONS="7.0:5.0"
|
||||
NVIDIA_VISIBLE_DEVICES=all
|
||||
NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
|
||||
TOKENIZERS_PARALLELISM=true
|
||||
TRANSFORMERS_NO_ADVISORY_WARNINGS=1
|
||||
OPENCV_FFMPEG_LOGLEVEL=8
|
||||
PYTHONWARNINGS="ignore:::numpy.core.getlimits"
|
||||
HAILORT_LOGGER_PATH=NONE
|
||||
TF_CPP_MIN_LOG_LEVEL=3
|
||||
TF_CPP_MIN_VLOG_LEVEL=3
|
||||
TF_ENABLE_ONEDNN_OPTS=0
|
||||
AUTOGRAPH_VERBOSITY=0
|
||||
GLOG_minloglevel=3
|
||||
GLOG_logtostderr=0
|
||||
EOF
|
||||
|
||||
cat <<EOF >/config/config.yml
|
||||
@@ -237,7 +269,6 @@ cameras:
|
||||
input_args: -re -stream_loop -1 -fflags +genpts
|
||||
roles:
|
||||
- detect
|
||||
- rtmp
|
||||
detect:
|
||||
height: 1080
|
||||
width: 1920
|
||||
@@ -255,6 +286,7 @@ ffmpeg:
|
||||
detectors:
|
||||
detector01:
|
||||
type: openvino
|
||||
device: AUTO
|
||||
model:
|
||||
width: 300
|
||||
height: 300
|
||||
|
||||
@@ -31,7 +31,7 @@ fi
|
||||
|
||||
if [[ -z "$KASM_URL" ]] || [[ -z "$KASM_VERSION" ]]; then
|
||||
msg_error "Unable to detect latest Kasm release URL."
|
||||
exit 1
|
||||
exit 250
|
||||
fi
|
||||
msg_ok "Detected Kasm Workspaces version $KASM_VERSION"
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ while true; do
|
||||
attempts=$((attempts + 1))
|
||||
if [[ "$attempts" -ge 3 ]]; then
|
||||
msg_error "Maximum attempts reached. Exiting."
|
||||
exit 1
|
||||
exit 254
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -76,11 +76,11 @@ for i in {1..60}; do
|
||||
elif [[ "$STATUS" == "unhealthy" ]]; then
|
||||
msg_error "NPMplus container is unhealthy! Check logs."
|
||||
docker logs "$CONTAINER_ID"
|
||||
exit 1
|
||||
exit 150
|
||||
fi
|
||||
fi
|
||||
sleep 2
|
||||
[[ $i -eq 60 ]] && msg_error "NPMplus container did not become healthy within 120s." && docker logs "$CONTAINER_ID" && exit 1
|
||||
[[ $i -eq 60 ]] && msg_error "NPMplus container did not become healthy within 120s." && docker logs "$CONTAINER_ID" && exit 150
|
||||
done
|
||||
msg_ok "Builded and started NPMplus"
|
||||
|
||||
|
||||
@@ -78,11 +78,11 @@ if curl -fL# -C - -o "$TMP_TAR" "$OLLAMA_URL"; then
|
||||
msg_ok "Installed Ollama ${RELEASE}"
|
||||
else
|
||||
msg_error "Extraction failed – archive corrupt or incomplete"
|
||||
exit 1
|
||||
exit 251
|
||||
fi
|
||||
else
|
||||
msg_error "Download failed – $OLLAMA_URL not reachable"
|
||||
exit 1
|
||||
exit 250
|
||||
fi
|
||||
|
||||
msg_info "Creating ollama User and Group"
|
||||
|
||||
@@ -59,7 +59,7 @@ EOF
|
||||
else
|
||||
msg_error "Failed to download or verify GPG key from $KEY_URL"
|
||||
[[ -f "$TMP_KEY_CONTENT" ]] && rm -f "$TMP_KEY_CONTENT"
|
||||
exit 1
|
||||
exit 250
|
||||
fi
|
||||
rm -f "$TMP_KEY_CONTENT"
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ for server in "${servers[@]}"; do
|
||||
done
|
||||
if ((attempt >= MAX_ATTEMPTS)); then
|
||||
msg_error "No more attempts - aborting script!"
|
||||
exit 1
|
||||
exit 254
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ update_os
|
||||
read -r -p "${TAB3}Enter PostgreSQL version (15/16/17/18): " ver
|
||||
[[ $ver =~ ^(15|16|17|18)$ ]] || {
|
||||
echo "Invalid version"
|
||||
exit 1
|
||||
exit 64
|
||||
}
|
||||
PG_VERSION=$ver setup_postgresql
|
||||
|
||||
|
||||
134
install/powerdns-install.sh
Normal file
134
install/powerdns-install.sh
Normal file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: Slaviša Arežina (tremor021)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://www.powerdns.com/
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
$STD apt install -y sqlite3
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
PHP_VERSION="8.3" PHP_APACHE="YES" PHP_FPM="YES" PHP_MODULE="gettext,tokenizer,sqlite3,ldap" setup_php
|
||||
setup_deb822_repo \
|
||||
"pdns" \
|
||||
"https://repo.powerdns.com/FD380FBB-pub.asc" \
|
||||
"http://repo.powerdns.com/debian" \
|
||||
"trixie-auth-50"
|
||||
|
||||
cat <<EOF >/etc/apt/preferences.d/auth-50
|
||||
Package: pdns-*
|
||||
Pin: origin repo.powerdns.com
|
||||
Pin-Priority: 600
|
||||
EOF
|
||||
|
||||
escape_sql() {
|
||||
printf '%s' "$1" | sed "s/'/''/g"
|
||||
}
|
||||
|
||||
msg_info "Setting up PowerDNS"
|
||||
$STD apt install -y \
|
||||
pdns-server \
|
||||
pdns-backend-sqlite3
|
||||
msg_ok "Setup PowerDNS"
|
||||
|
||||
fetch_and_deploy_gh_release "poweradmin" "poweradmin/poweradmin" "tarball"
|
||||
|
||||
msg_info "Setting up Poweradmin"
|
||||
sqlite3 /opt/poweradmin/powerdns.db </opt/poweradmin/sql/poweradmin-sqlite-db-structure.sql
|
||||
sqlite3 /opt/poweradmin/powerdns.db </opt/poweradmin/sql/pdns/49/schema.sqlite3.sql
|
||||
PA_ADMIN_USERNAME="admin"
|
||||
PA_ADMIN_EMAIL="admin@example.com"
|
||||
PA_ADMIN_FULLNAME="Administrator"
|
||||
PA_ADMIN_PASSWORD=$(openssl rand -base64 16 | tr -d "=+/" | cut -c1-16)
|
||||
PA_SESSION_KEY=$(openssl rand -base64 75 | tr -dc 'A-Za-z0-9^@#!(){}[]%_\-+=~' | head -c 50)
|
||||
PASSWORD_HASH=$(php -r "echo password_hash(\$argv[1], PASSWORD_DEFAULT);" -- "${PA_ADMIN_PASSWORD}" 2>/dev/null)
|
||||
sqlite3 /opt/poweradmin/powerdns.db "INSERT INTO users (username, password, fullname, email, description, perm_templ, active, use_ldap) \
|
||||
VALUES ('$(escape_sql "${PA_ADMIN_USERNAME}")', '$(escape_sql "${PASSWORD_HASH}")', '$(escape_sql "${PA_ADMIN_FULLNAME}")', \
|
||||
'$(escape_sql "${PA_ADMIN_EMAIL}")', 'System Administrator', 1, 1, 0);"
|
||||
|
||||
cat <<EOF >~/poweradmin.creds
|
||||
Admin Username: ${PA_ADMIN_USERNAME}
|
||||
Admin Password: ${PA_ADMIN_PASSWORD}
|
||||
EOF
|
||||
|
||||
cat <<EOF >/opt/poweradmin/config/settings.php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Poweradmin Settings Configuration File
|
||||
*
|
||||
* Generated by the installer on 2026-02-02 21:01:40
|
||||
*/
|
||||
|
||||
return [
|
||||
/**
|
||||
* Database Settings
|
||||
*/
|
||||
'database' => [
|
||||
'type' => 'sqlite',
|
||||
'file' => '/opt/poweradmin/powerdns.db',
|
||||
],
|
||||
|
||||
/**
|
||||
* Security Settings
|
||||
*/
|
||||
'security' => [
|
||||
'session_key' => '${PA_SESSION_KEY}',
|
||||
],
|
||||
|
||||
/**
|
||||
* Interface Settings
|
||||
*/
|
||||
'interface' => [
|
||||
'language' => 'en_EN',
|
||||
],
|
||||
|
||||
/**
|
||||
* DNS Settings
|
||||
*/
|
||||
'dns' => [
|
||||
'hostmaster' => 'localhost.lan',
|
||||
'ns1' => '8.8.8.8',
|
||||
'ns2' => '9.9.9.9',
|
||||
]
|
||||
];
|
||||
EOF
|
||||
rm -rf /opt/poweradmin/install
|
||||
msg_ok "Setup Poweradmin"
|
||||
|
||||
msg_info "Creating Service"
|
||||
rm /etc/apache2/sites-enabled/000-default.conf
|
||||
cat <<EOF >/etc/apache2/sites-enabled/poweradmin.conf
|
||||
<VirtualHost *:80>
|
||||
ServerName localhost
|
||||
DocumentRoot /opt/poweradmin
|
||||
|
||||
<Directory /opt/poweradmin>
|
||||
Options -Indexes +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
# For DDNS update functionality
|
||||
RewriteEngine On
|
||||
RewriteRule ^/update(.*)\$ /dynamic_update.php [L]
|
||||
RewriteRule ^/nic/update(.*)\$ /dynamic_update.php [L]
|
||||
</VirtualHost>
|
||||
EOF
|
||||
$STD a2enmod rewrite headers
|
||||
chown -R www-data:www-data /opt/poweradmin
|
||||
$STD systemctl restart apache2
|
||||
msg_info "Created Service"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
@@ -25,7 +25,7 @@ if useradd -r -m -d /opt/pulse-home -s /usr/sbin/nologin pulse; then
|
||||
msg_ok "Created User"
|
||||
else
|
||||
msg_error "User creation failed"
|
||||
exit 1
|
||||
exit 71
|
||||
fi
|
||||
|
||||
mkdir -p /etc/pulse
|
||||
|
||||
@@ -34,7 +34,7 @@ while true; do
|
||||
[Nn]|[Nn][Oo]|"")
|
||||
msg_error "Terms not accepted. Installation cannot proceed."
|
||||
msg_error "Please review the terms and run the script again if you wish to proceed."
|
||||
exit 1
|
||||
exit 254
|
||||
;;
|
||||
*)
|
||||
msg_error "Invalid response. Please enter 'y' for yes or 'n' for no."
|
||||
@@ -47,7 +47,7 @@ DOWNLOAD_URL=$(curl -s "https://www.splunk.com/en_us/download/splunk-enterprise.
|
||||
RELEASE=$(echo "$DOWNLOAD_URL" | sed 's|.*/releases/\([^/]*\)/.*|\1|')
|
||||
$STD curl -fsSL -o "splunk-enterprise.tgz" "$DOWNLOAD_URL" || {
|
||||
msg_error "Failed to download Splunk Enterprise from the provided link."
|
||||
exit 1
|
||||
exit 250
|
||||
}
|
||||
$STD tar -xzf "splunk-enterprise.tgz" -C /opt
|
||||
rm -f "splunk-enterprise.tgz"
|
||||
|
||||
62
install/tinyauth-install.sh
Normal file
62
install/tinyauth-install.sh
Normal file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/steveiliop56/tinyauth
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
$STD apt install -y \
|
||||
openssl \
|
||||
apache2-utils
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
fetch_and_deploy_gh_release "tinyauth" "steveiliop56/tinyauth" "singlefile" "latest" "/opt/tinyauth" "tinyauth-amd64"
|
||||
|
||||
msg_info "Setting up Tinyauth"
|
||||
PASS=$(openssl rand -base64 8 | tr -dc 'a-zA-Z0-9' | head -c 8)
|
||||
USER=$(htpasswd -Bbn "tinyauth" "${PASS}")
|
||||
cat <<EOF >/opt/tinyauth/credentials.txt
|
||||
Tinyauth Credentials
|
||||
Username: tinyauth
|
||||
Password: ${PASS}
|
||||
EOF
|
||||
msg_ok "Set up Tinyauth"
|
||||
|
||||
read -r -p "${TAB3}Enter your Tinyauth subdomain (e.g. https://tinyauth.example.com): " app_url
|
||||
|
||||
msg_info "Creating Service"
|
||||
cat <<EOF >/opt/tinyauth/.env
|
||||
TINYAUTH_DATABASE_PATH=/opt/tinyauth/database.db
|
||||
TINYAUTH_AUTH_USERS='${USER}'
|
||||
TINYAUTH_APPURL=${app_url}
|
||||
EOF
|
||||
cat <<EOF >/etc/systemd/system/tinyauth.service
|
||||
[Unit]
|
||||
Description=Tinyauth Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
EnvironmentFile=/opt/tinyauth/.env
|
||||
ExecStart=/opt/tinyauth/tinyauth
|
||||
WorkingDirectory=/opt/tinyauth
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now tinyauth
|
||||
msg_ok "Created Service"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
@@ -44,7 +44,20 @@ $STD timescaledb-tune -yes -memory "$ram_for_tsdb"MB
|
||||
$STD systemctl restart postgresql
|
||||
msg_ok "Installed TimescaleDB"
|
||||
|
||||
PG_DB_NAME="tracearr_db" PG_DB_USER="tracearr" PG_DB_EXTENSIONS="timescaledb,timescaledb_toolkit" setup_postgresql_db
|
||||
PG_DB_NAME="tracearr_db" PG_DB_USER="tracearr" PG_DB_EXTENSIONS="timescaledb,timescaledb_toolkit" PG_DB_GRANT_SUPERUSER="true" setup_postgresql_db
|
||||
|
||||
msg_info "Installing tailscale"
|
||||
setup_deb822_repo \
|
||||
"tailscale" \
|
||||
"https://pkgs.tailscale.com/stable/$(get_os_info id)/$(get_os_info codename).noarmor.gpg" \
|
||||
"https://pkgs.tailscale.com/stable/$(get_os_info id)/" \
|
||||
"$(get_os_info codename)"
|
||||
$STD apt install -y tailscale
|
||||
# Tracearr runs tailscaled in user mode, disable the service.
|
||||
$STD systemctl disable --now tailscaled
|
||||
$STD systemctl stop tailscaled
|
||||
msg_ok "Installed tailscale"
|
||||
|
||||
fetch_and_deploy_gh_release "tracearr" "connorgallopo/Tracearr" "tarball" "latest" "/opt/tracearr.build"
|
||||
|
||||
msg_info "Building Tracearr"
|
||||
@@ -75,6 +88,7 @@ msg_info "Configuring Tracearr"
|
||||
$STD useradd -r -s /bin/false -U tracearr
|
||||
$STD chown -R tracearr:tracearr /opt/tracearr
|
||||
install -d -m 750 -o tracearr -g tracearr /data/tracearr
|
||||
install -d -m 750 -o tracearr -g tracearr /data/backup
|
||||
export JWT_SECRET=$(openssl rand -hex 32)
|
||||
export COOKIE_SECRET=$(openssl rand -hex 32)
|
||||
cat <<EOF >/data/tracearr/.env
|
||||
@@ -89,7 +103,6 @@ JWT_SECRET=$JWT_SECRET
|
||||
COOKIE_SECRET=$COOKIE_SECRET
|
||||
APP_VERSION=$(cat /root/.tracearr)
|
||||
#CORS_ORIGIN=http://localhost:5173
|
||||
#MOBILE_BETA_MODE=true
|
||||
EOF
|
||||
chmod 600 /data/tracearr/.env
|
||||
chown -R tracearr:tracearr /data/tracearr
|
||||
@@ -140,6 +153,7 @@ if [ -f \$pg_config_file ]; then
|
||||
fi
|
||||
fi
|
||||
systemctl restart postgresql
|
||||
sudo -u postgres psql -c "ALTER USER tracearr WITH SUPERUSER;"
|
||||
EOF
|
||||
chmod +x /data/tracearr/prestart.sh
|
||||
cat <<EOF >/lib/systemd/system/tracearr.service
|
||||
|
||||
@@ -16,13 +16,13 @@ update_os
|
||||
if [[ "${CTTYPE:-1}" != "0" ]]; then
|
||||
msg_error "UniFi OS Server requires a privileged LXC container."
|
||||
msg_error "Recreate the container with unprivileged=0."
|
||||
exit 1
|
||||
exit 10
|
||||
fi
|
||||
|
||||
if [[ ! -e /dev/net/tun ]]; then
|
||||
msg_error "Missing /dev/net/tun in container."
|
||||
msg_error "Enable TUN/TAP (var_tun=yes) or add /dev/net/tun passthrough."
|
||||
exit 1
|
||||
exit 236
|
||||
fi
|
||||
|
||||
msg_info "Installing dependencies"
|
||||
@@ -48,7 +48,7 @@ TEMP_JSON="$(mktemp)"
|
||||
if ! curl -fsSL "$API_URL" -o "$TEMP_JSON"; then
|
||||
rm -f "$TEMP_JSON"
|
||||
msg_error "Failed to fetch data from Ubiquiti API"
|
||||
exit 1
|
||||
exit 250
|
||||
fi
|
||||
LATEST=$(jq -r '
|
||||
._embedded.firmware
|
||||
@@ -62,7 +62,7 @@ UOS_URL=$(echo "$LATEST" | jq -r '._links.data.href')
|
||||
rm -f "$TEMP_JSON"
|
||||
if [[ -z "$UOS_URL" || -z "$UOS_VERSION" || "$UOS_URL" == "null" ]]; then
|
||||
msg_error "Failed to parse UniFi OS Server version or download URL"
|
||||
exit 1
|
||||
exit 250
|
||||
fi
|
||||
msg_ok "Found UniFi OS Server ${UOS_VERSION}"
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ if gpg --verify /tmp/zerotier-install.sh >/dev/null 2>&1; then
|
||||
$STD bash /tmp/zerotier-install.sh
|
||||
else
|
||||
msg_warn "Could not verify signature of Zerotier-One install script. Exiting..."
|
||||
exit 1
|
||||
exit 250
|
||||
fi
|
||||
msg_ok "Setup Zerotier-One"
|
||||
|
||||
|
||||
119
misc/api.func
119
misc/api.func
@@ -134,6 +134,7 @@ detect_repo_source
|
||||
# * Proxmox custom codes (200-231)
|
||||
# * Tools & Addon Scripts (232-238)
|
||||
# * Node.js/npm errors (239, 243, 245-249)
|
||||
# * Application Install/Update errors (250-254)
|
||||
# - Returns description string for given exit code
|
||||
# ------------------------------------------------------------------------------
|
||||
explain_exit_code() {
|
||||
@@ -322,6 +323,13 @@ explain_exit_code() {
|
||||
248) echo "Node.js: Invalid C++ addon / N-API failure" ;;
|
||||
249) echo "npm/pnpm/yarn: Unknown fatal error" ;;
|
||||
|
||||
# --- Application Install/Update Errors (250-254) ---
|
||||
250) echo "App: Download failed or version not determined" ;;
|
||||
251) echo "App: File extraction failed (corrupt or incomplete archive)" ;;
|
||||
252) echo "App: Required file or resource not found" ;;
|
||||
253) echo "App: Data migration required — update aborted" ;;
|
||||
254) echo "App: User declined prompt or input timed out" ;;
|
||||
|
||||
# --- DPKG ---
|
||||
255) echo "DPKG: Fatal internal error" ;;
|
||||
|
||||
@@ -338,18 +346,20 @@ explain_exit_code() {
|
||||
# - Handles backslashes, quotes, newlines, tabs, and carriage returns
|
||||
# ------------------------------------------------------------------------------
|
||||
json_escape() {
|
||||
local s="$1"
|
||||
# Strip ANSI escape sequences (color codes etc.)
|
||||
s=$(printf '%s' "$s" | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g')
|
||||
s=${s//\\/\\\\}
|
||||
s=${s//"/\\"/}
|
||||
s=${s//$'\n'/\\n}
|
||||
s=${s//$'\r'/}
|
||||
s=${s//$'\t'/\\t}
|
||||
# Remove any remaining control characters (0x00-0x1F except those already handled)
|
||||
# Also remove DEL (0x7F) and invalid high bytes that break JSON parsers
|
||||
s=$(printf '%s' "$s" | tr -d '\000-\010\013\014\016-\037\177')
|
||||
printf '%s' "$s"
|
||||
# Escape a string for safe JSON embedding using awk (handles any input size).
|
||||
# Pipeline: strip ANSI → remove control chars → escape \ " TAB → join lines with \n
|
||||
printf '%s' "$1" \
|
||||
| sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' \
|
||||
| tr -d '\000-\010\013\014\016-\037\177\r' \
|
||||
| awk '
|
||||
BEGIN { ORS = "" }
|
||||
{
|
||||
gsub(/\\/, "\\\\") # backslash → \\
|
||||
gsub(/"/, "\\\"") # double quote → \"
|
||||
gsub(/\t/, "\\t") # tab → \t
|
||||
if (NR > 1) printf "\\n"
|
||||
printf "%s", $0
|
||||
}'
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
@@ -385,6 +395,11 @@ get_error_text() {
|
||||
logfile="$BUILD_LOG"
|
||||
fi
|
||||
|
||||
# Try SILENT_LOGFILE as last resort (captures $STD command output)
|
||||
if [[ -z "$logfile" || ! -s "$logfile" ]] && [[ -n "${SILENT_LOGFILE:-}" && -s "${SILENT_LOGFILE}" ]]; then
|
||||
logfile="$SILENT_LOGFILE"
|
||||
fi
|
||||
|
||||
if [[ -n "$logfile" && -s "$logfile" ]]; then
|
||||
tail -n 20 "$logfile" 2>/dev/null | sed 's/\r$//' | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g'
|
||||
fi
|
||||
@@ -430,6 +445,13 @@ get_full_log() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fall back to SILENT_LOGFILE (captures $STD command output)
|
||||
if [[ -z "$logfile" || ! -s "$logfile" ]]; then
|
||||
if [[ -n "${SILENT_LOGFILE:-}" && -s "${SILENT_LOGFILE}" ]]; then
|
||||
logfile="$SILENT_LOGFILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$logfile" && -s "$logfile" ]]; then
|
||||
# Strip ANSI codes, carriage returns, and anonymize IP addresses (GDPR)
|
||||
sed 's/\r$//' "$logfile" 2>/dev/null |
|
||||
@@ -667,18 +689,23 @@ EOF
|
||||
[[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] Sending to: $TELEMETRY_URL" >&2
|
||||
[[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] Payload: $JSON_PAYLOAD" >&2
|
||||
|
||||
# Fire-and-forget: never block, never fail
|
||||
local http_code
|
||||
if [[ "${DEV_MODE:-}" == "true" ]]; then
|
||||
http_code=$(curl -sS -w "%{http_code}" -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$JSON_PAYLOAD" -o /dev/stderr 2>&1) || true
|
||||
echo "[DEBUG] HTTP response code: $http_code" >&2
|
||||
else
|
||||
curl -fsS -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$JSON_PAYLOAD" &>/dev/null || true
|
||||
fi
|
||||
# Send initial "installing" record with retry.
|
||||
# This record MUST exist for all subsequent updates to succeed.
|
||||
local http_code="" attempt
|
||||
for attempt in 1 2 3; do
|
||||
if [[ "${DEV_MODE:-}" == "true" ]]; then
|
||||
http_code=$(curl -sS -w "%{http_code}" -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$JSON_PAYLOAD" -o /dev/stderr 2>&1) || http_code="000"
|
||||
echo "[DEBUG] post_to_api attempt $attempt HTTP=$http_code" >&2
|
||||
else
|
||||
http_code=$(curl -sS -w "%{http_code}" -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$JSON_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000"
|
||||
fi
|
||||
[[ "$http_code" =~ ^2[0-9]{2}$ ]] && break
|
||||
[[ "$attempt" -lt 3 ]] && sleep 1
|
||||
done
|
||||
|
||||
POST_TO_API_DONE=true
|
||||
}
|
||||
@@ -769,10 +796,15 @@ post_to_api_vm() {
|
||||
EOF
|
||||
)
|
||||
|
||||
# Fire-and-forget: never block, never fail
|
||||
curl -fsS -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$JSON_PAYLOAD" &>/dev/null || true
|
||||
# Send initial "installing" record with retry (must succeed for updates to work)
|
||||
local http_code="" attempt
|
||||
for attempt in 1 2 3; do
|
||||
http_code=$(curl -sS -w "%{http_code}" -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$JSON_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000"
|
||||
[[ "$http_code" =~ ^2[0-9]{2}$ ]] && break
|
||||
[[ "$attempt" -lt 3 ]] && sleep 1
|
||||
done
|
||||
|
||||
POST_TO_API_DONE=true
|
||||
}
|
||||
@@ -868,7 +900,7 @@ post_update_to_api() {
|
||||
esac
|
||||
|
||||
# For failed/unknown status, resolve exit code and error description
|
||||
local short_error=""
|
||||
local short_error="" medium_error=""
|
||||
if [[ "$pb_status" == "failed" ]] || [[ "$pb_status" == "unknown" ]]; then
|
||||
if [[ "$raw_exit_code" =~ ^[0-9]+$ ]]; then
|
||||
exit_code="$raw_exit_code"
|
||||
@@ -888,6 +920,18 @@ post_update_to_api() {
|
||||
short_error=$(json_escape "$(explain_exit_code "$exit_code")")
|
||||
error_category=$(categorize_error "$exit_code")
|
||||
[[ -z "$error" ]] && error="Unknown error"
|
||||
|
||||
# Build medium error for attempt 2: explanation + last 100 log lines (≤16KB)
|
||||
# This is the critical middle ground between full 120KB log and generic-only description
|
||||
local medium_log=""
|
||||
medium_log=$(get_full_log 16384) || true # 16KB max
|
||||
if [[ -z "$medium_log" ]]; then
|
||||
medium_log=$(get_error_text) || true
|
||||
fi
|
||||
local medium_full
|
||||
medium_full=$(build_error_string "$exit_code" "$medium_log")
|
||||
medium_error=$(json_escape "$medium_full")
|
||||
[[ -z "$medium_error" ]] && medium_error="$short_error"
|
||||
fi
|
||||
|
||||
# Calculate duration if timer was started
|
||||
@@ -904,6 +948,11 @@ post_update_to_api() {
|
||||
|
||||
local http_code=""
|
||||
|
||||
# Strip 'G' suffix from disk size (VMs set DISK_SIZE=32G)
|
||||
local DISK_SIZE_API="${DISK_SIZE:-0}"
|
||||
DISK_SIZE_API="${DISK_SIZE_API%G}"
|
||||
[[ ! "$DISK_SIZE_API" =~ ^[0-9]+$ ]] && DISK_SIZE_API=0
|
||||
|
||||
# ── Attempt 1: Full payload with complete error text (includes full log) ──
|
||||
local JSON_PAYLOAD
|
||||
JSON_PAYLOAD=$(
|
||||
@@ -915,7 +964,7 @@ post_update_to_api() {
|
||||
"nsapp": "${NSAPP:-unknown}",
|
||||
"status": "${pb_status}",
|
||||
"ct_type": ${CT_TYPE:-1},
|
||||
"disk_size": ${DISK_SIZE:-0},
|
||||
"disk_size": ${DISK_SIZE_API},
|
||||
"core_count": ${CORE_COUNT:-0},
|
||||
"ram_size": ${RAM_SIZE:-0},
|
||||
"os_type": "${var_os:-}",
|
||||
@@ -946,7 +995,7 @@ EOF
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ── Attempt 2: Short error text (no full log) ──
|
||||
# ── Attempt 2: Medium error text (truncated log ≤16KB instead of full 120KB) ──
|
||||
sleep 1
|
||||
local RETRY_PAYLOAD
|
||||
RETRY_PAYLOAD=$(
|
||||
@@ -958,7 +1007,7 @@ EOF
|
||||
"nsapp": "${NSAPP:-unknown}",
|
||||
"status": "${pb_status}",
|
||||
"ct_type": ${CT_TYPE:-1},
|
||||
"disk_size": ${DISK_SIZE:-0},
|
||||
"disk_size": ${DISK_SIZE_API},
|
||||
"core_count": ${CORE_COUNT:-0},
|
||||
"ram_size": ${RAM_SIZE:-0},
|
||||
"os_type": "${var_os:-}",
|
||||
@@ -966,7 +1015,7 @@ EOF
|
||||
"pve_version": "${pve_version}",
|
||||
"method": "${METHOD:-default}",
|
||||
"exit_code": ${exit_code},
|
||||
"error": "${short_error}",
|
||||
"error": "${medium_error}",
|
||||
"error_category": "${error_category}",
|
||||
"install_duration": ${duration},
|
||||
"cpu_vendor": "${cpu_vendor}",
|
||||
@@ -989,7 +1038,7 @@ EOF
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ── Attempt 3: Minimal payload (bare minimum to set status) ──
|
||||
# ── Attempt 3: Minimal payload with medium error (bare minimum to set status) ──
|
||||
sleep 2
|
||||
local MINIMAL_PAYLOAD
|
||||
MINIMAL_PAYLOAD=$(
|
||||
@@ -1001,7 +1050,7 @@ EOF
|
||||
"nsapp": "${NSAPP:-unknown}",
|
||||
"status": "${pb_status}",
|
||||
"exit_code": ${exit_code},
|
||||
"error": "${short_error}",
|
||||
"error": "${medium_error}",
|
||||
"error_category": "${error_category}",
|
||||
"install_duration": ${duration}
|
||||
}
|
||||
|
||||
@@ -195,6 +195,14 @@ if ! declare -f explain_exit_code &>/dev/null; then
|
||||
247) echo "Node.js: Fatal internal error" ;;
|
||||
248) echo "Node.js: Invalid C++ addon / N-API failure" ;;
|
||||
249) echo "npm/pnpm/yarn: Unknown fatal error" ;;
|
||||
|
||||
# --- Application Install/Update Errors (250-254) ---
|
||||
250) echo "App: Download failed or version not determined" ;;
|
||||
251) echo "App: File extraction failed (corrupt or incomplete archive)" ;;
|
||||
252) echo "App: Required file or resource not found" ;;
|
||||
253) echo "App: Data migration required — update aborted" ;;
|
||||
254) echo "App: User declined prompt or input timed out" ;;
|
||||
|
||||
255) echo "DPKG: Fatal internal error" ;;
|
||||
*) echo "Unknown error" ;;
|
||||
esac
|
||||
@@ -400,10 +408,29 @@ _send_abort_telemetry() {
|
||||
[[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0
|
||||
[[ -z "${RANDOM_UUID:-}" ]] && return 0
|
||||
|
||||
# Collect last 20 log lines for error diagnosis (best-effort)
|
||||
# Collect last 200 log lines for error diagnosis (best-effort)
|
||||
# Container context has no get_full_log(), so we gather as much as possible
|
||||
local error_text=""
|
||||
local logfile=""
|
||||
if [[ -n "${INSTALL_LOG:-}" && -s "${INSTALL_LOG}" ]]; then
|
||||
error_text=$(tail -n 20 "$INSTALL_LOG" 2>/dev/null | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g; s/\\/\\\\/g; s/"/\\"/g; s/\r//g' | tr '\n' '|' | sed 's/|$//' | tr -d '\000-\010\013\014\016-\037\177') || true
|
||||
logfile="${INSTALL_LOG}"
|
||||
elif [[ -n "${SILENT_LOGFILE:-}" && -s "${SILENT_LOGFILE}" ]]; then
|
||||
logfile="${SILENT_LOGFILE}"
|
||||
fi
|
||||
|
||||
if [[ -n "$logfile" ]]; then
|
||||
error_text=$(tail -n 200 "$logfile" 2>/dev/null | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g; s/\\/\\\\/g; s/"/\\"/g; s/\r//g' | tr '\n' '|' | sed 's/|$//' | head -c 16384 | tr -d '\000-\010\013\014\016-\037\177') || true
|
||||
fi
|
||||
|
||||
# Prepend exit code explanation header (like build_error_string does on host)
|
||||
local explanation=""
|
||||
if declare -f explain_exit_code &>/dev/null; then
|
||||
explanation=$(explain_exit_code "$exit_code" 2>/dev/null) || true
|
||||
fi
|
||||
if [[ -n "$explanation" && -n "$error_text" ]]; then
|
||||
error_text="exit_code=${exit_code} | ${explanation}|---|${error_text}"
|
||||
elif [[ -n "$explanation" && -z "$error_text" ]]; then
|
||||
error_text="exit_code=${exit_code} | ${explanation}"
|
||||
fi
|
||||
|
||||
# Calculate duration if start time is available
|
||||
@@ -412,10 +439,17 @@ _send_abort_telemetry() {
|
||||
duration=$(($(date +%s) - DIAGNOSTICS_START_TIME))
|
||||
fi
|
||||
|
||||
# Categorize error if function is available (may not be in minimal container context)
|
||||
local error_category=""
|
||||
if declare -f categorize_error &>/dev/null; then
|
||||
error_category=$(categorize_error "$exit_code" 2>/dev/null) || true
|
||||
fi
|
||||
|
||||
# Build JSON payload with error context
|
||||
local payload
|
||||
payload="{\"random_id\":\"${RANDOM_UUID}\",\"execution_id\":\"${EXECUTION_ID:-${RANDOM_UUID}}\",\"type\":\"${TELEMETRY_TYPE:-lxc}\",\"nsapp\":\"${NSAPP:-${app:-unknown}}\",\"status\":\"failed\",\"exit_code\":${exit_code}"
|
||||
[[ -n "$error_text" ]] && payload="${payload},\"error\":\"${error_text}\""
|
||||
[[ -n "$error_category" ]] && payload="${payload},\"error_category\":\"${error_category}\""
|
||||
[[ -n "$duration" ]] && payload="${payload},\"duration\":${duration}"
|
||||
payload="${payload}}"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user