mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2026-03-03 17:35:55 +01:00
Compare commits
31 Commits
exit_code_
...
revert-#11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdabc13db1 | ||
|
|
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
|
||||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -414,14 +414,27 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
|||||||
|
|
||||||
### 🆕 New Scripts
|
### 🆕 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
|
### 🚀 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
|
- #### 💥 Breaking Changes
|
||||||
|
|
||||||
- Migrate: DokPloy, Komodo, Coolify, Dockge, Runtipi to Addons [@MickLesk](https://github.com/MickLesk) ([#12275](https://github.com/community-scripts/ProxmoxVE/pull/12275))
|
- 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
|
### 💾 Core
|
||||||
|
|
||||||
- #### ✨ New Features
|
- #### ✨ New Features
|
||||||
|
|||||||
6
ct/headers/powerdns
Normal file
6
ct/headers/powerdns
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
____ ____ _ _______
|
||||||
|
/ __ \____ _ _____ _____/ __ \/ | / / ___/
|
||||||
|
/ /_/ / __ \ | /| / / _ \/ ___/ / / / |/ /\__ \
|
||||||
|
/ ____/ /_/ / |/ |/ / __/ / / /_/ / /| /___/ /
|
||||||
|
/_/ \____/|__/|__/\___/_/ /_____/_/ |_//____/
|
||||||
|
|
||||||
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}"
|
||||||
@@ -75,10 +75,31 @@ if [ -f \$pg_config_file ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
systemctl restart postgresql
|
systemctl restart postgresql
|
||||||
|
sudo -u postgres psql -c "ALTER USER tracearr WITH SUPERUSER;"
|
||||||
EOF
|
EOF
|
||||||
chmod +x /data/tracearr/prestart.sh
|
chmod +x /data/tracearr/prestart.sh
|
||||||
msg_ok "Updated prestart script"
|
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
|
if check_for_gh_release "tracearr" "connorgallopo/Tracearr"; then
|
||||||
msg_info "Stopping Services"
|
msg_info "Stopping Services"
|
||||||
systemctl stop tracearr postgresql redis
|
systemctl stop tracearr postgresql redis
|
||||||
@@ -122,6 +143,8 @@ EOF
|
|||||||
sed -i "s/^APP_VERSION=.*/APP_VERSION=$(cat /root/.tracearr)/" /data/tracearr/.env
|
sed -i "s/^APP_VERSION=.*/APP_VERSION=$(cat /root/.tracearr)/" /data/tracearr/.env
|
||||||
chmod 600 /data/tracearr/.env
|
chmod 600 /data/tracearr/.env
|
||||||
chown -R tracearr:tracearr /data/tracearr
|
chown -R tracearr:tracearr /data/tracearr
|
||||||
|
mkdir -p /data/backup
|
||||||
|
chown -R tracearr:tracearr /data/backup
|
||||||
msg_ok "Configured Tracearr"
|
msg_ok "Configured Tracearr"
|
||||||
|
|
||||||
msg_info "Starting services"
|
msg_info "Starting services"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"generated": "2026-03-02T12:12:21Z",
|
"generated": "2026-03-02T18:14:19Z",
|
||||||
"versions": [
|
"versions": [
|
||||||
{
|
{
|
||||||
"slug": "2fauth",
|
"slug": "2fauth",
|
||||||
@@ -200,9 +200,9 @@
|
|||||||
{
|
{
|
||||||
"slug": "cleanuparr",
|
"slug": "cleanuparr",
|
||||||
"repo": "Cleanuparr/Cleanuparr",
|
"repo": "Cleanuparr/Cleanuparr",
|
||||||
"version": "v2.7.6",
|
"version": "v2.7.7",
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"date": "2026-02-27T19:32:02Z"
|
"date": "2026-03-02T13:08:32Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "cloudreve",
|
"slug": "cloudreve",
|
||||||
@@ -424,9 +424,9 @@
|
|||||||
{
|
{
|
||||||
"slug": "frigate",
|
"slug": "frigate",
|
||||||
"repo": "blakeblackshear/frigate",
|
"repo": "blakeblackshear/frigate",
|
||||||
"version": "v0.16.4",
|
"version": "v0.17.0",
|
||||||
"pinned": true,
|
"pinned": true,
|
||||||
"date": "2026-01-29T00:42:14Z"
|
"date": "2026-02-27T03:03:01Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "gatus",
|
"slug": "gatus",
|
||||||
@@ -1149,6 +1149,13 @@
|
|||||||
"pinned": false,
|
"pinned": false,
|
||||||
"date": "2026-02-23T19:50:48Z"
|
"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",
|
"slug": "privatebin",
|
||||||
"repo": "PrivateBin/PrivateBin",
|
"repo": "PrivateBin/PrivateBin",
|
||||||
@@ -1222,9 +1229,9 @@
|
|||||||
{
|
{
|
||||||
"slug": "pulse",
|
"slug": "pulse",
|
||||||
"repo": "rcourtman/Pulse",
|
"repo": "rcourtman/Pulse",
|
||||||
"version": "v5.1.16",
|
"version": "v5.1.17",
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"date": "2026-03-01T23:13:09Z"
|
"date": "2026-03-02T17:52:12Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "pve-scripts-local",
|
"slug": "pve-scripts-local",
|
||||||
@@ -1348,9 +1355,9 @@
|
|||||||
{
|
{
|
||||||
"slug": "scanopy",
|
"slug": "scanopy",
|
||||||
"repo": "scanopy/scanopy",
|
"repo": "scanopy/scanopy",
|
||||||
"version": "v0.14.10",
|
"version": "v0.14.11",
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"date": "2026-02-28T21:05:12Z"
|
"date": "2026-03-02T08:48:42Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "scraparr",
|
"slug": "scraparr",
|
||||||
|
|||||||
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -2,42 +2,68 @@
|
|||||||
|
|
||||||
import type { z } from "zod";
|
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 { CalendarIcon, Check, Clipboard, Download } from "lucide-react";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "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 { format } from "date-fns";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
import type { Category } from "@/lib/types";
|
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
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 { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { Calendar } from "@/components/ui/calendar";
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { basePath } from "@/config/site-config";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { fetchCategories } from "@/lib/data";
|
import { fetchCategories } from "@/lib/data";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import type { Script } from "./_schemas/schemas";
|
import type { Script } from "./_schemas/schemas";
|
||||||
|
|
||||||
|
import { ScriptItem } from "../scripts/_components/script-item";
|
||||||
import InstallMethod from "./_components/install-method";
|
import InstallMethod from "./_components/install-method";
|
||||||
import { ScriptSchema } from "./_schemas/schemas";
|
import { ScriptSchema } from "./_schemas/schemas";
|
||||||
import Categories from "./_components/categories";
|
import Categories from "./_components/categories";
|
||||||
import Note from "./_components/note";
|
import Note from "./_components/note";
|
||||||
|
|
||||||
import { githubGist, nord } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
function search(scripts: Script[], query: string): Script[] {
|
||||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
const queryLower = query.toLowerCase().trim();
|
||||||
import { ScriptItem } from "../scripts/_components/script-item";
|
const searchWords = queryLower.split(/\s+/).filter(Boolean);
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
return scripts
|
||||||
import { search } from "@/components/command-menu";
|
.map((script) => {
|
||||||
import { basePath } from "@/config/site-config";
|
const nameLower = script.name.toLowerCase();
|
||||||
import Image from "next/image";
|
const descriptionLower = (script.description || "").toLowerCase();
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
|
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 = {
|
const initialScript: Script = {
|
||||||
name: "",
|
name: "",
|
||||||
@@ -77,32 +103,32 @@ export default function JSONGenerator() {
|
|||||||
|
|
||||||
const selectedCategoryObj = useMemo(
|
const selectedCategoryObj = useMemo(
|
||||||
() => categories.find(cat => cat.id.toString() === selectedCategory),
|
() => categories.find(cat => cat.id.toString() === selectedCategory),
|
||||||
[categories, selectedCategory]
|
[categories, selectedCategory],
|
||||||
);
|
);
|
||||||
|
|
||||||
const allScripts = useMemo(
|
const allScripts = useMemo(
|
||||||
() => categories.flatMap(cat => cat.scripts || []),
|
() => categories.flatMap(cat => cat.scripts || []),
|
||||||
[categories]
|
[categories],
|
||||||
);
|
);
|
||||||
|
|
||||||
const scripts = useMemo(() => {
|
const scripts = useMemo(() => {
|
||||||
const query = searchQuery.trim()
|
const query = searchQuery.trim();
|
||||||
|
|
||||||
if (query) {
|
if (query) {
|
||||||
return search(allScripts, query)
|
return search(allScripts, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedCategoryObj) {
|
if (selectedCategoryObj) {
|
||||||
return selectedCategoryObj.scripts || []
|
return selectedCategoryObj.scripts || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return []
|
return [];
|
||||||
}, [allScripts, selectedCategoryObj, searchQuery]);
|
}, [allScripts, selectedCategoryObj, searchQuery]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCategories()
|
fetchCategories()
|
||||||
.then(setCategories)
|
.then(setCategories)
|
||||||
.catch((error) => console.error("Error fetching categories:", error));
|
.catch(error => console.error("Error fetching categories:", error));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -122,11 +148,14 @@ export default function JSONGenerator() {
|
|||||||
|
|
||||||
if (updated.type === "pve") {
|
if (updated.type === "pve") {
|
||||||
scriptPath = `tools/pve/${updated.slug}.sh`;
|
scriptPath = `tools/pve/${updated.slug}.sh`;
|
||||||
} else if (updated.type === "addon") {
|
}
|
||||||
|
else if (updated.type === "addon") {
|
||||||
scriptPath = `tools/addon/${updated.slug}.sh`;
|
scriptPath = `tools/addon/${updated.slug}.sh`;
|
||||||
} else if (method.type === "alpine") {
|
}
|
||||||
|
else if (method.type === "alpine") {
|
||||||
scriptPath = `${updated.type}/alpine-${updated.slug}.sh`;
|
scriptPath = `${updated.type}/alpine-${updated.slug}.sh`;
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
scriptPath = `${updated.type}/${updated.slug}.sh`;
|
scriptPath = `${updated.type}/${updated.slug}.sh`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,11 +174,13 @@ export default function JSONGenerator() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCopy = useCallback(() => {
|
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));
|
navigator.clipboard.writeText(JSON.stringify(script, null, 2));
|
||||||
setIsCopied(true);
|
setIsCopied(true);
|
||||||
setTimeout(() => setIsCopied(false), 2000);
|
setTimeout(() => setIsCopied(false), 2000);
|
||||||
if (isValid) toast.success("Copied metadata to clipboard");
|
if (isValid)
|
||||||
|
toast.success("Copied metadata to clipboard");
|
||||||
}, [script]);
|
}, [script]);
|
||||||
|
|
||||||
const importScript = (script: Script) => {
|
const importScript = (script: Script) => {
|
||||||
@@ -166,11 +197,11 @@ export default function JSONGenerator() {
|
|||||||
setIsValid(true);
|
setIsValid(true);
|
||||||
setZodErrors(null);
|
setZodErrors(null);
|
||||||
toast.success("Imported JSON successfully");
|
toast.success("Imported JSON successfully");
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
toast.error("Failed to read or parse the JSON file.");
|
toast.error("Failed to read or parse the JSON file.");
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const handleFileImport = useCallback(() => {
|
const handleFileImport = useCallback(() => {
|
||||||
const input = document.createElement("input");
|
const input = document.createElement("input");
|
||||||
@@ -180,7 +211,8 @@ export default function JSONGenerator() {
|
|||||||
input.onchange = (e: Event) => {
|
input.onchange = (e: Event) => {
|
||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
const file = target.files?.[0];
|
const file = target.files?.[0];
|
||||||
if (!file) return;
|
if (!file)
|
||||||
|
return;
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (event) => {
|
reader.onload = (event) => {
|
||||||
@@ -189,7 +221,8 @@ export default function JSONGenerator() {
|
|||||||
const parsed = JSON.parse(content);
|
const parsed = JSON.parse(content);
|
||||||
importScript(parsed);
|
importScript(parsed);
|
||||||
toast.success("Imported JSON successfully");
|
toast.success("Imported JSON successfully");
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
toast.error("Failed to read the JSON file.");
|
toast.error("Failed to read the JSON file.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -243,7 +276,10 @@ export default function JSONGenerator() {
|
|||||||
<div className="mt-2 space-y-1">
|
<div className="mt-2 space-y-1">
|
||||||
{zodErrors.issues.map((error, index) => (
|
{zodErrors.issues.map((error, index) => (
|
||||||
<AlertDescription key={index} className="p-1 text-red-500">
|
<AlertDescription key={index} className="p-1 text-red-500">
|
||||||
{error.path.join(".")} -{error.message}
|
{error.path.join(".")}
|
||||||
|
{" "}
|
||||||
|
-
|
||||||
|
{error.message}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -270,7 +306,7 @@ export default function JSONGenerator() {
|
|||||||
onOpenChange={setIsImportDialogOpen}
|
onOpenChange={setIsImportDialogOpen}
|
||||||
>
|
>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
<DropdownMenuItem onSelect={e => e.preventDefault()}>
|
||||||
Import existing script
|
Import existing script
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
@@ -292,7 +328,7 @@ export default function JSONGenerator() {
|
|||||||
<SelectValue placeholder="Category" />
|
<SelectValue placeholder="Category" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{categories.map((category) => (
|
{categories.map(category => (
|
||||||
<SelectItem key={category.id} value={category.id.toString()}>
|
<SelectItem key={category.id} value={category.id.toString()}>
|
||||||
{category.name}
|
{category.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -302,40 +338,44 @@ export default function JSONGenerator() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Search for a script..."
|
placeholder="Search for a script..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={e => setSearchQuery(e.target.value)}
|
||||||
/>
|
/>
|
||||||
{!selectedCategory && !searchQuery ? (
|
{!selectedCategory && !searchQuery
|
||||||
<p className="text-muted-foreground text-sm text-center">
|
? (
|
||||||
Select a category or search for a script
|
<p className="text-muted-foreground text-sm text-center">
|
||||||
</p>
|
Select a category or search for a script
|
||||||
) : scripts.length === 0 ? (
|
</p>
|
||||||
<p className="text-muted-foreground text-sm text-center">
|
)
|
||||||
No scripts found
|
: scripts.length === 0
|
||||||
</p>
|
? (
|
||||||
) : (
|
<p className="text-muted-foreground text-sm text-center">
|
||||||
<div className="grid grid-cols-3 auto-rows-min h-64 overflow-y-auto gap-4">
|
No scripts found
|
||||||
{scripts.map(script => (
|
</p>
|
||||||
<div
|
)
|
||||||
key={script.slug}
|
: (
|
||||||
className="p-2 border rounded cursor-pointer hover:bg-accent hover:text-accent-foreground"
|
<div className="grid grid-cols-3 auto-rows-min h-64 overflow-y-auto gap-4">
|
||||||
onClick={() => {
|
{scripts.map(script => (
|
||||||
importScript(script);
|
<div
|
||||||
setIsImportDialogOpen(false);
|
key={script.slug}
|
||||||
}}
|
className="p-2 border rounded cursor-pointer hover:bg-accent hover:text-accent-foreground"
|
||||||
>
|
onClick={() => {
|
||||||
<Image
|
importScript(script);
|
||||||
src={script.logo || `/${basePath}/logo.png`}
|
setIsImportDialogOpen(false);
|
||||||
alt={script.name}
|
}}
|
||||||
className="w-full h-12 object-contain mb-2"
|
>
|
||||||
width={16}
|
<Image
|
||||||
height={16}
|
src={script.logo || `/${basePath}/logo.png`}
|
||||||
unoptimized
|
alt={script.name}
|
||||||
/>
|
className="w-full h-12 object-contain mb-2"
|
||||||
<p className="text-sm text-center">{script.name}</p>
|
width={16}
|
||||||
</div>
|
height={16}
|
||||||
))}
|
unoptimized
|
||||||
</div>
|
/>
|
||||||
)}
|
<p className="text-sm text-center">{script.name}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -348,15 +388,19 @@ export default function JSONGenerator() {
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
Name <span className="text-red-500">*</span>
|
Name
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</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>
|
||||||
<div>
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
Slug <span className="text-red-500">*</span>
|
Slug
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</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>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -366,7 +410,7 @@ export default function JSONGenerator() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Full logo URL"
|
placeholder="Full logo URL"
|
||||||
value={script.logo || ""}
|
value={script.logo || ""}
|
||||||
onChange={(e) => updateScript("logo", e.target.value || null)}
|
onChange={e => updateScript("logo", e.target.value || null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -374,24 +418,28 @@ export default function JSONGenerator() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Path to config file"
|
placeholder="Path to config file"
|
||||||
value={script.config_path || ""}
|
value={script.config_path || ""}
|
||||||
onChange={(e) => updateScript("config_path", e.target.value || "")}
|
onChange={e => updateScript("config_path", e.target.value || "")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
Description <span className="text-red-500">*</span>
|
Description
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="Example"
|
placeholder="Example"
|
||||||
value={script.description}
|
value={script.description}
|
||||||
onChange={(e) => updateScript("description", e.target.value)}
|
onChange={e => updateScript("description", e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Categories script={script} setScript={setScript} categories={categories} />
|
<Categories script={script} setScript={setScript} categories={categories} />
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<Label>
|
<Label>
|
||||||
Date Created <span className="text-red-500">*</span>
|
Date Created
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild className="flex-1">
|
<PopoverTrigger asChild className="flex-1">
|
||||||
@@ -415,7 +463,7 @@ export default function JSONGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<Label>Type</Label>
|
<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">
|
<SelectTrigger className="flex-1">
|
||||||
<SelectValue placeholder="Type" />
|
<SelectValue placeholder="Type" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -430,17 +478,17 @@ export default function JSONGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-full flex gap-5">
|
<div className="w-full flex gap-5">
|
||||||
<div className="flex items-center space-x-2">
|
<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>
|
<label>Updateable</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<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>
|
<label>Privileged</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Switch
|
<Switch
|
||||||
checked={script.disable || false}
|
checked={script.disable || false}
|
||||||
onCheckedChange={(checked) => updateScript("disable", checked)}
|
onCheckedChange={checked => updateScript("disable", checked)}
|
||||||
/>
|
/>
|
||||||
<label>Disabled</label>
|
<label>Disabled</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -448,12 +496,14 @@ export default function JSONGenerator() {
|
|||||||
{script.disable && (
|
{script.disable && (
|
||||||
<div>
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
Disable Description <span className="text-red-500">*</span>
|
Disable Description
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="Explain why this script is disabled..."
|
placeholder="Explain why this script is disabled..."
|
||||||
value={script.disable_description || ""}
|
value={script.disable_description || ""}
|
||||||
onChange={(e) => updateScript("disable_description", e.target.value)}
|
onChange={e => updateScript("disable_description", e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -461,18 +511,18 @@ export default function JSONGenerator() {
|
|||||||
placeholder="Interface Port"
|
placeholder="Interface Port"
|
||||||
type="number"
|
type="number"
|
||||||
value={script.interface_port || ""}
|
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">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Website URL"
|
placeholder="Website URL"
|
||||||
value={script.website || ""}
|
value={script.website || ""}
|
||||||
onChange={(e) => updateScript("website", e.target.value || null)}
|
onChange={e => updateScript("website", e.target.value || null)}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Documentation URL"
|
placeholder="Documentation URL"
|
||||||
value={script.documentation || ""}
|
value={script.documentation || ""}
|
||||||
onChange={(e) => updateScript("documentation", e.target.value || null)}
|
onChange={e => updateScript("documentation", e.target.value || null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<InstallMethod script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
<InstallMethod script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
||||||
@@ -480,22 +530,20 @@ export default function JSONGenerator() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
value={script.default_credentials.username || ""}
|
value={script.default_credentials.username || ""}
|
||||||
onChange={(e) =>
|
onChange={e =>
|
||||||
updateScript("default_credentials", {
|
updateScript("default_credentials", {
|
||||||
...script.default_credentials,
|
...script.default_credentials,
|
||||||
username: e.target.value || null,
|
username: e.target.value || null,
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
value={script.default_credentials.password || ""}
|
value={script.default_credentials.password || ""}
|
||||||
onChange={(e) =>
|
onChange={e =>
|
||||||
updateScript("default_credentials", {
|
updateScript("default_credentials", {
|
||||||
...script.default_credentials,
|
...script.default_credentials,
|
||||||
password: e.target.value || null,
|
password: e.target.value || null,
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Note script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
<Note script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
||||||
</form>
|
</form>
|
||||||
@@ -504,7 +552,7 @@ export default function JSONGenerator() {
|
|||||||
<Tabs
|
<Tabs
|
||||||
defaultValue="json"
|
defaultValue="json"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onValueChange={(value) => setCurrentTab(value as "json" | "preview")}
|
onValueChange={value => setCurrentTab(value as "json" | "preview")}
|
||||||
value={currentTab}
|
value={currentTab}
|
||||||
>
|
>
|
||||||
<TabsList className="grid w-full grid-cols-2">
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { ArrowRightIcon, Sparkles } from "lucide-react";
|
import { ArrowRightIcon, Sparkles } from "lucide-react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import type { Category, Script } from "@/lib/types";
|
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 { DialogTitle } from "./ui/dialog";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { Badge } from "./ui/badge";
|
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) {
|
export function formattedBadge(type: string) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -79,11 +51,9 @@ function getRandomScript(categories: Category[], previouslySelected: Set<string>
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CommandMenu() {
|
function CommandMenu() {
|
||||||
const [query, setQuery] = React.useState("");
|
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [links, setLinks] = React.useState<Category[]>([]);
|
const [links, setLinks] = React.useState<Category[]>([]);
|
||||||
const [isLoading, setIsLoading] = React.useState(false);
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
const [results, setResults] = React.useState<Script[]>([]);
|
|
||||||
const [selectedScripts, setSelectedScripts] = React.useState<Set<string>>(new Set());
|
const [selectedScripts, setSelectedScripts] = React.useState<Set<string>>(new Set());
|
||||||
const router = useRouter();
|
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(() => {
|
React.useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||||
@@ -248,46 +197,49 @@ function CommandMenu() {
|
|||||||
|
|
||||||
<CommandDialog
|
<CommandDialog
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={setOpen}
|
||||||
setOpen(open);
|
filter={(value: string, search: string) => {
|
||||||
if (open) {
|
const searchLower = search.toLowerCase().trim();
|
||||||
setQuery("");
|
if (!searchLower)
|
||||||
setResults([]);
|
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>
|
<DialogTitle className="sr-only">Search scripts</DialogTitle>
|
||||||
<CommandInput
|
<CommandInput placeholder="Search for a script..." />
|
||||||
placeholder="Search for a script..."
|
|
||||||
onValueChange={setQuery}
|
|
||||||
value={query}
|
|
||||||
/>
|
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>
|
<CommandEmpty>
|
||||||
{isLoading ? (
|
{isLoading
|
||||||
"Searching..."
|
? (
|
||||||
) : (
|
"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">
|
<div className="flex flex-col items-center justify-center py-6 text-center">
|
||||||
<p className="text-xs text-muted-foreground mb-2">Want to add a new script?</p>
|
<p className="text-sm text-muted-foreground">No scripts match your search.</p>
|
||||||
<Button variant="outline" size="sm" asChild>
|
<div className="mt-4">
|
||||||
<Link
|
<p className="text-xs text-muted-foreground mb-2">Want to add a new script?</p>
|
||||||
href={`https://github.com/community-scripts/${basePath}/tree/main/docs/contribution/GUIDE.md`}
|
<Button variant="outline" size="sm" asChild>
|
||||||
target="_blank"
|
<Link
|
||||||
rel="noopener noreferrer"
|
href={`https://github.com/community-scripts/${basePath}/tree/main/docs/contribution/GUIDE.md`}
|
||||||
>
|
target="_blank"
|
||||||
Documentation <ArrowRightIcon className="ml-2 h-4 w-4" />
|
rel="noopener noreferrer"
|
||||||
</Link>
|
>
|
||||||
</Button>
|
Documentation
|
||||||
</div>
|
{" "}
|
||||||
</div>
|
<ArrowRightIcon className="ml-2 h-4 w-4" />
|
||||||
)}
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
|
{Object.entries(uniqueScriptsByCategory).map(([categoryName, scripts]) => (
|
||||||
{results.length > 0 ? (
|
<CommandGroup key={`category:${categoryName}`} heading={categoryName}>
|
||||||
<CommandGroup heading="Search Results">
|
{scripts.map(script => (
|
||||||
{results.map(script => (
|
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={`script:${script.slug}`}
|
key={`script:${script.slug}`}
|
||||||
value={`${script.name} ${script.type} ${script.description || ""}`}
|
value={`${script.name} ${script.type} ${script.description || ""}`}
|
||||||
@@ -320,44 +272,7 @@ function CommandMenu() {
|
|||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
</CommandGroup>
|
</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>
|
</CommandList>
|
||||||
</CommandDialog>
|
</CommandDialog>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Copyright (c) 2021-2026 community-scripts ORG
|
# Copyright (c) 2021-2026 community-scripts ORG
|
||||||
# Authors: MickLesk (CanbiZ)
|
# Authors: MickLesk (CanbiZ) | Co-Authors: remz1337
|
||||||
# Co-Authors: remz1337
|
|
||||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
# Source: https://frigate.video/ | Github: https://github.com/blakeblackshear/frigate
|
# Source: https://frigate.video/ | Github: https://github.com/blakeblackshear/frigate
|
||||||
|
|
||||||
@@ -85,6 +84,7 @@ $STD apt install -y \
|
|||||||
tclsh \
|
tclsh \
|
||||||
libopenblas-dev \
|
libopenblas-dev \
|
||||||
liblapack-dev \
|
liblapack-dev \
|
||||||
|
libgomp1 \
|
||||||
make \
|
make \
|
||||||
moreutils
|
moreutils
|
||||||
msg_ok "Installed Dependencies"
|
msg_ok "Installed Dependencies"
|
||||||
@@ -101,9 +101,16 @@ export NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
|
|||||||
export TOKENIZERS_PARALLELISM=true
|
export TOKENIZERS_PARALLELISM=true
|
||||||
export TRANSFORMERS_NO_ADVISORY_WARNINGS=1
|
export TRANSFORMERS_NO_ADVISORY_WARNINGS=1
|
||||||
export OPENCV_FFMPEG_LOGLEVEL=8
|
export OPENCV_FFMPEG_LOGLEVEL=8
|
||||||
|
export PYTHONWARNINGS="ignore:::numpy.core.getlimits"
|
||||||
export HAILORT_LOGGER_PATH=NONE
|
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"
|
msg_info "Building Nginx"
|
||||||
$STD bash /opt/frigate/docker/main/build_nginx.sh
|
$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
|
ldconfig
|
||||||
msg_ok "Built libUSB"
|
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"
|
msg_info "Installing Python Dependencies"
|
||||||
$STD pip3 install -r /opt/frigate/docker/main/requirements.txt
|
$STD pip3 install -r /opt/frigate/docker/main/requirements.txt
|
||||||
msg_ok "Installed Python Dependencies"
|
msg_ok "Installed Python Dependencies"
|
||||||
|
|
||||||
msg_info "Building Python Wheels (Patience)"
|
msg_info "Building Python Wheels (Patience)"
|
||||||
mkdir -p /wheels
|
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
|
$STD bash /opt/frigate/docker/main/build_pysqlite3.sh
|
||||||
for i in {1..3}; do
|
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
|
$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
|
done
|
||||||
msg_ok "Built Python Wheels"
|
msg_ok "Built Python Wheels"
|
||||||
|
|
||||||
NODE_VERSION="22" NODE_MODULE="yarn" setup_nodejs
|
NODE_VERSION="20" setup_nodejs
|
||||||
|
|
||||||
msg_info "Downloading Inference Models"
|
msg_info "Downloading Inference Models"
|
||||||
mkdir -p /models /openvino-model
|
mkdir -p /models /openvino-model
|
||||||
@@ -183,6 +196,10 @@ $STD pip3 install -U /wheels/*.whl
|
|||||||
ldconfig
|
ldconfig
|
||||||
msg_ok "Installed HailoRT Runtime"
|
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"
|
msg_info "Installing OpenVino"
|
||||||
$STD pip3 install -r /opt/frigate/docker/main/requirements-ov.txt
|
$STD pip3 install -r /opt/frigate/docker/main/requirements-ov.txt
|
||||||
msg_ok "Installed OpenVino"
|
msg_ok "Installed OpenVino"
|
||||||
@@ -209,6 +226,8 @@ $STD make version
|
|||||||
cd /opt/frigate/web
|
cd /opt/frigate/web
|
||||||
$STD npm install
|
$STD npm install
|
||||||
$STD npm run build
|
$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/
|
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
|
sed -i '/^s6-svc -O \.$/s/^/#/' /opt/frigate/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/run
|
||||||
msg_ok "Built Frigate Application"
|
msg_ok "Built Frigate Application"
|
||||||
@@ -224,6 +243,19 @@ echo "tmpfs /tmp/cache tmpfs defaults 0 0" >>/etc/fstab
|
|||||||
cat <<EOF >/etc/frigate.env
|
cat <<EOF >/etc/frigate.env
|
||||||
DEFAULT_FFMPEG_VERSION="7.0"
|
DEFAULT_FFMPEG_VERSION="7.0"
|
||||||
INCLUDED_FFMPEG_VERSIONS="7.0:5.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
|
EOF
|
||||||
|
|
||||||
cat <<EOF >/config/config.yml
|
cat <<EOF >/config/config.yml
|
||||||
@@ -237,7 +269,6 @@ cameras:
|
|||||||
input_args: -re -stream_loop -1 -fflags +genpts
|
input_args: -re -stream_loop -1 -fflags +genpts
|
||||||
roles:
|
roles:
|
||||||
- detect
|
- detect
|
||||||
- rtmp
|
|
||||||
detect:
|
detect:
|
||||||
height: 1080
|
height: 1080
|
||||||
width: 1920
|
width: 1920
|
||||||
@@ -255,6 +286,7 @@ ffmpeg:
|
|||||||
detectors:
|
detectors:
|
||||||
detector01:
|
detector01:
|
||||||
type: openvino
|
type: openvino
|
||||||
|
device: AUTO
|
||||||
model:
|
model:
|
||||||
width: 300
|
width: 300
|
||||||
height: 300
|
height: 300
|
||||||
|
|||||||
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
|
||||||
@@ -44,7 +44,20 @@ $STD timescaledb-tune -yes -memory "$ram_for_tsdb"MB
|
|||||||
$STD systemctl restart postgresql
|
$STD systemctl restart postgresql
|
||||||
msg_ok "Installed TimescaleDB"
|
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"
|
fetch_and_deploy_gh_release "tracearr" "connorgallopo/Tracearr" "tarball" "latest" "/opt/tracearr.build"
|
||||||
|
|
||||||
msg_info "Building Tracearr"
|
msg_info "Building Tracearr"
|
||||||
@@ -75,6 +88,7 @@ msg_info "Configuring Tracearr"
|
|||||||
$STD useradd -r -s /bin/false -U tracearr
|
$STD useradd -r -s /bin/false -U tracearr
|
||||||
$STD chown -R tracearr:tracearr /opt/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/tracearr
|
||||||
|
install -d -m 750 -o tracearr -g tracearr /data/backup
|
||||||
export JWT_SECRET=$(openssl rand -hex 32)
|
export JWT_SECRET=$(openssl rand -hex 32)
|
||||||
export COOKIE_SECRET=$(openssl rand -hex 32)
|
export COOKIE_SECRET=$(openssl rand -hex 32)
|
||||||
cat <<EOF >/data/tracearr/.env
|
cat <<EOF >/data/tracearr/.env
|
||||||
@@ -89,7 +103,6 @@ JWT_SECRET=$JWT_SECRET
|
|||||||
COOKIE_SECRET=$COOKIE_SECRET
|
COOKIE_SECRET=$COOKIE_SECRET
|
||||||
APP_VERSION=$(cat /root/.tracearr)
|
APP_VERSION=$(cat /root/.tracearr)
|
||||||
#CORS_ORIGIN=http://localhost:5173
|
#CORS_ORIGIN=http://localhost:5173
|
||||||
#MOBILE_BETA_MODE=true
|
|
||||||
EOF
|
EOF
|
||||||
chmod 600 /data/tracearr/.env
|
chmod 600 /data/tracearr/.env
|
||||||
chown -R tracearr:tracearr /data/tracearr
|
chown -R tracearr:tracearr /data/tracearr
|
||||||
@@ -140,6 +153,7 @@ if [ -f \$pg_config_file ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
systemctl restart postgresql
|
systemctl restart postgresql
|
||||||
|
sudo -u postgres psql -c "ALTER USER tracearr WITH SUPERUSER;"
|
||||||
EOF
|
EOF
|
||||||
chmod +x /data/tracearr/prestart.sh
|
chmod +x /data/tracearr/prestart.sh
|
||||||
cat <<EOF >/lib/systemd/system/tracearr.service
|
cat <<EOF >/lib/systemd/system/tracearr.service
|
||||||
|
|||||||
@@ -346,18 +346,20 @@ explain_exit_code() {
|
|||||||
# - Handles backslashes, quotes, newlines, tabs, and carriage returns
|
# - Handles backslashes, quotes, newlines, tabs, and carriage returns
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
json_escape() {
|
json_escape() {
|
||||||
local s="$1"
|
# Escape a string for safe JSON embedding using awk (handles any input size).
|
||||||
# Strip ANSI escape sequences (color codes etc.)
|
# Pipeline: strip ANSI → remove control chars → escape \ " TAB → join lines with \n
|
||||||
s=$(printf '%s' "$s" | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g')
|
printf '%s' "$1" \
|
||||||
s=${s//\\/\\\\}
|
| sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' \
|
||||||
s=${s//"/\\"/}
|
| tr -d '\000-\010\013\014\016-\037\177\r' \
|
||||||
s=${s//$'\n'/\\n}
|
| awk '
|
||||||
s=${s//$'\r'/}
|
BEGIN { ORS = "" }
|
||||||
s=${s//$'\t'/\\t}
|
{
|
||||||
# Remove any remaining control characters (0x00-0x1F except those already handled)
|
gsub(/\\/, "\\\\") # backslash → \\
|
||||||
# Also remove DEL (0x7F) and invalid high bytes that break JSON parsers
|
gsub(/"/, "\\\"") # double quote → \"
|
||||||
s=$(printf '%s' "$s" | tr -d '\000-\010\013\014\016-\037\177')
|
gsub(/\t/, "\\t") # tab → \t
|
||||||
printf '%s' "$s"
|
if (NR > 1) printf "\\n"
|
||||||
|
printf "%s", $0
|
||||||
|
}'
|
||||||
}
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
@@ -687,18 +689,23 @@ EOF
|
|||||||
[[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] Sending to: $TELEMETRY_URL" >&2
|
[[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] Sending to: $TELEMETRY_URL" >&2
|
||||||
[[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] Payload: $JSON_PAYLOAD" >&2
|
[[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] Payload: $JSON_PAYLOAD" >&2
|
||||||
|
|
||||||
# Fire-and-forget: never block, never fail
|
# Send initial "installing" record with retry.
|
||||||
local http_code
|
# This record MUST exist for all subsequent updates to succeed.
|
||||||
if [[ "${DEV_MODE:-}" == "true" ]]; then
|
local http_code="" attempt
|
||||||
http_code=$(curl -sS -w "%{http_code}" -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
|
for attempt in 1 2 3; do
|
||||||
-H "Content-Type: application/json" \
|
if [[ "${DEV_MODE:-}" == "true" ]]; then
|
||||||
-d "$JSON_PAYLOAD" -o /dev/stderr 2>&1) || true
|
http_code=$(curl -sS -w "%{http_code}" -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
|
||||||
echo "[DEBUG] HTTP response code: $http_code" >&2
|
-H "Content-Type: application/json" \
|
||||||
else
|
-d "$JSON_PAYLOAD" -o /dev/stderr 2>&1) || http_code="000"
|
||||||
curl -fsS -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
|
echo "[DEBUG] post_to_api attempt $attempt HTTP=$http_code" >&2
|
||||||
-H "Content-Type: application/json" \
|
else
|
||||||
-d "$JSON_PAYLOAD" &>/dev/null || true
|
http_code=$(curl -sS -w "%{http_code}" -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
|
||||||
fi
|
-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
|
POST_TO_API_DONE=true
|
||||||
}
|
}
|
||||||
@@ -789,10 +796,15 @@ post_to_api_vm() {
|
|||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fire-and-forget: never block, never fail
|
# Send initial "installing" record with retry (must succeed for updates to work)
|
||||||
curl -fsS -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
|
local http_code="" attempt
|
||||||
-H "Content-Type: application/json" \
|
for attempt in 1 2 3; do
|
||||||
-d "$JSON_PAYLOAD" &>/dev/null || true
|
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
|
POST_TO_API_DONE=true
|
||||||
}
|
}
|
||||||
@@ -936,6 +948,11 @@ post_update_to_api() {
|
|||||||
|
|
||||||
local http_code=""
|
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) ──
|
# ── Attempt 1: Full payload with complete error text (includes full log) ──
|
||||||
local JSON_PAYLOAD
|
local JSON_PAYLOAD
|
||||||
JSON_PAYLOAD=$(
|
JSON_PAYLOAD=$(
|
||||||
@@ -947,7 +964,7 @@ post_update_to_api() {
|
|||||||
"nsapp": "${NSAPP:-unknown}",
|
"nsapp": "${NSAPP:-unknown}",
|
||||||
"status": "${pb_status}",
|
"status": "${pb_status}",
|
||||||
"ct_type": ${CT_TYPE:-1},
|
"ct_type": ${CT_TYPE:-1},
|
||||||
"disk_size": ${DISK_SIZE:-0},
|
"disk_size": ${DISK_SIZE_API},
|
||||||
"core_count": ${CORE_COUNT:-0},
|
"core_count": ${CORE_COUNT:-0},
|
||||||
"ram_size": ${RAM_SIZE:-0},
|
"ram_size": ${RAM_SIZE:-0},
|
||||||
"os_type": "${var_os:-}",
|
"os_type": "${var_os:-}",
|
||||||
@@ -990,7 +1007,7 @@ EOF
|
|||||||
"nsapp": "${NSAPP:-unknown}",
|
"nsapp": "${NSAPP:-unknown}",
|
||||||
"status": "${pb_status}",
|
"status": "${pb_status}",
|
||||||
"ct_type": ${CT_TYPE:-1},
|
"ct_type": ${CT_TYPE:-1},
|
||||||
"disk_size": ${DISK_SIZE:-0},
|
"disk_size": ${DISK_SIZE_API},
|
||||||
"core_count": ${CORE_COUNT:-0},
|
"core_count": ${CORE_COUNT:-0},
|
||||||
"ram_size": ${RAM_SIZE:-0},
|
"ram_size": ${RAM_SIZE:-0},
|
||||||
"os_type": "${var_os:-}",
|
"os_type": "${var_os:-}",
|
||||||
|
|||||||
Reference in New Issue
Block a user