Merge branch 'main' into develop

This commit is contained in:
MacRimi
2026-04-17 23:49:29 +02:00
committed by GitHub
11 changed files with 21859 additions and 2818 deletions

View File

@@ -0,0 +1,117 @@
title: "[Prompt] "
labels:
- custom-prompt
- community
body:
- type: markdown
attributes:
value: |
## Share Your Custom Prompt
Thank you for sharing your custom prompt with the community!
**Title format suggestion:** Include the provider in the title for easy filtering.
Example: `[Gemini] Clean Spanish - Structured, no emojis`
This helps others find prompts for their specific AI provider.
- type: dropdown
id: provider
attributes:
label: AI Provider
description: Which AI provider did you test this prompt with?
options:
- OpenAI
- Gemini
- Groq
- Ollama
- Anthropic
- OpenRouter
- DeepSeek
- Other
validations:
required: true
- type: input
id: model
attributes:
label: Model
description: The specific model you tested with
placeholder: "e.g., gpt-4o-mini, gemini-2.0-flash, llama3.2:3b"
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: Describe what your prompt does, main features, and output language
placeholder: |
This prompt generates concise notifications in Spanish.
Features:
- Brief format (2-3 lines)
- Includes severity indicators
- Uses emojis for visual clarity
validations:
required: true
- type: textarea
id: prompt-content
attributes:
label: Prompt Content
description: Paste your complete custom prompt here
render: text
placeholder: |
You are a notification formatter for ProxMenux Monitor.
Your task is to...
RULES:
1. ...
2. ...
OUTPUT FORMAT:
[TITLE]
...
[BODY]
...
validations:
required: true
- type: textarea
id: example-output
attributes:
label: Example Output
description: Show an example of how a notification looks with your prompt
placeholder: |
**Input notification:**
CPU usage high on node pve01
**Output with this prompt:**
pve01: High CPU Usage
CPU at 95% for 5 minutes. Check running processes.
validations:
required: false
- type: textarea
id: additional-notes
attributes:
label: Additional Notes
description: Any tips, variations, or known limitations
placeholder: |
- Works best with models that support system prompts
- May need adjustment for very long notifications
- Tested with Proxmox VE 8.x
validations:
required: false
- type: checkboxes
id: confirmation
attributes:
label: Confirmation
options:
- label: I have tested this prompt and it works correctly
required: true
- label: I am sharing this prompt for the community to use freely
required: true

View File

@@ -3,26 +3,28 @@ import json
import re import re
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Any
import requests import requests
# ---------- Config ----------
API_URL = "https://api.github.com/repos/community-scripts/ProxmoxVE/contents/frontend/public/json"
SCRIPT_BASE = "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main" SCRIPT_BASE = "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
POCKETBASE_BASE = "https://db.community-scripts.org/api/collections"
SCRIPT_COLLECTION_URL = f"{POCKETBASE_BASE}/script_scripts/records"
CATEGORY_COLLECTION_URL = f"{POCKETBASE_BASE}/script_categories/records"
# Escribimos siempre en <raiz_repo>/json/helpers_cache.json, independientemente del cwd
REPO_ROOT = Path(__file__).resolve().parents[2] REPO_ROOT = Path(__file__).resolve().parents[2]
OUTPUT_FILE = REPO_ROOT / "json" / "helpers_cache.json" OUTPUT_FILE = REPO_ROOT / "json" / "helpers_cache.json"
OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True) OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True)
# ----------------------------
TYPE_TO_PATH_PREFIX = {
"lxc": "ct",
"vm": "vm",
"addon": "tools/addon",
"pve": "tools/pve",
}
def to_mirror_url(raw_url: str) -> str: def to_mirror_url(raw_url: str) -> str:
"""
Convierte una URL raw de GitHub al raw del mirror.
GH : https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/docker.sh
MIR: https://git.community-scripts.org/community-scripts/ProxmoxVE/raw/branch/main/ct/docker.sh
"""
m = re.match(r"^https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/([^/]+)/(.+)$", raw_url or "") m = re.match(r"^https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/([^/]+)/(.+)$", raw_url or "")
if not m: if not m:
return "" return ""
@@ -32,143 +34,202 @@ def to_mirror_url(raw_url: str) -> str:
return f"https://git.community-scripts.org/community-scripts/ProxmoxVE/raw/branch/{branch}/{path}" return f"https://git.community-scripts.org/community-scripts/ProxmoxVE/raw/branch/{branch}/{path}"
def guess_os_from_script_path(script_path: str) -> str | None: def fetch_json(url: str, *, params: dict[str, Any] | None = None) -> dict[str, Any]:
""" r = requests.get(url, params=params, timeout=60)
Heurística suave cuando el JSON no publica resources.os:
- tools/pve/* -> proxmox
- ct/alpine-* -> alpine
- tools/addon/* -> generic (suele ejecutarse sobre LXC existente)
- ct/* -> debian (por defecto para CTs)
"""
if not script_path:
return None
if script_path.startswith("tools/pve/") or script_path == "tools/pve/host-backup.sh" or script_path.startswith("vm/"):
return "proxmox"
if "/alpine-" in script_path or script_path.startswith("ct/alpine-"):
return "alpine"
if script_path.startswith("tools/addon/"):
return "generic"
if script_path.startswith("ct/"):
return "debian"
return None
def fetch_directory_json(api_url: str) -> list[dict]:
r = requests.get(api_url, timeout=30)
r.raise_for_status() r.raise_for_status()
data = r.json() data = r.json()
if not isinstance(data, list): if not isinstance(data, dict):
raise RuntimeError("GitHub API no devolvió una lista.") raise RuntimeError(f"Unexpected response from {url}: expected object")
return data return data
def fetch_all_records(url: str, *, expand: str | None = None, per_page: int = 500) -> list[dict[str, Any]]:
page = 1
items: list[dict[str, Any]] = []
while True:
params: dict[str, Any] = {"page": page, "perPage": per_page}
if expand:
params["expand"] = expand
data = fetch_json(url, params=params)
page_items = data.get("items", [])
if not isinstance(page_items, list):
raise RuntimeError(f"Unexpected items list from {url}")
items.extend(page_items)
total_pages = data.get("totalPages", page)
if not isinstance(total_pages, int) or page >= total_pages:
break
page += 1
return items
def normalize_os_variants(install_methods_json: list[dict[str, Any]]) -> list[str]:
os_values: list[str] = []
for item in install_methods_json:
if not isinstance(item, dict):
continue
resources = item.get("resources", {})
if not isinstance(resources, dict):
continue
os_name = resources.get("os")
if isinstance(os_name, str) and os_name.strip():
normalized = os_name.strip().lower()
if normalized not in os_values:
os_values.append(normalized)
return os_values
def build_script_path(type_name: str, slug: str) -> str:
type_name = (type_name or "").strip().lower()
slug = (slug or "").strip()
if type_name == "turnkey":
return "turnkey/turnkey.sh"
prefix = TYPE_TO_PATH_PREFIX.get(type_name)
if not prefix or not slug:
return ""
return f"{prefix}/{slug}.sh"
def main() -> int: def main() -> int:
try: try:
directory = fetch_directory_json(API_URL) scripts = fetch_all_records(SCRIPT_COLLECTION_URL, expand="type,categories")
categories = fetch_all_records(CATEGORY_COLLECTION_URL)
except Exception as e: except Exception as e:
print(f"ERROR: No se pudo leer el índice de JSONs: {e}", file=sys.stderr) print(f"ERROR: Unable to fetch PocketBase data: {e}", file=sys.stderr)
return 1 return 1
cache: list[dict] = [] category_map: dict[str, dict[str, Any]] = {}
seen: set[tuple[str, str]] = set() # (slug, script) para evitar duplicados for category in categories:
category_id = category.get("id")
if isinstance(category_id, str) and category_id:
category_map[category_id] = category
total_items = len(directory) cache: list[dict[str, Any]] = []
processed = 0
kept = 0
for item in directory: print(f"Fetched {len(scripts)} scripts and {len(category_map)} categories")
url = item.get("download_url")
name_in_dir = item.get("name", "") for idx, raw in enumerate(scripts, start=1):
if not url or not url.endswith(".json"): if not isinstance(raw, dict):
continue continue
try:
raw = requests.get(url, timeout=30).json()
if not isinstance(raw, dict):
continue
except Exception:
print(f"❌ Error al obtener/parsing {name_in_dir}", file=sys.stderr)
continue
processed += 1
name = raw.get("name", "")
slug = raw.get("slug") slug = raw.get("slug")
type_ = raw.get("type", "") name = raw.get("name", "")
desc = raw.get("description", "") desc = raw.get("description", "")
categories = raw.get("categories", [])
notes = [n.get("text", "") for n in raw.get("notes", []) if isinstance(n, dict)]
# Credenciales (si existen, se copian tal cual) if not isinstance(slug, str) or not slug.strip():
credentials = raw.get("default_credentials", {})
cred_username = credentials.get("username") if isinstance(credentials, dict) else None
cred_password = credentials.get("password") if isinstance(credentials, dict) else None
add_credentials = any([
cred_username not in (None, ""),
cred_password not in (None, "")
])
install_methods = raw.get("install_methods", [])
if not isinstance(install_methods, list) or not install_methods:
# Sin install_methods válidos -> continuamos
continue continue
for im in install_methods: expand = raw.get("expand", {}) if isinstance(raw.get("expand"), dict) else {}
if not isinstance(im, dict): type_expanded = expand.get("type", {}) if isinstance(expand.get("type"), dict) else {}
continue type_name = type_expanded.get("type", "") if isinstance(type_expanded.get("type"), str) else ""
script = im.get("script", "")
if not script:
continue
# OS desde resources u heurística script_path = build_script_path(type_name, slug)
resources = im.get("resources", {}) if isinstance(im, dict) else {} if not script_path:
os_name = resources.get("os") if isinstance(resources, dict) else None print(f"[{idx:03d}] WARNING: Unable to build script path for slug={slug} type={type_name!r}", file=sys.stderr)
if not os_name: continue
os_name = guess_os_from_script_path(script)
if isinstance(os_name, str):
os_name = os_name.strip().lower()
full_script_url = f"{SCRIPT_BASE}/{script}" full_script_url = f"{SCRIPT_BASE}/{script_path}"
script_url_mirror = to_mirror_url(full_script_url) script_url_mirror = to_mirror_url(full_script_url)
key = (slug or "", script) install_methods_json = raw.get("install_methods_json", [])
if key in seen: if not isinstance(install_methods_json, list):
continue install_methods_json = []
seen.add(key)
entry = { notes_json = raw.get("notes_json", [])
"name": name, if not isinstance(notes_json, list):
"slug": slug, notes_json = []
"desc": desc,
"script": script, notes = [
"script_url": full_script_url, note.get("text", "")
"script_url_mirror": script_url_mirror, # nuevo for note in notes_json
"os": os_name, # nuevo if isinstance(note, dict) and isinstance(note.get("text"), str) and note.get("text", "").strip()
"categories": categories, ]
"notes": notes,
"type": type_, category_ids = raw.get("categories", [])
if not isinstance(category_ids, list):
category_ids = []
expanded_categories = expand.get("categories", []) if isinstance(expand.get("categories"), list) else []
category_names: list[str] = []
for cat in expanded_categories:
if isinstance(cat, dict):
cat_name = cat.get("name")
if isinstance(cat_name, str) and cat_name.strip():
category_names.append(cat_name.strip())
if not category_names:
for cat_id in category_ids:
cat = category_map.get(cat_id, {})
cat_name = cat.get("name")
if isinstance(cat_name, str) and cat_name.strip():
category_names.append(cat_name.strip())
# Shared fields across all install method entries
default_user = raw.get("default_user")
default_passwd = raw.get("default_passwd")
default_credentials: dict[str, str] | None = None
if (isinstance(default_user, str) and default_user.strip()) or (isinstance(default_passwd, str) and default_passwd.strip()):
default_credentials = {
"username": default_user if isinstance(default_user, str) else "",
"password": default_passwd if isinstance(default_passwd, str) else "",
} }
if add_credentials:
entry["default_credentials"] = {
"username": cred_username,
"password": cred_password,
}
base_entry: dict[str, Any] = {
"name": name,
"slug": slug,
"desc": desc,
"script": script_path,
"script_url": full_script_url,
"script_url_mirror": script_url_mirror,
"type": type_name,
"type_id": raw.get("type", ""),
"categories": category_ids,
"category_names": category_names,
"notes": notes,
"port": raw.get("port", 0),
"website": raw.get("website", ""),
"documentation": raw.get("documentation", ""),
"logo": raw.get("logo", ""),
"updateable": bool(raw.get("updateable", False)),
"privileged": bool(raw.get("privileged", False)),
"has_arm": bool(raw.get("has_arm", False)),
"is_dev": bool(raw.get("is_dev", False)),
"execute_in": raw.get("execute_in", []),
"config_path": raw.get("config_path", ""),
}
if default_credentials:
base_entry["default_credentials"] = default_credentials
# Emit one entry per install method so the menu shell can offer an
# explicit OS choice. When there is only one method (or none), a
# single entry is emitted with os="" (script decides at runtime).
os_variants = normalize_os_variants(install_methods_json)
if len(os_variants) > 1:
for os_name in os_variants:
entry = {**base_entry, "os": os_name}
cache.append(entry)
print(f"[{len(cache):03d}] {slug:<24}{script_path:<28} type={type_name:<7} os={os_name}")
else:
os_name = os_variants[0] if os_variants else ""
entry = {**base_entry, "os": os_name}
cache.append(entry) cache.append(entry)
kept += 1 print(f"[{len(cache):03d}] {slug:<24}{script_path:<28} type={type_name:<7} os={os_name or 'n/a'}")
# Progreso ligero
print(f"[{kept:03d}] {slug or name:<24}{script:<28} os={os_name or 'n/a'} src={'GH+MR' if script_url_mirror else 'GH'}")
# Orden estable para commits reproducibles
cache.sort(key=lambda x: (x.get("slug") or "", x.get("script") or "")) cache.sort(key=lambda x: (x.get("slug") or "", x.get("script") or ""))
with OUTPUT_FILE.open("w", encoding="utf-8") as f: with OUTPUT_FILE.open("w", encoding="utf-8") as f:
json.dump(cache, f, ensure_ascii=False, indent=2) json.dump(cache, f, ensure_ascii=False, indent=2)
print(f"\n✅ helpers_cache.json → {OUTPUT_FILE}") print(f"\n✅ helpers_cache.json → {OUTPUT_FILE}")
print(f" Total JSON en índice: {total_items}") print(f" Guardados: {len(cache)}")
print(f" Procesados: {processed} | Guardados: {kept} | Únicos (slug,script): {len(seen)}")
return 0 return 0

View File

@@ -0,0 +1,178 @@
#!/usr/bin/env python3
import json
import re
import sys
from pathlib import Path
import requests
# ---------- Config ----------
# API_URL = "https://api.github.com/repos/community-scripts/ProxmoxVE/contents/frontend/public/json"
API_URL = "https://api.github.com/repos/community-scripts/ProxmoxVE-Frontend-Archive/contents/public/json"
SCRIPT_BASE = "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
# Escribimos siempre en <raiz_repo>/json/helpers_cache.json, independientemente del cwd
REPO_ROOT = Path(__file__).resolve().parents[2]
OUTPUT_FILE = REPO_ROOT / "json" / "helpers_cache.json"
OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True)
# ----------------------------
def to_mirror_url(raw_url: str) -> str:
"""
Convierte una URL raw de GitHub al raw del mirror.
GH : https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/docker.sh
MIR: https://git.community-scripts.org/community-scripts/ProxmoxVE/raw/branch/main/ct/docker.sh
"""
m = re.match(r"^https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/([^/]+)/(.+)$", raw_url or "")
if not m:
return ""
org, repo, branch, path = m.groups()
if org.lower() != "community-scripts" or repo != "ProxmoxVE":
return ""
return f"https://git.community-scripts.org/community-scripts/ProxmoxVE/raw/branch/{branch}/{path}"
def guess_os_from_script_path(script_path: str) -> str | None:
"""
Heurística suave cuando el JSON no publica resources.os:
- tools/pve/* -> proxmox
- ct/alpine-* -> alpine
- tools/addon/* -> generic (suele ejecutarse sobre LXC existente)
- ct/* -> debian (por defecto para CTs)
"""
if not script_path:
return None
if script_path.startswith("tools/pve/") or script_path == "tools/pve/host-backup.sh" or script_path.startswith("vm/"):
return "proxmox"
if "/alpine-" in script_path or script_path.startswith("ct/alpine-"):
return "alpine"
if script_path.startswith("tools/addon/"):
return "generic"
if script_path.startswith("ct/"):
return "debian"
return None
def fetch_directory_json(api_url: str) -> list[dict]:
r = requests.get(api_url, timeout=30)
r.raise_for_status()
data = r.json()
if not isinstance(data, list):
raise RuntimeError("GitHub API no devolvió una lista.")
return data
def main() -> int:
try:
directory = fetch_directory_json(API_URL)
except Exception as e:
print(f"ERROR: No se pudo leer el índice de JSONs: {e}", file=sys.stderr)
return 1
cache: list[dict] = []
seen: set[tuple[str, str]] = set() # (slug, script) para evitar duplicados
total_items = len(directory)
processed = 0
kept = 0
for item in directory:
url = item.get("download_url")
name_in_dir = item.get("name", "")
if not url or not url.endswith(".json"):
continue
try:
raw = requests.get(url, timeout=30).json()
if not isinstance(raw, dict):
continue
except Exception:
print(f"❌ Error al obtener/parsing {name_in_dir}", file=sys.stderr)
continue
processed += 1
name = raw.get("name", "")
slug = raw.get("slug")
type_ = raw.get("type", "")
desc = raw.get("description", "")
categories = raw.get("categories", [])
notes = [n.get("text", "") for n in raw.get("notes", []) if isinstance(n, dict)]
# Credenciales (si existen, se copian tal cual)
credentials = raw.get("default_credentials", {})
cred_username = credentials.get("username") if isinstance(credentials, dict) else None
cred_password = credentials.get("password") if isinstance(credentials, dict) else None
add_credentials = any([
cred_username not in (None, ""),
cred_password not in (None, "")
])
install_methods = raw.get("install_methods", [])
if not isinstance(install_methods, list) or not install_methods:
# Sin install_methods válidos -> continuamos
continue
for im in install_methods:
if not isinstance(im, dict):
continue
script = im.get("script", "")
if not script:
continue
# OS desde resources u heurística
resources = im.get("resources", {}) if isinstance(im, dict) else {}
os_name = resources.get("os") if isinstance(resources, dict) else None
if not os_name:
os_name = guess_os_from_script_path(script)
if isinstance(os_name, str):
os_name = os_name.strip().lower()
full_script_url = f"{SCRIPT_BASE}/{script}"
script_url_mirror = to_mirror_url(full_script_url)
key = (slug or "", script)
if key in seen:
continue
seen.add(key)
entry = {
"name": name,
"slug": slug,
"desc": desc,
"script": script,
"script_url": full_script_url,
"script_url_mirror": script_url_mirror, # nuevo
"os": os_name, # nuevo
"categories": categories,
"notes": notes,
"type": type_,
}
if add_credentials:
entry["default_credentials"] = {
"username": cred_username,
"password": cred_password,
}
cache.append(entry)
kept += 1
# Progreso ligero
print(f"[{kept:03d}] {slug or name:<24}{script:<28} os={os_name or 'n/a'} src={'GH+MR' if script_url_mirror else 'GH'}")
# Orden estable para commits reproducibles
cache.sort(key=lambda x: (x.get("slug") or "", x.get("script") or ""))
with OUTPUT_FILE.open("w", encoding="utf-8") as f:
json.dump(cache, f, ensure_ascii=False, indent=2)
print(f"\n✅ helpers_cache.json → {OUTPUT_FILE}")
print(f" Total JSON en índice: {total_items}")
print(f" Procesados: {processed} | Guardados: {kept} | Únicos (slug,script): {len(seen)}")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,24 +1,29 @@
name: Build ProxMenux Monitor AppImage name: Build AppImage Release
on: on:
workflow_dispatch:
workflow_dispatch:
permissions: permissions:
contents: write contents: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs: jobs:
build: build:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout code - name: Checkout main
uses: actions/checkout@v4 uses: actions/checkout@v6
with:
ref: main
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version: '20' node-version: '22'
- name: Install dependencies - name: Install dependencies
working-directory: AppImage working-directory: AppImage
@@ -45,13 +50,6 @@ jobs:
id: version id: version
working-directory: AppImage working-directory: AppImage
run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Upload AppImage artifact
uses: actions/upload-artifact@v4
with:
name: ProxMenux-${{ steps.version.outputs.VERSION }}-AppImage
path: AppImage/dist/*.AppImage
retention-days: 30
- name: Generate SHA256 checksum - name: Generate SHA256 checksum
run: | run: |
@@ -60,22 +58,26 @@ jobs:
echo "Generated SHA256:" echo "Generated SHA256:"
cat ProxMenux-Monitor.AppImage.sha256 cat ProxMenux-Monitor.AppImage.sha256
- name: Upload AppImage and checksum to /AppImage folder in main - name: Upload AppImage artifact
uses: actions/upload-artifact@v6
with:
name: ProxMenux-${{ steps.version.outputs.VERSION }}-AppImage
path: |
AppImage/dist/*.AppImage
AppImage/dist/*.sha256
retention-days: 30
- name: Commit AppImage to main
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
git config --global user.name "github-actions[bot]" git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com" git config --global user.email "github-actions[bot]@users.noreply.github.com"
git fetch origin main
git checkout main
rm -f AppImage/*.AppImage AppImage/*.sha256 || true rm -f AppImage/*.AppImage AppImage/*.sha256 || true
# Copy new files
cp AppImage/dist/*.AppImage AppImage/ cp AppImage/dist/*.AppImage AppImage/
cp AppImage/dist/ProxMenux-Monitor.AppImage.sha256 AppImage/ cp AppImage/dist/ProxMenux-Monitor.AppImage.sha256 AppImage/
git add AppImage/*.AppImage AppImage/*.sha256 git add AppImage/*.AppImage AppImage/*.sha256
git commit -m "Update AppImage build ($(date +'%Y-%m-%d %H:%M:%S'))" || echo "No changes to commit" git commit -m "Update AppImage release build ($(date +'%Y-%m-%d %H:%M:%S'))" || echo "No changes to commit"
git push origin main git push origin main

BIN
AppImage/ProxMenux-1.0.2.AppImage Executable file

Binary file not shown.

View File

@@ -1,3 +1,38 @@
## 2026-03-14
### New version v1.1.9 — *Helper Scripts Catalog Rebuilt*
### Changed
- **Helper Scripts Menu — Full Catalog Rebuild**
The Helper Scripts catalog has been completely rebuilt to adapt to the new data architecture of the [Community Scripts](https://community-scripts.github.io/ProxmoxVE/) project.
The previous implementation relied on a `metadata.json` file that no longer exists in the upstream repository. The catalog now connects directly to the **PocketBase API** (`db.community-scripts.org`), which is the new official data source for the project.
A new GitHub Actions workflow generates a local `helpers_cache.json` index that replaces the old metadata dependency. This new cache is richer, more structured, and includes:
- Script type, slug, description, notes, and default credentials
- OS variants per script (e.g. Debian, Alpine) — each shown as a separate selectable option in the menu
- Direct GitHub URL and **Mirror URL** (`git.community-scripts.org`) for every script
- Category names embedded directly in the cache — no external requests needed to build the menu
- Additional metadata: default port, website, logo, update support, ARM availability
Scripts that support multiple OS variants (e.g. Docker with Alpine and Debian) now correctly show **one entry per OS**, each with its own GitHub and Mirror download option — restoring the behavior that existed before the upstream migration.
---
### 🎖 Special Acknowledgment
This update would not have been possible without the openness and collaboration of the **Community Scripts** maintainers.
When the upstream metadata structure changed and broke the ProxMenux catalog, the maintainers responded quickly, explained the new architecture in detail, and provided all the information needed to rebuild the integration cleanly.
Special thanks to:
- **MickLeskCanbiZ ([@MickLesk](https://github.com/MickLesk))** — for documenting the new script path structure by type and slug, and for the clear and direct technical guidance.
- **Michel Roegl-Brunner ([@michelroegl-brunner](https://github.com/michelroegl-brunner))** — for explaining the new PocketBase collections structure (`script_scripts`, `script_categories`).
The Helper Scripts project is an extraordinary resource for the Proxmox community. The scripts belong entirely to their authors and maintainers — ProxMenux simply offers a guided way to discover and launch them. All credit goes to the community behind [community-scripts/ProxmoxVE](https://github.com/community-scripts/ProxmoxVE).
## 2025-09-18 ## 2025-09-18
### New version v1.1.8 — *ProxMenux Offline Mode* ### New version v1.1.8 — *ProxMenux Offline Mode*

View File

@@ -171,6 +171,7 @@ If you find ProxMenux useful, consider giving it a ⭐ on GitHub — it helps ot
[![Star History Chart](https://api.star-history.com/svg?repos=MacRimi/ProxMenux&type=Date)](https://www.star-history.com/#MacRimi/ProxMenux&Date) [![Star History Chart](https://api.star-history.com/svg?repos=MacRimi/ProxMenux&type=Date)](https://www.star-history.com/#MacRimi/ProxMenux&Date)
<div style="display: flex; justify-content: center; align-items: center;"> <div style="display: flex; justify-content: center; align-items: center;">
<a href="https://ko-fi.com/G2G313ECAN" target="_blank" style="display: flex; align-items: center; text-decoration: none;"> <a href="https://ko-fi.com/G2G313ECAN" target="_blank" style="display: flex; align-items: center; text-decoration: none;">
<img src="https://raw.githubusercontent.com/MacRimi/HWEncoderX/main/images/kofi.png" alt="Support me on Ko-fi" style="width:140px; margin-right:40px;"/> <img src="https://raw.githubusercontent.com/MacRimi/HWEncoderX/main/images/kofi.png" alt="Support me on Ko-fi" style="width:140px; margin-right:40px;"/>

File diff suppressed because it is too large Load Diff

8417
json/helpers_cache_back.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -173,28 +173,13 @@ run_script_by_slug() {
credentials=$(format_credentials "$first") credentials=$(format_credentials "$first")
# Build info message # Build info message
local msg="\Zb\Z4$(translate "Description"):\Zn\n$desc" local msg="\Zb\Z4$(translate "Description"):\Zn\n$desc"
if [[ -n "$notes" ]]; then [[ -n "$notes_dialog" ]] && msg+="\n\n\Zb\Z4$(translate "Notes"):\Zn\n$notes_dialog"
local notes_short=""
local char_count=0
local max_chars=400
while IFS= read -r line; do
[[ -z "$line" ]] && continue
char_count=$(( char_count + ${#line} ))
if [[ $char_count -lt $max_chars ]]; then
notes_short+="$line\n"
else
notes_short+="...\n"
break
fi
done <<< "$notes"
msg+="\n\n\Zb\Z4$(translate "Notes"):\Zn\n$notes_short"
fi
[[ -n "$credentials" ]] && msg+="\n\n\Zb\Z4$(translate "Default Credentials"):\Zn\n$credentials" [[ -n "$credentials" ]] && msg+="\n\n\Zb\Z4$(translate "Default Credentials"):\Zn\n$credentials"
[[ "$port" -gt 0 ]] && msg+="\n\n\Zb\Z4$(translate "Default Port"):\Zn $port" [[ "$port" -gt 0 ]] && msg+="\n\n\Zb\Z4$(translate "Default Port"):\Zn $port"
[[ -n "$website" ]] && msg+="\n\Zb\Z4$(translate "Website"):\Zn $website" [[ -n "$website" ]] && msg+="\n\Zb\Z4$(translate "Website"):\Zn $website"
msg+="\n\n$(translate "Choose how to run the script:")" msg+="\n\n$(translate "Choose how to run the script:"):"
# Build menu: one or two entries per script_info (GH + optional Mirror) # Build menu: one or two entries per script_info (GH + optional Mirror)
declare -a MENU_OPTS=() declare -a MENU_OPTS=()
@@ -398,7 +383,7 @@ while true; do
SELECTED_IDX=$(dialog --backtitle "ProxMenux" \ SELECTED_IDX=$(dialog --backtitle "ProxMenux" \
--title "Proxmox VE Helper-Scripts" \ --title "Proxmox VE Helper-Scripts" \
--menu "$(translate "Select a category or search for scripts:"):" \ --menu "$(translate "Select a category or search for scripts:"):" \
22 75 15 "${MENU_ITEMS[@]}" 3>&1 1>&2 2>&3) || { 20 70 14 "${MENU_ITEMS[@]}" 3>&1 1>&2 2>&3) || {
dialog --clear --title "ProxMenux" \ dialog --clear --title "ProxMenux" \
--msgbox "\n\n$(translate "Visit the website to discover more scripts, stay updated with the latest updates, and support the project:")\n\nhttps://community-scripts.github.io/ProxmoxVE" 15 70 --msgbox "\n\n$(translate "Visit the website to discover more scripts, stay updated with the latest updates, and support the project:")\n\nhttps://community-scripts.github.io/ProxmoxVE" 15 70
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh" exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
@@ -440,7 +425,7 @@ while true; do
SCRIPT_INDEX=$(dialog --colors --backtitle "ProxMenux" \ SCRIPT_INDEX=$(dialog --colors --backtitle "ProxMenux" \
--title "$(translate "Scripts in") ${CATEGORY_NAMES[$SELECTED]}" \ --title "$(translate "Scripts in") ${CATEGORY_NAMES[$SELECTED]}" \
--menu "$(translate "Choose a script to execute:"):" \ --menu "$(translate "Choose a script to execute:"):" \
22 75 15 "${SCRIPTS[@]}" 3>&1 1>&2 2>&3) || break 20 70 14 "${SCRIPTS[@]}" 3>&1 1>&2 2>&3) || break
SCRIPT_SELECTED="${INDEX_TO_SLUG[$SCRIPT_INDEX]}" SCRIPT_SELECTED="${INDEX_TO_SLUG[$SCRIPT_INDEX]}"
run_script_by_slug "$SCRIPT_SELECTED" run_script_by_slug "$SCRIPT_SELECTED"

View File

@@ -1 +1 @@
1.1.9 1.1.9