Compare commits

...

23 Commits

Author SHA1 Message Date
push-app-to-main[bot]
e97bf159d8 Add powerdns (ct) 2026-03-02 15:04:43 +00: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
37 changed files with 653 additions and 76 deletions

173
.github/workflows/push-json-to-pocketbase.yml generated vendored Normal file
View File

@@ -0,0 +1,173 @@
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) typeValueToId[item.type] = 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); }
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) 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;
if (existingId) {
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);
}
}
console.log('Done.');
})().catch(e => { console.error(e); process.exit(1); });
ENDSCRIPT
shell: bash

View File

@@ -418,10 +418,22 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
### 🚀 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

View File

@@ -39,7 +39,7 @@ function update_script() {
COMPOSE_FILE=$(find /opt/komodo -maxdepth 1 -type f -name '*.compose.yaml' ! -name 'compose.env' | head -n1) COMPOSE_FILE=$(find /opt/komodo -maxdepth 1 -type f -name '*.compose.yaml' ! -name 'compose.env' | head -n1)
if [[ -z "$COMPOSE_FILE" ]]; then if [[ -z "$COMPOSE_FILE" ]]; then
msg_error "No valid compose file found in /opt/komodo!" msg_error "No valid compose file found in /opt/komodo!"
exit 1 exit 252
fi fi
$STD docker compose -p komodo -f "$COMPOSE_FILE" --env-file /opt/komodo/compose.env pull $STD docker compose -p komodo -f "$COMPOSE_FILE" --env-file /opt/komodo/compose.env pull
$STD docker compose -p komodo -f "$COMPOSE_FILE" --env-file /opt/komodo/compose.env up -d $STD docker compose -p komodo -f "$COMPOSE_FILE" --env-file /opt/komodo/compose.env up -d

View File

@@ -26,7 +26,7 @@ function update_script() {
if [[ ! -d /opt/endurain ]]; then if [[ ! -d /opt/endurain ]]; then
msg_error "No ${APP} installation found!" msg_error "No ${APP} installation found!"
exit 1 exit 233
fi fi
if check_for_gh_release "endurain" "endurain-project/endurain"; then if check_for_gh_release "endurain" "endurain-project/endurain"; then
msg_info "Stopping Service" msg_info "Stopping Service"

View File

@@ -25,7 +25,7 @@ function update_script() {
check_container_resources check_container_resources
if ! command -v evcc >/dev/null 2>&1; then if ! command -v evcc >/dev/null 2>&1; then
msg_error "No ${APP} Installation Found!" msg_error "No ${APP} Installation Found!"
exit 1 exit 233
fi fi
if [[ -f /etc/apt/sources.list.d/evcc-stable.list ]]; then if [[ -f /etc/apt/sources.list.d/evcc-stable.list ]]; then

View File

@@ -26,7 +26,7 @@ function update_script() {
if ! dpkg -s grafana >/dev/null 2>&1; then if ! dpkg -s grafana >/dev/null 2>&1; then
msg_error "No ${APP} Installation Found!" msg_error "No ${APP} Installation Found!"
exit 1 exit 233
fi fi
if [[ -f /etc/apt/sources.list.d/grafana.list ]] || [[ ! -f /etc/apt/sources.list.d/grafana.sources ]]; then if [[ -f /etc/apt/sources.list.d/grafana.list ]] || [[ ! -f /etc/apt/sources.list.d/grafana.sources ]]; then

View File

@@ -26,7 +26,7 @@ function update_script() {
if [[ ! -f /etc/itsm-ng/config_db.php ]]; then if [[ ! -f /etc/itsm-ng/config_db.php ]]; then
msg_error "No ${APP} Installation Found!" msg_error "No ${APP} Installation Found!"
exit 1 exit 233
fi fi
setup_mariadb setup_mariadb

View File

@@ -45,7 +45,7 @@ function update_script() {
if [[ -z "$KASM_URL" ]] || [[ -z "$KASM_VERSION" ]]; then if [[ -z "$KASM_URL" ]] || [[ -z "$KASM_VERSION" ]]; then
msg_error "Unable to detect latest Kasm release URL." msg_error "Unable to detect latest Kasm release URL."
exit 1 exit 250
fi fi
msg_info "Checked for new version" msg_info "Checked for new version"

View File

@@ -43,7 +43,7 @@ function update_script() {
COMPOSE_FILE=$(find /opt/komodo -maxdepth 1 -type f -name '*.compose.yaml' ! -name 'compose.env' | head -n1) COMPOSE_FILE=$(find /opt/komodo -maxdepth 1 -type f -name '*.compose.yaml' ! -name 'compose.env' | head -n1)
if [[ -z "$COMPOSE_FILE" ]]; then if [[ -z "$COMPOSE_FILE" ]]; then
msg_error "No valid compose file found in /opt/komodo!" msg_error "No valid compose file found in /opt/komodo!"
exit 1 exit 252
fi fi
$STD docker compose -p komodo -f "$COMPOSE_FILE" --env-file /opt/komodo/compose.env pull $STD docker compose -p komodo -f "$COMPOSE_FILE" --env-file /opt/komodo/compose.env pull
$STD docker compose -p komodo -f "$COMPOSE_FILE" --env-file /opt/komodo/compose.env up -d $STD docker compose -p komodo -f "$COMPOSE_FILE" --env-file /opt/komodo/compose.env up -d

View File

@@ -26,7 +26,7 @@ function update_script() {
if ! dpkg -s loki >/dev/null 2>&1; then if ! dpkg -s loki >/dev/null 2>&1; then
msg_error "No ${APP} Installation Found!" msg_error "No ${APP} Installation Found!"
exit 1 exit 233
fi fi
CHOICE=$(msg_menu "Loki Update Options" \ CHOICE=$(msg_menu "Loki Update Options" \

View File

@@ -44,7 +44,7 @@ function update_script() {
echo -e "${TAB}${GATEWAY}${BGN}https://github.com/community-scripts/ProxmoxVE/discussions/9223${CL}" echo -e "${TAB}${GATEWAY}${BGN}https://github.com/community-scripts/ProxmoxVE/discussions/9223${CL}"
echo -e "" echo -e ""
msg_custom "⚠️" "Update aborted. Please migrate your data first." msg_custom "⚠️" "Update aborted. Please migrate your data first."
exit 1 exit 253
fi fi
fi fi

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

@@ -34,14 +34,14 @@ function update_script() {
msg_warn "This requires MANUAL config changes in /etc/vikunja/config.yml." msg_warn "This requires MANUAL config changes in /etc/vikunja/config.yml."
msg_warn "See: https://vikunja.io/changelog/whats-new-in-vikunja-1.0.0/#config-changes" msg_warn "See: https://vikunja.io/changelog/whats-new-in-vikunja-1.0.0/#config-changes"
read -rp "Continue with update? (y to proceed): " -t 30 CONFIRM1 || exit 1 read -rp "Continue with update? (y to proceed): " -t 30 CONFIRM1 || exit 254
[[ "$CONFIRM1" =~ ^[yY]$ ]] || exit 0 [[ "$CONFIRM1" =~ ^[yY]$ ]] || exit 0
echo echo
msg_warn "Vikunja may not start after the update until you manually adjust the config." msg_warn "Vikunja may not start after the update until you manually adjust the config."
msg_warn "Details: https://vikunja.io/changelog/whats-new-in-vikunja-1.0.0/#config-changes" msg_warn "Details: https://vikunja.io/changelog/whats-new-in-vikunja-1.0.0/#config-changes"
read -rp "Acknowledge and continue? (y): " -t 30 CONFIRM2 || exit 1 read -rp "Acknowledge and continue? (y): " -t 30 CONFIRM2 || exit 254
[[ "$CONFIRM2" =~ ^[yY]$ ]] || exit 0 [[ "$CONFIRM2" =~ ^[yY]$ ]] || exit 0
fi fi

View File

@@ -13,7 +13,7 @@
"website": "https://2fauth.app/", "website": "https://2fauth.app/",
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/2fauth.webp", "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/2fauth.webp",
"config_path": "cat /opt/2fauth/.env", "config_path": "cat /opt/2fauth/.env",
"description": "2FAuth is a web based self-hosted alternative to One Time Passcode (OTP) generators like Google Authenticator, designed for both mobile and desktop. It aims to ease you perform your 2FA authentication steps whatever the device you handle, with a clean and suitable interface.", "description": "2FAuth is a web based self-hosted alternative to One Time Passcode (OTP) generators like Google Authenticator, designed for both mobile and desktop. It aims to ease you perform your 2FA authentication steps whatever the device you handle, with a clean and suitable interface. - Workflow - Test",
"install_methods": [ "install_methods": [
{ {
"type": "default", "type": "default",

View File

@@ -1,5 +1,5 @@
{ {
"generated": "2026-03-02T12:12:21Z", "generated": "2026-03-02T13:48:26Z",
"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",

View File

@@ -0,0 +1,40 @@
{
"name": "PowerDNS",
"slug": "powerdns",
"categories": [
5
],
"date_created": "2026-02-11",
"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

@@ -14,7 +14,7 @@ network_check
update_os update_os
read -r -p "${TAB3}Enter PostgreSQL version (15/16/17): " ver read -r -p "${TAB3}Enter PostgreSQL version (15/16/17): " ver
[[ $ver =~ ^(15|16|17)$ ]] || { echo "Invalid version"; exit 1; } [[ $ver =~ ^(15|16|17)$ ]] || { echo "Invalid version"; exit 64; }
msg_info "Installing PostgreSQL ${ver}" msg_info "Installing PostgreSQL ${ver}"
$STD apk add --no-cache postgresql${ver} postgresql${ver}-contrib postgresql${ver}-openrc sudo $STD apk add --no-cache postgresql${ver} postgresql${ver}-contrib postgresql${ver}-openrc sudo

View File

@@ -25,7 +25,7 @@ case $version in
;; ;;
*) *)
msg_error "Invalid JDK version selected. Please enter 8, 11, 17 or 21." msg_error "Invalid JDK version selected. Please enter 8, 11, 17 or 21."
exit 1 exit 64
;; ;;
esac esac
;; ;;
@@ -39,7 +39,7 @@ case $version in
;; ;;
*) *)
msg_error "Invalid JDK version selected. Please enter 11, 17 or 21." msg_error "Invalid JDK version selected. Please enter 11, 17 or 21."
exit 1 exit 64
;; ;;
esac esac
;; ;;
@@ -53,13 +53,13 @@ case $version in
;; ;;
*) *)
msg_error "Invalid JDK version selected. Please enter 17 or 21." msg_error "Invalid JDK version selected. Please enter 17 or 21."
exit 1 exit 64
;; ;;
esac esac
;; ;;
*) *)
msg_error "Invalid Tomcat version selected. Please enter 9, 10.1 or 11." msg_error "Invalid Tomcat version selected. Please enter 9, 10.1 or 11."
exit 1 exit 64
;; ;;
esac esac

View File

@@ -59,7 +59,7 @@ mkdir -p /opt/booklore/dist
JAR_PATH=$(find /opt/booklore/booklore-api/build/libs -maxdepth 1 -type f -name "booklore-api-*.jar" ! -name "*plain*" | head -n1) JAR_PATH=$(find /opt/booklore/booklore-api/build/libs -maxdepth 1 -type f -name "booklore-api-*.jar" ! -name "*plain*" | head -n1)
if [[ -z "$JAR_PATH" ]]; then if [[ -z "$JAR_PATH" ]]; then
msg_error "Backend JAR not found" msg_error "Backend JAR not found"
exit 1 exit 153
fi fi
cp "$JAR_PATH" /opt/booklore/dist/app.jar cp "$JAR_PATH" /opt/booklore/dist/app.jar
msg_ok "Built Backend" msg_ok "Built Backend"

View File

@@ -86,7 +86,7 @@ EOF
msg_ok "Docker TCP socket available on $socket" msg_ok "Docker TCP socket available on $socket"
else else
msg_error "Docker failed to restart. Check journalctl -xeu docker.service" msg_error "Docker failed to restart. Check journalctl -xeu docker.service"
exit 1 exit 150
fi fi
fi fi

View File

@@ -21,7 +21,7 @@ msg_info "Fetching latest EMQX Enterprise version"
LATEST_VERSION=$(curl -fsSL https://www.emqx.com/en/downloads/enterprise | grep -oP '/en/downloads/enterprise/v\K[0-9]+\.[0-9]+\.[0-9]+' | sort -V | tail -n1) LATEST_VERSION=$(curl -fsSL https://www.emqx.com/en/downloads/enterprise | grep -oP '/en/downloads/enterprise/v\K[0-9]+\.[0-9]+\.[0-9]+' | sort -V | tail -n1)
if [[ -z "$LATEST_VERSION" ]]; then if [[ -z "$LATEST_VERSION" ]]; then
msg_error "Failed to determine latest EMQX version" msg_error "Failed to determine latest EMQX version"
exit 1 exit 250
fi fi
msg_ok "Latest version: v$LATEST_VERSION" msg_ok "Latest version: v$LATEST_VERSION"

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
@@ -17,7 +16,7 @@ update_os
source /etc/os-release source /etc/os-release
if [[ "$VERSION_ID" != "12" ]]; then if [[ "$VERSION_ID" != "12" ]]; then
msg_error "Frigate requires Debian 12 (Bookworm) due to Python 3.11 dependencies" msg_error "Frigate requires Debian 12 (Bookworm) due to Python 3.11 dependencies"
exit 1 exit 238
fi fi
msg_info "Converting APT sources to DEB822 format" msg_info "Converting APT sources to DEB822 format"
@@ -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

View File

@@ -31,7 +31,7 @@ fi
if [[ -z "$KASM_URL" ]] || [[ -z "$KASM_VERSION" ]]; then if [[ -z "$KASM_URL" ]] || [[ -z "$KASM_VERSION" ]]; then
msg_error "Unable to detect latest Kasm release URL." msg_error "Unable to detect latest Kasm release URL."
exit 1 exit 250
fi fi
msg_ok "Detected Kasm Workspaces version $KASM_VERSION" msg_ok "Detected Kasm Workspaces version $KASM_VERSION"

View File

@@ -51,7 +51,7 @@ while true; do
attempts=$((attempts + 1)) attempts=$((attempts + 1))
if [[ "$attempts" -ge 3 ]]; then if [[ "$attempts" -ge 3 ]]; then
msg_error "Maximum attempts reached. Exiting." msg_error "Maximum attempts reached. Exiting."
exit 1 exit 254
fi fi
done done
@@ -76,11 +76,11 @@ for i in {1..60}; do
elif [[ "$STATUS" == "unhealthy" ]]; then elif [[ "$STATUS" == "unhealthy" ]]; then
msg_error "NPMplus container is unhealthy! Check logs." msg_error "NPMplus container is unhealthy! Check logs."
docker logs "$CONTAINER_ID" docker logs "$CONTAINER_ID"
exit 1 exit 150
fi fi
fi fi
sleep 2 sleep 2
[[ $i -eq 60 ]] && msg_error "NPMplus container did not become healthy within 120s." && docker logs "$CONTAINER_ID" && exit 1 [[ $i -eq 60 ]] && msg_error "NPMplus container did not become healthy within 120s." && docker logs "$CONTAINER_ID" && exit 150
done done
msg_ok "Builded and started NPMplus" msg_ok "Builded and started NPMplus"

View File

@@ -78,11 +78,11 @@ if curl -fL# -C - -o "$TMP_TAR" "$OLLAMA_URL"; then
msg_ok "Installed Ollama ${RELEASE}" msg_ok "Installed Ollama ${RELEASE}"
else else
msg_error "Extraction failed archive corrupt or incomplete" msg_error "Extraction failed archive corrupt or incomplete"
exit 1 exit 251
fi fi
else else
msg_error "Download failed $OLLAMA_URL not reachable" msg_error "Download failed $OLLAMA_URL not reachable"
exit 1 exit 250
fi fi
msg_info "Creating ollama User and Group" msg_info "Creating ollama User and Group"

View File

@@ -59,7 +59,7 @@ EOF
else else
msg_error "Failed to download or verify GPG key from $KEY_URL" msg_error "Failed to download or verify GPG key from $KEY_URL"
[[ -f "$TMP_KEY_CONTENT" ]] && rm -f "$TMP_KEY_CONTENT" [[ -f "$TMP_KEY_CONTENT" ]] && rm -f "$TMP_KEY_CONTENT"
exit 1 exit 250
fi fi
rm -f "$TMP_KEY_CONTENT" rm -f "$TMP_KEY_CONTENT"

View File

@@ -34,7 +34,7 @@ for server in "${servers[@]}"; do
done done
if ((attempt >= MAX_ATTEMPTS)); then if ((attempt >= MAX_ATTEMPTS)); then
msg_error "No more attempts - aborting script!" msg_error "No more attempts - aborting script!"
exit 1 exit 254
fi fi
done done

View File

@@ -16,7 +16,7 @@ update_os
read -r -p "${TAB3}Enter PostgreSQL version (15/16/17/18): " ver read -r -p "${TAB3}Enter PostgreSQL version (15/16/17/18): " ver
[[ $ver =~ ^(15|16|17|18)$ ]] || { [[ $ver =~ ^(15|16|17|18)$ ]] || {
echo "Invalid version" echo "Invalid version"
exit 1 exit 64
} }
PG_VERSION=$ver setup_postgresql PG_VERSION=$ver setup_postgresql

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

@@ -25,7 +25,7 @@ if useradd -r -m -d /opt/pulse-home -s /usr/sbin/nologin pulse; then
msg_ok "Created User" msg_ok "Created User"
else else
msg_error "User creation failed" msg_error "User creation failed"
exit 1 exit 71
fi fi
mkdir -p /etc/pulse mkdir -p /etc/pulse

View File

@@ -34,7 +34,7 @@ while true; do
[Nn]|[Nn][Oo]|"") [Nn]|[Nn][Oo]|"")
msg_error "Terms not accepted. Installation cannot proceed." msg_error "Terms not accepted. Installation cannot proceed."
msg_error "Please review the terms and run the script again if you wish to proceed." msg_error "Please review the terms and run the script again if you wish to proceed."
exit 1 exit 254
;; ;;
*) *)
msg_error "Invalid response. Please enter 'y' for yes or 'n' for no." msg_error "Invalid response. Please enter 'y' for yes or 'n' for no."
@@ -47,7 +47,7 @@ DOWNLOAD_URL=$(curl -s "https://www.splunk.com/en_us/download/splunk-enterprise.
RELEASE=$(echo "$DOWNLOAD_URL" | sed 's|.*/releases/\([^/]*\)/.*|\1|') RELEASE=$(echo "$DOWNLOAD_URL" | sed 's|.*/releases/\([^/]*\)/.*|\1|')
$STD curl -fsSL -o "splunk-enterprise.tgz" "$DOWNLOAD_URL" || { $STD curl -fsSL -o "splunk-enterprise.tgz" "$DOWNLOAD_URL" || {
msg_error "Failed to download Splunk Enterprise from the provided link." msg_error "Failed to download Splunk Enterprise from the provided link."
exit 1 exit 250
} }
$STD tar -xzf "splunk-enterprise.tgz" -C /opt $STD tar -xzf "splunk-enterprise.tgz" -C /opt
rm -f "splunk-enterprise.tgz" rm -f "splunk-enterprise.tgz"

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

@@ -16,13 +16,13 @@ update_os
if [[ "${CTTYPE:-1}" != "0" ]]; then if [[ "${CTTYPE:-1}" != "0" ]]; then
msg_error "UniFi OS Server requires a privileged LXC container." msg_error "UniFi OS Server requires a privileged LXC container."
msg_error "Recreate the container with unprivileged=0." msg_error "Recreate the container with unprivileged=0."
exit 1 exit 10
fi fi
if [[ ! -e /dev/net/tun ]]; then if [[ ! -e /dev/net/tun ]]; then
msg_error "Missing /dev/net/tun in container." msg_error "Missing /dev/net/tun in container."
msg_error "Enable TUN/TAP (var_tun=yes) or add /dev/net/tun passthrough." msg_error "Enable TUN/TAP (var_tun=yes) or add /dev/net/tun passthrough."
exit 1 exit 236
fi fi
msg_info "Installing dependencies" msg_info "Installing dependencies"
@@ -48,7 +48,7 @@ TEMP_JSON="$(mktemp)"
if ! curl -fsSL "$API_URL" -o "$TEMP_JSON"; then if ! curl -fsSL "$API_URL" -o "$TEMP_JSON"; then
rm -f "$TEMP_JSON" rm -f "$TEMP_JSON"
msg_error "Failed to fetch data from Ubiquiti API" msg_error "Failed to fetch data from Ubiquiti API"
exit 1 exit 250
fi fi
LATEST=$(jq -r ' LATEST=$(jq -r '
._embedded.firmware ._embedded.firmware
@@ -62,7 +62,7 @@ UOS_URL=$(echo "$LATEST" | jq -r '._links.data.href')
rm -f "$TEMP_JSON" rm -f "$TEMP_JSON"
if [[ -z "$UOS_URL" || -z "$UOS_VERSION" || "$UOS_URL" == "null" ]]; then if [[ -z "$UOS_URL" || -z "$UOS_VERSION" || "$UOS_URL" == "null" ]]; then
msg_error "Failed to parse UniFi OS Server version or download URL" msg_error "Failed to parse UniFi OS Server version or download URL"
exit 1 exit 250
fi fi
msg_ok "Found UniFi OS Server ${UOS_VERSION}" msg_ok "Found UniFi OS Server ${UOS_VERSION}"

View File

@@ -31,7 +31,7 @@ if gpg --verify /tmp/zerotier-install.sh >/dev/null 2>&1; then
$STD bash /tmp/zerotier-install.sh $STD bash /tmp/zerotier-install.sh
else else
msg_warn "Could not verify signature of Zerotier-One install script. Exiting..." msg_warn "Could not verify signature of Zerotier-One install script. Exiting..."
exit 1 exit 250
fi fi
msg_ok "Setup Zerotier-One" msg_ok "Setup Zerotier-One"

View File

@@ -134,6 +134,7 @@ detect_repo_source
# * Proxmox custom codes (200-231) # * Proxmox custom codes (200-231)
# * Tools & Addon Scripts (232-238) # * Tools & Addon Scripts (232-238)
# * Node.js/npm errors (239, 243, 245-249) # * Node.js/npm errors (239, 243, 245-249)
# * Application Install/Update errors (250-254)
# - Returns description string for given exit code # - Returns description string for given exit code
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
explain_exit_code() { explain_exit_code() {
@@ -322,6 +323,13 @@ explain_exit_code() {
248) echo "Node.js: Invalid C++ addon / N-API failure" ;; 248) echo "Node.js: Invalid C++ addon / N-API failure" ;;
249) echo "npm/pnpm/yarn: Unknown fatal error" ;; 249) echo "npm/pnpm/yarn: Unknown fatal error" ;;
# --- Application Install/Update Errors (250-254) ---
250) echo "App: Download failed or version not determined" ;;
251) echo "App: File extraction failed (corrupt or incomplete archive)" ;;
252) echo "App: Required file or resource not found" ;;
253) echo "App: Data migration required — update aborted" ;;
254) echo "App: User declined prompt or input timed out" ;;
# --- DPKG --- # --- DPKG ---
255) echo "DPKG: Fatal internal error" ;; 255) echo "DPKG: Fatal internal error" ;;
@@ -385,6 +393,11 @@ get_error_text() {
logfile="$BUILD_LOG" logfile="$BUILD_LOG"
fi fi
# Try SILENT_LOGFILE as last resort (captures $STD command output)
if [[ -z "$logfile" || ! -s "$logfile" ]] && [[ -n "${SILENT_LOGFILE:-}" && -s "${SILENT_LOGFILE}" ]]; then
logfile="$SILENT_LOGFILE"
fi
if [[ -n "$logfile" && -s "$logfile" ]]; then if [[ -n "$logfile" && -s "$logfile" ]]; then
tail -n 20 "$logfile" 2>/dev/null | sed 's/\r$//' | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' tail -n 20 "$logfile" 2>/dev/null | sed 's/\r$//' | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g'
fi fi
@@ -430,6 +443,13 @@ get_full_log() {
fi fi
fi fi
# Fall back to SILENT_LOGFILE (captures $STD command output)
if [[ -z "$logfile" || ! -s "$logfile" ]]; then
if [[ -n "${SILENT_LOGFILE:-}" && -s "${SILENT_LOGFILE}" ]]; then
logfile="$SILENT_LOGFILE"
fi
fi
if [[ -n "$logfile" && -s "$logfile" ]]; then if [[ -n "$logfile" && -s "$logfile" ]]; then
# Strip ANSI codes, carriage returns, and anonymize IP addresses (GDPR) # Strip ANSI codes, carriage returns, and anonymize IP addresses (GDPR)
sed 's/\r$//' "$logfile" 2>/dev/null | sed 's/\r$//' "$logfile" 2>/dev/null |
@@ -667,18 +687,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
} }
@@ -769,10 +794,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
} }
@@ -868,7 +898,7 @@ post_update_to_api() {
esac esac
# For failed/unknown status, resolve exit code and error description # For failed/unknown status, resolve exit code and error description
local short_error="" local short_error="" medium_error=""
if [[ "$pb_status" == "failed" ]] || [[ "$pb_status" == "unknown" ]]; then if [[ "$pb_status" == "failed" ]] || [[ "$pb_status" == "unknown" ]]; then
if [[ "$raw_exit_code" =~ ^[0-9]+$ ]]; then if [[ "$raw_exit_code" =~ ^[0-9]+$ ]]; then
exit_code="$raw_exit_code" exit_code="$raw_exit_code"
@@ -888,6 +918,18 @@ post_update_to_api() {
short_error=$(json_escape "$(explain_exit_code "$exit_code")") short_error=$(json_escape "$(explain_exit_code "$exit_code")")
error_category=$(categorize_error "$exit_code") error_category=$(categorize_error "$exit_code")
[[ -z "$error" ]] && error="Unknown error" [[ -z "$error" ]] && error="Unknown error"
# Build medium error for attempt 2: explanation + last 100 log lines (≤16KB)
# This is the critical middle ground between full 120KB log and generic-only description
local medium_log=""
medium_log=$(get_full_log 16384) || true # 16KB max
if [[ -z "$medium_log" ]]; then
medium_log=$(get_error_text) || true
fi
local medium_full
medium_full=$(build_error_string "$exit_code" "$medium_log")
medium_error=$(json_escape "$medium_full")
[[ -z "$medium_error" ]] && medium_error="$short_error"
fi fi
# Calculate duration if timer was started # Calculate duration if timer was started
@@ -904,6 +946,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=$(
@@ -915,7 +962,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:-}",
@@ -946,7 +993,7 @@ EOF
return 0 return 0
fi fi
# ── Attempt 2: Short error text (no full log) ── # ── Attempt 2: Medium error text (truncated log ≤16KB instead of full 120KB) ──
sleep 1 sleep 1
local RETRY_PAYLOAD local RETRY_PAYLOAD
RETRY_PAYLOAD=$( RETRY_PAYLOAD=$(
@@ -958,7 +1005,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:-}",
@@ -966,7 +1013,7 @@ EOF
"pve_version": "${pve_version}", "pve_version": "${pve_version}",
"method": "${METHOD:-default}", "method": "${METHOD:-default}",
"exit_code": ${exit_code}, "exit_code": ${exit_code},
"error": "${short_error}", "error": "${medium_error}",
"error_category": "${error_category}", "error_category": "${error_category}",
"install_duration": ${duration}, "install_duration": ${duration},
"cpu_vendor": "${cpu_vendor}", "cpu_vendor": "${cpu_vendor}",
@@ -989,7 +1036,7 @@ EOF
return 0 return 0
fi fi
# ── Attempt 3: Minimal payload (bare minimum to set status) ── # ── Attempt 3: Minimal payload with medium error (bare minimum to set status) ──
sleep 2 sleep 2
local MINIMAL_PAYLOAD local MINIMAL_PAYLOAD
MINIMAL_PAYLOAD=$( MINIMAL_PAYLOAD=$(
@@ -1001,7 +1048,7 @@ EOF
"nsapp": "${NSAPP:-unknown}", "nsapp": "${NSAPP:-unknown}",
"status": "${pb_status}", "status": "${pb_status}",
"exit_code": ${exit_code}, "exit_code": ${exit_code},
"error": "${short_error}", "error": "${medium_error}",
"error_category": "${error_category}", "error_category": "${error_category}",
"install_duration": ${duration} "install_duration": ${duration}
} }

View File

@@ -195,6 +195,14 @@ if ! declare -f explain_exit_code &>/dev/null; then
247) echo "Node.js: Fatal internal error" ;; 247) echo "Node.js: Fatal internal error" ;;
248) echo "Node.js: Invalid C++ addon / N-API failure" ;; 248) echo "Node.js: Invalid C++ addon / N-API failure" ;;
249) echo "npm/pnpm/yarn: Unknown fatal error" ;; 249) echo "npm/pnpm/yarn: Unknown fatal error" ;;
# --- Application Install/Update Errors (250-254) ---
250) echo "App: Download failed or version not determined" ;;
251) echo "App: File extraction failed (corrupt or incomplete archive)" ;;
252) echo "App: Required file or resource not found" ;;
253) echo "App: Data migration required — update aborted" ;;
254) echo "App: User declined prompt or input timed out" ;;
255) echo "DPKG: Fatal internal error" ;; 255) echo "DPKG: Fatal internal error" ;;
*) echo "Unknown error" ;; *) echo "Unknown error" ;;
esac esac
@@ -400,10 +408,29 @@ _send_abort_telemetry() {
[[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0 [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0
[[ -z "${RANDOM_UUID:-}" ]] && return 0 [[ -z "${RANDOM_UUID:-}" ]] && return 0
# Collect last 20 log lines for error diagnosis (best-effort) # Collect last 200 log lines for error diagnosis (best-effort)
# Container context has no get_full_log(), so we gather as much as possible
local error_text="" local error_text=""
local logfile=""
if [[ -n "${INSTALL_LOG:-}" && -s "${INSTALL_LOG}" ]]; then if [[ -n "${INSTALL_LOG:-}" && -s "${INSTALL_LOG}" ]]; then
error_text=$(tail -n 20 "$INSTALL_LOG" 2>/dev/null | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g; s/\\/\\\\/g; s/"/\\"/g; s/\r//g' | tr '\n' '|' | sed 's/|$//' | tr -d '\000-\010\013\014\016-\037\177') || true logfile="${INSTALL_LOG}"
elif [[ -n "${SILENT_LOGFILE:-}" && -s "${SILENT_LOGFILE}" ]]; then
logfile="${SILENT_LOGFILE}"
fi
if [[ -n "$logfile" ]]; then
error_text=$(tail -n 200 "$logfile" 2>/dev/null | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g; s/\\/\\\\/g; s/"/\\"/g; s/\r//g' | tr '\n' '|' | sed 's/|$//' | head -c 16384 | tr -d '\000-\010\013\014\016-\037\177') || true
fi
# Prepend exit code explanation header (like build_error_string does on host)
local explanation=""
if declare -f explain_exit_code &>/dev/null; then
explanation=$(explain_exit_code "$exit_code" 2>/dev/null) || true
fi
if [[ -n "$explanation" && -n "$error_text" ]]; then
error_text="exit_code=${exit_code} | ${explanation}|---|${error_text}"
elif [[ -n "$explanation" && -z "$error_text" ]]; then
error_text="exit_code=${exit_code} | ${explanation}"
fi fi
# Calculate duration if start time is available # Calculate duration if start time is available
@@ -412,10 +439,17 @@ _send_abort_telemetry() {
duration=$(($(date +%s) - DIAGNOSTICS_START_TIME)) duration=$(($(date +%s) - DIAGNOSTICS_START_TIME))
fi fi
# Categorize error if function is available (may not be in minimal container context)
local error_category=""
if declare -f categorize_error &>/dev/null; then
error_category=$(categorize_error "$exit_code" 2>/dev/null) || true
fi
# Build JSON payload with error context # Build JSON payload with error context
local payload local payload
payload="{\"random_id\":\"${RANDOM_UUID}\",\"execution_id\":\"${EXECUTION_ID:-${RANDOM_UUID}}\",\"type\":\"${TELEMETRY_TYPE:-lxc}\",\"nsapp\":\"${NSAPP:-${app:-unknown}}\",\"status\":\"failed\",\"exit_code\":${exit_code}" payload="{\"random_id\":\"${RANDOM_UUID}\",\"execution_id\":\"${EXECUTION_ID:-${RANDOM_UUID}}\",\"type\":\"${TELEMETRY_TYPE:-lxc}\",\"nsapp\":\"${NSAPP:-${app:-unknown}}\",\"status\":\"failed\",\"exit_code\":${exit_code}"
[[ -n "$error_text" ]] && payload="${payload},\"error\":\"${error_text}\"" [[ -n "$error_text" ]] && payload="${payload},\"error\":\"${error_text}\""
[[ -n "$error_category" ]] && payload="${payload},\"error_category\":\"${error_category}\""
[[ -n "$duration" ]] && payload="${payload},\"duration\":${duration}" [[ -n "$duration" ]] && payload="${payload},\"duration\":${duration}"
payload="${payload}}" payload="${payload}}"