refactor: move json/ to top-level, remove frontend/, update workflows
- Move frontend/public/json/ to json/ (top-level) - Update all workflow path references to json/ - Delete frontend-cicd.yml workflow - Delete frontend/ directory entirely - Clean up .gitattributes (remove frontend entries)
This commit is contained in:
5
frontend/.eslintrc.json
generated
5
frontend/.eslintrc.json
generated
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint"]
|
||||
}
|
||||
39
frontend/.gitignore
vendored
39
frontend/.gitignore
vendored
@@ -1,39 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# wrangler
|
||||
.worker-next
|
||||
.wrangler
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
out
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# # local env files
|
||||
# .env*.local
|
||||
# .env
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -1,5 +0,0 @@
|
||||
dist
|
||||
node_modules
|
||||
.next
|
||||
build
|
||||
.contentlayer
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"plugins": ["prettier-plugin-tailwindcss", "prettier-plugin-organize-imports"]
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Bram Suurd
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
17
frontend/components.json
generated
17
frontend/components.json
generated
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "@/styles/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
webpack: (config) => {
|
||||
config.resolve.alias.canvas = false;
|
||||
|
||||
return config;
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "**",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
env: {
|
||||
BASE_PATH: "ProxmoxVED",
|
||||
},
|
||||
|
||||
output: "export",
|
||||
basePath: `/ProxmoxVED`,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
10794
frontend/package-lock.json
generated
10794
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
93
frontend/package.json
generated
93
frontend/package.json
generated
@@ -1,93 +0,0 @@
|
||||
{
|
||||
"name": "proxmox-helper-scripts-website",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"author": {
|
||||
"name": "Bram Suurd",
|
||||
"url": "https://github.com/community-scripts"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test": "vitest",
|
||||
"deploy": "next build && touch out/.nojekyll && git add out/ && git commit -m \"Deploy\" && git subtree push --prefix out origin gh-pages",
|
||||
"format:write": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache",
|
||||
"format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-accordion": "^1.2.3",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.5",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@tanstack/react-query": "^5.71.1",
|
||||
"chart.js": "^4.4.8",
|
||||
"chartjs-plugin-datalabels": "^2.2.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"framer-motion": "^11.18.2",
|
||||
"fuse.js": "^7.1.0",
|
||||
"lucide-react": "^0.453.0",
|
||||
"mini-svg-data-uri": "^1.4.4",
|
||||
"next": "15.5.7",
|
||||
"next-themes": "^0.3.0",
|
||||
"nuqs": "^2.4.1",
|
||||
"pocketbase": "^0.21.5",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"react": "19.0.0",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-code-blocks": "^0.1.6",
|
||||
"react-datepicker": "^7.6.0",
|
||||
"react-day-picker": "8.10.1",
|
||||
"react-dom": "19.0.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-simple-typewriter": "^5.0.1",
|
||||
"sharp": "^0.33.5",
|
||||
"simple-icons": "^13.21.0",
|
||||
"sonner": "^1.7.4",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/eslint-plugin-query": "^5.68.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@types/node": "^22.13.16",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.29.0",
|
||||
"@typescript-eslint/parser": "^8.29.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.23.0",
|
||||
"eslint-config-next": "15.0.2",
|
||||
"jsdom": "^25.0.1",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tailwindcss-animated": "^1.1.2",
|
||||
"typescript": "^5.8.2",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.1.1"
|
||||
},
|
||||
"overrides": {
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 76 KiB |
44
frontend/public/json/affine.json
generated
44
frontend/public/json/affine.json
generated
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"name": "AFFiNE",
|
||||
"slug": "affine",
|
||||
"categories": [
|
||||
12
|
||||
],
|
||||
"date_created": "2026-01-18",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 3010,
|
||||
"documentation": "https://affine.pro/docs",
|
||||
"website": "https://affine.pro/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/affine.webp",
|
||||
"config_path": "/opt/affine/.env",
|
||||
"description": "Open-source, privacy-first workspace for notes, docs, and knowledge management with offline-first design and end-to-end encryption.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/affine.sh",
|
||||
"resources": {
|
||||
"cpu": 4,
|
||||
"ram": 12288,
|
||||
"hdd": 20,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Initial build takes 20-30 minutes due to native module compilation.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Requires at least 8GB RAM for building and 4GB for runtime.",
|
||||
"type": "warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
40
frontend/public/json/allstarlink.json
generated
40
frontend/public/json/allstarlink.json
generated
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"name": "AllStarLink",
|
||||
"slug": "allstarlink-vm",
|
||||
"categories": [
|
||||
24
|
||||
],
|
||||
"date_created": "2025-05-05",
|
||||
"type": "vm",
|
||||
"updateable": false,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://allstarlink.github.io/",
|
||||
"website": "https://www.allstarlink.org/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/allstar-link.webp",
|
||||
"config_path": "",
|
||||
"description": "AllStarLink is a network of Amateur Radio repeaters, remote base stations and hot spots accessible to each other via Voice over Internet Protocol.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "vm/allstarlink-vm.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 8,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Options to Install Allmon3",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
44
frontend/public/json/almalinux-10-vm.json
generated
44
frontend/public/json/almalinux-10-vm.json
generated
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"name": "AlmaLinux 10",
|
||||
"slug": "almalinux-10-vm",
|
||||
"categories": [
|
||||
2
|
||||
],
|
||||
"date_created": "2026-02-19",
|
||||
"type": "vm",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://wiki.almalinux.org/",
|
||||
"website": "https://almalinux.org/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/almalinux.webp",
|
||||
"config_path": "",
|
||||
"description": "AlmaLinux 10 (Heliotrope Lion) is a community-driven, RHEL-compatible enterprise Linux distribution. It provides a stable, production-ready platform with long-term support, making it ideal for servers, workloads, and cloud deployments.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "vm/almalinux-10-vm.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 10,
|
||||
"os": null,
|
||||
"version": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Cloud-Init is enabled by default. Configure your SSH key and credentials in the Proxmox Cloud-Init tab before starting the VM.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "If you use Cloud-init, checkout cloud-init discussion: `https://github.com/community-scripts/ProxmoxVE/discussions/272`",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
44
frontend/public/json/alpine-borgbackup-server.json
generated
44
frontend/public/json/alpine-borgbackup-server.json
generated
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"name": "Alpine-BorgBackup-Server",
|
||||
"slug": "alpine-borgbackup-server",
|
||||
"categories": [
|
||||
7
|
||||
],
|
||||
"date_created": "2026-02-16",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://www.borgbackup.org/",
|
||||
"website": "https://www.borgbackup.org/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/borg.webp",
|
||||
"config_path": "",
|
||||
"description": "Alpine-BorgBackup-Server is a lightweight containerized backup server based on Alpine Linux and BorgBackup. It provides a secure and efficient way to manage your backups with minimal resource usage, making it ideal for Proxmox VE environments.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/alpine-borgbackup-server.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 1024,
|
||||
"hdd": 20,
|
||||
"os": "alpine",
|
||||
"version": "3.23"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "After installation, use the 'update' option in the script to configure SSH access and/or password authentication.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Refer to the official BorgBackup documentation for setup and usage instructions.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
40
frontend/public/json/alpine-ntfy.json
generated
40
frontend/public/json/alpine-ntfy.json
generated
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"name": "Alpine-ntfy",
|
||||
"slug": "alpine-ntfy",
|
||||
"categories": [
|
||||
18
|
||||
],
|
||||
"date_created": "2025-06-10",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 80,
|
||||
"documentation": "https://docs.ntfy.sh/",
|
||||
"website": "https://ntfy.sh/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/ntfy.webp",
|
||||
"config_path": "/etc/ntfy/server.yml",
|
||||
"description": "ntfy is a simple HTTP-based pub-sub notification service that lets you send push notifications to your phone or desktop via scripts, without signup or cost.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/alpine-ntfy.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 256,
|
||||
"hdd": 2,
|
||||
"os": "Alpine",
|
||||
"version": "3.22"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Alpine-based LXC using apk package manager.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
48
frontend/public/json/anytype.json
generated
48
frontend/public/json/anytype.json
generated
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"name": "Anytype",
|
||||
"slug": "anytype",
|
||||
"categories": [
|
||||
12
|
||||
],
|
||||
"date_created": "2026-01-29",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 33010,
|
||||
"documentation": "https://doc.anytype.io/",
|
||||
"website": "https://anytype.io/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/anytype.webp",
|
||||
"config_path": "/opt/anytype/.env",
|
||||
"description": "Anytype is a local-first, privacy-focused alternative to Notion. This script deploys the any-sync-bundle which provides a self-hosted sync server for Anytype clients with external MongoDB and Redis Stack.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/anytype-server.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 4096,
|
||||
"hdd": 16,
|
||||
"os": "Ubuntu",
|
||||
"version": "24.04"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "After installation, import /opt/anytype/data/client-config.yml into your Anytype apps.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "This uses the community any-sync-bundle by grishy, not the official Anytype deployment.",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "Firewall: Open TCP 33010 (DRPC) and UDP 33020 (QUIC) for external access.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
42
frontend/public/json/arm.json
generated
42
frontend/public/json/arm.json
generated
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"name": "ARM",
|
||||
"slug": "arm",
|
||||
"categories": [13],
|
||||
"date_created": "2026-03-06",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": true,
|
||||
"interface_port": 8080,
|
||||
"documentation": "https://github.com/automatic-ripping-machine/automatic-ripping-machine/wiki",
|
||||
"website": "https://github.com/automatic-ripping-machine/automatic-ripping-machine",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/arm.webp",
|
||||
"config_path": "/opt/arm/arm.yaml",
|
||||
"description": "Automatic Ripping Machine (ARM) automatically detects and rips Blu-rays, DVDs, and CDs. It identifies disc type, retrieves metadata, and handles transcoding with MakeMKV and HandBrake.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/arm.sh",
|
||||
"resources": {
|
||||
"cpu": 4,
|
||||
"ram": 4096,
|
||||
"hdd": 16,
|
||||
"os": "Debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "admin",
|
||||
"password": "password"
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Optical drive passthrough to the LXC container is required. Configure device mappings in the Proxmox container settings.",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "Navigate to http://IP:8080/setup on first launch to complete initial setup. Default credentials: admin / password.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
42
frontend/public/json/authentik.json
generated
42
frontend/public/json/authentik.json
generated
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"name": "authentik",
|
||||
"slug": "authentik",
|
||||
"categories": [6],
|
||||
"date_created": "2026-02-21",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 9000,
|
||||
"documentation": "https://docs.goauthentik.io/",
|
||||
"website": "https://goauthentik.io/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/authentik.webp",
|
||||
"config_path": "/etc/authentik/config.yml",
|
||||
"description": "authentik is an IdP (Identity Provider) and SSO (Single Sign On) platform.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/authentik.sh",
|
||||
"resources": {
|
||||
"cpu": 4,
|
||||
"ram": 4096,
|
||||
"hdd": 10,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "You will get a Not Found error if initial setup URL doesn't include the trailing forward slash /. Make sure you use the complete url (http://<your server's IP or hostname>:9000/if/flow/initial-setup/) including the trailing forward slash.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "If you want automatic GeoIP updates, create a free account at https://www.maxmind.com/en/geolite2/signup, then edit the /usr/local/etc/GeoIP.conf file with your credentials and remove the # in front of the geoipupdate line in the crontab.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
35
frontend/public/json/bak/almalinux.json
generated
35
frontend/public/json/bak/almalinux.json
generated
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "AlmaLinux",
|
||||
"slug": "almalinux",
|
||||
"categories": [
|
||||
2
|
||||
],
|
||||
"date_created": "2025-12-04",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://wiki.almalinux.org/",
|
||||
"website": "https://almalinux.org/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/almalinux.webp",
|
||||
"config_path": "",
|
||||
"description": "AlmaLinux is an open-source, community-driven Linux distribution, 1:1 binary compatible with RHEL. A free alternative to CentOS.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/almalinux.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 4,
|
||||
"os": "almalinux",
|
||||
"version": "10"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "root",
|
||||
"password": null
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
35
frontend/public/json/bak/alpine.json
generated
35
frontend/public/json/bak/alpine.json
generated
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "Alpine Linux",
|
||||
"slug": "alpine",
|
||||
"categories": [
|
||||
2
|
||||
],
|
||||
"date_created": "2025-12-04",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://wiki.alpinelinux.org/",
|
||||
"website": "https://alpinelinux.org/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/alpine-linux.webp",
|
||||
"config_path": "",
|
||||
"description": "Alpine Linux is a security-oriented, lightweight Linux distribution based on musl libc and busybox. Perfect for containers due to its minimal footprint.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/alpine.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 1,
|
||||
"os": "alpine",
|
||||
"version": "3.22"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "root",
|
||||
"password": null
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
40
frontend/public/json/bak/centos.json
generated
40
frontend/public/json/bak/centos.json
generated
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"name": "CentOS Stream",
|
||||
"slug": "centos",
|
||||
"categories": [
|
||||
2
|
||||
],
|
||||
"date_created": "2025-12-04",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://docs.centos.org/",
|
||||
"website": "https://www.centos.org/centos-stream/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/centos.webp",
|
||||
"config_path": "",
|
||||
"description": "CentOS Stream is a continuously delivered Linux distribution that tracks just ahead of RHEL development. The upstream for the next RHEL release.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/centos.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 4,
|
||||
"os": "centos",
|
||||
"version": "9"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "root",
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "CentOS Stream is the upstream development platform for RHEL.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
35
frontend/public/json/bak/debian.json
generated
35
frontend/public/json/bak/debian.json
generated
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "Debian",
|
||||
"slug": "debian",
|
||||
"categories": [
|
||||
2
|
||||
],
|
||||
"date_created": "2025-12-04",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://www.debian.org/doc/",
|
||||
"website": "https://www.debian.org/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/debian.webp",
|
||||
"config_path": "",
|
||||
"description": "Debian is a free operating system (OS) that comes with over 59000 packages. It's known for its stability, security, and extensive software repository.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/debian.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 4,
|
||||
"os": "debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "root",
|
||||
"password": null
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
40
frontend/public/json/bak/devuan.json
generated
40
frontend/public/json/bak/devuan.json
generated
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"name": "Devuan",
|
||||
"slug": "devuan",
|
||||
"categories": [
|
||||
2
|
||||
],
|
||||
"date_created": "2025-12-04",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://www.devuan.org/os/documentation",
|
||||
"website": "https://www.devuan.org/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/devuan.webp",
|
||||
"config_path": "",
|
||||
"description": "Devuan is a fork of Debian without systemd. It uses sysvinit and other alternative init systems, providing freedom of choice.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/devuan.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 4,
|
||||
"os": "devuan",
|
||||
"version": "5.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "root",
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Devuan uses sysvinit instead of systemd.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
35
frontend/public/json/bak/fedora.json
generated
35
frontend/public/json/bak/fedora.json
generated
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "Fedora",
|
||||
"slug": "fedora",
|
||||
"categories": [
|
||||
2
|
||||
],
|
||||
"date_created": "2025-12-04",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://docs.fedoraproject.org/",
|
||||
"website": "https://fedoraproject.org/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/fedora.webp",
|
||||
"config_path": "",
|
||||
"description": "Fedora is a cutting-edge Linux distribution sponsored by Red Hat. It features the latest software and technologies while maintaining stability.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/fedora.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 4,
|
||||
"os": "fedora",
|
||||
"version": "42"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "root",
|
||||
"password": null
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
40
frontend/public/json/bak/gentoo.json
generated
40
frontend/public/json/bak/gentoo.json
generated
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"name": "Gentoo",
|
||||
"slug": "gentoo",
|
||||
"categories": [
|
||||
2
|
||||
],
|
||||
"date_created": "2025-12-04",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://wiki.gentoo.org/",
|
||||
"website": "https://www.gentoo.org/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/gentoo.webp",
|
||||
"config_path": "",
|
||||
"description": "Gentoo is a highly customizable source-based Linux distribution. Users compile software optimized for their specific hardware.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/gentoo.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 1024,
|
||||
"hdd": 8,
|
||||
"os": "gentoo",
|
||||
"version": "current"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "root",
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Gentoo compiles packages from source. Initial setup may take time.",
|
||||
"type": "warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
35
frontend/public/json/bak/openeuler.json
generated
35
frontend/public/json/bak/openeuler.json
generated
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "openEuler",
|
||||
"slug": "openeuler",
|
||||
"categories": [
|
||||
2
|
||||
],
|
||||
"date_created": "2025-12-04",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://docs.openeuler.org/",
|
||||
"website": "https://www.openeuler.org/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/openeuler.webp",
|
||||
"config_path": "",
|
||||
"description": "openEuler is an open-source Linux distribution from Huawei. It's designed for servers and cloud computing with enterprise-grade features.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/openeuler.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 4,
|
||||
"os": "openeuler",
|
||||
"version": "25.03"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "root",
|
||||
"password": null
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
35
frontend/public/json/bak/opensuse.json
generated
35
frontend/public/json/bak/opensuse.json
generated
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "openSUSE",
|
||||
"slug": "opensuse",
|
||||
"categories": [
|
||||
2
|
||||
],
|
||||
"date_created": "2025-12-04",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://doc.opensuse.org/",
|
||||
"website": "https://www.opensuse.org/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/opensuse.webp",
|
||||
"config_path": "",
|
||||
"description": "openSUSE is a stable, easy-to-use, and complete Linux distribution. Known for its YaST configuration tool and zypper package manager.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/opensuse.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 4,
|
||||
"os": "opensuse",
|
||||
"version": "15.6"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "root",
|
||||
"password": null
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
35
frontend/public/json/bak/rockylinux.json
generated
35
frontend/public/json/bak/rockylinux.json
generated
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "Rocky Linux",
|
||||
"slug": "rockylinux",
|
||||
"categories": [
|
||||
2
|
||||
],
|
||||
"date_created": "2025-12-04",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://docs.rockylinux.org/",
|
||||
"website": "https://rockylinux.org/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/rocky-linux.webp",
|
||||
"config_path": "",
|
||||
"description": "Rocky Linux is an enterprise-grade Linux distribution designed to be 100% bug-for-bug compatible with Red Hat Enterprise Linux (RHEL).",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/rockylinux.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 4,
|
||||
"os": "rockylinux",
|
||||
"version": "10"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "root",
|
||||
"password": null
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
35
frontend/public/json/bak/ubuntu.json
generated
35
frontend/public/json/bak/ubuntu.json
generated
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "Ubuntu",
|
||||
"slug": "ubuntu",
|
||||
"categories": [
|
||||
2
|
||||
],
|
||||
"date_created": "2025-12-04",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://help.ubuntu.com/",
|
||||
"website": "https://ubuntu.com/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/ubuntu.webp",
|
||||
"config_path": "",
|
||||
"description": "Ubuntu is a popular Linux distribution based on Debian. Known for its ease of use and regular release cycle with LTS versions.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/ubuntu.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 4,
|
||||
"os": "ubuntu",
|
||||
"version": "24.04"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "root",
|
||||
"password": null
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
35
frontend/public/json/caddymanager.json
generated
35
frontend/public/json/caddymanager.json
generated
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "CaddyManager",
|
||||
"slug": "caddymanager",
|
||||
"categories": [
|
||||
21
|
||||
],
|
||||
"date_created": "2026-02-11",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 80,
|
||||
"documentation": "https://caddymanager.online/#/docs",
|
||||
"config_path": "/opt/caddymanager/caddymanager.env",
|
||||
"website": "https://caddymanager.online",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/caddymanager.webp",
|
||||
"description": "Caddy Manager is a free, open source tool to easily manage your Caddy web servers with a modern web interace. Simplify your workflow and focus on what matters.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/caddymanager.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 1024,
|
||||
"hdd": 4,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "admin",
|
||||
"password": "caddyrocks"
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
52
frontend/public/json/dependency-check.json
generated
52
frontend/public/json/dependency-check.json
generated
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"name": "PVE Startup Dependency Check",
|
||||
"slug": "dependency-check",
|
||||
"categories": [
|
||||
1
|
||||
],
|
||||
"date_created": "2025-08-12",
|
||||
"type": "pve",
|
||||
"updateable": false,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": null,
|
||||
"website": null,
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/proxmox.webp",
|
||||
"config_path": "/etc/default/pve-auto-hook",
|
||||
"description": "This script checks for the presence of required dependencies before starting a VM or LXC container in Proxmox. It ensures that all referenced storages are available and, additionally, supports the usage of tags to check for specific dependencies. If any required dependency is missing, the VM or container will not start until the issue is resolved. This script is designed to be used as a Proxmox hookscript, which can be applied to both QEMU VMs and LXC containers.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "tools/pve/dependency-check.sh",
|
||||
"resources": {
|
||||
"cpu": null,
|
||||
"ram": null,
|
||||
"hdd": null,
|
||||
"os": null,
|
||||
"version": "PVE 8.x / 9.x"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Execute within the Proxmox shell",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "The script supports --install (default), --status and --uninstall for clean lifecycle management.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "To wait until a certain host is available, tag the VM or container with `dep_ping_<hostname>` where `<hostname>` is the name or IP of the host to ping. The script will wait until the host is reachable before proceeding with the startup.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "To wait until a certain TCP port is open, tag the VM or container with `dep_tcp_<hostname>_<port>` where `<hostname>` is the name or IP of the host and `<port>` is the TCP port number. The script will wait until the port is open before proceeding with the startup.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
48
frontend/public/json/discourse.json
generated
48
frontend/public/json/discourse.json
generated
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"name": "Discourse",
|
||||
"slug": "discourse",
|
||||
"categories": [
|
||||
23
|
||||
],
|
||||
"date_created": "2026-02-02",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 80,
|
||||
"documentation": "https://www.discourse.org/",
|
||||
"website": "https://www.discourse.org/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/discourse.webp",
|
||||
"config_path": "/opt/discourse/.env",
|
||||
"description": "Discourse is the civilized discussion platform. Use it as a mailing list, discussion forum, or long-form chat room.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/discourse.sh",
|
||||
"resources": {
|
||||
"cpu": 4,
|
||||
"ram": 4096,
|
||||
"hdd": 20,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "admin",
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Minimum 4GB RAM and 4 CPU cores recommended for production use.",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "Admin user is created with username 'admin'. Set password in first login.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Configure SMTP settings in admin panel for email notifications.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
48
frontend/public/json/ente.json
generated
48
frontend/public/json/ente.json
generated
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"name": "Ente",
|
||||
"slug": "ente",
|
||||
"categories": [
|
||||
11
|
||||
],
|
||||
"date_created": "2025-11-22",
|
||||
"type": "ct",
|
||||
"updateable": false,
|
||||
"privileged": false,
|
||||
"config_path": "/opt",
|
||||
"interface_port": 3000,
|
||||
"documentation": "https://github.com/ente-io/ente",
|
||||
"website": "https://ente.io/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/ente-photos.webp",
|
||||
"description": "Ente is a service that provides a fully open source, end-to-end encrypted platform for you to store your data in the cloud without needing to trust the service provider. On top of this platform, we have built two apps so far: Ente Photos (an alternative to Apple and Google Photos) and Ente Auth (a 2FA alternative to the deprecated Authy).",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/ente.sh",
|
||||
"resources": {
|
||||
"cpu": 4,
|
||||
"ram": 4096,
|
||||
"hdd": 10,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Please use `journalctl -u ente-museum.service -n 10` to read logs for the signup verification code",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "If you want to use the Ente CLI to add/whitelist admins, please follow the instructions at https://ente.io/help/self-hosting/administration/cli",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "To see Museium config: `cat /opt/ente/server/museum.yaml`",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
35
frontend/public/json/forgejo-runner.json
generated
35
frontend/public/json/forgejo-runner.json
generated
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "Forgejo Runner",
|
||||
"slug": "forgejo-runner",
|
||||
"categories": [
|
||||
20
|
||||
],
|
||||
"date_created": "2026-01-12",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://forgejo.org/docs/latest/admin/actions/runner-installation/",
|
||||
"config_path": "/root/.runner",
|
||||
"website": "https://forgejo.org/docs/latest/admin/actions/runner-installation/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/forgejo.webp",
|
||||
"description": "Forgejo Runner is a lightweight service that executes CI/CD jobs for Forgejo, enabling automated builds, tests, and deployments.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/forgejo-runner.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 8,
|
||||
"os": "Debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
44
frontend/public/json/garmin-grafana.json
generated
44
frontend/public/json/garmin-grafana.json
generated
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"name": "garmin-grafana",
|
||||
"slug": "garmin-grafana",
|
||||
"categories": [
|
||||
9
|
||||
],
|
||||
"date_created": "2025-05-08",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 3000,
|
||||
"documentation": "https://github.com/arpanghosh8453/garmin-grafana",
|
||||
"config_path": "/opt/garmin-grafana/.env",
|
||||
"website": "https://github.com/arpanghosh8453/garmin-grafana",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/garmin-grafana.webp",
|
||||
"description": "A self-hosted solution to fetch data from Garmin servers and store it in a local InfluxDB database for visualization with Grafana.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/garmin-grafana.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 8,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Grafana and InfluxDB credentials are stored in `/opt/garmin-grafana/.env`.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "`garmin-grafana` only imports the past 7 days by default. To import historical data, use `/usr/local/bin/garmin-bulk-import` after installation.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
48
frontend/public/json/github-runner.json
generated
48
frontend/public/json/github-runner.json
generated
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"name": "GitHub Runner",
|
||||
"slug": "github-runner",
|
||||
"categories": [
|
||||
20
|
||||
],
|
||||
"date_created": "2026-03-04",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners",
|
||||
"config_path": "/opt/actions-runner",
|
||||
"website": "https://github.com/actions/runner",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/github.webp",
|
||||
"description": "GitHub Actions self-hosted runner executes workflows for your repository or organization, enabling CI/CD, builds, and deployments.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/github-runner.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 8,
|
||||
"os": "Debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Get your token: repo Settings → Actions → Runners → New self-hosted runner → copy the token.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Then run: cd /opt/actions-runner && sudo -u runner ./config.sh --url https://github.com/your-username/your-repo --token <YOUR_TOKEN>",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Start the runner: systemctl start actions-runner",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
138
frontend/public/json/github-versions.json
generated
138
frontend/public/json/github-versions.json
generated
@@ -1,138 +0,0 @@
|
||||
{
|
||||
"generated": "2026-02-04T19:01:08Z",
|
||||
"versions": [
|
||||
{
|
||||
"slug": "affine",
|
||||
"repo": "toeverything/AFFiNE",
|
||||
"version": "v0.25.7",
|
||||
"pinned": false,
|
||||
"date": "2025-12-09T04:34:14Z"
|
||||
},
|
||||
{
|
||||
"slug": "anytype",
|
||||
"repo": "grishy/any-sync-bundle",
|
||||
"version": "v1.3.0-2026-01-31",
|
||||
"pinned": false,
|
||||
"date": "2026-02-01T17:27:12Z"
|
||||
},
|
||||
{
|
||||
"slug": "checkmate",
|
||||
"repo": "bluewave-labs/Checkmate",
|
||||
"version": "v3.3",
|
||||
"pinned": false,
|
||||
"date": "2026-01-28T14:25:25Z"
|
||||
},
|
||||
{
|
||||
"slug": "databasus",
|
||||
"repo": "databasus/databasus",
|
||||
"version": "v3.8.3",
|
||||
"pinned": false,
|
||||
"date": "2026-02-02T18:14:53Z"
|
||||
},
|
||||
{
|
||||
"slug": "ente",
|
||||
"repo": "ente-io/ente",
|
||||
"version": "photos-v1.3.10",
|
||||
"pinned": false,
|
||||
"date": "2026-02-03T11:06:10Z"
|
||||
},
|
||||
{
|
||||
"slug": "frigate",
|
||||
"repo": "blakeblackshear/frigate",
|
||||
"version": "v0.16.4",
|
||||
"pinned": false,
|
||||
"date": "2026-01-29T00:42:14Z"
|
||||
},
|
||||
{
|
||||
"slug": "garmin-grafana",
|
||||
"repo": "arpanghosh8453/garmin-grafana",
|
||||
"version": "v0.3.0",
|
||||
"pinned": false,
|
||||
"date": "2025-05-15T21:39:19Z"
|
||||
},
|
||||
{
|
||||
"slug": "hoodik",
|
||||
"repo": "hudikhq/hoodik",
|
||||
"version": "v1.8.1",
|
||||
"pinned": false,
|
||||
"date": "2025-12-22T20:32:27Z"
|
||||
},
|
||||
{
|
||||
"slug": "isponsorblocktv",
|
||||
"repo": "dmunozv04/iSponsorBlockTV",
|
||||
"version": "v2.6.1",
|
||||
"pinned": false,
|
||||
"date": "2025-10-19T17:43:10Z"
|
||||
},
|
||||
{
|
||||
"slug": "minthcm",
|
||||
"repo": "minthcm/minthcm",
|
||||
"version": "4.2.2",
|
||||
"pinned": false,
|
||||
"date": "2025-10-10T09:37:21Z"
|
||||
},
|
||||
{
|
||||
"slug": "nextexplorer",
|
||||
"repo": "vikramsoni2/nextExplorer",
|
||||
"version": "v2.1.2a",
|
||||
"pinned": false,
|
||||
"date": "2026-01-31T00:09:18Z"
|
||||
},
|
||||
{
|
||||
"slug": "nginx-ui",
|
||||
"repo": "0xJacky/nginx-ui",
|
||||
"version": "v2.3.2",
|
||||
"pinned": false,
|
||||
"date": "2025-12-09T09:47:15Z"
|
||||
},
|
||||
{
|
||||
"slug": "nightscout",
|
||||
"repo": "nightscout/cgm-remote-monitor",
|
||||
"version": "15.0.3",
|
||||
"pinned": false,
|
||||
"date": "2025-05-08T22:12:34Z"
|
||||
},
|
||||
{
|
||||
"slug": "opencloud",
|
||||
"repo": "opencloud-eu/opencloud",
|
||||
"version": "v5.0.1",
|
||||
"pinned": true,
|
||||
"date": "2026-01-28T15:08:23Z"
|
||||
},
|
||||
{
|
||||
"slug": "pixelfed",
|
||||
"repo": "pixelfed/pixelfed",
|
||||
"version": "v0.12.6",
|
||||
"pinned": false,
|
||||
"date": "2025-09-03T12:12:04Z"
|
||||
},
|
||||
{
|
||||
"slug": "romm",
|
||||
"repo": "RetroAchievements/RALibretro",
|
||||
"version": "1.8.2",
|
||||
"pinned": false,
|
||||
"date": "2026-01-23T17:03:31Z"
|
||||
},
|
||||
{
|
||||
"slug": "seer",
|
||||
"repo": "seerr-team/seerr",
|
||||
"version": "v2.7.3",
|
||||
"pinned": false,
|
||||
"date": "2025-08-14T20:43:46Z"
|
||||
},
|
||||
{
|
||||
"slug": "sonobarr",
|
||||
"repo": "Dodelidoo-Labs/sonobarr",
|
||||
"version": "0.11.0",
|
||||
"pinned": false,
|
||||
"date": "2026-01-21T19:07:21Z"
|
||||
},
|
||||
{
|
||||
"slug": "wger",
|
||||
"repo": "wger-project/wger",
|
||||
"version": "2.4",
|
||||
"pinned": false,
|
||||
"date": "2026-01-18T12:12:02Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
52
frontend/public/json/gluetun.json
generated
52
frontend/public/json/gluetun.json
generated
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"name": "Gluetun",
|
||||
"slug": "gluetun",
|
||||
"categories": [
|
||||
4
|
||||
],
|
||||
"date_created": "2026-03-10",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 8000,
|
||||
"documentation": "https://github.com/qdm12/gluetun-wiki",
|
||||
"config_path": "/opt/gluetun-data/.env",
|
||||
"website": "https://github.com/qdm12/gluetun",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/gluetun.webp",
|
||||
"description": "Gluetun is a lightweight VPN client supporting multiple providers (Mullvad, NordVPN, PIA, ProtonVPN, Surfshark, etc.) with OpenVPN and WireGuard, built-in DNS over TLS, firewall kill switch, HTTP proxy, and Shadowsocks proxy.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/gluetun.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 8,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "You must configure your VPN provider credentials in /opt/gluetun-data/.env before the service will connect",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "TUN device support is required and enabled by default during container creation",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Port 8000 provides the HTTP control server API",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Supports 30+ VPN providers - see https://github.com/qdm12/gluetun-wiki for provider-specific setup",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
44
frontend/public/json/homebrew.json
generated
44
frontend/public/json/homebrew.json
generated
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"name": "Homebrew (Linuxbrew)",
|
||||
"slug": "homebrew",
|
||||
"categories": [
|
||||
20
|
||||
],
|
||||
"date_created": "2026-02-26",
|
||||
"type": "addon",
|
||||
"updateable": false,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://docs.brew.sh/",
|
||||
"website": "https://brew.sh/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/homebrew.webp",
|
||||
"config_path": "",
|
||||
"description": "Homebrew (Linuxbrew) is a package manager for Linux that installs software from source into a user-managed prefix, allowing non-root users to install development tools and utilities without system-wide changes.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "tools/addon/homebrew.sh",
|
||||
"resources": {
|
||||
"cpu": null,
|
||||
"ram": null,
|
||||
"hdd": null,
|
||||
"os": null,
|
||||
"version": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Execute within an existing LXC Console",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Requires at least one non-root user (uid >= 1000) in the container",
|
||||
"type": "warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
56
frontend/public/json/hoodik.json
generated
56
frontend/public/json/hoodik.json
generated
@@ -1,56 +0,0 @@
|
||||
{
|
||||
"name": "Hoodik",
|
||||
"slug": "hoodik",
|
||||
"categories": [
|
||||
11
|
||||
],
|
||||
"date_created": "2025-12-10",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 5443,
|
||||
"documentation": "https://github.com/hudikhq/hoodik#readme",
|
||||
"config_path": "/opt/hoodik/.env",
|
||||
"website": "https://github.com/hudikhq/hoodik",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/hoodik.webp",
|
||||
"description": "Hoodik is a lightweight, self-hosted cloud storage solution with end-to-end encryption. Files are encrypted on your device before upload using hybrid RSA/AES encryption, ensuring only you can access your data.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/hoodik.sh",
|
||||
"resources": {
|
||||
"cpu": 4,
|
||||
"ram": 4096,
|
||||
"hdd": 20,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "First visit will prompt you to create an admin account",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Installation builds Rust backend and Vue frontend from source - takes 15-20 minutes",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "Requires 4GB RAM and 20GB disk for build process",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "Data is stored in /opt/hoodik_data",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "SSL is disabled by default - use a reverse proxy for HTTPS",
|
||||
"type": "warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
40
frontend/public/json/igotify.json
generated
40
frontend/public/json/igotify.json
generated
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"name": "iGotify",
|
||||
"slug": "igotify",
|
||||
"categories": [
|
||||
19
|
||||
],
|
||||
"date_created": "2026-02-18",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 80,
|
||||
"documentation": "https://github.com/androidseb25/iGotify-Notification-Assistent",
|
||||
"website": "https://github.com/androidseb25/iGotify-Notification-Assistent",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/gotify.webp",
|
||||
"config_path": "/opt/igotify/.env",
|
||||
"description": "Companion service to Gotify that forwards notifications to iOS devices via Apple Push Notification service (APNs).",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/igotify.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 4,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Requires a running Gotify instance. Configure the Gotify server URL and client token in the iGotify web interface after setup.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
40
frontend/public/json/invidious.json
generated
40
frontend/public/json/invidious.json
generated
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"name": "Invidious",
|
||||
"slug": "invidious",
|
||||
"categories": [
|
||||
13
|
||||
],
|
||||
"date_created": "2026-03-09",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 3000,
|
||||
"documentation": "https:/docs.invidious.io",
|
||||
"website": "https://invidious.io",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/invidious.webp",
|
||||
"config_path": "/opt/invidious/config",
|
||||
"description": "A self-hosted alternative frontend for YouTube",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/invidious.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 4,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Invidious Companion comes pre-installed and is running on port 8082.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
44
frontend/public/json/isponsorblocktv.json
generated
44
frontend/public/json/isponsorblocktv.json
generated
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"name": "iSponsorBlockTV",
|
||||
"slug": "isponsorblocktv",
|
||||
"categories": [
|
||||
13
|
||||
],
|
||||
"date_created": "2026-01-25",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://github.com/dmunozv04/iSponsorBlockTV/wiki",
|
||||
"website": "https://github.com/dmunozv04/iSponsorBlockTV",
|
||||
"logo": "https://raw.githubusercontent.com/ajayyy/SponsorBlock/master/public/icons/IconSponsorBlocker512px.png",
|
||||
"config_path": "/var/lib/isponsorblocktv/config.json",
|
||||
"description": "iSponsorBlockTV connects to YouTube TV clients and automatically skips SponsorBlock segments, mutes ads, and presses the Skip Ad button when available.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/isponsorblocktv.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 1024,
|
||||
"hdd": 4,
|
||||
"os": "debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "No web UI; run `iSponsorBlockTV setup` inside the container to configure.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "SSDP auto-discovery requires multicast on your bridge; manual pairing works without it.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
52
frontend/public/json/localagi.json
generated
52
frontend/public/json/localagi.json
generated
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"name": "LocalAGI",
|
||||
"slug": "localagi",
|
||||
"categories": [
|
||||
20
|
||||
],
|
||||
"date_created": "2026-03-03",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 3000,
|
||||
"documentation": "https://github.com/mudler/LocalAGI#installation-options",
|
||||
"website": "https://github.com/mudler/LocalAGI",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/localagi.webp",
|
||||
"config_path": "/opt/localagi/.env",
|
||||
"description": "LocalAGI is a self-hostable AI agent platform with a web UI, OpenAI-compatible APIs, and local-first model orchestration.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/localagi.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 4096,
|
||||
"hdd": 20,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "This script builds LocalAGI from source (Go + Bun) and runs it as a systemd service.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "This Proxmox script runs LocalAGI in external-backend mode and does not provision local ROCm/NVIDIA runtimes.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "By default, LocalAGI is configured to call an OpenAI-compatible backend at `http://127.0.0.1:11434/v1` (Ollama-compatible) via `LOCALAGI_LLM_API_URL`.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "To use an external Ollama host, edit `/opt/localagi/.env` and set `LOCALAGI_LLM_API_URL=http://<ollama-host>:11434/v1`, then restart LocalAGI with `systemctl restart localagi`.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
31
frontend/public/json/metadata.json
generated
31
frontend/public/json/metadata.json
generated
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"categories": [
|
||||
{ "name": "Proxmox & Virtualization", "id": 1, "sort_order": 1.0, "description": "Tools and scripts to manage Proxmox VE and virtualization platforms effectively." },
|
||||
{ "name": "Operating Systems", "id": 2, "sort_order": 2.0, "description": "Scripts for deploying and managing various operating systems." },
|
||||
{ "name": "Containers & Docker", "id": 3, "sort_order": 3.0, "description": "Solutions for containerization using Docker and related technologies." },
|
||||
{ "name": "Network & Firewall", "id": 4, "sort_order": 4.0, "description": "Enhance network security and configure firewalls with ease." },
|
||||
{ "name": "Adblock & DNS", "id": 5, "sort_order": 5.0, "description": "Optimize your network with DNS and ad-blocking solutions." },
|
||||
{ "name": "Authentication & Security", "id": 6, "sort_order": 6.0, "description": "Secure your infrastructure with authentication and security tools." },
|
||||
{ "name": "Backup & Recovery", "id": 7, "sort_order": 7.0, "description": "Reliable backup and recovery scripts to protect your data." },
|
||||
{ "name": "Databases", "id": 8, "sort_order": 8.0, "description": "Deploy and manage robust database systems with ease." },
|
||||
{ "name": "Monitoring & Analytics", "id": 9, "sort_order": 9.0, "description": "Monitor system performance and analyze data seamlessly." },
|
||||
{ "name": "Dashboards & Frontends", "id": 10, "sort_order": 10.0, "description": "Create interactive dashboards and user-friendly frontends." },
|
||||
{ "name": "Files & Downloads", "id": 11, "sort_order": 11.0, "description": "Manage file sharing and downloading solutions efficiently." },
|
||||
{ "name": "Documents & Notes", "id": 12, "sort_order": 12.0, "description": "Organize and manage documents and note-taking tools." },
|
||||
{ "name": "Media & Streaming", "id": 13, "sort_order": 13.0, "description": "Stream and manage media effortlessly across devices." },
|
||||
{ "name": "*Arr Suite", "id": 14, "sort_order": 14.0, "description": "Automated media management with the popular *Arr suite tools." },
|
||||
{ "name": "NVR & Cameras", "id": 15, "sort_order": 15.0, "description": "Manage network video recorders and camera setups." },
|
||||
{ "name": "IoT & Smart Home", "id": 16, "sort_order": 16.0, "description": "Control and automate IoT devices and smart home systems." },
|
||||
{ "name": "ZigBee, Z-Wave & Matter", "id": 17, "sort_order": 17.0, "description": "Solutions for ZigBee, Z-Wave, and Matter-based device management." },
|
||||
{ "name": "MQTT & Messaging", "id": 18, "sort_order": 18.0, "description": "Set up reliable messaging and MQTT-based communication systems." },
|
||||
{ "name": "Automation & Scheduling", "id": 19, "sort_order": 19.0, "description": "Automate tasks and manage scheduling with powerful tools." },
|
||||
{ "name": "AI / Coding & Dev-Tools", "id": 20, "sort_order": 20.0, "description": "Leverage AI and developer tools for smarter coding workflows." },
|
||||
{ "name": "Webservers & Proxies", "id": 21, "sort_order": 21.0, "description": "Deploy and configure web servers and proxy solutions." },
|
||||
{ "name": "Bots & ChatOps", "id": 22, "sort_order": 22.0, "description": "Enhance collaboration with bots and ChatOps integrations." },
|
||||
{ "name": "Finance & Budgeting", "id": 23, "sort_order": 23.0, "description": "Track expenses and manage budgets efficiently." },
|
||||
{ "name": "Gaming & Leisure", "id": 24, "sort_order": 24.0, "description": "Scripts for gaming servers and leisure-related tools." },
|
||||
{ "name": "Business & ERP", "id": 25, "sort_order": 25.0, "description": "Streamline business operations with ERP and management tools." },
|
||||
{ "name": "Miscellaneous", "id": 0, "sort_order": 99.0, "description": "General scripts and tools that don't fit into other categories." }
|
||||
]
|
||||
}
|
||||
|
||||
40
frontend/public/json/minthcm.json
generated
40
frontend/public/json/minthcm.json
generated
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"name": "MintHCM",
|
||||
"slug": "minthcm",
|
||||
"categories": [
|
||||
25
|
||||
],
|
||||
"date_created": "2025-12-12",
|
||||
"type": "ct",
|
||||
"updateable": false,
|
||||
"privileged": false,
|
||||
"interface_port": 80,
|
||||
"documentation": "https://wiki.minthcm.org/",
|
||||
"config_path": "/var/www/MintHCM",
|
||||
"website": "https://minthcm.org/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/minthcm.webp",
|
||||
"description": "MintCHM is a free and open-source tool for Human Capital Management. Main features: recruitment, time management, onboarding & offboarding, calendar, leave management, resources booking, travel & expenses, workplace management, analytics, roles & permissions management, job descriptions, employer branding, employee profiles, competences & skills, employment history, employee evaluations.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/minthcm.sh",
|
||||
"resources": {
|
||||
"cpu": 4,
|
||||
"ram": 4096,
|
||||
"hdd": 20,
|
||||
"os": "debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "admin",
|
||||
"password": "minthcm"
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Database credentials are stored in ~/minthcm.creds",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
41
frontend/public/json/nextexplorer.json
generated
41
frontend/public/json/nextexplorer.json
generated
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"name": "nextExplorer",
|
||||
"slug": "nextexplorer",
|
||||
"categories": [
|
||||
11,
|
||||
12
|
||||
],
|
||||
"date_created": "2025-12-11",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 3000,
|
||||
"documentation": "https://explorer.nxz.ai",
|
||||
"website": "https://github.com/nxzai/nextExplorer",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/nextexplorer.webp",
|
||||
"config_path": "/etc/nextExplorer/.env",
|
||||
"description": "Self-hosted file access for teams, homelabs, and agencies with a refined interface.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/nextexplorer.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 3072,
|
||||
"hdd": 8,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Bind mount your volume(s) in the `/mnt` directory",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
36
frontend/public/json/oxicloud.json
generated
36
frontend/public/json/oxicloud.json
generated
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"name": "OxiCloud",
|
||||
"slug": "oxicloud",
|
||||
"categories": [
|
||||
11,
|
||||
12
|
||||
],
|
||||
"date_created": "2026-03-09",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 8086,
|
||||
"documentation": "https://github.com/DioCrafts/OxiCloud/tree/main/doc",
|
||||
"website": "https://github.com/DioCrafts/OxiCloud",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/oxicloud.webp",
|
||||
"config_path": "/etc/oxicloud/.env",
|
||||
"description": "Ultra-fast, secure & lightweight self-hosted cloud storage — your files, photos, calendars & contacts, all in one place. Built in Rust.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/oxicloud.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 3072,
|
||||
"hdd": 20,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
52
frontend/public/json/pixelfed.json
generated
52
frontend/public/json/pixelfed.json
generated
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"name": "Pixelfed",
|
||||
"slug": "pixelfed",
|
||||
"categories": [
|
||||
0
|
||||
],
|
||||
"date_created": "2024-06-15",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 80,
|
||||
"documentation": "https://docs.pixelfed.org/",
|
||||
"config_path": "/opt/pixelfed/.env",
|
||||
"website": "https://pixelfed.org/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/pixelfed.webp",
|
||||
"description": "Pixelfed is a free, ethical, and decentralized photo sharing platform powered by ActivityPub federation. It offers an ad-free, privacy-focused alternative to Instagram with features like Stories, Collections, and photo filters.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/pixelfed.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 10,
|
||||
"os": "Debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Create admin account with: cd /opt/pixelfed && sudo -u pixelfed php artisan user:create",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "Credentials saved in /root/pixelfed.creds",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "ActivityPub federation is enabled by default",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Uses PostgreSQL, Redis (socket), and PHP-FPM",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
44
frontend/public/json/postiz.json
generated
44
frontend/public/json/postiz.json
generated
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"name": "Postiz",
|
||||
"slug": "postiz",
|
||||
"categories": [
|
||||
19
|
||||
],
|
||||
"date_created": "2026-02-22",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 80,
|
||||
"documentation": "https://docs.postiz.com/",
|
||||
"website": "https://postiz.com/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/postiz.webp",
|
||||
"config_path": "/opt/postiz/.env",
|
||||
"description": "Postiz is an AI-powered social media scheduling tool for managing posts, building audiences, and growing your business across platforms like X, LinkedIn, Instagram, TikTok, Reddit, and more.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/postiz.sh",
|
||||
"resources": {
|
||||
"cpu": 4,
|
||||
"ram": 8192,
|
||||
"hdd": 20,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "The build process requires significant RAM (4GB+). Do not reduce container RAM below 4GB.",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "NEXT_PUBLIC_* variables are baked at build time. If you change the IP/URL, rebuild with: cd /opt/postiz && pnpm run build",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
48
frontend/public/json/protonmail-bridge.json
generated
48
frontend/public/json/protonmail-bridge.json
generated
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"name": "Proton Mail Bridge",
|
||||
"slug": "protonmail-bridge",
|
||||
"categories": [
|
||||
18
|
||||
],
|
||||
"date_created": "2026-02-22",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://proton.me/support/bridge-cli-guide",
|
||||
"website": "https://proton.me/mail/bridge",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/proton-mail.webp",
|
||||
"config_path": "",
|
||||
"description": "Proton Mail Bridge runs a local IMAP/SMTP service that lets traditional mail clients access a Proton mailbox. This LXC runs Bridge headless and forwards IMAP/SMTP to the LAN using systemd socket activation (systemd-socket-proxyd).",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/protonmail-bridge.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 1024,
|
||||
"hdd": 8,
|
||||
"os": "debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "After install, run protonmailbridge-configure inside the container for first-time setup.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "LAN forwarding (container IP): IMAP 143 -> 127.0.0.1:1143, SMTP 587 -> 127.0.0.1:1025.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "You can later use protonmailbridge-configure to temporarily stop the service and enter the Bridge CLI.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
48
frontend/public/json/simplelogin.json
generated
48
frontend/public/json/simplelogin.json
generated
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"name": "SimpleLogin",
|
||||
"slug": "simplelogin",
|
||||
"categories": [
|
||||
6
|
||||
],
|
||||
"date_created": "2026-02-22",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 80,
|
||||
"documentation": "https://github.com/simple-login/app/blob/master/docs/",
|
||||
"website": "https://simplelogin.io/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/simplelogin.webp",
|
||||
"config_path": "/opt/simplelogin/.env",
|
||||
"description": "SimpleLogin is an open-source email alias solution that lets you create anonymous email aliases to protect your real email address from spam, trackers and data breaches.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/simplelogin.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 10,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "After installation, update EMAIL_DOMAIN in /opt/simplelogin/.env with your actual domain and configure DNS (MX, SPF, DKIM) accordingly.",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "DKIM keys are generated at /opt/simplelogin/dkim/. Add the public key as a TXT record: dkim._domainkey.yourdomain.com",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Create your first admin account by visiting the web interface and registering.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
44
frontend/public/json/skylite-ux.json
generated
44
frontend/public/json/skylite-ux.json
generated
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"name": "Skylite-UX",
|
||||
"slug": "skylite-ux",
|
||||
"categories": [
|
||||
19
|
||||
],
|
||||
"date_created": "2026-02-06",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 3000,
|
||||
"documentation": "https://github.com/Wetzel402/Skylite-UX",
|
||||
"website": "https://github.com/Wetzel402/Skylite-UX",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/skylite-ux.webp",
|
||||
"config_path": "/opt/skylite-ux/.env",
|
||||
"description": "Skylite-UX is an open-source, self-hosted family management app with calendar, todos, shopping lists, and user management.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/skylite-ux.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 4096,
|
||||
"hdd": 8,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Database credentials: `cat ~/skylite-ux.creds`",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Build process requires ~4GB RAM. Runtime usage is much lower — RAM can be reduced after installation.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
44
frontend/public/json/split-pro.json
generated
44
frontend/public/json/split-pro.json
generated
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"name": "Split Pro",
|
||||
"slug": "split-pro",
|
||||
"categories": [
|
||||
12
|
||||
],
|
||||
"date_created": "2026-02-12",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 3000,
|
||||
"documentation": "https://github.com/oss-apps/split-pro/blob/main/docker/README.md",
|
||||
"website": "https://github.com/oss-apps/split-pro",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/splitpro.webp",
|
||||
"config_path": "/opt/split-pro/.env",
|
||||
"description": "SplitPro is a self-hosted, open source way to share expenses with friends. It is designed as a replacement for Splitwise.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/split-pro.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 4096,
|
||||
"hdd": 6,
|
||||
"os": "debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Before first use you must configure email credentials or authentication (OAuth/OIDC) provider in `/opt/split-pro/.env` and restart the service `systemctl restart split-pro`.",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "Receipt uploads are stored in `/opt/split-pro_data/uploads`",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
40
frontend/public/json/step-ca.json
generated
40
frontend/public/json/step-ca.json
generated
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"name": "step-ca",
|
||||
"slug": "step-ca",
|
||||
"categories": [
|
||||
6
|
||||
],
|
||||
"date_created": "2026-02-03",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 443,
|
||||
"documentation": "https://smallstep.com/docs/step-ca/",
|
||||
"website": "https://github.com/smallstep/certificates",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/step-ca.webp",
|
||||
"config_path": "/etc/step-ca",
|
||||
"description": "A private certificate authority (X.509 & SSH) & ACME server for secure automated certificate management, so you can use TLS everywhere & SSO for SSH.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/step-ca.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 2,
|
||||
"os": "debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "root",
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "For required post installation actions, checkout: `https://github.com/community-scripts/ProxmoxVE/discussions/11504`",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
35
frontend/public/json/tor-snowflake.json
generated
35
frontend/public/json/tor-snowflake.json
generated
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "Tor Snowflake",
|
||||
"slug": "tor-snowflake",
|
||||
"categories": [
|
||||
4
|
||||
],
|
||||
"date_created": "2025-12-19",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://community.torproject.org/relay/setup/snowflake/standalone/",
|
||||
"website": "https://snowflake.torproject.org/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/tor.webp",
|
||||
"config_path": "",
|
||||
"description": "Snowflake is a pluggable transport that proxies traffic through temporary proxies using WebRTC. Snowflake allows users in censored locations to access the open internet by connecting through volunteer-run proxies. Running a Snowflake proxy helps users circumvent internet censorship by forwarding their traffic through your server.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/tor-snowflake.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 4,
|
||||
"os": "debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
48
frontend/public/json/twenty.json
generated
48
frontend/public/json/twenty.json
generated
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"name": "Twenty",
|
||||
"slug": "twenty",
|
||||
"categories": [
|
||||
25
|
||||
],
|
||||
"date_created": "2026-02-22",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 3000,
|
||||
"documentation": "https://docs.twenty.com/",
|
||||
"website": "https://twenty.com/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/twenty-crm.webp",
|
||||
"config_path": "/opt/twenty/.env",
|
||||
"description": "Twenty is a modern open-source CRM platform that helps you manage customer relationships, track deals, and organize contacts with a clean and powerful interface.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/twenty.sh",
|
||||
"resources": {
|
||||
"cpu": 4,
|
||||
"ram": 8192,
|
||||
"hdd": 20,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "First visit will prompt you to create a workspace and admin account.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Building from source requires significant resources and takes 10-15 minutes.",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "Update SERVER_URL in /opt/twenty/.env if accessing from a different hostname.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
35
frontend/public/json/unifi-os-server-vm.json
generated
35
frontend/public/json/unifi-os-server-vm.json
generated
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "Unifi OS Server VM",
|
||||
"slug": "unifi-os-server-vm",
|
||||
"categories": [
|
||||
2
|
||||
],
|
||||
"date_created": "2025-12-02",
|
||||
"type": "vm",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": "https://help.ui.com/hc/en-us",
|
||||
"website": "https://www.ui.com/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/ubiquiti-unifi.webp",
|
||||
"config_path": "",
|
||||
"description": "Unifi OS Server is the operating system that powers Ubiquiti's UniFi line of network devices. It provides a centralized platform for managing and monitoring UniFi access points, switches, and security gateways, offering features such as network configuration, device provisioning, and performance analytics.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "vm/unifi-os-server-vm.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 4096,
|
||||
"hdd": 32,
|
||||
"os": null,
|
||||
"version": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": []
|
||||
}
|
||||
80
frontend/public/json/update-apps.json
generated
80
frontend/public/json/update-apps.json
generated
@@ -1,80 +0,0 @@
|
||||
{
|
||||
"name": "PVE LXC Apps Update",
|
||||
"slug": "update-apps",
|
||||
"categories": [
|
||||
1
|
||||
],
|
||||
"date_created": "2025-09-15",
|
||||
"type": "pve",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": null,
|
||||
"website": null,
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/proxmox.webp",
|
||||
"config_path": "",
|
||||
"description": "This script updates community-scripts managed LXC containers on a Proxmox VE node. It detects the installed service, verifies available update scripts, and applies updates interactively or unattended. Optionally, containers can be backed up before the update process. If additional build resources (CPU/RAM) are required, the script adjusts container resources temporarily and restores them after the update. Containers requiring a reboot will be listed at the end of the process.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "tools/pve/update-apps.sh",
|
||||
"resources": {
|
||||
"cpu": null,
|
||||
"ram": null,
|
||||
"hdd": null,
|
||||
"os": null,
|
||||
"version": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Execute within the Proxmox shell.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Only containers with `community-script` or `proxmox-helper-scripts` tags are listed for update.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Optionally performs a vzdump backup before updating containers.",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "If required, the script will temporarily increase container CPU/RAM resources for the build process and restore them after completion.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "At the end of the update, containers requiring a reboot will be listed, and you may choose to reboot them directly.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Use `var_backup=yes|no` to enable/disable backup (skip prompt).",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Use `var_backup_storage=<name>` to set backup storage location.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Use `var_container=all|all_running|all_stopped|101,102,...` to select containers.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Use `var_unattended=yes|no` to run updates without interaction.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Use `var_skip_confirm=yes` to skip initial confirmation dialog.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Use `var_auto_reboot=yes|no` to auto-reboot containers after update.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
487
frontend/public/json/versions.json
generated
487
frontend/public/json/versions.json
generated
@@ -1,487 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "theonedev/onedev",
|
||||
"version": "v12.0.0",
|
||||
"date": "2025-07-14T06:19:59Z"
|
||||
},
|
||||
{
|
||||
"name": "wazuh/wazuh",
|
||||
"version": "coverity-w28-4.13.0",
|
||||
"date": "2025-07-08T11:25:24Z"
|
||||
},
|
||||
{
|
||||
"name": "Luligu/matterbridge",
|
||||
"version": "3.1.3",
|
||||
"date": "2025-07-14T07:14:31Z"
|
||||
},
|
||||
{
|
||||
"name": "morpheus65535/bazarr",
|
||||
"version": "v1.5.3-beta.9",
|
||||
"date": "2025-07-14T06:07:20Z"
|
||||
},
|
||||
{
|
||||
"name": "Jackett/Jackett",
|
||||
"version": "v0.22.2154",
|
||||
"date": "2025-07-14T06:00:33Z"
|
||||
},
|
||||
{
|
||||
"name": "firefly-iii/firefly-iii",
|
||||
"version": "v6.2.20",
|
||||
"date": "2025-07-02T04:03:37Z"
|
||||
},
|
||||
{
|
||||
"name": "steveiliop56/tinyauth",
|
||||
"version": "v3.6.1",
|
||||
"date": "2025-07-12T13:41:57Z"
|
||||
},
|
||||
{
|
||||
"name": "homebridge/homebridge",
|
||||
"version": "v1.11.0",
|
||||
"date": "2025-07-13T19:22:47Z"
|
||||
},
|
||||
{
|
||||
"name": "wavelog/wavelog",
|
||||
"version": "2.0.6",
|
||||
"date": "2025-07-13T19:10:15Z"
|
||||
},
|
||||
{
|
||||
"name": "MediaBrowser/Emby.Releases",
|
||||
"version": "4.9.1.2",
|
||||
"date": "2025-06-26T22:08:00Z"
|
||||
},
|
||||
{
|
||||
"name": "openhab/openhab-core",
|
||||
"version": "5.0.0.M4",
|
||||
"date": "2025-07-13T16:04:39Z"
|
||||
},
|
||||
{
|
||||
"name": "msgbyte/tianji",
|
||||
"version": "v1.23.5",
|
||||
"date": "2025-07-13T15:23:02Z"
|
||||
},
|
||||
{
|
||||
"name": "mayswind/AriaNg",
|
||||
"version": "1.3.11",
|
||||
"date": "2025-07-13T13:33:48Z"
|
||||
},
|
||||
{
|
||||
"name": "evcc-io/evcc",
|
||||
"version": "0.205.0",
|
||||
"date": "2025-07-13T12:27:31Z"
|
||||
},
|
||||
{
|
||||
"name": "rcourtman/Pulse",
|
||||
"version": "v3.41.1",
|
||||
"date": "2025-07-10T17:10:46Z"
|
||||
},
|
||||
{
|
||||
"name": "authelia/authelia",
|
||||
"version": "v4.39.5",
|
||||
"date": "2025-07-13T06:12:47Z"
|
||||
},
|
||||
{
|
||||
"name": "henrygd/beszel",
|
||||
"version": "v0.11.1",
|
||||
"date": "2025-04-29T01:14:35Z"
|
||||
},
|
||||
{
|
||||
"name": "esphome/esphome",
|
||||
"version": "2025.6.3",
|
||||
"date": "2025-07-03T01:07:26Z"
|
||||
},
|
||||
{
|
||||
"name": "OliveTin/OliveTin",
|
||||
"version": "2025.7.13",
|
||||
"date": "2025-07-12T23:32:05Z"
|
||||
},
|
||||
{
|
||||
"name": "Ombi-app/Ombi",
|
||||
"version": "v4.47.1",
|
||||
"date": "2025-01-05T21:14:23Z"
|
||||
},
|
||||
{
|
||||
"name": "dgtlmoon/changedetection.io",
|
||||
"version": "0.50.6",
|
||||
"date": "2025-07-12T19:52:52Z"
|
||||
},
|
||||
{
|
||||
"name": "advplyr/audiobookshelf",
|
||||
"version": "v2.26.0",
|
||||
"date": "2025-07-12T19:31:21Z"
|
||||
},
|
||||
{
|
||||
"name": "jellyfin/jellyfin",
|
||||
"version": "v10.10.7",
|
||||
"date": "2025-04-05T19:14:59Z"
|
||||
},
|
||||
{
|
||||
"name": "cross-seed/cross-seed",
|
||||
"version": "v6.13.0",
|
||||
"date": "2025-07-12T15:52:03Z"
|
||||
},
|
||||
{
|
||||
"name": "blakeblackshear/frigate",
|
||||
"version": "v0.14.1",
|
||||
"date": "2024-08-29T22:32:51Z"
|
||||
},
|
||||
{
|
||||
"name": "homarr-labs/homarr",
|
||||
"version": "v1.28.1",
|
||||
"date": "2025-07-12T08:50:59Z"
|
||||
},
|
||||
{
|
||||
"name": "fallenbagel/jellyseerr",
|
||||
"version": "preview-seerr",
|
||||
"date": "2025-07-12T08:15:55Z"
|
||||
},
|
||||
{
|
||||
"name": "leiweibau/Pi.Alert",
|
||||
"version": "v2025-07-12",
|
||||
"date": "2025-07-12T07:53:52Z"
|
||||
},
|
||||
{
|
||||
"name": "keycloak/keycloak",
|
||||
"version": "26.3.1",
|
||||
"date": "2025-07-09T15:41:43Z"
|
||||
},
|
||||
{
|
||||
"name": "fuma-nama/fumadocs",
|
||||
"version": "fumadocs-openapi@9.1.2",
|
||||
"date": "2025-07-12T03:58:47Z"
|
||||
},
|
||||
{
|
||||
"name": "eclipse-mosquitto/mosquitto",
|
||||
"version": "v2.0.22",
|
||||
"date": "2025-07-11T21:34:20Z"
|
||||
},
|
||||
{
|
||||
"name": "mongodb/mongo",
|
||||
"version": "r8.2.0-alpha0",
|
||||
"date": "2025-07-11T21:06:26Z"
|
||||
},
|
||||
{
|
||||
"name": "duplicati/duplicati",
|
||||
"version": "v2.1.0.124-2.1.0.124_canary_2025-07-11",
|
||||
"date": "2025-07-11T20:09:08Z"
|
||||
},
|
||||
{
|
||||
"name": "TandoorRecipes/recipes",
|
||||
"version": "1.5.35",
|
||||
"date": "2025-06-22T08:30:10Z"
|
||||
},
|
||||
{
|
||||
"name": "neo4j/neo4j",
|
||||
"version": "2025.06.2",
|
||||
"date": "2025-07-11T18:03:51Z"
|
||||
},
|
||||
{
|
||||
"name": "n8n-io/n8n",
|
||||
"version": "n8n@1.101.2",
|
||||
"date": "2025-07-11T12:03:41Z"
|
||||
},
|
||||
{
|
||||
"name": "FlowiseAI/Flowise",
|
||||
"version": "flowise@3.0.4",
|
||||
"date": "2025-07-11T13:26:54Z"
|
||||
},
|
||||
{
|
||||
"name": "zwave-js/zwave-js-ui",
|
||||
"version": "v10.9.0",
|
||||
"date": "2025-07-11T12:57:54Z"
|
||||
},
|
||||
{
|
||||
"name": "zitadel/zitadel",
|
||||
"version": "v3.3.1",
|
||||
"date": "2025-07-11T11:51:48Z"
|
||||
},
|
||||
{
|
||||
"name": "prometheus/prometheus",
|
||||
"version": "v2.53.5",
|
||||
"date": "2025-06-30T11:01:12Z"
|
||||
},
|
||||
{
|
||||
"name": "Paymenter/Paymenter",
|
||||
"version": "v1.2.2",
|
||||
"date": "2025-07-11T10:09:47Z"
|
||||
},
|
||||
{
|
||||
"name": "traefik/traefik",
|
||||
"version": "v3.4.4",
|
||||
"date": "2025-07-11T08:41:34Z"
|
||||
},
|
||||
{
|
||||
"name": "mattermost/mattermost",
|
||||
"version": "preview-v0.1",
|
||||
"date": "2025-06-27T14:35:47Z"
|
||||
},
|
||||
{
|
||||
"name": "documenso/documenso",
|
||||
"version": "v1.12.2-rc.1",
|
||||
"date": "2025-07-11T02:55:56Z"
|
||||
},
|
||||
{
|
||||
"name": "outline/outline",
|
||||
"version": "v0.85.1",
|
||||
"date": "2025-07-11T01:17:53Z"
|
||||
},
|
||||
{
|
||||
"name": "jenkinsci/jenkins",
|
||||
"version": "jenkins-2.518",
|
||||
"date": "2025-07-08T13:52:55Z"
|
||||
},
|
||||
{
|
||||
"name": "LibreTranslate/LibreTranslate",
|
||||
"version": "v1.7.2",
|
||||
"date": "2025-07-10T19:29:26Z"
|
||||
},
|
||||
{
|
||||
"name": "binwiederhier/ntfy",
|
||||
"version": "v2.13.0",
|
||||
"date": "2025-07-10T19:27:54Z"
|
||||
},
|
||||
{
|
||||
"name": "ollama/ollama",
|
||||
"version": "v0.9.6",
|
||||
"date": "2025-07-08T01:26:29Z"
|
||||
},
|
||||
{
|
||||
"name": "forgejo/forgejo",
|
||||
"version": "v11.0.3",
|
||||
"date": "2025-07-10T13:12:00Z"
|
||||
},
|
||||
{
|
||||
"name": "crowdsecurity/crowdsec",
|
||||
"version": "v1.6.10",
|
||||
"date": "2025-07-10T12:04:30Z"
|
||||
},
|
||||
{
|
||||
"name": "meilisearch/meilisearch",
|
||||
"version": "prototype-incremental-vector-store-3",
|
||||
"date": "2025-07-07T10:27:19Z"
|
||||
},
|
||||
{
|
||||
"name": "pocket-id/pocket-id",
|
||||
"version": "v1.6.2",
|
||||
"date": "2025-07-09T22:14:10Z"
|
||||
},
|
||||
{
|
||||
"name": "NginxProxyManager/nginx-proxy-manager",
|
||||
"version": "v2.12.6",
|
||||
"date": "2025-07-09T21:52:15Z"
|
||||
},
|
||||
{
|
||||
"name": "apache/tika",
|
||||
"version": "3.2.1",
|
||||
"date": "2025-07-09T20:47:29Z"
|
||||
},
|
||||
{
|
||||
"name": "sabnzbd/sabnzbd",
|
||||
"version": "4.5.2",
|
||||
"date": "2025-07-09T19:08:28Z"
|
||||
},
|
||||
{
|
||||
"name": "raydak-labs/configarr",
|
||||
"version": "v1.13.6",
|
||||
"date": "2025-07-09T17:23:01Z"
|
||||
},
|
||||
{
|
||||
"name": "hargata/lubelog",
|
||||
"version": "v1.4.9",
|
||||
"date": "2025-07-09T16:27:46Z"
|
||||
},
|
||||
{
|
||||
"name": "nicolargo/glances",
|
||||
"version": "v4.3.3",
|
||||
"date": "2025-07-09T15:35:44Z"
|
||||
},
|
||||
{
|
||||
"name": "rclone/rclone",
|
||||
"version": "v1.70.3",
|
||||
"date": "2025-07-09T15:06:31Z"
|
||||
},
|
||||
{
|
||||
"name": "home-assistant/operating-system",
|
||||
"version": "16.0",
|
||||
"date": "2025-07-09T13:28:43Z"
|
||||
},
|
||||
{
|
||||
"name": "element-hq/synapse",
|
||||
"version": "v1.133.0",
|
||||
"date": "2025-07-01T15:13:42Z"
|
||||
},
|
||||
{
|
||||
"name": "AdguardTeam/AdGuardHome",
|
||||
"version": "v0.107.63",
|
||||
"date": "2025-06-26T14:34:19Z"
|
||||
},
|
||||
{
|
||||
"name": "cockpit-project/cockpit",
|
||||
"version": "342",
|
||||
"date": "2025-07-09T08:48:21Z"
|
||||
},
|
||||
{
|
||||
"name": "Prowlarr/Prowlarr",
|
||||
"version": "v1.37.0.5076",
|
||||
"date": "2025-06-04T11:04:53Z"
|
||||
},
|
||||
{
|
||||
"name": "Radarr/Radarr",
|
||||
"version": "v5.26.2.10099",
|
||||
"date": "2025-06-11T20:10:39Z"
|
||||
},
|
||||
{
|
||||
"name": "ipfs/kubo",
|
||||
"version": "v0.35.0",
|
||||
"date": "2025-05-21T18:00:32Z"
|
||||
},
|
||||
{
|
||||
"name": "grokability/snipe-it",
|
||||
"version": "v8.1.18",
|
||||
"date": "2025-07-08T20:36:37Z"
|
||||
},
|
||||
{
|
||||
"name": "Stirling-Tools/Stirling-PDF",
|
||||
"version": "v1.0.2",
|
||||
"date": "2025-07-08T19:14:31Z"
|
||||
},
|
||||
{
|
||||
"name": "TwiN/gatus",
|
||||
"version": "v5.20.0",
|
||||
"date": "2025-07-08T16:27:11Z"
|
||||
},
|
||||
{
|
||||
"name": "bunkerity/bunkerweb",
|
||||
"version": "v1.6.2",
|
||||
"date": "2025-07-08T13:52:33Z"
|
||||
},
|
||||
{
|
||||
"name": "docker/compose",
|
||||
"version": "v2.38.2",
|
||||
"date": "2025-07-08T09:35:14Z"
|
||||
},
|
||||
{
|
||||
"name": "Checkmk/checkmk",
|
||||
"version": "v2.4.0p7",
|
||||
"date": "2025-07-08T05:51:08Z"
|
||||
},
|
||||
{
|
||||
"name": "VictoriaMetrics/VictoriaMetrics",
|
||||
"version": "pmm-6401-v1.121.0",
|
||||
"date": "2025-07-07T16:16:13Z"
|
||||
},
|
||||
{
|
||||
"name": "photoprism/photoprism",
|
||||
"version": "250707-d28b3101e",
|
||||
"date": "2025-07-07T15:15:21Z"
|
||||
},
|
||||
{
|
||||
"name": "traccar/traccar",
|
||||
"version": "v6.8.1",
|
||||
"date": "2025-07-07T14:40:11Z"
|
||||
},
|
||||
{
|
||||
"name": "BookStackApp/BookStack",
|
||||
"version": "v25.05.2",
|
||||
"date": "2025-07-07T14:08:25Z"
|
||||
},
|
||||
{
|
||||
"name": "nzbgetcom/nzbget",
|
||||
"version": "v25.2",
|
||||
"date": "2025-07-04T08:21:42Z"
|
||||
},
|
||||
{
|
||||
"name": "slskd/slskd",
|
||||
"version": "0.23.1",
|
||||
"date": "2025-07-06T23:57:52Z"
|
||||
},
|
||||
{
|
||||
"name": "pelican-dev/panel",
|
||||
"version": "v1.0.0-beta22",
|
||||
"date": "2025-07-06T21:16:00Z"
|
||||
},
|
||||
{
|
||||
"name": "pelican-dev/wings",
|
||||
"version": "v1.0.0-beta14",
|
||||
"date": "2025-07-06T21:07:07Z"
|
||||
},
|
||||
{
|
||||
"name": "bluenviron/mediamtx",
|
||||
"version": "v1.13.0",
|
||||
"date": "2025-07-06T19:23:55Z"
|
||||
},
|
||||
{
|
||||
"name": "syncthing/syncthing",
|
||||
"version": "v1.30.0",
|
||||
"date": "2025-07-01T11:29:11Z"
|
||||
},
|
||||
{
|
||||
"name": "Part-DB/Part-DB-server",
|
||||
"version": "v1.17.2",
|
||||
"date": "2025-07-06T12:21:52Z"
|
||||
},
|
||||
{
|
||||
"name": "redis/redis",
|
||||
"version": "8.0.3",
|
||||
"date": "2025-07-06T12:19:24Z"
|
||||
},
|
||||
{
|
||||
"name": "hyperion-project/hyperion.ng",
|
||||
"version": "2.1.1",
|
||||
"date": "2025-06-14T17:45:06Z"
|
||||
},
|
||||
{
|
||||
"name": "Kareadita/Kavita",
|
||||
"version": "v0.8.7",
|
||||
"date": "2025-07-05T20:08:58Z"
|
||||
},
|
||||
{
|
||||
"name": "runtipi/runtipi",
|
||||
"version": "v4.3.0",
|
||||
"date": "2025-07-05T12:14:52Z"
|
||||
},
|
||||
{
|
||||
"name": "linkwarden/linkwarden",
|
||||
"version": "v2.11.3",
|
||||
"date": "2025-07-05T04:34:46Z"
|
||||
},
|
||||
{
|
||||
"name": "home-assistant/core",
|
||||
"version": "2025.7.1",
|
||||
"date": "2025-07-04T20:02:52Z"
|
||||
},
|
||||
{
|
||||
"name": "emqx/emqx",
|
||||
"version": "e6.0.0-M1.202507-alpha.1",
|
||||
"date": "2025-07-04T14:58:23Z"
|
||||
},
|
||||
{
|
||||
"name": "kimai/kimai",
|
||||
"version": "2.37.0",
|
||||
"date": "2025-07-04T14:49:43Z"
|
||||
},
|
||||
{
|
||||
"name": "Graylog2/graylog2-server",
|
||||
"version": "6.3.1",
|
||||
"date": "2025-07-04T11:20:48Z"
|
||||
},
|
||||
{
|
||||
"name": "cloudflare/cloudflared",
|
||||
"version": "2025.7.0",
|
||||
"date": "2025-07-03T17:08:15Z"
|
||||
},
|
||||
{
|
||||
"name": "rabbitmq/rabbitmq-server",
|
||||
"version": "v4.1.2",
|
||||
"date": "2025-07-03T16:59:29Z"
|
||||
},
|
||||
{
|
||||
"name": "influxdata/influxdb",
|
||||
"version": "v3.2.1",
|
||||
"date": "2025-07-03T16:09:19Z"
|
||||
},
|
||||
{
|
||||
"name": "Dolibarr/dolibarr",
|
||||
"version": "18.0.7",
|
||||
"date": "2025-07-03T08:57:21Z"
|
||||
}
|
||||
]
|
||||
48
frontend/public/json/versitygw.json
generated
48
frontend/public/json/versitygw.json
generated
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"name": "VersityGW",
|
||||
"slug": "versitygw",
|
||||
"categories": [
|
||||
11
|
||||
],
|
||||
"date_created": "2026-03-10",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 7070,
|
||||
"documentation": "https://github.com/versity/versitygw/wiki",
|
||||
"config_path": "/etc/versitygw.d/gateway.conf",
|
||||
"website": "https://www.versity.com/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/versitygw.webp",
|
||||
"description": "VersityGW is a high-performance, cloud-native S3-compatible gateway that provides S3 API access to various storage backends including POSIX filesystems.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/versitygw.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 8,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "S3 access credentials are generated during installation and stored in /etc/versitygw.d/gateway.conf",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Data is stored in /opt/versitygw-data (POSIX backend)",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Use any S3-compatible client to connect to the gateway endpoint",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
44
frontend/public/json/yamtrack.json
generated
44
frontend/public/json/yamtrack.json
generated
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"name": "Yamtrack",
|
||||
"slug": "yamtrack",
|
||||
"categories": [
|
||||
13
|
||||
],
|
||||
"date_created": "2026-02-22",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 8000,
|
||||
"documentation": "https://github.com/FuzzyGrim/Yamtrack/wiki",
|
||||
"website": "https://github.com/FuzzyGrim/Yamtrack",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/yamtrack.webp",
|
||||
"config_path": "/opt/yamtrack/src/.env",
|
||||
"description": "Yamtrack is a self-hosted media tracker for movies, TV shows, anime, manga, video games, books, comics, and board games with multi-user support and Celery-powered background tasks.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/yamtrack.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 8,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Set API keys (TMDB_API, MAL_API, IGDB_ID, IGDB_SECRET) in /opt/yamtrack/src/.env to enable media search from external providers.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "If using a reverse proxy, set the URLS variable in .env to your external URL (e.g., URLS=https://yamtrack.example.com).",
|
||||
"type": "warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
44
frontend/public/json/zitadel.json
generated
44
frontend/public/json/zitadel.json
generated
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"name": "Zitadel",
|
||||
"slug": "zitadel",
|
||||
"categories": [
|
||||
6
|
||||
],
|
||||
"date_created": "2025-02-10",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 8080,
|
||||
"documentation": "https://zitadel.com/docs/guides/overview",
|
||||
"website": "https://zitadel.com",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/zitadel.webp",
|
||||
"config_path": "/etc/zitadel/config.yaml",
|
||||
"description": "Zitadel is an open-source identity and access management (IAM) solution designed to provide secure authentication, authorization, and user management for modern applications and services. Built with a focus on flexibility, scalability, and security, Zitadel offers a comprehensive set of features for developers and organizations looking to implement robust identity management.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/zitadel.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 1024,
|
||||
"hdd": 6,
|
||||
"os": "debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": "zitadel-admin@zitadel.localhost",
|
||||
"password": "Password1!"
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Application credentials: `cat /etc/zitadel/INSTALLATION_INFO.txt`",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Change the ExternalDomain value in `/etc/zitadel/config.yaml` to your domain/hostname/IP and run `bash zitadel-rerun.sh`",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB |
@@ -1,11 +0,0 @@
|
||||
import { screen } from "@testing-library/dom";
|
||||
import { render } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import Page from "@/app/page";
|
||||
|
||||
describe("Page", () => {
|
||||
it("should show button to view scripts", () => {
|
||||
render(<Page />);
|
||||
expect(screen.getByRole("button", { name: "View Scripts" })).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,57 +0,0 @@
|
||||
import { describe, it, assert, beforeAll } from "vitest";
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
import { ScriptSchema, type Script } from "@/app/json-editor/_schemas/schemas";
|
||||
import { Metadata } from "@/lib/types";
|
||||
console.log('Current directory: ' + process.cwd());
|
||||
const jsonDir = "public/json";
|
||||
const metadataFileName = "metadata.json";
|
||||
const versionsFileName = "versions.json";
|
||||
const githubVersionsFileName = "github-versions.json";
|
||||
const encoding = "utf-8";
|
||||
|
||||
const fileNames = (await fs.readdir(jsonDir))
|
||||
.filter((fileName) => fileName.endsWith(".json") && fileName !== metadataFileName && fileName !== versionsFileName && fileName !== githubVersionsFileName);
|
||||
|
||||
describe.each(fileNames)("%s", async (fileName) => {
|
||||
let script: Script;
|
||||
|
||||
beforeAll(async () => {
|
||||
const filePath = path.resolve(jsonDir, fileName);
|
||||
const fileContent = await fs.readFile(filePath, encoding)
|
||||
script = JSON.parse(fileContent);
|
||||
})
|
||||
|
||||
|
||||
it("should have valid json according to script schema", () => {
|
||||
ScriptSchema.parse(script);
|
||||
});
|
||||
|
||||
it("should have a corresponding script file", () => {
|
||||
script.install_methods.forEach((method) => {
|
||||
const scriptPath = path.resolve("..", method.script)
|
||||
//FIXME: Dose note account for new dir structure and files in /script/tools
|
||||
|
||||
assert(fs.stat(scriptPath), `Script file not found: ${scriptPath}`)
|
||||
})
|
||||
});
|
||||
})
|
||||
|
||||
describe(`${metadataFileName}`, async () => {
|
||||
let metadata: Metadata;
|
||||
|
||||
beforeAll(async () => {
|
||||
const filePath = path.resolve(jsonDir, metadataFileName);
|
||||
const fileContent = await fs.readFile(filePath, encoding)
|
||||
metadata = JSON.parse(fileContent);
|
||||
})
|
||||
it("should have valid json according to metadata schema", () => {
|
||||
// TODO: create zod schema for metadata. Move zod schemas to /lib/types.ts
|
||||
assert(metadata.categories.length > 0);
|
||||
metadata.categories.forEach((category) => {
|
||||
assert.isString(category.name)
|
||||
assert.isNumber(category.id)
|
||||
assert.isNumber(category.sort_order)
|
||||
});
|
||||
});
|
||||
})
|
||||
@@ -1,4 +0,0 @@
|
||||
import { vi } from "vitest";
|
||||
|
||||
// Mock canvas getContext
|
||||
HTMLCanvasElement.prototype.getContext = vi.fn();
|
||||
@@ -1,58 +0,0 @@
|
||||
import { Metadata, Script } from "@/lib/types";
|
||||
import { promises as fs } from "fs";
|
||||
import { NextResponse } from "next/server";
|
||||
import path from "path";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
const jsonDir = "public/json";
|
||||
const metadataFileName = "metadata.json";
|
||||
const encoding = "utf-8";
|
||||
|
||||
const getMetadata = async () => {
|
||||
const filePath = path.resolve(jsonDir, metadataFileName);
|
||||
console.log("TEST");
|
||||
console.log("FilePath: ", filePath);
|
||||
const fileContent = await fs.readFile(filePath, encoding);
|
||||
const metadata: Metadata = JSON.parse(fileContent);
|
||||
return metadata;
|
||||
};
|
||||
|
||||
const getScripts = async () => {
|
||||
const filePaths = (await fs.readdir(jsonDir))
|
||||
.filter((fileName) => fileName !== metadataFileName && fileName.endsWith(".json"))
|
||||
.map((fileName) => path.resolve(jsonDir, fileName));
|
||||
|
||||
const scripts = await Promise.all(
|
||||
filePaths.map(async (filePath) => {
|
||||
const fileContent = await fs.readFile(filePath, encoding);
|
||||
const script: Script = JSON.parse(fileContent);
|
||||
return script;
|
||||
}),
|
||||
);
|
||||
return scripts;
|
||||
};
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const metadata = await getMetadata();
|
||||
const scripts = await getScripts();
|
||||
|
||||
const categories = metadata.categories
|
||||
.map((category) => {
|
||||
category.scripts = scripts.filter((script) =>
|
||||
script.categories?.includes(category.id),
|
||||
);
|
||||
return category;
|
||||
})
|
||||
.sort((a, b) => a.sort_order - b.sort_order);
|
||||
|
||||
return NextResponse.json(categories);
|
||||
} catch (error) {
|
||||
console.error(error as Error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch categories" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { AppVersion } from "@/lib/types";
|
||||
import { error } from "console";
|
||||
import { promises as fs } from "fs";
|
||||
// import Error from "next/error";
|
||||
import { NextResponse } from "next/server";
|
||||
import path from "path";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
const jsonDir = "public/json";
|
||||
const versionsFileName = "versions.json";
|
||||
const encoding = "utf-8";
|
||||
|
||||
const getVersions = async () => {
|
||||
const filePath = path.resolve(jsonDir, versionsFileName);
|
||||
const fileContent = await fs.readFile(filePath, encoding);
|
||||
const versions: AppVersion[] = JSON.parse(fileContent);
|
||||
console.log("Versions: ", versions);
|
||||
const modifiedVersions = versions.map(version => {
|
||||
let newName = version.name;
|
||||
// Ensure date is included in the returned object
|
||||
newName = newName.toLowerCase().replace(/[^a-z0-9/]/g, '');
|
||||
return { ...version, name: newName};
|
||||
});
|
||||
|
||||
return modifiedVersions;
|
||||
};
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
|
||||
const versions = await getVersions();
|
||||
return NextResponse.json(versions);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const err = error as globalThis.Error;
|
||||
return NextResponse.json({
|
||||
name: err.name,
|
||||
message: err.message || "An unexpected error occurred",
|
||||
version: "No version found - Error"
|
||||
}, {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { Category } from "@/lib/types";
|
||||
|
||||
const defaultLogo = "/default-logo.png"; // Fallback logo path
|
||||
const MAX_DESCRIPTION_LENGTH = 100; // Set max length for description
|
||||
const MAX_LOGOS = 5; // Max logos to display at once
|
||||
|
||||
const formattedBadge = (type: string) => {
|
||||
switch (type) {
|
||||
case "vm":
|
||||
return <Badge className="text-blue-500/75 border-blue-500/75 badge">VM</Badge>;
|
||||
case "ct":
|
||||
return (
|
||||
<Badge className="text-yellow-500/75 border-yellow-500/75 badge">LXC</Badge>
|
||||
);
|
||||
case "misc":
|
||||
return <Badge className="text-green-500/75 border-green-500/75 badge">MISC</Badge>;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const CategoryView = () => {
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [selectedCategoryIndex, setSelectedCategoryIndex] = useState<number | null>(null);
|
||||
const [currentScripts, setCurrentScripts] = useState<any[]>([]);
|
||||
const [logoIndices, setLogoIndices] = useState<{ [key: string]: number }>({});
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const basePath = process.env.NODE_ENV === "production" ? "/ProxmoxVED" : "";
|
||||
const response = await fetch(`${basePath}/api/categories`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch categories");
|
||||
}
|
||||
const data = await response.json();
|
||||
setCategories(data);
|
||||
|
||||
// Initialize logo indices
|
||||
const initialLogoIndices: { [key: string]: number } = {};
|
||||
data.forEach((category: any) => {
|
||||
initialLogoIndices[category.name] = 0;
|
||||
});
|
||||
setLogoIndices(initialLogoIndices);
|
||||
} catch (error) {
|
||||
console.error("Error fetching categories:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCategories();
|
||||
}, []);
|
||||
|
||||
const handleCategoryClick = (index: number) => {
|
||||
setSelectedCategoryIndex(index);
|
||||
setCurrentScripts(categories[index]?.scripts || []); // Update scripts for the selected category
|
||||
};
|
||||
|
||||
const handleBackClick = () => {
|
||||
setSelectedCategoryIndex(null);
|
||||
setCurrentScripts([]); // Clear scripts when going back
|
||||
};
|
||||
|
||||
const handleScriptClick = (scriptSlug: string) => {
|
||||
router.push(`/scripts?id=${scriptSlug}`);
|
||||
};
|
||||
|
||||
const navigateCategory = (direction: "prev" | "next") => {
|
||||
if (selectedCategoryIndex !== null) {
|
||||
const newIndex =
|
||||
direction === "prev"
|
||||
? (selectedCategoryIndex - 1 + categories.length) % categories.length
|
||||
: (selectedCategoryIndex + 1) % categories.length;
|
||||
setSelectedCategoryIndex(newIndex);
|
||||
setCurrentScripts(categories[newIndex]?.scripts || []); // Update scripts for the new category
|
||||
}
|
||||
};
|
||||
|
||||
const switchLogos = (categoryName: string, direction: "prev" | "next") => {
|
||||
setLogoIndices((prev) => {
|
||||
const currentIndex = prev[categoryName] || 0;
|
||||
const category = categories.find((cat) => cat.name === categoryName);
|
||||
if (!category || !category.scripts) return prev;
|
||||
|
||||
const totalLogos = category.scripts.length;
|
||||
const newIndex =
|
||||
direction === "prev"
|
||||
? (currentIndex - MAX_LOGOS + totalLogos) % totalLogos
|
||||
: (currentIndex + MAX_LOGOS) % totalLogos;
|
||||
|
||||
return { ...prev, [categoryName]: newIndex };
|
||||
});
|
||||
};
|
||||
|
||||
const truncateDescription = (text: string) => {
|
||||
return text.length > MAX_DESCRIPTION_LENGTH
|
||||
? `${text.slice(0, MAX_DESCRIPTION_LENGTH)}...`
|
||||
: text;
|
||||
};
|
||||
|
||||
const renderResources = (script: any) => {
|
||||
const cpu = script.install_methods[0]?.resources.cpu;
|
||||
const ram = script.install_methods[0]?.resources.ram;
|
||||
const hdd = script.install_methods[0]?.resources.hdd;
|
||||
|
||||
const resourceParts = [];
|
||||
if (cpu) resourceParts.push(<span key="cpu"><b>CPU:</b> {cpu}vCPU</span>);
|
||||
if (ram) resourceParts.push(<span key="ram"><b>RAM:</b> {ram}MB</span>);
|
||||
if (hdd) resourceParts.push(<span key="hdd"><b>HDD:</b> {hdd}GB</span>);
|
||||
|
||||
return resourceParts.length > 0 ? (
|
||||
<div className="text-sm text-gray-400">
|
||||
{resourceParts.map((part, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{part}
|
||||
{index < resourceParts.length - 1 && " | "}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 mt-20">
|
||||
{categories.length === 0 && (
|
||||
<p className="text-center text-gray-500">No categories available. Please check the API endpoint.</p>
|
||||
)}
|
||||
{selectedCategoryIndex !== null ? (
|
||||
<div>
|
||||
{/* Header with Navigation */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => navigateCategory("prev")}
|
||||
className="p-2 transition-transform duration-300 hover:scale-105"
|
||||
>
|
||||
<ChevronLeft className="h-6 w-6" />
|
||||
</Button>
|
||||
<h2 className="text-3xl font-semibold transition-opacity duration-300 hover:opacity-90">
|
||||
{categories[selectedCategoryIndex].name}
|
||||
</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => navigateCategory("next")}
|
||||
className="p-2 transition-transform duration-300 hover:scale-105"
|
||||
>
|
||||
<ChevronRight className="h-6 w-6" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Scripts Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||
{currentScripts
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((script) => (
|
||||
<Card
|
||||
key={script.name}
|
||||
className="p-4 cursor-pointer hover:shadow-md transition-shadow duration-300"
|
||||
onClick={() => handleScriptClick(script.slug)}
|
||||
>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<h3 className="text-lg font-bold script-text text-center hover:text-blue-600 transition-colors duration-300">
|
||||
{script.name}
|
||||
</h3>
|
||||
<img
|
||||
src={script.logo || defaultLogo}
|
||||
alt={script.name || "Script logo"}
|
||||
className="h-12 w-12 object-contain mx-auto"
|
||||
/>
|
||||
<p className="text-sm text-gray-500 text-center">
|
||||
<b>Created at:</b> {script.date_created || "No date available"}
|
||||
</p>
|
||||
<p
|
||||
className="text-sm text-gray-700 hover:text-gray-900 text-center transition-colors duration-300"
|
||||
title={script.description || "No description available."}
|
||||
>
|
||||
{truncateDescription(script.description || "No description available.")}
|
||||
</p>
|
||||
{renderResources(script)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Back to Categories Button */}
|
||||
<div className="mt-8 text-center">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={handleBackClick}
|
||||
className="px-6 py-2 text-white bg-blue-600 hover:bg-blue-700 rounded-lg shadow-md transition-transform duration-300 hover:scale-105"
|
||||
>
|
||||
Back to Categories
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{/* Categories Grid */}
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-semibold mb-4">Categories</h1>
|
||||
<p className="text-sm text-gray-500">
|
||||
{categories.reduce((total, category) => total + (category.scripts?.length || 0), 0)} Total scripts
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8">
|
||||
{categories.map((category, index) => (
|
||||
<Card
|
||||
key={category.name}
|
||||
onClick={() => handleCategoryClick(index)}
|
||||
className="cursor-pointer hover:shadow-lg flex flex-col items-center justify-center py-6 transition-shadow duration-300"
|
||||
>
|
||||
<CardContent className="flex flex-col items-center">
|
||||
<h3 className="text-xl font-bold mb-4 category-title transition-colors duration-300 hover:text-blue-600">
|
||||
{category.name}
|
||||
</h3>
|
||||
<div className="flex justify-center items-center gap-2 mb-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
switchLogos(category.name, "prev");
|
||||
}}
|
||||
className="p-1 transition-transform duration-300 hover:scale-110"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
{category.scripts &&
|
||||
category.scripts
|
||||
.slice(logoIndices[category.name] || 0, (logoIndices[category.name] || 0) + MAX_LOGOS)
|
||||
.map((script, i) => (
|
||||
<div key={i} className="flex flex-col items-center">
|
||||
<img
|
||||
src={script.logo || defaultLogo}
|
||||
alt={script.name || "Script logo"}
|
||||
title={script.name}
|
||||
className="h-8 w-8 object-contain cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleScriptClick(script.slug);
|
||||
}}
|
||||
/>
|
||||
{formattedBadge(script.type)}
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
switchLogos(category.name, "next");
|
||||
}}
|
||||
className="p-1 transition-transform duration-300 hover:scale-110"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 text-center">
|
||||
{(category as any).description || "No description available."}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryView;
|
||||
@@ -1,206 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import FilterComponent from "../../components/FilterComponent";
|
||||
|
||||
interface DataModel {
|
||||
status: string;
|
||||
type: string;
|
||||
nsapp: string;
|
||||
os_type: string;
|
||||
disk_size: number;
|
||||
core_count: number;
|
||||
ram_size: number;
|
||||
method: string;
|
||||
pve_version: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
const DataFetcher: React.FC = () => {
|
||||
const [data, setData] = useState<DataModel[]>([]);
|
||||
const [filteredData, setFilteredData] = useState<DataModel[]>([]);
|
||||
const [filters, setFilters] = useState<Record<string, any>>({});
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(25);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPaginatedData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`https://api.htl-braunau.at/data/paginated?page=${currentPage}&limit=${itemsPerPage === 0 ? '' : itemsPerPage}`);
|
||||
if (!response.ok) throw new Error(`Failed to fetch data: ${response.statusText}`);
|
||||
const result: DataModel[] = await response.json();
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPaginatedData();
|
||||
}, [currentPage, itemsPerPage]);
|
||||
|
||||
|
||||
|
||||
const applyFilters = async (column: string, operator: string, value: any) => {
|
||||
setFilters((prev) => {
|
||||
const updatedFilters = { ...prev };
|
||||
if (!updatedFilters[column]) updatedFilters[column] = [];
|
||||
|
||||
// Prevent duplicate filters
|
||||
const alreadyExists = updatedFilters[column].some((filter: { operator: string; value: any }) =>
|
||||
filter.operator === operator && filter.value === value
|
||||
);
|
||||
|
||||
if (!alreadyExists) {
|
||||
updatedFilters[column].push({ operator, value });
|
||||
}
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
};
|
||||
|
||||
const removeFilter = (column: string, index: number) => {
|
||||
setFilters((prev) => {
|
||||
const updatedFilters = { ...prev };
|
||||
updatedFilters[column] = updatedFilters[column].filter((_: any, i: number) => i !== index);
|
||||
|
||||
// If no filters remain, remove the column entry
|
||||
if (updatedFilters[column].length === 0) delete updatedFilters[column];
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
let filtered = [...data];
|
||||
|
||||
Object.keys(filters).forEach((key) => {
|
||||
if (!filters[key] || filters[key].length === 0) return;
|
||||
|
||||
filtered = filtered.filter((item) => {
|
||||
const itemValue = item[key as keyof DataModel];
|
||||
|
||||
return filters[key].some(({ operator, value }: { operator: string; value: any }) => {
|
||||
if (typeof itemValue === "number") {
|
||||
value = parseFloat(value);
|
||||
if (operator === "greater") return itemValue > value;
|
||||
if (operator === "greater or equal") return itemValue >= value;
|
||||
if (operator === "less") return itemValue < value;
|
||||
if (operator === "less or equal") return itemValue <= value;
|
||||
}
|
||||
|
||||
if (typeof itemValue === "string") {
|
||||
if (operator === "equals") return itemValue.toLowerCase() === value.toLowerCase();
|
||||
if (operator === "not equals") return itemValue.toLowerCase() !== value.toLowerCase();
|
||||
if (operator === "contains") return itemValue.toLowerCase().includes(value.toLowerCase());
|
||||
if (operator === "does not contain") return !itemValue.toLowerCase().includes(value.toLowerCase());
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
setFilteredData(filtered);
|
||||
}, [filters, data]);
|
||||
|
||||
const columns: { key: string; type: "text" | "number"; label: string }[] = [
|
||||
{ key: "status", type: "text", label: "Status" },
|
||||
{ key: "type", type: "text", label: "Type" },
|
||||
{ key: "nsapp", type: "text", label: "NS App" },
|
||||
{ key: "os_type", type: "text", label: "OS Type" },
|
||||
{ key: "disk_size", type: "number", label: "Disk Size" },
|
||||
{ key: "core_count", type: "number", label: "CPU Cores" },
|
||||
{ key: "ram_size", type: "number", label: "RAM Size" },
|
||||
{ key: "method", type: "text", label: "Method" },
|
||||
{ key: "pve_version", type: "text", label: "PVE Version" },
|
||||
{ key: "created_at", type: "text", label: "Created At" }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-6 mt-20">
|
||||
<h1 className="text-2xl font-bold mb-4 text-center">Created LXCs</h1>
|
||||
|
||||
<table className="min-w-full table-auto border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(({ key, type, label }) => (
|
||||
<th key={key} className="px-4 py-2 border-b text-left">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-semibold">{label}</span>
|
||||
<FilterComponent
|
||||
column={key}
|
||||
type={type}
|
||||
activeFilters={filters[key] || []}
|
||||
onApplyFilter={applyFilters}
|
||||
onRemoveFilter={removeFilter}
|
||||
allData={data}
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{/* Filters Row - Displays below headers */}
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(({ key, label }) => (
|
||||
<th key={key} className="px-4 py-2 border-b text-left">
|
||||
{filters[key] && filters[key].length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{filters[key].map((filter: { operator: string; value: any }, index: number) => (
|
||||
<div key={`${key}-${filter.value}-${index}`} className="bg-gray-800 text-white px-2 py-1 rounded flex items-center">
|
||||
<span className="text-sm italic">
|
||||
{filter.operator} <b>"{filter.value}"</b>
|
||||
</span>
|
||||
<button className="text-red-500 ml-2" onClick={() => removeFilter(key, index)}>
|
||||
✖
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-gray-500">—</span>
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
|
||||
<tbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td className="px-4 py-2 border-b">{item.status}</td>
|
||||
<td className="px-4 py-2 border-b">{item.type}</td>
|
||||
<td className="px-4 py-2 border-b">{item.nsapp}</td>
|
||||
<td className="px-4 py-2 border-b">{item.os_type}</td>
|
||||
<td className="px-4 py-2 border-b">{item.disk_size}</td>
|
||||
<td className="px-4 py-2 border-b">{item.core_count}</td>
|
||||
<td className="px-4 py-2 border-b">{item.ram_size}</td>
|
||||
<td className="px-4 py-2 border-b">{item.method}</td>
|
||||
<td className="px-4 py-2 border-b">{item.pve_version}</td>
|
||||
<td className="px-4 py-2 border-b">{item.created_at}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="px-4 py-2 text-center text-gray-500">
|
||||
No results found
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataFetcher;
|
||||
@@ -1,198 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { JSX, useEffect, useState } from "react";
|
||||
import DatePicker from 'react-datepicker';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import ApplicationChart from "../../components/ApplicationChart";
|
||||
|
||||
interface DataModel {
|
||||
id: number;
|
||||
ct_type: number;
|
||||
disk_size: number;
|
||||
core_count: number;
|
||||
ram_size: number;
|
||||
os_type: string;
|
||||
os_version: string;
|
||||
disableip6: string;
|
||||
nsapp: string;
|
||||
created_at: string;
|
||||
method: string;
|
||||
pve_version: string;
|
||||
status: string;
|
||||
error: string;
|
||||
type: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface SummaryData {
|
||||
total_entries: number;
|
||||
status_count: Record<string, number>;
|
||||
nsapp_count: Record<string, number>;
|
||||
}
|
||||
|
||||
const DataFetcher: React.FC = () => {
|
||||
const [data, setData] = useState<DataModel[]>([]);
|
||||
const [summary, setSummary] = useState<SummaryData | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(25);
|
||||
const [sortConfig, setSortConfig] = useState<{ key: string; direction: 'ascending' | 'descending' } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSummary = async () => {
|
||||
try {
|
||||
const response = await fetch("https://api.htl-braunau.at/dev/data/summary");
|
||||
if (!response.ok) throw new Error(`Failed to fetch summary: ${response.statusText}`);
|
||||
const result: SummaryData = await response.json();
|
||||
setSummary(result);
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
fetchSummary();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPaginatedData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`https://api.htl-braunau.at/dev/data/paginated?page=${currentPage}&limit=${itemsPerPage === 0 ? '' : itemsPerPage}`);
|
||||
if (!response.ok) throw new Error(`Failed to fetch data: ${response.statusText}`);
|
||||
const result: DataModel[] = await response.json();
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPaginatedData();
|
||||
}, [currentPage, itemsPerPage]);
|
||||
|
||||
const sortedData = React.useMemo(() => {
|
||||
if (!sortConfig) return data;
|
||||
const sorted = [...data].sort((a, b) => {
|
||||
if (a[sortConfig.key] < b[sortConfig.key]) {
|
||||
return sortConfig.direction === 'ascending' ? -1 : 1;
|
||||
}
|
||||
if (a[sortConfig.key] > b[sortConfig.key]) {
|
||||
return sortConfig.direction === 'ascending' ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return sorted;
|
||||
}, [data, sortConfig]);
|
||||
|
||||
if (loading) return <p>Loading...</p>;
|
||||
if (error) return <p>Error: {error}</p>;
|
||||
|
||||
const requestSort = (key: string) => {
|
||||
let direction: 'ascending' | 'descending' = 'ascending';
|
||||
if (sortConfig && sortConfig.key === key && sortConfig.direction === 'ascending') {
|
||||
direction = 'descending';
|
||||
}
|
||||
setSortConfig({ key, direction });
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string): string => {
|
||||
const date = new Date(dateString);
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const timezoneOffset = dateString.slice(-6);
|
||||
return `${day}.${month}.${year} ${hours}:${minutes} ${timezoneOffset} GMT`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 mt-20">
|
||||
<h1 className="text-2xl font-bold mb-4 text-center">Created LXCs</h1>
|
||||
{summary && <ApplicationChart data={Object.entries(summary.nsapp_count).map(([nsapp]) => ({ nsapp }))} />}
|
||||
<p className="text-lg font-bold mt-4"> </p>
|
||||
<div className="mb-4 flex justify-between items-center">
|
||||
<p className="text-lg font-bold">{summary?.total_entries} results found</p>
|
||||
<p className="text-lg font">Status Legend: 🔄 installing {summary?.status_count["installing"] ?? 0} | ✔️ completed {summary?.status_count["done"] ?? 0} | ❌ failed {summary?.status_count["failed"] ?? 0} | ❓ unknown</p>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-y-auto lg:overflow-y-visible">
|
||||
<table className="min-w-full table-auto border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('status')}>Status</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('type')}>Type</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('nsapp')}>Application</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('os_type')}>OS</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('os_version')}>OS Version</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('disk_size')}>Disk Size</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('core_count')}>Core Count</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('ram_size')}>RAM Size</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('method')}>Method</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('pve_version')}>PVE Version</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('error')}>Error Message</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('created_at')}>Created At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedData.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td className="px-4 py-2 border-b">
|
||||
{item.status === "done" ? (
|
||||
"✔️"
|
||||
) : item.status === "failed" ? (
|
||||
"❌"
|
||||
) : item.status === "installing" ? (
|
||||
"🔄"
|
||||
) : (
|
||||
item.status
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2 border-b">{item.type === "lxc" ? (
|
||||
"📦"
|
||||
) : item.type === "vm" ? (
|
||||
"🖥️"
|
||||
) : (
|
||||
item.type
|
||||
)}</td>
|
||||
<td className="px-4 py-2 border-b">{item.nsapp}</td>
|
||||
<td className="px-4 py-2 border-b">{item.os_type}</td>
|
||||
<td className="px-4 py-2 border-b">{item.os_version}</td>
|
||||
<td className="px-4 py-2 border-b">{item.disk_size}</td>
|
||||
<td className="px-4 py-2 border-b">{item.core_count}</td>
|
||||
<td className="px-4 py-2 border-b">{item.ram_size}</td>
|
||||
<td className="px-4 py-2 border-b">{item.method}</td>
|
||||
<td className="px-4 py-2 border-b">{item.pve_version}</td>
|
||||
<td className="px-4 py-2 border-b">{item.error}</td>
|
||||
<td className="px-4 py-2 border-b">{formatDate(item.created_at)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-between items-center">
|
||||
<button onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} disabled={currentPage === 1} className="p-2 border">Previous</button>
|
||||
<span>Page {currentPage}</span>
|
||||
<button onClick={() => setCurrentPage(prev => prev + 1)} className="p-2 border">Next</button>
|
||||
<select
|
||||
value={itemsPerPage}
|
||||
onChange={(e) => setItemsPerPage(Number(e.target.value))}
|
||||
className="p-2 border"
|
||||
>
|
||||
<option value={10}>10</option>
|
||||
<option value={20}>20</option>
|
||||
<option value={50}>50</option>
|
||||
<option value={100}>100</option>
|
||||
<option value={250}>250</option>
|
||||
<option value={500}>500</option>
|
||||
<option value={5000}>5000</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataFetcher;
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,117 +0,0 @@
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Category } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { z } from "zod";
|
||||
import { type Script } from "../_schemas/schemas";
|
||||
import { memo } from "react";
|
||||
|
||||
type CategoryProps = {
|
||||
script: Script;
|
||||
setScript: (script: Script) => void;
|
||||
setIsValid: (isValid: boolean) => void;
|
||||
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
||||
categories: Category[];
|
||||
};
|
||||
|
||||
const CategoryTag = memo(({
|
||||
category,
|
||||
onRemove
|
||||
}: {
|
||||
category: Category;
|
||||
onRemove: () => void;
|
||||
}) => (
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
{category.name}
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 inline-flex text-blue-400 hover:text-blue-600"
|
||||
onClick={onRemove}
|
||||
>
|
||||
<span className="sr-only">Remove</span>
|
||||
<svg
|
||||
className="h-3 w-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
));
|
||||
|
||||
CategoryTag.displayName = 'CategoryTag';
|
||||
|
||||
function Categories({
|
||||
script,
|
||||
setScript,
|
||||
categories,
|
||||
}: Omit<CategoryProps, "setIsValid" | "setZodErrors">) {
|
||||
const addCategory = (categoryId: number) => {
|
||||
setScript({
|
||||
...script,
|
||||
categories: [...new Set([...script.categories, categoryId])],
|
||||
});
|
||||
};
|
||||
|
||||
const removeCategory = (categoryId: number) => {
|
||||
setScript({
|
||||
...script,
|
||||
categories: script.categories.filter((id: number) => id !== categoryId),
|
||||
});
|
||||
};
|
||||
|
||||
const categoryMap = new Map(categories.map(c => [c.id, c]));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Label>
|
||||
Category <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Select onValueChange={(value) => addCategory(Number(value))}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map((category) => (
|
||||
<SelectItem key={category.id} value={category.id.toString()}>
|
||||
{category.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-wrap gap-2",
|
||||
script.categories.length !== 0 && "mt-2",
|
||||
)}
|
||||
>
|
||||
{script.categories.map((categoryId) => {
|
||||
const category = categoryMap.get(categoryId);
|
||||
return category ? (
|
||||
<CategoryTag
|
||||
key={categoryId}
|
||||
category={category}
|
||||
onRemove={() => removeCategory(categoryId)}
|
||||
/>
|
||||
) : null;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Categories);
|
||||
@@ -1,226 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { OperatingSystems } from "@/config/siteConfig";
|
||||
import { PlusCircle, Trash2 } from "lucide-react";
|
||||
import { memo, useCallback, useRef } from "react";
|
||||
import { z } from "zod";
|
||||
import { InstallMethodSchema, ScriptSchema, type Script } from "../_schemas/schemas";
|
||||
|
||||
type InstallMethodProps = {
|
||||
script: Script;
|
||||
setScript: (value: Script | ((prevState: Script) => Script)) => void;
|
||||
setIsValid: (isValid: boolean) => void;
|
||||
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
||||
};
|
||||
|
||||
function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallMethodProps) {
|
||||
const cpuRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
const ramRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
const hddRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
|
||||
const addInstallMethod = useCallback(() => {
|
||||
setScript((prev) => {
|
||||
const { type, slug } = prev;
|
||||
const newMethodType = "default";
|
||||
|
||||
let scriptPath = "";
|
||||
|
||||
if (type === "pve") {
|
||||
scriptPath = `tools/pve/${slug}.sh`;
|
||||
} else if (type === "addon") {
|
||||
scriptPath = `tools/addon/${slug}.sh`;
|
||||
} else {
|
||||
scriptPath = `${type}/${slug}.sh`;
|
||||
}
|
||||
|
||||
const method = InstallMethodSchema.parse({
|
||||
type: newMethodType,
|
||||
script: scriptPath,
|
||||
resources: {
|
||||
cpu: null,
|
||||
ram: null,
|
||||
hdd: null,
|
||||
os: null,
|
||||
version: null,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...prev,
|
||||
install_methods: [...prev.install_methods, method],
|
||||
};
|
||||
});
|
||||
}, [setScript]);
|
||||
|
||||
const updateInstallMethod = useCallback(
|
||||
(
|
||||
index: number,
|
||||
key: keyof Script["install_methods"][number],
|
||||
value: Script["install_methods"][number][keyof Script["install_methods"][number]],
|
||||
) => {
|
||||
setScript((prev) => {
|
||||
const updatedMethods = prev.install_methods.map((method, i) => {
|
||||
if (i === index) {
|
||||
const updatedMethod = { ...method, [key]: value };
|
||||
|
||||
if (key === "type") {
|
||||
updatedMethod.script =
|
||||
value === "alpine" ? `${prev.type}/alpine-${prev.slug}.sh` : `${prev.type}/${prev.slug}.sh`;
|
||||
|
||||
// Set OS to Alpine and reset version if type is alpine
|
||||
if (value === "alpine") {
|
||||
updatedMethod.resources.os = "Alpine";
|
||||
updatedMethod.resources.version = null;
|
||||
}
|
||||
}
|
||||
|
||||
return updatedMethod;
|
||||
}
|
||||
return method;
|
||||
});
|
||||
|
||||
const updated = {
|
||||
...prev,
|
||||
install_methods: updatedMethods,
|
||||
};
|
||||
|
||||
const result = ScriptSchema.safeParse(updated);
|
||||
setIsValid(result.success);
|
||||
if (!result.success) {
|
||||
setZodErrors(result.error);
|
||||
} else {
|
||||
setZodErrors(null);
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
},
|
||||
[setScript, setIsValid, setZodErrors],
|
||||
);
|
||||
|
||||
const removeInstallMethod = useCallback(
|
||||
(index: number) => {
|
||||
setScript((prev) => ({
|
||||
...prev,
|
||||
install_methods: prev.install_methods.filter((_, i) => i !== index),
|
||||
}));
|
||||
},
|
||||
[setScript],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="text-xl font-semibold">Install Methods</h3>
|
||||
{script.install_methods.map((method, index) => (
|
||||
<div key={index} className="space-y-2 border p-4 rounded">
|
||||
<Select value={method.type} onValueChange={(value) => updateInstallMethod(index, "type", value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="default">Default</SelectItem>
|
||||
<SelectItem value="alpine">Alpine</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
ref={(el) => {
|
||||
cpuRefs.current[index] = el;
|
||||
}}
|
||||
placeholder="CPU in Cores"
|
||||
type="number"
|
||||
value={method.resources.cpu || ""}
|
||||
onChange={(e) =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
cpu: e.target.value ? Number(e.target.value) : null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
ref={(el) => {
|
||||
ramRefs.current[index] = el;
|
||||
}}
|
||||
placeholder="RAM in MB"
|
||||
type="number"
|
||||
value={method.resources.ram || ""}
|
||||
onChange={(e) =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
ram: e.target.value ? Number(e.target.value) : null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
ref={(el) => {
|
||||
hddRefs.current[index] = el;
|
||||
}}
|
||||
placeholder="HDD in GB"
|
||||
type="number"
|
||||
value={method.resources.hdd || ""}
|
||||
onChange={(e) =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
hdd: e.target.value ? Number(e.target.value) : null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
value={method.resources.os || undefined}
|
||||
onValueChange={(value) =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
os: value || null,
|
||||
version: null, // Reset version when OS changes
|
||||
})
|
||||
}
|
||||
disabled={method.type === "alpine"}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="OS" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{OperatingSystems.map((os) => (
|
||||
<SelectItem key={os.name} value={os.name}>
|
||||
{os.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={method.resources.version || undefined}
|
||||
onValueChange={(value) =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
version: value || null,
|
||||
})
|
||||
}
|
||||
disabled={method.type === "alpine"}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Version" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{OperatingSystems.find((os) => os.name === method.resources.os)?.versions.map((version) => (
|
||||
<SelectItem key={version.slug} value={version.name}>
|
||||
{version.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button variant="destructive" size="sm" type="button" onClick={() => removeInstallMethod(index)}>
|
||||
<Trash2 className="mr-2 h-4 w-4" /> Remove Install Method
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button type="button" size="sm" disabled={script.install_methods.length >= 2} onClick={addInstallMethod}>
|
||||
<PlusCircle className="mr-2 h-4 w-4" /> Add Install Method
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(InstallMethod);
|
||||
@@ -1,150 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { AlertColors } from "@/config/siteConfig";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { PlusCircle, Trash2 } from "lucide-react";
|
||||
import { z } from "zod";
|
||||
import { ScriptSchema, type Script } from "../_schemas/schemas";
|
||||
import { memo, useCallback, useRef } from "react";
|
||||
|
||||
type NoteProps = {
|
||||
script: Script;
|
||||
setScript: (script: Script) => void;
|
||||
setIsValid: (isValid: boolean) => void;
|
||||
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
||||
};
|
||||
|
||||
function Note({
|
||||
script,
|
||||
setScript,
|
||||
setIsValid,
|
||||
setZodErrors,
|
||||
}: NoteProps) {
|
||||
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
|
||||
const addNote = useCallback(() => {
|
||||
setScript({
|
||||
...script,
|
||||
notes: [...script.notes, { text: "", type: "" }],
|
||||
});
|
||||
}, [script, setScript]);
|
||||
|
||||
const updateNote = useCallback((
|
||||
index: number,
|
||||
key: keyof Script["notes"][number],
|
||||
value: string,
|
||||
) => {
|
||||
const updated: Script = {
|
||||
...script,
|
||||
notes: script.notes.map((note, i) =>
|
||||
i === index ? { ...note, [key]: value } : note,
|
||||
),
|
||||
};
|
||||
const result = ScriptSchema.safeParse(updated);
|
||||
setIsValid(result.success);
|
||||
setZodErrors(result.success ? null : result.error);
|
||||
setScript(updated);
|
||||
// Restore focus after state update
|
||||
if (key === "text") {
|
||||
setTimeout(() => {
|
||||
inputRefs.current[index]?.focus();
|
||||
}, 0);
|
||||
}
|
||||
}, [script, setScript, setIsValid, setZodErrors]);
|
||||
|
||||
const removeNote = useCallback((index: number) => {
|
||||
setScript({
|
||||
...script,
|
||||
notes: script.notes.filter((_, i) => i !== index),
|
||||
});
|
||||
}, [script, setScript]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="text-xl font-semibold">Notes</h3>
|
||||
{script.notes.map((note, index) => (
|
||||
<NoteItem key={index} note={note} index={index} updateNote={updateNote} removeNote={removeNote} />
|
||||
))}
|
||||
<Button type="button" size="sm" onClick={addNote}>
|
||||
<PlusCircle className="mr-2 h-4 w-4" /> Add Note
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const NoteItem = memo(
|
||||
({
|
||||
note,
|
||||
index,
|
||||
updateNote,
|
||||
removeNote,
|
||||
}: {
|
||||
note: Script["notes"][number];
|
||||
index: number;
|
||||
updateNote: (index: number, key: keyof Script["notes"][number], value: string) => void;
|
||||
removeNote: (index: number) => void;
|
||||
}) => {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const handleTextChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateNote(index, "text", e.target.value);
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 0);
|
||||
}, [index, updateNote]);
|
||||
|
||||
return (
|
||||
<div className="space-y-2 border p-4 rounded">
|
||||
<Input
|
||||
placeholder="Note Text"
|
||||
value={note.text}
|
||||
onChange={handleTextChange}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<Select
|
||||
value={note.type}
|
||||
onValueChange={(value) => updateNote(index, "type", value)}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue placeholder="Type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.keys(AlertColors).map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
<span className="flex items-center gap-2">
|
||||
{type.charAt(0).toUpperCase() + type.slice(1)}{" "}
|
||||
<div
|
||||
className={cn(
|
||||
"size-4 rounded-full border",
|
||||
AlertColors[type as keyof typeof AlertColors],
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
type="button"
|
||||
onClick={() => removeNote(index)}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" /> Remove Note
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
NoteItem.displayName = 'NoteItem';
|
||||
|
||||
|
||||
export default memo(Note);
|
||||
@@ -1,46 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const InstallMethodSchema = z.object({
|
||||
type: z.enum(["default", "alpine"], {
|
||||
errorMap: () => ({ message: "Type must be either 'default' or 'alpine'" })
|
||||
}),
|
||||
script: z.string().min(1, "Script content cannot be empty"),
|
||||
resources: z.object({
|
||||
cpu: z.number().nullable(),
|
||||
ram: z.number().nullable(),
|
||||
hdd: z.number().nullable(),
|
||||
os: z.string().nullable(),
|
||||
version: z.string().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
const NoteSchema = z.object({
|
||||
text: z.string().min(1, "Note text cannot be empty"),
|
||||
type: z.string().min(1, "Note type cannot be empty"),
|
||||
});
|
||||
|
||||
export const ScriptSchema = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
slug: z.string().min(1, "Slug is required"),
|
||||
categories: z.array(z.number()),
|
||||
date_created: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format").min(1, "Date is required"),
|
||||
type: z.enum(["vm", "ct", "pve", "addon", "turnkey"], {
|
||||
errorMap: () => ({ message: "Type must be either 'vm', 'ct', 'pve', 'addon' or 'turnkey'" })
|
||||
}),
|
||||
updateable: z.boolean(),
|
||||
privileged: z.boolean(),
|
||||
interface_port: z.number().nullable(),
|
||||
documentation: z.string().nullable(),
|
||||
website: z.string().url().nullable(),
|
||||
logo: z.string().url().nullable(),
|
||||
description: z.string().min(1, "Description is required"),
|
||||
config_path: z.string(),
|
||||
install_methods: z.array(InstallMethodSchema).min(1, "At least one install method is required"),
|
||||
default_credentials: z.object({
|
||||
username: z.string().nullable(),
|
||||
password: z.string().nullable(),
|
||||
}),
|
||||
notes: z.array(NoteSchema),
|
||||
});
|
||||
|
||||
export type Script = z.infer<typeof ScriptSchema>;
|
||||
@@ -1,307 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { fetchCategories } from "@/lib/data";
|
||||
import { Category } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { format } from "date-fns";
|
||||
import { CalendarIcon, Check, Clipboard, Download } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import Categories from "./_components/Categories";
|
||||
import InstallMethod from "./_components/InstallMethod";
|
||||
import Note from "./_components/Note";
|
||||
import { ScriptSchema, type Script } from "./_schemas/schemas";
|
||||
|
||||
const initialScript: Script = {
|
||||
name: "",
|
||||
slug: "",
|
||||
categories: [],
|
||||
date_created: "",
|
||||
type: "ct",
|
||||
updateable: false,
|
||||
privileged: false,
|
||||
interface_port: null,
|
||||
documentation: null,
|
||||
config_path: "",
|
||||
website: null,
|
||||
logo: null,
|
||||
description: "",
|
||||
install_methods: [],
|
||||
default_credentials: {
|
||||
username: null,
|
||||
password: null,
|
||||
},
|
||||
notes: [],
|
||||
};
|
||||
|
||||
export default function JSONGenerator() {
|
||||
const [script, setScript] = useState<Script>(initialScript);
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const [isValid, setIsValid] = useState(false);
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [zodErrors, setZodErrors] = useState<z.ZodError | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCategories()
|
||||
.then(setCategories)
|
||||
.catch((error) => console.error("Error fetching categories:", error));
|
||||
}, []);
|
||||
|
||||
const updateScript = useCallback((key: keyof Script, value: Script[keyof Script]) => {
|
||||
setScript((prev) => {
|
||||
const updated = { ...prev, [key]: value };
|
||||
|
||||
if (updated.slug && updated.type) {
|
||||
updated.install_methods = updated.install_methods.map((method) => {
|
||||
let scriptPath = "";
|
||||
|
||||
if (updated.type === "pve") {
|
||||
scriptPath = `tools/pve/${updated.slug}.sh`;
|
||||
} else if (updated.type === "addon") {
|
||||
scriptPath = `tools/addon/${updated.slug}.sh`;
|
||||
} else if (method.type === "alpine") {
|
||||
scriptPath = `${updated.type}/alpine-${updated.slug}.sh`;
|
||||
} else {
|
||||
scriptPath = `${updated.type}/${updated.slug}.sh`;
|
||||
}
|
||||
|
||||
return {
|
||||
...method,
|
||||
script: scriptPath,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const result = ScriptSchema.safeParse(updated);
|
||||
setIsValid(result.success);
|
||||
setZodErrors(result.success ? null : result.error);
|
||||
return updated;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
navigator.clipboard.writeText(JSON.stringify(script, null, 2));
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
toast.success("Copied metadata to clipboard");
|
||||
}, [script]);
|
||||
|
||||
const handleDownload = useCallback(() => {
|
||||
const jsonString = JSON.stringify(script, null, 2);
|
||||
const blob = new Blob([jsonString], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `${script.slug || "script"}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
}, [script]);
|
||||
|
||||
const handleDateSelect = useCallback(
|
||||
(date: Date | undefined) => {
|
||||
updateScript("date_created", format(date || new Date(), "yyyy-MM-dd"));
|
||||
},
|
||||
[updateScript],
|
||||
);
|
||||
|
||||
const formattedDate = useMemo(
|
||||
() => (script.date_created ? format(script.date_created, "PPP") : undefined),
|
||||
[script.date_created],
|
||||
);
|
||||
|
||||
const validationAlert = useMemo(
|
||||
() => (
|
||||
<Alert className={cn("text-black", isValid ? "bg-green-100" : "bg-red-100")}>
|
||||
<AlertTitle>{isValid ? "Valid JSON" : "Invalid JSON"}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{isValid
|
||||
? "The current JSON is valid according to the schema."
|
||||
: "The current JSON does not match the required schema."}
|
||||
</AlertDescription>
|
||||
{zodErrors && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{zodErrors.errors.map((error, index) => (
|
||||
<AlertDescription key={index} className="p-1 text-red-500">
|
||||
{error.path.join(".")} - {error.message}
|
||||
</AlertDescription>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Alert>
|
||||
),
|
||||
[isValid, zodErrors],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen mt-20">
|
||||
<div className="w-1/2 p-4 overflow-y-auto">
|
||||
<h2 className="text-2xl font-bold mb-4">JSON Generator</h2>
|
||||
<form className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>
|
||||
Name <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input placeholder="Example" value={script.name} onChange={(e) => updateScript("name", e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>
|
||||
Slug <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input placeholder="example" value={script.slug} onChange={(e) => updateScript("slug", e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>
|
||||
Logo <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Full logo URL"
|
||||
value={script.logo || ""}
|
||||
onChange={(e) => updateScript("logo", e.target.value || null)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>
|
||||
Description <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
placeholder="Example"
|
||||
value={script.description}
|
||||
onChange={(e) => updateScript("description", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Config Path</Label>
|
||||
<Input
|
||||
placeholder="Path to config file"
|
||||
value={script.config_path || ""}
|
||||
onChange={(e) => updateScript("config_path", e.target.value || null)}
|
||||
/>
|
||||
</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</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild className="flex-1">
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className={cn("pl-3 text-left font-normal w-full", !script.date_created && "text-muted-foreground")}
|
||||
>
|
||||
{formattedDate || <span>Pick a date</span>}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={new Date(script.date_created)}
|
||||
onSelect={handleDateSelect}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<Label>Type</Label>
|
||||
<Select value={script.type} onValueChange={(value) => updateScript("type", value)}>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue placeholder="Type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ct">LXC Container</SelectItem>
|
||||
<SelectItem value="vm">Virtual Machine</SelectItem>
|
||||
<SelectItem value="pve">PVE-Tool</SelectItem>
|
||||
<SelectItem value="addon">Add-On</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</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)} />
|
||||
<label>Updateable</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch checked={script.privileged} onCheckedChange={(checked) => updateScript("privileged", checked)} />
|
||||
<label>Privileged</label>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
placeholder="Interface Port"
|
||||
type="number"
|
||||
value={script.interface_port || ""}
|
||||
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)}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Documentation URL"
|
||||
value={script.documentation || ""}
|
||||
onChange={(e) => updateScript("documentation", e.target.value || null)}
|
||||
/>
|
||||
</div>
|
||||
<InstallMethod script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
||||
<h3 className="text-xl font-semibold">Default Credentials</h3>
|
||||
<Input
|
||||
placeholder="Username"
|
||||
value={script.default_credentials.username || ""}
|
||||
onChange={(e) =>
|
||||
updateScript("default_credentials", {
|
||||
...script.default_credentials,
|
||||
username: e.target.value || null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Password"
|
||||
value={script.default_credentials.password || ""}
|
||||
onChange={(e) =>
|
||||
updateScript("default_credentials", {
|
||||
...script.default_credentials,
|
||||
password: e.target.value || null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Note script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
||||
</form>
|
||||
</div>
|
||||
<div className="w-1/2 p-4 bg-background overflow-y-auto">
|
||||
{validationAlert}
|
||||
<div className="relative">
|
||||
<div className="absolute right-2 top-2 flex gap-1">
|
||||
<Button size="icon" variant="outline" onClick={handleCopy}>
|
||||
{isCopied ? <Check className="h-4 w-4" /> : <Clipboard className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Button size="icon" variant="outline" onClick={handleDownload}>
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<pre className="mt-4 p-4 bg-secondary rounded shadow overflow-x-scroll">
|
||||
{JSON.stringify(script, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import Footer from "@/components/Footer";
|
||||
import Navbar from "@/components/Navbar";
|
||||
import QueryProvider from "@/components/query-provider"; // HINZUGEFÜGT
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { analytics, basePath } from "@/config/siteConfig";
|
||||
import "@/styles/globals.css";
|
||||
import { Inter } from "next/font/google";
|
||||
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
||||
import React from "react";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata = {
|
||||
title: "Proxmox VE Helper-Scripts DEVELOP",
|
||||
generator: "Next.js",
|
||||
applicationName: "Proxmox VE Helper-Scripts DEVELOP",
|
||||
referrer: "origin-when-cross-origin",
|
||||
keywords: [
|
||||
"Proxmox VE",
|
||||
"Helper-Scripts",
|
||||
"tteck",
|
||||
"helper",
|
||||
"scripts",
|
||||
"proxmox",
|
||||
"VE",
|
||||
"Development",
|
||||
],
|
||||
authors: { name: "Bram Suurd" },
|
||||
creator: "Bram Suurd",
|
||||
publisher: "Bram Suurd",
|
||||
description:
|
||||
"A Front-end for the Proxmox VE Helper-Scripts (DEVELOP) Repository. Featuring over 200+ scripts to help you manage your Proxmox VE environment.",
|
||||
favicon: "/app/favicon.ico",
|
||||
formatDetection: {
|
||||
email: false,
|
||||
address: false,
|
||||
telephone: false,
|
||||
},
|
||||
metadataBase: new URL(`https://community-scripts.github.io/${basePath}/`),
|
||||
openGraph: {
|
||||
title: "Proxmox VE Helper-Scripts DEVELOP",
|
||||
description:
|
||||
"A Front-end for the Proxmox VE Helper-Scripts (Community) Repository. Featuring over 200+ scripts to help you manage your Proxmox VE environment.",
|
||||
url: "/defaultimg.png",
|
||||
images: [
|
||||
{
|
||||
url: `https://community-scripts.github.io/${basePath}/defaultimg.png`,
|
||||
},
|
||||
],
|
||||
locale: "en_US",
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<script
|
||||
defer
|
||||
src={`https://${analytics.url}/script.js`}
|
||||
data-website-id={analytics.token}
|
||||
></script>
|
||||
<link rel="canonical" href={metadata.metadataBase.href} />
|
||||
<link rel="manifest" href="manifest.webmanifest" />
|
||||
<link rel="preconnect" href="https://api.github.com" />
|
||||
</head>
|
||||
<body className={inter.className}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<div className="flex w-full flex-col justify-center">
|
||||
<Navbar />
|
||||
<div className="flex min-h-screen flex-col justify-center">
|
||||
<div className="flex w-full justify-center">
|
||||
<div className="w-full max-w-7xl ">
|
||||
<QueryProvider>
|
||||
<NuqsAdapter>{children}</NuqsAdapter>
|
||||
</QueryProvider>
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export const generateStaticParams = () => {
|
||||
return [];
|
||||
};
|
||||
|
||||
export default function manifest(): MetadataRoute.Manifest {
|
||||
return {
|
||||
name: "Proxmox VE Helper-Scripts Development",
|
||||
short_name: "Proxmox VE Helper-Scripts Development",
|
||||
description:
|
||||
"A Re-designed Front-end for the Proxmox VE Helper-Scripts Repository. Featuring over 200+ scripts to help you manage your Proxmox VE environment.",
|
||||
theme_color: "#030712",
|
||||
background_color: "#030712",
|
||||
display: "standalone",
|
||||
orientation: "portrait",
|
||||
scope: `${basePath}`,
|
||||
start_url: `${basePath}`,
|
||||
icons: [
|
||||
{
|
||||
src: "logo.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export default function NotFoundPage() {
|
||||
return (
|
||||
<div className="flex h-screen w-full flex-col items-center justify-center gap-5 bg-background px-4 md:px-6">
|
||||
<div className="space-y-2 text-center">
|
||||
<h1 className="text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl">
|
||||
404
|
||||
</h1>
|
||||
<p className="text-muted-foreground md:text-xl">
|
||||
Oops, the page you are looking for could not be found.
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => window.history.back()} variant="secondary">
|
||||
Go Back
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
"use client";
|
||||
import FAQ from "@/components/FAQ";
|
||||
import AnimatedGradientText from "@/components/ui/animated-gradient-text";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardFooter } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import Particles from "@/components/ui/particles";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ArrowRightIcon, ExternalLink } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FaGithub } from "react-icons/fa";
|
||||
|
||||
function CustomArrowRightIcon() {
|
||||
return <ArrowRightIcon className="h-4 w-4" width={1} />;
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const { theme } = useTheme();
|
||||
|
||||
const [color, setColor] = useState("#000000");
|
||||
|
||||
useEffect(() => {
|
||||
setColor(theme === "dark" ? "#ffffff" : "#000000");
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full mt-16">
|
||||
<Particles className="absolute inset-0 -z-40" quantity={100} ease={80} color={color} refresh />
|
||||
<div className="container mx-auto">
|
||||
<div className="flex h-[80vh] flex-col items-center justify-center gap-4 py-20 lg:py-40">
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
<div>
|
||||
<AnimatedGradientText>
|
||||
<div
|
||||
className={cn(
|
||||
`absolute inset-0 block size-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] [border-radius:inherit] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]`,
|
||||
`p-px ![mask-composite:subtract]`,
|
||||
)}
|
||||
/>
|
||||
❤️ <Separator className="mx-2 h-4" orientation="vertical" />
|
||||
<span
|
||||
className={cn(
|
||||
`animate-gradient bg-gradient-to-r from-[#ffaa40] via-[#9c40ff] to-[#ffaa40] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`,
|
||||
`inline`,
|
||||
)}
|
||||
>
|
||||
Scripts by tteck
|
||||
</span>
|
||||
</AnimatedGradientText>
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Thank You!</DialogTitle>
|
||||
<DialogDescription>
|
||||
A big thank you to tteck and the many contributors who have made this project possible. Your hard
|
||||
work is truly appreciated by the entire Proxmox community!
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<CardFooter className="flex flex-col gap-2">
|
||||
<Button className="w-full" variant="outline" asChild>
|
||||
<a
|
||||
href="https://github.com/tteck"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<FaGithub className="mr-2 h-4 w-4" /> Tteck's GitHub
|
||||
</a>
|
||||
</Button>
|
||||
<Button className="w-full" asChild>
|
||||
<a
|
||||
href={`https://github.com/community-scripts/${basePath}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<ExternalLink className="mr-2 h-4 w-4" /> Proxmox Helper Scripts
|
||||
</a>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<h1 className="max-w-2xl text-center text-3xl font-semibold tracking-tighter md:text-7xl">
|
||||
Make managing your Homelab a breeze
|
||||
</h1>
|
||||
<div className="max-w-2xl gap-2 flex flex-col text-center sm:text-lg text-sm leading-relaxed tracking-tight text-muted-foreground md:text-xl">
|
||||
<p>
|
||||
We are a community-driven initiative that simplifies the setup of Proxmox Virtual Environment (VE).
|
||||
</p>
|
||||
<p>
|
||||
With 300+ scripts to help you manage your <b>Proxmox VE environment</b>. Whether you're a seasoned
|
||||
user or a newcomer, we've got you covered.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-3">
|
||||
<Link href="/scripts">
|
||||
<Button
|
||||
size="lg"
|
||||
variant="expandIcon"
|
||||
Icon={CustomArrowRightIcon}
|
||||
iconPlacement="right"
|
||||
className="hover:"
|
||||
>
|
||||
View Scripts
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<div className="py-20" id="faq">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl font-bold tracking-tighter md:text-5xl mb-4">Frequently Asked Questions</h2>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
Find answers to common questions about our Proxmox VE scripts
|
||||
</p>
|
||||
</div>
|
||||
<FAQ />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { fetchCategories } from "@/lib/data";
|
||||
import { Category, Script } from "@/lib/types";
|
||||
import { ScriptItem } from "@/app/scripts/_components/ScriptItem";
|
||||
import { Loader2, RefreshCw } from "lucide-react";
|
||||
|
||||
function getRandomScript(categories: Category[]): Script | null {
|
||||
const allScripts = categories.flatMap((cat) => cat.scripts || []);
|
||||
if (allScripts.length === 0) return null;
|
||||
const idx = Math.floor(Math.random() * allScripts.length);
|
||||
return allScripts[idx];
|
||||
}
|
||||
|
||||
export default function RandomScriptPage() {
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [randomScript, setRandomScript] = useState<Script | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Fetch categories/scripts on mount
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
fetchCategories()
|
||||
.then((cats) => {
|
||||
setCategories(cats);
|
||||
setRandomScript(getRandomScript(cats));
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
// Handler to re-roll a new random script
|
||||
const reroll = () => {
|
||||
setRandomScript(getRandomScript(categories));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<div className="mt-20 flex flex-col items-center sm:px-4 xl:px-0">
|
||||
<div className="w-full max-w-5xl flex flex-col items-center">
|
||||
<div className="w-full flex justify-between items-center mb-6">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Random Script</h1>
|
||||
<button
|
||||
onClick={reroll}
|
||||
className="flex items-center gap-2 rounded-lg bg-accent/30 px-4 py-2 text-base font-medium hover:bg-accent/50 transition-colors"
|
||||
title="Pick another random script"
|
||||
disabled={loading || categories.length === 0}
|
||||
>
|
||||
<RefreshCw className="h-5 w-5" />
|
||||
Re-Roll
|
||||
</button>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="flex flex-col items-center justify-center w-full h-64">
|
||||
<Loader2 className="h-10 w-10 animate-spin" />
|
||||
</div>
|
||||
) : randomScript ? (
|
||||
<ScriptItem item={randomScript} setSelectedScript={() => { }} />
|
||||
) : (
|
||||
<div className="text-center text-muted-foreground">
|
||||
No scripts available.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: {
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
},
|
||||
sitemap: `https://community-scripts.github.io/${basePath}/sitemap.xml`,
|
||||
};
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { CPUIcon, HDDIcon, RAMIcon } from "@/components/icons/resource-icons";
|
||||
import { getDisplayValueFromRAM } from "@/lib/utils/resource-utils";
|
||||
|
||||
interface ResourceDisplayProps {
|
||||
title: string;
|
||||
cpu: number | null;
|
||||
ram: number | null;
|
||||
hdd: number | null;
|
||||
}
|
||||
|
||||
interface IconTextProps {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
}
|
||||
|
||||
function IconText({ icon, label }: IconTextProps) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 rounded-md bg-accent/20 px-2 py-1 text-sm">
|
||||
{icon}
|
||||
<span className="text-foreground/90">{label}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResourceDisplay({ title, cpu, ram, hdd }: ResourceDisplayProps) {
|
||||
const hasCPU = typeof cpu === "number" && cpu > 0;
|
||||
const hasRAM = typeof ram === "number" && ram > 0;
|
||||
const hasHDD = typeof hdd === "number" && hdd > 0;
|
||||
|
||||
if (!hasCPU && !hasRAM && !hasHDD) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm font-medium text-muted-foreground">{title}</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{hasCPU && <IconText icon={<CPUIcon />} label={`${cpu} vCPU`} />}
|
||||
{hasRAM && <IconText icon={<RAMIcon />} label={getDisplayValueFromRAM(ram!)} />}
|
||||
{hasHDD && <IconText icon={<HDDIcon />} label={`${hdd} GB`} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
import { formattedBadge } from "@/components/CommandMenu";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Category } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
|
||||
export default function ScriptAccordion({
|
||||
items,
|
||||
selectedScript,
|
||||
setSelectedScript,
|
||||
}: {
|
||||
items: Category[];
|
||||
selectedScript: string | null;
|
||||
setSelectedScript: (script: string | null) => void;
|
||||
}) {
|
||||
const [expandedItem, setExpandedItem] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const linkRefs = useRef<{ [key: string]: HTMLAnchorElement | null }>({});
|
||||
|
||||
const handleAccordionChange = (value: string | undefined) => {
|
||||
setExpandedItem(value);
|
||||
};
|
||||
|
||||
const handleSelected = useCallback(
|
||||
(slug: string) => {
|
||||
setSelectedScript(slug);
|
||||
},
|
||||
[setSelectedScript],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedScript) {
|
||||
const category = items.find((category) =>
|
||||
category.scripts.some((script) => script.slug === selectedScript),
|
||||
);
|
||||
if (category) {
|
||||
setExpandedItem(category.name);
|
||||
handleSelected(selectedScript);
|
||||
}
|
||||
}
|
||||
}, [selectedScript, items, handleSelected]);
|
||||
return (
|
||||
<Accordion
|
||||
type="single"
|
||||
value={expandedItem}
|
||||
onValueChange={handleAccordionChange}
|
||||
collapsible
|
||||
className="overflow-y-scroll max-h-[calc(100vh-225px)] overflow-x-hidden mt-3 p-2"
|
||||
>
|
||||
{items.map((category) => (
|
||||
<AccordionItem
|
||||
key={category.id + ":category"}
|
||||
value={category.name}
|
||||
className={cn("sm:text-md flex flex-col border-none", {
|
||||
"rounded-lg bg-accent/30": expandedItem === category.name,
|
||||
})}
|
||||
>
|
||||
<AccordionTrigger
|
||||
className={cn(
|
||||
"duration-250 rounded-lg transition ease-in-out hover:-translate-y-1 hover:scale-105 hover:bg-accent",
|
||||
)}
|
||||
>
|
||||
<div className="mr-2 flex w-full items-center justify-between">
|
||||
<span className="pl-2 text-left">{category.name} </span>
|
||||
<span className="rounded-full bg-gray-200 px-2 py-1 text-xs text-muted-foreground hover:no-underline dark:bg-blue-800/20">
|
||||
{category.scripts.length}
|
||||
</span>
|
||||
</div>{" "}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent
|
||||
data-state={expandedItem === category.name ? "open" : "closed"}
|
||||
className="pt-0"
|
||||
>
|
||||
{category.scripts
|
||||
.slice()
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((script, index) => (
|
||||
<div key={index}>
|
||||
<Link
|
||||
href={{
|
||||
pathname: "/scripts",
|
||||
query: { id: script.slug },
|
||||
}}
|
||||
prefetch={false}
|
||||
className={`flex cursor-pointer items-center justify-between gap-1 px-1 py-1 text-muted-foreground hover:rounded-lg hover:bg-accent/60 hover:dark:bg-accent/20 ${
|
||||
selectedScript === script.slug
|
||||
? "rounded-lg bg-accent font-semibold dark:bg-accent/30 dark:text-white"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => handleSelected(script.slug)}
|
||||
ref={(el) => {
|
||||
linkRefs.current[script.slug] = el;
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Image
|
||||
src={script.logo || `/${basePath}/logo.png`}
|
||||
height={16}
|
||||
width={16}
|
||||
unoptimized
|
||||
onError={(e) =>
|
||||
((e.currentTarget as HTMLImageElement).src =
|
||||
`/${basePath}/logo.png`)
|
||||
}
|
||||
alt={script.name}
|
||||
className="mr-1 w-4 h-4 rounded-full"
|
||||
/>
|
||||
<span className="flex items-center gap-2">
|
||||
{script.name}
|
||||
</span>
|
||||
</div>
|
||||
{formattedBadge(script.type)}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { basePath, mostPopularScripts } from "@/config/siteConfig";
|
||||
import { extractDate } from "@/lib/time";
|
||||
import { Category, Script } from "@/lib/types";
|
||||
import { CalendarPlus } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
const ITEMS_PER_PAGE = 3;
|
||||
|
||||
export const getDisplayValueFromType = (type: string) => {
|
||||
switch (type) {
|
||||
case "ct":
|
||||
return "LXC";
|
||||
case "vm":
|
||||
return "VM";
|
||||
case "pve":
|
||||
case "addon":
|
||||
return "";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
export function LatestScripts({ items }: { items: Category[] }) {
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
const latestScripts = useMemo(() => {
|
||||
if (!items) return [];
|
||||
|
||||
const scripts = items.flatMap((category) => category.scripts || []);
|
||||
|
||||
// Filter out duplicates by slug
|
||||
const uniqueScriptsMap = new Map<string, Script>();
|
||||
scripts.forEach((script) => {
|
||||
if (!uniqueScriptsMap.has(script.slug)) {
|
||||
uniqueScriptsMap.set(script.slug, script);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(uniqueScriptsMap.values()).sort(
|
||||
(a, b) => new Date(b.date_created).getTime() - new Date(a.date_created).getTime(),
|
||||
);
|
||||
}, [items]);
|
||||
|
||||
const goToNextPage = () => {
|
||||
setPage((prevPage) => prevPage + 1);
|
||||
};
|
||||
|
||||
const goToPreviousPage = () => {
|
||||
setPage((prevPage) => prevPage - 1);
|
||||
};
|
||||
|
||||
const startIndex = (page - 1) * ITEMS_PER_PAGE;
|
||||
const endIndex = page * ITEMS_PER_PAGE;
|
||||
|
||||
if (!items) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
{latestScripts.length > 0 && (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<h2 className="text-lg font-semibold">Newest Scripts</h2>
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
{page > 1 && (
|
||||
<div className="cursor-pointer select-none p-2 text-sm font-semibold" onClick={goToPreviousPage}>
|
||||
Previous
|
||||
</div>
|
||||
)}
|
||||
{endIndex < latestScripts.length && (
|
||||
<div onClick={goToNextPage} className="cursor-pointer select-none p-2 text-sm font-semibold">
|
||||
{page === 1 ? "More.." : "Next"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w flex w-full flex-row flex-wrap gap-4">
|
||||
{latestScripts.slice(startIndex, endIndex).map((script) => (
|
||||
<Card key={script.slug} className="min-w-[250px] flex-1 flex-grow bg-accent/30">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-3">
|
||||
<div className="flex h-16 w-16 min-w-16 items-center justify-center rounded-lg bg-accent p-1">
|
||||
<Image
|
||||
src={script.logo || `/${basePath}/logo.png`}
|
||||
unoptimized
|
||||
height={64}
|
||||
width={64}
|
||||
alt=""
|
||||
onError={(e) => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
||||
className="h-11 w-11 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-lg line-clamp-1">
|
||||
{script.name} {getDisplayValueFromType(script.type)}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
||||
<CalendarPlus className="h-4 w-4" />
|
||||
{extractDate(script.date_created)}
|
||||
</p>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription className="line-clamp-3 text-card-foreground">{script.description}</CardDescription>
|
||||
</CardContent>
|
||||
<CardFooter className="">
|
||||
<Button asChild variant="outline">
|
||||
<Link
|
||||
href={{
|
||||
pathname: "/scripts",
|
||||
query: { id: script.slug },
|
||||
}}
|
||||
>
|
||||
View Script
|
||||
</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MostViewedScripts({ items }: { items: Category[] }) {
|
||||
const mostViewedScripts = items.reduce((acc: Script[], category) => {
|
||||
const foundScripts = category.scripts.filter((script) => mostPopularScripts.includes(script.slug));
|
||||
return acc.concat(foundScripts);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
{mostViewedScripts.length > 0 && (
|
||||
<>
|
||||
<h2 className="text-lg font-semibold">Most Viewed Scripts</h2>
|
||||
</>
|
||||
)}
|
||||
<div className="min-w flex w-full flex-row flex-wrap gap-4">
|
||||
{mostViewedScripts.map((script) => (
|
||||
<Card key={script.slug} className="min-w-[250px] flex-1 flex-grow bg-accent/30">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-3">
|
||||
<div className="flex size-16 min-w-16 items-center justify-center rounded-lg bg-accent p-1">
|
||||
<Image
|
||||
unoptimized
|
||||
src={script.logo || `/${basePath}/logo.png`}
|
||||
height={64}
|
||||
width={64}
|
||||
alt=""
|
||||
onError={(e) => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
||||
className="h-11 w-11 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="line-clamp-1 text-lg">
|
||||
{script.name} {getDisplayValueFromType(script.type)}
|
||||
</p>
|
||||
<p className="flex items-center gap-1 text-sm text-muted-foreground">
|
||||
<CalendarPlus className="h-4 w-4" />
|
||||
{extractDate(script.date_created)}
|
||||
</p>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription className="line-clamp-3 text-card-foreground break-words">
|
||||
{script.description}
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
<CardFooter className="">
|
||||
<Button asChild variant="outline">
|
||||
<Link
|
||||
href={{
|
||||
pathname: "/scripts",
|
||||
query: { id: script.slug },
|
||||
}}
|
||||
prefetch={false}
|
||||
>
|
||||
View Script
|
||||
</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { extractDate } from "@/lib/time";
|
||||
import { AppVersion, Script } from "@/lib/types";
|
||||
|
||||
import { X } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { useVersions } from "@/hooks/useVersions";
|
||||
import { cleanSlug } from "@/lib/utils/resource-utils";
|
||||
import { Suspense } from "react";
|
||||
import { ResourceDisplay } from "./ResourceDisplay";
|
||||
import { getDisplayValueFromType } from "./ScriptInfoBlocks";
|
||||
import Alerts from "./ScriptItems/Alerts";
|
||||
import Buttons from "./ScriptItems/Buttons";
|
||||
import DefaultPassword from "./ScriptItems/DefaultPassword";
|
||||
import Description from "./ScriptItems/Description";
|
||||
import InstallCommand from "./ScriptItems/InstallCommand";
|
||||
import ConfigFile from "./ScriptItems/ConfigFile";
|
||||
import InterFaces from "./ScriptItems/InterFaces";
|
||||
import Tooltips from "./ScriptItems/Tooltips";
|
||||
|
||||
|
||||
interface ScriptItemProps {
|
||||
item: Script;
|
||||
setSelectedScript: (script: string | null) => void;
|
||||
}
|
||||
|
||||
function ScriptHeader({ item }: { item: Script }) {
|
||||
const defaultInstallMethod = item.install_methods?.[0];
|
||||
const os = defaultInstallMethod?.resources?.os || "Proxmox Node";
|
||||
const version = defaultInstallMethod?.resources?.version || "";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row gap-6 w-full">
|
||||
<div className="flex flex-col md:flex-row gap-6 flex-grow">
|
||||
<div className="flex-shrink-0">
|
||||
<Image
|
||||
className="h-32 w-32 rounded-xl bg-gradient-to-br from-accent/40 to-accent/60 object-contain p-3 shadow-lg transition-transform hover:scale-105"
|
||||
src={item.logo || `/${basePath}/logo.png`}
|
||||
width={400}
|
||||
onError={(e) => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
||||
height={400}
|
||||
alt={item.name}
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col justify-between flex-grow space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight flex items-center gap-2">
|
||||
{item.name}
|
||||
<VersionInfo item={item} />
|
||||
<span className="inline-flex items-center rounded-md bg-accent/30 px-2 py-1 text-sm">
|
||||
{getDisplayValueFromType(item.type)}
|
||||
</span>
|
||||
</h1>
|
||||
<div className="mt-1 flex items-center gap-3 text-sm text-muted-foreground">
|
||||
<span>Added {extractDate(item.date_created)}</span>
|
||||
<span>•</span>
|
||||
<span className=" capitalize">
|
||||
{os} {version}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* <VersionInfo item={item} /> */}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 text-sm text-muted-foreground">
|
||||
{defaultInstallMethod?.resources && (
|
||||
<ResourceDisplay
|
||||
title="Default"
|
||||
cpu={defaultInstallMethod.resources.cpu}
|
||||
ram={defaultInstallMethod.resources.ram}
|
||||
hdd={defaultInstallMethod.resources.hdd}
|
||||
/>
|
||||
)}
|
||||
{item.install_methods.find((method) => method.type === "alpine")?.resources && (
|
||||
<ResourceDisplay
|
||||
title="Alpine"
|
||||
{...item.install_methods.find((method) => method.type === "alpine")!.resources!}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 justify-between">
|
||||
<InterFaces item={item} />
|
||||
<div className="flex justify-end">
|
||||
<Buttons item={item} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function VersionInfo({ item }: { item: Script }) {
|
||||
const { data: versions = [], isLoading } = useVersions();
|
||||
|
||||
if (isLoading || versions.length === 0) {
|
||||
return <p className="text-sm text-muted-foreground">Loading versions...</p>;
|
||||
}
|
||||
|
||||
const matchedVersion = versions.find((v: AppVersion) => {
|
||||
const cleanName = v.name.replace(/[^a-z0-9]/gi, "").toLowerCase();
|
||||
return cleanName === cleanSlug(item.slug) || cleanName.includes(cleanSlug(item.slug));
|
||||
});
|
||||
|
||||
if (!matchedVersion) return null;
|
||||
|
||||
return <span className="font-medium text-sm">{matchedVersion.version}</span>;
|
||||
}
|
||||
|
||||
export function ScriptItem({ item, setSelectedScript }: ScriptItemProps) {
|
||||
const closeScript = () => {
|
||||
window.history.pushState({}, document.title, window.location.pathname);
|
||||
setSelectedScript(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-5xl mx-auto">
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 className="text-2xl font-semibold tracking-tight text-foreground/90">Selected Script</h2>
|
||||
<button
|
||||
onClick={closeScript}
|
||||
className="rounded-full p-2 text-muted-foreground hover:bg-card/50 transition-colors"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-border/40 bg-gradient-to-b from-card/30 to-background/50 backdrop-blur-sm shadow-sm">
|
||||
<div className="p-6 space-y-6">
|
||||
<Suspense fallback={<div className="animate-pulse h-32 bg-accent/20 rounded-xl" />}>
|
||||
<ScriptHeader item={item} />
|
||||
</Suspense>
|
||||
|
||||
<Description item={item} />
|
||||
<Alerts item={item} />
|
||||
|
||||
<div className="mt-4 rounded-lg border shadow-sm">
|
||||
|
||||
<div className="flex gap-3 px-4 py-2 bg-accent/25">
|
||||
<h2 className="text-lg font-semibold">
|
||||
How to {item.type === "pve" ? "use" : item.type === "addon" ? "apply" : "install"}
|
||||
</h2>
|
||||
<Tooltips item={item} />
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="">
|
||||
<InstallCommand item={item} />
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex gap-3 px-4 py-2 bg-accent/25">
|
||||
<h2 className="text-lg font-semibold">
|
||||
Location of config file
|
||||
</h2>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="">
|
||||
<ConfigFile item={item} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<DefaultPassword item={item} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import TextCopyBlock from "@/components/TextCopyBlock";
|
||||
import { AlertColors } from "@/config/siteConfig";
|
||||
import { Script } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AlertCircle, NotepadText } from "lucide-react";
|
||||
|
||||
type NoteProps = {
|
||||
text: string;
|
||||
type: keyof typeof AlertColors;
|
||||
}
|
||||
|
||||
export default function Alerts({ item }: { item: Script }) {
|
||||
return (
|
||||
<>
|
||||
{item?.notes?.length > 0 &&
|
||||
item.notes.map((note: NoteProps, index: number) => (
|
||||
<div key={index} className="mt-4 flex flex-col shadow-sm gap-2">
|
||||
<p
|
||||
className={cn(
|
||||
"inline-flex items-center gap-2 rounded-lg border p-2 pl-4 text-sm",
|
||||
AlertColors[note.type],
|
||||
)}
|
||||
>
|
||||
{note.type == "info" ? (
|
||||
<NotepadText className="h-4 min-h-4 w-4 min-w-4" />
|
||||
) : (
|
||||
<AlertCircle className="h-4 min-h-4 w-4 min-w-4" />
|
||||
)}
|
||||
<span>{TextCopyBlock(note.text)}</span>
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { Script } from "@/lib/types";
|
||||
import { BookOpenText, Code, Globe, LinkIcon, RefreshCcw } from "lucide-react";
|
||||
|
||||
const generateInstallSourceUrl = (slug: string) => {
|
||||
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
|
||||
return `${baseUrl}/install/${slug}-install.sh`;
|
||||
};
|
||||
|
||||
const generateSourceUrl = (slug: string, type: string) => {
|
||||
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
|
||||
|
||||
switch (type) {
|
||||
case "vm":
|
||||
return `${baseUrl}/vm/${slug}.sh`;
|
||||
case "pve":
|
||||
return `${baseUrl}/tools/pve/${slug}.sh`;
|
||||
case "addon":
|
||||
return `${baseUrl}/tools/addon/${slug}.sh`;
|
||||
default:
|
||||
return `${baseUrl}/ct/${slug}.sh`; // fallback for "ct"
|
||||
}
|
||||
};
|
||||
|
||||
const generateUpdateUrl = (slug: string) => {
|
||||
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
|
||||
return `${baseUrl}/ct/${slug}.sh`;
|
||||
};
|
||||
|
||||
interface LinkItem {
|
||||
href: string;
|
||||
icon: React.ReactNode;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export default function Buttons({ item }: { item: Script }) {
|
||||
const isCtOrDefault = ["ct"].includes(item.type);
|
||||
const installSourceUrl = isCtOrDefault ? generateInstallSourceUrl(item.slug) : null;
|
||||
const updateSourceUrl = isCtOrDefault ? generateUpdateUrl(item.slug) : null;
|
||||
const sourceUrl = !isCtOrDefault ? generateSourceUrl(item.slug, item.type) : null;
|
||||
|
||||
const links = [
|
||||
item.website && {
|
||||
href: item.website,
|
||||
icon: <Globe className="h-4 w-4" />,
|
||||
text: "Website",
|
||||
},
|
||||
item.documentation && {
|
||||
href: item.documentation,
|
||||
icon: <BookOpenText className="h-4 w-4" />,
|
||||
text: "Documentation",
|
||||
},
|
||||
installSourceUrl && {
|
||||
href: installSourceUrl,
|
||||
icon: <Code className="h-4 w-4" />,
|
||||
text: "Install Source",
|
||||
},
|
||||
updateSourceUrl && {
|
||||
href: updateSourceUrl,
|
||||
icon: <RefreshCcw className="h-4 w-4" />,
|
||||
text: "Update Source",
|
||||
},
|
||||
sourceUrl && {
|
||||
href: sourceUrl,
|
||||
icon: <Code className="h-4 w-4" />,
|
||||
text: "Source Code",
|
||||
},
|
||||
].filter(Boolean) as LinkItem[];
|
||||
|
||||
if (links.length === 0) return null;
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="flex items-center gap-2">
|
||||
<LinkIcon className="size-4" />
|
||||
Links
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{links.map((link, index) => (
|
||||
<DropdownMenuItem key={index} asChild>
|
||||
<a href={link.href} target="_blank" rel="noopener noreferrer" className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground size-4">{link.icon}</span>
|
||||
<span>{link.text}</span>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import ConfigCopyButton from "@/components/ui/config-copy-button";
|
||||
import { Script } from "@/lib/types";
|
||||
|
||||
export default function ConfigFile({ item }: { item: Script }) {
|
||||
return (
|
||||
<div className="px-4 pb-4">
|
||||
<ConfigCopyButton>{item.config_path ? item.config_path : "No config path set"}</ConfigCopyButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import handleCopy from "@/components/handleCopy";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Script } from "@/lib/types";
|
||||
|
||||
export default function DefaultPassword({ item }: { item: Script }) {
|
||||
const { username, password } = item.default_credentials;
|
||||
const hasDefaultLogin = username && password;
|
||||
|
||||
if (!hasDefaultLogin) return null;
|
||||
|
||||
const copyCredential = (type: "username" | "password") => {
|
||||
handleCopy(type, item.default_credentials[type] ?? "");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4 rounded-lg border shadow-sm">
|
||||
<div className="flex gap-3 px-4 py-2 bg-accent/25">
|
||||
<h2 className="text-lg font-semibold">Default Login Credentials</h2>
|
||||
</div>
|
||||
<Separator className="w-full" />
|
||||
<div className="flex flex-col gap-2 p-4">
|
||||
<p className="mb-2 text-sm">
|
||||
You can use the following credentials to login to the {item.name} {item.type}.
|
||||
</p>
|
||||
{["username", "password"].map((type) => (
|
||||
<div key={type} className="text-sm">
|
||||
{type.charAt(0).toUpperCase() + type.slice(1)}:{" "}
|
||||
<Button variant="secondary" size="null" onClick={() => copyCredential(type as "username" | "password")}>
|
||||
{item.default_credentials[type as "username" | "password"]}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Script } from "@/lib/types";
|
||||
|
||||
export default function DefaultSettings({ item }: { item: Script }) {
|
||||
const getDisplayValueFromRAM = (ram: number) => (ram >= 1024 ? `${Math.floor(ram / 1024)}GB` : `${ram}MB`);
|
||||
|
||||
const ResourceDisplay = ({ settings, title }: { settings: (typeof item.install_methods)[0]; title: string }) => {
|
||||
const { cpu, ram, hdd } = settings.resources;
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-md font-semibold">{title}</h2>
|
||||
<p className="text-sm text-muted-foreground">CPU: {cpu}vCPU</p>
|
||||
<p className="text-sm text-muted-foreground">RAM: {getDisplayValueFromRAM(ram ?? 0)}</p>
|
||||
<p className="text-sm text-muted-foreground">HDD: {hdd}GB</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const defaultSettings = item.install_methods.find((method) => method.type === "default");
|
||||
const defaultAlpineSettings = item.install_methods.find((method) => method.type === "alpine");
|
||||
|
||||
const hasDefaultSettings = defaultSettings?.resources && Object.values(defaultSettings.resources).some(Boolean);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 flex-col flex">
|
||||
{hasDefaultSettings && <ResourceDisplay settings={defaultSettings} title="Default settings" />}
|
||||
{defaultAlpineSettings && <ResourceDisplay settings={defaultAlpineSettings} title="Default Alpine settings" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import TextCopyBlock from "@/components/TextCopyBlock";
|
||||
import { Script } from "@/lib/types";
|
||||
|
||||
export default function Description({ item }: { item: Script }) {
|
||||
return (
|
||||
<div className="p-2">
|
||||
<h2 className="mb-2 max-w-prose text-lg font-semibold">Description</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{TextCopyBlock(item.description)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import CodeCopyButton from "@/components/ui/code-copy-button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { Script } from "@/lib/types";
|
||||
import { getDisplayValueFromType } from "../ScriptInfoBlocks";
|
||||
|
||||
const getInstallCommand = (scriptPath = "", isAlpine = false) => {
|
||||
const url = `https://raw.githubusercontent.com/community-scripts/${basePath}/main/${scriptPath}`;
|
||||
return isAlpine ? `bash -c "$(curl -fsSL ${url})"` : `bash -c "$(curl -fsSL ${url})"`;
|
||||
};
|
||||
|
||||
export default function InstallCommand({ item }: { item: Script }) {
|
||||
const alpineScript = item.install_methods.find((method) => method.type === "alpine");
|
||||
|
||||
const defaultScript = item.install_methods.find((method) => method.type === "default");
|
||||
|
||||
const renderInstructions = (isAlpine = false) => (
|
||||
<>
|
||||
<p className="text-sm mt-2">
|
||||
{isAlpine ? (
|
||||
<>
|
||||
As an alternative option, you can use Alpine Linux and the {item.name} package to create a {item.name}{" "}
|
||||
{getDisplayValueFromType(item.type)} container with faster creation time and minimal system resource usage.
|
||||
You are also obliged to adhere to updates provided by the package maintainer.
|
||||
</>
|
||||
) : item.type === "pve" ? (
|
||||
<>
|
||||
To use the {item.name} script, run the command below **only** in the Proxmox VE Shell. This script is
|
||||
intended for managing or enhancing the host system directly.
|
||||
</>
|
||||
) : item.type === "addon" ? (
|
||||
<>
|
||||
This script enhances an existing setup. You can use it inside a running LXC container or directly on the
|
||||
Proxmox VE host to extend functionality with {item.name}.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
To create a new Proxmox VE {item.name} {getDisplayValueFromType(item.type)}, run the command below in the
|
||||
Proxmox VE Shell.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
{isAlpine && (
|
||||
<p className="mt-2 text-sm">
|
||||
To create a new Proxmox VE Alpine-{item.name} {getDisplayValueFromType(item.type)}, run the command below in
|
||||
the Proxmox VE Shell.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
{alpineScript ? (
|
||||
<Tabs defaultValue="default" className="mt-2 w-full max-w-4xl">
|
||||
<TabsList>
|
||||
<TabsTrigger value="default">Default</TabsTrigger>
|
||||
<TabsTrigger value="alpine">Alpine Linux</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="default">
|
||||
{renderInstructions()}
|
||||
<CodeCopyButton>{getInstallCommand(defaultScript?.script)}</CodeCopyButton>
|
||||
</TabsContent>
|
||||
<TabsContent value="alpine">
|
||||
{renderInstructions(true)}
|
||||
<CodeCopyButton>{getInstallCommand(alpineScript.script, true)}</CodeCopyButton>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
) : defaultScript?.script ? (
|
||||
<>
|
||||
{renderInstructions()}
|
||||
<CodeCopyButton>{getInstallCommand(defaultScript.script)}</CodeCopyButton>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import handleCopy from "@/components/handleCopy";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { Script } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ClipboardIcon } from "lucide-react";
|
||||
|
||||
export default function InterFaces({ item }: { item: Script }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
{item.interface_port !== null ? (
|
||||
<div className="flex items-center justify-end">
|
||||
<h2 className="mr-2 text-end text-lg font-semibold">Default Interface:</h2>
|
||||
<span className={cn(buttonVariants({ size: "sm", variant: "outline" }), "flex items-center gap-2")}>
|
||||
{item.interface_port}
|
||||
<ClipboardIcon onClick={() => handleCopy("default interface", String(item.interface_port))} className="size-4 cursor-pointer" />
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Script } from "@/lib/types";
|
||||
import { CircleHelp } from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
interface TooltipProps {
|
||||
variant: "warning" | "success";
|
||||
label: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const TooltipBadge: React.FC<TooltipProps> = ({ variant, label, content }) => (
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={100}>
|
||||
<TooltipTrigger className="flex items-center">
|
||||
<Badge variant={variant} className="flex items-center gap-1">
|
||||
{label} <CircleHelp className="size-3" />
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="text-sm max-w-64">
|
||||
{content}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
export default function Tooltips({ item }: { item: Script }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{item.privileged && (
|
||||
<TooltipBadge
|
||||
variant="warning"
|
||||
label="Privileged"
|
||||
content="This script will be run in a privileged LXC"
|
||||
/>
|
||||
)}
|
||||
{item.updateable && (
|
||||
<TooltipBadge
|
||||
variant="success"
|
||||
label="Updateable"
|
||||
content={`To Update ${item.name}, run the command below (or type update) in the LXC Console.`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { Category, Script } from "@/lib/types";
|
||||
import ScriptAccordion from "./ScriptAccordion";
|
||||
|
||||
const Sidebar = ({
|
||||
items,
|
||||
selectedScript,
|
||||
setSelectedScript,
|
||||
}: {
|
||||
items: Category[];
|
||||
selectedScript: string | null;
|
||||
setSelectedScript: (script: string | null) => void;
|
||||
}) => {
|
||||
const filteredItems = items.filter(category => category.scripts && category.scripts.length > 0);
|
||||
|
||||
const uniqueScripts = filteredItems.reduce((acc, category) => {
|
||||
for (const script of category.scripts) {
|
||||
if (!acc.some((s) => s.name === script.name)) {
|
||||
acc.push(script);
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, [] as Script[]);
|
||||
|
||||
return (
|
||||
<div className="flex min-w-72 flex-col sm:max-w-72">
|
||||
<div className="flex items-end justify-between pb-4">
|
||||
<h1 className="text-xl font-bold">Categories</h1>
|
||||
<p className="text-xs italic text-muted-foreground">
|
||||
{uniqueScripts.length} Total scripts
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg">
|
||||
<ScriptAccordion
|
||||
items={filteredItems}
|
||||
selectedScript={selectedScript}
|
||||
setSelectedScript={setSelectedScript}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
@@ -1,13 +0,0 @@
|
||||
import { AppVersion } from "@/lib/types";
|
||||
|
||||
interface VersionBadgeProps {
|
||||
version: AppVersion;
|
||||
}
|
||||
|
||||
export function VersionBadge({ version }: VersionBadgeProps) {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<span className="font-medium text-sm">{version.version}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user