mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2026-03-03 17:35:55 +01:00
Compare commits
1 Commits
automated/
...
add-script
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e97bf159d8 |
84
.github/workflows/push-json-to-pocketbase.yml
generated
vendored
84
.github/workflows/push-json-to-pocketbase.yml
generated
vendored
@@ -105,11 +105,7 @@ jobs:
|
||||
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;
|
||||
});
|
||||
(typesData.items || []).forEach(function(item) { if (item.type) typeValueToId[item.type] = item.id; });
|
||||
}
|
||||
} catch (e) { console.warn('Could not fetch z_ref_script_types:', e.message); }
|
||||
try {
|
||||
@@ -119,33 +115,6 @@ jobs:
|
||||
(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'));
|
||||
@@ -168,7 +137,6 @@ jobs:
|
||||
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;
|
||||
@@ -181,49 +149,7 @@ jobs:
|
||||
});
|
||||
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',
|
||||
@@ -239,14 +165,6 @@ jobs:
|
||||
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.');
|
||||
|
||||
19
CHANGELOG.md
19
CHANGELOG.md
@@ -410,28 +410,11 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
||||
|
||||
</details>
|
||||
|
||||
## 2026-03-03
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Tinyauth: v5 Support & add Debian Version [@MickLesk](https://github.com/MickLesk) ([#12501](https://github.com/community-scripts/ProxmoxVE/pull/12501))
|
||||
|
||||
### 🗑️ Deleted Scripts
|
||||
|
||||
- Remove Unifi Network Server scripts (dead APT repo) [@Copilot](https://github.com/Copilot) ([#12500](https://github.com/community-scripts/ProxmoxVE/pull/12500))
|
||||
|
||||
### 🌐 Website
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Revert #11534 PR that messed up search [@BramSuurdje](https://github.com/BramSuurdje) ([#12492](https://github.com/community-scripts/ProxmoxVE/pull/12492))
|
||||
|
||||
## 2026-03-02
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- PowerDNS ([#12481](https://github.com/community-scripts/ProxmoxVE/pull/12481))
|
||||
- Profilarr ([#12441](https://github.com/community-scripts/ProxmoxVE/pull/12441))
|
||||
- Profilarr ([#12441](https://github.com/community-scripts/ProxmoxVE/pull/12441))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
|
||||
@@ -35,20 +35,6 @@ function update_script() {
|
||||
$STD service tinyauth stop
|
||||
msg_ok "Service Stopped"
|
||||
|
||||
if [[ -f /opt/tinyauth/.env ]] && ! grep -q "^TINYAUTH_" /opt/tinyauth/.env; then
|
||||
msg_info "Migrating .env to v5 format"
|
||||
sed -i \
|
||||
-e 's/^DATABASE_PATH=/TINYAUTH_DATABASE_PATH=/' \
|
||||
-e 's/^USERS=/TINYAUTH_AUTH_USERS=/' \
|
||||
-e "s/^USERS='/TINYAUTH_AUTH_USERS='/" \
|
||||
-e 's/^APP_URL=/TINYAUTH_APPURL=/' \
|
||||
-e 's/^SECRET=/TINYAUTH_AUTH_SECRET=/' \
|
||||
-e 's/^PORT=/TINYAUTH_SERVER_PORT=/' \
|
||||
-e 's/^ADDRESS=/TINYAUTH_SERVER_ADDRESS=/' \
|
||||
/opt/tinyauth/.env
|
||||
msg_ok "Migrated .env to v5 format"
|
||||
fi
|
||||
|
||||
msg_info "Updating Tinyauth"
|
||||
rm -f /opt/tinyauth/tinyauth
|
||||
curl -fsSL "https://github.com/steveiliop56/tinyauth/releases/download/v${RELEASE}/tinyauth-amd64" -o /opt/tinyauth/tinyauth
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
____ ____ _ _______
|
||||
/ __ \____ _ _____ _____/ __ \/ | / / ___/
|
||||
/ /_/ / __ \ | /| / / _ \/ ___/ / / / |/ /\__ \
|
||||
/ ____/ /_/ / |/ |/ / __/ / / /_/ / /| /___/ /
|
||||
/_/ \____/|__/|__/\___/_/ /_____/_/ |_//____/
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
_______ __ __
|
||||
/_ __(_)___ __ ______ ___ __/ /_/ /_
|
||||
/ / / / __ \/ / / / __ `/ / / / __/ __ \
|
||||
/ / / / / / / /_/ / /_/ / /_/ / /_/ / / /
|
||||
/_/ /_/_/ /_/\__, /\__,_/\__,_/\__/_/ /_/
|
||||
/____/
|
||||
6
ct/headers/unifi
Normal file
6
ct/headers/unifi
Normal file
@@ -0,0 +1,6 @@
|
||||
__ __ _ _____
|
||||
/ / / /___ (_) __(_)
|
||||
/ / / / __ \/ / /_/ /
|
||||
/ /_/ / / / / / __/ /
|
||||
\____/_/ /_/_/_/ /_/
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
#!/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}"
|
||||
47
ct/unifi.sh
Normal file
47
ct/unifi.sh
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
|
||||
# Copyright (c) 2021-2026 tteck
|
||||
# Author: tteck (tteckster)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://ui.com/download/unifi
|
||||
|
||||
APP="Unifi"
|
||||
var_tags="${var_tags:-network;unifi}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-2048}"
|
||||
var_disk="${var_disk:-8}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-12}"
|
||||
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 /usr/lib/unifi ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
JAVA_VERSION="21" setup_java
|
||||
|
||||
msg_info "Updating ${APP}"
|
||||
$STD apt update --allow-releaseinfo-change
|
||||
ensure_dependencies unifi
|
||||
msg_ok "Updated successfully!"
|
||||
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}https://${IP}:8443${CL}"
|
||||
@@ -13,7 +13,7 @@
|
||||
"website": "https://2fauth.app/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/2fauth.webp",
|
||||
"config_path": "cat /opt/2fauth/.env",
|
||||
"description": "2FAuth is a web based self-hosted alternative to One Time Passcode (OTP) generators like Google Authenticator, designed for both mobile and desktop. It aims to ease you perform your 2FA authentication steps whatever the device you handle, with a clean and suitable interface.",
|
||||
"description": "2FAuth is a web based self-hosted alternative to One Time Passcode (OTP) generators like Google Authenticator, designed for both mobile and desktop. It aims to ease you perform your 2FA authentication steps whatever the device you handle, with a clean and suitable interface. - Workflow - Test",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "Tinyauth",
|
||||
"slug": "tinyauth",
|
||||
"name": "Alpine-Tinyauth",
|
||||
"slug": "alpine-tinyauth",
|
||||
"categories": [
|
||||
6
|
||||
],
|
||||
"date_created": "2026-03-03",
|
||||
"date_created": "2025-05-06",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
@@ -17,13 +17,13 @@
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/tinyauth.sh",
|
||||
"script": "ct/alpine-tinyauth.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 4,
|
||||
"os": "debian",
|
||||
"version": "13"
|
||||
"ram": 256,
|
||||
"hdd": 2,
|
||||
"os": "alpine",
|
||||
"version": "3.23"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generated": "2026-03-03T12:12:16Z",
|
||||
"generated": "2026-03-02T13:48:26Z",
|
||||
"versions": [
|
||||
{
|
||||
"slug": "2fauth",
|
||||
@@ -291,9 +291,9 @@
|
||||
{
|
||||
"slug": "dispatcharr",
|
||||
"repo": "Dispatcharr/Dispatcharr",
|
||||
"version": "v0.20.2",
|
||||
"version": "v0.20.1",
|
||||
"pinned": false,
|
||||
"date": "2026-03-03T01:40:33Z"
|
||||
"date": "2026-02-26T21:38:19Z"
|
||||
},
|
||||
{
|
||||
"slug": "docmost",
|
||||
@@ -382,9 +382,9 @@
|
||||
{
|
||||
"slug": "firefly",
|
||||
"repo": "firefly-iii/firefly-iii",
|
||||
"version": "v6.5.2",
|
||||
"version": "v6.5.1",
|
||||
"pinned": false,
|
||||
"date": "2026-03-03T05:42:27Z"
|
||||
"date": "2026-02-27T20:55:55Z"
|
||||
},
|
||||
{
|
||||
"slug": "fladder",
|
||||
@@ -585,9 +585,9 @@
|
||||
{
|
||||
"slug": "immich-public-proxy",
|
||||
"repo": "alangrainger/immich-public-proxy",
|
||||
"version": "v1.15.4",
|
||||
"version": "v1.15.3",
|
||||
"pinned": false,
|
||||
"date": "2026-03-02T21:28:06Z"
|
||||
"date": "2026-02-16T22:54:27Z"
|
||||
},
|
||||
{
|
||||
"slug": "inspircd",
|
||||
@@ -613,9 +613,9 @@
|
||||
{
|
||||
"slug": "jackett",
|
||||
"repo": "Jackett/Jackett",
|
||||
"version": "v0.24.1261",
|
||||
"version": "v0.24.1247",
|
||||
"pinned": false,
|
||||
"date": "2026-03-03T05:54:20Z"
|
||||
"date": "2026-03-02T05:56:37Z"
|
||||
},
|
||||
{
|
||||
"slug": "jellystat",
|
||||
@@ -872,9 +872,9 @@
|
||||
{
|
||||
"slug": "metube",
|
||||
"repo": "alexta69/metube",
|
||||
"version": "2026.03.02",
|
||||
"version": "2026.02.27",
|
||||
"pinned": false,
|
||||
"date": "2026-03-02T19:19:10Z"
|
||||
"date": "2026-02-27T11:47:02Z"
|
||||
},
|
||||
{
|
||||
"slug": "miniflux",
|
||||
@@ -1149,13 +1149,6 @@
|
||||
"pinned": false,
|
||||
"date": "2026-02-23T19:50:48Z"
|
||||
},
|
||||
{
|
||||
"slug": "powerdns",
|
||||
"repo": "poweradmin/poweradmin",
|
||||
"version": "v4.0.7",
|
||||
"pinned": false,
|
||||
"date": "2026-02-15T20:09:48Z"
|
||||
},
|
||||
{
|
||||
"slug": "privatebin",
|
||||
"repo": "PrivateBin/PrivateBin",
|
||||
@@ -1229,9 +1222,9 @@
|
||||
{
|
||||
"slug": "pulse",
|
||||
"repo": "rcourtman/Pulse",
|
||||
"version": "v5.1.17",
|
||||
"version": "v5.1.16",
|
||||
"pinned": false,
|
||||
"date": "2026-03-02T20:15:31Z"
|
||||
"date": "2026-03-01T23:13:09Z"
|
||||
},
|
||||
{
|
||||
"slug": "pve-scripts-local",
|
||||
@@ -1355,9 +1348,9 @@
|
||||
{
|
||||
"slug": "scanopy",
|
||||
"repo": "scanopy/scanopy",
|
||||
"version": "v0.14.11",
|
||||
"version": "v0.14.10",
|
||||
"pinned": false,
|
||||
"date": "2026-03-02T08:48:42Z"
|
||||
"date": "2026-02-28T21:05:12Z"
|
||||
},
|
||||
{
|
||||
"slug": "scraparr",
|
||||
@@ -1562,13 +1555,6 @@
|
||||
"pinned": false,
|
||||
"date": "2026-02-13T16:30:09Z"
|
||||
},
|
||||
{
|
||||
"slug": "tinyauth",
|
||||
"repo": "steveiliop56/tinyauth",
|
||||
"version": "v5.0.0",
|
||||
"pinned": false,
|
||||
"date": "2026-03-02T18:43:57Z"
|
||||
},
|
||||
{
|
||||
"slug": "traccar",
|
||||
"repo": "traccar/traccar",
|
||||
@@ -1740,9 +1726,9 @@
|
||||
{
|
||||
"slug": "wealthfolio",
|
||||
"repo": "afadil/wealthfolio",
|
||||
"version": "v3.0.2",
|
||||
"version": "v3.0.0",
|
||||
"pinned": false,
|
||||
"date": "2026-03-03T05:01:49Z"
|
||||
"date": "2026-02-24T22:37:05Z"
|
||||
},
|
||||
{
|
||||
"slug": "web-check",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"categories": [
|
||||
5
|
||||
],
|
||||
"date_created": "2026-03-02",
|
||||
"date_created": "2026-02-11",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
@@ -37,4 +37,4 @@
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
42
frontend/public/json/unifi.json
Normal file
42
frontend/public/json/unifi.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "UniFi Network Server",
|
||||
"slug": "unifi",
|
||||
"categories": [
|
||||
4
|
||||
],
|
||||
"date_created": "2024-05-02",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 8443,
|
||||
"documentation": "https://help.ui.com/hc/en-us/articles/360012282453-Self-Hosting-a-UniFi-Network-Server",
|
||||
"website": "https://www.ui.com/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/ubiquiti-unifi.webp",
|
||||
"config_path": "",
|
||||
"description": "UniFi Network Server is a software that helps manage and monitor UniFi networks (Wi-Fi, Ethernet, etc.) by providing an intuitive user interface and advanced features. It allows network administrators to configure, monitor, and upgrade network devices, as well as view network statistics, client devices, and historical events. The aim of the application is to make the management of UniFi networks easier and more efficient.",
|
||||
"disable": true,
|
||||
"disable_description": "This script is disabled because UniFi no longer delivers APT packages for Debian systems. The installation relies on APT repositories that are no longer maintained or available. For more details, see: https://github.com/community-scripts/ProxmoxVE/issues/11876",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/unifi.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 8,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "For non-AVX CPUs, MongoDB 4.4 is installed. Please note this is a legacy solution that may present security risks and could become unsupported in future updates.",
|
||||
"type": "warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,68 +2,42 @@
|
||||
|
||||
import type { z } from "zod";
|
||||
|
||||
import { githubGist, nord } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||
import { CalendarIcon, Check, Clipboard, Download } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import { useTheme } from "next-themes";
|
||||
import { format } from "date-fns";
|
||||
import { toast } from "sonner";
|
||||
import Image from "next/image";
|
||||
|
||||
import type { Category } from "@/lib/types";
|
||||
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { basePath } from "@/config/site-config";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { fetchCategories } from "@/lib/data";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import type { Script } from "./_schemas/schemas";
|
||||
|
||||
import { ScriptItem } from "../scripts/_components/script-item";
|
||||
import InstallMethod from "./_components/install-method";
|
||||
import { ScriptSchema } from "./_schemas/schemas";
|
||||
import Categories from "./_components/categories";
|
||||
import Note from "./_components/note";
|
||||
|
||||
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);
|
||||
}
|
||||
import { githubGist, nord } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import { ScriptItem } from "../scripts/_components/script-item";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { search } from "@/components/command-menu";
|
||||
import { basePath } from "@/config/site-config";
|
||||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
const initialScript: Script = {
|
||||
name: "",
|
||||
@@ -103,32 +77,32 @@ export default function JSONGenerator() {
|
||||
|
||||
const selectedCategoryObj = useMemo(
|
||||
() => categories.find(cat => cat.id.toString() === selectedCategory),
|
||||
[categories, selectedCategory],
|
||||
[categories, selectedCategory]
|
||||
);
|
||||
|
||||
const allScripts = useMemo(
|
||||
() => categories.flatMap(cat => cat.scripts || []),
|
||||
[categories],
|
||||
[categories]
|
||||
);
|
||||
|
||||
const scripts = useMemo(() => {
|
||||
const query = searchQuery.trim();
|
||||
const query = searchQuery.trim()
|
||||
|
||||
if (query) {
|
||||
return search(allScripts, query);
|
||||
return search(allScripts, query)
|
||||
}
|
||||
|
||||
if (selectedCategoryObj) {
|
||||
return selectedCategoryObj.scripts || [];
|
||||
return selectedCategoryObj.scripts || []
|
||||
}
|
||||
|
||||
return [];
|
||||
return []
|
||||
}, [allScripts, selectedCategoryObj, searchQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCategories()
|
||||
.then(setCategories)
|
||||
.catch(error => console.error("Error fetching categories:", error));
|
||||
.catch((error) => console.error("Error fetching categories:", error));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -148,14 +122,11 @@ export default function JSONGenerator() {
|
||||
|
||||
if (updated.type === "pve") {
|
||||
scriptPath = `tools/pve/${updated.slug}.sh`;
|
||||
}
|
||||
else if (updated.type === "addon") {
|
||||
} else if (updated.type === "addon") {
|
||||
scriptPath = `tools/addon/${updated.slug}.sh`;
|
||||
}
|
||||
else if (method.type === "alpine") {
|
||||
} else if (method.type === "alpine") {
|
||||
scriptPath = `${updated.type}/alpine-${updated.slug}.sh`;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
scriptPath = `${updated.type}/${updated.slug}.sh`;
|
||||
}
|
||||
|
||||
@@ -174,13 +145,11 @@ export default function JSONGenerator() {
|
||||
}, []);
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
if (!isValid)
|
||||
toast.warning("JSON schema is invalid. Copying anyway.");
|
||||
if (!isValid) toast.warning("JSON schema is invalid. Copying anyway.");
|
||||
navigator.clipboard.writeText(JSON.stringify(script, null, 2));
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
if (isValid)
|
||||
toast.success("Copied metadata to clipboard");
|
||||
if (isValid) toast.success("Copied metadata to clipboard");
|
||||
}, [script]);
|
||||
|
||||
const importScript = (script: Script) => {
|
||||
@@ -197,11 +166,11 @@ export default function JSONGenerator() {
|
||||
setIsValid(true);
|
||||
setZodErrors(null);
|
||||
toast.success("Imported JSON successfully");
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
toast.error("Failed to read or parse the JSON file.");
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
const handleFileImport = useCallback(() => {
|
||||
const input = document.createElement("input");
|
||||
@@ -211,8 +180,7 @@ export default function JSONGenerator() {
|
||||
input.onchange = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const file = target.files?.[0];
|
||||
if (!file)
|
||||
return;
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
@@ -221,8 +189,7 @@ export default function JSONGenerator() {
|
||||
const parsed = JSON.parse(content);
|
||||
importScript(parsed);
|
||||
toast.success("Imported JSON successfully");
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
toast.error("Failed to read the JSON file.");
|
||||
}
|
||||
};
|
||||
@@ -276,10 +243,7 @@ export default function JSONGenerator() {
|
||||
<div className="mt-2 space-y-1">
|
||||
{zodErrors.issues.map((error, index) => (
|
||||
<AlertDescription key={index} className="p-1 text-red-500">
|
||||
{error.path.join(".")}
|
||||
{" "}
|
||||
-
|
||||
{error.message}
|
||||
{error.path.join(".")} -{error.message}
|
||||
</AlertDescription>
|
||||
))}
|
||||
</div>
|
||||
@@ -306,7 +270,7 @@ export default function JSONGenerator() {
|
||||
onOpenChange={setIsImportDialogOpen}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem onSelect={e => e.preventDefault()}>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
Import existing script
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
@@ -328,7 +292,7 @@ export default function JSONGenerator() {
|
||||
<SelectValue placeholder="Category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map(category => (
|
||||
{categories.map((category) => (
|
||||
<SelectItem key={category.id} value={category.id.toString()}>
|
||||
{category.name}
|
||||
</SelectItem>
|
||||
@@ -338,44 +302,40 @@ export default function JSONGenerator() {
|
||||
<Input
|
||||
placeholder="Search for a script..."
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
{!selectedCategory && !searchQuery
|
||||
? (
|
||||
<p className="text-muted-foreground text-sm text-center">
|
||||
Select a category or search for a script
|
||||
</p>
|
||||
)
|
||||
: scripts.length === 0
|
||||
? (
|
||||
<p className="text-muted-foreground text-sm text-center">
|
||||
No scripts found
|
||||
</p>
|
||||
)
|
||||
: (
|
||||
<div className="grid grid-cols-3 auto-rows-min h-64 overflow-y-auto gap-4">
|
||||
{scripts.map(script => (
|
||||
<div
|
||||
key={script.slug}
|
||||
className="p-2 border rounded cursor-pointer hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => {
|
||||
importScript(script);
|
||||
setIsImportDialogOpen(false);
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={script.logo || `/${basePath}/logo.png`}
|
||||
alt={script.name}
|
||||
className="w-full h-12 object-contain mb-2"
|
||||
width={16}
|
||||
height={16}
|
||||
unoptimized
|
||||
/>
|
||||
<p className="text-sm text-center">{script.name}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!selectedCategory && !searchQuery ? (
|
||||
<p className="text-muted-foreground text-sm text-center">
|
||||
Select a category or search for a script
|
||||
</p>
|
||||
) : scripts.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm text-center">
|
||||
No scripts found
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 auto-rows-min h-64 overflow-y-auto gap-4">
|
||||
{scripts.map(script => (
|
||||
<div
|
||||
key={script.slug}
|
||||
className="p-2 border rounded cursor-pointer hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => {
|
||||
importScript(script);
|
||||
setIsImportDialogOpen(false);
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={script.logo || `/${basePath}/logo.png`}
|
||||
alt={script.name}
|
||||
className="w-full h-12 object-contain mb-2"
|
||||
width={16}
|
||||
height={16}
|
||||
unoptimized
|
||||
/>
|
||||
<p className="text-sm text-center">{script.name}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
@@ -388,19 +348,15 @@ export default function JSONGenerator() {
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>
|
||||
Name
|
||||
{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
Name <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input placeholder="Example" value={script.name} onChange={e => updateScript("name", e.target.value)} />
|
||||
<Input placeholder="Example" value={script.name} onChange={(e) => updateScript("name", e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>
|
||||
Slug
|
||||
{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
Slug <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input placeholder="example" value={script.slug} onChange={e => updateScript("slug", e.target.value)} />
|
||||
<Input placeholder="example" value={script.slug} onChange={(e) => updateScript("slug", e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -410,7 +366,7 @@ export default function JSONGenerator() {
|
||||
<Input
|
||||
placeholder="Full logo URL"
|
||||
value={script.logo || ""}
|
||||
onChange={e => updateScript("logo", e.target.value || null)}
|
||||
onChange={(e) => updateScript("logo", e.target.value || null)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -418,28 +374,24 @@ export default function JSONGenerator() {
|
||||
<Input
|
||||
placeholder="Path to config file"
|
||||
value={script.config_path || ""}
|
||||
onChange={e => updateScript("config_path", e.target.value || "")}
|
||||
onChange={(e) => updateScript("config_path", e.target.value || "")}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>
|
||||
Description
|
||||
{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
Description <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
placeholder="Example"
|
||||
value={script.description}
|
||||
onChange={e => updateScript("description", e.target.value)}
|
||||
onChange={(e) => updateScript("description", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Categories script={script} setScript={setScript} categories={categories} />
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<Label>
|
||||
Date Created
|
||||
{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
Date Created <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild className="flex-1">
|
||||
@@ -463,7 +415,7 @@ export default function JSONGenerator() {
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<Label>Type</Label>
|
||||
<Select value={script.type} onValueChange={value => updateScript("type", value)}>
|
||||
<Select value={script.type} onValueChange={(value) => updateScript("type", value)}>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue placeholder="Type" />
|
||||
</SelectTrigger>
|
||||
@@ -478,17 +430,17 @@ export default function JSONGenerator() {
|
||||
</div>
|
||||
<div className="w-full flex gap-5">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch checked={script.updateable} onCheckedChange={checked => updateScript("updateable", checked)} />
|
||||
<Switch checked={script.updateable} onCheckedChange={(checked) => updateScript("updateable", checked)} />
|
||||
<label>Updateable</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch checked={script.privileged} onCheckedChange={checked => updateScript("privileged", checked)} />
|
||||
<Switch checked={script.privileged} onCheckedChange={(checked) => updateScript("privileged", checked)} />
|
||||
<label>Privileged</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={script.disable || false}
|
||||
onCheckedChange={checked => updateScript("disable", checked)}
|
||||
onCheckedChange={(checked) => updateScript("disable", checked)}
|
||||
/>
|
||||
<label>Disabled</label>
|
||||
</div>
|
||||
@@ -496,14 +448,12 @@ export default function JSONGenerator() {
|
||||
{script.disable && (
|
||||
<div>
|
||||
<Label>
|
||||
Disable Description
|
||||
{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
Disable Description <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
placeholder="Explain why this script is disabled..."
|
||||
value={script.disable_description || ""}
|
||||
onChange={e => updateScript("disable_description", e.target.value)}
|
||||
onChange={(e) => updateScript("disable_description", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -511,18 +461,18 @@ export default function JSONGenerator() {
|
||||
placeholder="Interface Port"
|
||||
type="number"
|
||||
value={script.interface_port || ""}
|
||||
onChange={e => updateScript("interface_port", e.target.value ? Number(e.target.value) : null)}
|
||||
onChange={(e) => updateScript("interface_port", e.target.value ? Number(e.target.value) : null)}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Website URL"
|
||||
value={script.website || ""}
|
||||
onChange={e => updateScript("website", e.target.value || null)}
|
||||
onChange={(e) => updateScript("website", e.target.value || null)}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Documentation URL"
|
||||
value={script.documentation || ""}
|
||||
onChange={e => updateScript("documentation", e.target.value || null)}
|
||||
onChange={(e) => updateScript("documentation", e.target.value || null)}
|
||||
/>
|
||||
</div>
|
||||
<InstallMethod script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
||||
@@ -530,20 +480,22 @@ export default function JSONGenerator() {
|
||||
<Input
|
||||
placeholder="Username"
|
||||
value={script.default_credentials.username || ""}
|
||||
onChange={e =>
|
||||
onChange={(e) =>
|
||||
updateScript("default_credentials", {
|
||||
...script.default_credentials,
|
||||
username: e.target.value || null,
|
||||
})}
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Password"
|
||||
value={script.default_credentials.password || ""}
|
||||
onChange={e =>
|
||||
onChange={(e) =>
|
||||
updateScript("default_credentials", {
|
||||
...script.default_credentials,
|
||||
password: e.target.value || null,
|
||||
})}
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Note script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
||||
</form>
|
||||
@@ -552,7 +504,7 @@ export default function JSONGenerator() {
|
||||
<Tabs
|
||||
defaultValue="json"
|
||||
className="w-full"
|
||||
onValueChange={value => setCurrentTab(value as "json" | "preview")}
|
||||
onValueChange={(value) => setCurrentTab(value as "json" | "preview")}
|
||||
value={currentTab}
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ArrowRightIcon, Sparkles } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ArrowRightIcon, Sparkles } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
import type { Category, Script } from "@/lib/types";
|
||||
@@ -22,6 +21,35 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/t
|
||||
import { DialogTitle } from "./ui/dialog";
|
||||
import { Button } from "./ui/button";
|
||||
import { Badge } from "./ui/badge";
|
||||
import Link from "next/link";
|
||||
|
||||
export function search(scripts: Script[], query: string): Script[] {
|
||||
const queryLower = query.toLowerCase().trim();
|
||||
const searchWords = queryLower.split(/\s+/).filter(Boolean);
|
||||
|
||||
return scripts
|
||||
.map(script => {
|
||||
const nameLower = script.name.toLowerCase();
|
||||
const descriptionLower = (script.description || "").toLowerCase();
|
||||
|
||||
let score = 0;
|
||||
|
||||
for (const word of searchWords) {
|
||||
if (nameLower.includes(word)) {
|
||||
score += 10;
|
||||
}
|
||||
if (descriptionLower.includes(word)) {
|
||||
score += 5;
|
||||
}
|
||||
}
|
||||
|
||||
return { script, score };
|
||||
})
|
||||
.filter(({ score }) => score > 0)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 20)
|
||||
.map(({ script }) => script);
|
||||
}
|
||||
|
||||
export function formattedBadge(type: string) {
|
||||
switch (type) {
|
||||
@@ -51,9 +79,11 @@ function getRandomScript(categories: Category[], previouslySelected: Set<string>
|
||||
}
|
||||
|
||||
function CommandMenu() {
|
||||
const [query, setQuery] = React.useState("");
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [links, setLinks] = React.useState<Category[]>([]);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [results, setResults] = React.useState<Script[]>([]);
|
||||
const [selectedScripts, setSelectedScripts] = React.useState<Set<string>>(new Set());
|
||||
const router = useRouter();
|
||||
|
||||
@@ -70,6 +100,27 @@ function CommandMenu() {
|
||||
});
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (query.trim() === "") {
|
||||
fetchSortedCategories();
|
||||
}
|
||||
else {
|
||||
const scriptMap = new Map<string, Script>();
|
||||
|
||||
for (const category of links) {
|
||||
for (const script of category.scripts || []) {
|
||||
if (!scriptMap.has(script.slug)) {
|
||||
scriptMap.set(script.slug, script);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueScripts = Array.from(scriptMap.values());
|
||||
const filteredResults = search(uniqueScripts, query);
|
||||
setResults(filteredResults);
|
||||
}
|
||||
}, [query]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||
@@ -197,49 +248,46 @@ function CommandMenu() {
|
||||
|
||||
<CommandDialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
filter={(value: string, search: string) => {
|
||||
const searchLower = search.toLowerCase().trim();
|
||||
if (!searchLower)
|
||||
return 1;
|
||||
const valueLower = value.toLowerCase();
|
||||
const searchWords = searchLower.split(/\s+/).filter(Boolean);
|
||||
// All search words must appear somewhere in the value (name + description)
|
||||
const allWordsMatch = searchWords.every((word: string) => valueLower.includes(word));
|
||||
return allWordsMatch ? 1 : 0;
|
||||
onOpenChange={(open) => {
|
||||
setOpen(open);
|
||||
if (open) {
|
||||
setQuery("");
|
||||
setResults([]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle className="sr-only">Search scripts</DialogTitle>
|
||||
<CommandInput placeholder="Search for a script..." />
|
||||
<CommandInput
|
||||
placeholder="Search for a script..."
|
||||
onValueChange={setQuery}
|
||||
value={query}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{isLoading
|
||||
? (
|
||||
"Searching..."
|
||||
)
|
||||
: (
|
||||
<div className="flex flex-col items-center justify-center py-6 text-center">
|
||||
<p className="text-sm text-muted-foreground">No scripts match your search.</p>
|
||||
<div className="mt-4">
|
||||
<p className="text-xs text-muted-foreground mb-2">Want to add a new script?</p>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link
|
||||
href={`https://github.com/community-scripts/${basePath}/tree/main/docs/contribution/GUIDE.md`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation
|
||||
{" "}
|
||||
<ArrowRightIcon className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isLoading ? (
|
||||
"Searching..."
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-6 text-center">
|
||||
<p className="text-sm text-muted-foreground">No scripts match your search.</p>
|
||||
<div className="mt-4">
|
||||
<p className="text-xs text-muted-foreground mb-2">Want to add a new script?</p>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link
|
||||
href={`https://github.com/community-scripts/${basePath}/tree/main/docs/contribution/GUIDE.md`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation <ArrowRightIcon className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CommandEmpty>
|
||||
{Object.entries(uniqueScriptsByCategory).map(([categoryName, scripts]) => (
|
||||
<CommandGroup key={`category:${categoryName}`} heading={categoryName}>
|
||||
{scripts.map(script => (
|
||||
|
||||
{results.length > 0 ? (
|
||||
<CommandGroup heading="Search Results">
|
||||
{results.map(script => (
|
||||
<CommandItem
|
||||
key={`script:${script.slug}`}
|
||||
value={`${script.name} ${script.type} ${script.description || ""}`}
|
||||
@@ -272,7 +320,44 @@ function CommandMenu() {
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
) : ( // When no search results, show all scripts grouped by category
|
||||
Object.entries(uniqueScriptsByCategory).map(([categoryName, scripts]) => (
|
||||
<CommandGroup key={`category:${categoryName}`} heading={categoryName}>
|
||||
{scripts.map(script => (
|
||||
<CommandItem
|
||||
key={`script:${script.slug}`}
|
||||
value={`${script.name} ${script.type} ${script.description || ""}`}
|
||||
onSelect={() => {
|
||||
setOpen(false);
|
||||
router.push(`/scripts?id=${script.slug}`);
|
||||
}}
|
||||
tabIndex={0}
|
||||
aria-label={`Open script ${script.name}`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
setOpen(false);
|
||||
router.push(`/scripts?id=${script.slug}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-2" onClick={() => setOpen(false)}>
|
||||
<Image
|
||||
src={script.logo || `/${basePath}/logo.png`}
|
||||
onError={e => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
||||
unoptimized
|
||||
width={16}
|
||||
height={16}
|
||||
alt=""
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
<span>{script.name}</span>
|
||||
<span>{formattedBadge(script.type)}</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))
|
||||
)}
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
</>
|
||||
|
||||
@@ -36,9 +36,9 @@ msg_ok "Installed Tinyauth"
|
||||
read -r -p "${TAB3}Enter your Tinyauth subdomain (e.g. https://tinyauth.example.com): " app_url
|
||||
|
||||
cat <<EOF >/opt/tinyauth/.env
|
||||
TINYAUTH_DATABASE_PATH=/opt/tinyauth/database.db
|
||||
TINYAUTH_AUTH_USERS='${USER}'
|
||||
TINYAUTH_APPURL=${app_url}
|
||||
DATABASE_PATH=/opt/tinyauth/database.db
|
||||
USERS='${USER}'
|
||||
APP_URL=${app_url}
|
||||
EOF
|
||||
|
||||
msg_info "Creating Service"
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
#!/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
|
||||
53
install/unifi-install.sh
Normal file
53
install/unifi-install.sh
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 tteck
|
||||
# Author: tteck (tteckster)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://ui.com/download/unifi
|
||||
|
||||
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 apt-transport-https
|
||||
curl -fsSL "https://dl.ui.com/unifi/unifi-repo.gpg" -o "/usr/share/keyrings/unifi-repo.gpg"
|
||||
cat <<EOF | sudo tee /etc/apt/sources.list.d/100-ubnt-unifi.sources >/dev/null
|
||||
Types: deb
|
||||
URIs: https://www.ui.com/downloads/unifi/debian
|
||||
Suites: stable
|
||||
Components: ubiquiti
|
||||
Architectures: amd64
|
||||
Signed-By: /usr/share/keyrings/unifi-repo.gpg
|
||||
EOF
|
||||
$STD apt update
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
JAVA_VERSION="21" setup_java
|
||||
|
||||
if lscpu | grep -q 'avx'; then
|
||||
MONGO_VERSION="8.0" setup_mongodb
|
||||
else
|
||||
msg_error "No AVX detected (CPU-Flag)! We have discontinued support for this. You are welcome to try it manually with a Debian LXC, but due to the many issues with Unifi, we currently only support AVX CPUs."
|
||||
exit 10
|
||||
fi
|
||||
|
||||
if ! dpkg -l | grep -q 'libssl1.1'; then
|
||||
msg_info "Installing libssl (if needed)"
|
||||
curl -fsSL "https://security.debian.org/debian-security/pool/updates/main/o/openssl/libssl1.1_1.1.1w-0+deb11u4_amd64.deb" -o "/tmp/libssl.deb"
|
||||
$STD dpkg -i /tmp/libssl.deb
|
||||
rm -f /tmp/libssl.deb
|
||||
msg_ok "Installed libssl1.1"
|
||||
fi
|
||||
|
||||
msg_info "Installing UniFi Network Server"
|
||||
$STD apt install -y unifi
|
||||
msg_ok "Installed UniFi Network Server"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
@@ -346,20 +346,18 @@ explain_exit_code() {
|
||||
# - Handles backslashes, quotes, newlines, tabs, and carriage returns
|
||||
# ------------------------------------------------------------------------------
|
||||
json_escape() {
|
||||
# Escape a string for safe JSON embedding using awk (handles any input size).
|
||||
# Pipeline: strip ANSI → remove control chars → escape \ " TAB → join lines with \n
|
||||
printf '%s' "$1" \
|
||||
| sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' \
|
||||
| tr -d '\000-\010\013\014\016-\037\177\r' \
|
||||
| awk '
|
||||
BEGIN { ORS = "" }
|
||||
{
|
||||
gsub(/\\/, "\\\\") # backslash → \\
|
||||
gsub(/"/, "\\\"") # double quote → \"
|
||||
gsub(/\t/, "\\t") # tab → \t
|
||||
if (NR > 1) printf "\\n"
|
||||
printf "%s", $0
|
||||
}'
|
||||
local s="$1"
|
||||
# Strip ANSI escape sequences (color codes etc.)
|
||||
s=$(printf '%s' "$s" | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g')
|
||||
s=${s//\\/\\\\}
|
||||
s=${s//"/\\"/}
|
||||
s=${s//$'\n'/\\n}
|
||||
s=${s//$'\r'/}
|
||||
s=${s//$'\t'/\\t}
|
||||
# Remove any remaining control characters (0x00-0x1F except those already handled)
|
||||
# Also remove DEL (0x7F) and invalid high bytes that break JSON parsers
|
||||
s=$(printf '%s' "$s" | tr -d '\000-\010\013\014\016-\037\177')
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user