mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2026-03-04 09:55:59 +01:00
Compare commits
36 Commits
MickLesk-p
...
tinyauth
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6d7cec2a3 | ||
|
|
f9b59d7634 | ||
|
|
f279536eb7 | ||
|
|
8f34c7cd2e | ||
|
|
f922f669a8 | ||
|
|
2694d60faf | ||
|
|
a326994459 | ||
|
|
4c8d1ef010 | ||
|
|
86a9a0aac6 | ||
|
|
5059ed2320 | ||
|
|
df65c60fe4 | ||
|
|
cff2c90041 | ||
|
|
88d1494e46 | ||
|
|
8b62b8f3c5 | ||
|
|
cd38bc3a65 | ||
|
|
46d25645c2 | ||
|
|
3701737eff | ||
|
|
b39e296684 | ||
|
|
d8a7620c64 | ||
|
|
7d5900de18 | ||
|
|
e8b3b936df | ||
|
|
5c246310f4 | ||
|
|
bdad2cc941 | ||
|
|
f23c33fee7 | ||
|
|
5b207cf5bd | ||
|
|
f20c9e4ec9 | ||
|
|
1398ff8397 | ||
|
|
ebc3512f50 | ||
|
|
564a8136a5 | ||
|
|
00047c95b8 | ||
|
|
9849ce79a7 | ||
|
|
cea9858193 | ||
|
|
ee8ea672ef | ||
|
|
fc59910bd2 | ||
|
|
20ab7bc005 | ||
|
|
17de8e761b |
255
.github/workflows/push-json-to-pocketbase.yml
generated
vendored
Normal file
255
.github/workflows/push-json-to-pocketbase.yml
generated
vendored
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
name: Push JSON changes to PocketBase
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "frontend/public/json/**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
push-json:
|
||||||
|
runs-on: self-hosted
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Get changed JSON files with slug
|
||||||
|
id: changed
|
||||||
|
run: |
|
||||||
|
changed=$(git diff --name-only "${{ github.event.before }}" "${{ github.event.after }}" -- frontend/public/json/ | grep '\.json$' || true)
|
||||||
|
with_slug=""
|
||||||
|
for f in $changed; do
|
||||||
|
[[ -f "$f" ]] || continue
|
||||||
|
jq -e '.slug' "$f" >/dev/null 2>&1 && with_slug="$with_slug $f"
|
||||||
|
done
|
||||||
|
with_slug=$(echo $with_slug | xargs -n1)
|
||||||
|
if [[ -z "$with_slug" ]]; then
|
||||||
|
echo "No app JSON files changed (or no files with slug)."
|
||||||
|
echo "count=0" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "$with_slug" > changed_app_jsons.txt
|
||||||
|
echo "count=$(echo "$with_slug" | wc -w)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Push to PocketBase
|
||||||
|
if: steps.changed.outputs.count != '0'
|
||||||
|
env:
|
||||||
|
POCKETBASE_URL: ${{ secrets.POCKETBASE_URL }}
|
||||||
|
POCKETBASE_COLLECTION: ${{ secrets.POCKETBASE_COLLECTION }}
|
||||||
|
POCKETBASE_ADMIN_EMAIL: ${{ secrets.POCKETBASE_ADMIN_EMAIL }}
|
||||||
|
POCKETBASE_ADMIN_PASSWORD: ${{ secrets.POCKETBASE_ADMIN_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
node << 'ENDSCRIPT'
|
||||||
|
(async function() {
|
||||||
|
const fs = require('fs');
|
||||||
|
const https = require('https');
|
||||||
|
const http = require('http');
|
||||||
|
const url = require('url');
|
||||||
|
function request(fullUrl, opts) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
const u = url.parse(fullUrl);
|
||||||
|
const isHttps = u.protocol === 'https:';
|
||||||
|
const body = opts.body;
|
||||||
|
const options = {
|
||||||
|
hostname: u.hostname,
|
||||||
|
port: u.port || (isHttps ? 443 : 80),
|
||||||
|
path: u.path,
|
||||||
|
method: opts.method || 'GET',
|
||||||
|
headers: opts.headers || {}
|
||||||
|
};
|
||||||
|
if (body) options.headers['Content-Length'] = Buffer.byteLength(body);
|
||||||
|
const lib = isHttps ? https : http;
|
||||||
|
const req = lib.request(options, function(res) {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', function(chunk) { data += chunk; });
|
||||||
|
res.on('end', function() {
|
||||||
|
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, statusCode: res.statusCode, body: data });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
if (body) req.write(body);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const raw = process.env.POCKETBASE_URL.replace(/\/$/, '');
|
||||||
|
const apiBase = /\/api$/i.test(raw) ? raw : raw + '/api';
|
||||||
|
const coll = process.env.POCKETBASE_COLLECTION;
|
||||||
|
const files = fs.readFileSync('changed_app_jsons.txt', 'utf8').trim().split(/\s+/).filter(Boolean);
|
||||||
|
const authUrl = apiBase + '/collections/users/auth-with-password';
|
||||||
|
console.log('Auth URL: ' + authUrl);
|
||||||
|
const authBody = JSON.stringify({
|
||||||
|
identity: process.env.POCKETBASE_ADMIN_EMAIL,
|
||||||
|
password: process.env.POCKETBASE_ADMIN_PASSWORD
|
||||||
|
});
|
||||||
|
const authRes = await request(authUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: authBody
|
||||||
|
});
|
||||||
|
if (!authRes.ok) {
|
||||||
|
throw new Error('Auth failed. Tried: ' + authUrl + ' - Verify POST to that URL with body {"identity":"...","password":"..."} works. Response: ' + authRes.body);
|
||||||
|
}
|
||||||
|
const token = JSON.parse(authRes.body).token;
|
||||||
|
const recordsUrl = apiBase + '/collections/' + encodeURIComponent(coll) + '/records';
|
||||||
|
let categoryIdToName = {};
|
||||||
|
try {
|
||||||
|
const metadata = JSON.parse(fs.readFileSync('frontend/public/json/metadata.json', 'utf8'));
|
||||||
|
(metadata.categories || []).forEach(function(cat) { categoryIdToName[cat.id] = cat.name; });
|
||||||
|
} catch (e) { console.warn('Could not load metadata.json:', e.message); }
|
||||||
|
let typeValueToId = {};
|
||||||
|
let categoryNameToPbId = {};
|
||||||
|
try {
|
||||||
|
const typesRes = await request(apiBase + '/collections/z_ref_script_types/records?perPage=500', { headers: { 'Authorization': token } });
|
||||||
|
if (typesRes.ok) {
|
||||||
|
const typesData = JSON.parse(typesRes.body);
|
||||||
|
(typesData.items || []).forEach(function(item) {
|
||||||
|
if (item.type != null) typeValueToId[item.type] = item.id;
|
||||||
|
if (item.name != null) typeValueToId[item.name] = item.id;
|
||||||
|
if (item.value != null) typeValueToId[item.value] = item.id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) { console.warn('Could not fetch z_ref_script_types:', e.message); }
|
||||||
|
try {
|
||||||
|
const catRes = await request(apiBase + '/collections/script_categories/records?perPage=500', { headers: { 'Authorization': token } });
|
||||||
|
if (catRes.ok) {
|
||||||
|
const catData = JSON.parse(catRes.body);
|
||||||
|
(catData.items || []).forEach(function(item) { if (item.name) categoryNameToPbId[item.name] = item.id; });
|
||||||
|
}
|
||||||
|
} catch (e) { console.warn('Could not fetch script_categories:', e.message); }
|
||||||
|
var noteTypeToId = {};
|
||||||
|
var installMethodTypeToId = {};
|
||||||
|
var osToId = {};
|
||||||
|
var osVersionToId = {};
|
||||||
|
try {
|
||||||
|
const res = await request(apiBase + '/collections/z_ref_note_types/records?perPage=500', { headers: { 'Authorization': token } });
|
||||||
|
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.type != null) noteTypeToId[item.type] = item.id; });
|
||||||
|
} catch (e) { console.warn('z_ref_note_types:', e.message); }
|
||||||
|
try {
|
||||||
|
const res = await request(apiBase + '/collections/z_ref_install_method_types/records?perPage=500', { headers: { 'Authorization': token } });
|
||||||
|
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.type != null) installMethodTypeToId[item.type] = item.id; });
|
||||||
|
} catch (e) { console.warn('z_ref_install_method_types:', e.message); }
|
||||||
|
try {
|
||||||
|
const res = await request(apiBase + '/collections/z_ref_os/records?perPage=500', { headers: { 'Authorization': token } });
|
||||||
|
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.os != null) osToId[item.os] = item.id; });
|
||||||
|
} catch (e) { console.warn('z_ref_os:', e.message); }
|
||||||
|
try {
|
||||||
|
const res = await request(apiBase + '/collections/z_ref_os_version/records?perPage=500&expand=os', { headers: { 'Authorization': token } });
|
||||||
|
if (res.ok) {
|
||||||
|
(JSON.parse(res.body).items || []).forEach(function(item) {
|
||||||
|
var osName = item.expand && item.expand.os && item.expand.os.os != null ? item.expand.os.os : null;
|
||||||
|
if (osName != null && item.version != null) osVersionToId[osName + '|' + item.version] = item.id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) { console.warn('z_ref_os_version:', e.message); }
|
||||||
|
var notesCollUrl = apiBase + '/collections/script_notes/records';
|
||||||
|
var installMethodsCollUrl = apiBase + '/collections/script_install_methods/records';
|
||||||
|
for (const file of files) {
|
||||||
|
if (!fs.existsSync(file)) continue;
|
||||||
|
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||||
|
if (!data.slug) { console.log('Skipping', file, '(no slug)'); continue; }
|
||||||
|
var payload = {
|
||||||
|
name: data.name,
|
||||||
|
slug: data.slug,
|
||||||
|
script_created: data.date_created || data.script_created,
|
||||||
|
script_updated: data.date_created || data.script_updated,
|
||||||
|
updateable: data.updateable,
|
||||||
|
privileged: data.privileged,
|
||||||
|
port: data.interface_port != null ? data.interface_port : data.port,
|
||||||
|
documentation: data.documentation,
|
||||||
|
website: data.website,
|
||||||
|
logo: data.logo,
|
||||||
|
description: data.description,
|
||||||
|
config_path: data.config_path,
|
||||||
|
default_user: (data.default_credentials && data.default_credentials.username) || data.default_user,
|
||||||
|
default_passwd: (data.default_credentials && data.default_credentials.password) || data.default_passwd,
|
||||||
|
is_dev: false
|
||||||
|
};
|
||||||
|
var resolvedType = typeValueToId[data.type];
|
||||||
|
if (resolvedType == null && data.type === 'ct') resolvedType = typeValueToId['lxc'];
|
||||||
|
if (resolvedType) payload.type = resolvedType;
|
||||||
|
var resolvedCats = (data.categories || []).map(function(n) { return categoryNameToPbId[categoryIdToName[n]]; }).filter(Boolean);
|
||||||
|
if (resolvedCats.length) payload.categories = resolvedCats;
|
||||||
|
if (data.version !== undefined) payload.version = data.version;
|
||||||
|
if (data.changelog !== undefined) payload.changelog = data.changelog;
|
||||||
|
if (data.screenshots !== undefined) payload.screenshots = data.screenshots;
|
||||||
|
const filter = "(slug='" + data.slug + "')";
|
||||||
|
const listRes = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1', {
|
||||||
|
headers: { 'Authorization': token }
|
||||||
|
});
|
||||||
|
const list = JSON.parse(listRes.body);
|
||||||
|
const existingId = list.items && list.items[0] && list.items[0].id;
|
||||||
|
async function resolveNotesAndInstallMethods(scriptId) {
|
||||||
|
var noteIds = [];
|
||||||
|
for (var i = 0; i < (data.notes || []).length; i++) {
|
||||||
|
var note = data.notes[i];
|
||||||
|
var typeId = noteTypeToId[note.type];
|
||||||
|
if (typeId == null) continue;
|
||||||
|
var postRes = await request(notesCollUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ text: note.text || '', type: typeId })
|
||||||
|
});
|
||||||
|
if (postRes.ok) noteIds.push(JSON.parse(postRes.body).id);
|
||||||
|
}
|
||||||
|
var installMethodIds = [];
|
||||||
|
for (var j = 0; j < (data.install_methods || []).length; j++) {
|
||||||
|
var im = data.install_methods[j];
|
||||||
|
var typeId = installMethodTypeToId[im.type];
|
||||||
|
var res = im.resources || {};
|
||||||
|
var osId = osToId[res.os];
|
||||||
|
var osVersionKey = (res.os != null && res.version != null) ? res.os + '|' + res.version : null;
|
||||||
|
var osVersionId = osVersionKey ? osVersionToId[osVersionKey] : null;
|
||||||
|
var imBody = {
|
||||||
|
script: scriptId,
|
||||||
|
resources_cpu: res.cpu != null ? res.cpu : 0,
|
||||||
|
resources_ram: res.ram != null ? res.ram : 0,
|
||||||
|
resources_hdd: res.hdd != null ? res.hdd : 0
|
||||||
|
};
|
||||||
|
if (typeId) imBody.type = typeId;
|
||||||
|
if (osId) imBody.os = osId;
|
||||||
|
if (osVersionId) imBody.os_version = osVersionId;
|
||||||
|
var imPostRes = await request(installMethodsCollUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(imBody)
|
||||||
|
});
|
||||||
|
if (imPostRes.ok) installMethodIds.push(JSON.parse(imPostRes.body).id);
|
||||||
|
}
|
||||||
|
return { noteIds: noteIds, installMethodIds: installMethodIds };
|
||||||
|
}
|
||||||
|
if (existingId) {
|
||||||
|
var resolved = await resolveNotesAndInstallMethods(existingId);
|
||||||
|
payload.notes = resolved.noteIds;
|
||||||
|
payload.install_methods = resolved.installMethodIds;
|
||||||
|
console.log('Updating', file, '(slug=' + data.slug + ')');
|
||||||
|
const r = await request(recordsUrl + '/' + existingId, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
if (!r.ok) throw new Error('PATCH failed: ' + r.body);
|
||||||
|
} else {
|
||||||
|
console.log('Creating', file, '(slug=' + data.slug + ')');
|
||||||
|
const r = await request(recordsUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
if (!r.ok) throw new Error('POST failed: ' + r.body);
|
||||||
|
var scriptId = JSON.parse(r.body).id;
|
||||||
|
var resolved = await resolveNotesAndInstallMethods(scriptId);
|
||||||
|
var patchRes = await request(recordsUrl + '/' + scriptId, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ install_methods: resolved.installMethodIds, notes: resolved.noteIds })
|
||||||
|
});
|
||||||
|
if (!patchRes.ok) throw new Error('PATCH relations failed: ' + patchRes.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('Done.');
|
||||||
|
})().catch(e => { console.error(e); process.exit(1); });
|
||||||
|
ENDSCRIPT
|
||||||
|
shell: bash
|
||||||
23
CHANGELOG.md
23
CHANGELOG.md
@@ -410,18 +410,39 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
## 2026-03-03
|
||||||
|
|
||||||
|
### 🌐 Website
|
||||||
|
|
||||||
|
- #### 🐞 Bug Fixes
|
||||||
|
|
||||||
|
- Revert #11534 PR that messed up search [@BramSuurdje](https://github.com/BramSuurdje) ([#12492](https://github.com/community-scripts/ProxmoxVE/pull/12492))
|
||||||
|
|
||||||
## 2026-03-02
|
## 2026-03-02
|
||||||
|
|
||||||
### 🆕 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -35,6 +35,20 @@ function update_script() {
|
|||||||
$STD service tinyauth stop
|
$STD service tinyauth stop
|
||||||
msg_ok "Service Stopped"
|
msg_ok "Service Stopped"
|
||||||
|
|
||||||
|
if [[ -f /opt/tinyauth/.env ]] && ! grep -q "^TINYAUTH_" /opt/tinyauth/.env; then
|
||||||
|
msg_info "Migrating .env to v5 format"
|
||||||
|
sed -i \
|
||||||
|
-e 's/^DATABASE_PATH=/TINYAUTH_DATABASE_PATH=/' \
|
||||||
|
-e 's/^USERS=/TINYAUTH_AUTH_USERS=/' \
|
||||||
|
-e "s/^USERS='/TINYAUTH_AUTH_USERS='/" \
|
||||||
|
-e 's/^APP_URL=/TINYAUTH_APPURL=/' \
|
||||||
|
-e 's/^SECRET=/TINYAUTH_AUTH_SECRET=/' \
|
||||||
|
-e 's/^PORT=/TINYAUTH_SERVER_PORT=/' \
|
||||||
|
-e 's/^ADDRESS=/TINYAUTH_SERVER_ADDRESS=/' \
|
||||||
|
/opt/tinyauth/.env
|
||||||
|
msg_ok "Migrated .env to v5 format"
|
||||||
|
fi
|
||||||
|
|
||||||
msg_info "Updating Tinyauth"
|
msg_info "Updating Tinyauth"
|
||||||
rm -f /opt/tinyauth/tinyauth
|
rm -f /opt/tinyauth/tinyauth
|
||||||
curl -fsSL "https://github.com/steveiliop56/tinyauth/releases/download/v${RELEASE}/tinyauth-amd64" -o /opt/tinyauth/tinyauth
|
curl -fsSL "https://github.com/steveiliop56/tinyauth/releases/download/v${RELEASE}/tinyauth-amd64" -o /opt/tinyauth/tinyauth
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function update_script() {
|
|||||||
|
|
||||||
if [[ ! -d /opt/endurain ]]; then
|
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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
6
ct/headers/powerdns
Normal file
6
ct/headers/powerdns
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
____ ____ _ _______
|
||||||
|
/ __ \____ _ _____ _____/ __ \/ | / / ___/
|
||||||
|
/ /_/ / __ \ | /| / / _ \/ ___/ / / / |/ /\__ \
|
||||||
|
/ ____/ /_/ / |/ |/ / __/ / / /_/ / /| /___/ /
|
||||||
|
/_/ \____/|__/|__/\___/_/ /_____/_/ |_//____/
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ function update_script() {
|
|||||||
|
|
||||||
if [[ ! -f /etc/itsm-ng/config_db.php ]]; then
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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" \
|
||||||
|
|||||||
@@ -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
68
ct/powerdns.sh
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
|
||||||
|
|
||||||
|
# Copyright (c) 2021-2026 community-scripts ORG
|
||||||
|
# Author: Slaviša Arežina (tremor021)
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
|
# Source: https://www.powerdns.com/
|
||||||
|
|
||||||
|
APP="PowerDNS"
|
||||||
|
var_tags="${var_tags:-dns}"
|
||||||
|
var_cpu="${var_cpu:-1}"
|
||||||
|
var_ram="${var_ram:-1024}"
|
||||||
|
var_disk="${var_disk:-4}"
|
||||||
|
var_os="${var_os:-debian}"
|
||||||
|
var_version="${var_version:-13}"
|
||||||
|
var_unprivileged="${var_unprivileged:-1}"
|
||||||
|
|
||||||
|
header_info "$APP"
|
||||||
|
variables
|
||||||
|
color
|
||||||
|
catch_errors
|
||||||
|
|
||||||
|
function update_script() {
|
||||||
|
header_info
|
||||||
|
check_container_storage
|
||||||
|
check_container_resources
|
||||||
|
if [[ ! -d /opt/poweradmin ]]; then
|
||||||
|
msg_error "No ${APP} Installation Found!"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
msg_info "Updating PowerDNS"
|
||||||
|
$STD apt update
|
||||||
|
$STD apt install -y --only-upgrade pdns-server pdns-backend-sqlite3
|
||||||
|
msg_ok "Updated PowerDNS"
|
||||||
|
|
||||||
|
if check_for_gh_release "poweradmin" "poweradmin/poweradmin"; then
|
||||||
|
msg_info "Backing up Configuration"
|
||||||
|
cp /opt/poweradmin/config/settings.php /opt/poweradmin_settings.php.bak
|
||||||
|
cp /opt/poweradmin/powerdns.db /opt/poweradmin_powerdns.db.bak
|
||||||
|
msg_ok "Backed up Configuration"
|
||||||
|
|
||||||
|
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "poweradmin" "poweradmin/poweradmin" "tarball"
|
||||||
|
|
||||||
|
msg_info "Updating Poweradmin"
|
||||||
|
cp /opt/poweradmin_settings.php.bak /opt/poweradmin/config/settings.php
|
||||||
|
cp /opt/poweradmin_powerdns.db.bak /opt/poweradmin/powerdns.db
|
||||||
|
rm -rf /opt/poweradmin/install
|
||||||
|
rm -f /opt/poweradmin_settings.php.bak /opt/poweradmin_powerdns.db.bak
|
||||||
|
chown -R www-data:www-data /opt/poweradmin
|
||||||
|
msg_ok "Updated Poweradmin"
|
||||||
|
|
||||||
|
msg_info "Restarting Services"
|
||||||
|
systemctl restart pdns apache2
|
||||||
|
msg_ok "Restarted Services"
|
||||||
|
msg_ok "Updated successfully!"
|
||||||
|
fi
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
start
|
||||||
|
build_container
|
||||||
|
description
|
||||||
|
|
||||||
|
msg_ok "Completed Successfully!\n"
|
||||||
|
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||||
|
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
|
||||||
|
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}${CL}"
|
||||||
53
ct/tinyauth.sh
Normal file
53
ct/tinyauth.sh
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
|
||||||
|
# Copyright (c) 2021-2026 community-scripts ORG
|
||||||
|
# Author: MickLesk (CanbiZ)
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
|
# Source: https://github.com/steveiliop56/tinyauth
|
||||||
|
|
||||||
|
APP="Tinyauth"
|
||||||
|
var_tags="${var_tags:-auth}"
|
||||||
|
var_cpu="${var_cpu:-1}"
|
||||||
|
var_ram="${var_ram:-512}"
|
||||||
|
var_disk="${var_disk:-4}"
|
||||||
|
var_os="${var_os:-debian}"
|
||||||
|
var_version="${var_version:-13}"
|
||||||
|
var_unprivileged="${var_unprivileged:-1}"
|
||||||
|
|
||||||
|
header_info "$APP"
|
||||||
|
variables
|
||||||
|
color
|
||||||
|
catch_errors
|
||||||
|
|
||||||
|
function update_script() {
|
||||||
|
header_info
|
||||||
|
check_container_storage
|
||||||
|
check_container_resources
|
||||||
|
if [[ ! -d /opt/tinyauth ]]; then
|
||||||
|
msg_error "No ${APP} Installation Found!"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
if check_for_gh_release "tinyauth" "steveiliop56/tinyauth"; then
|
||||||
|
msg_info "Stopping Service"
|
||||||
|
systemctl stop tinyauth
|
||||||
|
msg_ok "Stopped Service"
|
||||||
|
|
||||||
|
fetch_and_deploy_gh_release "tinyauth" "steveiliop56/tinyauth" "singlefile" "latest" "/opt/tinyauth" "tinyauth-amd64"
|
||||||
|
|
||||||
|
msg_info "Starting Service"
|
||||||
|
systemctl start tinyauth
|
||||||
|
msg_ok "Started Service"
|
||||||
|
msg_ok "Updated successfully!"
|
||||||
|
fi
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
start
|
||||||
|
build_container
|
||||||
|
description
|
||||||
|
|
||||||
|
msg_ok "Completed successfully!\n"
|
||||||
|
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||||
|
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
|
||||||
|
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:3000${CL}"
|
||||||
@@ -75,10 +75,31 @@ if [ -f \$pg_config_file ]; then
|
|||||||
fi
|
fi
|
||||||
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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Alpine-Tinyauth",
|
|
||||||
"slug": "alpine-tinyauth",
|
|
||||||
"categories": [
|
|
||||||
6
|
|
||||||
],
|
|
||||||
"date_created": "2025-05-06",
|
|
||||||
"type": "ct",
|
|
||||||
"updateable": true,
|
|
||||||
"privileged": false,
|
|
||||||
"interface_port": 3000,
|
|
||||||
"documentation": "https://tinyauth.app",
|
|
||||||
"config_path": "/opt/tinyauth/.env",
|
|
||||||
"website": "https://tinyauth.app",
|
|
||||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/tinyauth.webp",
|
|
||||||
"description": "Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic provider to all of your docker apps.",
|
|
||||||
"install_methods": [
|
|
||||||
{
|
|
||||||
"type": "default",
|
|
||||||
"script": "ct/alpine-tinyauth.sh",
|
|
||||||
"resources": {
|
|
||||||
"cpu": 1,
|
|
||||||
"ram": 256,
|
|
||||||
"hdd": 2,
|
|
||||||
"os": "alpine",
|
|
||||||
"version": "3.23"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "alpine",
|
|
||||||
"script": "ct/alpine-tinyauth.sh",
|
|
||||||
"resources": {
|
|
||||||
"cpu": 1,
|
|
||||||
"ram": 256,
|
|
||||||
"hdd": 2,
|
|
||||||
"os": "alpine",
|
|
||||||
"version": "3.23"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"default_credentials": {
|
|
||||||
"username": null,
|
|
||||||
"password": null
|
|
||||||
},
|
|
||||||
"notes": [
|
|
||||||
{
|
|
||||||
"text": "The default credentials are located in `/opt/tinyauth/credentials.txt`.",
|
|
||||||
"type": "info"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"generated": "2026-03-02T12:12:21Z",
|
"generated": "2026-03-03T06:17:56Z",
|
||||||
"versions": [
|
"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",
|
||||||
@@ -291,9 +291,9 @@
|
|||||||
{
|
{
|
||||||
"slug": "dispatcharr",
|
"slug": "dispatcharr",
|
||||||
"repo": "Dispatcharr/Dispatcharr",
|
"repo": "Dispatcharr/Dispatcharr",
|
||||||
"version": "v0.20.1",
|
"version": "v0.20.2",
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"date": "2026-02-26T21:38:19Z"
|
"date": "2026-03-03T01:40:33Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "docmost",
|
"slug": "docmost",
|
||||||
@@ -382,9 +382,9 @@
|
|||||||
{
|
{
|
||||||
"slug": "firefly",
|
"slug": "firefly",
|
||||||
"repo": "firefly-iii/firefly-iii",
|
"repo": "firefly-iii/firefly-iii",
|
||||||
"version": "v6.5.1",
|
"version": "v6.5.2",
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"date": "2026-02-27T20:55:55Z"
|
"date": "2026-03-03T05:42:27Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "fladder",
|
"slug": "fladder",
|
||||||
@@ -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",
|
||||||
@@ -585,9 +585,9 @@
|
|||||||
{
|
{
|
||||||
"slug": "immich-public-proxy",
|
"slug": "immich-public-proxy",
|
||||||
"repo": "alangrainger/immich-public-proxy",
|
"repo": "alangrainger/immich-public-proxy",
|
||||||
"version": "v1.15.3",
|
"version": "v1.15.4",
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"date": "2026-02-16T22:54:27Z"
|
"date": "2026-03-02T21:28:06Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "inspircd",
|
"slug": "inspircd",
|
||||||
@@ -613,9 +613,9 @@
|
|||||||
{
|
{
|
||||||
"slug": "jackett",
|
"slug": "jackett",
|
||||||
"repo": "Jackett/Jackett",
|
"repo": "Jackett/Jackett",
|
||||||
"version": "v0.24.1247",
|
"version": "v0.24.1261",
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"date": "2026-03-02T05:56:37Z"
|
"date": "2026-03-03T05:54:20Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "jellystat",
|
"slug": "jellystat",
|
||||||
@@ -872,9 +872,9 @@
|
|||||||
{
|
{
|
||||||
"slug": "metube",
|
"slug": "metube",
|
||||||
"repo": "alexta69/metube",
|
"repo": "alexta69/metube",
|
||||||
"version": "2026.02.27",
|
"version": "2026.03.02",
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"date": "2026-02-27T11:47:02Z"
|
"date": "2026-03-02T19:19:10Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "miniflux",
|
"slug": "miniflux",
|
||||||
@@ -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-02T20:15:31Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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",
|
||||||
@@ -1726,9 +1733,9 @@
|
|||||||
{
|
{
|
||||||
"slug": "wealthfolio",
|
"slug": "wealthfolio",
|
||||||
"repo": "afadil/wealthfolio",
|
"repo": "afadil/wealthfolio",
|
||||||
"version": "v3.0.0",
|
"version": "v3.0.2",
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"date": "2026-02-24T22:37:05Z"
|
"date": "2026-03-03T05:01:49Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "web-check",
|
"slug": "web-check",
|
||||||
|
|||||||
40
frontend/public/json/powerdns.json
Normal file
40
frontend/public/json/powerdns.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "PowerDNS",
|
||||||
|
"slug": "powerdns",
|
||||||
|
"categories": [
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"date_created": "2026-03-02",
|
||||||
|
"type": "ct",
|
||||||
|
"updateable": true,
|
||||||
|
"privileged": false,
|
||||||
|
"interface_port": 80,
|
||||||
|
"documentation": "https://doc.powerdns.com/index.html",
|
||||||
|
"config_path": "/opt/poweradmin/config/settings.php",
|
||||||
|
"website": "https://www.powerdns.com/",
|
||||||
|
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/powerdns.webp",
|
||||||
|
"description": "The PowerDNS Authoritative Server is a versatile nameserver which supports a large number of backends. These backends can either be plain zone files or be more dynamic in nature. PowerDNS has the concepts of ‘backends’. A backend is a datastore that the server will consult that contains DNS records (and some metadata). The backends range from database backends (MySQL, PostgreSQL) and BIND zone files to co-processes and JSON API’s.",
|
||||||
|
"install_methods": [
|
||||||
|
{
|
||||||
|
"type": "default",
|
||||||
|
"script": "ct/powerdns.sh",
|
||||||
|
"resources": {
|
||||||
|
"cpu": 1,
|
||||||
|
"ram": 1024,
|
||||||
|
"hdd": 4,
|
||||||
|
"os": "Debian",
|
||||||
|
"version": "13"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default_credentials": {
|
||||||
|
"username": null,
|
||||||
|
"password": null
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
{
|
||||||
|
"text": "For administrator credentials type: `cat ~/poweradmin.creds` inside LXC.",
|
||||||
|
"type": "info"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
51
frontend/public/json/tinyauth.json
Normal file
51
frontend/public/json/tinyauth.json
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"name": "Tinyauth",
|
||||||
|
"slug": "tinyauth",
|
||||||
|
"categories": [
|
||||||
|
6
|
||||||
|
],
|
||||||
|
"date_created": "2025-05-06",
|
||||||
|
"type": "ct",
|
||||||
|
"updateable": true,
|
||||||
|
"privileged": false,
|
||||||
|
"interface_port": 3000,
|
||||||
|
"documentation": "https://tinyauth.app",
|
||||||
|
"config_path": "/opt/tinyauth/.env",
|
||||||
|
"website": "https://tinyauth.app",
|
||||||
|
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/tinyauth.webp",
|
||||||
|
"description": "Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic provider to all of your docker apps.",
|
||||||
|
"install_methods": [
|
||||||
|
{
|
||||||
|
"type": "default",
|
||||||
|
"script": "ct/tinyauth.sh",
|
||||||
|
"resources": {
|
||||||
|
"cpu": 1,
|
||||||
|
"ram": 512,
|
||||||
|
"hdd": 4,
|
||||||
|
"os": "debian",
|
||||||
|
"version": "13"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "alpine",
|
||||||
|
"script": "ct/alpine-tinyauth.sh",
|
||||||
|
"resources": {
|
||||||
|
"cpu": 1,
|
||||||
|
"ram": 256,
|
||||||
|
"hdd": 2,
|
||||||
|
"os": "alpine",
|
||||||
|
"version": "3.23"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default_credentials": {
|
||||||
|
"username": null,
|
||||||
|
"password": null
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
{
|
||||||
|
"text": "The default credentials are located in `/opt/tinyauth/credentials.txt`.",
|
||||||
|
"type": "info"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -2,42 +2,68 @@
|
|||||||
|
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
|
|
||||||
|
import { githubGist, nord } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||||
import { CalendarIcon, Check, Clipboard, Download } from "lucide-react";
|
import { CalendarIcon, Check, Clipboard, Download } from "lucide-react";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
import type { Category } from "@/lib/types";
|
import type { Category } from "@/lib/types";
|
||||||
|
|
||||||
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { Calendar } from "@/components/ui/calendar";
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { basePath } from "@/config/site-config";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { fetchCategories } from "@/lib/data";
|
import { fetchCategories } from "@/lib/data";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import type { Script } from "./_schemas/schemas";
|
import type { Script } from "./_schemas/schemas";
|
||||||
|
|
||||||
|
import { ScriptItem } from "../scripts/_components/script-item";
|
||||||
import InstallMethod from "./_components/install-method";
|
import InstallMethod from "./_components/install-method";
|
||||||
import { ScriptSchema } from "./_schemas/schemas";
|
import { ScriptSchema } from "./_schemas/schemas";
|
||||||
import Categories from "./_components/categories";
|
import Categories from "./_components/categories";
|
||||||
import Note from "./_components/note";
|
import Note from "./_components/note";
|
||||||
|
|
||||||
import { githubGist, nord } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
function search(scripts: Script[], query: string): Script[] {
|
||||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
const queryLower = query.toLowerCase().trim();
|
||||||
import { ScriptItem } from "../scripts/_components/script-item";
|
const searchWords = queryLower.split(/\s+/).filter(Boolean);
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
return scripts
|
||||||
import { search } from "@/components/command-menu";
|
.map((script) => {
|
||||||
import { basePath } from "@/config/site-config";
|
const nameLower = script.name.toLowerCase();
|
||||||
import Image from "next/image";
|
const descriptionLower = (script.description || "").toLowerCase();
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
for (const word of searchWords) {
|
||||||
|
if (nameLower.includes(word)) {
|
||||||
|
score += 10;
|
||||||
|
}
|
||||||
|
if (descriptionLower.includes(word)) {
|
||||||
|
score += 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { script, score };
|
||||||
|
})
|
||||||
|
.filter(({ score }) => score > 0)
|
||||||
|
.sort((a, b) => b.score - a.score)
|
||||||
|
.slice(0, 20)
|
||||||
|
.map(({ script }) => script);
|
||||||
|
}
|
||||||
|
|
||||||
const initialScript: Script = {
|
const initialScript: Script = {
|
||||||
name: "",
|
name: "",
|
||||||
@@ -77,32 +103,32 @@ export default function JSONGenerator() {
|
|||||||
|
|
||||||
const selectedCategoryObj = useMemo(
|
const selectedCategoryObj = useMemo(
|
||||||
() => categories.find(cat => cat.id.toString() === selectedCategory),
|
() => categories.find(cat => cat.id.toString() === selectedCategory),
|
||||||
[categories, selectedCategory]
|
[categories, selectedCategory],
|
||||||
);
|
);
|
||||||
|
|
||||||
const allScripts = useMemo(
|
const allScripts = useMemo(
|
||||||
() => categories.flatMap(cat => cat.scripts || []),
|
() => categories.flatMap(cat => cat.scripts || []),
|
||||||
[categories]
|
[categories],
|
||||||
);
|
);
|
||||||
|
|
||||||
const scripts = useMemo(() => {
|
const scripts = useMemo(() => {
|
||||||
const query = searchQuery.trim()
|
const query = searchQuery.trim();
|
||||||
|
|
||||||
if (query) {
|
if (query) {
|
||||||
return search(allScripts, query)
|
return search(allScripts, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedCategoryObj) {
|
if (selectedCategoryObj) {
|
||||||
return selectedCategoryObj.scripts || []
|
return selectedCategoryObj.scripts || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return []
|
return [];
|
||||||
}, [allScripts, selectedCategoryObj, searchQuery]);
|
}, [allScripts, selectedCategoryObj, searchQuery]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCategories()
|
fetchCategories()
|
||||||
.then(setCategories)
|
.then(setCategories)
|
||||||
.catch((error) => console.error("Error fetching categories:", error));
|
.catch(error => console.error("Error fetching categories:", error));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -122,11 +148,14 @@ export default function JSONGenerator() {
|
|||||||
|
|
||||||
if (updated.type === "pve") {
|
if (updated.type === "pve") {
|
||||||
scriptPath = `tools/pve/${updated.slug}.sh`;
|
scriptPath = `tools/pve/${updated.slug}.sh`;
|
||||||
} else if (updated.type === "addon") {
|
}
|
||||||
|
else if (updated.type === "addon") {
|
||||||
scriptPath = `tools/addon/${updated.slug}.sh`;
|
scriptPath = `tools/addon/${updated.slug}.sh`;
|
||||||
} else if (method.type === "alpine") {
|
}
|
||||||
|
else if (method.type === "alpine") {
|
||||||
scriptPath = `${updated.type}/alpine-${updated.slug}.sh`;
|
scriptPath = `${updated.type}/alpine-${updated.slug}.sh`;
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
scriptPath = `${updated.type}/${updated.slug}.sh`;
|
scriptPath = `${updated.type}/${updated.slug}.sh`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,11 +174,13 @@ export default function JSONGenerator() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCopy = useCallback(() => {
|
const handleCopy = useCallback(() => {
|
||||||
if (!isValid) toast.warning("JSON schema is invalid. Copying anyway.");
|
if (!isValid)
|
||||||
|
toast.warning("JSON schema is invalid. Copying anyway.");
|
||||||
navigator.clipboard.writeText(JSON.stringify(script, null, 2));
|
navigator.clipboard.writeText(JSON.stringify(script, null, 2));
|
||||||
setIsCopied(true);
|
setIsCopied(true);
|
||||||
setTimeout(() => setIsCopied(false), 2000);
|
setTimeout(() => setIsCopied(false), 2000);
|
||||||
if (isValid) toast.success("Copied metadata to clipboard");
|
if (isValid)
|
||||||
|
toast.success("Copied metadata to clipboard");
|
||||||
}, [script]);
|
}, [script]);
|
||||||
|
|
||||||
const importScript = (script: Script) => {
|
const importScript = (script: Script) => {
|
||||||
@@ -166,11 +197,11 @@ export default function JSONGenerator() {
|
|||||||
setIsValid(true);
|
setIsValid(true);
|
||||||
setZodErrors(null);
|
setZodErrors(null);
|
||||||
toast.success("Imported JSON successfully");
|
toast.success("Imported JSON successfully");
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
toast.error("Failed to read or parse the JSON file.");
|
toast.error("Failed to read or parse the JSON file.");
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const handleFileImport = useCallback(() => {
|
const handleFileImport = useCallback(() => {
|
||||||
const input = document.createElement("input");
|
const input = document.createElement("input");
|
||||||
@@ -180,7 +211,8 @@ export default function JSONGenerator() {
|
|||||||
input.onchange = (e: Event) => {
|
input.onchange = (e: Event) => {
|
||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
const file = target.files?.[0];
|
const file = target.files?.[0];
|
||||||
if (!file) return;
|
if (!file)
|
||||||
|
return;
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (event) => {
|
reader.onload = (event) => {
|
||||||
@@ -189,7 +221,8 @@ export default function JSONGenerator() {
|
|||||||
const parsed = JSON.parse(content);
|
const parsed = JSON.parse(content);
|
||||||
importScript(parsed);
|
importScript(parsed);
|
||||||
toast.success("Imported JSON successfully");
|
toast.success("Imported JSON successfully");
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
toast.error("Failed to read the JSON file.");
|
toast.error("Failed to read the JSON file.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -243,7 +276,10 @@ export default function JSONGenerator() {
|
|||||||
<div className="mt-2 space-y-1">
|
<div className="mt-2 space-y-1">
|
||||||
{zodErrors.issues.map((error, index) => (
|
{zodErrors.issues.map((error, index) => (
|
||||||
<AlertDescription key={index} className="p-1 text-red-500">
|
<AlertDescription key={index} className="p-1 text-red-500">
|
||||||
{error.path.join(".")} -{error.message}
|
{error.path.join(".")}
|
||||||
|
{" "}
|
||||||
|
-
|
||||||
|
{error.message}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -270,7 +306,7 @@ export default function JSONGenerator() {
|
|||||||
onOpenChange={setIsImportDialogOpen}
|
onOpenChange={setIsImportDialogOpen}
|
||||||
>
|
>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
<DropdownMenuItem onSelect={e => e.preventDefault()}>
|
||||||
Import existing script
|
Import existing script
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
@@ -292,7 +328,7 @@ export default function JSONGenerator() {
|
|||||||
<SelectValue placeholder="Category" />
|
<SelectValue placeholder="Category" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{categories.map((category) => (
|
{categories.map(category => (
|
||||||
<SelectItem key={category.id} value={category.id.toString()}>
|
<SelectItem key={category.id} value={category.id.toString()}>
|
||||||
{category.name}
|
{category.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -302,40 +338,44 @@ export default function JSONGenerator() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Search for a script..."
|
placeholder="Search for a script..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={e => setSearchQuery(e.target.value)}
|
||||||
/>
|
/>
|
||||||
{!selectedCategory && !searchQuery ? (
|
{!selectedCategory && !searchQuery
|
||||||
<p className="text-muted-foreground text-sm text-center">
|
? (
|
||||||
Select a category or search for a script
|
<p className="text-muted-foreground text-sm text-center">
|
||||||
</p>
|
Select a category or search for a script
|
||||||
) : scripts.length === 0 ? (
|
</p>
|
||||||
<p className="text-muted-foreground text-sm text-center">
|
)
|
||||||
No scripts found
|
: scripts.length === 0
|
||||||
</p>
|
? (
|
||||||
) : (
|
<p className="text-muted-foreground text-sm text-center">
|
||||||
<div className="grid grid-cols-3 auto-rows-min h-64 overflow-y-auto gap-4">
|
No scripts found
|
||||||
{scripts.map(script => (
|
</p>
|
||||||
<div
|
)
|
||||||
key={script.slug}
|
: (
|
||||||
className="p-2 border rounded cursor-pointer hover:bg-accent hover:text-accent-foreground"
|
<div className="grid grid-cols-3 auto-rows-min h-64 overflow-y-auto gap-4">
|
||||||
onClick={() => {
|
{scripts.map(script => (
|
||||||
importScript(script);
|
<div
|
||||||
setIsImportDialogOpen(false);
|
key={script.slug}
|
||||||
}}
|
className="p-2 border rounded cursor-pointer hover:bg-accent hover:text-accent-foreground"
|
||||||
>
|
onClick={() => {
|
||||||
<Image
|
importScript(script);
|
||||||
src={script.logo || `/${basePath}/logo.png`}
|
setIsImportDialogOpen(false);
|
||||||
alt={script.name}
|
}}
|
||||||
className="w-full h-12 object-contain mb-2"
|
>
|
||||||
width={16}
|
<Image
|
||||||
height={16}
|
src={script.logo || `/${basePath}/logo.png`}
|
||||||
unoptimized
|
alt={script.name}
|
||||||
/>
|
className="w-full h-12 object-contain mb-2"
|
||||||
<p className="text-sm text-center">{script.name}</p>
|
width={16}
|
||||||
</div>
|
height={16}
|
||||||
))}
|
unoptimized
|
||||||
</div>
|
/>
|
||||||
)}
|
<p className="text-sm text-center">{script.name}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -348,15 +388,19 @@ export default function JSONGenerator() {
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
Name <span className="text-red-500">*</span>
|
Name
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input placeholder="Example" value={script.name} onChange={(e) => updateScript("name", e.target.value)} />
|
<Input placeholder="Example" value={script.name} onChange={e => updateScript("name", e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
Slug <span className="text-red-500">*</span>
|
Slug
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input placeholder="example" value={script.slug} onChange={(e) => updateScript("slug", e.target.value)} />
|
<Input placeholder="example" value={script.slug} onChange={e => updateScript("slug", e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -366,7 +410,7 @@ export default function JSONGenerator() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Full logo URL"
|
placeholder="Full logo URL"
|
||||||
value={script.logo || ""}
|
value={script.logo || ""}
|
||||||
onChange={(e) => updateScript("logo", e.target.value || null)}
|
onChange={e => updateScript("logo", e.target.value || null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -374,24 +418,28 @@ export default function JSONGenerator() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Path to config file"
|
placeholder="Path to config file"
|
||||||
value={script.config_path || ""}
|
value={script.config_path || ""}
|
||||||
onChange={(e) => updateScript("config_path", e.target.value || "")}
|
onChange={e => updateScript("config_path", e.target.value || "")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
Description <span className="text-red-500">*</span>
|
Description
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="Example"
|
placeholder="Example"
|
||||||
value={script.description}
|
value={script.description}
|
||||||
onChange={(e) => updateScript("description", e.target.value)}
|
onChange={e => updateScript("description", e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Categories script={script} setScript={setScript} categories={categories} />
|
<Categories script={script} setScript={setScript} categories={categories} />
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<Label>
|
<Label>
|
||||||
Date Created <span className="text-red-500">*</span>
|
Date Created
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild className="flex-1">
|
<PopoverTrigger asChild className="flex-1">
|
||||||
@@ -415,7 +463,7 @@ export default function JSONGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<Label>Type</Label>
|
<Label>Type</Label>
|
||||||
<Select value={script.type} onValueChange={(value) => updateScript("type", value)}>
|
<Select value={script.type} onValueChange={value => updateScript("type", value)}>
|
||||||
<SelectTrigger className="flex-1">
|
<SelectTrigger className="flex-1">
|
||||||
<SelectValue placeholder="Type" />
|
<SelectValue placeholder="Type" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -430,17 +478,17 @@ export default function JSONGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-full flex gap-5">
|
<div className="w-full flex gap-5">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Switch checked={script.updateable} onCheckedChange={(checked) => updateScript("updateable", checked)} />
|
<Switch checked={script.updateable} onCheckedChange={checked => updateScript("updateable", checked)} />
|
||||||
<label>Updateable</label>
|
<label>Updateable</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Switch checked={script.privileged} onCheckedChange={(checked) => updateScript("privileged", checked)} />
|
<Switch checked={script.privileged} onCheckedChange={checked => updateScript("privileged", checked)} />
|
||||||
<label>Privileged</label>
|
<label>Privileged</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Switch
|
<Switch
|
||||||
checked={script.disable || false}
|
checked={script.disable || false}
|
||||||
onCheckedChange={(checked) => updateScript("disable", checked)}
|
onCheckedChange={checked => updateScript("disable", checked)}
|
||||||
/>
|
/>
|
||||||
<label>Disabled</label>
|
<label>Disabled</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -448,12 +496,14 @@ export default function JSONGenerator() {
|
|||||||
{script.disable && (
|
{script.disable && (
|
||||||
<div>
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
Disable Description <span className="text-red-500">*</span>
|
Disable Description
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="Explain why this script is disabled..."
|
placeholder="Explain why this script is disabled..."
|
||||||
value={script.disable_description || ""}
|
value={script.disable_description || ""}
|
||||||
onChange={(e) => updateScript("disable_description", e.target.value)}
|
onChange={e => updateScript("disable_description", e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -461,18 +511,18 @@ export default function JSONGenerator() {
|
|||||||
placeholder="Interface Port"
|
placeholder="Interface Port"
|
||||||
type="number"
|
type="number"
|
||||||
value={script.interface_port || ""}
|
value={script.interface_port || ""}
|
||||||
onChange={(e) => updateScript("interface_port", e.target.value ? Number(e.target.value) : null)}
|
onChange={e => updateScript("interface_port", e.target.value ? Number(e.target.value) : null)}
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Website URL"
|
placeholder="Website URL"
|
||||||
value={script.website || ""}
|
value={script.website || ""}
|
||||||
onChange={(e) => updateScript("website", e.target.value || null)}
|
onChange={e => updateScript("website", e.target.value || null)}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Documentation URL"
|
placeholder="Documentation URL"
|
||||||
value={script.documentation || ""}
|
value={script.documentation || ""}
|
||||||
onChange={(e) => updateScript("documentation", e.target.value || null)}
|
onChange={e => updateScript("documentation", e.target.value || null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<InstallMethod script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
<InstallMethod script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
||||||
@@ -480,22 +530,20 @@ export default function JSONGenerator() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
value={script.default_credentials.username || ""}
|
value={script.default_credentials.username || ""}
|
||||||
onChange={(e) =>
|
onChange={e =>
|
||||||
updateScript("default_credentials", {
|
updateScript("default_credentials", {
|
||||||
...script.default_credentials,
|
...script.default_credentials,
|
||||||
username: e.target.value || null,
|
username: e.target.value || null,
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
value={script.default_credentials.password || ""}
|
value={script.default_credentials.password || ""}
|
||||||
onChange={(e) =>
|
onChange={e =>
|
||||||
updateScript("default_credentials", {
|
updateScript("default_credentials", {
|
||||||
...script.default_credentials,
|
...script.default_credentials,
|
||||||
password: e.target.value || null,
|
password: e.target.value || null,
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Note script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
<Note script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
||||||
</form>
|
</form>
|
||||||
@@ -504,7 +552,7 @@ export default function JSONGenerator() {
|
|||||||
<Tabs
|
<Tabs
|
||||||
defaultValue="json"
|
defaultValue="json"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onValueChange={(value) => setCurrentTab(value as "json" | "preview")}
|
onValueChange={value => setCurrentTab(value as "json" | "preview")}
|
||||||
value={currentTab}
|
value={currentTab}
|
||||||
>
|
>
|
||||||
<TabsList className="grid w-full grid-cols-2">
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { ArrowRightIcon, Sparkles } from "lucide-react";
|
import { ArrowRightIcon, Sparkles } from "lucide-react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import type { Category, Script } from "@/lib/types";
|
import type { Category, Script } from "@/lib/types";
|
||||||
@@ -21,35 +22,6 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/t
|
|||||||
import { DialogTitle } from "./ui/dialog";
|
import { DialogTitle } from "./ui/dialog";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { Badge } from "./ui/badge";
|
import { Badge } from "./ui/badge";
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
export function search(scripts: Script[], query: string): Script[] {
|
|
||||||
const queryLower = query.toLowerCase().trim();
|
|
||||||
const searchWords = queryLower.split(/\s+/).filter(Boolean);
|
|
||||||
|
|
||||||
return scripts
|
|
||||||
.map(script => {
|
|
||||||
const nameLower = script.name.toLowerCase();
|
|
||||||
const descriptionLower = (script.description || "").toLowerCase();
|
|
||||||
|
|
||||||
let score = 0;
|
|
||||||
|
|
||||||
for (const word of searchWords) {
|
|
||||||
if (nameLower.includes(word)) {
|
|
||||||
score += 10;
|
|
||||||
}
|
|
||||||
if (descriptionLower.includes(word)) {
|
|
||||||
score += 5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { script, score };
|
|
||||||
})
|
|
||||||
.filter(({ score }) => score > 0)
|
|
||||||
.sort((a, b) => b.score - a.score)
|
|
||||||
.slice(0, 20)
|
|
||||||
.map(({ script }) => script);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formattedBadge(type: string) {
|
export function formattedBadge(type: string) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -79,11 +51,9 @@ function getRandomScript(categories: Category[], previouslySelected: Set<string>
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CommandMenu() {
|
function CommandMenu() {
|
||||||
const [query, setQuery] = React.useState("");
|
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [links, setLinks] = React.useState<Category[]>([]);
|
const [links, setLinks] = React.useState<Category[]>([]);
|
||||||
const [isLoading, setIsLoading] = React.useState(false);
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
const [results, setResults] = React.useState<Script[]>([]);
|
|
||||||
const [selectedScripts, setSelectedScripts] = React.useState<Set<string>>(new Set());
|
const [selectedScripts, setSelectedScripts] = React.useState<Set<string>>(new Set());
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -100,27 +70,6 @@ function CommandMenu() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (query.trim() === "") {
|
|
||||||
fetchSortedCategories();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const scriptMap = new Map<string, Script>();
|
|
||||||
|
|
||||||
for (const category of links) {
|
|
||||||
for (const script of category.scripts || []) {
|
|
||||||
if (!scriptMap.has(script.slug)) {
|
|
||||||
scriptMap.set(script.slug, script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueScripts = Array.from(scriptMap.values());
|
|
||||||
const filteredResults = search(uniqueScripts, query);
|
|
||||||
setResults(filteredResults);
|
|
||||||
}
|
|
||||||
}, [query]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||||
@@ -248,46 +197,49 @@ function CommandMenu() {
|
|||||||
|
|
||||||
<CommandDialog
|
<CommandDialog
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={setOpen}
|
||||||
setOpen(open);
|
filter={(value: string, search: string) => {
|
||||||
if (open) {
|
const searchLower = search.toLowerCase().trim();
|
||||||
setQuery("");
|
if (!searchLower)
|
||||||
setResults([]);
|
return 1;
|
||||||
}
|
const valueLower = value.toLowerCase();
|
||||||
|
const searchWords = searchLower.split(/\s+/).filter(Boolean);
|
||||||
|
// All search words must appear somewhere in the value (name + description)
|
||||||
|
const allWordsMatch = searchWords.every((word: string) => valueLower.includes(word));
|
||||||
|
return allWordsMatch ? 1 : 0;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogTitle className="sr-only">Search scripts</DialogTitle>
|
<DialogTitle className="sr-only">Search scripts</DialogTitle>
|
||||||
<CommandInput
|
<CommandInput placeholder="Search for a script..." />
|
||||||
placeholder="Search for a script..."
|
|
||||||
onValueChange={setQuery}
|
|
||||||
value={query}
|
|
||||||
/>
|
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>
|
<CommandEmpty>
|
||||||
{isLoading ? (
|
{isLoading
|
||||||
"Searching..."
|
? (
|
||||||
) : (
|
"Searching..."
|
||||||
<div className="flex flex-col items-center justify-center py-6 text-center">
|
)
|
||||||
<p className="text-sm text-muted-foreground">No scripts match your search.</p>
|
: (
|
||||||
<div className="mt-4">
|
<div className="flex flex-col items-center justify-center py-6 text-center">
|
||||||
<p className="text-xs text-muted-foreground mb-2">Want to add a new script?</p>
|
<p className="text-sm text-muted-foreground">No scripts match your search.</p>
|
||||||
<Button variant="outline" size="sm" asChild>
|
<div className="mt-4">
|
||||||
<Link
|
<p className="text-xs text-muted-foreground mb-2">Want to add a new script?</p>
|
||||||
href={`https://github.com/community-scripts/${basePath}/tree/main/docs/contribution/GUIDE.md`}
|
<Button variant="outline" size="sm" asChild>
|
||||||
target="_blank"
|
<Link
|
||||||
rel="noopener noreferrer"
|
href={`https://github.com/community-scripts/${basePath}/tree/main/docs/contribution/GUIDE.md`}
|
||||||
>
|
target="_blank"
|
||||||
Documentation <ArrowRightIcon className="ml-2 h-4 w-4" />
|
rel="noopener noreferrer"
|
||||||
</Link>
|
>
|
||||||
</Button>
|
Documentation
|
||||||
</div>
|
{" "}
|
||||||
</div>
|
<ArrowRightIcon className="ml-2 h-4 w-4" />
|
||||||
)}
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
|
{Object.entries(uniqueScriptsByCategory).map(([categoryName, scripts]) => (
|
||||||
{results.length > 0 ? (
|
<CommandGroup key={`category:${categoryName}`} heading={categoryName}>
|
||||||
<CommandGroup heading="Search Results">
|
{scripts.map(script => (
|
||||||
{results.map(script => (
|
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={`script:${script.slug}`}
|
key={`script:${script.slug}`}
|
||||||
value={`${script.name} ${script.type} ${script.description || ""}`}
|
value={`${script.name} ${script.type} ${script.description || ""}`}
|
||||||
@@ -320,44 +272,7 @@ function CommandMenu() {
|
|||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
) : ( // When no search results, show all scripts grouped by category
|
))}
|
||||||
Object.entries(uniqueScriptsByCategory).map(([categoryName, scripts]) => (
|
|
||||||
<CommandGroup key={`category:${categoryName}`} heading={categoryName}>
|
|
||||||
{scripts.map(script => (
|
|
||||||
<CommandItem
|
|
||||||
key={`script:${script.slug}`}
|
|
||||||
value={`${script.name} ${script.type} ${script.description || ""}`}
|
|
||||||
onSelect={() => {
|
|
||||||
setOpen(false);
|
|
||||||
router.push(`/scripts?id=${script.slug}`);
|
|
||||||
}}
|
|
||||||
tabIndex={0}
|
|
||||||
aria-label={`Open script ${script.name}`}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
|
||||||
setOpen(false);
|
|
||||||
router.push(`/scripts?id=${script.slug}`);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex gap-2" onClick={() => setOpen(false)}>
|
|
||||||
<Image
|
|
||||||
src={script.logo || `/${basePath}/logo.png`}
|
|
||||||
onError={e => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
|
||||||
unoptimized
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
alt=""
|
|
||||||
className="h-5 w-5"
|
|
||||||
/>
|
|
||||||
<span>{script.name}</span>
|
|
||||||
<span>{formattedBadge(script.type)}</span>
|
|
||||||
</div>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</CommandDialog>
|
</CommandDialog>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ msg_ok "Installed Tinyauth"
|
|||||||
read -r -p "${TAB3}Enter your Tinyauth subdomain (e.g. https://tinyauth.example.com): " app_url
|
read -r -p "${TAB3}Enter your Tinyauth subdomain (e.g. https://tinyauth.example.com): " app_url
|
||||||
|
|
||||||
cat <<EOF >/opt/tinyauth/.env
|
cat <<EOF >/opt/tinyauth/.env
|
||||||
DATABASE_PATH=/opt/tinyauth/database.db
|
TINYAUTH_DATABASE_PATH=/opt/tinyauth/database.db
|
||||||
USERS='${USER}'
|
TINYAUTH_AUTH_USERS='${USER}'
|
||||||
APP_URL=${app_url}
|
TINYAUTH_APPURL=${app_url}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
msg_info "Creating Service"
|
msg_info "Creating Service"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
134
install/powerdns-install.sh
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Copyright (c) 2021-2026 community-scripts ORG
|
||||||
|
# Author: Slaviša Arežina (tremor021)
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
|
# Source: https://www.powerdns.com/
|
||||||
|
|
||||||
|
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||||
|
color
|
||||||
|
verb_ip6
|
||||||
|
catch_errors
|
||||||
|
setting_up_container
|
||||||
|
network_check
|
||||||
|
update_os
|
||||||
|
|
||||||
|
msg_info "Installing Dependencies"
|
||||||
|
$STD apt install -y sqlite3
|
||||||
|
msg_ok "Installed Dependencies"
|
||||||
|
|
||||||
|
PHP_VERSION="8.3" PHP_APACHE="YES" PHP_FPM="YES" PHP_MODULE="gettext,tokenizer,sqlite3,ldap" setup_php
|
||||||
|
setup_deb822_repo \
|
||||||
|
"pdns" \
|
||||||
|
"https://repo.powerdns.com/FD380FBB-pub.asc" \
|
||||||
|
"http://repo.powerdns.com/debian" \
|
||||||
|
"trixie-auth-50"
|
||||||
|
|
||||||
|
cat <<EOF >/etc/apt/preferences.d/auth-50
|
||||||
|
Package: pdns-*
|
||||||
|
Pin: origin repo.powerdns.com
|
||||||
|
Pin-Priority: 600
|
||||||
|
EOF
|
||||||
|
|
||||||
|
escape_sql() {
|
||||||
|
printf '%s' "$1" | sed "s/'/''/g"
|
||||||
|
}
|
||||||
|
|
||||||
|
msg_info "Setting up PowerDNS"
|
||||||
|
$STD apt install -y \
|
||||||
|
pdns-server \
|
||||||
|
pdns-backend-sqlite3
|
||||||
|
msg_ok "Setup PowerDNS"
|
||||||
|
|
||||||
|
fetch_and_deploy_gh_release "poweradmin" "poweradmin/poweradmin" "tarball"
|
||||||
|
|
||||||
|
msg_info "Setting up Poweradmin"
|
||||||
|
sqlite3 /opt/poweradmin/powerdns.db </opt/poweradmin/sql/poweradmin-sqlite-db-structure.sql
|
||||||
|
sqlite3 /opt/poweradmin/powerdns.db </opt/poweradmin/sql/pdns/49/schema.sqlite3.sql
|
||||||
|
PA_ADMIN_USERNAME="admin"
|
||||||
|
PA_ADMIN_EMAIL="admin@example.com"
|
||||||
|
PA_ADMIN_FULLNAME="Administrator"
|
||||||
|
PA_ADMIN_PASSWORD=$(openssl rand -base64 16 | tr -d "=+/" | cut -c1-16)
|
||||||
|
PA_SESSION_KEY=$(openssl rand -base64 75 | tr -dc 'A-Za-z0-9^@#!(){}[]%_\-+=~' | head -c 50)
|
||||||
|
PASSWORD_HASH=$(php -r "echo password_hash(\$argv[1], PASSWORD_DEFAULT);" -- "${PA_ADMIN_PASSWORD}" 2>/dev/null)
|
||||||
|
sqlite3 /opt/poweradmin/powerdns.db "INSERT INTO users (username, password, fullname, email, description, perm_templ, active, use_ldap) \
|
||||||
|
VALUES ('$(escape_sql "${PA_ADMIN_USERNAME}")', '$(escape_sql "${PASSWORD_HASH}")', '$(escape_sql "${PA_ADMIN_FULLNAME}")', \
|
||||||
|
'$(escape_sql "${PA_ADMIN_EMAIL}")', 'System Administrator', 1, 1, 0);"
|
||||||
|
|
||||||
|
cat <<EOF >~/poweradmin.creds
|
||||||
|
Admin Username: ${PA_ADMIN_USERNAME}
|
||||||
|
Admin Password: ${PA_ADMIN_PASSWORD}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat <<EOF >/opt/poweradmin/config/settings.php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Poweradmin Settings Configuration File
|
||||||
|
*
|
||||||
|
* Generated by the installer on 2026-02-02 21:01:40
|
||||||
|
*/
|
||||||
|
|
||||||
|
return [
|
||||||
|
/**
|
||||||
|
* Database Settings
|
||||||
|
*/
|
||||||
|
'database' => [
|
||||||
|
'type' => 'sqlite',
|
||||||
|
'file' => '/opt/poweradmin/powerdns.db',
|
||||||
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security Settings
|
||||||
|
*/
|
||||||
|
'security' => [
|
||||||
|
'session_key' => '${PA_SESSION_KEY}',
|
||||||
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface Settings
|
||||||
|
*/
|
||||||
|
'interface' => [
|
||||||
|
'language' => 'en_EN',
|
||||||
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DNS Settings
|
||||||
|
*/
|
||||||
|
'dns' => [
|
||||||
|
'hostmaster' => 'localhost.lan',
|
||||||
|
'ns1' => '8.8.8.8',
|
||||||
|
'ns2' => '9.9.9.9',
|
||||||
|
]
|
||||||
|
];
|
||||||
|
EOF
|
||||||
|
rm -rf /opt/poweradmin/install
|
||||||
|
msg_ok "Setup Poweradmin"
|
||||||
|
|
||||||
|
msg_info "Creating Service"
|
||||||
|
rm /etc/apache2/sites-enabled/000-default.conf
|
||||||
|
cat <<EOF >/etc/apache2/sites-enabled/poweradmin.conf
|
||||||
|
<VirtualHost *:80>
|
||||||
|
ServerName localhost
|
||||||
|
DocumentRoot /opt/poweradmin
|
||||||
|
|
||||||
|
<Directory /opt/poweradmin>
|
||||||
|
Options -Indexes +FollowSymLinks
|
||||||
|
AllowOverride All
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
# For DDNS update functionality
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteRule ^/update(.*)\$ /dynamic_update.php [L]
|
||||||
|
RewriteRule ^/nic/update(.*)\$ /dynamic_update.php [L]
|
||||||
|
</VirtualHost>
|
||||||
|
EOF
|
||||||
|
$STD a2enmod rewrite headers
|
||||||
|
chown -R www-data:www-data /opt/poweradmin
|
||||||
|
$STD systemctl restart apache2
|
||||||
|
msg_info "Created Service"
|
||||||
|
|
||||||
|
motd_ssh
|
||||||
|
customize
|
||||||
|
cleanup_lxc
|
||||||
@@ -25,7 +25,7 @@ if useradd -r -m -d /opt/pulse-home -s /usr/sbin/nologin pulse; then
|
|||||||
msg_ok "Created User"
|
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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
62
install/tinyauth-install.sh
Normal file
62
install/tinyauth-install.sh
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Copyright (c) 2021-2026 community-scripts ORG
|
||||||
|
# Author: MickLesk (CanbiZ)
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
|
# Source: https://github.com/steveiliop56/tinyauth
|
||||||
|
|
||||||
|
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||||
|
color
|
||||||
|
verb_ip6
|
||||||
|
catch_errors
|
||||||
|
setting_up_container
|
||||||
|
network_check
|
||||||
|
update_os
|
||||||
|
|
||||||
|
msg_info "Installing Dependencies"
|
||||||
|
$STD apt install -y \
|
||||||
|
openssl \
|
||||||
|
apache2-utils
|
||||||
|
msg_ok "Installed Dependencies"
|
||||||
|
|
||||||
|
fetch_and_deploy_gh_release "tinyauth" "steveiliop56/tinyauth" "singlefile" "latest" "/opt/tinyauth" "tinyauth-amd64"
|
||||||
|
|
||||||
|
msg_info "Setting up Tinyauth"
|
||||||
|
PASS=$(openssl rand -base64 8 | tr -dc 'a-zA-Z0-9' | head -c 8)
|
||||||
|
USER=$(htpasswd -Bbn "tinyauth" "${PASS}")
|
||||||
|
cat <<EOF >/opt/tinyauth/credentials.txt
|
||||||
|
Tinyauth Credentials
|
||||||
|
Username: tinyauth
|
||||||
|
Password: ${PASS}
|
||||||
|
EOF
|
||||||
|
msg_ok "Set up Tinyauth"
|
||||||
|
|
||||||
|
read -r -p "${TAB3}Enter your Tinyauth subdomain (e.g. https://tinyauth.example.com): " app_url
|
||||||
|
|
||||||
|
msg_info "Creating Service"
|
||||||
|
cat <<EOF >/opt/tinyauth/.env
|
||||||
|
TINYAUTH_DATABASE_PATH=/opt/tinyauth/database.db
|
||||||
|
TINYAUTH_AUTH_USERS='${USER}'
|
||||||
|
TINYAUTH_APPURL=${app_url}
|
||||||
|
EOF
|
||||||
|
cat <<EOF >/etc/systemd/system/tinyauth.service
|
||||||
|
[Unit]
|
||||||
|
Description=Tinyauth Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
EnvironmentFile=/opt/tinyauth/.env
|
||||||
|
ExecStart=/opt/tinyauth/tinyauth
|
||||||
|
WorkingDirectory=/opt/tinyauth
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
systemctl enable -q --now tinyauth
|
||||||
|
msg_ok "Created Service"
|
||||||
|
|
||||||
|
motd_ssh
|
||||||
|
customize
|
||||||
|
cleanup_lxc
|
||||||
@@ -44,7 +44,20 @@ $STD timescaledb-tune -yes -memory "$ram_for_tsdb"MB
|
|||||||
$STD systemctl restart postgresql
|
$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
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
119
misc/api.func
119
misc/api.func
@@ -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" ;;
|
||||||
|
|
||||||
@@ -338,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
|
||||||
|
}'
|
||||||
}
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
@@ -385,6 +395,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 +445,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 +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
|
||||||
}
|
}
|
||||||
@@ -769,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
|
||||||
}
|
}
|
||||||
@@ -868,7 +900,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 +920,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 +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=$(
|
||||||
@@ -915,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:-}",
|
||||||
@@ -946,7 +995,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 +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:-}",
|
||||||
@@ -966,7 +1015,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 +1038,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 +1050,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}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}}"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user