Compare commits

..

31 Commits

Author SHA1 Message Date
Bram Suurd
bdabc13db1 Refactor JSON editor and command menu components
- Moved the search function to the JSON editor for better organization.
- Improved search functionality in the command menu with a new filtering approach.
- Cleaned up unused imports and optimized state management.
- Enhanced user experience by refining the search results display and handling.
- Updated error handling for JSON import and file reading processes.
2026-03-02 23:16:40 +01:00
community-scripts-pr-app[bot]
a326994459 Update .app files (#12485)
Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>
2026-03-02 20:35:25 +01:00
community-scripts-pr-app[bot]
4c8d1ef010 chore: update github-versions.json (#12491)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-02 18:14:27 +00:00
community-scripts-pr-app[bot]
86a9a0aac6 Update CHANGELOG.md (#12487)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-02 17:34:35 +00:00
community-scripts-pr-app[bot]
5059ed2320 Update CHANGELOG.md (#12486)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-02 17:34:13 +00:00
community-scripts-pr-app[bot]
df65c60fe4 Update date in json (#12484)
Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>
2026-03-02 17:33:59 +00:00
push-app-to-main[bot]
cff2c90041 Add powerdns (ct) (#12481)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-03-02 18:33:26 +01:00
Michel Roegl-Brunner
88d1494e46 Adapt workflow 2026-03-02 16:28:35 +01:00
CanbiZ (MickLesk)
8b62b8f3c5 fix(api): rewrite json_escape to use awk for reliable JSON escaping 2026-03-02 16:25:22 +01:00
CanbiZ (MickLesk)
cd38bc3a65 fix: strip G suffix from DISK_SIZE in post_update_to_api for VMs
VMs set DISK_SIZE=32G (with G suffix), but post_update_to_api used
\ directly in JSON, producing 'disk_size: 32G' which is
invalid JSON. The server rejected these with 'invalid character G'.

Now strips the G suffix and validates numeric-only before embedding.
2026-03-02 15:58:34 +01:00
CanbiZ (MickLesk)
46d25645c2 fix: add retry to initial installing POST (post_to_api / post_to_api_vm)
The initial 'installing' record MUST exist for all subsequent status
updates to succeed. Previously this was fire-and-forget with no retry,
so timeouts/503s silently dropped ~50% of installations.

Both post_to_api (LXC) and post_to_api_vm now retry up to 3 times
with 1s delay between attempts. Also captures HTTP response code to
detect failures instead of using curl -f (silent fail).
2026-03-02 15:43:29 +01:00
Michel Roegl-Brunner
3701737eff Workflow test 2026-03-02 15:35:41 +01:00
Michel Roegl-Brunner
b39e296684 Workflow test 2026-03-02 15:35:28 +01:00
Michel Roegl-Brunner
d8a7620c64 Workflow test 2026-03-02 15:26:52 +01:00
Michel Roegl-Brunner
7d5900de18 Add workflow to push json changes to pocketbase 2026-03-02 15:26:41 +01:00
community-scripts-pr-app[bot]
e8b3b936df chore: update github-versions.json (#12479)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-02 13:48:34 +00:00
Michel Roegl-Brunner
5c246310f4 Workflow test 2026-03-02 14:46:46 +01:00
Michel Roegl-Brunner
bdad2cc941 Add workflow to push json changes to pocketbase 2026-03-02 14:46:31 +01:00
community-scripts-pr-app[bot]
f23c33fee7 Update CHANGELOG.md (#12478)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-02 13:43:06 +00:00
durzo
5b207cf5bd Tracearr: prepare for imminent v1.4.19 release (#12413)
* Tracearr: prepare for upcoming v1.4.19

* remove read prompts, per review
2026-03-02 14:42:39 +01:00
community-scripts-pr-app[bot]
f20c9e4ec9 Update CHANGELOG.md (#12477)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-02 13:41:55 +00:00
CanbiZ (MickLesk)
1398ff8397 Frigate: Bump to v0.17 (#12474) 2026-03-02 14:41:25 +01:00
CanbiZ (MickLesk)
ebc3512f50 fix: improve error trace propagation for telemetry
- post_update_to_api: Attempts 2/3 now send medium_error (16KB truncated
  log) instead of short_error (generic description only). This is the
  primary fix — when attempt 1 fails (120KB payload too large/timeout),
  attempts 2/3 no longer discard all log data.

- _send_abort_telemetry: Increased container fallback from 20 to 200
  log lines (capped at 16KB). Added SILENT_LOGFILE as fallback source.
  Added exit code explanation header and error_category to payload.

- get_error_text/get_full_log: Added SILENT_LOGFILE as last-resort
  fallback when INSTALL_LOG, combined log, and BUILD_LOG are all
  empty/missing.
2026-03-02 14:38:55 +01:00
Michel Roegl-Brunner
564a8136a5 Workflow test 2026-03-02 14:38:01 +01:00
Michel Roegl-Brunner
00047c95b8 Add workflow to push json changes to pocketbase 2026-03-02 14:37:50 +01:00
Michel Roegl-Brunner
9849ce79a7 Workflow test 2026-03-02 14:36:14 +01:00
Michel Roegl-Brunner
cea9858193 Add workflow to push json changes to pocketbase 2026-03-02 14:36:02 +01:00
Michel Roegl-Brunner
ee8ea672ef Workflow test 2026-03-02 14:33:35 +01:00
Michel Roegl-Brunner
fc59910bd2 Add workflow to push json changes to pocketbase 2026-03-02 14:15:44 +01:00
community-scripts-pr-app[bot]
20ab7bc005 Update CHANGELOG.md (#12476)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-02 12:58:07 +00:00
CanbiZ (MickLesk)
17de8e761b fix: replace generic exit 1 with specific exit codes in ct/ and install/ scripts (#12475)
Part of #12467 — scripts only (no framework changes).

New exit codes 250-254 registered in api.func and error_handler.func:
- 250: App download failed or version not determined
- 251: App file extraction failed (corrupt/incomplete archive)
- 252: App required file or resource not found
- 253: App data migration required — update aborted
- 254: App user declined prompt or input timed out

Existing codes reused where applicable:
- 10: privileged/Docker required (unifi-os-server)
- 64: invalid user input (postgresql, tomcat)
- 71: system error (pulse useradd)
- 150: service failed to start (docker, npmplus)
- 153: build failed (booklore)
- 233: app not installed (evcc, endurain, grafana, loki, itsm-ng)
- 236: hardware not detected (unifi-os-server /dev/net/tun)
- 238: OS not supported (frigate)
2026-03-02 13:57:42 +01:00
13 changed files with 833 additions and 261 deletions

255
.github/workflows/push-json-to-pocketbase.yml generated vendored Normal file
View 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

View File

@@ -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
View File

@@ -0,0 +1,6 @@
____ ____ _ _______
/ __ \____ _ _____ _____/ __ \/ | / / ___/
/ /_/ / __ \ | /| / / _ \/ ___/ / / / |/ /\__ \
/ ____/ /_/ / |/ |/ / __/ / / /_/ / /| /___/ /
/_/ \____/|__/|__/\___/_/ /_____/_/ |_//____/

68
ct/powerdns.sh Normal file
View 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}"

View File

@@ -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"

View File

@@ -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",

View 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 APIs.",
"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"
}
]
}

View File

@@ -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">

View File

@@ -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>
</> </>

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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:-}",